diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index 3ae8ccd54..b47545bd5 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -5703,6 +5703,10 @@ }, "kpack.build.v1alpha2.BuildStatus": { "type": "object", + "required": [ + "lifecycleVersion", + "lifecycleCommit" + ], "properties": { "buildMetadata": { "type": "array", @@ -5721,12 +5725,23 @@ "x-kubernetes-patch-merge-key": "type", "x-kubernetes-patch-strategy": "merge" }, + "latestAttestationImage": { + "type": "string" + }, "latestCacheImage": { "type": "string" }, "latestImage": { "type": "string" }, + "lifecycleCommit": { + "type": "string", + "default": "" + }, + "lifecycleVersion": { + "type": "string", + "default": "" + }, "observedGeneration": { "description": "ObservedGeneration is the 'Generation' of the Service that was last processed by the controller.", "type": "integer", @@ -5886,6 +5901,10 @@ "default": "" } }, + "lifecycle": { + "default": {}, + "$ref": "#/definitions/io.k8s.api.core.v1.ObjectReference" + }, "order": { "type": "array", "items": { @@ -5930,6 +5949,10 @@ "latestImage": { "type": "string" }, + "lifecycle": { + "default": {}, + "$ref": "#/definitions/kpack.build.v1alpha2.ResolvedClusterLifecycle" + }, "observedGeneration": { "description": "ObservedGeneration is the 'Generation' of the Service that was last processed by the controller.", "type": "integer", @@ -6129,6 +6152,10 @@ "default": "" } }, + "lifecycle": { + "default": {}, + "$ref": "#/definitions/io.k8s.api.core.v1.ObjectReference" + }, "order": { "type": "array", "items": { @@ -6250,6 +6277,109 @@ } } }, + "kpack.build.v1alpha2.ClusterLifecycle": { + "type": "object", + "required": [ + "spec", + "status" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + }, + "spec": { + "default": {}, + "$ref": "#/definitions/kpack.build.v1alpha2.ClusterLifecycleSpec" + }, + "status": { + "default": {}, + "$ref": "#/definitions/kpack.build.v1alpha2.ClusterLifecycleStatus" + } + } + }, + "kpack.build.v1alpha2.ClusterLifecycleList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "$ref": "#/definitions/kpack.build.v1alpha2.ClusterLifecycle" + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + } + }, + "kpack.build.v1alpha2.ClusterLifecycleSpec": { + "type": "object", + "properties": { + "image": { + "type": "string" + }, + "serviceAccountRef": { + "$ref": "#/definitions/io.k8s.api.core.v1.ObjectReference" + } + } + }, + "kpack.build.v1alpha2.ClusterLifecycleStatus": { + "type": "object", + "properties": { + "api": { + "description": "Deprecated: Use `LifecycleAPIs` instead", + "default": {}, + "$ref": "#/definitions/kpack.build.v1alpha2.LifecycleAPI" + }, + "apis": { + "default": {}, + "$ref": "#/definitions/kpack.build.v1alpha2.LifecycleAPIs" + }, + "commit": { + "type": "string" + }, + "conditions": { + "description": "Conditions the latest available observations of a resource's current state.", + "type": "array", + "items": { + "default": {}, + "$ref": "#/definitions/kpack.core.v1alpha1.Condition" + }, + "x-kubernetes-patch-merge-key": "type", + "x-kubernetes-patch-strategy": "merge" + }, + "observedGeneration": { + "description": "ObservedGeneration is the 'Generation' of the Service that was last processed by the controller.", + "type": "integer", + "format": "int64" + }, + "version": { + "type": "string" + } + } + }, "kpack.build.v1alpha2.ClusterStack": { "type": "object", "required": [ @@ -6825,6 +6955,10 @@ "default": "" } }, + "lifecycle": { + "default": {}, + "$ref": "#/definitions/io.k8s.api.core.v1.ObjectReference" + }, "order": { "type": "array", "items": { @@ -6864,6 +6998,26 @@ } } }, + "kpack.build.v1alpha2.ResolvedClusterLifecycle": { + "type": "object", + "properties": { + "api": { + "description": "Deprecated: Use `LifecycleAPIs` instead", + "default": {}, + "$ref": "#/definitions/kpack.build.v1alpha2.LifecycleAPI" + }, + "apis": { + "default": {}, + "$ref": "#/definitions/kpack.build.v1alpha2.LifecycleAPIs" + }, + "commit": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, "kpack.build.v1alpha2.ResolvedClusterStack": { "type": "object", "properties": { diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 90a73f24c..a93aa088d 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -3,7 +3,6 @@ package main import ( "context" "flag" - "fmt" "log" "net/http" "os" @@ -14,7 +13,6 @@ import ( ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote" "go.uber.org/zap" "golang.org/x/sync/errgroup" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/dynamic" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" @@ -30,7 +28,6 @@ import ( "knative.dev/pkg/metrics" "knative.dev/pkg/profiling" "knative.dev/pkg/signals" - "knative.dev/pkg/system" "github.com/pivotal/kpack/cmd" _ "github.com/pivotal/kpack/internal/logrus/fatal" @@ -52,10 +49,10 @@ import ( "github.com/pivotal/kpack/pkg/reconciler/buildpack" "github.com/pivotal/kpack/pkg/reconciler/clusterbuilder" "github.com/pivotal/kpack/pkg/reconciler/clusterbuildpack" + "github.com/pivotal/kpack/pkg/reconciler/clusterlifecycle" "github.com/pivotal/kpack/pkg/reconciler/clusterstack" "github.com/pivotal/kpack/pkg/reconciler/clusterstore" "github.com/pivotal/kpack/pkg/reconciler/image" - "github.com/pivotal/kpack/pkg/reconciler/lifecycle" "github.com/pivotal/kpack/pkg/reconciler/sourceresolver" "github.com/pivotal/kpack/pkg/registry" "github.com/pivotal/kpack/pkg/secret" @@ -133,6 +130,7 @@ func main() { buildpackInformer := informerFactory.Kpack().V1alpha2().Buildpacks() clusterBuilderInformer := informerFactory.Kpack().V1alpha2().ClusterBuilders() clusterBuildpackInformer := informerFactory.Kpack().V1alpha2().ClusterBuildpacks() + clusterLifecycleInformer := informerFactory.Kpack().V1alpha2().ClusterLifecycles() clusterStoreInformer := informerFactory.Kpack().V1alpha2().ClusterStores() clusterStackInformer := informerFactory.Kpack().V1alpha2().ClusterStacks() @@ -148,15 +146,6 @@ func main() { if err != nil { log.Fatalf("could not create k8s keychain factory: %s", err) } - lifecycleConfigmapInformerFactory := informers.NewSharedInformerFactoryWithOptions( - k8sClient, - options.ResyncPeriod, - informers.WithNamespace(system.Namespace()), - informers.WithTweakListOptions(func(options *metav1.ListOptions) { - options.FieldSelector = fmt.Sprintf("metadata.namespace=%s,metadata.name=%s", system.Namespace(), config.LifecycleConfigName) - }), - ) - lifecycleConfigmapInformer := lifecycleConfigmapInformerFactory.Core().V1().ConfigMaps() metadataRetriever := &cnb.RemoteMetadataRetriever{ ImageFetcher: ®istry.Client{}, @@ -195,14 +184,14 @@ func main() { RegistryClient: ®istry.Client{}, } - lifecycleProvider := config.NewLifecycleProvider(®istry.Client{}, keychainFactory) + remoteLifecycleReader := &cnb.RemoteLifecycleReader{ + RegistryClient: ®istry.Client{}, + } builderCreator := &cnb.RemoteBuilderCreator{ - RegistryClient: ®istry.Client{}, - KpackVersion: cmd.Identifer, - LifecycleProvider: lifecycleProvider, - KeychainFactory: keychainFactory, - ImageSigner: cosign.NewImageSigner(sign.SignCmd, ociremote.SignatureTag), + RegistryClient: ®istry.Client{}, + KpackVersion: cmd.Identifer, + ImageSigner: cosign.NewImageSigner(sign.SignCmd, ociremote.SignatureTag), } podProgressLogger := &buildchange.ProgressLogger{ @@ -212,8 +201,7 @@ func main() { slsaAttester := slsa.Attester{ Version: cmd.Version, - LifecycleProvider: lifecycleProvider, - ImageReader: slsa.NewImageReader(®istry.Client{}), + ImageReader: slsa.NewImageReader(®istry.Client{}), Images: images, Features: featureFlags, @@ -229,21 +217,17 @@ func main() { buildController := build.NewController(ctx, options, k8sClient, buildInformer, podInformer, metadataRetriever, buildpodGenerator, podProgressLogger, keychainFactory, &slsaAttester, secretFetcher, featureFlags) 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) + builderController := builder.NewController(ctx, options, builderInformer, builderCreator, keychainFactory, clusterStoreInformer, buildpackInformer, clusterBuildpackInformer, clusterStackInformer, clusterLifecycleInformer, secretFetcher) buildpackController := buildpack.NewController(ctx, options, keychainFactory, buildpackInformer, remoteStoreReader) - clusterBuilderController, clusterBuilderResync := clusterbuilder.NewController(ctx, options, clusterBuilderInformer, builderCreator, keychainFactory, clusterStoreInformer, clusterBuildpackInformer, clusterStackInformer, secretFetcher) + clusterBuilderController := clusterbuilder.NewController(ctx, options, clusterBuilderInformer, builderCreator, keychainFactory, clusterStoreInformer, clusterBuildpackInformer, clusterStackInformer, clusterLifecycleInformer, secretFetcher) clusterBuildpackController := clusterbuildpack.NewController(ctx, options, keychainFactory, clusterBuildpackInformer, remoteStoreReader) clusterStoreController := clusterstore.NewController(ctx, options, keychainFactory, clusterStoreInformer, remoteStoreReader) clusterStackController := clusterstack.NewController(ctx, options, keychainFactory, clusterStackInformer, remoteStackReader) - lifecycleController := lifecycle.NewController(ctx, options, k8sClient, config.LifecycleConfigName, lifecycleConfigmapInformer, lifecycleProvider) - - lifecycleProvider.AddEventHandler(builderResync) - lifecycleProvider.AddEventHandler(clusterBuilderResync) + clusterLifecycleController := clusterlifecycle.NewController(ctx, options, keychainFactory, clusterLifecycleInformer, remoteLifecycleReader) stopChan := make(chan struct{}) informerFactory.Start(stopChan) k8sInformerFactory.Start(stopChan) - lifecycleConfigmapInformerFactory.Start(stopChan) waitForSync(stopChan, buildInformer.Informer(), @@ -251,7 +235,6 @@ func main() { sourceResolverInformer.Informer(), pvcInformer.Informer(), podInformer.Informer(), - lifecycleConfigmapInformer.Informer(), builderInformer.Informer(), buildpackInformer.Informer(), clusterBuilderInformer.Informer(), @@ -263,6 +246,7 @@ func main() { err = runGroup( ctx, run(clusterStackController, routinesPerController), + run(clusterLifecycleController, routinesPerController), run(imageController, routinesPerController), run(buildController, routinesPerController), run(builderController, routinesPerController), @@ -270,7 +254,6 @@ func main() { run(clusterBuilderController, routinesPerController), run(clusterBuildpackController, routinesPerController), run(clusterStoreController, routinesPerController), - run(lifecycleController, routinesPerController), run(sourceResolverController, 2*routinesPerController), func(ctx context.Context) error { return configMapWatcher.Start(ctx.Done()) diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index 140f3ea04..5ca12a3e5 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -39,6 +39,7 @@ var types = map[schema.GroupVersionKind]resourcesemantics.GenericCRD{ v1alpha2.SchemeGroupVersion.WithKind(v1alpha2.ClusterBuildpackKind): &v1alpha2.ClusterBuildpack{}, v1alpha2.SchemeGroupVersion.WithKind(v1alpha2.ClusterStoreKind): &v1alpha2.ClusterStore{}, v1alpha2.SchemeGroupVersion.WithKind(v1alpha2.ClusterStackKind): &v1alpha2.ClusterStack{}, + v1alpha2.SchemeGroupVersion.WithKind(v1alpha2.ClusterLifecycleKind): &v1alpha2.ClusterLifecycle{}, } func init() { diff --git a/config/clusterlifecycle.yaml b/config/clusterlifecycle.yaml new file mode 100644 index 000000000..6edaaeabf --- /dev/null +++ b/config/clusterlifecycle.yaml @@ -0,0 +1,41 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clusterlifecycles.kpack.io +spec: + group: kpack.io + versions: + - name: v1alpha1 + served: true + storage: false + schema: + openAPIV3Schema: + type: object + x-kubernetes-preserve-unknown-fields: true + subresources: + status: {} + additionalPrinterColumns: + - name: Ready + type: string + jsonPath: ".status.conditions[?(@.type==\"Ready\")].status" + - name: v1alpha2 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + x-kubernetes-preserve-unknown-fields: true + subresources: + status: {} + additionalPrinterColumns: + - name: Ready + type: string + jsonPath: ".status.conditions[?(@.type==\"Ready\")].status" + names: + kind: ClusterLifecycle + listKind: ClusterLifecycleList + singular: clusterlifecycle + plural: clusterlifecycles + categories: + - kpack + scope: Cluster diff --git a/config/controllerrole.yaml b/config/controllerrole.yaml index b1e0bd264..fce11e440 100644 --- a/config/controllerrole.yaml +++ b/config/controllerrole.yaml @@ -27,6 +27,8 @@ rules: - clusterbuilders/status - clusterbuildpacks - clusterbuildpacks/status + - clusterlifecycles + - clusterlifecycles/status - clusterstores - clusterstores/status - clusterstacks diff --git a/hack/build.sh b/hack/build.sh index 696f4516a..b89754522 100644 --- a/hack/build.sh +++ b/hack/build.sh @@ -1,10 +1,5 @@ #!/bin/bash -function lifecycle_image_build() { - image=$1 - go run hack/lifecycle/main.go --tag=${image} -} - function generate_kbld_config_pack() { path=$1 registry=$2 @@ -195,9 +190,6 @@ function compile() { completion_image=${IMAGE_PREFIX}completion lifecycle_image=${IMAGE_PREFIX}lifecycle - echo "Building Lifecycle" - lifecycle_image_build ${lifecycle_image} - echo "Generating kbld config" temp_dir=$(mktemp -d) kbld_config_path="${temp_dir}/kbld-config" diff --git a/hack/lifecycle/main.go b/hack/lifecycle/main.go deleted file mode 100644 index b153468f4..000000000 --- a/hack/lifecycle/main.go +++ /dev/null @@ -1,279 +0,0 @@ -package main - -import ( - "archive/tar" - "bytes" - "compress/gzip" - "flag" - "fmt" - "io" - "log" - "net/http" - "os" - "path" - "reflect" - "regexp" - "time" - - "github.com/BurntSushi/toml" - "github.com/buildpacks/imgutil/layer" - "github.com/google/go-containerregistry/pkg/authn" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/mutate" - "github.com/google/go-containerregistry/pkg/v1/random" - "github.com/google/go-containerregistry/pkg/v1/tarball" - "github.com/pkg/errors" - - "github.com/pivotal/kpack/pkg/cnb" - "github.com/pivotal/kpack/pkg/registry" - "github.com/pivotal/kpack/pkg/registry/imagehelpers" -) - -const ( - lifecycleMetadataLabel = "io.buildpacks.lifecycle.metadata" - lifecycleLocation = "/cnb/lifecycle/" - lifecycleVersion = "0.17.3" -) - -var ( - tag = flag.String("tag", "", "tag for lifecycle image") - - normalizedTime = time.Date(1980, time.January, 1, 0, 0, 1, 0, time.UTC) -) - -func main() { - flag.Parse() - - image, err := lifecycleImage( - fmt.Sprintf("https://github.com/buildpacks/lifecycle/releases/download/v%s/lifecycle-v%s+linux.x86-64.tgz", lifecycleVersion, lifecycleVersion), - fmt.Sprintf("https://github.com/buildpacks/lifecycle/releases/download/v%s/lifecycle-v%s+windows.x86-64.tgz", lifecycleVersion, lifecycleVersion), - ) - if err != nil { - log.Fatal(err) - } - - identifier, err := (®istry.Client{}).Save(authn.DefaultKeychain, *tag, image) - if err != nil { - log.Fatal(err) - } - - log.Println(fmt.Sprintf("saved lifecycle image: %s ", identifier)) - -} - -func lifecycleImage(linuxUrl, windowsUrl string) (v1.Image, error) { - image, err := random.Image(0, 0) - if err != nil { - return nil, err - } - - linuxDescriptor, err := lifecycleDescriptor(linuxUrl) - if err != nil { - return nil, err - } - windowsDescriptor, err := lifecycleDescriptor(windowsUrl) - if err != nil { - return nil, err - } - if !reflect.DeepEqual(linuxDescriptor, windowsDescriptor) { - return nil, errors.New("linux and windows lifecycle descriptors do not match. Check urls.") - } - - linuxLayer, err := lifecycleLayer(linuxUrl, "linux") - if err != nil { - return nil, err - } - linuxDiffID, err := linuxLayer.DiffID() - if err != nil { - return nil, err - } - - image, err = imagehelpers.SetStringLabel(image, "linux", linuxDiffID.String()) - if err != nil { - return nil, err - } - - windowsLayer, err := lifecycleLayer(windowsUrl, "windows") - if err != nil { - return nil, err - } - - windowsDiffID, err := windowsLayer.DiffID() - if err != nil { - return nil, err - } - - image, err = imagehelpers.SetStringLabel(image, "windows", windowsDiffID.String()) - if err != nil { - return nil, err - } - - image, err = mutate.AppendLayers(image, linuxLayer, windowsLayer) - if err != nil { - return nil, err - } - - return imagehelpers.SetLabels(image, map[string]interface{}{ - lifecycleMetadataLabel: lifecycleDescriptorToMetadata(linuxDescriptor), - }) -} - -func lifecycleDescriptor(url string) (cnb.LifecycleDescriptor, error) { - lr, err := lifecycleReader(url) - if err != nil { - return cnb.LifecycleDescriptor{}, err - } - defer lr.Close() - tr := tar.NewReader(lr) - for { - header, err := tr.Next() - if err != nil { - break - } - - name := header.Name - if name == "lifecycle.toml" { - descriptor := cnb.LifecycleDescriptor{} - if _, err := toml.NewDecoder(tr).Decode(&descriptor); err != nil { - return cnb.LifecycleDescriptor{}, err - } - return descriptor, nil - } - - continue - } - return cnb.LifecycleDescriptor{}, errors.New("could not find lifecycle descriptor lifecyle.toml") -} - -func lifecycleDescriptorToMetadata(descriptor cnb.LifecycleDescriptor) cnb.LifecycleMetadata { - return cnb.LifecycleMetadata{ - LifecycleInfo: descriptor.Info, - API: descriptor.API, - APIs: descriptor.APIs, - } -} - -func lifecycleLayer(url, os string) (v1.Layer, error) { - b := &bytes.Buffer{} - tw := newLayerWriter(b, os) - - err := tw.WriteHeader(&tar.Header{ - Typeflag: tar.TypeDir, - Name: lifecycleLocation, - Mode: 0755, - ModTime: normalizedTime, - }) - if err != nil { - return nil, err - } - - var regex = regexp.MustCompile(`^[^/]+/([^/]+)$`) - - lr, err := lifecycleReader(url) - if err != nil { - return nil, err - } - defer lr.Close() - tr := tar.NewReader(lr) - for { - header, err := tr.Next() - if err != nil { - break - } - - pathMatches := regex.FindStringSubmatch(path.Clean(header.Name)) - if pathMatches != nil { - binaryName := pathMatches[1] - - header.Name = lifecycleLocation + binaryName - header.ModTime = normalizedTime - err = tw.WriteHeader(header) - if err != nil { - return nil, err - } - - buf, err := io.ReadAll(tr) - if err != nil { - return nil, err - } - - _, err = tw.Write(buf) - if err != nil { - return nil, err - } - } - - } - - return tarball.LayerFromReader(b) -} - -func lifecycleReader(url string) (io.ReadCloser, error) { - dir, err := os.MkdirTemp("", "lifecycle") - if err != nil { - return nil, err - } - - lifecycleLocation := dir + "/lifecycle.tgz" - - err = download(lifecycleLocation, url) - if err != nil { - return nil, err - } - - file, err := os.Open(lifecycleLocation) - if err != nil { - return nil, err - } - - gzr, err := gzip.NewReader(file) - - return &ReadCloserWrapper{ - Reader: gzr, - closer: func() error { - defer os.RemoveAll(dir) - defer file.Close() - return gzr.Close() - }, - }, err -} - -func download(filepath string, url string) error { - resp, err := http.Get(url) - if err != nil { - return err - } - defer resp.Body.Close() - - out, err := os.Create(filepath) - if err != nil { - return err - } - defer out.Close() - - _, err = io.Copy(out, resp.Body) - return err -} - -// copied from github.com/docker/docker/pkg/ioutils -type ReadCloserWrapper struct { - io.Reader - closer func() error -} - -func (r *ReadCloserWrapper) Close() error { - return r.closer() -} - -func newLayerWriter(fileWriter io.Writer, os string) layerWriter { - if os == "windows" { - return layer.NewWindowsWriter(fileWriter) - } - return tar.NewWriter(fileWriter) -} - -type layerWriter interface { - WriteHeader(hdr *tar.Header) error - Write(b []byte) (int, error) - Close() error -} diff --git a/pkg/apis/build/v1alpha2/build_types.go b/pkg/apis/build/v1alpha2/build_types.go index b7254e1b8..651577c3a 100644 --- a/pkg/apis/build/v1alpha2/build_types.go +++ b/pkg/apis/build/v1alpha2/build_types.go @@ -132,6 +132,8 @@ type BuildStatus struct { corev1alpha1.Status `json:",inline"` BuildMetadata corev1alpha1.BuildpackMetadataList `json:"buildMetadata,omitempty"` Stack corev1alpha1.BuildStack `json:"stack,omitempty"` + LifecycleVersion string `json:"lifecycleVersion"` + LifecycleCommit string `json:"lifecycleCommit"` LatestImage string `json:"latestImage,omitempty"` LatestCacheImage string `json:"latestCacheImage,omitempty"` LatestAttestationImage string `json:"latestAttestationImage,omitempty"` diff --git a/pkg/apis/build/v1alpha2/builder_lifecycle.go b/pkg/apis/build/v1alpha2/builder_lifecycle.go index c2913fc1f..3f4dbfd2c 100644 --- a/pkg/apis/build/v1alpha2/builder_lifecycle.go +++ b/pkg/apis/build/v1alpha2/builder_lifecycle.go @@ -11,6 +11,7 @@ import ( type BuilderRecord struct { Image string Stack corev1alpha1.BuildStack + Lifecycle ResolvedClusterLifecycle Buildpacks corev1alpha1.BuildpackMetadataList Order []corev1alpha1.OrderEntry ObservedStoreGeneration int64 @@ -21,6 +22,7 @@ type BuilderRecord struct { func (bs *BuilderStatus) BuilderRecord(record BuilderRecord) { bs.Stack = record.Stack + bs.Lifecycle = record.Lifecycle bs.BuilderMetadata = record.Buildpacks bs.LatestImage = record.Image bs.Conditions = corev1alpha1.Conditions{ diff --git a/pkg/apis/build/v1alpha2/builder_resource.go b/pkg/apis/build/v1alpha2/builder_resource.go index f34c64abb..4c8ed3aac 100644 --- a/pkg/apis/build/v1alpha2/builder_resource.go +++ b/pkg/apis/build/v1alpha2/builder_resource.go @@ -10,6 +10,7 @@ type BuilderResource interface { UpToDate() bool BuildpackMetadata() corev1alpha1.BuildpackMetadataList RunImage() string + LifecycleVersion() string GetKind() string ConditionReadyMessage() string } diff --git a/pkg/apis/build/v1alpha2/builder_types.go b/pkg/apis/build/v1alpha2/builder_types.go index 48bccde5c..c19edc246 100644 --- a/pkg/apis/build/v1alpha2/builder_types.go +++ b/pkg/apis/build/v1alpha2/builder_types.go @@ -34,7 +34,9 @@ type Builder struct { type BuilderSpec struct { Tag string `json:"tag,omitempty"` Stack corev1.ObjectReference `json:"stack,omitempty"` - Store corev1.ObjectReference `json:"store,omitempty"` + // TODO: confirm, should `json:"lifecycle,...` actually be `json:"clusterlifecycle,...`? + Lifecycle corev1.ObjectReference `json:"lifecycle,omitempty"` + Store corev1.ObjectReference `json:"store,omitempty"` // +listType Order []BuilderOrderEntry `json:"order,omitempty"` AdditionalLabels map[string]string `json:"additionalLabels,omitempty"` @@ -72,6 +74,7 @@ type BuilderStatus struct { BuilderMetadata corev1alpha1.BuildpackMetadataList `json:"builderMetadata,omitempty"` Order []corev1alpha1.OrderEntry `json:"order,omitempty"` Stack corev1alpha1.BuildStack `json:"stack,omitempty"` + Lifecycle ResolvedClusterLifecycle `json:"lifecycle,omitempty"` LatestImage string `json:"latestImage,omitempty"` ObservedStackGeneration int64 `json:"observedStackGeneration,omitempty"` ObservedStoreGeneration int64 `json:"observedStoreGeneration,omitempty"` diff --git a/pkg/apis/build/v1alpha2/cluster_lifecycle_types.go b/pkg/apis/build/v1alpha2/cluster_lifecycle_types.go new file mode 100644 index 000000000..8d9847b6c --- /dev/null +++ b/pkg/apis/build/v1alpha2/cluster_lifecycle_types.go @@ -0,0 +1,81 @@ +package v1alpha2 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" +) + +const ( + ClusterLifecycleKind = "ClusterLifecycle" + ClusterLifecycleCRName = "clusterlifecycles.kpack.io" +) + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object,k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMetaAccessor + +// +k8s:openapi-gen=true +type ClusterLifecycle struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ClusterLifecycleSpec `json:"spec"` + Status ClusterLifecycleStatus `json:"status"` +} + +// +k8s:openapi-gen=true +type ClusterLifecycleSpec struct { + // +listType + corev1alpha1.ImageSource `json:",inline"` + ServiceAccountRef *corev1.ObjectReference `json:"serviceAccountRef,omitempty"` +} + +// +k8s:openapi-gen=true +type ClusterLifecycleStatus struct { + corev1alpha1.Status `json:",inline"` + ResolvedClusterLifecycle `json:",inline"` +} + +// +k8s:openapi-gen=true +type ResolvedClusterLifecycle struct { + Version string `json:"version,omitempty"` + + // Deprecated: Use `LifecycleAPIs` instead + API LifecycleAPI `json:"api,omitempty"` + APIs LifecycleAPIs `json:"apis,omitempty"` +} + +type LifecycleAPI struct { + BuildpackVersion string `toml:"buildpack" json:"buildpack,omitempty"` + PlatformVersion string `toml:"platform" json:"platform,omitempty"` +} + +type LifecycleAPIs struct { + Buildpack APIVersions `toml:"buildpack" json:"buildpack"` + Platform APIVersions `toml:"platform" json:"platform"` +} + +type APIVersions struct { + Deprecated APISet `toml:"deprecated" json:"deprecated"` + Supported APISet `toml:"supported" json:"supported"` +} + +type APISet []string + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// +k8s:openapi-gen=true +type ClusterLifecycleList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + // +k8s:listType=atomic + Items []ClusterLifecycle `json:"items"` +} + +func (*ClusterLifecycle) GetGroupVersionKind() schema.GroupVersionKind { + return SchemeGroupVersion.WithKind(ClusterLifecycleKind) +} diff --git a/pkg/apis/build/v1alpha2/cluster_lifecycle_validation.go b/pkg/apis/build/v1alpha2/cluster_lifecycle_validation.go new file mode 100644 index 000000000..a94a11eb1 --- /dev/null +++ b/pkg/apis/build/v1alpha2/cluster_lifecycle_validation.go @@ -0,0 +1,30 @@ +package v1alpha2 + +import ( + "context" + + "knative.dev/pkg/apis" + + "github.com/pivotal/kpack/pkg/apis/validate" +) + +func (cl *ClusterLifecycle) SetDefaults(context.Context) { + // TODO: confirm, should we add something here? +} + +func (cl *ClusterLifecycle) Validate(ctx context.Context) *apis.FieldError { + return cl.Spec.Validate(ctx).ViaField("spec") +} + +func (cls *ClusterLifecycleSpec) Validate(ctx context.Context) *apis.FieldError { + if cls.ServiceAccountRef != nil { + if cls.ServiceAccountRef.Name == "" { + return apis.ErrMissingField("name").ViaField("serviceAccountRef") + } + if cls.ServiceAccountRef.Namespace == "" { + return apis.ErrMissingField("namespace").ViaField("serviceAccountRef") + } + } + + return validate.FieldNotEmpty(cls.Image, "image") +} diff --git a/pkg/apis/build/v1alpha2/image_builds.go b/pkg/apis/build/v1alpha2/image_builds.go index 728f77faa..d45a56ca4 100644 --- a/pkg/apis/build/v1alpha2/image_builds.go +++ b/pkg/apis/build/v1alpha2/image_builds.go @@ -29,6 +29,7 @@ const ( BuildReasonCommit = "COMMIT" BuildReasonBuildpack = "BUILDPACK" BuildReasonStack = "STACK" + BuildReasonLifecycle = "LIFECYCLE" BuildReasonTrigger = "TRIGGER" ) diff --git a/pkg/apis/build/v1alpha2/image_builds_test.go b/pkg/apis/build/v1alpha2/image_builds_test.go index d4dd42924..caa934c08 100644 --- a/pkg/apis/build/v1alpha2/image_builds_test.go +++ b/pkg/apis/build/v1alpha2/image_builds_test.go @@ -371,15 +371,16 @@ func testImageBuilds(t *testing.T, when spec.G, it spec.S) { } type TestBuilderResource struct { - BuilderReady bool - BuilderUpToDate bool - BuilderMetadata []corev1alpha1.BuildpackMetadata - ImagePullSecrets []corev1.LocalObjectReference - Kind string - LatestImage string - LatestRunImage string - Name string - Namespace string + BuilderReady bool + BuilderUpToDate bool + BuilderMetadata []corev1alpha1.BuildpackMetadata + ImagePullSecrets []corev1.LocalObjectReference + Kind string + LatestImage string + LatestRunImage string + LatestLifecycleVersion string + Name string + Namespace string } func (t TestBuilderResource) ConditionReadyMessage() string { @@ -409,6 +410,10 @@ func (t TestBuilderResource) RunImage() string { return t.LatestRunImage } +func (t TestBuilderResource) LifecycleVersion() string { + return t.LatestLifecycleVersion +} + func (t TestBuilderResource) GetName() string { return t.Name } diff --git a/pkg/apis/build/v1alpha2/register.go b/pkg/apis/build/v1alpha2/register.go index 8c42a86e8..2124023a9 100644 --- a/pkg/apis/build/v1alpha2/register.go +++ b/pkg/apis/build/v1alpha2/register.go @@ -59,6 +59,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &SourceResolverList{}, &ClusterStack{}, &ClusterStackList{}, + &ClusterLifecycle{}, + &ClusterLifecycleList{}, &ClusterStore{}, &ClusterStoreList{}, &ClusterBuildpack{}, diff --git a/pkg/apis/build/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/build/v1alpha2/zz_generated.deepcopy.go index 1411d8cf9..d5bccc3ab 100644 --- a/pkg/apis/build/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/build/v1alpha2/zz_generated.deepcopy.go @@ -28,6 +28,52 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in APISet) DeepCopyInto(out *APISet) { + { + in := &in + *out = make(APISet, len(*in)) + copy(*out, *in) + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APISet. +func (in APISet) DeepCopy() APISet { + if in == nil { + return nil + } + out := new(APISet) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIVersions) DeepCopyInto(out *APIVersions) { + *out = *in + if in.Deprecated != nil { + in, out := &in.Deprecated, &out.Deprecated + *out = make(APISet, len(*in)) + copy(*out, *in) + } + if in.Supported != nil { + in, out := &in.Supported, &out.Supported + *out = make(APISet, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIVersions. +func (in *APIVersions) DeepCopy() *APIVersions { + if in == nil { + return nil + } + out := new(APIVersions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Build) DeepCopyInto(out *Build) { *out = *in @@ -456,6 +502,7 @@ func (in *BuilderOrderEntry) DeepCopy() *BuilderOrderEntry { func (in *BuilderRecord) DeepCopyInto(out *BuilderRecord) { *out = *in out.Stack = in.Stack + in.Lifecycle.DeepCopyInto(&out.Lifecycle) if in.Buildpacks != nil { in, out := &in.Buildpacks, &out.Buildpacks *out = make(v1alpha1.BuildpackMetadataList, len(*in)) @@ -490,6 +537,7 @@ func (in *BuilderRecord) DeepCopy() *BuilderRecord { func (in *BuilderSpec) DeepCopyInto(out *BuilderSpec) { *out = *in out.Stack = in.Stack + out.Lifecycle = in.Lifecycle out.Store = in.Store if in.Order != nil { in, out := &in.Order, &out.Order @@ -498,6 +546,13 @@ func (in *BuilderSpec) DeepCopyInto(out *BuilderSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.AdditionalLabels != nil { + in, out := &in.AdditionalLabels, &out.AdditionalLabels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } return } @@ -528,6 +583,7 @@ func (in *BuilderStatus) DeepCopyInto(out *BuilderStatus) { } } out.Stack = in.Stack + in.Lifecycle.DeepCopyInto(&out.Lifecycle) if in.SignaturePaths != nil { in, out := &in.SignaturePaths, &out.SignaturePaths *out = make([]CosignSignature, len(*in)) @@ -858,6 +914,115 @@ func (in *ClusterBuildpackStatus) DeepCopy() *ClusterBuildpackStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterLifecycle) DeepCopyInto(out *ClusterLifecycle) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterLifecycle. +func (in *ClusterLifecycle) DeepCopy() *ClusterLifecycle { + if in == nil { + return nil + } + out := new(ClusterLifecycle) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObjectMetaAccessor is an autogenerated deepcopy function, copying the receiver, creating a new metav1.ObjectMetaAccessor. +func (in *ClusterLifecycle) DeepCopyObjectMetaAccessor() metav1.ObjectMetaAccessor { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterLifecycle) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterLifecycleList) DeepCopyInto(out *ClusterLifecycleList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterLifecycle, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterLifecycleList. +func (in *ClusterLifecycleList) DeepCopy() *ClusterLifecycleList { + if in == nil { + return nil + } + out := new(ClusterLifecycleList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterLifecycleList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterLifecycleSpec) DeepCopyInto(out *ClusterLifecycleSpec) { + *out = *in + out.ImageSource = in.ImageSource + if in.ServiceAccountRef != nil { + in, out := &in.ServiceAccountRef, &out.ServiceAccountRef + *out = new(v1.ObjectReference) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterLifecycleSpec. +func (in *ClusterLifecycleSpec) DeepCopy() *ClusterLifecycleSpec { + if in == nil { + return nil + } + out := new(ClusterLifecycleSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterLifecycleStatus) DeepCopyInto(out *ClusterLifecycleStatus) { + *out = *in + in.Status.DeepCopyInto(&out.Status) + in.ResolvedClusterLifecycle.DeepCopyInto(&out.ResolvedClusterLifecycle) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterLifecycleStatus. +func (in *ClusterLifecycleStatus) DeepCopy() *ClusterLifecycleStatus { + if in == nil { + return nil + } + out := new(ClusterLifecycleStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterStack) DeepCopyInto(out *ClusterStack) { *out = *in @@ -1449,6 +1614,40 @@ func (in *LastBuild) DeepCopy() *LastBuild { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LifecycleAPI) DeepCopyInto(out *LifecycleAPI) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LifecycleAPI. +func (in *LifecycleAPI) DeepCopy() *LifecycleAPI { + if in == nil { + return nil + } + out := new(LifecycleAPI) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LifecycleAPIs) DeepCopyInto(out *LifecycleAPIs) { + *out = *in + in.Buildpack.DeepCopyInto(&out.Buildpack) + in.Platform.DeepCopyInto(&out.Platform) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LifecycleAPIs. +func (in *LifecycleAPIs) DeepCopy() *LifecycleAPIs { + if in == nil { + return nil + } + out := new(LifecycleAPIs) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NamespacedBuilderSpec) DeepCopyInto(out *NamespacedBuilderSpec) { *out = *in @@ -1482,6 +1681,24 @@ func (in *RegistryCache) DeepCopy() *RegistryCache { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResolvedClusterLifecycle) DeepCopyInto(out *ResolvedClusterLifecycle) { + *out = *in + out.API = in.API + in.APIs.DeepCopyInto(&out.APIs) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResolvedClusterLifecycle. +func (in *ResolvedClusterLifecycle) DeepCopy() *ResolvedClusterLifecycle { + if in == nil { + return nil + } + out := new(ResolvedClusterLifecycle) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResolvedClusterStack) DeepCopyInto(out *ResolvedClusterStack) { *out = *in diff --git a/pkg/buildchange/lifecycle_change.go b/pkg/buildchange/lifecycle_change.go new file mode 100644 index 000000000..bebb6cd0f --- /dev/null +++ b/pkg/buildchange/lifecycle_change.go @@ -0,0 +1,30 @@ +package buildchange + +import ( + buildapi "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" +) + +func NewLifecycleChange(oldLifecycle, newLifecycle string) Change { + return lifecycleChange{ + oldLifecycle: oldLifecycle, + newLifecycle: newLifecycle, + } +} + +type lifecycleChange struct { + oldLifecycle string + newLifecycle string + err error +} + +func (l lifecycleChange) Reason() buildapi.BuildReason { return buildapi.BuildReasonLifecycle } + +func (l lifecycleChange) IsBuildRequired() (bool, error) { + return l.oldLifecycle != l.newLifecycle, l.err +} + +func (l lifecycleChange) Old() interface{} { return l.oldLifecycle } + +func (l lifecycleChange) New() interface{} { return l.newLifecycle } + +func (l lifecycleChange) Priority() buildapi.BuildPriority { return buildapi.BuildPriorityLow } diff --git a/pkg/client/clientset/versioned/typed/build/v1alpha2/build_client.go b/pkg/client/clientset/versioned/typed/build/v1alpha2/build_client.go index 4cc70b2c6..e3ce9fb97 100644 --- a/pkg/client/clientset/versioned/typed/build/v1alpha2/build_client.go +++ b/pkg/client/clientset/versioned/typed/build/v1alpha2/build_client.go @@ -33,6 +33,7 @@ type KpackV1alpha2Interface interface { BuildpacksGetter ClusterBuildersGetter ClusterBuildpacksGetter + ClusterLifecyclesGetter ClusterStacksGetter ClusterStoresGetter ImagesGetter @@ -64,6 +65,10 @@ func (c *KpackV1alpha2Client) ClusterBuildpacks() ClusterBuildpackInterface { return newClusterBuildpacks(c) } +func (c *KpackV1alpha2Client) ClusterLifecycles() ClusterLifecycleInterface { + return newClusterLifecycles(c) +} + func (c *KpackV1alpha2Client) ClusterStacks() ClusterStackInterface { return newClusterStacks(c) } diff --git a/pkg/client/clientset/versioned/typed/build/v1alpha2/clusterlifecycle.go b/pkg/client/clientset/versioned/typed/build/v1alpha2/clusterlifecycle.go new file mode 100644 index 000000000..be82d7589 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/build/v1alpha2/clusterlifecycle.go @@ -0,0 +1,184 @@ +/* + * Copyright 2019 The original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "context" + "time" + + v1alpha2 "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + scheme "github.com/pivotal/kpack/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// ClusterLifecyclesGetter has a method to return a ClusterLifecycleInterface. +// A group's client should implement this interface. +type ClusterLifecyclesGetter interface { + ClusterLifecycles() ClusterLifecycleInterface +} + +// ClusterLifecycleInterface has methods to work with ClusterLifecycle resources. +type ClusterLifecycleInterface interface { + Create(ctx context.Context, clusterLifecycle *v1alpha2.ClusterLifecycle, opts v1.CreateOptions) (*v1alpha2.ClusterLifecycle, error) + Update(ctx context.Context, clusterLifecycle *v1alpha2.ClusterLifecycle, opts v1.UpdateOptions) (*v1alpha2.ClusterLifecycle, error) + UpdateStatus(ctx context.Context, clusterLifecycle *v1alpha2.ClusterLifecycle, opts v1.UpdateOptions) (*v1alpha2.ClusterLifecycle, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha2.ClusterLifecycle, error) + List(ctx context.Context, opts v1.ListOptions) (*v1alpha2.ClusterLifecycleList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha2.ClusterLifecycle, err error) + ClusterLifecycleExpansion +} + +// clusterLifecycles implements ClusterLifecycleInterface +type clusterLifecycles struct { + client rest.Interface +} + +// newClusterLifecycles returns a ClusterLifecycles +func newClusterLifecycles(c *KpackV1alpha2Client) *clusterLifecycles { + return &clusterLifecycles{ + client: c.RESTClient(), + } +} + +// Get takes name of the clusterLifecycle, and returns the corresponding clusterLifecycle object, and an error if there is any. +func (c *clusterLifecycles) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha2.ClusterLifecycle, err error) { + result = &v1alpha2.ClusterLifecycle{} + err = c.client.Get(). + Resource("clusterlifecycles"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of ClusterLifecycles that match those selectors. +func (c *clusterLifecycles) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha2.ClusterLifecycleList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1alpha2.ClusterLifecycleList{} + err = c.client.Get(). + Resource("clusterlifecycles"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested clusterLifecycles. +func (c *clusterLifecycles) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Resource("clusterlifecycles"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a clusterLifecycle and creates it. Returns the server's representation of the clusterLifecycle, and an error, if there is any. +func (c *clusterLifecycles) Create(ctx context.Context, clusterLifecycle *v1alpha2.ClusterLifecycle, opts v1.CreateOptions) (result *v1alpha2.ClusterLifecycle, err error) { + result = &v1alpha2.ClusterLifecycle{} + err = c.client.Post(). + Resource("clusterlifecycles"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(clusterLifecycle). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a clusterLifecycle and updates it. Returns the server's representation of the clusterLifecycle, and an error, if there is any. +func (c *clusterLifecycles) Update(ctx context.Context, clusterLifecycle *v1alpha2.ClusterLifecycle, opts v1.UpdateOptions) (result *v1alpha2.ClusterLifecycle, err error) { + result = &v1alpha2.ClusterLifecycle{} + err = c.client.Put(). + Resource("clusterlifecycles"). + Name(clusterLifecycle.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(clusterLifecycle). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *clusterLifecycles) UpdateStatus(ctx context.Context, clusterLifecycle *v1alpha2.ClusterLifecycle, opts v1.UpdateOptions) (result *v1alpha2.ClusterLifecycle, err error) { + result = &v1alpha2.ClusterLifecycle{} + err = c.client.Put(). + Resource("clusterlifecycles"). + Name(clusterLifecycle.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(clusterLifecycle). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the clusterLifecycle and deletes it. Returns an error if one occurs. +func (c *clusterLifecycles) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Resource("clusterlifecycles"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *clusterLifecycles) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Resource("clusterlifecycles"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched clusterLifecycle. +func (c *clusterLifecycles) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha2.ClusterLifecycle, err error) { + result = &v1alpha2.ClusterLifecycle{} + err = c.client.Patch(pt). + Resource("clusterlifecycles"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/client/clientset/versioned/typed/build/v1alpha2/fake/fake_build_client.go b/pkg/client/clientset/versioned/typed/build/v1alpha2/fake/fake_build_client.go index 6e91f1358..8c610335b 100644 --- a/pkg/client/clientset/versioned/typed/build/v1alpha2/fake/fake_build_client.go +++ b/pkg/client/clientset/versioned/typed/build/v1alpha2/fake/fake_build_client.go @@ -48,6 +48,10 @@ func (c *FakeKpackV1alpha2) ClusterBuildpacks() v1alpha2.ClusterBuildpackInterfa return &FakeClusterBuildpacks{c} } +func (c *FakeKpackV1alpha2) ClusterLifecycles() v1alpha2.ClusterLifecycleInterface { + return &FakeClusterLifecycles{c} +} + func (c *FakeKpackV1alpha2) ClusterStacks() v1alpha2.ClusterStackInterface { return &FakeClusterStacks{c} } diff --git a/pkg/client/clientset/versioned/typed/build/v1alpha2/fake/fake_clusterlifecycle.go b/pkg/client/clientset/versioned/typed/build/v1alpha2/fake/fake_clusterlifecycle.go new file mode 100644 index 000000000..0a5e79cf3 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/build/v1alpha2/fake/fake_clusterlifecycle.go @@ -0,0 +1,133 @@ +/* + * Copyright 2019 The original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1alpha2 "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeClusterLifecycles implements ClusterLifecycleInterface +type FakeClusterLifecycles struct { + Fake *FakeKpackV1alpha2 +} + +var clusterlifecyclesResource = schema.GroupVersionResource{Group: "kpack.io", Version: "v1alpha2", Resource: "clusterlifecycles"} + +var clusterlifecyclesKind = schema.GroupVersionKind{Group: "kpack.io", Version: "v1alpha2", Kind: "ClusterLifecycle"} + +// Get takes name of the clusterLifecycle, and returns the corresponding clusterLifecycle object, and an error if there is any. +func (c *FakeClusterLifecycles) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha2.ClusterLifecycle, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootGetAction(clusterlifecyclesResource, name), &v1alpha2.ClusterLifecycle{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ClusterLifecycle), err +} + +// List takes label and field selectors, and returns the list of ClusterLifecycles that match those selectors. +func (c *FakeClusterLifecycles) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha2.ClusterLifecycleList, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootListAction(clusterlifecyclesResource, clusterlifecyclesKind, opts), &v1alpha2.ClusterLifecycleList{}) + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha2.ClusterLifecycleList{ListMeta: obj.(*v1alpha2.ClusterLifecycleList).ListMeta} + for _, item := range obj.(*v1alpha2.ClusterLifecycleList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested clusterLifecycles. +func (c *FakeClusterLifecycles) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewRootWatchAction(clusterlifecyclesResource, opts)) +} + +// Create takes the representation of a clusterLifecycle and creates it. Returns the server's representation of the clusterLifecycle, and an error, if there is any. +func (c *FakeClusterLifecycles) Create(ctx context.Context, clusterLifecycle *v1alpha2.ClusterLifecycle, opts v1.CreateOptions) (result *v1alpha2.ClusterLifecycle, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootCreateAction(clusterlifecyclesResource, clusterLifecycle), &v1alpha2.ClusterLifecycle{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ClusterLifecycle), err +} + +// Update takes the representation of a clusterLifecycle and updates it. Returns the server's representation of the clusterLifecycle, and an error, if there is any. +func (c *FakeClusterLifecycles) Update(ctx context.Context, clusterLifecycle *v1alpha2.ClusterLifecycle, opts v1.UpdateOptions) (result *v1alpha2.ClusterLifecycle, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateAction(clusterlifecyclesResource, clusterLifecycle), &v1alpha2.ClusterLifecycle{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ClusterLifecycle), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeClusterLifecycles) UpdateStatus(ctx context.Context, clusterLifecycle *v1alpha2.ClusterLifecycle, opts v1.UpdateOptions) (*v1alpha2.ClusterLifecycle, error) { + obj, err := c.Fake. + Invokes(testing.NewRootUpdateSubresourceAction(clusterlifecyclesResource, "status", clusterLifecycle), &v1alpha2.ClusterLifecycle{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ClusterLifecycle), err +} + +// Delete takes name of the clusterLifecycle and deletes it. Returns an error if one occurs. +func (c *FakeClusterLifecycles) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewRootDeleteActionWithOptions(clusterlifecyclesResource, name, opts), &v1alpha2.ClusterLifecycle{}) + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeClusterLifecycles) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewRootDeleteCollectionAction(clusterlifecyclesResource, listOpts) + + _, err := c.Fake.Invokes(action, &v1alpha2.ClusterLifecycleList{}) + return err +} + +// Patch applies the patch and returns the patched clusterLifecycle. +func (c *FakeClusterLifecycles) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha2.ClusterLifecycle, err error) { + obj, err := c.Fake. + Invokes(testing.NewRootPatchSubresourceAction(clusterlifecyclesResource, name, pt, data, subresources...), &v1alpha2.ClusterLifecycle{}) + if obj == nil { + return nil, err + } + return obj.(*v1alpha2.ClusterLifecycle), err +} diff --git a/pkg/client/clientset/versioned/typed/build/v1alpha2/generated_expansion.go b/pkg/client/clientset/versioned/typed/build/v1alpha2/generated_expansion.go index 198507aa7..d2346ffda 100644 --- a/pkg/client/clientset/versioned/typed/build/v1alpha2/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/build/v1alpha2/generated_expansion.go @@ -28,6 +28,8 @@ type ClusterBuilderExpansion interface{} type ClusterBuildpackExpansion interface{} +type ClusterLifecycleExpansion interface{} + type ClusterStackExpansion interface{} type ClusterStoreExpansion interface{} diff --git a/pkg/client/informers/externalversions/build/v1alpha2/clusterlifecycle.go b/pkg/client/informers/externalversions/build/v1alpha2/clusterlifecycle.go new file mode 100644 index 000000000..0a454cdf7 --- /dev/null +++ b/pkg/client/informers/externalversions/build/v1alpha2/clusterlifecycle.go @@ -0,0 +1,89 @@ +/* + * Copyright 2019 The original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + "context" + time "time" + + buildv1alpha2 "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + versioned "github.com/pivotal/kpack/pkg/client/clientset/versioned" + internalinterfaces "github.com/pivotal/kpack/pkg/client/informers/externalversions/internalinterfaces" + v1alpha2 "github.com/pivotal/kpack/pkg/client/listers/build/v1alpha2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// ClusterLifecycleInformer provides access to a shared informer and lister for +// ClusterLifecycles. +type ClusterLifecycleInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha2.ClusterLifecycleLister +} + +type clusterLifecycleInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewClusterLifecycleInformer constructs a new informer for ClusterLifecycle type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewClusterLifecycleInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredClusterLifecycleInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredClusterLifecycleInformer constructs a new informer for ClusterLifecycle type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredClusterLifecycleInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KpackV1alpha2().ClusterLifecycles().List(context.TODO(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.KpackV1alpha2().ClusterLifecycles().Watch(context.TODO(), options) + }, + }, + &buildv1alpha2.ClusterLifecycle{}, + resyncPeriod, + indexers, + ) +} + +func (f *clusterLifecycleInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredClusterLifecycleInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *clusterLifecycleInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&buildv1alpha2.ClusterLifecycle{}, f.defaultInformer) +} + +func (f *clusterLifecycleInformer) Lister() v1alpha2.ClusterLifecycleLister { + return v1alpha2.NewClusterLifecycleLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/build/v1alpha2/interface.go b/pkg/client/informers/externalversions/build/v1alpha2/interface.go index 97f4916c5..4dfda503a 100644 --- a/pkg/client/informers/externalversions/build/v1alpha2/interface.go +++ b/pkg/client/informers/externalversions/build/v1alpha2/interface.go @@ -34,6 +34,8 @@ type Interface interface { ClusterBuilders() ClusterBuilderInformer // ClusterBuildpacks returns a ClusterBuildpackInformer. ClusterBuildpacks() ClusterBuildpackInformer + // ClusterLifecycles returns a ClusterLifecycleInformer. + ClusterLifecycles() ClusterLifecycleInformer // ClusterStacks returns a ClusterStackInformer. ClusterStacks() ClusterStackInformer // ClusterStores returns a ClusterStoreInformer. @@ -80,6 +82,11 @@ func (v *version) ClusterBuildpacks() ClusterBuildpackInformer { return &clusterBuildpackInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} } +// ClusterLifecycles returns a ClusterLifecycleInformer. +func (v *version) ClusterLifecycles() ClusterLifecycleInformer { + return &clusterLifecycleInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} + // ClusterStacks returns a ClusterStackInformer. func (v *version) ClusterStacks() ClusterStackInformer { return &clusterStackInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index 00a5f8a24..12a93c471 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -80,6 +80,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Kpack().V1alpha2().ClusterBuilders().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("clusterbuildpacks"): return &genericInformer{resource: resource.GroupResource(), informer: f.Kpack().V1alpha2().ClusterBuildpacks().Informer()}, nil + case v1alpha2.SchemeGroupVersion.WithResource("clusterlifecycles"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Kpack().V1alpha2().ClusterLifecycles().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("clusterstacks"): return &genericInformer{resource: resource.GroupResource(), informer: f.Kpack().V1alpha2().ClusterStacks().Informer()}, nil case v1alpha2.SchemeGroupVersion.WithResource("clusterstores"): diff --git a/pkg/client/listers/build/v1alpha2/clusterlifecycle.go b/pkg/client/listers/build/v1alpha2/clusterlifecycle.go new file mode 100644 index 000000000..ea380641e --- /dev/null +++ b/pkg/client/listers/build/v1alpha2/clusterlifecycle.go @@ -0,0 +1,68 @@ +/* + * Copyright 2019 The original author or authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha2 + +import ( + v1alpha2 "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// ClusterLifecycleLister helps list ClusterLifecycles. +// All objects returned here must be treated as read-only. +type ClusterLifecycleLister interface { + // List lists all ClusterLifecycles in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*v1alpha2.ClusterLifecycle, err error) + // Get retrieves the ClusterLifecycle from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*v1alpha2.ClusterLifecycle, error) + ClusterLifecycleListerExpansion +} + +// clusterLifecycleLister implements the ClusterLifecycleLister interface. +type clusterLifecycleLister struct { + indexer cache.Indexer +} + +// NewClusterLifecycleLister returns a new ClusterLifecycleLister. +func NewClusterLifecycleLister(indexer cache.Indexer) ClusterLifecycleLister { + return &clusterLifecycleLister{indexer: indexer} +} + +// List lists all ClusterLifecycles in the indexer. +func (s *clusterLifecycleLister) List(selector labels.Selector) (ret []*v1alpha2.ClusterLifecycle, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha2.ClusterLifecycle)) + }) + return ret, err +} + +// Get retrieves the ClusterLifecycle from the index for a given name. +func (s *clusterLifecycleLister) Get(name string) (*v1alpha2.ClusterLifecycle, error) { + obj, exists, err := s.indexer.GetByKey(name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha2.Resource("clusterlifecycle"), name) + } + return obj.(*v1alpha2.ClusterLifecycle), nil +} diff --git a/pkg/client/listers/build/v1alpha2/expansion_generated.go b/pkg/client/listers/build/v1alpha2/expansion_generated.go index e369f428b..75027dde3 100644 --- a/pkg/client/listers/build/v1alpha2/expansion_generated.go +++ b/pkg/client/listers/build/v1alpha2/expansion_generated.go @@ -50,6 +50,10 @@ type ClusterBuilderListerExpansion interface{} // ClusterBuildpackLister. type ClusterBuildpackListerExpansion interface{} +// ClusterLifecycleListerExpansion allows custom methods to be added to +// ClusterLifecycleLister. +type ClusterLifecycleListerExpansion interface{} + // ClusterStackListerExpansion allows custom methods to be added to // ClusterStackLister. type ClusterStackListerExpansion interface{} diff --git a/pkg/cnb/build_metadata.go b/pkg/cnb/build_metadata.go index 44cd4d2c3..a2d9b0927 100644 --- a/pkg/cnb/build_metadata.go +++ b/pkg/cnb/build_metadata.go @@ -25,6 +25,8 @@ type BuildMetadata struct { LatestImage string `json:"latestImage"` StackID string `json:"stackID"` StackRunImage string `json:"stackRunImage"` + LifecycleVersion string `json:"lifecycleVersion"` + LifecycleCommit string `json:"lifecycleCommit"` } type ImageFetcher interface { @@ -48,6 +50,7 @@ func (r *RemoteMetadataRetriever) GetBuildMetadata(builtImageRef, cacheTag strin LatestCacheImage: cacheImageRef, StackRunImage: buildImg.stack.RunImage, StackID: buildImg.stack.ID, + LifecycleVersion: buildImg.lifecycle.version, }, nil } @@ -107,6 +110,7 @@ func readBuiltImage(appImage ggcrv1.Image, appImageId string) (builtImage, error RunImage: baseImageRef.Context().String() + "@" + runImageRef.Identifier(), ID: stackId, }, + lifecycle: builtImageLifecycle{version: buildMetadata.Launcher.Version}, }, nil } @@ -114,6 +118,11 @@ type builtImage struct { identifier string buildpackMetadata []lifecyclebuildpack.GroupElement stack builtImageStack + lifecycle builtImageLifecycle +} + +type builtImageLifecycle struct { + version string } type appLayersMetadata struct { diff --git a/pkg/cnb/builder_builder.go b/pkg/cnb/builder_builder.go index f43883191..b125a65f3 100644 --- a/pkg/cnb/builder_builder.go +++ b/pkg/cnb/builder_builder.go @@ -51,6 +51,8 @@ type builderBlder struct { runImage string mixins []string os string + arch string + archVariant string additionalLabels map[string]string } @@ -67,6 +69,8 @@ func (bb *builderBlder) AddStack(baseImage v1.Image, clusterStack *buildapi.Clus } bb.os = file.OS + bb.arch = file.Architecture + bb.archVariant = file.Variant bb.baseImage = baseImage bb.stackId = clusterStack.Status.Id bb.mixins = clusterStack.Status.Mixins diff --git a/pkg/cnb/buildpack_metadata.go b/pkg/cnb/buildpack_metadata.go index ea2555b81..dbdeb50da 100644 --- a/pkg/cnb/buildpack_metadata.go +++ b/pkg/cnb/buildpack_metadata.go @@ -61,7 +61,7 @@ type LifecycleMetadata struct { } type LifecycleDescriptor struct { - Info LifecycleInfo `toml:"lifecycle"` + Info LifecycleInfo `toml:"lifecycle" json:"lifecycle,omitempty"` // Deprecated: Use `LifecycleAPIs` instead API LifecycleAPI `toml:"api" json:"api,omitempty"` diff --git a/pkg/cnb/create_builder.go b/pkg/cnb/create_builder.go index 09bb2eee0..cd86886ce 100644 --- a/pkg/cnb/create_builder.go +++ b/pkg/cnb/create_builder.go @@ -2,6 +2,7 @@ package cnb import ( "context" + "errors" "fmt" "github.com/google/go-containerregistry/pkg/authn" @@ -11,7 +12,6 @@ import ( buildapi "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" "github.com/pivotal/kpack/pkg/cosign" - "github.com/pivotal/kpack/pkg/registry" ) type RegistryClient interface { @@ -19,29 +19,24 @@ type RegistryClient interface { Save(keychain authn.Keychain, tag string, image ggcrv1.Image) (string, error) } -type LifecycleProvider interface { - LayerForOS(os string) (ggcrv1.Layer, LifecycleMetadata, error) -} - type RemoteBuilderCreator struct { - RegistryClient RegistryClient - LifecycleProvider LifecycleProvider - KpackVersion string - KeychainFactory registry.KeychainFactory - ImageSigner cosign.BuilderSigner + RegistryClient RegistryClient + KpackVersion string + ImageSigner cosign.BuilderSigner } func (r *RemoteBuilderCreator) CreateBuilder( ctx context.Context, builderKeychain authn.Keychain, stackKeychain authn.Keychain, + lifecycleKeychain authn.Keychain, fetcher RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, + clusterLifecycle *buildapi.ClusterLifecycle, spec buildapi.BuilderSpec, serviceAccountSecrets []*corev1.Secret, resolvedBuilderRef string, ) (buildapi.BuilderRecord, error) { - buildImage, _, err := r.RegistryClient.Fetch(stackKeychain, clusterStack.Status.BuildImage.LatestImage) if err != nil { return buildapi.BuilderRecord{}, err @@ -50,6 +45,10 @@ func (r *RemoteBuilderCreator) CreateBuilder( if err != nil { return buildapi.BuilderRecord{}, err } + lifecycleImage, _, err := r.RegistryClient.Fetch(lifecycleKeychain, clusterLifecycle.Spec.Image) // TODO: confirm, should there be a "latest image" on the status? + if err != nil { + return buildapi.BuilderRecord{}, err + } builderBldr := newBuilderBldr(r.KpackVersion) @@ -64,11 +63,10 @@ func (r *RemoteBuilderCreator) CreateBuilder( return buildapi.BuilderRecord{}, err } - lifecycleLayer, lifecycleMetadata, err := r.LifecycleProvider.LayerForOS(builderBldr.os) + lifecycleLayer, lifecycleMetadata, err := layerForOS(clusterLifecycle, lifecycleImage, builderBldr) if err != nil { return buildapi.BuilderRecord{}, err } - builderBldr.AddLifecycle(lifecycleLayer, lifecycleMetadata) for _, group := range spec.Order { @@ -119,6 +117,11 @@ func (r *RemoteBuilderCreator) CreateBuilder( RunImage: relocatedRunImage, ID: clusterStack.Status.Id, }, + Lifecycle: buildapi.ResolvedClusterLifecycle{ + Version: clusterLifecycle.Status.ResolvedClusterLifecycle.Version, + API: clusterLifecycle.Status.ResolvedClusterLifecycle.API, + APIs: clusterLifecycle.Status.ResolvedClusterLifecycle.APIs, + }, Buildpacks: buildpackMetadata(builderBldr.buildpacks()), Order: builderBldr.order, ObservedStackGeneration: clusterStack.Status.ObservedGeneration, @@ -130,6 +133,88 @@ func (r *RemoteBuilderCreator) CreateBuilder( return builder, nil } +func layerForOS(clusterLifecycle *buildapi.ClusterLifecycle, lifecycleImage ggcrv1.Image, builderBlder *builderBlder) (lifecycleLayer ggcrv1.Layer, lifecycleMetadata LifecycleMetadata, err error) { + lifecycleMetadata = LifecycleMetadata{ + LifecycleInfo: LifecycleInfo{ + Version: clusterLifecycle.Status.ResolvedClusterLifecycle.Version, + }, + API: LifecycleAPI{ + BuildpackVersion: clusterLifecycle.Status.ResolvedClusterLifecycle.API.BuildpackVersion, + PlatformVersion: clusterLifecycle.Status.ResolvedClusterLifecycle.API.PlatformVersion, + }, + APIs: LifecycleAPIs{ + Buildpack: APIVersions{ + Deprecated: toCNBAPISet(clusterLifecycle.Status.ResolvedClusterLifecycle.APIs.Buildpack.Deprecated), + Supported: toCNBAPISet(clusterLifecycle.Status.ResolvedClusterLifecycle.APIs.Buildpack.Supported), + }, + Platform: APIVersions{ + Deprecated: toCNBAPISet(clusterLifecycle.Status.ResolvedClusterLifecycle.APIs.Platform.Deprecated), + Supported: toCNBAPISet(clusterLifecycle.Status.ResolvedClusterLifecycle.APIs.Platform.Supported), + }, + }, + } + + lifecycleLayer, err = func() (ggcrv1.Layer, error) { + manifest, err := lifecycleImage.Manifest() + if err != nil || manifest == nil { + return nil, fmt.Errorf("failed to get manifest file: %w", err) + } + if len(manifest.Layers) < 1 { + return nil, errors.New("failed to find lifecycle image layers") + } + lastLayerDigest := manifest.Layers[len(manifest.Layers)-1].Digest + return lifecycleImage.LayerByDigest(lastLayerDigest) + }() + if err != nil { + return nil, LifecycleMetadata{}, fmt.Errorf("failed to find lifecycle layer: %w", err) + } + + err = func() error { + cfg, err := lifecycleImage.ConfigFile() + if err != nil || cfg == nil { + return fmt.Errorf("failed to get config file: %w", err) + } + if !platformMatches( + builderBlder.os, builderBlder.arch, builderBlder.archVariant, + cfg.OS, cfg.Architecture, cfg.Variant, + ) { + return fmt.Errorf( + "validating lifecycle image %s: expected platform to be %s/%s/%s but got %s/%s/%s", + clusterLifecycle.Spec.Image, + builderBlder.os, builderBlder.arch, builderBlder.archVariant, + cfg.OS, cfg.Architecture, cfg.Variant, + ) + } + return nil + }() + if err != nil { + return nil, LifecycleMetadata{}, err + } + + return lifecycleLayer, lifecycleMetadata, nil +} + +func platformMatches(wantOS, wantArch, wantArchVariant string, gotOS, gotArch, gotArchVariant string) bool { + if wantOS != gotOS { + return false + } + if wantArch != "" && gotArch != "" && wantArch != gotArch { + return false + } + if wantArchVariant != "" && gotArchVariant != "" && wantArchVariant != gotArchVariant { + return false + } + return true +} + +func toCNBAPISet(from buildapi.APISet) APISet { + var to APISet + for _, f := range from { + to = append(to, f) + } + return to +} + func buildpackMetadata(buildpacks []DescriptiveBuildpackInfo) corev1alpha1.BuildpackMetadataList { m := make(corev1alpha1.BuildpackMetadataList, 0, len(buildpacks)) for _, b := range buildpacks { diff --git a/pkg/cnb/create_builder_test.go b/pkg/cnb/create_builder_test.go index cb3268aff..dc8ed1c8c 100644 --- a/pkg/cnb/create_builder_test.go +++ b/pkg/cnb/create_builder_test.go @@ -21,7 +21,6 @@ import ( buildapi "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" - "github.com/pivotal/kpack/pkg/registry" "github.com/pivotal/kpack/pkg/registry/imagehelpers" "github.com/pivotal/kpack/pkg/registry/registryfakes" ) @@ -46,6 +45,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { relocatedRunImageTag = "custom/example:test-builder-run-image" buildImageTag = "paketo-buildpacks/build:full-cnb" runImageTag = "paketo-buildpacks/run:full-cnb" + lifecycleImageTag = "buildpacksio/lifecycle:latest" buildImageLayers = 10 lifecycleImageLayers = 1 @@ -56,29 +56,16 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { var ( registryClient = registryfakes.NewFakeClient() - keychainFactory = ®istryfakes.FakeKeychainFactory{} - builderKeychain = authn.NewMultiKeychain(authn.DefaultKeychain) - stackKeychain = authn.NewMultiKeychain(authn.DefaultKeychain) - secretRef = registry.SecretRef{} - runImage = createRunImage(os) - runImageDigest = digest(runImage) - runImageRef = fmt.Sprintf("%s@%s", runImageTag, runImageDigest) - ctx = context.Background() + builderKeychain = authn.NewMultiKeychain(authn.DefaultKeychain) + stackKeychain = authn.NewMultiKeychain(authn.DefaultKeychain) + lifecycleKeychain = authn.NewMultiKeychain(authn.DefaultKeychain) + runImage = createRunImage(os) + runImageDigest = digest(runImage) + runImageRef = fmt.Sprintf("%s@%s", runImageTag, runImageDigest) + ctx = context.Background() fetcher = &fakeFetcher{buildpacks: map[string][]buildpackLayer{}, observedGeneration: 10} - linuxLifecycle = &fakeLayer{ - digest: "sha256:5d43d12dabe6070c4a4036e700a6f88a52278c02097b5f200e0b49b3d874c954", - diffID: "sha256:5d43d12dabe6070c4a4036e700a6f88a52278c02097b5f200e0b49b3d874c954", - size: 200, - } - - windowsLifecycle = &fakeLayer{ - digest: "sha256:e40a7455f5495621a585e68523ab66ad8a0b7c791f40bf3aa97c7858003c1287", - diffID: "sha256:e40a7455f5495621a585e68523ab66ad8a0b7c791f40bf3aa97c7858003c1287", - size: 200, - } - buildpack1Layer = &fakeLayer{ digest: "sha256:1bd8899667b8d1e6b124f663faca32903b470831e5e4e99265c839ab34628838", diffID: "sha256:1bf8899667b8d1e6b124f663faca32903b470831e5e4e992644ac5c839ab3462", @@ -129,6 +116,23 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }, } + clusterLifecycle = &buildapi.ClusterLifecycle{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sample-stack", + }, + Spec: buildapi.ClusterLifecycleSpec{ + ImageSource: corev1alpha1.ImageSource{Image: lifecycleImageTag}, + }, + Status: buildapi.ClusterLifecycleStatus{ + Status: corev1alpha1.Status{ + ObservedGeneration: 11, + }, + ResolvedClusterLifecycle: buildapi.ResolvedClusterLifecycle{ + Version: "some-version", + }, + }, + } + clusterBuilderSpec = buildapi.BuilderSpec{ Tag: builderTag, Stack: corev1.ObjectReference{ @@ -176,13 +180,9 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }, } - lifecycleProvider = &fakeLifecycleProvider{} - subject = RemoteBuilderCreator{ - RegistryClient: registryClient, - KpackVersion: "v1.2.3 (git sha: abcdefg123456)", - KeychainFactory: keychainFactory, - LifecycleProvider: lifecycleProvider, + RegistryClient: registryClient, + KpackVersion: "v1.2.3 (git sha: abcdefg123456)", ImageSigner: &fakeBuilderSigner{ signBuilder: func(ctx context.Context, s string, secrets []*corev1.Secret, keychain authn.Keychain) ([]buildapi.CosignSignature, error) { // no-op @@ -211,8 +211,6 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { ) it.Before(func() { - keychainFactory.AddKeychainForSecretRef(t, secretRef, builderKeychain) - buildpack1 := buildpackLayer{ v1Layer: buildpack1Layer, BuildpackInfo: DescriptiveBuildpackInfo{ @@ -233,7 +231,6 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }, }, } - buildpack2 := buildpackLayer{ v1Layer: buildpack2Layer, BuildpackInfo: DescriptiveBuildpackInfo{ @@ -316,12 +313,15 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { when("CreateBuilder", func() { var ( - buildImg v1.Image + buildImg v1.Image + lifecycleImg v1.Image ) it.Before(func() { var err error + // build image + buildImg, err = random.Image(1, int64(buildImageLayers)) require.NoError(t, err) @@ -333,33 +333,42 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { registryClient.AddImage(buildImage, buildImg, stackKeychain) - lifecycleProvider.metadata = LifecycleMetadata{ - LifecycleInfo: LifecycleInfo{ - Version: "0.5.0", - }, - API: LifecycleAPI{ + // lifecycle image + + lifecycleImg, err = random.Image(1, int64(lifecycleImageLayers)) + require.NoError(t, err) + + lConfig, err := lifecycleImg.ConfigFile() + require.NoError(t, err) + + lConfig.OS = os + lifecycleImg, err = mutate.ConfigFile(lifecycleImg, config) + + registryClient.AddImage(lifecycleImageTag, lifecycleImg, lifecycleKeychain) + + // cluster lifecycle + + clusterLifecycle.Status.ResolvedClusterLifecycle = buildapi.ResolvedClusterLifecycle{ + Version: "0.5.0", + API: buildapi.LifecycleAPI{ BuildpackVersion: "0.2", PlatformVersion: "0.1", }, - APIs: LifecycleAPIs{ - Buildpack: APIVersions{ + APIs: buildapi.LifecycleAPIs{ + Buildpack: buildapi.APIVersions{ Deprecated: []string{"0.2"}, Supported: []string{"0.3"}, }, - Platform: APIVersions{ + Platform: buildapi.APIVersions{ Deprecated: []string{"0.3"}, Supported: []string{"0.4"}, }, }, } - lifecycleProvider.layers = map[string]v1.Layer{ - "linux": linuxLifecycle, - "windows": windowsLifecycle, - } }) it("creates a custom builder with a relocated run image", func() { - builderRecord, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}, builderTag) + builderRecord, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, lifecycleKeychain, fetcher, stack, clusterLifecycle, clusterBuilderSpec, []*corev1.Secret{}, builderTag) require.NoError(t, err) assert.Len(t, builderRecord.Buildpacks, 4) @@ -371,6 +380,11 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { assert.Equal(t, int64(10), builderRecord.ObservedStoreGeneration) assert.Equal(t, int64(11), builderRecord.ObservedStackGeneration) assert.Equal(t, os, builderRecord.OS) + assert.Equal(t, builderRecord.Lifecycle, buildapi.ResolvedClusterLifecycle{ + Version: clusterLifecycle.Status.ResolvedClusterLifecycle.Version, + API: clusterLifecycle.Status.ResolvedClusterLifecycle.API, + APIs: clusterLifecycle.Status.ResolvedClusterLifecycle.APIs, + }) assert.Equal(t, builderRecord.Order, []corev1alpha1.OrderEntry{ { @@ -467,13 +481,16 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }) }) - layerTester.testNextLayer("Lifecycle Layer", func(index int) { - if os == "linux" { - assert.Equal(t, layers[index], linuxLifecycle) - } else { - assert.Equal(t, layers[index], windowsLifecycle) - } - }) + lifecycleImgManifest, err := lifecycleImg.Manifest() + require.NoError(t, err) + for i := 0; i < lifecycleImageLayers; i++ { + layerTester.testNextLayer("Lifecycle Layer", func(index int) { + lifecycleImgLayer, err := lifecycleImg.LayerByDigest(lifecycleImgManifest.Layers[i].Digest) + require.NoError(t, err) + + assert.Equal(t, layers[index+i], lifecycleImgLayer) + }) + } layerTester.testNextLayer("Largest Buildpack Layer", func(index int) { assert.Equal(t, layers[index], buildpack3Layer) @@ -660,11 +677,11 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }) it("creates images deterministically ", func() { - original, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}, builderTag) + original, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, lifecycleKeychain, fetcher, stack, clusterLifecycle, clusterBuilderSpec, []*corev1.Secret{}, builderTag) require.NoError(t, err) for i := 1; i <= 50; i++ { - other, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}, builderTag) + other, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, lifecycleKeychain, fetcher, stack, clusterLifecycle, clusterBuilderSpec, []*corev1.Secret{}, builderTag) require.NoError(t, err) @@ -673,6 +690,28 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { } }) + when("validating os", func() { + var wrongOS string + + it.Before(func() { + cfg, err := lifecycleImg.ConfigFile() + require.NoError(t, err) + wrongOS = "windows" + if os == "windows" { + wrongOS = "linux" + } + cfg.OS = wrongOS + lifecycleImg, err = mutate.ConfigFile(lifecycleImg, cfg) + require.NoError(t, err) + registryClient.AddImage(lifecycleImageTag, lifecycleImg, lifecycleKeychain) + }) + + it("errors with unsupported os", func() { + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, lifecycleKeychain, fetcher, stack, clusterLifecycle, clusterBuilderSpec, []*corev1.Secret{}, builderTag) + require.EqualError(t, err, fmt.Sprintf("validating lifecycle image %s: expected platform to be %s// but got %s//", lifecycleImageTag, os, wrongOS)) + }) + }) + when("validating buildpacks", func() { it("errors with unsupported stack", func() { addBuildpack(t, "io.buildpack.unsupported.stack", "v4", "buildpack.4.com", "0.2", @@ -695,7 +734,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }, } - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}, builderTag) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, lifecycleKeychain, fetcher, stack, clusterLifecycle, clusterBuilderSpec, []*corev1.Secret{}, builderTag) require.EqualError(t, err, "validating buildpack io.buildpack.unsupported.stack@v4: stack io.buildpacks.stacks.some-stack is not supported") }) @@ -719,25 +758,23 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }}, }} - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}, builderTag) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, lifecycleKeychain, fetcher, stack, clusterLifecycle, clusterBuilderSpec, []*corev1.Secret{}, builderTag) require.EqualError(t, err, "validating buildpack io.buildpack.unsupported.mixin@v4: stack missing mixin(s): something-missing-mixin, something-missing-mixin2") }) it("works with relaxed mixin contract", func() { - lifecycleProvider.metadata = LifecycleMetadata{ - LifecycleInfo: LifecycleInfo{ - Version: "0.5.0", - }, - API: LifecycleAPI{ + clusterLifecycle.Status.ResolvedClusterLifecycle = buildapi.ResolvedClusterLifecycle{ + Version: "0.5.0", + API: buildapi.LifecycleAPI{ BuildpackVersion: "0.2", PlatformVersion: "0.7", }, - APIs: LifecycleAPIs{ - Buildpack: APIVersions{ + APIs: buildapi.LifecycleAPIs{ + Buildpack: buildapi.APIVersions{ Deprecated: []string{"0.2"}, Supported: []string{"0.3"}, }, - Platform: APIVersions{ + Platform: buildapi.APIVersions{ Deprecated: []string{}, Supported: []string{relaxedMixinMinPlatformAPI}, }, @@ -764,7 +801,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }}, }} - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}, builderTag) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, lifecycleKeychain, fetcher, stack, clusterLifecycle, clusterBuilderSpec, []*corev1.Secret{}, builderTag) require.Nil(t, err) }) @@ -789,7 +826,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }}, }} - _, err := subject.CreateBuilder(ctx, builderKeychain, nil, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}, builderTag) + _, err := subject.CreateBuilder(ctx, builderKeychain, nil, nil, fetcher, stack, clusterLifecycle, clusterBuilderSpec, []*corev1.Secret{}, builderTag) require.Error(t, err, "validating buildpack io.buildpack.relaxed.old.mixin@v4: stack missing mixin(s): build:common-mixin, run:common-mixin, another-common-mixin") }) @@ -812,25 +849,23 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }}, }} - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}, builderTag) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, lifecycleKeychain, fetcher, stack, clusterLifecycle, clusterBuilderSpec, []*corev1.Secret{}, builderTag) require.EqualError(t, err, "validating buildpack io.buildpack.unsupported.buildpack.api@v4: unsupported buildpack api: 0.1, expecting: 0.2, 0.3") }) it("supports anystack buildpacks", func() { - lifecycleProvider.metadata = LifecycleMetadata{ - LifecycleInfo: LifecycleInfo{ - Version: "0.5.0", - }, - API: LifecycleAPI{ + clusterLifecycle.Status.ResolvedClusterLifecycle = buildapi.ResolvedClusterLifecycle{ + Version: "0.5.0", + API: buildapi.LifecycleAPI{ BuildpackVersion: "0.2", PlatformVersion: "0.1", }, - APIs: LifecycleAPIs{ - Buildpack: APIVersions{ + APIs: buildapi.LifecycleAPIs{ + Buildpack: buildapi.APIVersions{ Deprecated: []string{"0.2"}, Supported: []string{"0.3", "0.4", "0.5"}, }, - Platform: APIVersions{ + Platform: buildapi.APIVersions{ Deprecated: []string{"0.3"}, Supported: []string{"0.4"}, }, @@ -855,41 +890,39 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }}, }} - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}, builderTag) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, lifecycleKeychain, fetcher, stack, clusterLifecycle, clusterBuilderSpec, []*corev1.Secret{}, builderTag) require.NoError(t, err) }) }) when("validating platform api", func() { it("errors if no lifecycle platform api is supported", func() { - lifecycleProvider.metadata = LifecycleMetadata{ - LifecycleInfo: LifecycleInfo{ - Version: "0.5.0", - }, - API: LifecycleAPI{ + clusterLifecycle.Status.ResolvedClusterLifecycle = buildapi.ResolvedClusterLifecycle{ + Version: "0.5.0", + API: buildapi.LifecycleAPI{ BuildpackVersion: "0.2", PlatformVersion: "0.1", }, - APIs: LifecycleAPIs{ - Buildpack: APIVersions{ + APIs: buildapi.LifecycleAPIs{ + Buildpack: buildapi.APIVersions{ Deprecated: []string{"0.2"}, Supported: []string{"0.3"}, }, - Platform: APIVersions{ + Platform: buildapi.APIVersions{ Deprecated: []string{"0.1"}, Supported: []string{"0.2", "0.999"}, }, }, } - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}, builderTag) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, lifecycleKeychain, fetcher, stack, clusterLifecycle, clusterBuilderSpec, []*corev1.Secret{}, builderTag) require.EqualError(t, err, "unsupported platform apis in kpack lifecycle: 0.1, 0.2, 0.999, expecting one of: 0.3, 0.4, 0.5, 0.6, 0.7, 0.8") }) }) when("signing a builder image", func() { it("does not populate the signature paths when no secrets were present", func() { - builderRecord, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{}, builderTag) + builderRecord, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, lifecycleKeychain, fetcher, stack, clusterLifecycle, clusterBuilderSpec, []*corev1.Secret{}, builderTag) require.NoError(t, err) require.NotNil(t, builderRecord) require.Empty(t, builderRecord.SignaturePaths) @@ -909,7 +942,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }, } - _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{&fakeSecret}, builderTag) + _, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, lifecycleKeychain, fetcher, stack, clusterLifecycle, clusterBuilderSpec, []*corev1.Secret{&fakeSecret}, builderTag) require.Error(t, err) }) @@ -932,7 +965,7 @@ func testCreateBuilderOs(os string, t *testing.T, when spec.G, it spec.S) { }, } - builderRecord, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, fetcher, stack, clusterBuilderSpec, []*corev1.Secret{&fakeSecret}, builderTag) + builderRecord, err := subject.CreateBuilder(ctx, builderKeychain, stackKeychain, lifecycleKeychain, fetcher, stack, clusterLifecycle, clusterBuilderSpec, []*corev1.Secret{&fakeSecret}, builderTag) require.NoError(t, err) require.NotNil(t, builderRecord) require.NotEmpty(t, builderRecord.SignaturePaths) @@ -949,15 +982,6 @@ func (s *fakeBuilderSigner) SignBuilder(ctx context.Context, imageReference stri return s.signBuilder(ctx, imageReference, signingSecrets, builderKeychain) } -type fakeLifecycleProvider struct { - metadata LifecycleMetadata - layers map[string]v1.Layer -} - -func (p *fakeLifecycleProvider) LayerForOS(os string) (v1.Layer, LifecycleMetadata, error) { - return p.layers[os], p.metadata, nil -} - func buildpackInfoInLayers(buildpackLayers []buildpackLayer, id, version string) DescriptiveBuildpackInfo { for _, b := range buildpackLayers { if b.BuildpackInfo.Id == id && b.BuildpackInfo.Version == version { @@ -1063,24 +1087,6 @@ func (i *layerIteratorTester) testNextLayer(name string, test func(index int)) { *i++ } -func layerToRemoteBuildpack(bpLayer buildpackLayer, layer *fakeLayer, secretRef registry.SecretRef) K8sRemoteBuildpack { - return K8sRemoteBuildpack{ - Buildpack: corev1alpha1.BuildpackStatus{ - BuildpackInfo: corev1alpha1.BuildpackInfo{ - Id: bpLayer.BuildpackInfo.Id, - Version: bpLayer.BuildpackInfo.Version, - }, - DiffId: layer.diffID, - Digest: layer.digest, - Size: layer.size, - Homepage: bpLayer.BuildpackInfo.Homepage, - API: bpLayer.BuildpackLayerInfo.API, - Stacks: bpLayer.BuildpackLayerInfo.Stacks, - }, - SecretRef: secretRef, - } -} - func createRunImage(os string) v1.Image { runImg, _ := random.Image(1, int64(5)) diff --git a/pkg/cnb/remote_lifecycle_reader.go b/pkg/cnb/remote_lifecycle_reader.go new file mode 100644 index 000000000..4a4172d85 --- /dev/null +++ b/pkg/cnb/remote_lifecycle_reader.go @@ -0,0 +1,59 @@ +package cnb + +import ( + "github.com/google/go-containerregistry/pkg/authn" + + buildapi "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + "github.com/pivotal/kpack/pkg/registry/imagehelpers" +) + +const lifecycleBuilderMetadataLabel = "io.buildpacks.builder.metadata" + +type RemoteLifecycleReader struct { + RegistryClient RegistryClient +} + +func (r *RemoteLifecycleReader) Read(keychain authn.Keychain, clusterLifecycleSpec buildapi.ClusterLifecycleSpec) (buildapi.ResolvedClusterLifecycle, error) { + lifecycleImg, _, err := r.RegistryClient.Fetch(keychain, clusterLifecycleSpec.Image) + if err != nil { + return buildapi.ResolvedClusterLifecycle{}, err + } + + deprecatedLifecycleMD := LifecycleDescriptor{} + err = imagehelpers.GetLabel(lifecycleImg, lifecycleBuilderMetadataLabel, &deprecatedLifecycleMD) + if err != nil { + return buildapi.ResolvedClusterLifecycle{}, err + } + + lifecycleMD := LifecycleAPIs{} + err = imagehelpers.GetLabel(lifecycleImg, lifecycleApisLabel, &lifecycleMD) + if err != nil { + return buildapi.ResolvedClusterLifecycle{}, err + } + + return buildapi.ResolvedClusterLifecycle{ + Version: deprecatedLifecycleMD.Info.Version, + API: buildapi.LifecycleAPI{ + BuildpackVersion: deprecatedLifecycleMD.API.BuildpackVersion, + PlatformVersion: deprecatedLifecycleMD.API.PlatformVersion, + }, + APIs: buildapi.LifecycleAPIs{ + Buildpack: buildapi.APIVersions{ + Deprecated: toBuildAPISet(lifecycleMD.Buildpack.Deprecated), + Supported: toBuildAPISet(lifecycleMD.Buildpack.Supported), + }, + Platform: buildapi.APIVersions{ + Deprecated: toBuildAPISet(lifecycleMD.Platform.Deprecated), + Supported: toBuildAPISet(lifecycleMD.Platform.Supported), + }, + }, + }, nil +} + +func toBuildAPISet(from APISet) buildapi.APISet { + var to buildapi.APISet + for _, f := range from { + to = append(to, f) + } + return to +} diff --git a/pkg/cnb/remote_lifecycle_reader_test.go b/pkg/cnb/remote_lifecycle_reader_test.go new file mode 100644 index 000000000..745457448 --- /dev/null +++ b/pkg/cnb/remote_lifecycle_reader_test.go @@ -0,0 +1,79 @@ +package cnb_test + +import ( + "fmt" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/sclevine/spec" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + buildapi "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" + "github.com/pivotal/kpack/pkg/cnb" + "github.com/pivotal/kpack/pkg/registry/imagehelpers" + "github.com/pivotal/kpack/pkg/registry/registryfakes" +) + +func TestRemoteLifecycleReader(t *testing.T) { + spec.Run(t, "Test Lifecycle Reader", testRemoteLifecycleReader) +} + +func testRemoteLifecycleReader(t *testing.T, when spec.G, it spec.S) { + when("Remote Lifecycle Reader", func() { + const lifecycleTag = "gcr.io/image/lifecycle" + + var ( + fakeClient = registryfakes.NewFakeClient() + + expectedKeychain = authn.NewMultiKeychain(authn.DefaultKeychain) + remoteLifecycleReader = &cnb.RemoteLifecycleReader{ + RegistryClient: fakeClient, + } + ) + + it("returns lifecycle metadata", func() { + lifecycleImage := lifecycleImage(t, "some-version") + + fakeClient.AddImage(lifecycleTag, lifecycleImage, expectedKeychain) + + resolvedLifecycle, err := remoteLifecycleReader.Read(expectedKeychain, buildapi.ClusterLifecycleSpec{ + ImageSource: corev1alpha1.ImageSource{ + Image: lifecycleTag, + }, + }) + require.NoError(t, err) + + assert.Equal(t, buildapi.ResolvedClusterLifecycle{ + Version: "some-version", + API: buildapi.LifecycleAPI{ + BuildpackVersion: "0.7", + PlatformVersion: "0.7", + }, + APIs: buildapi.LifecycleAPIs{ + Buildpack: buildapi.APIVersions{Supported: buildapi.APISet{"0.7", "0.8", "0.9", "0.10", "0.11"}}, + Platform: buildapi.APIVersions{Supported: buildapi.APISet{"0.7", "0.8", "0.9", "0.10", "0.11", "0.12", "0.13"}}, + }, + }, resolvedLifecycle) + }) + }) +} + +func lifecycleImage(t *testing.T, version string) v1.Image { + image, err := random.Image(10, 10) + require.NoError(t, err) + + image, err = imagehelpers.SetStringLabels( + image, + map[string]string{ + "io.buildpacks.builder.metadata": fmt.Sprintf("{\"lifecycle\":{\"version\":\"%s\"},\"api\":{\"buildpack\":\"0.7\",\"platform\":\"0.7\"}}", version), + "io.buildpacks.lifecycle.apis": "{\"buildpack\":{\"deprecated\":[],\"supported\":[\"0.7\",\"0.8\",\"0.9\",\"0.10\",\"0.11\"]},\"platform\":{\"deprecated\":[],\"supported\":[\"0.7\",\"0.8\",\"0.9\",\"0.10\",\"0.11\",\"0.12\",\"0.13\"]}}", + }, + ) + require.NoError(t, err) + + return image +} diff --git a/pkg/config/lifecycle_provider.go b/pkg/config/lifecycle_provider.go deleted file mode 100644 index 2a75ab5c6..000000000 --- a/pkg/config/lifecycle_provider.go +++ /dev/null @@ -1,233 +0,0 @@ -package config - -import ( - "context" - "sync/atomic" - - "github.com/google/go-containerregistry/pkg/authn" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/pkg/errors" - corev1 "k8s.io/api/core/v1" - "knative.dev/pkg/system" - - "github.com/pivotal/kpack/pkg/cnb" - "github.com/pivotal/kpack/pkg/registry" - "github.com/pivotal/kpack/pkg/registry/imagehelpers" -) - -const ( - LifecycleConfigName = "lifecycle-image" - LifecycleConfigKey = "image" - serviceAccountNameKey = "serviceAccountRef.name" - serviceAccountNamespaceKey = "serviceAccountRef.namespace" - lifecycleMetadataLabel = "io.buildpacks.lifecycle.metadata" -) - -type RegistryClient interface { - Fetch(keychain authn.Keychain, repoName string) (v1.Image, string, error) -} - -type LifecycleProvider struct { - registryClient RegistryClient - keychainFactory registry.KeychainFactory - lifecycleData atomic.Value - handlers []func() -} - -func NewLifecycleProvider(client RegistryClient, keychainFactory registry.KeychainFactory) *LifecycleProvider { - return &LifecycleProvider{ - registryClient: client, - keychainFactory: keychainFactory, - } -} - -func (l *LifecycleProvider) Metadata() (cnb.LifecycleMetadata, error) { - lifecycle, err := l.lifecycle() - if err != nil { - return cnb.LifecycleMetadata{}, err - } - return lifecycle.metadata, nil -} - -func (l *LifecycleProvider) LayerForOS(os string) (v1.Layer, cnb.LifecycleMetadata, error) { - lifecycle, err := l.lifecycle() - if err != nil { - return nil, cnb.LifecycleMetadata{}, err - } - - switch os { - case "linux": - layer, err := lifecycle.linux.toLazyLayer(lifecycle.keychain) - return layer, lifecycle.metadata, err - case "windows": - layer, err := lifecycle.windows.toLazyLayer(lifecycle.keychain) - return layer, lifecycle.metadata, err - default: - return nil, cnb.LifecycleMetadata{}, errors.Errorf("unrecognized os %s", os) - } -} - -func (l *LifecycleProvider) UpdateImage(cm *corev1.ConfigMap) error { - lifecycle, err := l.read(context.Background(), cm) - if err != nil { - l.lifecycleData.Store(configmapRead{err: err}) - return err - } - - if l.isNewImage(lifecycle) { - l.callHandlers() - } - l.lifecycleData.Store(configmapRead{lifecycle: lifecycle}) - return nil -} - -func (l *LifecycleProvider) AddEventHandler(handler func()) { - l.handlers = append(l.handlers, handler) -} - -func (l *LifecycleProvider) read(ctx context.Context, cm *corev1.ConfigMap) (*lifecycle, error) { - imageRef, ok := cm.Data[LifecycleConfigKey] - if !ok { - return nil, errors.Errorf("%s config invalid", LifecycleConfigName) - } - - keychain, err := l.keychainFactory.KeychainForSecretRef(ctx, registry.SecretRef{ - ServiceAccount: cm.Data[serviceAccountNameKey], - Namespace: cm.Data[serviceAccountNamespaceKey], - }) - if err != nil { - return nil, errors.Wrapf(err, "fetching keychain to read lifecycle") - } - - img, _, err := l.registryClient.Fetch(keychain, imageRef) - if err != nil { - return nil, errors.Wrap(err, "failed to fetch lifecycle image") - } - - lifecycleMd := cnb.LifecycleMetadata{} - err = imagehelpers.GetLabel(img, lifecycleMetadataLabel, &lifecycleMd) - if err != nil { - return nil, err - } - - digest, err := img.Digest() - if err != nil { - return nil, err - } - - linuxLayer, err := lifecycleLayerForOS(imageRef, img, "linux") - if err != nil { - return nil, err - } - - windowsLayer, err := lifecycleLayerForOS(imageRef, img, "windows") - if err != nil { - return nil, err - } - - return &lifecycle{ - keychain: keychain, - digest: digest, - metadata: lifecycleMd, - linux: linuxLayer, - windows: windowsLayer, - }, nil -} - -func lifecycleLayerForOS(imageRef string, image v1.Image, os string) (*lifecycleLayer, error) { - diffId, err := imagehelpers.GetStringLabel(image, os) - if err != nil { - return nil, errors.Wrapf(err, "could not find lifecycle for os: %s", os) - } - - diffID, err := v1.NewHash(diffId) - if err != nil { - return nil, err - } - - layer, err := image.LayerByDiffID(diffID) - if err != nil { - return nil, err - } - - digest, err := layer.Digest() - if err != nil { - return nil, err - } - - size, err := layer.Size() - if err != nil { - return nil, err - } - - return &lifecycleLayer{ - Digest: digest.String(), - DiffId: diffID.String(), - Image: imageRef, - Size: size, - }, nil -} - -func (l *LifecycleProvider) isLifecycleLoaded() bool { - _, ok := l.lifecycleData.Load().(configmapRead) - return ok -} - -func (l *LifecycleProvider) lifecycle() (*lifecycle, error) { - d, ok := l.lifecycleData.Load().(configmapRead) - if !ok { - return nil, errors.Errorf("Error: lifecycle image has not been loaded from ConfigMap '%s' in namespace '%s'", LifecycleConfigName, system.Namespace()) - } - - return d.lifecycle, d.err -} - -func (l *LifecycleProvider) isNewImage(newLifecycle *lifecycle) bool { - if !l.isLifecycleLoaded() { - return true - } - - lifecycle, err := l.lifecycle() - if err != nil { - return false - } - - return lifecycle.digest.String() != newLifecycle.digest.String() -} - -func (l *LifecycleProvider) callHandlers() { - for _, cb := range l.handlers { - cb() - } -} - -type configmapRead struct { - lifecycle *lifecycle - err error -} - -type lifecycle struct { - digest v1.Hash - metadata cnb.LifecycleMetadata - linux *lifecycleLayer - windows *lifecycleLayer - keychain authn.Keychain -} - -type lifecycleLayer struct { - Digest string - DiffId string - Image string - Size int64 - Keychain authn.Keychain -} - -func (l *lifecycleLayer) toLazyLayer(keychain authn.Keychain) (v1.Layer, error) { - return imagehelpers.NewLazyMountableLayer(imagehelpers.LazyMountableLayerArgs{ - Digest: l.Digest, - DiffId: l.DiffId, - Image: l.Image, - Size: l.Size, - Keychain: keychain, - }) -} diff --git a/pkg/config/lifecycle_provider_test.go b/pkg/config/lifecycle_provider_test.go deleted file mode 100644 index 49a2fd601..000000000 --- a/pkg/config/lifecycle_provider_test.go +++ /dev/null @@ -1,228 +0,0 @@ -package config - -import ( - "testing" - - "github.com/google/go-containerregistry/pkg/authn" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/empty" - "github.com/google/go-containerregistry/pkg/v1/mutate" - "github.com/google/go-containerregistry/pkg/v1/random" - "github.com/google/go-containerregistry/pkg/v1/types" - "github.com/sclevine/spec" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - - "github.com/pivotal/kpack/pkg/cnb" - "github.com/pivotal/kpack/pkg/registry" - "github.com/pivotal/kpack/pkg/registry/imagehelpers" - "github.com/pivotal/kpack/pkg/registry/registryfakes" -) - -func TestProvider(t *testing.T) { - spec.Run(t, "LifecycleProvider", testProvider) -} - -func testProvider(t *testing.T, when spec.G, it spec.S) { - var ( - lifecycleMetadata = cnb.LifecycleMetadata{ - LifecycleInfo: cnb.LifecycleInfo{ - Version: "0.5.0", - }, - API: cnb.LifecycleAPI{ - BuildpackVersion: "0.2", - PlatformVersion: "0.1", - }, - APIs: cnb.LifecycleAPIs{ - Buildpack: cnb.APIVersions{ - Deprecated: []string{"0.2"}, - Supported: []string{"0.3"}, - }, - Platform: cnb.APIVersions{ - Deprecated: []string{"0.3"}, - Supported: []string{"0.4"}, - }, - }, - } - client = registryfakes.NewFakeClient() - keychain = authn.NewMultiKeychain(authn.DefaultKeychain) - lifecycleImgRef = "some-image" - lifecycleImg v1.Image - linuxLayer v1.Layer - windowsLayer v1.Layer - callBack *fakeCallback - keychainFactory = ®istryfakes.FakeKeychainFactory{} - p *LifecycleProvider - ) - - it.Before(func() { - linuxLayer = testLayer(t) - windowsLayer = testLayer(t) - lifecycleImg = generateLifecycleImage(t, lifecycleMetadata, linuxLayer, windowsLayer) - - keychainFactory.AddKeychainForSecretRef(t, registry.SecretRef{Namespace: "some-service-account-namespace", ServiceAccount: "some-service-account"}, keychain) - client.AddImage(lifecycleImgRef, lifecycleImg, keychain) - client.AddImage("some-other-lifecycle-image", generateLifecycleImage(t, lifecycleMetadata, testLayer(t), testLayer(t)), keychain) - - p = NewLifecycleProvider(client, keychainFactory) - callBack = &fakeCallback{} - p.AddEventHandler(callBack.callBack) - }) - - when("Update Image is called with a ConfigMap", func() { - it("calls all Handlers", func() { - require.Equal(t, callBack.called, 0) - - p.UpdateImage(&corev1.ConfigMap{ - Data: map[string]string{"image": lifecycleImgRef, "serviceAccountRef.name": "some-service-account", "serviceAccountRef.namespace": "some-service-account-namespace"}, - }) - require.Equal(t, callBack.called, 1) - - p.UpdateImage(&corev1.ConfigMap{ - Data: map[string]string{"image": "some-other-lifecycle-image", "serviceAccountRef.name": "some-service-account", "serviceAccountRef.namespace": "some-service-account-namespace"}, - }) - require.Equal(t, callBack.called, 2) - }) - - it("does not call Handlers when lifecycle image is not updated", func() { - require.Equal(t, callBack.called, 0) - - p.UpdateImage(&corev1.ConfigMap{ - Data: map[string]string{"image": lifecycleImgRef, "serviceAccountRef.name": "some-service-account", "serviceAccountRef.namespace": "some-service-account-namespace"}, - }) - require.Equal(t, callBack.called, 1) - - p.UpdateImage(&corev1.ConfigMap{ - Data: map[string]string{"image": lifecycleImgRef, "serviceAccountRef.name": "some-service-account", "serviceAccountRef.namespace": "some-service-account-namespace"}, - }) - require.Equal(t, callBack.called, 1) - }) - - it("does not call Handlers when updated lifecycle image is invalid", func() { - require.Equal(t, callBack.called, 0) - - p.UpdateImage(&corev1.ConfigMap{ - Data: map[string]string{"image": lifecycleImgRef, "serviceAccountRef.name": "some-service-account", "serviceAccountRef.namespace": "some-service-account-namespace"}, - }) - require.Equal(t, callBack.called, 1) - - p.UpdateImage(&corev1.ConfigMap{ - Data: map[string]string{"image": "some-invalid-image", "serviceAccountRef.name": "some-service-account", "serviceAccountRef.namespace": "some-service-account-namespace"}, - }) - require.Equal(t, callBack.called, 1) - - _, _, err := p.LayerForOS("linux") - require.Error(t, err) - }) - }) - - when("LayerForOS()", func() { - it.Before(func() { - p.UpdateImage(&corev1.ConfigMap{ - Data: map[string]string{"image": lifecycleImgRef, "serviceAccountRef.name": "some-service-account", "serviceAccountRef.namespace": "some-service-account-namespace"}, - }) - }) - - it("returns the linux layer as a lazy layer", func() { - layer, readMetadata, err := p.LayerForOS("linux") - require.NoError(t, err) - require.Equal(t, readMetadata, lifecycleMetadata) - - expectedDigest, err := linuxLayer.Digest() - require.NoError(t, err) - - expectedDiffID, err := linuxLayer.DiffID() - require.NoError(t, err) - - expectedSize, err := linuxLayer.Size() - require.NoError(t, err) - - expectedLayer, err := imagehelpers.NewLazyMountableLayer(imagehelpers.LazyMountableLayerArgs{ - Digest: expectedDigest.String(), - DiffId: expectedDiffID.String(), - Image: lifecycleImgRef, - Size: expectedSize, - Keychain: keychain, - }) - require.NoError(t, err) - - require.Equal(t, expectedLayer, layer) - - }) - - it("returns the windows layer as a lazy layer", func() { - layer, readMetadata, err := p.LayerForOS("windows") - require.NoError(t, err) - require.Equal(t, readMetadata, lifecycleMetadata) - - expectedDigest, err := windowsLayer.Digest() - require.NoError(t, err) - - expectedDiffID, err := windowsLayer.DiffID() - require.NoError(t, err) - - expectedSize, err := windowsLayer.Size() - require.NoError(t, err) - - expectedLayer, err := imagehelpers.NewLazyMountableLayer(imagehelpers.LazyMountableLayerArgs{ - Digest: expectedDigest.String(), - DiffId: expectedDiffID.String(), - Image: lifecycleImgRef, - Size: expectedSize, - Keychain: keychain, - }) - require.NoError(t, err) - - require.Equal(t, expectedLayer, layer) - }) - - it("returns error on invalid os", func() { - _, _, err := p.LayerForOS("kpack-invalid-test-os") - require.EqualError(t, err, "unrecognized os kpack-invalid-test-os") - }) - - it("can return the metadata by itself", func() { - readMetadata, err := p.Metadata() - require.NoError(t, err) - require.Equal(t, readMetadata, lifecycleMetadata) - }) - }) -} - -type fakeCallback struct { - called int -} - -func (cb *fakeCallback) callBack() { - cb.called++ -} - -func generateLifecycleImage(t *testing.T, metadata cnb.LifecycleMetadata, linuxLifecycle, windowsLifecycle v1.Layer) v1.Image { - lifecycleImg, err := mutate.AppendLayers(empty.Image, linuxLifecycle, windowsLifecycle) - require.NoError(t, err) - - linuxDiffID, err := linuxLifecycle.DiffID() - require.NoError(t, err) - - lifecycleImg, err = imagehelpers.SetStringLabel(lifecycleImg, "linux", linuxDiffID.String()) - require.NoError(t, err) - - windowsDiffId, err := windowsLifecycle.DiffID() - require.NoError(t, err) - - lifecycleImg, err = imagehelpers.SetStringLabel(lifecycleImg, "windows", windowsDiffId.String()) - require.NoError(t, err) - - lifecycleImg, err = imagehelpers.SetLabels(lifecycleImg, map[string]interface{}{ - lifecycleMetadataLabel: metadata, - }) - require.NoError(t, err) - - return lifecycleImg -} - -func testLayer(t *testing.T) v1.Layer { - linuxLifecycle, err := random.Layer(10, types.DockerLayer) - require.NoError(t, err) - return linuxLifecycle -} diff --git a/pkg/duckbuilder/duck_builder.go b/pkg/duckbuilder/duck_builder.go index db0c21bc5..617e6fa91 100644 --- a/pkg/duckbuilder/duck_builder.go +++ b/pkg/duckbuilder/duck_builder.go @@ -38,8 +38,8 @@ func (b *DuckBuilder) Ready() bool { } func (b *DuckBuilder) UpToDate() bool { - return b.Status.GetCondition(buildapi.ConditionUpToDate).IsTrue() && - (b.Generation == b.Status.ObservedGeneration) + return b.Status.GetCondition(buildapi.ConditionUpToDate).IsTrue() && + (b.Generation == b.Status.ObservedGeneration) } func (b *DuckBuilder) BuildBuilderSpec() corev1alpha1.BuildBuilderSpec { @@ -57,6 +57,10 @@ func (b *DuckBuilder) RunImage() string { return b.Status.Stack.RunImage } +func (b *DuckBuilder) LifecycleVersion() string { + return b.Status.Lifecycle.Version +} + func (b *DuckBuilder) ConditionReadyMessage() string { condition := b.Status.GetCondition(corev1alpha1.ConditionReady) if condition == nil { diff --git a/pkg/openapi/openapi_generated.go b/pkg/openapi/openapi_generated.go index 84c8d8f76..d8d512da8 100644 --- a/pkg/openapi/openapi_generated.go +++ b/pkg/openapi/openapi_generated.go @@ -89,6 +89,10 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ClusterBuildpackList": schema_pkg_apis_build_v1alpha2_ClusterBuildpackList(ref), "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ClusterBuildpackSpec": schema_pkg_apis_build_v1alpha2_ClusterBuildpackSpec(ref), "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ClusterBuildpackStatus": schema_pkg_apis_build_v1alpha2_ClusterBuildpackStatus(ref), + "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ClusterLifecycle": schema_pkg_apis_build_v1alpha2_ClusterLifecycle(ref), + "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ClusterLifecycleList": schema_pkg_apis_build_v1alpha2_ClusterLifecycleList(ref), + "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ClusterLifecycleSpec": schema_pkg_apis_build_v1alpha2_ClusterLifecycleSpec(ref), + "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ClusterLifecycleStatus": schema_pkg_apis_build_v1alpha2_ClusterLifecycleStatus(ref), "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ClusterStack": schema_pkg_apis_build_v1alpha2_ClusterStack(ref), "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ClusterStackList": schema_pkg_apis_build_v1alpha2_ClusterStackList(ref), "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ClusterStackSpec": schema_pkg_apis_build_v1alpha2_ClusterStackSpec(ref), @@ -113,6 +117,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.LastBuild": schema_pkg_apis_build_v1alpha2_LastBuild(ref), "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.NamespacedBuilderSpec": schema_pkg_apis_build_v1alpha2_NamespacedBuilderSpec(ref), "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.RegistryCache": schema_pkg_apis_build_v1alpha2_RegistryCache(ref), + "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ResolvedClusterLifecycle": schema_pkg_apis_build_v1alpha2_ResolvedClusterLifecycle(ref), "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ResolvedClusterStack": schema_pkg_apis_build_v1alpha2_ResolvedClusterStack(ref), "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.SourceResolver": schema_pkg_apis_build_v1alpha2_SourceResolver(ref), "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.SourceResolverList": schema_pkg_apis_build_v1alpha2_SourceResolverList(ref), @@ -2384,6 +2389,20 @@ func schema_pkg_apis_build_v1alpha2_BuildStatus(ref common.ReferenceCallback) co Ref: ref("github.com/pivotal/kpack/pkg/apis/core/v1alpha1.BuildStack"), }, }, + "lifecycleVersion": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "lifecycleCommit": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, "latestImage": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, @@ -2396,6 +2415,12 @@ func schema_pkg_apis_build_v1alpha2_BuildStatus(ref common.ReferenceCallback) co Format: "", }, }, + "latestAttestationImage": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, "podName": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, @@ -2440,6 +2465,7 @@ func schema_pkg_apis_build_v1alpha2_BuildStatus(ref common.ReferenceCallback) co }, }, }, + Required: []string{"lifecycleVersion", "lifecycleCommit"}, }, }, Dependencies: []string{ @@ -2679,6 +2705,12 @@ func schema_pkg_apis_build_v1alpha2_BuilderSpec(ref common.ReferenceCallback) co Ref: ref("k8s.io/api/core/v1.ObjectReference"), }, }, + "lifecycle": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/api/core/v1.ObjectReference"), + }, + }, "store": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, @@ -2791,6 +2823,12 @@ func schema_pkg_apis_build_v1alpha2_BuilderStatus(ref common.ReferenceCallback) Ref: ref("github.com/pivotal/kpack/pkg/apis/core/v1alpha1.BuildStack"), }, }, + "lifecycle": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ResolvedClusterLifecycle"), + }, + }, "latestImage": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, @@ -2832,7 +2870,7 @@ func schema_pkg_apis_build_v1alpha2_BuilderStatus(ref common.ReferenceCallback) }, }, Dependencies: []string{ - "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.CosignSignature", "github.com/pivotal/kpack/pkg/apis/core/v1alpha1.BuildStack", "github.com/pivotal/kpack/pkg/apis/core/v1alpha1.BuildpackMetadata", "github.com/pivotal/kpack/pkg/apis/core/v1alpha1.Condition", "github.com/pivotal/kpack/pkg/apis/core/v1alpha1.OrderEntry"}, + "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.CosignSignature", "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ResolvedClusterLifecycle", "github.com/pivotal/kpack/pkg/apis/core/v1alpha1.BuildStack", "github.com/pivotal/kpack/pkg/apis/core/v1alpha1.BuildpackMetadata", "github.com/pivotal/kpack/pkg/apis/core/v1alpha1.Condition", "github.com/pivotal/kpack/pkg/apis/core/v1alpha1.OrderEntry"}, } } @@ -3127,6 +3165,12 @@ func schema_pkg_apis_build_v1alpha2_ClusterBuilderSpec(ref common.ReferenceCallb Ref: ref("k8s.io/api/core/v1.ObjectReference"), }, }, + "lifecycle": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/api/core/v1.ObjectReference"), + }, + }, "store": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, @@ -3359,6 +3403,192 @@ func schema_pkg_apis_build_v1alpha2_ClusterBuildpackStatus(ref common.ReferenceC } } +func schema_pkg_apis_build_v1alpha2_ClusterLifecycle(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ClusterLifecycleSpec"), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ClusterLifecycleStatus"), + }, + }, + }, + Required: []string{"spec", "status"}, + }, + }, + Dependencies: []string{ + "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ClusterLifecycleSpec", "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ClusterLifecycleStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_build_v1alpha2_ClusterLifecycleList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ClusterLifecycle"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.ClusterLifecycle", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_build_v1alpha2_ClusterLifecycleSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "image": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "serviceAccountRef": { + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/api/core/v1.ObjectReference"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "k8s.io/api/core/v1.ObjectReference"}, + } +} + +func schema_pkg_apis_build_v1alpha2_ClusterLifecycleStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "observedGeneration": { + SchemaProps: spec.SchemaProps{ + Description: "ObservedGeneration is the 'Generation' of the Service that was last processed by the controller.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "conditions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-patch-merge-key": "type", + "x-kubernetes-patch-strategy": "merge", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Conditions the latest available observations of a resource's current state.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/pivotal/kpack/pkg/apis/core/v1alpha1.Condition"), + }, + }, + }, + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "commit": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "api": { + SchemaProps: spec.SchemaProps{ + Description: "Deprecated: Use `LifecycleAPIs` instead", + Default: map[string]interface{}{}, + Ref: ref("github.com/pivotal/kpack/pkg/apis/build/v1alpha2.LifecycleAPI"), + }, + }, + "apis": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/pivotal/kpack/pkg/apis/build/v1alpha2.LifecycleAPIs"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.LifecycleAPI", "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.LifecycleAPIs", "github.com/pivotal/kpack/pkg/apis/core/v1alpha1.Condition"}, + } +} + func schema_pkg_apis_build_v1alpha2_ClusterStack(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -4453,6 +4683,12 @@ func schema_pkg_apis_build_v1alpha2_NamespacedBuilderSpec(ref common.ReferenceCa Ref: ref("k8s.io/api/core/v1.ObjectReference"), }, }, + "lifecycle": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/api/core/v1.ObjectReference"), + }, + }, "store": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, @@ -4532,6 +4768,45 @@ func schema_pkg_apis_build_v1alpha2_RegistryCache(ref common.ReferenceCallback) } } +func schema_pkg_apis_build_v1alpha2_ResolvedClusterLifecycle(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "version": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "commit": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "api": { + SchemaProps: spec.SchemaProps{ + Description: "Deprecated: Use `LifecycleAPIs` instead", + Default: map[string]interface{}{}, + Ref: ref("github.com/pivotal/kpack/pkg/apis/build/v1alpha2.LifecycleAPI"), + }, + }, + "apis": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/pivotal/kpack/pkg/apis/build/v1alpha2.LifecycleAPIs"), + }, + }, + }, + }, + }, + Dependencies: []string{ + "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.LifecycleAPI", "github.com/pivotal/kpack/pkg/apis/build/v1alpha2.LifecycleAPIs"}, + } +} + func schema_pkg_apis_build_v1alpha2_ResolvedClusterStack(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/reconciler/build/build.go b/pkg/reconciler/build/build.go index 033fa8dd7..921da3d3b 100644 --- a/pkg/reconciler/build/build.go +++ b/pkg/reconciler/build/build.go @@ -215,6 +215,8 @@ func (c *Reconciler) reconcile(ctx context.Context, build *buildapi.Build) error build.Status.LatestAttestationImage = attestDigest build.Status.Stack.RunImage = buildMetadata.StackRunImage build.Status.Stack.ID = buildMetadata.StackID + build.Status.LifecycleVersion = buildMetadata.LifecycleVersion + build.Status.LifecycleCommit = buildMetadata.LifecycleCommit } build.Status.PodName = pod.Name diff --git a/pkg/reconciler/builder/builder.go b/pkg/reconciler/builder/builder.go index 2592115f5..115a016ac 100644 --- a/pkg/reconciler/builder/builder.go +++ b/pkg/reconciler/builder/builder.go @@ -41,9 +41,11 @@ type BuilderCreator interface { CreateBuilder( ctx context.Context, builderKeychain authn.Keychain, - keychain authn.Keychain, + stackKeychain authn.Keychain, + lifecycleKeychain authn.Keychain, fetcher cnb.RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, + clusterLifecycle *buildapi.ClusterLifecycle, spec buildapi.BuilderSpec, serviceAccountSecrets []*corev1.Secret, resolvedBuilderRef string, @@ -64,8 +66,9 @@ func NewController( buildpackInformer buildinformers.BuildpackInformer, clusterBuildpackInformer buildinformers.ClusterBuildpackInformer, clusterStackInformer buildinformers.ClusterStackInformer, + clusterLifecycleInformer buildinformers.ClusterLifecycleInformer, secretFetcher Fetcher, -) (*controller.Impl, func()) { +) *controller.Impl { c := &Reconciler{ Client: opt.Client, BuilderLister: builderInformer.Lister(), @@ -75,6 +78,7 @@ func NewController( BuildpackLister: buildpackInformer.Lister(), ClusterBuildpackLister: clusterBuildpackInformer.Lister(), ClusterStackLister: clusterStackInformer.Lister(), + ClusterLifecycleLister: clusterLifecycleInformer.Lister(), SecretFetcher: secretFetcher, } @@ -102,6 +106,11 @@ func NewController( c.Tracker.OnChanged, buildapi.SchemeGroupVersion.WithKind(buildapi.ClusterStackKind)), )) + clusterLifecycleInformer.Informer().AddEventHandler(controller.HandleAll( + controller.EnsureTypeMeta( + c.Tracker.OnChanged, + buildapi.SchemeGroupVersion.WithKind(buildapi.ClusterLifecycleKind)), + )) buildpackInformer.Informer().AddEventHandler(controller.HandleAll( controller.EnsureTypeMeta( c.Tracker.OnChanged, @@ -113,9 +122,7 @@ func NewController( buildapi.SchemeGroupVersion.WithKind(buildapi.ClusterBuildpackKind)), )) - return impl, func() { - impl.GlobalResync(builderInformer.Informer()) - } + return impl } type Reconciler struct { @@ -128,6 +135,7 @@ type Reconciler struct { BuildpackLister buildlisters.BuildpackLister ClusterBuildpackLister buildlisters.ClusterBuildpackLister ClusterStackLister buildlisters.ClusterStackLister + ClusterLifecycleLister buildlisters.ClusterLifecycleLister SecretFetcher Fetcher } @@ -173,6 +181,17 @@ func (c *Reconciler) reconcileBuilder(ctx context.Context, builder *buildapi.Bui }, }, builder.NamespacedName()) + c.Tracker.Track(reconciler.Key{ + NamespacedName: types.NamespacedName{ + Name: builder.Spec.Lifecycle.Name, // TODO: confirm this is what we want + Namespace: metav1.NamespaceAll, + }, + GroupKind: schema.GroupKind{ + Group: "kpack.io", + Kind: buildapi.ClusterLifecycleKind, + }, + }, builder.NamespacedName()) + var ( clusterStore *buildapi.ClusterStore err error @@ -224,6 +243,15 @@ func (c *Reconciler) reconcileBuilder(ctx context.Context, builder *buildapi.Bui return buildapi.BuilderRecord{}, errors.Errorf("Error: clusterstack '%s' is not ready", clusterStack.Name) } + clusterLifecycle, err := c.ClusterLifecycleLister.Get(builder.Spec.Lifecycle.Name) // TODO: confirm this is what we want + if err != nil { + return buildapi.BuilderRecord{}, err + } + + if !clusterLifecycle.Status.GetCondition(corev1alpha1.ConditionReady).IsTrue() { + return buildapi.BuilderRecord{}, errors.Errorf("Error: clusterlifecycle '%s' is not ready", clusterLifecycle.Name) + } + builderKeychain, err := c.KeychainFactory.KeychainForSecretRef(ctx, registry.SecretRef{ ServiceAccount: builder.Spec.ServiceAccount(), Namespace: builder.Namespace, @@ -243,6 +271,17 @@ func (c *Reconciler) reconcileBuilder(ctx context.Context, builder *buildapi.Bui } } + lifecycleKeychain := builderKeychain + if clusterLifecycle.Spec.ServiceAccountRef != nil { + lifecycleKeychain, err = c.KeychainFactory.KeychainForSecretRef(ctx, registry.SecretRef{ + ServiceAccount: clusterLifecycle.Spec.ServiceAccountRef.Name, + Namespace: clusterLifecycle.Spec.ServiceAccountRef.Namespace, + }) + if err != nil { + return buildapi.BuilderRecord{}, err + } + } + fetcher := cnb.NewRemoteBuildpackFetcher(c.KeychainFactory, clusterStore, buildpacks, clusterBuildpacks) serviceAccountSecrets, err := c.SecretFetcher.SecretsForServiceAccount(ctx, builder.Spec.ServiceAccount(), builder.Namespace) @@ -259,8 +298,10 @@ func (c *Reconciler) reconcileBuilder(ctx context.Context, builder *buildapi.Bui ctx, builderKeychain, stackKeychain, + lifecycleKeychain, fetcher, clusterStack, + clusterLifecycle, builder.Spec.BuilderSpec, serviceAccountSecrets, resolvedBuilderRef, diff --git a/pkg/reconciler/builder/builder_test.go b/pkg/reconciler/builder/builder_test.go index 74f0ec7f5..f30725478 100644 --- a/pkg/reconciler/builder/builder_test.go +++ b/pkg/reconciler/builder/builder_test.go @@ -70,6 +70,7 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { BuildpackLister: listers.GetBuildpackLister(), ClusterBuildpackLister: listers.GetClusterBuildpackLister(), ClusterStackLister: listers.GetClusterStackLister(), + ClusterLifecycleLister: listers.GetClusterLifecycleLister(), SecretFetcher: fakeSecretFetcher, } return &kreconciler.NetworkErrorReconciler{Reconciler: r}, rtesting.ActionRecorderList{fakeClient, k8sfakeClient}, rtesting.EventList{Recorder: record.NewFakeRecorder(10)} @@ -133,6 +134,33 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { }, } + clusterLifecycle := &buildapi.ClusterLifecycle{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-lifecycle", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterLifecycle", + APIVersion: "kpack.io/v1alpha2", + }, + Spec: buildapi.ClusterLifecycleSpec{ + ServiceAccountRef: &corev1.ObjectReference{ + Name: "some-service-account", + Namespace: testNamespace, + }, + }, + Status: buildapi.ClusterLifecycleStatus{ + Status: corev1alpha1.Status{ + ObservedGeneration: 0, + Conditions: []corev1alpha1.Condition{ + { + Type: corev1alpha1.ConditionReady, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + } + buildpack := &buildapi.Buildpack{ ObjectMeta: metav1.ObjectMeta{ Name: "buildpack.id.3", @@ -167,6 +195,10 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { Kind: "Stack", Name: "some-stack", }, + Lifecycle: corev1.ObjectReference{ + Kind: "Lifecycle", + Name: "some-lifecycle", + }, Store: corev1.ObjectReference{ Kind: "ClusterStore", Name: "some-store", @@ -277,6 +309,7 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { rt.Test(rtesting.TableRow{ Key: builderKey, Objects: []runtime.Object{ + clusterLifecycle, clusterStack, clusterStore, builder, @@ -299,6 +332,7 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { StackKeychain: ®istryfakes.FakeKeychain{}, Fetcher: expectedFetcher, ClusterStack: clusterStack, + ClusterLifecycle: clusterLifecycle, BuilderSpec: builder.Spec.BuilderSpec, SigningSecrets: []*corev1.Secret{}, ResolvedBuilderTag: expectedResolvedTag, @@ -344,6 +378,7 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { rt.Test(rtesting.TableRow{ Key: builderKey, Objects: []runtime.Object{ + clusterLifecycle, clusterStack, clusterStore, buildpack, @@ -361,6 +396,9 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { require.True(t, fakeTracker.IsTracking( kreconciler.KeyForObject(clusterStack), builder.NamespacedName())) + require.True(t, fakeTracker.IsTracking( + kreconciler.KeyForObject(clusterLifecycle), + builder.NamespacedName())) require.True(t, fakeTracker.IsTrackingKind( kreconciler.KeyForObject(buildpack).GroupKind, @@ -415,6 +453,7 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { rt.Test(rtesting.TableRow{ Key: builderKey, Objects: []runtime.Object{ + clusterLifecycle, clusterStack, clusterStore, builder, @@ -431,6 +470,7 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { rt.Test(rtesting.TableRow{ Key: builderKey, Objects: []runtime.Object{ + clusterLifecycle, clusterStack, clusterStore, builder, @@ -622,6 +662,7 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { rt.Test(rtesting.TableRow{ Key: builderKey, Objects: []runtime.Object{ + clusterLifecycle, clusterStack, clusterStore, builder, @@ -664,6 +705,7 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { rt.Test(rtesting.TableRow{ Key: builderKey, Objects: []runtime.Object{ + clusterLifecycle, clusterStack, clusterStore, builder, @@ -705,6 +747,7 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { rt.Test(rtesting.TableRow{ Key: builderKey, Objects: []runtime.Object{ + clusterLifecycle, clusterStack, clusterStore, builder, diff --git a/pkg/reconciler/clusterbuilder/clusterbuilder.go b/pkg/reconciler/clusterbuilder/clusterbuilder.go index 20e670c9b..a7e52c6f2 100644 --- a/pkg/reconciler/clusterbuilder/clusterbuilder.go +++ b/pkg/reconciler/clusterbuilder/clusterbuilder.go @@ -41,8 +41,10 @@ type BuilderCreator interface { ctx context.Context, builderKeychain authn.Keychain, stackKeychain authn.Keychain, + lifecycleKeychain authn.Keychain, fetcher cnb.RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, + clusterLifecycle *buildapi.ClusterLifecycle, spec buildapi.BuilderSpec, serviceAccountSecrets []*corev1.Secret, resolvedBuilderRef string, @@ -62,8 +64,9 @@ func NewController( clusterStoreInformer buildinformers.ClusterStoreInformer, clusterBuildpackInformer buildinformers.ClusterBuildpackInformer, clusterStackInformer buildinformers.ClusterStackInformer, + clusterLifecycleInformer buildinformers.ClusterLifecycleInformer, secretFetcher Fetcher, -) (*controller.Impl, func()) { +) *controller.Impl { c := &Reconciler{ Client: opt.Client, ClusterBuilderLister: clusterBuilderInformer.Lister(), @@ -72,6 +75,7 @@ func NewController( ClusterStoreLister: clusterStoreInformer.Lister(), ClusterBuildpackLister: clusterBuildpackInformer.Lister(), ClusterStackLister: clusterStackInformer.Lister(), + ClusterLifecycleLister: clusterLifecycleInformer.Lister(), SecretFetcher: secretFetcher, } @@ -105,10 +109,13 @@ func NewController( c.Tracker.OnChanged, buildapi.SchemeGroupVersion.WithKind(buildapi.ClusterStackKind)), )) + clusterLifecycleInformer.Informer().AddEventHandler(controller.HandleAll( + controller.EnsureTypeMeta( + c.Tracker.OnChanged, + buildapi.SchemeGroupVersion.WithKind(buildapi.ClusterLifecycleKind)), + )) - return impl, func() { - impl.GlobalResync(clusterBuilderInformer.Informer()) - } + return impl } type Reconciler struct { @@ -120,6 +127,7 @@ type Reconciler struct { ClusterStoreLister buildlisters.ClusterStoreLister ClusterBuildpackLister buildlisters.ClusterBuildpackLister ClusterStackLister buildlisters.ClusterStackLister + ClusterLifecycleLister buildlisters.ClusterLifecycleLister SecretFetcher Fetcher } @@ -166,6 +174,17 @@ func (c *Reconciler) reconcileBuilder(ctx context.Context, builder *buildapi.Clu }, }, builder.NamespacedName()) + c.Tracker.Track(reconciler.Key{ + NamespacedName: types.NamespacedName{ + Name: builder.Spec.Lifecycle.Name, + Namespace: corev1.NamespaceAll, + }, + GroupKind: schema.GroupKind{ + Group: "kpack.io", + Kind: buildapi.ClusterLifecycleKind, + }, + }, builder.NamespacedName()) + var ( clusterStore *buildapi.ClusterStore err error @@ -207,6 +226,15 @@ func (c *Reconciler) reconcileBuilder(ctx context.Context, builder *buildapi.Clu return buildapi.BuilderRecord{}, errors.Errorf("stack %s is not ready", clusterStack.Name) } + clusterLifecycle, err := c.ClusterLifecycleLister.Get(builder.Spec.Lifecycle.Name) + if err != nil { + return buildapi.BuilderRecord{}, err + } + + if !clusterLifecycle.Status.GetCondition(corev1alpha1.ConditionReady).IsTrue() { + return buildapi.BuilderRecord{}, errors.Errorf("Error: clusterlifecycle '%s' is not ready", clusterLifecycle.Name) + } + builderKeychain, err := c.KeychainFactory.KeychainForSecretRef(ctx, registry.SecretRef{ ServiceAccount: builder.Spec.ServiceAccountRef.Name, Namespace: builder.Spec.ServiceAccountRef.Namespace, @@ -226,6 +254,17 @@ func (c *Reconciler) reconcileBuilder(ctx context.Context, builder *buildapi.Clu } } + lifecycleKeychain := builderKeychain + if clusterLifecycle.Spec.ServiceAccountRef != nil { + lifecycleKeychain, err = c.KeychainFactory.KeychainForSecretRef(ctx, registry.SecretRef{ + ServiceAccount: clusterLifecycle.Spec.ServiceAccountRef.Name, + Namespace: clusterLifecycle.Spec.ServiceAccountRef.Namespace, + }) + if err != nil { + return buildapi.BuilderRecord{}, err + } + } + fetcher := cnb.NewRemoteBuildpackFetcher(c.KeychainFactory, clusterStore, nil, clusterBuildpacks) serviceAccountSecrets, err := c.SecretFetcher.SecretsForServiceAccount(ctx, builder.Spec.ServiceAccountRef.Name, builder.Spec.ServiceAccountRef.Namespace) @@ -242,8 +281,10 @@ func (c *Reconciler) reconcileBuilder(ctx context.Context, builder *buildapi.Clu ctx, builderKeychain, stackKeychain, + lifecycleKeychain, fetcher, clusterStack, + clusterLifecycle, builder.Spec.BuilderSpec, serviceAccountSecrets, resolvedBuilderRef, diff --git a/pkg/reconciler/clusterbuilder/clusterbuilder_test.go b/pkg/reconciler/clusterbuilder/clusterbuilder_test.go index d8c1c0032..f56727cd9 100644 --- a/pkg/reconciler/clusterbuilder/clusterbuilder_test.go +++ b/pkg/reconciler/clusterbuilder/clusterbuilder_test.go @@ -65,6 +65,7 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { ClusterStoreLister: listers.GetClusterStoreLister(), ClusterBuildpackLister: listers.GetClusterBuildpackLister(), ClusterStackLister: listers.GetClusterStackLister(), + ClusterLifecycleLister: listers.GetClusterLifecycleLister(), SecretFetcher: fakeSecretFetcher, } return &kreconciler.NetworkErrorReconciler{Reconciler: r}, rtesting.ActionRecorderList{fakeClient}, rtesting.EventList{Recorder: record.NewFakeRecorder(10)} @@ -128,6 +129,33 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { }, } + clusterLifecycle := &buildapi.ClusterLifecycle{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-lifecycle", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "ClusterLifecycle", + APIVersion: "kpack.io/v1alpha2", + }, + Spec: buildapi.ClusterLifecycleSpec{ + ServiceAccountRef: &corev1.ObjectReference{ + Namespace: "some-sa-namespace", + Name: "some-sa-name", + }, + }, + Status: buildapi.ClusterLifecycleStatus{ + Status: corev1alpha1.Status{ + ObservedGeneration: 0, + Conditions: []corev1alpha1.Condition{ + { + Type: corev1alpha1.ConditionReady, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + } + clusterBuildpack := &buildapi.ClusterBuildpack{ ObjectMeta: metav1.ObjectMeta{ Name: "buildpack.id.4", @@ -153,6 +181,10 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { Kind: "Stack", Name: "some-stack", }, + Lifecycle: corev1.ObjectReference{ + Kind: "Lifecycle", + Name: "some-lifecycle", + }, Store: corev1.ObjectReference{ Kind: "ClusterStore", Name: "some-store", @@ -264,6 +296,7 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { rt.Test(rtesting.TableRow{ Key: builderKey, Objects: []runtime.Object{ + clusterLifecycle, clusterStack, clusterStore, builder, @@ -285,6 +318,7 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { StackKeychain: ®istryfakes.FakeKeychain{}, Fetcher: expectedFetcher, ClusterStack: clusterStack, + ClusterLifecycle: clusterLifecycle, BuilderSpec: builder.Spec.BuilderSpec, SigningSecrets: []*corev1.Secret{}, ResolvedBuilderTag: expectedResolvedTag, @@ -331,6 +365,7 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { rt.Test(rtesting.TableRow{ Key: builderKey, Objects: []runtime.Object{ + clusterLifecycle, clusterStack, clusterStore, expectedBuilder, @@ -344,6 +379,8 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { kreconciler.KeyForObject(clusterStore), expectedBuilder.NamespacedName())) require.True(t, fakeTracker.IsTracking( kreconciler.KeyForObject(clusterStack), builder.NamespacedName())) + require.True(t, fakeTracker.IsTracking( + kreconciler.KeyForObject(clusterLifecycle), builder.NamespacedName())) require.True(t, fakeTracker.IsTrackingKind( kreconciler.KeyForObject(clusterBuildpack).GroupKind, builder.NamespacedName())) @@ -394,6 +431,7 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { rt.Test(rtesting.TableRow{ Key: builderKey, Objects: []runtime.Object{ + clusterLifecycle, clusterStack, clusterStore, builder, @@ -435,6 +473,7 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { rt.Test(rtesting.TableRow{ Key: builderKey, Objects: []runtime.Object{ + clusterLifecycle, clusterStack, clusterStore, builder, @@ -610,6 +649,7 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { rt.Test(rtesting.TableRow{ Key: builderKey, Objects: []runtime.Object{ + clusterLifecycle, clusterStack, clusterStore, builder, @@ -653,6 +693,7 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { rt.Test(rtesting.TableRow{ Key: builderKey, Objects: []runtime.Object{ + clusterLifecycle, clusterStack, clusterStore, builder, @@ -695,6 +736,7 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { rt.Test(rtesting.TableRow{ Key: builderKey, Objects: []runtime.Object{ + clusterLifecycle, clusterStack, clusterStore, builder, diff --git a/pkg/reconciler/clusterlifecycle/clusterlifecycle.go b/pkg/reconciler/clusterlifecycle/clusterlifecycle.go new file mode 100644 index 000000000..50f876a76 --- /dev/null +++ b/pkg/reconciler/clusterlifecycle/clusterlifecycle.go @@ -0,0 +1,145 @@ +package clusterlifecycle + +import ( + "context" + + "github.com/google/go-containerregistry/pkg/authn" + "go.uber.org/zap" + "k8s.io/apimachinery/pkg/api/equality" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/cache" + "knative.dev/pkg/controller" + "knative.dev/pkg/logging/logkey" + + buildapi "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" + "github.com/pivotal/kpack/pkg/client/clientset/versioned" + buildinformers "github.com/pivotal/kpack/pkg/client/informers/externalversions/build/v1alpha2" + buildlisters "github.com/pivotal/kpack/pkg/client/listers/build/v1alpha2" + "github.com/pivotal/kpack/pkg/reconciler" + "github.com/pivotal/kpack/pkg/registry" +) + +const ( + ReconcilerName = "Lifecycles" + Kind = "Lifecycle" +) + +//go:generate counterfeiter . ClusterLifecycleReader +type ClusterLifecycleReader interface { + Read(keychain authn.Keychain, clusterLifecycleSpec buildapi.ClusterLifecycleSpec) (buildapi.ResolvedClusterLifecycle, error) +} + +func NewController( + ctx context.Context, + opt reconciler.Options, + keychainFactory registry.KeychainFactory, + clusterLifecycleInformer buildinformers.ClusterLifecycleInformer, + clusterLifecycleReader ClusterLifecycleReader, +) *controller.Impl { + c := &Reconciler{ + Client: opt.Client, + ClusterLifecycleLister: clusterLifecycleInformer.Lister(), + ClusterLifecycleReader: clusterLifecycleReader, + KeychainFactory: keychainFactory, + } + + logger := opt.Logger.With( + zap.String(logkey.Kind, buildapi.ClusterLifecycleCRName), + ) + + impl := controller.NewContext( + ctx, + &reconciler.NetworkErrorReconciler{ + Reconciler: c, + }, + controller.ControllerOptions{WorkQueueName: ReconcilerName, Logger: logger}, + ) + clusterLifecycleInformer.Informer().AddEventHandler(controller.HandleAll(impl.Enqueue)) + return impl +} + +type Reconciler struct { + Client versioned.Interface + ClusterLifecycleLister buildlisters.ClusterLifecycleLister + ClusterLifecycleReader ClusterLifecycleReader + KeychainFactory registry.KeychainFactory +} + +func (c *Reconciler) Reconcile(ctx context.Context, key string) error { + _, clusterLifecycleName, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + return err + } + + clusterLifecycle, err := c.ClusterLifecycleLister.Get(clusterLifecycleName) + if k8serrors.IsNotFound(err) { + return nil + } else if err != nil { + return err + } + + clusterLifecycle = clusterLifecycle.DeepCopy() + + clusterLifecycle, err = c.reconcileClusterLifecycleStatus(ctx, clusterLifecycle) + + updateErr := c.updateClusterLifecycleStatus(ctx, clusterLifecycle) + if updateErr != nil { + return updateErr + } + + if err != nil { + return err + } + return nil +} + +func (c *Reconciler) reconcileClusterLifecycleStatus(ctx context.Context, clusterLifecycle *buildapi.ClusterLifecycle) (*buildapi.ClusterLifecycle, error) { + secretRef := registry.SecretRef{} + + if clusterLifecycle.Spec.ServiceAccountRef != nil { + secretRef = registry.SecretRef{ + ServiceAccount: clusterLifecycle.Spec.ServiceAccountRef.Name, + Namespace: clusterLifecycle.Spec.ServiceAccountRef.Namespace, + } + } + + keychain, err := c.KeychainFactory.KeychainForSecretRef(ctx, secretRef) + if err != nil { + clusterLifecycle.Status = buildapi.ClusterLifecycleStatus{ + Status: corev1alpha1.CreateStatusWithReadyCondition(clusterLifecycle.Generation, err), + } + return clusterLifecycle, err + } + + resolvedClusterLifecycle, err := c.ClusterLifecycleReader.Read(keychain, clusterLifecycle.Spec) + if err != nil { + clusterLifecycle.Status = buildapi.ClusterLifecycleStatus{ + Status: corev1alpha1.CreateStatusWithReadyCondition(clusterLifecycle.Generation, err), + } + return clusterLifecycle, err + } + + clusterLifecycle.Status = buildapi.ClusterLifecycleStatus{ + Status: corev1alpha1.CreateStatusWithReadyCondition(clusterLifecycle.Generation, nil), + ResolvedClusterLifecycle: resolvedClusterLifecycle, + } + return clusterLifecycle, nil +} + +func (c *Reconciler) updateClusterLifecycleStatus(ctx context.Context, desired *buildapi.ClusterLifecycle) error { + desired.Status.ObservedGeneration = desired.Generation + + original, err := c.ClusterLifecycleLister.Get(desired.Name) + if err != nil { + return err + } + + if equality.Semantic.DeepEqual(desired.Status, original.Status) { + return nil + } + + _, err = c.Client.KpackV1alpha2().ClusterLifecycles().UpdateStatus(ctx, desired, metav1.UpdateOptions{}) + return err +} diff --git a/pkg/reconciler/clusterlifecycle/clusterlifecycle_test.go b/pkg/reconciler/clusterlifecycle/clusterlifecycle_test.go new file mode 100644 index 000000000..23f8c101d --- /dev/null +++ b/pkg/reconciler/clusterlifecycle/clusterlifecycle_test.go @@ -0,0 +1,222 @@ +package clusterlifecycle_test + +import ( + "errors" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgotesting "k8s.io/client-go/testing" + "k8s.io/client-go/tools/record" + "knative.dev/pkg/controller" + rtesting "knative.dev/pkg/reconciler/testing" + + buildapi "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" + "github.com/pivotal/kpack/pkg/client/clientset/versioned/fake" + kreconciler "github.com/pivotal/kpack/pkg/reconciler" + "github.com/pivotal/kpack/pkg/reconciler/clusterlifecycle" + "github.com/pivotal/kpack/pkg/reconciler/clusterlifecycle/clusterlifecyclefakes" + "github.com/pivotal/kpack/pkg/reconciler/testhelpers" + "github.com/pivotal/kpack/pkg/registry" + "github.com/pivotal/kpack/pkg/registry/registryfakes" +) + +func TestClusterLifecycleReconciler(t *testing.T) { + spec.Run(t, "Lifecycle Reconciler", testClusterLifecycleReconciler) +} + +func testClusterLifecycleReconciler(t *testing.T, when spec.G, it spec.S) { + const ( + clusterLifecycleName = "some-clusterLifecycle" + clusterLifecycleKey = clusterLifecycleName + initialGeneration int64 = 1 + ) + + var ( + fakeKeyChainFactory = ®istryfakes.FakeKeychainFactory{} + ) + + fakeClusterLifecycleReader := &clusterlifecyclefakes.FakeClusterLifecycleReader{} + + testClusterLifecycle := &buildapi.ClusterLifecycle{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterLifecycleName, + Generation: initialGeneration, + }, + Spec: buildapi.ClusterLifecycleSpec{ + ImageSource: corev1alpha1.ImageSource{ + Image: "some-registry.io/lifecycle-image", + }, + }, + } + + rt := testhelpers.ReconcilerTester(t, + func(t *testing.T, row *rtesting.TableRow) (reconciler controller.Reconciler, lists rtesting.ActionRecorderList, list rtesting.EventList) { + listers := testhelpers.NewListers(row.Objects) + fakeClient := fake.NewSimpleClientset(listers.BuildServiceObjects()...) + r := &clusterlifecycle.Reconciler{ + Client: fakeClient, + ClusterLifecycleLister: listers.GetClusterLifecycleLister(), + ClusterLifecycleReader: fakeClusterLifecycleReader, + KeychainFactory: fakeKeyChainFactory, + } + return &kreconciler.NetworkErrorReconciler{Reconciler: r}, rtesting.ActionRecorderList{fakeClient}, rtesting.EventList{Recorder: record.NewFakeRecorder(10)} + }) + + when("#Reconcile", func() { + it("saves metadata to the status", func() { + resolvedClusterLifecycle := buildapi.ResolvedClusterLifecycle{ + Version: "some-version", + } + fakeClusterLifecycleReader.ReadReturns(resolvedClusterLifecycle, nil) + emptySecretRef := registry.SecretRef{} + defaultKeyChain := ®istryfakes.FakeKeychain{Name: "default"} + fakeKeyChainFactory.AddKeychainForSecretRef(t, emptySecretRef, defaultKeyChain) + + rt.Test(rtesting.TableRow{ + Key: clusterLifecycleKey, + Objects: []runtime.Object{ + testClusterLifecycle, + }, + WantErr: false, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{ + { + Object: &buildapi.ClusterLifecycle{ + ObjectMeta: testClusterLifecycle.ObjectMeta, + Spec: testClusterLifecycle.Spec, + Status: buildapi.ClusterLifecycleStatus{ + Status: corev1alpha1.Status{ + ObservedGeneration: 1, + Conditions: corev1alpha1.Conditions{ + { + Type: corev1alpha1.ConditionReady, + Status: corev1.ConditionTrue, + }, + }, + }, + ResolvedClusterLifecycle: resolvedClusterLifecycle, + }, + }, + }, + }, + }) + + require.Equal(t, 1, fakeClusterLifecycleReader.ReadCallCount()) + _, clusterLifecycleSpec := fakeClusterLifecycleReader.ReadArgsForCall(0) + require.Equal(t, testClusterLifecycle.Spec, clusterLifecycleSpec) + }) + + it("does not update the status with no status change", func() { + resolvedClusterLifecycle := buildapi.ResolvedClusterLifecycle{ + Version: "some-version", + } + fakeClusterLifecycleReader.ReadReturns(resolvedClusterLifecycle, nil) + emptySecretRef := registry.SecretRef{} + defaultKeyChain := ®istryfakes.FakeKeychain{Name: "default"} + fakeKeyChainFactory.AddKeychainForSecretRef(t, emptySecretRef, defaultKeyChain) + + testClusterLifecycle.Status = buildapi.ClusterLifecycleStatus{ + Status: corev1alpha1.Status{ + ObservedGeneration: 1, + Conditions: corev1alpha1.Conditions{ + { + Type: corev1alpha1.ConditionReady, + Status: corev1.ConditionTrue, + }, + }, + }, + ResolvedClusterLifecycle: resolvedClusterLifecycle, + } + rt.Test(rtesting.TableRow{ + Key: clusterLifecycleKey, + Objects: []runtime.Object{ + testClusterLifecycle, + }, + WantErr: false, + }) + }) + + it("sets the status to Ready False if error reading from clusterLifecycle", func() { + fakeClusterLifecycleReader.ReadReturns(buildapi.ResolvedClusterLifecycle{}, errors.New("invalid mixins on run image")) + emptySecretRef := registry.SecretRef{} + defaultKeyChain := ®istryfakes.FakeKeychain{Name: "default"} + fakeKeyChainFactory.AddKeychainForSecretRef(t, emptySecretRef, defaultKeyChain) + + rt.Test(rtesting.TableRow{ + Key: clusterLifecycleKey, + Objects: []runtime.Object{ + testClusterLifecycle, + }, + WantErr: true, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{ + { + Object: &buildapi.ClusterLifecycle{ + ObjectMeta: testClusterLifecycle.ObjectMeta, + Spec: testClusterLifecycle.Spec, + Status: buildapi.ClusterLifecycleStatus{ + Status: corev1alpha1.Status{ + ObservedGeneration: 1, + Conditions: corev1alpha1.Conditions{ + { + Message: "invalid mixins on run image", + Type: corev1alpha1.ConditionReady, + Status: corev1.ConditionFalse, + }, + }, + }, + }, + }, + }, + }, + }) + }) + + it("uses the keychain of the referenced service account", func() { + fakeClusterLifecycleReader.ReadReturns(buildapi.ResolvedClusterLifecycle{}, nil) + + testClusterLifecycle.Spec.ServiceAccountRef = &corev1.ObjectReference{Name: "private-account", Namespace: "my-namespace"} + secretRef := registry.SecretRef{ + ServiceAccount: "private-account", + Namespace: "my-namespace", + } + expectedKeyChain := ®istryfakes.FakeKeychain{Name: "secret"} + fakeKeyChainFactory.AddKeychainForSecretRef(t, secretRef, expectedKeyChain) + + rt.Test(rtesting.TableRow{ + Key: clusterLifecycleKey, + Objects: []runtime.Object{ + testClusterLifecycle, + }, + WantErr: false, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{ + { + Object: &buildapi.ClusterLifecycle{ + ObjectMeta: testClusterLifecycle.ObjectMeta, + Spec: testClusterLifecycle.Spec, + Status: buildapi.ClusterLifecycleStatus{ + Status: corev1alpha1.Status{ + ObservedGeneration: 1, + Conditions: corev1alpha1.Conditions{ + { + Type: corev1alpha1.ConditionReady, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + }, + }, + }, + }) + + assert.Equal(t, 1, fakeClusterLifecycleReader.ReadCallCount()) + actualKeyChain, _ := fakeClusterLifecycleReader.ReadArgsForCall(0) + assert.Equal(t, expectedKeyChain, actualKeyChain) + }) + }) +} diff --git a/pkg/reconciler/clusterlifecycle/clusterlifecyclefakes/fake_cluster_lifecycle_reader.go b/pkg/reconciler/clusterlifecycle/clusterlifecyclefakes/fake_cluster_lifecycle_reader.go new file mode 100644 index 000000000..3a2284cdb --- /dev/null +++ b/pkg/reconciler/clusterlifecycle/clusterlifecyclefakes/fake_cluster_lifecycle_reader.go @@ -0,0 +1,120 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package clusterlifecyclefakes + +import ( + "sync" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/pivotal/kpack/pkg/apis/build/v1alpha2" + "github.com/pivotal/kpack/pkg/reconciler/clusterlifecycle" +) + +type FakeClusterLifecycleReader struct { + ReadStub func(authn.Keychain, v1alpha2.ClusterLifecycleSpec) (v1alpha2.ResolvedClusterLifecycle, error) + readMutex sync.RWMutex + readArgsForCall []struct { + arg1 authn.Keychain + arg2 v1alpha2.ClusterLifecycleSpec + } + readReturns struct { + result1 v1alpha2.ResolvedClusterLifecycle + result2 error + } + readReturnsOnCall map[int]struct { + result1 v1alpha2.ResolvedClusterLifecycle + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeClusterLifecycleReader) Read(arg1 authn.Keychain, arg2 v1alpha2.ClusterLifecycleSpec) (v1alpha2.ResolvedClusterLifecycle, error) { + fake.readMutex.Lock() + ret, specificReturn := fake.readReturnsOnCall[len(fake.readArgsForCall)] + fake.readArgsForCall = append(fake.readArgsForCall, struct { + arg1 authn.Keychain + arg2 v1alpha2.ClusterLifecycleSpec + }{arg1, arg2}) + stub := fake.ReadStub + fakeReturns := fake.readReturns + fake.recordInvocation("Read", []interface{}{arg1, arg2}) + fake.readMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeClusterLifecycleReader) ReadCallCount() int { + fake.readMutex.RLock() + defer fake.readMutex.RUnlock() + return len(fake.readArgsForCall) +} + +func (fake *FakeClusterLifecycleReader) ReadCalls(stub func(authn.Keychain, v1alpha2.ClusterLifecycleSpec) (v1alpha2.ResolvedClusterLifecycle, error)) { + fake.readMutex.Lock() + defer fake.readMutex.Unlock() + fake.ReadStub = stub +} + +func (fake *FakeClusterLifecycleReader) ReadArgsForCall(i int) (authn.Keychain, v1alpha2.ClusterLifecycleSpec) { + fake.readMutex.RLock() + defer fake.readMutex.RUnlock() + argsForCall := fake.readArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeClusterLifecycleReader) ReadReturns(result1 v1alpha2.ResolvedClusterLifecycle, result2 error) { + fake.readMutex.Lock() + defer fake.readMutex.Unlock() + fake.ReadStub = nil + fake.readReturns = struct { + result1 v1alpha2.ResolvedClusterLifecycle + result2 error + }{result1, result2} +} + +func (fake *FakeClusterLifecycleReader) ReadReturnsOnCall(i int, result1 v1alpha2.ResolvedClusterLifecycle, result2 error) { + fake.readMutex.Lock() + defer fake.readMutex.Unlock() + fake.ReadStub = nil + if fake.readReturnsOnCall == nil { + fake.readReturnsOnCall = make(map[int]struct { + result1 v1alpha2.ResolvedClusterLifecycle + result2 error + }) + } + fake.readReturnsOnCall[i] = struct { + result1 v1alpha2.ResolvedClusterLifecycle + result2 error + }{result1, result2} +} + +func (fake *FakeClusterLifecycleReader) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.readMutex.RLock() + defer fake.readMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeClusterLifecycleReader) 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 _ clusterlifecycle.ClusterLifecycleReader = new(FakeClusterLifecycleReader) diff --git a/pkg/reconciler/image/build_required.go b/pkg/reconciler/image/build_required.go index a01841164..e4bf06797 100644 --- a/pkg/reconciler/image/build_required.go +++ b/pkg/reconciler/image/build_required.go @@ -33,8 +33,8 @@ func newBuildRequiredResult(summary buildchange.ChangeSummary) buildRequiredResu func isBuildRequired(img *buildapi.Image, lastBuild *buildapi.Build, srcResolver *buildapi.SourceResolver, - builder buildapi.BuilderResource) (buildRequiredResult, error) { - + builder buildapi.BuilderResource, +) (buildRequiredResult, error) { result := buildRequiredResult{ConditionStatus: corev1.ConditionUnknown} if !srcResolver.Ready() || !builder.Ready() { return result, nil @@ -46,6 +46,7 @@ func isBuildRequired(img *buildapi.Image, Process(configChange(img, lastBuild, srcResolver)). Process(buildpackChange(lastBuild, builder)). Process(stackChange(lastBuild, builder)). + Process(lifecycleChange(lastBuild, builder)). Summarize() if err != nil { return result, err @@ -131,3 +132,13 @@ func stackChange(lastBuild *buildapi.Build, builder buildapi.BuilderResource) bu newRunImageRefStr := builder.RunImage() return buildchange.NewStackChange(oldRunImageRefStr, newRunImageRefStr) } + +func lifecycleChange(lastBuild *buildapi.Build, builder buildapi.BuilderResource) buildchange.Change { + if lastBuild == nil || !lastBuild.IsSuccess() { + return nil + } + + oldLifecycle := lastBuild.Status.LifecycleVersion + newLifecycle := builder.LifecycleVersion() + return buildchange.NewLifecycleChange(oldLifecycle, newLifecycle) +} diff --git a/pkg/reconciler/image/build_required_test.go b/pkg/reconciler/image/build_required_test.go index c39444c16..c669e67d5 100644 --- a/pkg/reconciler/image/build_required_test.go +++ b/pkg/reconciler/image/build_required_test.go @@ -63,8 +63,9 @@ func testImageBuilds(t *testing.T, when spec.G, it spec.S) { BuilderMetadata: []corev1alpha1.BuildpackMetadata{ {Id: "buildpack.matches", Version: "1"}, }, - LatestRunImage: "some.registry.io/run-image@sha256:67e3de2af270bf09c02e9a644aeb7e87e6b3c049abe6766bf6b6c3728a83e7fb", - Kind: buildapi.BuilderKind, + LatestRunImage: "some.registry.io/run-image@sha256:67e3de2af270bf09c02e9a644aeb7e87e6b3c049abe6766bf6b6c3728a83e7fb", + LatestLifecycleVersion: "some-version", + Kind: buildapi.BuilderKind, } latestBuild := &buildapi.Build{ @@ -92,7 +93,8 @@ func testImageBuilds(t *testing.T, when spec.G, it spec.S) { RunImage: "some.registry.io/run-image@sha256:67e3de2af270bf09c02e9a644aeb7e87e6b3c049abe6766bf6b6c3728a83e7fb", ID: "io.buildpacks.stack.bionic", }, - LatestImage: "some.registry.io/built@sha256:67e3de2af270bf09c02e9a644aeb7e87e6b3c049abe6766bf6b6c3728a83e7fb", + LatestImage: "some.registry.io/built@sha256:67e3de2af270bf09c02e9a644aeb7e87e6b3c049abe6766bf6b6c3728a83e7fb", + LifecycleVersion: "some-version", }, } @@ -397,6 +399,26 @@ func testImageBuilds(t *testing.T, when spec.G, it spec.S) { assert.Equal(t, buildapi.BuildPriorityClassLow, result.PriorityClass) assert.Equal(t, expectedChanges, result.ChangesStr) }) + + it("true if builder has a different lifecycle image", func() { + builder.LatestLifecycleVersion = "some-new-version" + + expectedChanges := testhelpers.CompactJSON(` +[ + { + "reason": "LIFECYCLE", + "old": "some-version", + "new": "some-new-version" + } +]`) + + result, err := isBuildRequired(image, latestBuild, sourceResolver, builder) + assert.NoError(t, err) + assert.Equal(t, corev1.ConditionTrue, result.ConditionStatus) + assert.Equal(t, buildapi.BuildReasonLifecycle, result.ReasonsStr) + assert.Equal(t, buildapi.BuildPriorityClassLow, result.PriorityClass) + assert.Equal(t, expectedChanges, result.ChangesStr) + }) }) when("Git", func() { @@ -799,15 +821,16 @@ func testImageBuilds(t *testing.T, when spec.G, it spec.S) { } type TestBuilderResource struct { - BuilderReady bool - BuilderUpToDate bool - BuilderMetadata []corev1alpha1.BuildpackMetadata - ImagePullSecrets []corev1.LocalObjectReference - LatestImage string - LatestRunImage string - Name string - Namespace string - Kind string + BuilderReady bool + BuilderUpToDate bool + BuilderMetadata []corev1alpha1.BuildpackMetadata + ImagePullSecrets []corev1.LocalObjectReference + LatestImage string + LatestRunImage string + LatestLifecycleVersion string + Name string + Namespace string + Kind string } func (t TestBuilderResource) BuildBuilderSpec() corev1alpha1.BuildBuilderSpec { @@ -833,6 +856,10 @@ func (t TestBuilderResource) RunImage() string { return t.LatestRunImage } +func (t TestBuilderResource) LifecycleVersion() string { + return t.LatestLifecycleVersion +} + func (t TestBuilderResource) GetName() string { return t.Name } diff --git a/pkg/reconciler/image/image_test.go b/pkg/reconciler/image/image_test.go index d8b04d7e7..7fb1dc138 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, @@ -166,6 +166,9 @@ imageWithBuilder := &buildapi.Image{ RunImage: "some/run@sha256:67e3de2af270bf09c02e9a644aeb7e87e6b3c049abe6766bf6b6c3728a83e7fb", ID: "io.buildpacks.stacks.bionic", }, + Lifecycle: buildapi.ResolvedClusterLifecycle{ + Version: "some-version", + }, Status: corev1alpha1.Status{ Conditions: corev1alpha1.Conditions{ { @@ -787,15 +790,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 +847,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, @@ -921,10 +924,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 +938,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, @@ -1464,6 +1467,7 @@ imageWithBuilder := &buildapi.Image{ RunImage: "some/run@sha256:67e3de2af270bf09c02e9a644aeb7e87e6b3c049abe6766bf6b6c3728a83e7fb", ID: "io.buildpacks.stacks.bionic", }, + LifecycleVersion: "some-version", Status: corev1alpha1.Status{ Conditions: corev1alpha1.Conditions{ { @@ -1624,6 +1628,7 @@ imageWithBuilder := &buildapi.Image{ RunImage: "some/run@sha256:67e3de2af270bf09c02e9a644aeb7e87e6b3c049abe6766bf6b6c3728a83e7fb", ID: "io.buildpacks.stacks.bionic", }, + LifecycleVersion: "some-version", Status: corev1alpha1.Status{ Conditions: corev1alpha1.Conditions{ { @@ -1736,6 +1741,9 @@ imageWithBuilder := &buildapi.Image{ RunImage: "some/run@sha256:67e3de2af270bf09c02e9a644aeb7e87e6b3c049abe6766bf6b6c3728a83e7fb", ID: "io.buildpacks.stacks.bionic", }, + Lifecycle: buildapi.ResolvedClusterLifecycle{ + Version: "some-version", + }, BuilderMetadata: corev1alpha1.BuildpackMetadataList{ { Id: "io.buildpack", @@ -1784,6 +1792,7 @@ imageWithBuilder := &buildapi.Image{ RunImage: "some/run@sha256:67e3de2af270bf09c02e9a644aeb7e87e6b3c049abe6766bf6b6c3728a83e7fb", ID: "io.buildpacks.stacks.bionic", }, + LifecycleVersion: "some-version", BuildMetadata: corev1alpha1.BuildpackMetadataList{ { Id: "io.buildpack", @@ -1910,6 +1919,9 @@ imageWithBuilder := &buildapi.Image{ RunImage: updatedBuilderRunImage, ID: "io.buildpacks.stacks.bionic", }, + Lifecycle: buildapi.ResolvedClusterLifecycle{ + Version: "some-version", + }, BuilderMetadata: corev1alpha1.BuildpackMetadataList{ { Id: "io.buildpack", @@ -1958,6 +1970,7 @@ imageWithBuilder := &buildapi.Image{ RunImage: "gcr.io/test-project/install/run@sha256:42841631725942db48b7ba8b788b97374a2ada34c84ee02ca5e02ef3d4b0dfca", ID: "io.buildpacks.stacks.bionic", }, + LifecycleVersion: "some-version", BuildMetadata: corev1alpha1.BuildpackMetadataList{ { Id: "io.buildpack", @@ -2039,6 +2052,175 @@ imageWithBuilder := &buildapi.Image{ }) }) + it("schedules a build when the builder lifecycle is updated", func() { + imageWithBuilder.Status.BuildCounter = 1 + imageWithBuilder.Status.LatestBuildRef = "image-name-build-1" + const updatedBuilderImage = "some/builder@sha256:updated" + + sourceResolver := resolvedSourceResolver(imageWithBuilder) + rt.Test(rtesting.TableRow{ + Key: key, + Objects: []runtime.Object{ + imageWithBuilder, + &buildapi.Builder{ + ObjectMeta: metav1.ObjectMeta{ + Name: builderName, + Namespace: namespace, + }, + TypeMeta: metav1.TypeMeta{ + Kind: buildapi.BuilderKind, + }, + Status: buildapi.BuilderStatus{ + Status: corev1alpha1.Status{ + Conditions: corev1alpha1.Conditions{ + { + Type: corev1alpha1.ConditionReady, + Status: corev1.ConditionTrue, + }, + { + Type: buildapi.ConditionUpToDate, + Status: corev1.ConditionTrue, + }, + }, + }, + LatestImage: updatedBuilderImage, + Stack: corev1alpha1.BuildStack{ + RunImage: "some/run@sha256:67e3de2af270bf09c02e9a644aeb7e87e6b3c049abe6766bf6b6c3728a83e7fb", + ID: "io.buildpacks.stacks.bionic", + }, + Lifecycle: buildapi.ResolvedClusterLifecycle{ + Version: "some-new-version", + }, + BuilderMetadata: corev1alpha1.BuildpackMetadataList{ + { + Id: "io.buildpack", + Version: "version", + }, + }, + }, + }, + sourceResolver, + &buildapi.Build{ + ObjectMeta: metav1.ObjectMeta{ + Name: "image-name-build-1", + Namespace: namespace, + OwnerReferences: []metav1.OwnerReference{ + *kmeta.NewControllerRef(imageWithBuilder), + }, + Labels: map[string]string{ + buildapi.BuildNumberLabel: "1", + buildapi.ImageLabel: imageName, + }, + }, + Spec: buildapi.BuildSpec{ + Tags: []string{imageWithBuilder.Spec.Tag}, + Builder: corev1alpha1.BuildBuilderSpec{ + Image: updatedBuilderImage, + }, + ServiceAccountName: imageWithBuilder.Spec.ServiceAccountName, + Source: corev1alpha1.SourceConfig{ + Git: &corev1alpha1.Git{ + URL: sourceResolver.Status.Source.Git.URL, + Revision: sourceResolver.Status.Source.Git.Revision, + }, + }, + }, + Status: buildapi.BuildStatus{ + LatestImage: imageWithBuilder.Spec.Tag + "@sha256:just-built", + Status: corev1alpha1.Status{ + Conditions: corev1alpha1.Conditions{ + { + Type: corev1alpha1.ConditionSucceeded, + Status: corev1.ConditionTrue, + }, + }, + }, + Stack: corev1alpha1.BuildStack{ + RunImage: "some/run@sha256:67e3de2af270bf09c02e9a644aeb7e87e6b3c049abe6766bf6b6c3728a83e7fb", + ID: "io.buildpacks.stacks.bionic", + }, + LifecycleVersion: "some-version", + BuildMetadata: corev1alpha1.BuildpackMetadataList{ + { + Id: "io.buildpack", + Version: "version", + }, + }, + }, + }, + }, + WantErr: false, + WantCreates: []runtime.Object{ + &buildapi.Build{ + ObjectMeta: metav1.ObjectMeta{ + Name: imageName + "-build-2", + Namespace: namespace, + OwnerReferences: []metav1.OwnerReference{ + *kmeta.NewControllerRef(imageWithBuilder), + }, + Labels: map[string]string{ + buildapi.BuildNumberLabel: "2", + buildapi.ImageLabel: imageName, + buildapi.ImageGenerationLabel: generation(imageWithBuilder), + someLabelKey: someValueToPassThrough, + }, + Annotations: map[string]string{ + buildapi.BuilderNameAnnotation: builderName, + buildapi.BuilderKindAnnotation: buildapi.BuilderKind, + buildapi.BuildReasonAnnotation: buildapi.BuildReasonLifecycle, + buildapi.BuildChangesAnnotation: testhelpers.CompactJSON(` +[ + { + "reason": "LIFECYCLE", + "old": "some-version", + "new": "some-new-version" + } +]`), + }, + }, + Spec: buildapi.BuildSpec{ + Tags: []string{imageWithBuilder.Spec.Tag}, + Builder: corev1alpha1.BuildBuilderSpec{ + Image: updatedBuilderImage, + }, + ServiceAccountName: imageWithBuilder.Spec.ServiceAccountName, + Source: corev1alpha1.SourceConfig{ + Git: &corev1alpha1.Git{ + URL: sourceResolver.Status.Source.Git.URL, + Revision: sourceResolver.Status.Source.Git.Revision, + }, + }, + Cache: &buildapi.BuildCacheConfig{}, + RunImage: builderRunImage, + LastBuild: &buildapi.LastBuild{ + Image: "some/image@sha256:just-built", + StackId: "io.buildpacks.stacks.bionic", + }, + }, + }, + }, + WantStatusUpdates: []clientgotesting.UpdateActionImpl{ + { + Object: &buildapi.Image{ + ObjectMeta: imageWithBuilder.ObjectMeta, + Spec: imageWithBuilder.Spec, + Status: buildapi.ImageStatus{ + Status: corev1alpha1.Status{ + ObservedGeneration: originalGeneration, + Conditions: conditionBuildExecuting("image-name-build-2"), + }, + LatestBuildRef: "image-name-build-2", + LatestBuildReason: "LIFECYCLE", + LatestBuildImageGeneration: originalGeneration, + LatestImage: imageWithBuilder.Spec.Tag + "@sha256:just-built", + BuildCounter: 2, + }, + }, + }, + }, + }) + }) + it("schedules a build with previous build's LastBuild if the last build failed", func() { imageWithBuilder.Status.BuildCounter = 2 imageWithBuilder.Status.LatestBuildRef = "image-name-build200001" @@ -2317,6 +2499,7 @@ imageWithBuilder := &buildapi.Image{ RunImage: "some/run@sha256:67e3de2af270bf09c02e9a644aeb7e87e6b3c049abe6766bf6b6c3728a83e7fb", ID: "io.buildpacks.stacks.bionic", }, + LifecycleVersion: "some-version", Status: corev1alpha1.Status{ Conditions: corev1alpha1.Conditions{ { @@ -2747,6 +2930,7 @@ func builds(image *buildapi.Image, sourceResolver *buildapi.SourceResolver, coun RunImage: runImageRef, ID: "io.buildpacks.stacks.bionic", }, + LifecycleVersion: "some-version", Status: corev1alpha1.Status{ Conditions: corev1alpha1.Conditions{ condition, diff --git a/pkg/reconciler/lifecycle/lifecycle.go b/pkg/reconciler/lifecycle/lifecycle.go deleted file mode 100644 index a9a867dc5..000000000 --- a/pkg/reconciler/lifecycle/lifecycle.go +++ /dev/null @@ -1,79 +0,0 @@ -package lifecycle - -import ( - "context" - "fmt" - "github.com/pivotal/kpack/pkg/tracker" - corev1 "k8s.io/api/core/v1" - - "k8s.io/apimachinery/pkg/types" - coreinformers "k8s.io/client-go/informers/core/v1" - k8sclient "k8s.io/client-go/kubernetes" - corelisters "k8s.io/client-go/listers/core/v1" - "k8s.io/client-go/tools/cache" - "knative.dev/pkg/controller" - "knative.dev/pkg/logging" - "knative.dev/pkg/system" - - "github.com/pivotal/kpack/pkg/reconciler" -) - -type LifecycleProvider interface { - UpdateImage(cm *corev1.ConfigMap) error -} - -func NewController( - ctx context.Context, - opt reconciler.Options, - k8sClient k8sclient.Interface, - configmapName string, - configMapInformer coreinformers.ConfigMapInformer, - lifecycleProvider LifecycleProvider, -) *controller.Impl { - key := types.NamespacedName{ - Namespace: system.Namespace(), - Name: configmapName, - } - - c := &Reconciler{ - Key: key, - ConfigMapLister: configMapInformer.Lister(), - K8sClient: k8sClient, - LifecycleProvider: lifecycleProvider, - } - - const queueName = "lifecycle" - impl := controller.NewContext(ctx, c, controller.ControllerOptions{WorkQueueName: queueName, Logger: logging.FromContext(ctx).Named(queueName)}) - - // Reconcile when the lifecycle configmap changes. - configMapInformer.Informer().AddEventHandler(cache.FilteringResourceEventHandler{ - FilterFunc: controller.FilterWithNameAndNamespace(key.Namespace, key.Name), - Handler: controller.HandleAll(impl.Enqueue), - }) - - c.Tracker = tracker.New(impl.EnqueueKey, opt.TrackerResyncPeriod()) - - return impl -} - -type Reconciler struct { - Key types.NamespacedName - ConfigMapLister corelisters.ConfigMapLister - Tracker reconciler.Tracker - K8sClient k8sclient.Interface - LifecycleProvider LifecycleProvider -} - -func (c *Reconciler) Reconcile(ctx context.Context, key string) error { - namespace, configMapName, err := cache.SplitMetaNamespaceKey(key) - if err != nil { - return fmt.Errorf("failed splitting meta namespace key: %s", err) - } - - lifecycleConfigMap, err := c.ConfigMapLister.ConfigMaps(namespace).Get(configMapName) - if err != nil { - return err - } - - return c.LifecycleProvider.UpdateImage(lifecycleConfigMap) -} diff --git a/pkg/reconciler/lifecycle/lifecycle_test.go b/pkg/reconciler/lifecycle/lifecycle_test.go deleted file mode 100644 index 24e2e0935..000000000 --- a/pkg/reconciler/lifecycle/lifecycle_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package lifecycle_test - -import ( - "context" - "fmt" - "testing" - - "github.com/sclevine/spec" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - k8sfake "k8s.io/client-go/kubernetes/fake" - "knative.dev/pkg/controller" - - "github.com/pivotal/kpack/pkg/config" - "github.com/pivotal/kpack/pkg/reconciler/lifecycle" - "github.com/pivotal/kpack/pkg/reconciler/testhelpers" -) - -func TestLifecycleReconciler(t *testing.T) { - spec.Run(t, "Lifecycle Reconciler", testLifecycleReconciler) -} - -func testLifecycleReconciler(t *testing.T, when spec.G, it spec.S) { - - var ( - fakeTracker = &testhelpers.FakeTracker{} - lifecycleImageRef = "gcr.io/lifecycle@sha256:some-sha" - serviceAccountName = "lifecycle-sa" - namespace = "kpack" - key = types.NamespacedName{Namespace: namespace, Name: config.LifecycleConfigName} - lifecycleProvider = &fakeLifecycleProvider{} - ) - - lifecycleConfigMap := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: config.LifecycleConfigName, - Namespace: namespace, - }, - Data: map[string]string{ - config.LifecycleConfigKey: lifecycleImageRef, - "serviceAccountRef.name": serviceAccountName, - "serviceAccountRef.namespace": namespace, - }, - } - listers := testhelpers.NewListers([]runtime.Object{lifecycleConfigMap}) - k8sfakeClient := k8sfake.NewSimpleClientset(listers.GetKubeObjects()...) - - r := &lifecycle.Reconciler{ - Tracker: fakeTracker, - K8sClient: k8sfakeClient, - ConfigMapLister: listers.GetConfigMapLister(), - LifecycleProvider: lifecycleProvider, - } - - when("Reconcile", func() { - it("calls UpdateImage", func() { - err := r.Reconcile(context.TODO(), key.String()) - require.NoError(t, err) - require.Len(t, lifecycleProvider.Calls, 1) - assert.Equal(t, lifecycleProvider.Calls[0], lifecycleConfigMap) - }) - - it("returns error if key is invalid", func() { - err := r.Reconcile(context.TODO(), "my-namespace/fake/config-map") - require.Error(t, err, "unexpected key") - assert.False(t, controller.IsPermanentError(err)) - }) - - it("returns error if config map does not exist", func() { - err := r.Reconcile(context.TODO(), "my-namespace/fake-config-map") - require.Error(t, err, "configmap \"fake-config-map\" not found") - assert.False(t, controller.IsPermanentError(err)) - }) - - it("returns error update image fails", func() { - lifecycleProvider.returnsOnCall(fmt.Errorf("some update error")) - err := r.Reconcile(context.TODO(), "my-namespace/fake-config-map") - require.Error(t, err, "some update error") - assert.False(t, controller.IsPermanentError(err)) - }) - }) -} - -type fakeLifecycleProvider struct { - Calls []*corev1.ConfigMap - error error -} - -func (f *fakeLifecycleProvider) UpdateImage(cm *corev1.ConfigMap) error { - f.Calls = append(f.Calls, cm) - return f.error -} - -func (f *fakeLifecycleProvider) returnsOnCall(err error) { - f.error = err -} diff --git a/pkg/reconciler/testhelpers/fake_builder_creator.go b/pkg/reconciler/testhelpers/fake_builder_creator.go index d4f5f6567..7f35480fb 100644 --- a/pkg/reconciler/testhelpers/fake_builder_creator.go +++ b/pkg/reconciler/testhelpers/fake_builder_creator.go @@ -24,18 +24,31 @@ type CreateBuilderArgs struct { StackKeychain authn.Keychain Fetcher cnb.RemoteBuildpackFetcher ClusterStack *buildapi.ClusterStack + ClusterLifecycle *buildapi.ClusterLifecycle BuilderSpec buildapi.BuilderSpec SigningSecrets []*corev1.Secret ResolvedBuilderTag string } -func (f *FakeBuilderCreator) CreateBuilder(ctx context.Context, builderKeychain authn.Keychain, stackKeychain authn.Keychain, fetcher cnb.RemoteBuildpackFetcher, clusterStack *buildapi.ClusterStack, spec buildapi.BuilderSpec, signingSecrets []*corev1.Secret, resolvedBuilderTag string) (buildapi.BuilderRecord, error) { +func (f *FakeBuilderCreator) CreateBuilder( + ctx context.Context, + builderKeychain authn.Keychain, + stackKeychain authn.Keychain, + lifecycleKeychain authn.Keychain, + fetcher cnb.RemoteBuildpackFetcher, + clusterStack *buildapi.ClusterStack, + clusterLifecycle *buildapi.ClusterLifecycle, + spec buildapi.BuilderSpec, + signingSecrets []*corev1.Secret, + resolvedBuilderTag string, +) (buildapi.BuilderRecord, error) { f.CreateBuilderCalls = append(f.CreateBuilderCalls, CreateBuilderArgs{ Context: ctx, BuilderKeychain: builderKeychain, StackKeychain: stackKeychain, Fetcher: fetcher, ClusterStack: clusterStack, + ClusterLifecycle: clusterLifecycle, BuilderSpec: spec, SigningSecrets: signingSecrets, ResolvedBuilderTag: resolvedBuilderTag, diff --git a/pkg/reconciler/testhelpers/listers.go b/pkg/reconciler/testhelpers/listers.go index 858b33ddd..3be82de43 100644 --- a/pkg/reconciler/testhelpers/listers.go +++ b/pkg/reconciler/testhelpers/listers.go @@ -83,6 +83,10 @@ func (l *Listers) GetClusterStackLister() buildlisters.ClusterStackLister { return buildlisters.NewClusterStackLister(l.indexerFor(&buildapi.ClusterStack{})) } +func (l *Listers) GetClusterLifecycleLister() buildlisters.ClusterLifecycleLister { + return buildlisters.NewClusterLifecycleLister(l.indexerFor(&buildapi.ClusterLifecycle{})) +} + func (l *Listers) GetSourceResolverLister() buildlisters.SourceResolverLister { return buildlisters.NewSourceResolverLister(l.indexerFor(&buildapi.SourceResolver{})) } diff --git a/pkg/slsa/attest.go b/pkg/slsa/attest.go index 86242e9bd..adeb58002 100644 --- a/pkg/slsa/attest.go +++ b/pkg/slsa/attest.go @@ -35,8 +35,7 @@ type ImageReader interface { type Attester struct { Version string - ImageReader ImageReader - LifecycleProvider LifecycleProvider + ImageReader ImageReader Images config.Images Features config.FeatureFlags @@ -61,11 +60,6 @@ func (a *Attester) AttestBuild(build *buildv1alpha2.Build, buildMetadata *cnb.Bu start, stop := getStartStopTime(pod) - lifecycle, err := a.LifecycleProvider.Metadata() - if err != nil { - return intoto.Statement{}, fmt.Errorf("failed to read lifecycle metadata: %v", err) - } - builderDeps := make([]slsav1.ResourceDescriptor, 0) for i, fn := range depFns { dep, err := fn() @@ -102,7 +96,7 @@ func (a *Attester) AttestBuild(build *buildv1alpha2.Build, buildMetadata *cnb.Bu ID: string(builderId), Version: map[string]string{ "kpack": a.Version, - "lifecycle": lifecycle.Version, + "lifecycle": buildMetadata.LifecycleVersion, }, BuilderDependencies: builderDeps, }, diff --git a/pkg/slsa/attest_test.go b/pkg/slsa/attest_test.go index f4da89c76..a9cb718a8 100644 --- a/pkg/slsa/attest_test.go +++ b/pkg/slsa/attest_test.go @@ -57,7 +57,8 @@ func testAttester(t *testing.T, when spec.G, it spec.S) { } buildMetadata := &cnb.BuildMetadata{ - LatestImage: "some-registry.io/some/repo@sha256:27227f3eaf20afcd527f31bcaaa1a10d14f30c2a99b313c86b981906c54c07b9", + LatestImage: "some-registry.io/some/repo@sha256:27227f3eaf20afcd527f31bcaaa1a10d14f30c2a99b313c86b981906c54c07b9", + LifecycleVersion: "1.2.3", } pod := &corev1.Pod{ @@ -125,8 +126,7 @@ func testAttester(t *testing.T, when spec.G, it spec.S) { attester := &Attester{ Version: "v0.0.0", - LifecycleProvider: &fakeLifecycleProvider{}, - ImageReader: r, + ImageReader: r, Images: config.Images{ BuildInitImage: "build-init-image", BuildInitWindowsImage: "build-init-windows-image", @@ -138,7 +138,7 @@ func testAttester(t *testing.T, when spec.G, it spec.S) { Features: config.FeatureFlags{InjectedSidecarSupport: false}, } - it("", func() { + it("returns the expected, properly indented statement", func() { stmt, err := attester.AttestBuild(build, buildMetadata, pod, authn.DefaultKeychain, UnsignedBuildID) require.NoError(t, err) @@ -299,14 +299,3 @@ func testAttester(t *testing.T, when spec.G, it spec.S) { }) }) } - -type fakeLifecycleProvider struct { -} - -func (l *fakeLifecycleProvider) Metadata() (cnb.LifecycleMetadata, error) { - return cnb.LifecycleMetadata{ - LifecycleInfo: cnb.LifecycleInfo{ - Version: "1.2.3", - }, - }, nil -} diff --git a/test/cosign_e2e_test.go b/test/cosign_e2e_test.go index dcee00132..f384c3f27 100644 --- a/test/cosign_e2e_test.go +++ b/test/cosign_e2e_test.go @@ -32,6 +32,7 @@ func testSignBuilder(t *testing.T, _ spec.G, it spec.S) { buildpackName = "buildpack" clusterBuildpackName = "cluster-buildpack-cosign" clusterStackName = "stack-cosign" + clusterLifecycleName = "lifecycle-cosign" builderName = "custom-signed-builder" clusterBuilderName = "custom-signed-cluster-builder-cosign" cosignSecretName = "cosign-creds" @@ -73,6 +74,11 @@ func testSignBuilder(t *testing.T, _ spec.G, it spec.S) { require.NoError(t, err) } + err = clients.client.KpackV1alpha2().ClusterLifecycles().Delete(ctx, clusterLifecycleName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + err = clients.client.KpackV1alpha2().ClusterBuilders().Delete(ctx, clusterBuilderName, metav1.DeleteOptions{}) if !errors.IsNotFound(err) { require.NoError(t, err) @@ -175,6 +181,16 @@ func testSignBuilder(t *testing.T, _ spec.G, it spec.S) { }, }, metav1.CreateOptions{}) require.NoError(t, err) + + _, err = clients.client.KpackV1alpha2().ClusterLifecycles().Create(ctx, &buildapi.ClusterLifecycle{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterLifecycleName, + }, + Spec: buildapi.ClusterLifecycleSpec{ + ImageSource: corev1alpha1.ImageSource{Image: "buildpacksio/lifecycle"}, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) }) it("Signs a Builder image successfully when the key is not password-protected", func() { @@ -205,6 +221,10 @@ func testSignBuilder(t *testing.T, _ spec.G, it spec.S) { Name: clusterStackName, Kind: "ClusterStack", }, + Lifecycle: corev1.ObjectReference{ + Name: clusterLifecycleName, + Kind: "ClusterLifecycle", + }, Store: corev1.ObjectReference{ Name: clusterStoreName, Kind: "ClusterStore", @@ -322,6 +342,10 @@ func testSignBuilder(t *testing.T, _ spec.G, it spec.S) { Name: clusterStackName, Kind: "ClusterStack", }, + Lifecycle: corev1.ObjectReference{ + Name: clusterLifecycleName, + Kind: "ClusterLifecycle", + }, Store: corev1.ObjectReference{ Name: clusterStoreName, Kind: "ClusterStore", @@ -447,6 +471,10 @@ func testSignBuilder(t *testing.T, _ spec.G, it spec.S) { Name: clusterStackName, Kind: "ClusterStack", }, + Lifecycle: corev1.ObjectReference{ + Name: clusterLifecycleName, + Kind: "ClusterLifecycle", + }, Store: corev1.ObjectReference{ Name: clusterStoreName, Kind: "ClusterStore", @@ -575,6 +603,10 @@ func testSignBuilder(t *testing.T, _ spec.G, it spec.S) { Name: clusterStackName, Kind: "ClusterStack", }, + Lifecycle: corev1.ObjectReference{ + Name: clusterLifecycleName, + Kind: "ClusterLifecycle", + }, Store: corev1.ObjectReference{ Name: clusterStoreName, Kind: "ClusterStore", @@ -691,6 +723,10 @@ func testSignBuilder(t *testing.T, _ spec.G, it spec.S) { Name: clusterStackName, Kind: "ClusterStack", }, + Lifecycle: corev1.ObjectReference{ + Name: clusterLifecycleName, + Kind: "ClusterLifecycle", + }, Store: corev1.ObjectReference{ Name: clusterStoreName, Kind: "ClusterStore", @@ -805,6 +841,10 @@ func testSignBuilder(t *testing.T, _ spec.G, it spec.S) { Name: clusterStackName, Kind: "ClusterStack", }, + Lifecycle: corev1.ObjectReference{ + Name: clusterLifecycleName, + Kind: "ClusterLifecycle", + }, Store: corev1.ObjectReference{ Name: clusterStoreName, Kind: "ClusterStore", @@ -928,6 +968,10 @@ func testSignBuilder(t *testing.T, _ spec.G, it spec.S) { Name: clusterStackName, Kind: "ClusterStack", }, + Lifecycle: corev1.ObjectReference{ + Name: clusterLifecycleName, + Kind: "ClusterLifecycle", + }, Store: corev1.ObjectReference{ Name: clusterStoreName, Kind: "ClusterStore", @@ -1053,6 +1097,10 @@ func testSignBuilder(t *testing.T, _ spec.G, it spec.S) { Name: clusterStackName, Kind: "ClusterStack", }, + Lifecycle: corev1.ObjectReference{ + Name: clusterLifecycleName, + Kind: "ClusterLifecycle", + }, Store: corev1.ObjectReference{ Name: clusterStoreName, Kind: "ClusterStore", diff --git a/test/execute_build_test.go b/test/execute_build_test.go index ef8c2ccd4..08c51765f 100644 --- a/test/execute_build_test.go +++ b/test/execute_build_test.go @@ -49,6 +49,7 @@ func testCreateImage(t *testing.T, _ spec.G, it spec.S) { buildpackName = "buildpack" clusterBuildpackName = "cluster-buildpack" clusterStackName = "stack" + clusterLifecycleName = "lifecycle" builderName = "custom-builder" clusterBuilderName = "custom-cluster-builder" ) @@ -154,6 +155,11 @@ func testCreateImage(t *testing.T, _ spec.G, it spec.S) { require.NoError(t, err) } + err = clients.client.KpackV1alpha2().ClusterLifecycles().Delete(ctx, clusterLifecycleName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + err = clients.client.KpackV1alpha2().ClusterBuilders().Delete(ctx, clusterBuilderName, metav1.DeleteOptions{}) if !errors.IsNotFound(err) { require.NoError(t, err) @@ -258,6 +264,16 @@ func testCreateImage(t *testing.T, _ spec.G, it spec.S) { }, metav1.CreateOptions{}) require.NoError(t, err) + _, err = clients.client.KpackV1alpha2().ClusterLifecycles().Create(ctx, &buildapi.ClusterLifecycle{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterLifecycleName, + }, + Spec: buildapi.ClusterLifecycleSpec{ + ImageSource: corev1alpha1.ImageSource{Image: "buildpacksio/lifecycle"}, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + builder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Create(ctx, &buildapi.Builder{ ObjectMeta: metav1.ObjectMeta{ Name: builderName, @@ -270,6 +286,10 @@ func testCreateImage(t *testing.T, _ spec.G, it spec.S) { Name: clusterStackName, Kind: "ClusterStack", }, + Lifecycle: corev1.ObjectReference{ + Name: clusterLifecycleName, + Kind: "ClusterLifecycle", + }, Store: corev1.ObjectReference{ Name: clusterStoreName, Kind: "ClusterStore", @@ -366,6 +386,10 @@ func testCreateImage(t *testing.T, _ spec.G, it spec.S) { Name: clusterStackName, Kind: "ClusterStack", }, + Lifecycle: corev1.ObjectReference{ + Name: clusterLifecycleName, + Kind: "ClusterLifecycle", + }, Store: corev1.ObjectReference{ Name: clusterStoreName, Kind: "ClusterStore", diff --git a/test/slsa_test.go b/test/slsa_test.go index a1bb6fd15..3e91184d7 100644 --- a/test/slsa_test.go +++ b/test/slsa_test.go @@ -60,6 +60,7 @@ func testSlsaBuild(t *testing.T, when spec.G, it spec.S) { buildpackName = "buildpack" clusterBuildpackName = "cluster-buildpack-slsa" clusterStackName = "stack-slsa" + clusterLifecycleName = "lifecycle-slsa" builderName = "custom-builder" clusterBuilderName = "custom-cluster-builder-slsa" cosignSecretName = "cosign-creds" @@ -100,6 +101,11 @@ func testSlsaBuild(t *testing.T, when spec.G, it spec.S) { require.NoError(t, err) } + err = clients.client.KpackV1alpha2().ClusterLifecycles().Delete(ctx, clusterLifecycleName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + err = clients.client.KpackV1alpha2().ClusterBuilders().Delete(ctx, clusterBuilderName, metav1.DeleteOptions{}) if !errors.IsNotFound(err) { require.NoError(t, err) @@ -204,6 +210,16 @@ func testSlsaBuild(t *testing.T, when spec.G, it spec.S) { }, metav1.CreateOptions{}) require.NoError(t, err) + _, err = clients.client.KpackV1alpha2().ClusterLifecycles().Create(ctx, &buildapi.ClusterLifecycle{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterLifecycleName, + }, + Spec: buildapi.ClusterLifecycleSpec{ + ImageSource: corev1alpha1.ImageSource{Image: "buildpacksio/lifecycle"}, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + builder, err := clients.client.KpackV1alpha2().Builders(testNamespace).Create(ctx, &buildapi.Builder{ ObjectMeta: metav1.ObjectMeta{ Name: builderName, @@ -216,6 +232,10 @@ func testSlsaBuild(t *testing.T, when spec.G, it spec.S) { Name: clusterStackName, Kind: "ClusterStack", }, + Lifecycle: corev1.ObjectReference{ + Name: clusterLifecycleName, + Kind: "ClusterLifecycle", + }, Store: corev1.ObjectReference{ Name: clusterStoreName, Kind: "ClusterStore", @@ -312,6 +332,10 @@ func testSlsaBuild(t *testing.T, when spec.G, it spec.S) { Name: clusterStackName, Kind: "ClusterStack", }, + Lifecycle: corev1.ObjectReference{ + Name: clusterLifecycleName, + Kind: "ClusterLifecycle", + }, Store: corev1.ObjectReference{ Name: clusterStoreName, Kind: "ClusterStore",