From 10ff2a30f3236f973bf0d02c5c5477fe8abcce00 Mon Sep 17 00:00:00 2001 From: Johannes Dillmann Date: Mon, 28 Oct 2024 15:59:29 +0100 Subject: [PATCH] Add cascadeDelete option to the Image and Build objects Co-authored-by: Pavel Busko --- api/openapi-spec/swagger.json | 9 ++ cmd/controller/main.go | 2 +- docs/build.md | 4 +- docs/image.md | 1 + pkg/apis/build/v1alpha2/build.go | 4 + pkg/apis/build/v1alpha2/build_types.go | 2 + pkg/apis/build/v1alpha2/image_builds.go | 1 + pkg/apis/build/v1alpha2/image_types.go | 16 +-- pkg/openapi/openapi_generated.go | 30 ++++- pkg/reconciler/build/build.go | 82 ++++++++++++- pkg/reconciler/build/build_test.go | 56 +++++++++ .../build/buildfakes/fake_registry_client.go | 114 ++++++++++++++++++ pkg/reconciler/image/image_test.go | 62 ++++++---- .../testhelpers/reconciler_tester.go | 5 +- pkg/registry/client.go | 9 ++ 15 files changed, 353 insertions(+), 44 deletions(-) create mode 100644 pkg/reconciler/build/buildfakes/fake_registry_client.go diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index f00bbfd64..dc175e6fd 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -5591,6 +5591,9 @@ "cache": { "$ref": "#/definitions/kpack.build.v1alpha2.BuildCacheConfig" }, + "cascadeDelete": { + "type": "boolean" + }, "cnbBindings": { "type": "array", "items": { @@ -6724,6 +6727,9 @@ "cache": { "$ref": "#/definitions/kpack.build.v1alpha2.ImageCacheConfig" }, + "cascadeDelete": { + "type": "boolean" + }, "cosign": { "$ref": "#/definitions/kpack.build.v1alpha2.CosignConfig" }, @@ -7340,6 +7346,9 @@ "url" ], "properties": { + "auth": { + "type": "string" + }, "stripComponents": { "type": "integer", "format": "int64" diff --git a/cmd/controller/main.go b/cmd/controller/main.go index cf29131a5..8d4bf2fdd 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -227,7 +227,7 @@ func main() { SystemServiceAccountName: cfg.SystemServiceAccount, } - buildController := build.NewController(ctx, options, k8sClient, buildInformer, podInformer, metadataRetriever, buildpodGenerator, podProgressLogger, keychainFactory, &slsaAttester, secretFetcher, featureFlags) + buildController := build.NewController(ctx, options, k8sClient, buildInformer, podInformer, metadataRetriever, buildpodGenerator, podProgressLogger, keychainFactory, &slsaAttester, secretFetcher, featureFlags, ®istry.Client{}) imageController := image.NewController(ctx, options, k8sClient, imageInformer, buildInformer, duckBuilderInformer, sourceResolverInformer, pvcInformer, cfg.EnablePriorityClasses) sourceResolverController := sourceresolver.NewController(ctx, options, sourceResolverInformer, gitResolver, blobResolver, registryResolver) builderController, builderResync := builder.NewController(ctx, options, builderInformer, builderCreator, keychainFactory, clusterStoreInformer, buildpackInformer, clusterBuildpackInformer, clusterStackInformer, secretFetcher) diff --git a/docs/build.md b/docs/build.md index 389c28aae..4d4ad85a8 100644 --- a/docs/build.md +++ b/docs/build.md @@ -31,6 +31,7 @@ spec: git: url: https://github.com/buildpack/sample-java-app.git revision: main + cascadeDelete: true activeDeadlineSeconds: 1800 env: - name: "JAVA_BP_ENV" @@ -72,10 +73,11 @@ spec: - `env`: Optional list of build time environment variables. - `defaultProcess`: The [default process type](https://buildpacks.io/docs/app-developer-guide/run-an-app/) for the built OCI image - `projectDescriptorPath`: Path to the [project descriptor file](https://buildpacks.io/docs/reference/config/project-descriptor/) relative to source root dir or `subPath` if set. If unset, kpack will look for `project.toml` at the root dir or `subPath` if set. +- `cascadeDelete`: If set to `true`, produced image will garbage collected from the registry upon object deletion. - `resources`: Optional configurable resource limits on `CPU` and `memory`. - `tolerations`: Optional configurable pod spec tolerations - `nodeSelector`: Optional configurable pod spec nodeSelector -- `affinity`: Optional configurabl pod spec affinity +- `affinity`: Optional configurable pod spec affinity > Note: All fields on a build are immutable. Instead of updating a build, create a new one. diff --git a/docs/image.md b/docs/image.md index 5e77c0725..2ab75cecd 100644 --- a/docs/image.md +++ b/docs/image.md @@ -22,6 +22,7 @@ The following defines the relevant fields of the `image` resource spec in more d - `build`: Configuration that is passed to every image build. See [Build Configuration](#build-config) section below. - `defaultProcess`: The [default process type](https://buildpacks.io/docs/app-developer-guide/run-an-app/) for the built OCI image - `projectDescriptorPath`: Path to the [project descriptor file](https://buildpacks.io/docs/reference/config/project-descriptor/) relative to source root dir or `subPath` if set. If unset, kpack will look for `project.toml` at the root dir or `subPath` if set. +- `cascadeDelete`: If set to `true`, produced image will garbage collected from the registry upon `Build` objects deletion. - `cosign`: Configuration for additional cosign image signing. See [Cosign Configuration](#cosign-config) section below. ### Configuring Tags diff --git a/pkg/apis/build/v1alpha2/build.go b/pkg/apis/build/v1alpha2/build.go index cdd24cd0b..e4a9fa670 100644 --- a/pkg/apis/build/v1alpha2/build.go +++ b/pkg/apis/build/v1alpha2/build.go @@ -156,6 +156,10 @@ func (b *Build) DefaultProcess() string { return b.Spec.DefaultProcess } +func (b *Build) CascadeDeleteImage() bool { + return b.Spec.CascadeDelete +} + var buildSteps = map[string]struct{}{ PrepareContainerName: {}, AnalyzeContainerName: {}, diff --git a/pkg/apis/build/v1alpha2/build_types.go b/pkg/apis/build/v1alpha2/build_types.go index b7254e1b8..a94e239df 100644 --- a/pkg/apis/build/v1alpha2/build_types.go +++ b/pkg/apis/build/v1alpha2/build_types.go @@ -78,6 +78,8 @@ type BuildSpec struct { SchedulerName string `json:"schedulerName,omitempty"` PriorityClassName string `json:"priorityClassName,omitempty"` CreationTime string `json:"creationTime,omitempty"` + // +optional + CascadeDelete bool `json:"cascadeDelete,omitempty"` } func (bs *BuildSpec) RegistryCacheTag() string { diff --git a/pkg/apis/build/v1alpha2/image_builds.go b/pkg/apis/build/v1alpha2/image_builds.go index 728f77faa..1e28e449e 100644 --- a/pkg/apis/build/v1alpha2/image_builds.go +++ b/pkg/apis/build/v1alpha2/image_builds.go @@ -81,6 +81,7 @@ func (im *Image) Build(sourceResolver *SourceResolver, builder BuilderResource, PriorityClassName: priorityClass, ActiveDeadlineSeconds: im.BuildTimeout(), CreationTime: im.Spec.creationTime(), + CascadeDelete: im.Spec.CascadeDelete, }, } } diff --git a/pkg/apis/build/v1alpha2/image_types.go b/pkg/apis/build/v1alpha2/image_types.go index 610863d3c..a6d136d10 100644 --- a/pkg/apis/build/v1alpha2/image_types.go +++ b/pkg/apis/build/v1alpha2/image_types.go @@ -60,6 +60,8 @@ type ImageSpec struct { DefaultProcess string `json:"defaultProcess,omitempty"` // +listType AdditionalTags []string `json:"additionalTags,omitempty"` + // +optional + CascadeDelete bool `json:"cascadeDelete,omitempty"` } // +k8s:openapi-gen=true @@ -72,13 +74,13 @@ type ImageBuild struct { Env []corev1.EnvVar `json:"env,omitempty"` Resources corev1.ResourceRequirements `json:"resources,omitempty"` // +listType - Tolerations []corev1.Toleration `json:"tolerations,omitempty"` - NodeSelector map[string]string `json:"nodeSelector,omitempty"` - Affinity *corev1.Affinity `json:"affinity,omitempty"` - RuntimeClassName *string `json:"runtimeClassName,omitempty"` - SchedulerName string `json:"schedulerName,omitempty"` - BuildTimeout *int64 `json:"buildTimeout,omitempty"` - CreationTime string `json:"creationTime,omitempty"` + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + Affinity *corev1.Affinity `json:"affinity,omitempty"` + RuntimeClassName *string `json:"runtimeClassName,omitempty"` + SchedulerName string `json:"schedulerName,omitempty"` + BuildTimeout *int64 `json:"buildTimeout,omitempty"` + CreationTime string `json:"creationTime,omitempty"` } // +k8s:openapi-gen=true diff --git a/pkg/openapi/openapi_generated.go b/pkg/openapi/openapi_generated.go index 900f181e2..df7391da4 100644 --- a/pkg/openapi/openapi_generated.go +++ b/pkg/openapi/openapi_generated.go @@ -2281,6 +2281,12 @@ func schema_pkg_apis_build_v1alpha2_BuildSpec(ref common.ReferenceCallback) comm Format: "", }, }, + "cascadeDelete": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, }, Required: []string{"source"}, }, @@ -4317,6 +4323,12 @@ func schema_pkg_apis_build_v1alpha2_ImageSpec(ref common.ReferenceCallback) comm }, }, }, + "cascadeDelete": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, }, Required: []string{"tag", "source"}, }, @@ -4783,18 +4795,18 @@ func schema_pkg_apis_core_v1alpha1_Blob(ref common.ReferenceCallback) common.Ope Format: "", }, }, - "stripComponents": { - SchemaProps: spec.SchemaProps{ - Type: []string{"integer"}, - Format: "int64", - }, - }, "auth": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, Format: "", }, }, + "stripComponents": { + SchemaProps: spec.SchemaProps{ + Type: []string{"integer"}, + Format: "int64", + }, + }, }, Required: []string{"url"}, }, @@ -5426,6 +5438,12 @@ func schema_pkg_apis_core_v1alpha1_ResolvedBlobSource(ref common.ReferenceCallba Format: "", }, }, + "auth": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, "subPath": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, diff --git a/pkg/reconciler/build/build.go b/pkg/reconciler/build/build.go index 033fa8dd7..73a2bb1fd 100644 --- a/pkg/reconciler/build/build.go +++ b/pkg/reconciler/build/build.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "slices" "github.com/google/go-containerregistry/pkg/authn" ggcrv1 "github.com/google/go-containerregistry/pkg/v1" @@ -41,6 +42,7 @@ const ( Kind = "Build" k8sOSLabel = "kubernetes.io/os" ReasonCompleted = "Completed" + BuildFinalizer = "builds.kpack.io/finalizer" ) //go:generate counterfeiter . MetadataRetriever @@ -69,6 +71,11 @@ type SecretFetcher interface { SecretsForSystemServiceAccount(context.Context) ([]*corev1.Secret, error) } +//go:generate counterfeiter . RegistryClient +type RegistryClient interface { + Delete(keychain authn.Keychain, repoName string) error +} + func NewController( ctx context.Context, opt reconciler.Options, k8sClient k8sclient.Interface, informer buildinformers.BuildInformer, podInformer corev1Informers.PodInformer, @@ -78,6 +85,7 @@ func NewController( attester SLSAAttester, secretFetcher SecretFetcher, featureFlags config.FeatureFlags, + registryClient RegistryClient, ) *controller.Impl { c := &Reconciler{ Client: opt.Client, @@ -91,6 +99,7 @@ func NewController( Attester: attester, SecretFetcher: secretFetcher, FeatureFlags: featureFlags, + RegistryClient: registryClient, } logger := opt.Logger.With( @@ -121,6 +130,7 @@ type Reconciler struct { Attester SLSAAttester SecretFetcher SecretFetcher FeatureFlags config.FeatureFlags + RegistryClient RegistryClient } func (c *Reconciler) Reconcile(ctx context.Context, key string) error { @@ -136,6 +146,14 @@ func (c *Reconciler) Reconcile(ctx context.Context, key string) error { return err } + if !build.DeletionTimestamp.IsZero() { + return c.finalize(ctx, build) + } + + if err := c.setFinalizer(ctx, build); err != nil { + return err + } + build = build.DeepCopy() build.SetDefaults(ctx) @@ -352,6 +370,64 @@ func (c *Reconciler) conditionForPod(pod *corev1.Pod, stepsCompleted []string) c } } +func (c *Reconciler) finalize(ctx context.Context, build *buildapi.Build) error { + if !slices.Contains(build.GetFinalizers(), BuildFinalizer) { + return nil + } + + if build.Finished() { + keychain, err := c.KeychainFactory.KeychainForSecretRef(ctx, registry.SecretRef{ + ServiceAccount: build.Spec.ServiceAccountName, + Namespace: build.Namespace, + }) + if err != nil { + return err + } + + if err := c.RegistryClient.Delete(keychain, build.Status.LatestImage); err != nil { + return err + } + } + + mergePatch := map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": slices.DeleteFunc(build.GetFinalizers(), func(f string) bool { + return f == BuildFinalizer + }), + "resourceVersion": build.ResourceVersion, + }, + } + + patch, err := json.Marshal(mergePatch) + if err != nil { + return err + } + + _, err = c.Client.KpackV1alpha2().Builds(build.Namespace).Patch(ctx, build.Name, types.MergePatchType, patch, metav1.PatchOptions{}) + return err +} + +func (c *Reconciler) setFinalizer(ctx context.Context, build *buildapi.Build) error { + if slices.Contains(build.GetFinalizers(), BuildFinalizer) || !build.CascadeDeleteImage() { + return nil + } + + mergePatch := map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": append(build.GetFinalizers(), BuildFinalizer), + "resourceVersion": build.ResourceVersion, + }, + } + + patch, err := json.Marshal(mergePatch) + if err != nil { + return err + } + + _, err = c.Client.KpackV1alpha2().Builds(build.Namespace).Patch(ctx, build.Name, types.MergePatchType, patch, metav1.PatchOptions{}) + return err +} + func stepStates(pod *corev1.Pod) []corev1.ContainerState { states := make([]corev1.ContainerState, 0, len(buildapi.BuildSteps())) for _, s := range append(pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses...) { @@ -442,9 +518,9 @@ func (c *Reconciler) attestBuild(ctx context.Context, build *buildapi.Build, bui signers[i] = s } - buildId := slsa.UnsignedBuildID + buildID := slsa.UnsignedBuildID if len(signers) > 0 { - buildId = slsa.SignedBuildID + buildID = slsa.SignedBuildID } deps, err := c.attestBuildDeps(ctx, build, pod, secrets) @@ -452,7 +528,7 @@ func (c *Reconciler) attestBuild(ctx context.Context, build *buildapi.Build, bui return "", fmt.Errorf("failed to gather build deps: %v", err) } - statement, err := c.Attester.AttestBuild(build, buildMetadata, pod, keychain, buildId, deps...) + statement, err := c.Attester.AttestBuild(build, buildMetadata, pod, keychain, buildID, deps...) if err != nil { return "", fmt.Errorf("failed to generate statement: %v", err) } diff --git a/pkg/reconciler/build/build_test.go b/pkg/reconciler/build/build_test.go index f7d7a8b3a..eca86b825 100644 --- a/pkg/reconciler/build/build_test.go +++ b/pkg/reconciler/build/build_test.go @@ -10,9 +10,11 @@ import ( "encoding/pem" "errors" "fmt" + "strings" "testing" "time" + "github.com/google/go-cmp/cmp" "github.com/in-toto/in-toto-golang/in_toto" "github.com/sclevine/spec" "github.com/sigstore/cosign/v2/pkg/cosign" @@ -65,6 +67,7 @@ func testBuildReconciler(t *testing.T, when spec.G, it spec.S) { fakeMetadataRetriever = &buildfakes.FakeMetadataRetriever{} fakeAttester = &buildfakes.FakeSLSAAttester{} fakeSecretFetcher = &buildfakes.FakeSecretFetcher{} + fakeRegistryClient = &buildfakes.FakeRegistryClient{} keychainFactory = ®istryfakes.FakeKeychainFactory{} podGenerator = &testPodGenerator{} podProgressLogger = &testPodProgressLogger{} @@ -97,6 +100,7 @@ func testBuildReconciler(t *testing.T, when spec.G, it spec.S) { PodProgressLogger: podProgressLogger, Attester: fakeAttester, SecretFetcher: fakeSecretFetcher, + RegistryClient: fakeRegistryClient, FeatureFlags: featureFlags, } @@ -1674,6 +1678,58 @@ func testBuildReconciler(t *testing.T, when spec.G, it spec.S) { }) }) + + when("cascadeDelete is enabled", func() { + bld.Spec.CascadeDelete = true + bld.Finalizers = append(bld.GetFinalizers(), build.BuildFinalizer) + bld.SetDeletionTimestamp(&metav1.Time{Time: time.Now()}) + bld.Status.LatestImage = "foo/bar@sha256:123" + bld.Status.Conditions = corev1alpha1.Conditions{ + { + Type: corev1alpha1.ConditionSucceeded, + Status: corev1.ConditionTrue, + Reason: build.ReasonCompleted, + }, + } + + it("deletes the image from the registry", func() { + keychainFactory.AddKeychainForSecretRef(t, registry.SecretRef{ + ServiceAccount: bld.Spec.ServiceAccountName, + Namespace: bld.Namespace, + }, nil) + finalizerPatch, err := json.Marshal(map[string]interface{}{ + "metadata": map[string]interface{}{ + "finalizers": []string{}, + "resourceVersion": bld.ResourceVersion, + }, + }) + require.NoError(t, err) + + rt.Test(rtesting.TableRow{ + Key: key, + WantErr: false, + Objects: []runtime.Object{ + bld, + }, + WantPatches: []clientgotesting.PatchActionImpl{ + { + Name: bld.Name, + PatchType: types.MergePatchType, + Patch: finalizerPatch, + }, + }, + CmpOpts: []cmp.Option{ + cmp.FilterPath(func(p cmp.Path) bool { + t.Log(p.String()) + return strings.HasSuffix(p.String(), "ObjectMeta.Finalizers") + }, cmp.Ignore()), + }, + }) + + require.Len(t, fakeRegistryClient.Invocations()["Delete"], 1) + require.Equal(t, fakeRegistryClient.Invocations()["Delete"][0], []interface{}{nil, bld.Status.LatestImage}) + }) + }) }) } diff --git a/pkg/reconciler/build/buildfakes/fake_registry_client.go b/pkg/reconciler/build/buildfakes/fake_registry_client.go new file mode 100644 index 000000000..5693e7179 --- /dev/null +++ b/pkg/reconciler/build/buildfakes/fake_registry_client.go @@ -0,0 +1,114 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package buildfakes + +import ( + "sync" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/pivotal/kpack/pkg/reconciler/build" +) + +type FakeRegistryClient struct { + DeleteStub func(authn.Keychain, string) error + deleteMutex sync.RWMutex + deleteArgsForCall []struct { + arg1 authn.Keychain + arg2 string + } + deleteReturns struct { + result1 error + } + deleteReturnsOnCall map[int]struct { + result1 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeRegistryClient) Delete(arg1 authn.Keychain, arg2 string) error { + fake.deleteMutex.Lock() + ret, specificReturn := fake.deleteReturnsOnCall[len(fake.deleteArgsForCall)] + fake.deleteArgsForCall = append(fake.deleteArgsForCall, struct { + arg1 authn.Keychain + arg2 string + }{arg1, arg2}) + stub := fake.DeleteStub + fakeReturns := fake.deleteReturns + fake.recordInvocation("Delete", []interface{}{arg1, arg2}) + fake.deleteMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeRegistryClient) DeleteCallCount() int { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + return len(fake.deleteArgsForCall) +} + +func (fake *FakeRegistryClient) DeleteCalls(stub func(authn.Keychain, string) error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = stub +} + +func (fake *FakeRegistryClient) DeleteArgsForCall(i int) (authn.Keychain, string) { + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + argsForCall := fake.deleteArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeRegistryClient) DeleteReturns(result1 error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = nil + fake.deleteReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeRegistryClient) DeleteReturnsOnCall(i int, result1 error) { + fake.deleteMutex.Lock() + defer fake.deleteMutex.Unlock() + fake.DeleteStub = nil + if fake.deleteReturnsOnCall == nil { + fake.deleteReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.deleteReturnsOnCall[i] = struct { + result1 error + }{result1} +} + +func (fake *FakeRegistryClient) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.deleteMutex.RLock() + defer fake.deleteMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeRegistryClient) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ build.RegistryClient = new(FakeRegistryClient) diff --git a/pkg/reconciler/image/image_test.go b/pkg/reconciler/image/image_test.go index d8b04d7e7..14e04a3a7 100644 --- a/pkg/reconciler/image/image_test.go +++ b/pkg/reconciler/image/image_test.go @@ -75,7 +75,7 @@ func testImageReconciler(t *testing.T, when spec.G, it spec.S) { return r, actionRecorderList, eventList }) -imageWithBuilder := &buildapi.Image{ + imageWithBuilder := &buildapi.Image{ ObjectMeta: metav1.ObjectMeta{ Name: imageName, Namespace: namespace, @@ -101,6 +101,7 @@ imageWithBuilder := &buildapi.Image{ SuccessBuildHistoryLimit: limit(10), ImageTaggingStrategy: corev1alpha1.None, Build: &buildapi.ImageBuild{}, + CascadeDelete: true, }, Status: buildapi.ImageStatus{ Status: corev1alpha1.Status{ @@ -787,15 +788,15 @@ imageWithBuilder := &buildapi.Image{ Objects: []runtime.Object{ imageWithBuilder, builderWithCondition( - builder, + builder, corev1alpha1.Condition{ Type: corev1alpha1.ConditionReady, Status: corev1.ConditionFalse, Message: "something went wrong", }, corev1alpha1.Condition{ - Type: buildapi.ConditionUpToDate, - Status: corev1.ConditionFalse, + Type: buildapi.ConditionUpToDate, + Status: corev1.ConditionFalse, }, ), resolvedSourceResolver(imageWithBuilder), @@ -844,16 +845,16 @@ imageWithBuilder := &buildapi.Image{ Objects: []runtime.Object{ imageWithBuilder, builderWithCondition( - builder, + builder, corev1alpha1.Condition{ - Type: corev1alpha1.ConditionReady, - Status: corev1.ConditionTrue, + Type: corev1alpha1.ConditionReady, + Status: corev1.ConditionTrue, }, corev1alpha1.Condition{ Type: buildapi.ConditionUpToDate, Status: corev1.ConditionFalse, Message: "Builder failed to reconcile", - Reason: buildapi.ReconcileFailedReason, + Reason: buildapi.ReconcileFailedReason, }, ), sourceResolver, @@ -899,7 +900,8 @@ imageWithBuilder := &buildapi.Image{ }, }, Spec: buildapi.BuildSpec{ - Tags: []string{imageWithBuilder.Spec.Tag}, + CascadeDelete: true, + Tags: []string{imageWithBuilder.Spec.Tag}, Builder: corev1alpha1.BuildBuilderSpec{ Image: builder.Status.LatestImage, }, @@ -921,10 +923,10 @@ imageWithBuilder := &buildapi.Image{ ObjectMeta: imageWithBuilder.ObjectMeta, Spec: imageWithBuilder.Spec, Status: buildapi.ImageStatus{ - LatestBuildRef: "image-name-build-1", + LatestBuildRef: "image-name-build-1", LatestBuildImageGeneration: 1, - BuildCounter: 1, - LatestBuildReason: "CONFIG", + BuildCounter: 1, + LatestBuildReason: "CONFIG", Status: corev1alpha1.Status{ ObservedGeneration: originalGeneration, Conditions: corev1alpha1.Conditions{ @@ -935,9 +937,9 @@ imageWithBuilder := &buildapi.Image{ Message: "Build 'image-name-build-1' is executing", }, { - Type: buildapi.ConditionBuilderReady, - Status: corev1.ConditionTrue, - Reason: buildapi.BuilderReady, + Type: buildapi.ConditionBuilderReady, + Status: corev1.ConditionTrue, + Reason: buildapi.BuilderReady, }, { Type: buildapi.ConditionBuilderUpToDate, @@ -1004,7 +1006,8 @@ imageWithBuilder := &buildapi.Image{ }, }, Spec: buildapi.BuildSpec{ - Tags: []string{imageWithBuilder.Spec.Tag}, + CascadeDelete: true, + Tags: []string{imageWithBuilder.Spec.Tag}, Builder: corev1alpha1.BuildBuilderSpec{ Image: builder.Status.LatestImage, }, @@ -1097,7 +1100,8 @@ imageWithBuilder := &buildapi.Image{ }, }, Spec: buildapi.BuildSpec{ - Tags: []string{imageWithBuilder.Spec.Tag}, + CascadeDelete: true, + Tags: []string{imageWithBuilder.Spec.Tag}, Builder: corev1alpha1.BuildBuilderSpec{ Image: clusterBuilder.Status.LatestImage, }, @@ -1190,7 +1194,8 @@ imageWithBuilder := &buildapi.Image{ }, }, Spec: buildapi.BuildSpec{ - Tags: []string{imageWithBuilder.Spec.Tag}, + CascadeDelete: true, + Tags: []string{imageWithBuilder.Spec.Tag}, Builder: corev1alpha1.BuildBuilderSpec{ Image: builder.Status.LatestImage, }, @@ -1284,7 +1289,8 @@ imageWithBuilder := &buildapi.Image{ }, }, Spec: buildapi.BuildSpec{ - Tags: []string{imageWithBuilder.Spec.Tag}, + CascadeDelete: true, + Tags: []string{imageWithBuilder.Spec.Tag}, Builder: corev1alpha1.BuildBuilderSpec{ Image: clusterBuilder.Status.LatestImage, }, @@ -1380,7 +1386,8 @@ imageWithBuilder := &buildapi.Image{ }, }, Spec: buildapi.BuildSpec{ - Tags: []string{imageWithBuilder.Spec.Tag}, + CascadeDelete: true, + Tags: []string{imageWithBuilder.Spec.Tag}, Builder: corev1alpha1.BuildBuilderSpec{ Image: builder.Status.LatestImage, }, @@ -1529,7 +1536,8 @@ imageWithBuilder := &buildapi.Image{ }, }, Spec: buildapi.BuildSpec{ - Tags: []string{imageWithBuilder.Spec.Tag}, + CascadeDelete: true, + Tags: []string{imageWithBuilder.Spec.Tag}, Builder: corev1alpha1.BuildBuilderSpec{ Image: builder.Status.LatestImage, }, @@ -1658,7 +1666,8 @@ imageWithBuilder := &buildapi.Image{ }, }, Spec: buildapi.BuildSpec{ - Tags: []string{imageWithBuilder.Spec.Tag}, + CascadeDelete: true, + Tags: []string{imageWithBuilder.Spec.Tag}, Builder: corev1alpha1.BuildBuilderSpec{ Image: builder.Status.LatestImage, }, @@ -1828,7 +1837,8 @@ imageWithBuilder := &buildapi.Image{ }, }, Spec: buildapi.BuildSpec{ - Tags: []string{imageWithBuilder.Spec.Tag}, + CascadeDelete: true, + Tags: []string{imageWithBuilder.Spec.Tag}, Builder: corev1alpha1.BuildBuilderSpec{ Image: updatedBuilderImage, }, @@ -1997,7 +2007,8 @@ imageWithBuilder := &buildapi.Image{ }, }, Spec: buildapi.BuildSpec{ - Tags: []string{imageWithBuilder.Spec.Tag}, + CascadeDelete: true, + Tags: []string{imageWithBuilder.Spec.Tag}, Builder: corev1alpha1.BuildBuilderSpec{ Image: updatedBuilderImage, }, @@ -2145,7 +2156,8 @@ imageWithBuilder := &buildapi.Image{ }, }, Spec: buildapi.BuildSpec{ - Tags: []string{imageWithBuilder.Spec.Tag}, + CascadeDelete: true, + Tags: []string{imageWithBuilder.Spec.Tag}, Builder: corev1alpha1.BuildBuilderSpec{ Image: builder.Status.LatestImage, }, diff --git a/pkg/reconciler/testhelpers/reconciler_tester.go b/pkg/reconciler/testhelpers/reconciler_tester.go index 3cad08ee9..354bab019 100644 --- a/pkg/reconciler/testhelpers/reconciler_tester.go +++ b/pkg/reconciler/testhelpers/reconciler_tester.go @@ -31,8 +31,11 @@ func (rt SpecReconcilerTester) Test(test rtesting.TableRow) { test.Test(rt.t, rt.factory) + defaultOpts := []cmp.Option{safeDeployDiff, cmpopts.EquateEmpty()} + effectiveOpts := append(defaultOpts, test.CmpOpts...) + // Validate cached objects do not get soiled after controller loops - if diff := cmp.Diff(originObjects, test.Objects, safeDeployDiff, cmpopts.EquateEmpty()); diff != "" { + if diff := cmp.Diff(originObjects, test.Objects, effectiveOpts...); diff != "" { rt.t.Errorf("Unexpected objects in test %s (-want, +got): %v", test.Name, diff) } } diff --git a/pkg/registry/client.go b/pkg/registry/client.go index 9fa0d6153..4980c911f 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -60,6 +60,15 @@ func (t *Client) Save(keychain authn.Keychain, tag string, image v1.Image) (stri return identifier, remote.Tag(ref.Context().Tag(timestampTag()), image, remote.WithAuthFromKeychain(keychain)) } +func (t *Client) Delete(keychain authn.Keychain, repoName string) error { + reference, err := name.ParseReference(repoName) + if err != nil { + return err + } + + return remote.Delete(reference, remote.WithAuthFromKeychain(keychain)) +} + func timestampTag() string { now := time.Now() return fmt.Sprintf("%s%02d%02d%02d", now.Format("20060102"), now.Hour(), now.Minute(), now.Second())