From 1095c81e2a5bb8fb84cd25a472116911790e8cae Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Mon, 27 Jun 2022 10:22:40 -0400 Subject: [PATCH] Support base_image to specify base image to use. This enables chained ko builds, where a previous ko_image resource can be fed into a subsequent ko_image's base_image. If you're into that kind of thing. --- docs/resources/image.md | 1 + internal/provider/resource_ko_image.go | 86 ++++++++++++++------- internal/provider/resource_ko_image_test.go | 38 +++++++-- 3 files changed, 89 insertions(+), 36 deletions(-) diff --git a/docs/resources/image.md b/docs/resources/image.md index 1db6192d..d366e8c0 100644 --- a/docs/resources/image.md +++ b/docs/resources/image.md @@ -27,6 +27,7 @@ resource "ko_image" "example" { ### Optional +- `base_image` (String) base image to use - `platforms` (String) platforms to build ### Read-Only diff --git a/internal/provider/resource_ko_image.go b/internal/provider/resource_ko_image.go index c8b6e4e0..3ea6745c 100644 --- a/internal/provider/resource_ko_image.go +++ b/internal/provider/resource_ko_image.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "sync" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" @@ -16,8 +17,7 @@ import ( ) const ( - baseImage = "gcr.io/distroless/static:nonroot" - targetRepo = "gcr.io/jason-chainguard" + defaultBaseImage = "gcr.io/distroless/static:nonroot" ) func resourceImage() *schema.Resource { @@ -47,6 +47,13 @@ func resourceImage() *schema.Resource { Type: schema.TypeString, // TODO: type list of strings? ForceNew: true, // Any time this changes, don't try to update in-place, just create it. }, + "base_image": { + Description: "base image to use", + Default: defaultBaseImage, + Optional: true, + Type: schema.TypeString, + ForceNew: true, // Any time this changes, don't try to update in-place, just create it. + }, "image_ref": { Description: "built image reference by digest", Type: schema.TypeString, @@ -56,44 +63,76 @@ func resourceImage() *schema.Resource { } } -func doBuild(ctx context.Context, ip, platforms, repo string) (string, error) { +type buildOptions struct { + ip string + dockerRepo string + platforms string + baseImage string +} + +var baseImages sync.Map // Cache of base image lookups. + +func doBuild(ctx context.Context, opts buildOptions) (string, error) { b, err := build.NewGo(ctx, ".", - build.WithPlatforms(platforms), + build.WithPlatforms(opts.platforms), build.WithBaseImages(func(ctx context.Context, _ string) (name.Reference, build.Result, error) { - ref := name.MustParseReference(baseImage) - base, err := remote.Index(ref, remote.WithContext(ctx)) - return ref, base, err + ref, err := name.ParseReference(opts.baseImage) + if err != nil { + return nil, nil, err + } + + if cached, found := baseImages.Load(opts.baseImage); found { + return ref, cached.(build.Result), nil + } + + desc, err := remote.Get(ref, + remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + return nil, nil, err + } + if desc.MediaType.IsImage() { + img, err := desc.Image() + baseImages.Store(opts.baseImage, img) + return ref, img, err + } + if desc.MediaType.IsIndex() { + idx, err := desc.ImageIndex() + baseImages.Store(opts.baseImage, idx) + return ref, idx, err + } + return nil, nil, fmt.Errorf("Unexpected base image media type: %s", desc.MediaType) })) if err != nil { return "", fmt.Errorf("NewGo: %v", err) } - r, err := b.Build(ctx, ip) + r, err := b.Build(ctx, opts.ip) if err != nil { return "", fmt.Errorf("Build: %v", err) } - p, err := publish.NewDefault(repo, + p, err := publish.NewDefault(opts.dockerRepo, publish.WithAuthFromKeychain(authn.DefaultKeychain)) if err != nil { return "", fmt.Errorf("NewDefault: %v", err) } - ref, err := p.Publish(ctx, r, ip) + ref, err := p.Publish(ctx, r, opts.ip) if err != nil { return "", fmt.Errorf("Publish: %v", err) } return ref.String(), nil } -func resourceKoBuildCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - repo, ok := meta.(string) - if !ok { - return diag.Errorf("meta to be a string") +func fromData(d *schema.ResourceData, repo string) buildOptions { + return buildOptions{ + ip: d.Get("importpath").(string), + dockerRepo: repo, + platforms: d.Get("platforms").(string), + baseImage: d.Get("base_image").(string), } +} - ref, err := doBuild(ctx, - d.Get("importpath").(string), - d.Get("platforms").(string), - repo) +func resourceKoBuildCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + ref, err := doBuild(ctx, fromData(d, meta.(string))) if err != nil { return diag.Errorf("doBuild: %v", err) } @@ -104,16 +143,7 @@ func resourceKoBuildCreate(ctx context.Context, d *schema.ResourceData, meta int } func resourceKoBuildRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - // Build the image again, and only unset ID if it changed. - repo, ok := meta.(string) - if !ok { - return diag.Errorf("meta to be a string") - } - - ref, err := doBuild(ctx, - d.Get("importpath").(string), - d.Get("platforms").(string), - repo) + ref, err := doBuild(ctx, fromData(d, meta.(string))) if err != nil { return diag.Errorf("doBuild: %v", err) } diff --git a/internal/provider/resource_ko_image_test.go b/internal/provider/resource_ko_image_test.go index e091ef12..ea34ba4e 100644 --- a/internal/provider/resource_ko_image_test.go +++ b/internal/provider/resource_ko_image_test.go @@ -19,23 +19,45 @@ func TestAccResourceKoImage(t *testing.T) { url := fmt.Sprintf("localhost:%s", parts[len(parts)-1]) t.Setenv("KO_DOCKER_REPO", url) + imageRefRE := regexp.MustCompile("^" + url + "/github.com/imjasonh/terraform-provider-ko/cmd/test@sha256:") + resource.Test(t, resource.TestCase{ ProviderFactories: providerFactories, Steps: []resource.TestStep{{ - Config: testAccResourceKoImage, + Config: ` + resource "ko_image" "foo" { + importpath = "github.com/imjasonh/terraform-provider-ko/cmd/test" + } + `, Check: resource.ComposeTestCheckFunc( - resource.TestMatchResourceAttr( - "ko_image.foo", "image_ref", regexp.MustCompile("^"+url+"/github.com/imjasonh/terraform-provider-ko/cmd/test@sha256:")), + resource.TestMatchResourceAttr("ko_image.foo", "image_ref", imageRefRE), ), }}, // TODO: add a test that there's no terraform diff if the image hasn't changed. // TODO: add a test that there's a terraform diff if the image has changed. // TODO: add a test covering what happens if the build fails for any reason. }) -} -const testAccResourceKoImage = ` -resource "ko_image" "foo" { - importpath = "github.com/imjasonh/terraform-provider-ko/cmd/test" + // This tests building an image and using it as a base image for another image. + // Mostly just to prove we can. + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + Steps: []resource.TestStep{{ + Config: ` + resource "ko_image" "base" { + importpath = "github.com/imjasonh/terraform-provider-ko/cmd/test" + } + resource "ko_image" "top" { + importpath = "github.com/imjasonh/terraform-provider-ko/cmd/test" + base_image = "${ko_image.base.image_ref}" + } + `, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("ko_image.top", "image_ref", imageRefRE), + resource.TestMatchResourceAttr("ko_image.top", "base_image", imageRefRE), + resource.TestMatchResourceAttr("ko_image.base", "image_ref", imageRefRE), + // TODO(jason): Check that top's base_image attr matches base's image_ref exactly. + ), + }}, + }) } -`