diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index c7f5d0680..8fc21be6e 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -91,12 +91,6 @@ IMAGE_REGISTRY=gcr.io/ \ make e2e ``` -* The IMAGE_REGISTRY environment variable must point at a registry with local write access - e.g. - -```bash -export IMAGE_REGISTRY="gcr.io/" -``` - * The KPACK_TEST_NAMESPACE_LABELS environment variable allows you to define additional labels for the test namespace, e.g. ```bash diff --git a/Makefile b/Makefile index 08fd0b127..75ba5909a 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,6 @@ unit-ci: $(GOCMD) test ./pkg/... -coverprofile=coverage.txt -covermode=atomic e2e: - $(GOCMD) test --timeout=30m -v ./test/... + $(GOCMD) test --timeout=30m -failfast -v ./test/... .PHONY: unit unit-ci e2e diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 5ec3b38ec..96b723a8b 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -50,8 +50,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/clusterextension" "github.com/pivotal/kpack/pkg/reconciler/clusterstack" "github.com/pivotal/kpack/pkg/reconciler/clusterstore" + "github.com/pivotal/kpack/pkg/reconciler/extension" "github.com/pivotal/kpack/pkg/reconciler/image" "github.com/pivotal/kpack/pkg/reconciler/lifecycle" "github.com/pivotal/kpack/pkg/reconciler/sourceresolver" @@ -207,8 +209,10 @@ func main() { sourceResolverController := sourceresolver.NewController(ctx, options, sourceResolverInformer, gitResolver, blobResolver, registryResolver) builderController, builderResync := builder.NewController(ctx, options, builderInformer, builderCreator, keychainFactory, clusterStoreInformer, buildpackInformer, clusterBuildpackInformer, clusterStackInformer, extensionInformer, clusterExtensionInformer) buildpackController := buildpack.NewController(ctx, options, keychainFactory, buildpackInformer, remoteStoreReader) + extensionController := extension.NewController(ctx, options, keychainFactory, extensionInformer, remoteStoreReader) clusterBuilderController, clusterBuilderResync := clusterbuilder.NewController(ctx, options, clusterBuilderInformer, builderCreator, keychainFactory, clusterStoreInformer, clusterBuildpackInformer, clusterStackInformer, clusterExtensionInformer) clusterBuildpackController := clusterbuildpack.NewController(ctx, options, keychainFactory, clusterBuildpackInformer, remoteStoreReader) + clusterExtensionController := clusterextension.NewController(ctx, options, keychainFactory, clusterExtensionInformer, 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) @@ -243,8 +247,10 @@ func main() { run(buildController, routinesPerController), run(builderController, routinesPerController), run(buildpackController, routinesPerController), + run(extensionController, routinesPerController), run(clusterBuilderController, routinesPerController), run(clusterBuildpackController, routinesPerController), + run(clusterExtensionController, routinesPerController), run(clusterStoreController, routinesPerController), run(lifecycleController, routinesPerController), run(sourceResolverController, 2*routinesPerController), diff --git a/config/clusterextension.yaml b/config/clusterextension.yaml new file mode 100644 index 000000000..8e8b01147 --- /dev/null +++ b/config/clusterextension.yaml @@ -0,0 +1,31 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: clusterextensions.kpack.io +spec: + group: kpack.io + versions: + - 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: ClusterExtension + listKind: ClusterExtensionList + singular: clusterextension + plural: clusterextensions + shortNames: + - clstext + - clstexts + categories: + - kpack + scope: Cluster diff --git a/config/controllerrole.yaml b/config/controllerrole.yaml index 61e5b2af0..972037141 100644 --- a/config/controllerrole.yaml +++ b/config/controllerrole.yaml @@ -23,10 +23,14 @@ rules: - builders/status - buildpacks - buildpacks/status + - extensions + - extensions/status - clusterbuilders - clusterbuilders/status - clusterbuildpacks - clusterbuildpacks/status + - clusterextensions + - clusterextensions/status - clusterstores - clusterstores/status - clusterstacks diff --git a/config/extension.yaml b/config/extension.yaml new file mode 100644 index 000000000..005c47028 --- /dev/null +++ b/config/extension.yaml @@ -0,0 +1,32 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: extensions.kpack.io +spec: + group: kpack.io + versions: + - 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: Extension + listKind: ExtensionList + singular: extension + plural: extensions + shortNames: + - ext + - exts + categories: + - kpack + scope: Namespaced + diff --git a/go.sum b/go.sum index c3b91b4d8..875708d30 100644 --- a/go.sum +++ b/go.sum @@ -58,7 +58,7 @@ dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= -github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230618160516-e936619f9f18 h1:rd389Q26LMy03gG4anandGFC2LW/xvjga5GezeeaxQk= +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo/helper v0.2.0 h1:8+4G8JaejP8Xa6W46PzJEwisNgBXMvFcz78N6zG/ARw= github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo/helper v0.2.0/go.mod h1:GgeIE+1be8Ivm7Sh4RgwI42aTtC9qrcj+Y9Y6CjJhJs= github.com/Azure/azure-sdk-for-go v55.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= diff --git a/pkg/apis/build/v1alpha2/build_pod.go b/pkg/apis/build/v1alpha2/build_pod.go index 21a89dd00..3a0ead662 100644 --- a/pkg/apis/build/v1alpha2/build_pod.go +++ b/pkg/apis/build/v1alpha2/build_pod.go @@ -126,12 +126,13 @@ func (c BuildContext) os() string { } type BuildPodBuilderConfig struct { - StackID string - RunImage string - Uid int64 - Gid int64 - PlatformAPIs []string - OS string + StackID string + RunImage string + Uid int64 + Gid int64 + PlatformAPIs []string + OS string + HasExtensions bool } var ( @@ -629,6 +630,10 @@ func (b *Build) BuildPod(images BuildPodImages, buildContext BuildContext) (*cor }, } + if buildContext.BuildPodBuilderConfig.HasExtensions && buildContext.os() != "windows" { + b.useImageExtensions(pod) + } + if buildContext.InjectedSidecarSupport && buildContext.os() != "windows" { pod = b.useStandardContainers(images.BuildWaiterImage, pod) } @@ -712,8 +717,42 @@ func setUpBuildWaiter(container corev1.Container, waitFile string) corev1.Contai } -func (b *Build) useStandardContainers(buildWaiterImage string, pod *corev1.Pod) *corev1.Pod { +func (b *Build) useImageExtensions(pod *corev1.Pod) { + lifecycleExperimentalEnvVar := corev1.EnvVar{Name: "CNB_EXPERIMENTAL_MODE", Value: "warn"} + kanikoVolume := corev1.Volume{ + Name: "kaniko", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + } + pod.Spec.Volumes = append(pod.Spec.Volumes, kanikoVolume) + kanikoMount := corev1.VolumeMount{ + Name: "kaniko", + MountPath: "/kaniko", + } + + for idx, container := range pod.Spec.InitContainers { + container.Env = append(container.Env, lifecycleExperimentalEnvVar) + switch container.Name { + case RestoreContainerName: + container.VolumeMounts = append(container.VolumeMounts, kanikoMount) + container.Args = append(container.Args, fmt.Sprintf("-build-image=%s", b.Spec.Builder.Image)) + case BuildContainerName: + runAsNonRoot := false + rootUser := int64(0) + container.Name = ExtendContainerName + container.Command = []string{"/cnb/lifecycle/extender"} + container.VolumeMounts = append(container.VolumeMounts, kanikoMount) + container.SecurityContext.RunAsNonRoot = &runAsNonRoot + container.SecurityContext.RunAsUser = &rootUser + } + pod.Spec.InitContainers[idx] = container + } + +} + +func (b *Build) useStandardContainers(buildWaiterImage string, pod *corev1.Pod) *corev1.Pod { containers := pod.Spec.InitContainers pod.Spec.InitContainers = []corev1.Container{ { @@ -1085,7 +1124,12 @@ func (b *Build) setupCosignVolumes(secrets []corev1.Secret) ([]corev1.Volume, [] } var ( - supportedPlatformAPIVersions = []*semver.Version{semver.MustParse("0.9"), semver.MustParse("0.8"), semver.MustParse("0.7")} + supportedPlatformAPIVersions = []*semver.Version{ + semver.MustParse("0.10"), + semver.MustParse("0.9"), + semver.MustParse("0.8"), + semver.MustParse("0.7"), + } ) func (bc BuildContext) highestSupportedPlatformAPI(b *Build) (*semver.Version, error) { diff --git a/pkg/apis/build/v1alpha2/build_pod_test.go b/pkg/apis/build/v1alpha2/build_pod_test.go index 6ec78d9fb..e89ee8d10 100644 --- a/pkg/apis/build/v1alpha2/build_pod_test.go +++ b/pkg/apis/build/v1alpha2/build_pod_test.go @@ -889,6 +889,7 @@ func testBuildPod(t *testing.T, when spec.G, it spec.S) { "someimage/name:tag3", }, pod.Spec.InitContainers[5].Args) }) + it("configures export step with non-web default process", func() { build.Spec.DefaultProcess = "sys-info" pod, err := build.BuildPod(config, buildContext) @@ -2460,6 +2461,89 @@ func testBuildPod(t *testing.T, when spec.G, it spec.S) { }) }) + when("builder has extensions", func() { + it.Before(func() { + buildContext.BuildPodBuilderConfig.HasExtensions = true + }) + + it("sets CNB_EXPERIMENTAL_MODE=warn in the lifecycle env", func() { + pod, err := build.BuildPod(config, buildContext) + require.NoError(t, err) + + for _, container := range pod.Spec.InitContainers { + assert.Contains(t, container.Env, + corev1.EnvVar{ + Name: "CNB_EXPERIMENTAL_MODE", + Value: "warn", + }, + ) + } + }) + + it("provides -build-image to the restorer", func() { + pod, err := build.BuildPod(config, buildContext) + require.NoError(t, err) + + assert.Contains(t, pod.Spec.InitContainers[3].Args, "-build-image="+builderImage) + }) + + it("adds kaniko volume to pod and mounts it during restore and extend", func() { + pod, err := build.BuildPod(config, buildContext) + require.NoError(t, err) + + assert.Contains(t, pod.Spec.Volumes, corev1.Volume{ + Name: "kaniko", + VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}, + }) + + for _, container := range pod.Spec.InitContainers { + switch container.Name { + case buildapi.RestoreContainerName, buildapi.ExtendContainerName: + assert.Contains(t, container.VolumeMounts, corev1.VolumeMount{ + Name: "kaniko", + MountPath: "/kaniko", + }) + default: + assert.NotContains(t, container.VolumeMounts, corev1.VolumeMount{ + Name: "kaniko", + MountPath: "/kaniko", + }) + } + } + }) + + it("runs the extender (as root) instead of the builder", func() { + pod, err := build.BuildPod(config, buildContext) + require.NoError(t, err) + + assert.Equal(t, buildapi.ExtendContainerName, pod.Spec.InitContainers[4].Name) + assert.Equal(t, []string{"/cnb/lifecycle/extender"}, pod.Spec.InitContainers[4].Command) + + for _, container := range pod.Spec.InitContainers { + actualRunAsNonRoot := container.SecurityContext.RunAsNonRoot + actualRunAsUser := container.SecurityContext.RunAsUser + switch container.Name { + case buildapi.ExtendContainerName: + assert.Equal(t, false, *actualRunAsNonRoot) + assert.Equal(t, int64(0), *actualRunAsUser) + default: + assert.Equal(t, true, *actualRunAsNonRoot) + assert.NotEqual(t, nil, actualRunAsUser) // in real life this would be the user from the builder + } + } + }) + + it("is possible to use standard containers", func() { + buildContext.InjectedSidecarSupport = true + config.BuildWaiterImage = "some-image" + + pod, err := build.BuildPod(config, buildContext) + require.NoError(t, err) + + assert.Equal(t, buildapi.ExtendContainerName, pod.Spec.Containers[4].Name) + }) + }) + when("complying with the restricted pod security standard", func() { // enforces https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted var pod *corev1.Pod diff --git a/pkg/apis/build/v1alpha2/builder_conversion.go b/pkg/apis/build/v1alpha2/builder_conversion.go index 3c0cf6ef8..19b5e3c49 100644 --- a/pkg/apis/build/v1alpha2/builder_conversion.go +++ b/pkg/apis/build/v1alpha2/builder_conversion.go @@ -82,7 +82,7 @@ func (bs *NamespacedBuilderSpec) convertFrom(from *v1alpha1.NamespacedBuilderSpe func (bst *BuilderStatus) convertFrom(from *v1alpha1.BuilderStatus) { bst.Status = from.Status - bst.BuilderMetadata = from.BuilderMetadata + bst.BuilderMetadataBuildpacks = from.BuilderMetadata bst.Order = from.Order bst.Stack = from.Stack bst.LatestImage = from.LatestImage @@ -93,7 +93,7 @@ func (bst *BuilderStatus) convertFrom(from *v1alpha1.BuilderStatus) { func (bst *BuilderStatus) convertTo(to *v1alpha1.BuilderStatus) { to.Status = bst.Status - to.BuilderMetadata = bst.BuilderMetadata + to.BuilderMetadata = bst.BuilderMetadataBuildpacks to.Order = bst.Order to.Stack = bst.Stack to.LatestImage = bst.LatestImage diff --git a/pkg/apis/build/v1alpha2/builder_conversion_test.go b/pkg/apis/build/v1alpha2/builder_conversion_test.go index 94330e400..4de87d954 100644 --- a/pkg/apis/build/v1alpha2/builder_conversion_test.go +++ b/pkg/apis/build/v1alpha2/builder_conversion_test.go @@ -58,9 +58,9 @@ func testBuilderConversion(t *testing.T, when spec.G, it spec.S) { ServiceAccountName: "some-service-account", }, Status: BuilderStatus{ - Status: corev1alpha1.Status{Conditions: corev1alpha1.Conditions{{Type: "some-type"}}}, - BuilderMetadata: nil, - Order: nil, + Status: corev1alpha1.Status{Conditions: corev1alpha1.Conditions{{Type: "some-type"}}}, + BuilderMetadataBuildpacks: nil, + Order: nil, Stack: corev1alpha1.BuildStack{ RunImage: "", ID: "", diff --git a/pkg/apis/build/v1alpha2/builder_lifecycle.go b/pkg/apis/build/v1alpha2/builder_lifecycle.go index 9ebceee22..d351d654b 100644 --- a/pkg/apis/build/v1alpha2/builder_lifecycle.go +++ b/pkg/apis/build/v1alpha2/builder_lifecycle.go @@ -12,6 +12,7 @@ type BuilderRecord struct { Image string Stack corev1alpha1.BuildStack Buildpacks corev1alpha1.BuildpackMetadataList + Extensions corev1alpha1.BuildpackMetadataList Order []corev1alpha1.OrderEntry OrderExtensions []corev1alpha1.OrderEntry ObservedStoreGeneration int64 @@ -21,7 +22,8 @@ type BuilderRecord struct { func (bs *BuilderStatus) BuilderRecord(record BuilderRecord) { bs.Stack = record.Stack - bs.BuilderMetadata = record.Buildpacks + bs.BuilderMetadataBuildpacks = record.Buildpacks + bs.BuilderMetadataExtensions = record.Extensions bs.LatestImage = record.Image bs.Conditions = corev1alpha1.Conditions{ { diff --git a/pkg/apis/build/v1alpha2/builder_types.go b/pkg/apis/build/v1alpha2/builder_types.go index b4449d0d4..e39d2c836 100644 --- a/pkg/apis/build/v1alpha2/builder_types.go +++ b/pkg/apis/build/v1alpha2/builder_types.go @@ -58,15 +58,16 @@ type NamespacedBuilderSpec struct { // +k8s:openapi-gen=true type BuilderStatus struct { - corev1alpha1.Status `json:",inline"` - BuilderMetadata corev1alpha1.BuildpackMetadataList `json:"builderMetadata,omitempty"` - Order []corev1alpha1.OrderEntry `json:"order,omitempty"` - OrderExtensions []corev1alpha1.OrderEntry `json:"order-extensions,omitempty"` - Stack corev1alpha1.BuildStack `json:"stack,omitempty"` - LatestImage string `json:"latestImage,omitempty"` - ObservedStackGeneration int64 `json:"observedStackGeneration,omitempty"` - ObservedStoreGeneration int64 `json:"observedStoreGeneration,omitempty"` - OS string `json:"os,omitempty"` + corev1alpha1.Status `json:",inline"` + BuilderMetadataBuildpacks corev1alpha1.BuildpackMetadataList `json:"builderMetadata,omitempty"` + BuilderMetadataExtensions corev1alpha1.BuildpackMetadataList `json:"builderMetadataExtensions,omitempty"` + Order []corev1alpha1.OrderEntry `json:"order,omitempty"` + OrderExtensions []corev1alpha1.OrderEntry `json:"order-extensions,omitempty"` + Stack corev1alpha1.BuildStack `json:"stack,omitempty"` + LatestImage string `json:"latestImage,omitempty"` + ObservedStackGeneration int64 `json:"observedStackGeneration,omitempty"` + ObservedStoreGeneration int64 `json:"observedStoreGeneration,omitempty"` + OS string `json:"os,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/build/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/build/v1alpha2/zz_generated.deepcopy.go index e22d3f26c..52001390b 100644 --- a/pkg/apis/build/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/build/v1alpha2/zz_generated.deepcopy.go @@ -524,8 +524,8 @@ func (in *BuilderSpec) DeepCopy() *BuilderSpec { func (in *BuilderStatus) DeepCopyInto(out *BuilderStatus) { *out = *in in.Status.DeepCopyInto(&out.Status) - if in.BuilderMetadata != nil { - in, out := &in.BuilderMetadata, &out.BuilderMetadata + if in.BuilderMetadataBuildpacks != nil { + in, out := &in.BuilderMetadataBuildpacks, &out.BuilderMetadataBuildpacks *out = make(v1alpha1.BuildpackMetadataList, len(*in)) copy(*out, *in) } diff --git a/pkg/buildpod/generator.go b/pkg/buildpod/generator.go index 41a8b2964..49c997aff 100644 --- a/pkg/buildpod/generator.go +++ b/pkg/buildpod/generator.go @@ -2,10 +2,12 @@ package buildpod import ( "context" + "encoding/json" "fmt" "strconv" "github.com/Masterminds/semver/v3" + "github.com/buildpacks/lifecycle/buildpack" "github.com/buildpacks/lifecycle/platform" "github.com/google/go-containerregistry/pkg/authn" ggcrv1 "github.com/google/go-containerregistry/pkg/v1" @@ -27,6 +29,7 @@ import ( const ( builderMetadataLabel = "io.buildpacks.builder.metadata" + extensionOrderLabel = "io.buildpacks.buildpack.order-extensions" cnbUserId = "CNB_USER_ID" cnbGroupId = "CNB_GROUP_ID" ) @@ -254,15 +257,28 @@ func (g *Generator) fetchBuilderConfig(ctx context.Context, build BuildPodable) } return buildapi.BuildPodBuilderConfig{ - StackID: stackId, - RunImage: metadata.Stack.RunImage.Image, - PlatformAPIs: append(metadata.Lifecycle.APIs.Platform.Deprecated, metadata.Lifecycle.APIs.Platform.Supported...), - Uid: uid, - Gid: gid, - OS: config.OS, + StackID: stackId, + RunImage: metadata.Stack.RunImage.Image, + PlatformAPIs: append(metadata.Lifecycle.APIs.Platform.Deprecated, metadata.Lifecycle.APIs.Platform.Supported...), + Uid: uid, + Gid: gid, + OS: config.OS, + HasExtensions: hasExtensions(image), }, nil } +func hasExtensions(image ggcrv1.Image) bool { + orderExtLabel, err := imagehelpers.GetStringLabel(image, extensionOrderLabel) + if err != nil { + return false + } + var orderExt []buildpack.GroupElement + if err := json.Unmarshal([]byte(orderExtLabel), &orderExt); err != nil { + return false + } + return len(orderExt) > 0 +} + func parseCNBID(image ggcrv1.Image, env string) (int64, error) { v, err := imagehelpers.GetEnv(image, env) if err != nil { diff --git a/pkg/buildpod/generator_test.go b/pkg/buildpod/generator_test.go index 61d615c6c..aa92399a9 100644 --- a/pkg/buildpod/generator_test.go +++ b/pkg/buildpod/generator_test.go @@ -51,10 +51,11 @@ func TestGenerator(t *testing.T) { func testGenerator(t *testing.T, when spec.G, it spec.S) { when("Generate", func() { const ( - serviceAccountName = "serviceAccountName" - namespace = "some-namespace" - windowsBuilderImage = "builder/windows" - linuxBuilderImage = "builder/linux" + serviceAccountName = "serviceAccountName" + namespace = "some-namespace" + windowsBuilderImage = "builder/windows" + linuxBuilderImage = "builder/linux" + linuxBuilderImageWithExtensions = "builder/linux-with-extensions" ) var ( @@ -192,8 +193,9 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) { it.Before(func() { keychainFactory.AddKeychainForSecretRef(t, secretRef, keychain) - imageFetcher.AddImage(linuxBuilderImage, createImage(t, "linux"), keychain) - imageFetcher.AddImage(windowsBuilderImage, createImage(t, "windows"), keychain) + imageFetcher.AddImage(linuxBuilderImage, createImage(t, "linux", false), keychain) + imageFetcher.AddImage(linuxBuilderImageWithExtensions, createImage(t, "linux", true), keychain) + imageFetcher.AddImage(windowsBuilderImage, createImage(t, "windows", false), keychain) }) it("invokes the BuildPod with the builder and env config", func() { @@ -238,6 +240,51 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) { }}, build.buildPodCalls) }) + when("order contains extensions", func() { + it("invokes the BuildPod with the builder and env config", func() { + var build = &testBuildPodable{ + serviceAccount: serviceAccountName, + namespace: namespace, + buildBuilderSpec: corev1alpha1.BuildBuilderSpec{ + Image: linuxBuilderImageWithExtensions, + ImagePullSecrets: builderPullSecrets, + }, + } + + pod, err := generator.Generate(context.TODO(), build) + require.NoError(t, err) + assert.NotNil(t, pod) + + assert.Equal(t, []buildPodCall{{ + BuildPodImages: buildPodConfig, + BuildContext: buildapi.BuildContext{ + Secrets: []corev1.Secret{ + *gitSecret, + *dockerSecret, + }, + BuildPodBuilderConfig: buildapi.BuildPodBuilderConfig{ + StackID: "some.stack.id", + RunImage: "some-registry.io/run-image", + Uid: 1234, + Gid: 5678, + PlatformAPIs: []string{"0.4", "0.5", "0.6"}, + OS: "linux", + HasExtensions: true, + }, + Bindings: []buildapi.ServiceBinding{}, + ImagePullSecrets: []corev1.LocalObjectReference{ + { + Name: "image-pull-1", + }, + { + Name: "image-pull-2", + }, + }, + }, + }}, build.buildPodCalls) + }) + }) + it("dedups duplicate secrets on the service account", func() { var build = &testBuildPodable{ serviceAccount: serviceAccountName, @@ -602,7 +649,7 @@ func (tb *testBuildPodable) Services() buildapi.Services { return tb.services } -func createImage(t *testing.T, os string) ggcrv1.Image { +func createImage(t *testing.T, os string, withExtensions bool) ggcrv1.Image { image := randomImage(t) var err error @@ -654,6 +701,12 @@ func createImage(t *testing.T, os string) ggcrv1.Image { }`) require.NoError(t, err) + if withExtensions { + image, err = imagehelpers.SetStringLabel(image, "io.buildpacks.buildpack.order-extensions", //language=json + `[{"group":[{"id":"samples/curl","version":"0.0.1"}]}]`) + require.NoError(t, err) + } + image, err = imagehelpers.SetEnv(image, "CNB_USER_ID", "1234") require.NoError(t, err) diff --git a/pkg/cnb/builder_builder.go b/pkg/cnb/builder_builder.go index c49e92be5..0b7b6e7f9 100644 --- a/pkg/cnb/builder_builder.go +++ b/pkg/cnb/builder_builder.go @@ -35,7 +35,7 @@ const ( var ( normalizedTime = time.Date(1980, time.January, 1, 0, 0, 1, 0, time.UTC) - supportedPlatformApis = []string{"0.3", "0.4", "0.5", "0.6", "0.7", "0.8"} + supportedPlatformApis = []string{"0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9", "0.10"} ) type builderBlder struct { @@ -328,7 +328,7 @@ func (bb *builderBlder) orderLayer() (v1.Layer, error) { orderExt = append(orderExt, tomlOrderEntry{Group: exts}) } - err := toml.NewEncoder(orderBuf).Encode(tomlOrderFile{order, orderExt}) + err := toml.NewEncoder(orderBuf).Encode(tomlOrderFile{Order: order, OrderExtensions: orderExt}) if err != nil { return nil, err } diff --git a/pkg/cnb/create_builder.go b/pkg/cnb/create_builder.go index 9b6d99ea1..3e8f1da5d 100644 --- a/pkg/cnb/create_builder.go +++ b/pkg/cnb/create_builder.go @@ -103,6 +103,7 @@ func (r *RemoteBuilderCreator) CreateBuilder(ctx context.Context, builderKeychai ID: clusterStack.Status.Id, }, Buildpacks: buildpackMetadata(builderBldr.buildpacks()), + Extensions: buildpackMetadata(builderBldr.extensions()), Order: builderBldr.order, OrderExtensions: builderBldr.orderExtensions, ObservedStackGeneration: clusterStack.Status.ObservedGeneration, diff --git a/pkg/duckbuilder/duck_builder.go b/pkg/duckbuilder/duck_builder.go index 0b61a4960..3a7942ee4 100644 --- a/pkg/duckbuilder/duck_builder.go +++ b/pkg/duckbuilder/duck_builder.go @@ -44,8 +44,9 @@ func (b *DuckBuilder) BuildBuilderSpec() corev1alpha1.BuildBuilderSpec { } } +// TODO: add for extensions? func (b *DuckBuilder) BuildpackMetadata() corev1alpha1.BuildpackMetadataList { - return b.Status.BuilderMetadata + return b.Status.BuilderMetadataBuildpacks } func (b *DuckBuilder) RunImage() string { diff --git a/pkg/duckbuilder/duck_builder_test.go b/pkg/duckbuilder/duck_builder_test.go index 80b6b8f33..eae18bb47 100644 --- a/pkg/duckbuilder/duck_builder_test.go +++ b/pkg/duckbuilder/duck_builder_test.go @@ -38,7 +38,7 @@ func testDuckBuilder(t *testing.T, when spec.G, it spec.S) { }, }, }, - BuilderMetadata: corev1alpha1.BuildpackMetadataList{ + BuilderMetadataBuildpacks: corev1alpha1.BuildpackMetadataList{ { Id: "test.builder", Version: "test.version", diff --git a/pkg/duckbuilder/informer_test.go b/pkg/duckbuilder/informer_test.go index 35f4f6846..269c80ae6 100644 --- a/pkg/duckbuilder/informer_test.go +++ b/pkg/duckbuilder/informer_test.go @@ -40,7 +40,7 @@ func testDuckBuilderInformer(t *testing.T, when spec.G, it spec.S) { }, Spec: buildapi.NamespacedBuilderSpec{}, Status: buildapi.BuilderStatus{ - BuilderMetadata: corev1alpha1.BuildpackMetadataList{ + BuilderMetadataBuildpacks: corev1alpha1.BuildpackMetadataList{ { Id: "another-buildpack", Version: "another-version", @@ -57,7 +57,7 @@ func testDuckBuilderInformer(t *testing.T, when spec.G, it spec.S) { }, Spec: buildapi.ClusterBuilderSpec{}, Status: buildapi.BuilderStatus{ - BuilderMetadata: corev1alpha1.BuildpackMetadataList{ + BuilderMetadataBuildpacks: corev1alpha1.BuildpackMetadataList{ { Id: "another-buildpack", Version: "another-version", diff --git a/pkg/reconciler/builder/builder_test.go b/pkg/reconciler/builder/builder_test.go index 13cbcab7a..eb21d0326 100644 --- a/pkg/reconciler/builder/builder_test.go +++ b/pkg/reconciler/builder/builder_test.go @@ -236,7 +236,7 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { }, }, }, - BuilderMetadata: []corev1alpha1.BuildpackMetadata{ + BuilderMetadataBuildpacks: []corev1alpha1.BuildpackMetadata{ { Id: "buildpack.id.1", Version: "1.0.0", @@ -317,7 +317,7 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { }, }, }, - BuilderMetadata: []corev1alpha1.BuildpackMetadata{}, + BuilderMetadataBuildpacks: []corev1alpha1.BuildpackMetadata{}, Stack: corev1alpha1.BuildStack{ RunImage: "example.com/run-image@sha256:123456", ID: "fake.stack.id", @@ -384,7 +384,7 @@ func testBuilderReconciler(t *testing.T, when spec.G, it spec.S) { }, }, }, - BuilderMetadata: []corev1alpha1.BuildpackMetadata{ + BuilderMetadataBuildpacks: []corev1alpha1.BuildpackMetadata{ { Id: "buildpack.id.1", Version: "1.0.0", diff --git a/pkg/reconciler/buildpack/buildpack.go b/pkg/reconciler/buildpack/buildpack.go index b818d6059..b29fb4f13 100644 --- a/pkg/reconciler/buildpack/buildpack.go +++ b/pkg/reconciler/buildpack/buildpack.go @@ -21,7 +21,6 @@ import ( "github.com/pivotal/kpack/pkg/registry" ) -// TODO: add for extensions const ( ReconcilerName = "Buildpacks" Kind = "Buildpack" diff --git a/pkg/reconciler/clusterbuilder/clusterbuilder_test.go b/pkg/reconciler/clusterbuilder/clusterbuilder_test.go index 6fd76bbe8..b8b11581a 100644 --- a/pkg/reconciler/clusterbuilder/clusterbuilder_test.go +++ b/pkg/reconciler/clusterbuilder/clusterbuilder_test.go @@ -221,7 +221,7 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { }, }, }, - BuilderMetadata: []corev1alpha1.BuildpackMetadata{ + BuilderMetadataBuildpacks: []corev1alpha1.BuildpackMetadata{ { Id: "buildpack.id.1", Version: "1.0.0", @@ -301,7 +301,7 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { }, }, }, - BuilderMetadata: []corev1alpha1.BuildpackMetadata{}, + BuilderMetadataBuildpacks: []corev1alpha1.BuildpackMetadata{}, Stack: corev1alpha1.BuildStack{ RunImage: "example.com/run-image@sha256:123456", ID: "fake.stack.id", @@ -356,7 +356,7 @@ func testClusterBuilderReconciler(t *testing.T, when spec.G, it spec.S) { }, }, }, - BuilderMetadata: []corev1alpha1.BuildpackMetadata{ + BuilderMetadataBuildpacks: []corev1alpha1.BuildpackMetadata{ { Id: "buildpack.id.1", Version: "1.0.0", diff --git a/pkg/reconciler/clusterbuildpack/clusterbuildpack.go b/pkg/reconciler/clusterbuildpack/clusterbuildpack.go index 300539b35..8d11742ec 100644 --- a/pkg/reconciler/clusterbuildpack/clusterbuildpack.go +++ b/pkg/reconciler/clusterbuildpack/clusterbuildpack.go @@ -21,7 +21,6 @@ import ( "github.com/pivotal/kpack/pkg/registry" ) -// TODO: add for extensions const ( ReconcilerName = "ClusterBuildpacks" Kind = "ClusterBuildpack" diff --git a/pkg/reconciler/clusterextension/clusterextension.go b/pkg/reconciler/clusterextension/clusterextension.go new file mode 100644 index 000000000..b73febb3c --- /dev/null +++ b/pkg/reconciler/clusterextension/clusterextension.go @@ -0,0 +1,143 @@ +package clusterextension + +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 = "ClusterExtensions" +) + +//go:generate counterfeiter . StoreReader +type StoreReader interface { + ReadExtension(keychain authn.Keychain, storeImages []corev1alpha1.ImageSource) ([]corev1alpha1.BuildpackStatus, error) +} + +func NewController( + ctx context.Context, + opt reconciler.Options, + keychainFactory registry.KeychainFactory, + informer buildinformers.ClusterExtensionInformer, + storeReader StoreReader) *controller.Impl { + c := &Reconciler{ + Client: opt.Client, + Lister: informer.Lister(), + StoreReader: storeReader, + KeychainFactory: keychainFactory, + } + + logger := opt.Logger.With( + zap.String(logkey.Kind, buildapi.ClusterExtensionCRName), + ) + + impl := controller.NewContext( + ctx, + &reconciler.NetworkErrorReconciler{ + Reconciler: c, + }, + controller.ControllerOptions{WorkQueueName: ReconcilerName, Logger: logger}, + ) + informer.Informer().AddEventHandler(controller.HandleAll(impl.Enqueue)) + return impl +} + +type Reconciler struct { + Client versioned.Interface + StoreReader StoreReader + Lister buildlisters.ClusterExtensionLister + KeychainFactory registry.KeychainFactory +} + +func (c *Reconciler) Reconcile(ctx context.Context, key string) error { + _, moduleName, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + return err + } + + module, err := c.Lister.Get(moduleName) + if k8serrors.IsNotFound(err) { + return nil + } else if err != nil { + return err + } + + module = module.DeepCopy() + + module, err = c.reconcileStatus(ctx, module) + + updateErr := c.updateStatus(ctx, module) + if updateErr != nil { + return updateErr + } + + if err != nil { + return err + } + return nil +} + +func (c *Reconciler) updateStatus(ctx context.Context, desired *buildapi.ClusterExtension) error { + desired.Status.ObservedGeneration = desired.Generation + + original, err := c.Lister.Get(desired.Name) + if err != nil { + return err + } + + if equality.Semantic.DeepEqual(desired.Status, original.Status) { + return nil + } + + _, err = c.Client.KpackV1alpha2().ClusterExtensions().UpdateStatus(ctx, desired, metav1.UpdateOptions{}) + return err +} + +func (c *Reconciler) reconcileStatus(ctx context.Context, module *buildapi.ClusterExtension) (*buildapi.ClusterExtension, error) { + secretRef := registry.SecretRef{} + + if module.Spec.ServiceAccountRef != nil { + secretRef = registry.SecretRef{ + ServiceAccount: module.Spec.ServiceAccountRef.Name, + Namespace: module.Spec.ServiceAccountRef.Namespace, + } + } + + keychain, err := c.KeychainFactory.KeychainForSecretRef(ctx, secretRef) + if err != nil { + module.Status = buildapi.ClusterExtensionStatus{ + Status: corev1alpha1.CreateStatusWithReadyCondition(module.Generation, err), + } + return module, err + } + + modules, err := c.StoreReader.ReadExtension(keychain, []corev1alpha1.ImageSource{module.Spec.ImageSource}) + if err != nil { + module.Status = buildapi.ClusterExtensionStatus{ + Status: corev1alpha1.CreateStatusWithReadyCondition(module.Generation, err), + } + return module, err + } + + module.Status = buildapi.ClusterExtensionStatus{ + Extensions: modules, + Status: corev1alpha1.CreateStatusWithReadyCondition(module.Generation, nil), + } + return module, nil +} diff --git a/pkg/reconciler/clusterextension/clusterextensionfakes/fake_store_reader.go b/pkg/reconciler/clusterextension/clusterextensionfakes/fake_store_reader.go new file mode 100644 index 000000000..775e2df32 --- /dev/null +++ b/pkg/reconciler/clusterextension/clusterextensionfakes/fake_store_reader.go @@ -0,0 +1,125 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package clusterextensionfakes + +import ( + "sync" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" + clusterbuildpack "github.com/pivotal/kpack/pkg/reconciler/clusterextension" +) + +type FakeStoreReader struct { + ReadExtensionStub func(authn.Keychain, []v1alpha1.ImageSource) ([]v1alpha1.BuildpackStatus, error) + readExtensionMutex sync.RWMutex + readExtensionArgsForCall []struct { + arg1 authn.Keychain + arg2 []v1alpha1.ImageSource + } + readExtensionReturns struct { + result1 []v1alpha1.BuildpackStatus + result2 error + } + readExtensionReturnsOnCall map[int]struct { + result1 []v1alpha1.BuildpackStatus + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeStoreReader) ReadExtension(arg1 authn.Keychain, arg2 []v1alpha1.ImageSource) ([]v1alpha1.BuildpackStatus, error) { + var arg2Copy []v1alpha1.ImageSource + if arg2 != nil { + arg2Copy = make([]v1alpha1.ImageSource, len(arg2)) + copy(arg2Copy, arg2) + } + fake.readExtensionMutex.Lock() + ret, specificReturn := fake.readExtensionReturnsOnCall[len(fake.readExtensionArgsForCall)] + fake.readExtensionArgsForCall = append(fake.readExtensionArgsForCall, struct { + arg1 authn.Keychain + arg2 []v1alpha1.ImageSource + }{arg1, arg2Copy}) + stub := fake.ReadExtensionStub + fakeReturns := fake.readExtensionReturns + fake.recordInvocation("ReadExtension", []interface{}{arg1, arg2Copy}) + fake.readExtensionMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeStoreReader) ReadExtensionCallCount() int { + fake.readExtensionMutex.RLock() + defer fake.readExtensionMutex.RUnlock() + return len(fake.readExtensionArgsForCall) +} + +func (fake *FakeStoreReader) ReadExtensionCalls(stub func(authn.Keychain, []v1alpha1.ImageSource) ([]v1alpha1.BuildpackStatus, error)) { + fake.readExtensionMutex.Lock() + defer fake.readExtensionMutex.Unlock() + fake.ReadExtensionStub = stub +} + +func (fake *FakeStoreReader) ReadExtensionArgsForCall(i int) (authn.Keychain, []v1alpha1.ImageSource) { + fake.readExtensionMutex.RLock() + defer fake.readExtensionMutex.RUnlock() + argsForCall := fake.readExtensionArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeStoreReader) ReadExtensionReturns(result1 []v1alpha1.BuildpackStatus, result2 error) { + fake.readExtensionMutex.Lock() + defer fake.readExtensionMutex.Unlock() + fake.ReadExtensionStub = nil + fake.readExtensionReturns = struct { + result1 []v1alpha1.BuildpackStatus + result2 error + }{result1, result2} +} + +func (fake *FakeStoreReader) ReadExtensionReturnsOnCall(i int, result1 []v1alpha1.BuildpackStatus, result2 error) { + fake.readExtensionMutex.Lock() + defer fake.readExtensionMutex.Unlock() + fake.ReadExtensionStub = nil + if fake.readExtensionReturnsOnCall == nil { + fake.readExtensionReturnsOnCall = make(map[int]struct { + result1 []v1alpha1.BuildpackStatus + result2 error + }) + } + fake.readExtensionReturnsOnCall[i] = struct { + result1 []v1alpha1.BuildpackStatus + result2 error + }{result1, result2} +} + +func (fake *FakeStoreReader) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.readExtensionMutex.RLock() + defer fake.readExtensionMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeStoreReader) 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 _ clusterbuildpack.StoreReader = new(FakeStoreReader) diff --git a/pkg/reconciler/extension/extension.go b/pkg/reconciler/extension/extension.go new file mode 100644 index 000000000..38f917e69 --- /dev/null +++ b/pkg/reconciler/extension/extension.go @@ -0,0 +1,144 @@ +package extension + +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 = "Extensions" +) + +//go:generate counterfeiter . StoreReader +type StoreReader interface { + ReadExtension(keychain authn.Keychain, storeImages []corev1alpha1.ImageSource) ([]corev1alpha1.BuildpackStatus, error) +} + +func NewController( + ctx context.Context, + opt reconciler.Options, + keychainFactory registry.KeychainFactory, + informer buildinformers.ExtensionInformer, + storeReader StoreReader, +) *controller.Impl { + c := &Reconciler{ + Client: opt.Client, + Lister: informer.Lister(), + StoreReader: storeReader, + KeychainFactory: keychainFactory, + } + + logger := opt.Logger.With( + zap.String(logkey.Kind, buildapi.ExtensionCRName), + ) + + impl := controller.NewContext( + ctx, + &reconciler.NetworkErrorReconciler{ + Reconciler: c, + }, + controller.ControllerOptions{WorkQueueName: ReconcilerName, Logger: logger}, + ) + informer.Informer().AddEventHandler(controller.HandleAll(impl.Enqueue)) + return impl +} + +type Reconciler struct { + Client versioned.Interface + StoreReader StoreReader + Lister buildlisters.ExtensionLister + KeychainFactory registry.KeychainFactory +} + +func (c *Reconciler) Reconcile(ctx context.Context, key string) error { + namespace, moduleName, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + return err + } + + module, err := c.Lister.Extensions(namespace).Get(moduleName) + if k8serrors.IsNotFound(err) { + return nil + } else if err != nil { + return err + } + + module = module.DeepCopy() + + module, err = c.reconcileExtensionStatus(ctx, module) + + updateErr := c.updateModuleStatus(ctx, module) + if updateErr != nil { + return updateErr + } + + if err != nil { + return err + } + return nil +} + +func (c *Reconciler) updateModuleStatus(ctx context.Context, desired *buildapi.Extension) error { + desired.Status.ObservedGeneration = desired.Generation + + original, err := c.Lister.Extensions(desired.Namespace).Get(desired.Name) + if err != nil { + return err + } + + if equality.Semantic.DeepEqual(desired.Status, original.Status) { + return nil + } + + _, err = c.Client.KpackV1alpha2().Extensions(desired.Namespace).UpdateStatus(ctx, desired, metav1.UpdateOptions{}) + return err +} + +func (c *Reconciler) reconcileExtensionStatus(ctx context.Context, module *buildapi.Extension) (*buildapi.Extension, error) { + secretRef := registry.SecretRef{} + + if module.Spec.ServiceAccountName != "" { + secretRef = registry.SecretRef{ + ServiceAccount: module.Spec.ServiceAccountName, + Namespace: module.Namespace, + } + } + + keychain, err := c.KeychainFactory.KeychainForSecretRef(ctx, secretRef) + if err != nil { + module.Status = buildapi.ExtensionStatus{ + Status: corev1alpha1.CreateStatusWithReadyCondition(module.Generation, err), + } + return module, err + } + + modules, err := c.StoreReader.ReadExtension(keychain, []corev1alpha1.ImageSource{module.Spec.ImageSource}) + if err != nil { + module.Status = buildapi.ExtensionStatus{ + Status: corev1alpha1.CreateStatusWithReadyCondition(module.Generation, err), + } + return module, err + } + + module.Status = buildapi.ExtensionStatus{ + Extensions: modules, + Status: corev1alpha1.CreateStatusWithReadyCondition(module.Generation, nil), + } + return module, nil +} diff --git a/pkg/reconciler/extension/extensionfakes/fake_store_reader.go b/pkg/reconciler/extension/extensionfakes/fake_store_reader.go new file mode 100644 index 000000000..098b5d020 --- /dev/null +++ b/pkg/reconciler/extension/extensionfakes/fake_store_reader.go @@ -0,0 +1,125 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package extensionfakes + +import ( + "sync" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/pivotal/kpack/pkg/apis/core/v1alpha1" + "github.com/pivotal/kpack/pkg/reconciler/extension" +) + +type FakeStoreReader struct { + ReadExtensionStub func(authn.Keychain, []v1alpha1.ImageSource) ([]v1alpha1.BuildpackStatus, error) + readExtensionMutex sync.RWMutex + readExtensionArgsForCall []struct { + arg1 authn.Keychain + arg2 []v1alpha1.ImageSource + } + readExtensionReturns struct { + result1 []v1alpha1.BuildpackStatus + result2 error + } + readExtensionReturnsOnCall map[int]struct { + result1 []v1alpha1.BuildpackStatus + result2 error + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeStoreReader) ReadExtension(arg1 authn.Keychain, arg2 []v1alpha1.ImageSource) ([]v1alpha1.BuildpackStatus, error) { + var arg2Copy []v1alpha1.ImageSource + if arg2 != nil { + arg2Copy = make([]v1alpha1.ImageSource, len(arg2)) + copy(arg2Copy, arg2) + } + fake.readExtensionMutex.Lock() + ret, specificReturn := fake.readExtensionReturnsOnCall[len(fake.readExtensionArgsForCall)] + fake.readExtensionArgsForCall = append(fake.readExtensionArgsForCall, struct { + arg1 authn.Keychain + arg2 []v1alpha1.ImageSource + }{arg1, arg2Copy}) + stub := fake.ReadExtensionStub + fakeReturns := fake.readExtensionReturns + fake.recordInvocation("ReadExtension", []interface{}{arg1, arg2Copy}) + fake.readExtensionMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeStoreReader) ReadExtensionCallCount() int { + fake.readExtensionMutex.RLock() + defer fake.readExtensionMutex.RUnlock() + return len(fake.readExtensionArgsForCall) +} + +func (fake *FakeStoreReader) ReadExtensionCalls(stub func(authn.Keychain, []v1alpha1.ImageSource) ([]v1alpha1.BuildpackStatus, error)) { + fake.readExtensionMutex.Lock() + defer fake.readExtensionMutex.Unlock() + fake.ReadExtensionStub = stub +} + +func (fake *FakeStoreReader) ReadExtensionArgsForCall(i int) (authn.Keychain, []v1alpha1.ImageSource) { + fake.readExtensionMutex.RLock() + defer fake.readExtensionMutex.RUnlock() + argsForCall := fake.readExtensionArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeStoreReader) ReadExtensionReturns(result1 []v1alpha1.BuildpackStatus, result2 error) { + fake.readExtensionMutex.Lock() + defer fake.readExtensionMutex.Unlock() + fake.ReadExtensionStub = nil + fake.readExtensionReturns = struct { + result1 []v1alpha1.BuildpackStatus + result2 error + }{result1, result2} +} + +func (fake *FakeStoreReader) ReadExtensionReturnsOnCall(i int, result1 []v1alpha1.BuildpackStatus, result2 error) { + fake.readExtensionMutex.Lock() + defer fake.readExtensionMutex.Unlock() + fake.ReadExtensionStub = nil + if fake.readExtensionReturnsOnCall == nil { + fake.readExtensionReturnsOnCall = make(map[int]struct { + result1 []v1alpha1.BuildpackStatus + result2 error + }) + } + fake.readExtensionReturnsOnCall[i] = struct { + result1 []v1alpha1.BuildpackStatus + result2 error + }{result1, result2} +} + +func (fake *FakeStoreReader) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.readExtensionMutex.RLock() + defer fake.readExtensionMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeStoreReader) 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 _ extension.StoreReader = new(FakeStoreReader) diff --git a/pkg/reconciler/image/image_test.go b/pkg/reconciler/image/image_test.go index 0343e53ee..679642d56 100644 --- a/pkg/reconciler/image/image_test.go +++ b/pkg/reconciler/image/image_test.go @@ -156,7 +156,7 @@ func testImageReconciler(t *testing.T, when spec.G, it spec.S) { }, Status: buildapi.BuilderStatus{ LatestImage: "some/builder@sha256:acf123", - BuilderMetadata: corev1alpha1.BuildpackMetadataList{ + BuilderMetadataBuildpacks: corev1alpha1.BuildpackMetadataList{ { Id: "buildpack.version", Version: "version", @@ -191,7 +191,7 @@ func testImageReconciler(t *testing.T, when spec.G, it spec.S) { }, Status: buildapi.BuilderStatus{ LatestImage: "some/clusterbuilder@sha256:acf123", - BuilderMetadata: corev1alpha1.BuildpackMetadataList{ + BuilderMetadataBuildpacks: corev1alpha1.BuildpackMetadataList{ { Id: "buildpack.version", Version: "version", @@ -1594,7 +1594,7 @@ func testImageReconciler(t *testing.T, when spec.G, it spec.S) { RunImage: "some/run@sha256:67e3de2af270bf09c02e9a644aeb7e87e6b3c049abe6766bf6b6c3728a83e7fb", ID: "io.buildpacks.stacks.bionic", }, - BuilderMetadata: corev1alpha1.BuildpackMetadataList{ + BuilderMetadataBuildpacks: corev1alpha1.BuildpackMetadataList{ { Id: "io.buildpack", Version: "new-version", @@ -1764,7 +1764,7 @@ func testImageReconciler(t *testing.T, when spec.G, it spec.S) { RunImage: updatedBuilderRunImage, ID: "io.buildpacks.stacks.bionic", }, - BuilderMetadata: corev1alpha1.BuildpackMetadataList{ + BuilderMetadataBuildpacks: corev1alpha1.BuildpackMetadataList{ { Id: "io.buildpack", Version: "version", diff --git a/test/execute_build_test.go b/test/execute_build_test.go index 6be2d8452..3cad5ddf6 100644 --- a/test/execute_build_test.go +++ b/test/execute_build_test.go @@ -11,8 +11,10 @@ import ( "testing" "time" + "github.com/buildpacks/lifecycle/platform/files" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/sclevine/spec" "github.com/stretchr/testify/require" @@ -43,17 +45,21 @@ func TestCreateImage(t *testing.T) { func testCreateImage(t *testing.T, when spec.G, it spec.S) { const ( - testNamespace = "test" - dockerSecret = "docker-secret" - gitBasicSecret = "git-basic-secret" - gitSSHSecret = "git-ssh-secret" - serviceAccountName = "image-service-account" - clusterStoreName = "store" - buildpackName = "buildpack" - clusterBuildpackName = "cluster-buildpack" - clusterStackName = "stack" - builderName = "custom-builder" - clusterBuilderName = "custom-cluster-builder" + testNamespace = "test" + dockerSecret = "docker-secret" + gitBasicSecret = "git-basic-secret" + gitSSHSecret = "git-ssh-secret" + serviceAccountName = "image-service-account" + clusterStoreName = "store" + buildpackName = "buildpack" + extensionName = "extension" + clusterBuildpackName = "cluster-buildpack" + clusterExtensionName = "cluster-extension" + clusterStackName = "stack" + builderName = "custom-builder" + builderWithExtensionsName = "custom-builder-with-extensions" + clusterBuilderName = "custom-cluster-builder" + clusterBuilderWithExtensionsName = "custom-cluster-builder-with-extensions" ) var ( @@ -78,10 +84,18 @@ func testCreateImage(t *testing.T, when spec.G, it spec.S) { Kind: buildapi.BuilderKind, Name: builderName, }, + "custom-builder-with-extensions": { + Kind: buildapi.BuilderKind, + Name: builderWithExtensionsName, + }, "custom-cluster-builder": { Kind: buildapi.ClusterBuilderKind, Name: clusterBuilderName, }, + "custom-cluster-builder-with-extensions": { + Kind: buildapi.ClusterBuilderKind, + Name: clusterBuilderWithExtensionsName, + }, } ) @@ -89,6 +103,10 @@ func testCreateImage(t *testing.T, when spec.G, it spec.S) { for builderType := range builderConfigs { imageName := fmt.Sprintf("%s-%s", name, builderType) builder := builderConfigs[builderType] + var builderHasExtensions bool + if strings.Contains(builder.Name, "extensions") { // TODO: this is a bit hacky, maybe we can improve it + builderHasExtensions = true + } t.Run(imageName, func(t *testing.T) { t.Parallel() @@ -116,7 +134,20 @@ func testCreateImage(t *testing.T, when spec.G, it spec.S) { }, metav1.CreateOptions{}) require.NoError(t, err) - builtImages[validateImageCreate(t, clients, image, expectedResources)] = struct{}{} + expectImage := func(t *testing.T, image v1.Image) {} + if builderHasExtensions { + expectImage = func(t *testing.T, image v1.Image) { + cfg, err := image.ConfigFile() + require.NoError(t, err) + lifecycleMDLabel, ok := cfg.Config.Labels["io.buildpacks.lifecycle.metadata"] + require.True(t, ok) + var lifecycleMD files.LayersMetadata + require.NoError(t, json.Unmarshal([]byte(lifecycleMDLabel), &lifecycleMD)) + require.Equal(t, "gcr.io/paketo-buildpacks/run-jammy-tiny", lifecycleMD.Stack.RunImage.Image) + } + } + + builtImages[validateImageCreate(t, clients, image, expectedResources, expectImage)] = struct{}{} validateRebase(t, ctx, clients, image.Name, testNamespace) }) } @@ -147,11 +178,21 @@ func testCreateImage(t *testing.T, when spec.G, it spec.S) { require.NoError(t, err) } + err = clients.client.KpackV1alpha2().Extensions(testNamespace).Delete(ctx, extensionName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + err = clients.client.KpackV1alpha2().ClusterBuildpacks().Delete(ctx, clusterBuildpackName, metav1.DeleteOptions{}) if !errors.IsNotFound(err) { require.NoError(t, err) } + err = clients.client.KpackV1alpha2().ClusterExtensions().Delete(ctx, clusterExtensionName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + err = clients.client.KpackV1alpha2().ClusterStacks().Delete(ctx, clusterStackName, metav1.DeleteOptions{}) if !errors.IsNotFound(err) { require.NoError(t, err) @@ -162,6 +203,11 @@ func testCreateImage(t *testing.T, when spec.G, it spec.S) { require.NoError(t, err) } + err = clients.client.KpackV1alpha2().ClusterBuilders().Delete(ctx, clusterBuilderWithExtensionsName, metav1.DeleteOptions{}) + if !errors.IsNotFound(err) { + require.NoError(t, err) + } + deleteNamespace(t, ctx, clients, testNamespace) _, err = clients.k8sClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ @@ -233,6 +279,18 @@ func testCreateImage(t *testing.T, when spec.G, it spec.S) { }, metav1.CreateOptions{}) require.NoError(t, err) + _, err = clients.client.KpackV1alpha2().Extensions(testNamespace).Create(ctx, &buildapi.Extension{ + ObjectMeta: metav1.ObjectMeta{ + Name: extensionName, + }, + Spec: buildapi.ExtensionSpec{ + ImageSource: corev1alpha1.ImageSource{ + Image: "natalieparellano/sample-extension", // FIXME + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = clients.client.KpackV1alpha2().ClusterBuildpacks().Create(ctx, &buildapi.ClusterBuildpack{ ObjectMeta: metav1.ObjectMeta{ Name: clusterBuildpackName, @@ -245,6 +303,18 @@ func testCreateImage(t *testing.T, when spec.G, it spec.S) { }, metav1.CreateOptions{}) require.NoError(t, err) + _, err = clients.client.KpackV1alpha2().ClusterExtensions().Create(ctx, &buildapi.ClusterExtension{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterExtensionName, + }, + Spec: buildapi.ClusterExtensionSpec{ + ImageSource: corev1alpha1.ImageSource{ + Image: "natalieparellano/sample-extension", // FIXME + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + _, err = clients.client.KpackV1alpha2().ClusterStacks().Create(ctx, &buildapi.ClusterStack{ ObjectMeta: metav1.ObjectMeta{ Name: clusterStackName, @@ -358,6 +428,116 @@ func testCreateImage(t *testing.T, when spec.G, it spec.S) { }, metav1.CreateOptions{}) require.NoError(t, err) + builderWithExtensions, err := clients.client.KpackV1alpha2().Builders(testNamespace).Create(ctx, &buildapi.Builder{ + ObjectMeta: metav1.ObjectMeta{ + Name: builderWithExtensionsName, + Namespace: testNamespace, + }, + Spec: buildapi.NamespacedBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/go", + }, + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/nodejs", + }, + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: buildpackName, + Kind: "Buildpack", + }, + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + OrderExtensions: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "samples/curl", // FIXME + }, + }, + }, + }, + }, + }, + }, + ServiceAccountName: serviceAccountName, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + clusterBuilder, err := clients.client.KpackV1alpha2().ClusterBuilders().Create(ctx, &buildapi.ClusterBuilder{ ObjectMeta: metav1.ObjectMeta{ Name: clusterBuilderName, @@ -452,10 +632,117 @@ func testCreateImage(t *testing.T, when spec.G, it spec.S) { }, metav1.CreateOptions{}) require.NoError(t, err) - waitUntilReady(t, ctx, clients, builder, clusterBuilder) + clusterBuilderWithExtensions, err := clients.client.KpackV1alpha2().ClusterBuilders().Create(ctx, &buildapi.ClusterBuilder{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterBuilderWithExtensionsName, + }, + Spec: buildapi.ClusterBuilderSpec{ + BuilderSpec: buildapi.BuilderSpec{ + Tag: cfg.newImageTag(), + Stack: corev1.ObjectReference{ + Name: clusterStackName, + Kind: "ClusterStack", + }, + Store: corev1.ObjectReference{ + Name: clusterStoreName, + Kind: "ClusterStore", + }, + Order: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/go", + }, + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + ObjectReference: corev1.ObjectReference{ + Name: clusterBuildpackName, + Kind: "ClusterBuildpack", + }, + }, + }, + }, + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/bellsoft-liberica", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/gradle", + }, + Optional: true, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/syft", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/executable-jar", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/dist-zip", + }, + }, + }, + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "paketo-buildpacks/spring-boot", + }, + }, + }, + }, + }, + }, + OrderExtensions: []buildapi.BuilderOrderEntry{ + { + Group: []buildapi.BuilderBuildpackRef{ + { + BuildpackRef: corev1alpha1.BuildpackRef{ + BuildpackInfo: corev1alpha1.BuildpackInfo{ + Id: "samples/curl", // FIXME + }, + }, + }, + }, + }, + }, + }, + ServiceAccountRef: corev1.ObjectReference{ + Namespace: testNamespace, + Name: serviceAccountName, + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + waitUntilReady(t, ctx, clients, builder, clusterBuilder, builderWithExtensions, clusterBuilderWithExtensions) }) - it("builds and rebases git, blob, and registry images from unauthenticated sources", func() { + it.Focus("builds and rebases git, blob, and registry images from unauthenticated sources", func() { imageSources := map[string]corev1alpha1.SourceConfig{ "git-image": { Git: &corev1alpha1.Git{ @@ -594,7 +881,7 @@ func generateRebuild(ctx *context.Context, t *testing.T, cfg config, clients *cl }, metav1.CreateOptions{}) require.NoError(t, err) - originalImageTag := validateImageCreate(t, clients, image, expectedResources) + originalImageTag := validateImageCreate(t, clients, image, expectedResources, func(t *testing.T, image v1.Image) {}) list, err := clients.client.KpackV1alpha2().Builds(testNamespace).List(*ctx, metav1.ListOptions{ LabelSelector: fmt.Sprintf("image.kpack.io/image=%s", imageName), @@ -615,7 +902,7 @@ func generateRebuild(ctx *context.Context, t *testing.T, cfg config, clients *cl return len(list.Items) == 2 }, 5*time.Second, 1*time.Minute) - rebuiltImageTag := validateImageCreate(t, clients, image, expectedResources) + rebuiltImageTag := validateImageCreate(t, clients, image, expectedResources, func(t *testing.T, image v1.Image) {}) require.Equal(t, originalImageTag, rebuiltImageTag) return originalImageTag @@ -657,7 +944,13 @@ func waitUntilReady(t *testing.T, ctx context.Context, clients *clients, objects } } -func validateImageCreate(t *testing.T, clients *clients, image *buildapi.Image, expectedResources corev1.ResourceRequirements) string { +func validateImageCreate( + t *testing.T, + clients *clients, + image *buildapi.Image, + expectedResources corev1.ResourceRequirements, // TODO: this seems to no longer be used? + expectImage func(*testing.T, v1.Image), +) string { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -671,13 +964,16 @@ func validateImageCreate(t *testing.T, clients *clients, image *buildapi.Image, waitUntilReady(t, ctx, clients, image) registryClient := ®istry.Client{} - _, identifier, err := registryClient.Fetch(authn.DefaultKeychain, image.Spec.Tag) + builtImage, identifier, err := registryClient.Fetch(authn.DefaultKeychain, image.Spec.Tag) require.NoError(t, err) eventually(t, func() bool { return strings.Contains(logTail.String(), "Build successful") }, 1*time.Second, 10*time.Second) + // TODO: expect extend build image with kaniko + expectImage(t, builtImage) + buildList, err := clients.client.KpackV1alpha2().Builds(image.Namespace).List(ctx, metav1.ListOptions{ LabelSelector: fmt.Sprintf("image.kpack.io/image=%s", image.Name), })