diff --git a/api/api-rules/violation_exceptions.list b/api/api-rules/violation_exceptions.list index 4b077250..dbef0df7 100644 --- a/api/api-rules/violation_exceptions.list +++ b/api/api-rules/violation_exceptions.list @@ -3,6 +3,7 @@ API rule violation: list_type_missing,github.com/dominodatalab/hephaestus/pkg/ap API rule violation: list_type_missing,github.com/dominodatalab/hephaestus/pkg/api/hephaestus/v1,ImageBuildSpec,Images API rule violation: list_type_missing,github.com/dominodatalab/hephaestus/pkg/api/hephaestus/v1,ImageBuildSpec,ImportRemoteBuildCache API rule violation: list_type_missing,github.com/dominodatalab/hephaestus/pkg/api/hephaestus/v1,ImageBuildSpec,RegistryAuth +API rule violation: list_type_missing,github.com/dominodatalab/hephaestus/pkg/api/hephaestus/v1,ImageBuildSpec,Secrets API rule violation: list_type_missing,github.com/dominodatalab/hephaestus/pkg/api/hephaestus/v1,ImageBuildStatus,Conditions API rule violation: list_type_missing,github.com/dominodatalab/hephaestus/pkg/api/hephaestus/v1,ImageBuildStatus,Transitions API rule violation: list_type_missing,github.com/dominodatalab/hephaestus/pkg/api/hephaestus/v1,ImageBuildStatusTransitionMessage,ImageURLs diff --git a/deployments/crds/hephaestus.dominodatalab.com_imagebuilds.yaml b/deployments/crds/hephaestus.dominodatalab.com_imagebuilds.yaml index cc684a67..74ef5c11 100644 --- a/deployments/crds/hephaestus.dominodatalab.com_imagebuilds.yaml +++ b/deployments/crds/hephaestus.dominodatalab.com_imagebuilds.yaml @@ -125,6 +125,17 @@ spec: type: string type: object type: array + secrets: + description: Secrets provides references to Kubernetes secrets to + expose to individual image builds. + items: + properties: + name: + type: string + namespace: + type: string + type: object + type: array type: object status: properties: diff --git a/examples/resources/imagebuild.yaml b/examples/resources/imagebuild.yaml index 0238e9e2..30f33d78 100644 --- a/examples/resources/imagebuild.yaml +++ b/examples/resources/imagebuild.yaml @@ -8,6 +8,9 @@ spec: - username/repo:tag buildArgs: - ENV=development + secrets: + - name: mySecret + namespace: default cacheMode: min cacheTag: local-test disableCacheExports: false diff --git a/pkg/api/hephaestus/v1/imagebuild_types.go b/pkg/api/hephaestus/v1/imagebuild_types.go index b96e1d70..8a92213c 100644 --- a/pkg/api/hephaestus/v1/imagebuild_types.go +++ b/pkg/api/hephaestus/v1/imagebuild_types.go @@ -32,6 +32,8 @@ type ImageBuildSpec struct { DisableLocalBuildCache bool `json:"disableBuildCache,omitempty"` // DisableCacheLayerExport will remove the "inline" cache metadata from the image configuration. DisableCacheLayerExport bool `json:"disableCacheExport,omitempty"` + // Secrets provides references to Kubernetes secrets to expose to individual image builds. + Secrets []SecretReference `json:"secrets,omitempty"` } type ImageBuildTransition struct { diff --git a/pkg/api/hephaestus/v1/types.go b/pkg/api/hephaestus/v1/types.go index 456b0592..39ff79ac 100644 --- a/pkg/api/hephaestus/v1/types.go +++ b/pkg/api/hephaestus/v1/types.go @@ -26,6 +26,12 @@ const ( PhaseFailed Phase = "Failed" ) +const ( + // TODO: figuer out the best naming for this + //nolint: gosec // this is not a sensitive value, it's a label + HephaestusSecretLabel = "hephaestus-secret" +) + type BasicAuthCredentials struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` @@ -49,6 +55,11 @@ type RegistryCredentials struct { Secret *SecretCredentials `json:"secret,omitempty"` } +type SecretReference struct { + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` +} + // ImageBuildStatusTransitionMessage contains information about ImageBuild status transitions. // // This type is used to publish JSON-formatted messages to one or more configured messaging diff --git a/pkg/api/hephaestus/v1/zz_generated.deepcopy.go b/pkg/api/hephaestus/v1/zz_generated.deepcopy.go index e4b23b03..1f281f24 100644 --- a/pkg/api/hephaestus/v1/zz_generated.deepcopy.go +++ b/pkg/api/hephaestus/v1/zz_generated.deepcopy.go @@ -258,6 +258,11 @@ func (in *ImageBuildSpec) DeepCopyInto(out *ImageBuildSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Secrets != nil { + in, out := &in.Secrets, &out.Secrets + *out = make([]SecretReference, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageBuildSpec. @@ -506,3 +511,18 @@ func (in *SecretCredentials) DeepCopy() *SecretCredentials { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretReference) DeepCopyInto(out *SecretReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretReference. +func (in *SecretReference) DeepCopy() *SecretReference { + if in == nil { + return nil + } + out := new(SecretReference) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/buildkit/buildkit.go b/pkg/buildkit/buildkit.go index 94aad1c4..998780ee 100644 --- a/pkg/buildkit/buildkit.go +++ b/pkg/buildkit/buildkit.go @@ -104,6 +104,7 @@ type BuildOptions struct { ImportCache []string DisableInlineCacheExport bool Secrets map[string]string + SecretsData map[string][]byte FetchAndExtractTimeout time.Duration } @@ -177,6 +178,11 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { secrets[name] = contents } + // merge in preloaded data + for name, contents := range opts.SecretsData { + secrets[name] = contents + } + // build solve options solveOpt := bkclient.SolveOpt{ Frontend: "dockerfile.v0", diff --git a/pkg/controller/imagebuild/component/builddispatcher.go b/pkg/controller/imagebuild/component/builddispatcher.go index 6953e98d..9e24aae4 100644 --- a/pkg/controller/imagebuild/component/builddispatcher.go +++ b/pkg/controller/imagebuild/component/builddispatcher.go @@ -4,12 +4,15 @@ import ( "context" "fmt" "os" + "strings" "sync" "time" "github.com/dominodatalab/controller-util/core" "github.com/go-logr/logr" "github.com/newrelic/go-agent/v3/newrelic" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -181,6 +184,40 @@ func (c *BuildDispatcherComponent) Reconcile(ctx *core.Context) (ctrl.Result, er } clientInitSeg.End() + // TODO: refactor this secret extraction to a helper like the credentials persist call + // configDir, helpMessage, err := credentials.Persist(ctx, buildLog, ctx.Config, obj.Spec.RegistryAuth) + clientset, err := kubernetes.NewForConfig(ctx.Config) + if err != nil { + return ctrl.Result{}, c.phase.SetFailed(ctx, obj, fmt.Errorf("failure to get kubernetes client: %w", err)) + } + + // TODO: add logging and NewRelic stuff + // TODO: does it make more sense to project the secret into the filesystem somehow and then just use existing secrets? + // Extracts secrets into data to pass to buildkit + secretsData := make(map[string][]byte) + for _, secretRef := range obj.Spec.Secrets { + client := clientset.CoreV1().Secrets(secretRef.Namespace) + + secret, err := client.Get(ctx, secretRef.Name, metav1.GetOptions{}) + + if err != nil { + return ctrl.Result{}, c.phase.SetFailed(ctx, obj, + fmt.Errorf("failure to load secret %q/%q: %w", secretRef.Namespace, secretRef.Name, err)) + } + + // prevent exfiltration of arbitrary secret values by using the presence of this lable + if _, ok := secret.Labels[hephv1.HephaestusSecretLabel]; !ok { + return ctrl.Result{}, c.phase.SetFailed(ctx, obj, + fmt.Errorf("secret %q/%q requires label %q", secretRef.Namespace, secretRef.Name, hephv1.HephaestusSecretLabel)) + } + + // builds a path from the secret name and the key from the data array like {name}/{key} + for path, data := range secret.Data { + name := strings.Join([]string{secret.Name, path}, "/") + secretsData[name] = data + } + } + buildOpts := buildkit.BuildOptions{ Context: obj.Spec.Context, Images: obj.Spec.Images, @@ -189,6 +226,7 @@ func (c *BuildDispatcherComponent) Reconcile(ctx *core.Context) (ctrl.Result, er ImportCache: obj.Spec.ImportRemoteBuildCache, DisableInlineCacheExport: obj.Spec.DisableCacheLayerExport, Secrets: c.cfg.Secrets, + SecretsData: secretsData, FetchAndExtractTimeout: c.cfg.FetchAndExtractTimeout, } log.Info("Dispatching image build", "images", buildOpts.Images)