From 79b8af9288665354bd07765a7912bb7384ea00e6 Mon Sep 17 00:00:00 2001 From: David McWhorter <105815369+dmcwhorter-ddl@users.noreply.github.com> Date: Mon, 8 Apr 2024 17:35:13 -0400 Subject: [PATCH] add dockerfileContents to `ImageBuild` CRD (#140) * add dockerfileContents to CR * use switch * use existing temporary dir * validations and trim * add Digest to ImageBuild status * CRD update * add check for nil annotations map * disable funlen check * revert test changes * better error message and change temp file perms to 0644 --- ...haestus.dominodatalab.com_imagebuilds.yaml | 10 ++++++- pkg/api/hephaestus/v1/imagebuild_types.go | 6 ++++- pkg/api/hephaestus/v1/imagebuild_webhook.go | 17 +++++++----- pkg/buildkit/buildkit.go | 27 ++++++++++++++----- .../imagebuild/component/builddispatcher.go | 7 +++++ .../component/amqpmessenger.go | 5 +++- 6 files changed, 57 insertions(+), 15 deletions(-) diff --git a/deployments/crds/hephaestus.dominodatalab.com_imagebuilds.yaml b/deployments/crds/hephaestus.dominodatalab.com_imagebuilds.yaml index e568d971..375839e7 100644 --- a/deployments/crds/hephaestus.dominodatalab.com_imagebuilds.yaml +++ b/deployments/crds/hephaestus.dominodatalab.com_imagebuilds.yaml @@ -67,7 +67,8 @@ spec: type: string type: array context: - description: Context is a remote URL used to fetch the build context. + description: Context is a remote URL used to fetch the build context. Overrides + dockerfileContents if present. type: string disableBuildCache: description: DisableLocalBuildCache will disable the use of the local @@ -77,6 +78,10 @@ spec: description: DisableCacheLayerExport will remove the "inline" cache metadata from the image configuration. type: boolean + dockerfileContents: + description: DockerfileContents specifies the contents of the Dockerfile + directly in the CR. Ignored if context is present. + type: string images: description: Images is a list of images to build and push. items: @@ -226,6 +231,9 @@ spec: - type type: object type: array + digest: + description: Digest is the image digest + type: string labels: additionalProperties: type: string diff --git a/pkg/api/hephaestus/v1/imagebuild_types.go b/pkg/api/hephaestus/v1/imagebuild_types.go index 78f87f8a..e5bc2bd2 100644 --- a/pkg/api/hephaestus/v1/imagebuild_types.go +++ b/pkg/api/hephaestus/v1/imagebuild_types.go @@ -14,8 +14,10 @@ type ImageBuildAMQPOverrides struct { // ImageBuildSpec specifies the desired state of an ImageBuild resource. type ImageBuildSpec struct { - // Context is a remote URL used to fetch the build context. + // Context is a remote URL used to fetch the build context. Overrides dockerfileContents if present. Context string `json:"context,omitempty"` + // DockerfileContents specifies the contents of the Dockerfile directly in the CR. Ignored if context is present. + DockerfileContents string `json:"dockerfileContents,omitempty"` // Images is a list of images to build and push. Images []string `json:"images,omitempty"` // BuildArgs are applied to the build at runtime. @@ -51,6 +53,8 @@ type ImageBuildStatus struct { BuilderAddr string `json:"builderAddr,omitempty"` // CompressedImageSizeBytes is the total size of all the compressed layers in the image. CompressedImageSizeBytes string `json:"compressedImageSizeBytes,omitempty"` + // Digest is the image digest + Digest string `json:"digest,omitempty"` // Map of string keys and values corresponding OCI image config labels. // Labels contains arbitrary metadata for the container. Labels map[string]string `json:"labels,omitempty"` diff --git a/pkg/api/hephaestus/v1/imagebuild_webhook.go b/pkg/api/hephaestus/v1/imagebuild_webhook.go index f0aa83f7..27c85907 100644 --- a/pkg/api/hephaestus/v1/imagebuild_webhook.go +++ b/pkg/api/hephaestus/v1/imagebuild_webhook.go @@ -42,12 +42,17 @@ func (in *ImageBuild) validateImageBuild(action string) (admission.Warnings, err var errList field.ErrorList fp := field.NewPath("spec") - if strings.TrimSpace(in.Spec.Context) == "" { - log.V(1).Info("Context is blank") - errList = append(errList, field.Required(fp.Child("context"), "must not be blank")) - } else if _, err := url.ParseRequestURI(in.Spec.Context); err != nil { - log.V(1).Info("Context is not a valid URL") - errList = append(errList, field.Invalid(fp.Child("context"), in.Spec.Context, err.Error())) + if strings.TrimSpace(in.Spec.Context) == "" && strings.TrimSpace(in.Spec.DockerfileContents) == "" { + log.V(1).Info("Context and DockerfileContents are both blank") + errList = append(errList, field.Required(fp.Child("context"), "must not be blank if "+ + fp.Child("dockerfileContents").String()+" is blank")) + } + + if strings.TrimSpace(in.Spec.Context) != "" { + if _, err := url.ParseRequestURI(in.Spec.Context); err != nil { + log.V(1).Info("Context is not a valid URL") + errList = append(errList, field.Invalid(fp.Child("context"), in.Spec.Context, err.Error())) + } } if errs := validateImages(log, fp.Child("images"), in.Spec.Images); errs != nil { diff --git a/pkg/buildkit/buildkit.go b/pkg/buildkit/buildkit.go index 4b83216a..96dfd484 100644 --- a/pkg/buildkit/buildkit.go +++ b/pkg/buildkit/buildkit.go @@ -7,7 +7,9 @@ import ( "io" "net/url" "os" + "path" "path/filepath" + "strings" "time" "github.com/containerd/console" @@ -103,6 +105,7 @@ func (b *ClientBuilder) Build(ctx context.Context) (*Client, error) { type BuildOptions struct { Context string ContextDir string + DockerfileContents string Images []string BuildArgs []string NoCache bool @@ -165,18 +168,30 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) (string, error) { // process build context var contentsDir string - - if fi, err := os.Stat(opts.ContextDir); err == nil && fi.IsDir() { + fi, err := os.Stat(opts.ContextDir) + switch { + case err == nil && fi.IsDir(): c.log.Info("Using context dir", "dir", opts.ContextDir) contentsDir = opts.ContextDir - } else { + case strings.TrimSpace(opts.Context) != "": c.log.Info("Fetching remote context", "url", opts.Context) - extract, err := archive.FetchAndExtract(ctx, c.log, opts.Context, buildDir, opts.FetchAndExtractTimeout) - if err != nil { + extract, extractErr := archive.FetchAndExtract(ctx, c.log, opts.Context, buildDir, opts.FetchAndExtractTimeout) + if extractErr != nil { return "", fmt.Errorf("cannot fetch remote context: %w", err) } - contentsDir = extract.ContentsDir + case strings.TrimSpace(opts.DockerfileContents) != "": + c.log.Info("Creating context from DockerfileContents") + contentsDir, err = os.MkdirTemp(buildDir, "dockerfile-contents-") + if err != nil { + return "", fmt.Errorf("cannot create temp directory for dockerfileContents: %w", err) + } + err = os.WriteFile(path.Join(contentsDir, "Dockerfile"), []byte(opts.DockerfileContents), os.FileMode(0644)) + if err != nil { + return "", fmt.Errorf("cannot write temporary file for dockerfileContents: %w", err) + } + default: + return "", errors.New("no valid docker context provided") } c.log.V(1).Info("Context extracted", "dir", contentsDir) diff --git a/pkg/controller/imagebuild/component/builddispatcher.go b/pkg/controller/imagebuild/component/builddispatcher.go index 840caacb..15779408 100644 --- a/pkg/controller/imagebuild/component/builddispatcher.go +++ b/pkg/controller/imagebuild/component/builddispatcher.go @@ -218,6 +218,7 @@ func (c *BuildDispatcherComponent) Reconcile(coreCtx *core.Context) (ctrl.Result buildOpts := buildkit.BuildOptions{ Context: obj.Spec.Context, + DockerfileContents: obj.Spec.DockerfileContents, Images: obj.Spec.Images, BuildArgs: obj.Spec.BuildArgs, NoCache: obj.Spec.DisableLocalBuildCache, @@ -278,9 +279,15 @@ func populateBuildStatus(obj *hephv1.ImageBuild, log logr.Logger, img v1.Image, if err != nil { log.Error(err, "Cannot calculate image labels", "imageName", imageName) } + digest, err := img.Digest() + if err != nil { + log.Error(err, "Cannot retrieve image digest", "imageName", imageName) + } log.Info(fmt.Sprintf("Final image size: %d", imageSize)) + obj.Status.CompressedImageSizeBytes = strconv.FormatInt(imageSize, 10) + obj.Status.Digest = digest.String() obj.Status.Labels = make(map[string]string) for key, value := range labels { if len(value) > 0 { diff --git a/pkg/controller/imagebuildmessage/component/amqpmessenger.go b/pkg/controller/imagebuildmessage/component/amqpmessenger.go index 61ae53e9..712e88f5 100644 --- a/pkg/controller/imagebuildmessage/component/amqpmessenger.go +++ b/pkg/controller/imagebuildmessage/component/amqpmessenger.go @@ -60,7 +60,7 @@ func (c *AMQPMessengerComponent) Initialize(_ *core.Context, bldr *ctrl.Builder) return nil } -//nolint:maintidx +//nolint:maintidx,funlen func (c *AMQPMessengerComponent) Reconcile(ctx *core.Context) (ctrl.Result, error) { log := ctx.Log obj := ctx.Object @@ -205,6 +205,9 @@ func (c *AMQPMessengerComponent) Reconcile(ctx *core.Context) (ctrl.Result, erro images = append(images, reference.TagNameOnly(named).String()) } message.ImageURLs = images + if message.Annotations == nil { + message.Annotations = map[string]string{} + } message.Annotations[compressedImageSizeBytesAnnotation] = ib.Status.CompressedImageSizeBytes for key, value := range ib.Status.Labels { message.Annotations[key] = value