Skip to content
This repository has been archived by the owner on Jan 8, 2024. It is now read-only.

Commit

Permalink
force_architecture; config validation + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
thiskevinwang committed Jun 2, 2022
1 parent 6adb4fd commit 20bdea4
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 18 deletions.
110 changes: 92 additions & 18 deletions builtin/aws/ecr/pull/builder.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
package ecrpull

import (
"context"
"fmt"
"os"
"reflect"
"strings"

"github.com/hashicorp/go-hclog"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"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
Expand All @@ -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
}
Expand Down Expand Up @@ -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 == "" {
Expand All @@ -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,
Expand All @@ -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"),
Expand All @@ -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
Expand Down
26 changes: 26 additions & 0 deletions builtin/aws/ecr/pull/builder_test.go
Original file line number Diff line number Diff line change
@@ -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.")
})
}

0 comments on commit 20bdea4

Please sign in to comment.