From 20bdea44c1e4618ea014dbb829d919a7d8032953 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 2 Jun 2022 00:38:15 -0400 Subject: [PATCH] `force_architecture`; config validation + tests --- builtin/aws/ecr/pull/builder.go | 110 ++++++++++++++++++++++----- builtin/aws/ecr/pull/builder_test.go | 26 +++++++ 2 files changed, 118 insertions(+), 18 deletions(-) create mode 100644 builtin/aws/ecr/pull/builder_test.go diff --git a/builtin/aws/ecr/pull/builder.go b/builtin/aws/ecr/pull/builder.go index c7a6a9caea9..d5d316c4e51 100644 --- a/builtin/aws/ecr/pull/builder.go +++ b/builtin/aws/ecr/pull/builder.go @@ -1,7 +1,11 @@ package ecrpull import ( + "context" + "fmt" "os" + "reflect" + "strings" "github.com/hashicorp/go-hclog" "google.golang.org/grpc/codes" @@ -9,15 +13,18 @@ import ( "github.com/aws/aws-sdk-go/aws" awsecr "github.com/aws/aws-sdk-go/service/ecr" + "github.com/aws/aws-sdk-go/service/lambda" "github.com/hashicorp/waypoint-plugin-sdk/docs" "github.com/hashicorp/waypoint-plugin-sdk/terminal" "github.com/hashicorp/waypoint/builtin/aws/ecr" "github.com/hashicorp/waypoint/builtin/aws/utils" + + validation "github.com/go-ozzo/ozzo-validation/v4" ) // Builder uses `docker build` to build a Docker iamge. type Builder struct { - config BuilderConfig + config Config } // BuildFunc implements component.Builder @@ -26,14 +33,15 @@ func (b *Builder) BuildFunc() interface{} { } // Config is the configuration structure for the registry. -type BuilderConfig struct { - Region string `hcl:"region,attr"` - Repository string `hcl:"repository,attr"` - Tag string `hcl:"tag,attr"` +type Config struct { + Region string `hcl:"region,optional"` + Repository string `hcl:"repository,attr"` + Tag string `hcl:"tag,attr"` + ForceArchitecture string `hcl:"force_architecture,optional"` } func (b *Builder) Documentation() (*docs.Documentation, error) { - doc, err := docs.New(docs.FromConfig(&BuilderConfig{}), docs.FromFunc(b.BuildFunc())) + doc, err := docs.New(docs.FromConfig(&Config{}), docs.FromFunc(b.BuildFunc())) if err != nil { return nil, err } @@ -81,16 +89,64 @@ build { "tag", "the tag of the image to pull", ) + + doc.SetField( + "force_architecture", + "**Note**: This is a temporary field that enables overriding the `architecture` output attribute. Valid values are: `\"x86_64\"`, `\"arm64\"`", + docs.Default("`\"\"`"), + ) + return doc, nil } +// ConfigSet is called after a configuration has been decoded +func (p *Builder) ConfigSet(config interface{}) error { + c, ok := config.(*Config) + if !ok { + // this should never happen + return fmt.Errorf("Invalid configuration, expected *ecrpull.BuilderConfig, got %q", reflect.TypeOf(config)) + } + + // validate required fields + if err := utils.Error(validation.ValidateStruct(c, + validation.Field(&c.Repository, validation.Required), + validation.Field(&c.Tag, validation.Required), + )); err != nil { + return err + } + + // validate architecture + if c.ForceArchitecture != "" { + architectures := make([]interface{}, len(lambda.Architecture_Values())) + + for i, ca := range lambda.Architecture_Values() { + architectures[i] = ca + } + + var validArchitectures []string + for _, arch := range lambda.Architecture_Values() { + validArchitectures = append(validArchitectures, fmt.Sprintf("%q", arch)) + } + + if err := utils.Error(validation.ValidateStruct(c, + validation.Field(&c.ForceArchitecture, + validation.In(architectures...).Error(fmt.Sprintf("Unsupported force_architecture %q. Must be one of [%s], or left blank", c.ForceArchitecture, strings.Join(validArchitectures, ", "))), + ), + )); err != nil { + return err + } + } + + return nil +} + // Config implements Configurable func (b *Builder) Config() (interface{}, error) { return &b.config, nil } // Build -func (b *Builder) Build(ui terminal.UI, log hclog.Logger) (*ecr.Image, error) { +func (b *Builder) Build(ctx context.Context, ui terminal.UI, log hclog.Logger) (*ecr.Image, error) { // If there is no region setup. Try and load it from environment variables. if b.config.Region == "" { @@ -115,6 +171,7 @@ func (b *Builder) Build(ui terminal.UI, log hclog.Logger) (*ecr.Image, error) { } }() + // connect to AWS step.Update("Connecting to AWS") sess, err := utils.GetSession(&utils.SessionConfig{ Region: b.config.Region, @@ -128,13 +185,15 @@ func (b *Builder) Build(ui terminal.UI, log hclog.Logger) (*ecr.Image, error) { step.Done() + // find ECR image by repository and tag step = sg.Add("Verifying image exists") - svc := awsecr.New(sess) + ecrsvc := awsecr.New(sess) cfgTag := b.config.Tag cfgRepository := b.config.Repository - imgs, err := svc.DescribeImages(&awsecr.DescribeImagesInput{ + // should be acceptable to filter images by TAGGED status + imgs, err := ecrsvc.DescribeImages(&awsecr.DescribeImagesInput{ RepositoryName: aws.String(cfgRepository), Filter: &awsecr.DescribeImagesFilter{ TagStatus: aws.String("TAGGED"), @@ -155,26 +214,41 @@ func (b *Builder) Build(ui terminal.UI, log hclog.Logger) (*ecr.Image, error) { for _, img := range imgs.ImageDetails { for _, tag := range img.ImageTags { if *tag == cfgTag { + // an image with the specified tag was found + imageMatch := *img.RegistryId + ".dkr.ecr." + b.config.Region + ".amazonaws.com/" + cfgRepository - output.Image = *img.RegistryId + ".dkr.ecr." + b.config.Region + ".amazonaws.com/" + cfgRepository + output.Image = imageMatch output.Tag = *tag - // TODO(kevinwang): Do we need to get architecture? - // If we do, we can pull the image and inspect it via `cli.ImageInspectWithRaw`, - // - prior art: /builtin/docker/builder.go -> Build - // There is also an open issue for the ECR team to build a architecture feature into - // the UI, which probably comes with a CLI/API change. - // - see https://github.com/aws/containers-roadmap/issues/1591 + + st := ui.Status() + defer st.Close() + + if b.config.ForceArchitecture != "" { + output.Architecture = b.config.ForceArchitecture + st.Step(terminal.StatusOK, "Forcing output architecture: "+b.config.ForceArchitecture) + } else { + // TODO(kevinwang): Do we need to get architecture? + // If we do, we can pull the image and inspect it via `cli.ImageInspectWithRaw`, + // - prior art: /builtin/docker/builder.go -> Build + // There is also an open issue for the ECR team to build a architecture feature into + // the UI, which probably comes with a CLI/API change. + // - see https://github.com/aws/containers-roadmap/issues/1591 + st.Step(terminal.StatusWarn, "Automatic architecture detection is not yet implemented. Architecture will default to \"\"") + } + break } } + } + // if no image was found, return an error if output.Image == "" { - log.Error("no matching image found", "tag", cfgTag, "repository", cfgRepository) + log.Error("no matching image was found", "tag", cfgTag, "repository", cfgRepository) return nil, status.Error(codes.FailedPrecondition, "No matching tags found") } - step.Update("Using existing image: %s; TAG=%s", output.Image, output.Tag) + step.Update("Using image: " + output.Image + ":" + output.Tag) step.Done() return &output, nil diff --git a/builtin/aws/ecr/pull/builder_test.go b/builtin/aws/ecr/pull/builder_test.go new file mode 100644 index 00000000000..eded51cf26f --- /dev/null +++ b/builtin/aws/ecr/pull/builder_test.go @@ -0,0 +1,26 @@ +package ecrpull + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBuilderConfig(t *testing.T) { + t.Run("requires repository and tag", func(t *testing.T) { + var b Builder + cfg := &Config{} + require.EqualError(t, b.ConfigSet(cfg), "rpc error: code = InvalidArgument desc = Repository: cannot be blank; Tag: cannot be blank.") + }) + + t.Run("disallows unsupported architecture", func(t *testing.T) { + var b Builder + cfg := &Config{ + Repository: "foo", + Tag: "latest", + ForceArchitecture: "foobar", + } + + require.EqualError(t, b.ConfigSet(cfg), "rpc error: code = InvalidArgument desc = ForceArchitecture: Unsupported force_architecture \"foobar\". Must be one of [\"x86_64\", \"arm64\"], or left blank.") + }) +}