diff --git a/provider/image.go b/provider/image.go index a8ac8ff4..b95ad34b 100644 --- a/provider/image.go +++ b/provider/image.go @@ -59,8 +59,8 @@ func (p *dockerNativeProvider) dockerBuild(ctx context.Context, Registry: reg, } - build := marshalBuild(inputs["build"]) - cache := getCachedImages(img, inputs["build"]) + build := marshalBuildAndApplyDefaults(inputs["build"]) + cache := marshalCachedImages(img, inputs["build"]) build.CachedImages = cache img.Build = build @@ -199,11 +199,10 @@ func (p *dockerNativeProvider) dockerBuild(ctx context.Context, return img.Name, pbstruct, err } -func marshalBuild(b resource.PropertyValue) Build { +func marshalBuildAndApplyDefaults(b resource.PropertyValue) Build { // build can be nil, a string or an object; we will also use reasonable defaults here. var build Build - if b.IsNull() { // use the default build context build.Dockerfile = defaultDockerfile @@ -234,24 +233,11 @@ func marshalBuild(b resource.PropertyValue) Build { build.Context = buildObject["context"].StringValue() } // Envs - envs := make(map[string]string) - if !buildObject["env"].IsNull() { - for k, v := range buildObject["env"].ObjectValue() { - key := fmt.Sprintf("%v", k) - envs[key] = v.StringValue() - } - } - build.Env = envs + + build.Env = marshalEnvs(buildObject["env"]) + // Args - args := make(map[string]*string) - if !buildObject["args"].IsNull() { - for k, v := range buildObject["args"].ObjectValue() { - key := fmt.Sprintf("%v", k) - vStr := v.StringValue() - args[key] = &vStr - } - } - build.Args = args + build.Args = marshalArgs(buildObject["args"]) // ExtraOptions if !buildObject["extraOptions"].IsNull() { @@ -268,8 +254,7 @@ func marshalBuild(b resource.PropertyValue) Build { return build } -func getCachedImages(img Image, b resource.PropertyValue) []string { - +func marshalCachedImages(img Image, b resource.PropertyValue) []string { var cacheImages []string if b.IsNull() { return cacheImages @@ -301,10 +286,45 @@ func getCachedImages(img Image, b resource.PropertyValue) []string { func setRegistry(r resource.PropertyValue) Registry { var reg Registry if !r.IsNull() { - reg.Server = r.ObjectValue()["server"].StringValue() - reg.Username = r.ObjectValue()["username"].StringValue() - reg.Password = r.ObjectValue()["password"].StringValue() + if !r.ObjectValue()["server"].IsNull() { + reg.Server = r.ObjectValue()["server"].StringValue() + } + if !r.ObjectValue()["username"].IsNull() { + reg.Username = r.ObjectValue()["username"].StringValue() + } + if !r.ObjectValue()["password"].IsNull() { + reg.Password = r.ObjectValue()["password"].StringValue() + } return reg } return reg } + +func marshalArgs(a resource.PropertyValue) map[string]*string { + args := make(map[string]*string) + if !a.IsNull() { + for k, v := range a.ObjectValue() { + key := fmt.Sprintf("%v", k) + vStr := v.StringValue() + args[key] = &vStr + } + } + if len(args) == 0 { + return nil + } + return args +} + +func marshalEnvs(e resource.PropertyValue) map[string]string { + envs := make(map[string]string) + if !e.IsNull() { + for k, v := range e.ObjectValue() { + key := fmt.Sprintf("%v", k) + envs[key] = v.StringValue() + } + } + if len(envs) == 0 { + return nil + } + return envs +} diff --git a/provider/image_test.go b/provider/image_test.go new file mode 100644 index 00000000..bba61269 --- /dev/null +++ b/provider/image_test.go @@ -0,0 +1,297 @@ +package provider + +import ( + "github.com/pulumi/pulumi/sdk/v3/go/common/resource" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSetRegistry(t *testing.T) { + + t.Run("Valid Registry", func(t *testing.T) { + expected := Registry{ + Server: "https://index.docker.io/v1/", + Username: "pulumipus", + Password: "supersecret", + } + input := resource.NewObjectProperty(resource.PropertyMap{ + "server": resource.NewStringProperty("https://index.docker.io/v1/"), + "username": resource.NewStringProperty("pulumipus"), + "password": resource.NewStringProperty("supersecret"), + }) + + actual := setRegistry(input) + assert.Equal(t, expected, actual) + }) + t.Run("Incomplete Registry sets all available fields", func(t *testing.T) { + expected := Registry{ + Server: "https://index.docker.io/v1/", + Username: "pulumipus", + } + input := resource.NewObjectProperty(resource.PropertyMap{ + "server": resource.NewStringProperty("https://index.docker.io/v1/"), + "username": resource.NewStringProperty("pulumipus"), + }) + + actual := setRegistry(input) + assert.Equal(t, expected, actual) + }) + + t.Run("Registry can be nil", func(t *testing.T) { + expected := Registry{} + input := resource.PropertyValue{} + actual := setRegistry(input) + assert.Equal(t, expected, actual) + }) +} + +func TestMarshalBuildAndApplyDefaults(t *testing.T) { + + t.Run("Default Build on empty input", func(t *testing.T) { + expected := Build{ + Context: ".", + Dockerfile: "Dockerfile", + } + input := resource.NewObjectProperty(resource.PropertyMap{}) + actual := marshalBuildAndApplyDefaults(input) + assert.Equal(t, expected, actual) + }) + + t.Run("String input Build", func(t *testing.T) { + expected := Build{ + Context: "/twilight/sparkle/bin", + Dockerfile: "Dockerfile", + } + input := resource.NewStringProperty("/twilight/sparkle/bin") + actual := marshalBuildAndApplyDefaults(input) + assert.Equal(t, expected, actual) + }) + + t.Run("Custom Dockerfile with default context", func(t *testing.T) { + expected := Build{ + Context: ".", + Dockerfile: "TheLastUnicorn", + } + input := resource.NewObjectProperty(resource.PropertyMap{ + "dockerfile": resource.NewStringProperty("TheLastUnicorn"), + }) + actual := marshalBuildAndApplyDefaults(input) + assert.Equal(t, expected, actual) + }) + + t.Run("Custom Dockerfile with custom context", func(t *testing.T) { + expected := Build{ + Context: "/twilight/sparkle/bin", + Dockerfile: "TheLastUnicorn", + } + input := resource.NewObjectProperty(resource.PropertyMap{ + "dockerfile": resource.NewStringProperty("TheLastUnicorn"), + "context": resource.NewStringProperty("/twilight/sparkle/bin"), + }) + + actual := marshalBuildAndApplyDefaults(input) + assert.Equal(t, expected, actual) + }) + + t.Run("Setting Args", func(t *testing.T) { + argval := "Alicorn" + expected := Build{ + Context: ".", + Dockerfile: "Dockerfile", + Args: map[string]*string{ + "Swiftwind": &argval, + }, + } + + input := resource.NewObjectProperty(resource.PropertyMap{ + "args": resource.NewObjectProperty(resource.PropertyMap{ + "Swiftwind": resource.NewStringProperty("Alicorn"), + }), + }) + + actual := marshalBuildAndApplyDefaults(input) + assert.Equal(t, expected, actual) + }) + + t.Run("Setting Env", func(t *testing.T) { + + expected := Build{ + Context: ".", + Dockerfile: "Dockerfile", + Env: map[string]string{ + "Strawberry": "fruit", + }, + } + + input := resource.NewObjectProperty(resource.PropertyMap{ + "env": resource.NewObjectProperty(resource.PropertyMap{ + "Strawberry": resource.NewStringProperty("fruit"), + }), + }) + + actual := marshalBuildAndApplyDefaults(input) + assert.Equal(t, expected, actual) + }) + + t.Run("Sets Extra Options", func(t *testing.T) { + expected := Build{ + Context: ".", + Dockerfile: "Dockerfile", + ExtraOptions: []string{"cat", "dog", "pot-bellied pig"}, + } + + input := resource.NewObjectProperty(resource.PropertyMap{ + "extraOptions": resource.NewArrayProperty([]resource.PropertyValue{ + resource.NewStringProperty("cat"), + resource.NewStringProperty("dog"), + resource.NewStringProperty("pot-bellied pig"), + }), + }) + + actual := marshalBuildAndApplyDefaults(input) + assert.Equal(t, expected, actual) + }) + + t.Run("Does Not Set Extra Options on Empty Input", func(t *testing.T) { + expected := Build{ + Context: ".", + Dockerfile: "Dockerfile", + } + + input := resource.NewObjectProperty(resource.PropertyMap{ + "extraOptions": resource.NewArrayProperty([]resource.PropertyValue{}), + }) + + actual := marshalBuildAndApplyDefaults(input) + assert.Equal(t, expected, actual) + }) + t.Run("Sets Target", func(t *testing.T) { + expected := Build{ + Context: ".", + Dockerfile: "Dockerfile", + Target: "bullseye", + } + + input := resource.NewObjectProperty(resource.PropertyMap{ + "target": resource.NewStringProperty("bullseye"), + }) + + actual := marshalBuildAndApplyDefaults(input) + assert.Equal(t, expected, actual) + }) +} + +func TestMarshalArgs(t *testing.T) { + t.Run("Set any args", func(t *testing.T) { + a := "Alicorn" + p := "Pegasus" + tl := "Unicorn" + expected := map[string]*string{ + "Swiftwind": &a, + "Fledge": &p, + "The Last": &tl, + } + input := resource.NewObjectProperty(resource.PropertyMap{ + "Swiftwind": resource.NewStringProperty("Alicorn"), + "Fledge": resource.NewStringProperty("Pegasus"), + "The Last": resource.NewStringProperty("Unicorn"), + }) + actual := marshalArgs(input) + assert.Equal(t, expected, actual) + }) + + t.Run("Returns nil when no args set", func(t *testing.T) { + expected := map[string]*string(nil) + input := resource.NewObjectProperty(resource.PropertyMap{}) + actual := marshalArgs(input) + assert.Equal(t, expected, actual) + }) +} + +func TestMarshalEnvs(t *testing.T) { + t.Run("Set any environment variables", func(t *testing.T) { + expected := map[string]string{ + "Strawberry": "fruit", + "Carrot": "veggie", + "Docker": "a bit of a mess tbh", + } + input := resource.NewObjectProperty(resource.PropertyMap{ + "Strawberry": resource.NewStringProperty("fruit"), + "Carrot": resource.NewStringProperty("veggie"), + "Docker": resource.NewStringProperty("a bit of a mess tbh"), + }) + actual := marshalEnvs(input) + assert.Equal(t, expected, actual) + }) + t.Run("Returns nil when no environment variables set", func(t *testing.T) { + expected := map[string]string(nil) + input := resource.NewObjectProperty(resource.PropertyMap{}) + actual := marshalEnvs(input) + assert.Equal(t, expected, actual) + }) +} + +func TestMarshalCachedImages(t *testing.T) { + t.Run("Test Cached Images", func(t *testing.T) { + expected := []string{"apple", "banana", "cherry"} + imgInput := Image{ + Name: "unicornsareawesome", + SkipPush: false, + Registry: Registry{ + Server: "https://index.docker.io/v1/", + Username: "pulumipus", + Password: "supersecret", + }, + } + buildInput := resource.NewObjectProperty(resource.PropertyMap{ + "dockerfile": resource.NewStringProperty("TheLastUnicorn"), + "context": resource.NewStringProperty("/twilight/sparkle/bin"), + + "cacheFrom": resource.NewObjectProperty(resource.PropertyMap{ + "stages": resource.NewArrayProperty([]resource.PropertyValue{ + resource.NewStringProperty("apple"), + resource.NewStringProperty("banana"), + resource.NewStringProperty("cherry"), + }), + }), + }) + + actual := marshalCachedImages(imgInput, buildInput) + assert.Equal(t, expected, actual) + + }) + t.Run("Test Cached Images No Build Input Returns Nil", func(t *testing.T) { + expected := []string(nil) + imgInput := Image{ + Name: "unicornsareawesome", + SkipPush: false, + Registry: Registry{ + Server: "https://index.docker.io/v1/", + Username: "pulumipus", + Password: "supersecret", + }, + } + buildInput := resource.NewObjectProperty(resource.PropertyMap{}) + actual := marshalCachedImages(imgInput, buildInput) + assert.Equal(t, expected, actual) + }) + + t.Run("Test Cached Images No cacheFrom Input Returns Nil", func(t *testing.T) { + expected := []string(nil) + imgInput := Image{ + Name: "unicornsareawesome", + SkipPush: false, + Registry: Registry{ + Server: "https://index.docker.io/v1/", + Username: "pulumipus", + Password: "supersecret", + }, + } + buildInput := resource.NewObjectProperty(resource.PropertyMap{ + "dockerfile": resource.NewStringProperty("TheLastUnicorn"), + "context": resource.NewStringProperty("/twilight/sparkle/bin"), + }) + actual := marshalCachedImages(imgInput, buildInput) + assert.Equal(t, expected, actual) + }) +}