From bdeaab0995953f11cccd9e02b0528cb89cf42abd Mon Sep 17 00:00:00 2001 From: Tom Kennedy Date: Thu, 1 Sep 2022 13:10:39 -0400 Subject: [PATCH] Allow MAXIMUM_PLATFORM_API_VERSION to be configured on the kpack controller --- cmd/controller/main.go | 36 +++++++++---- pkg/apis/build/v1alpha2/build_pod.go | 50 ++++++++---------- pkg/apis/build/v1alpha2/build_pod_test.go | 62 +++++++++++++++++++++++ pkg/buildpod/generator.go | 21 ++++---- pkg/buildpod/generator_test.go | 21 ++++++++ 5 files changed, 141 insertions(+), 49 deletions(-) diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 6834a1417..f1579454d 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -9,6 +9,7 @@ import ( "strconv" "time" + "github.com/Masterminds/semver/v3" "github.com/google/go-containerregistry/pkg/authn" "go.uber.org/zap" "golang.org/x/sync/errgroup" @@ -69,12 +70,13 @@ var ( kubeconfig = flag.String("kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.") masterURL = flag.String("master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.") - buildInitImage = flag.String("build-init-image", os.Getenv("BUILD_INIT_IMAGE"), "The image used to initialize a build") - buildInitWindowsImage = flag.String("build-init-windows-image", os.Getenv("BUILD_INIT_WINDOWS_IMAGE"), "The image used to initialize a build on windows") - rebaseImage = flag.String("rebase-image", os.Getenv("REBASE_IMAGE"), "The image used to perform rebases") - completionImage = flag.String("completion-image", os.Getenv("COMPLETION_IMAGE"), "The image used to finish a build") - completionWindowsImage = flag.String("completion-windows-image", os.Getenv("COMPLETION_WINDOWS_IMAGE"), "The image used to finish a build on windows") - enablePriorityClasses = flag.Bool("enable-priority-classes", getEnvBool("ENABLE_PRIORITY_CLASSES", false), "if set to true, enables different pod priority classes for normal builds and automated builds") + buildInitImage = flag.String("build-init-image", os.Getenv("BUILD_INIT_IMAGE"), "The image used to initialize a build") + buildInitWindowsImage = flag.String("build-init-windows-image", os.Getenv("BUILD_INIT_WINDOWS_IMAGE"), "The image used to initialize a build on windows") + rebaseImage = flag.String("rebase-image", os.Getenv("REBASE_IMAGE"), "The image used to perform rebases") + completionImage = flag.String("completion-image", os.Getenv("COMPLETION_IMAGE"), "The image used to finish a build") + completionWindowsImage = flag.String("completion-windows-image", os.Getenv("COMPLETION_WINDOWS_IMAGE"), "The image used to finish a build on windows") + enablePriorityClasses = flag.Bool("enable-priority-classes", getEnvBool("ENABLE_PRIORITY_CLASSES", false), "if set to true, enables different pod priority classes for normal builds and automated builds") + maximumPlatformApiVersion = flag.String("maximum-platform-api-version", os.Getenv("MAXIMUM_PLATFORM_API_VERSION"), "The maximum allowed platform api version a build can utilize") ) func main() { @@ -139,6 +141,11 @@ func main() { log.Fatalf("could not get dynamic client: %s", err) } + maxPlatformApi, err := parseMaxPlatformApiVersion() + if err != nil { + log.Fatalf("could not resolve provided maximum platform api version: %s", err) + } + buildpodGenerator := &buildpod.Generator{ BuildPodConfig: buildapi.BuildPodImages{ BuildInitImage: *buildInitImage, @@ -147,10 +154,11 @@ func main() { BuildInitWindowsImage: *buildInitWindowsImage, CompletionWindowsImage: *completionWindowsImage, }, - K8sClient: k8sClient, - KeychainFactory: keychainFactory, - ImageFetcher: ®istry.Client{}, - DynamicClient: dynamicClient, + K8sClient: k8sClient, + KeychainFactory: keychainFactory, + ImageFetcher: ®istry.Client{}, + DynamicClient: dynamicClient, + MaximumPlatformApiVersion: maxPlatformApi, } gitResolver := git.NewResolver(k8sClient) @@ -290,3 +298,11 @@ func waitForSync(stopCh <-chan struct{}, indexFormers ...cache.SharedIndexInform cache.WaitForCacheSync(stopCh, informer.HasSynced) } } + +func parseMaxPlatformApiVersion() (*semver.Version, error) { + if *maximumPlatformApiVersion != "" { + return semver.NewVersion(*maximumPlatformApiVersion) + } + + return nil, nil +} \ No newline at end of file diff --git a/pkg/apis/build/v1alpha2/build_pod.go b/pkg/apis/build/v1alpha2/build_pod.go index 89f5ed93e..36ae32662 100644 --- a/pkg/apis/build/v1alpha2/build_pod.go +++ b/pkg/apis/build/v1alpha2/build_pod.go @@ -56,7 +56,7 @@ const ( buildChangesEnvVar = "BUILD_CHANGES" CacheTagEnvVar = "CACHE_TAG" - platformAPIEnvVar = "CNB_PLATFORM_API" + platformApiVersionEnvVarName = "CNB_PLATFORM_API" serviceBindingRootEnvVar = "SERVICE_BINDING_ROOT" TerminationMessagePathEnvVar = "TERMINATION_MESSAGE_PATH" @@ -104,10 +104,11 @@ func terminationMsgPath(os string) string { // +k8s:deepcopy-gen=false type BuildContext struct { - BuildPodBuilderConfig BuildPodBuilderConfig - Secrets []corev1.Secret - Bindings []ServiceBinding - ImagePullSecrets []corev1.LocalObjectReference + BuildPodBuilderConfig BuildPodBuilderConfig + Secrets []corev1.Secret + Bindings []ServiceBinding + ImagePullSecrets []corev1.LocalObjectReference + MaximumPlatformApiVersion *semver.Version } func (c BuildContext) os() string { @@ -181,10 +182,11 @@ var ( type stepModifier func(corev1.Container) corev1.Container func (b *Build) BuildPod(images BuildPodImages, buildContext BuildContext) (*corev1.Pod, error) { - platformAPI, err := buildContext.BuildPodBuilderConfig.highestSupportedPlatformAPI(b) + platformAPI, err := buildContext.highestSupportedPlatformAPI(b) if err != nil { return nil, err } + platformApiVersionEnvVar := corev1.EnvVar{Name: platformApiVersionEnvVarName, Value: platformAPI.Original()} if b.rebasable(buildContext.BuildPodBuilderConfig.StackID) { return b.rebasePod(buildContext, images) @@ -301,10 +303,7 @@ func (b *Build) BuildPod(images BuildPodImages, buildContext BuildContext) (*cor }()), Env: []corev1.EnvVar{ homeEnv, - { - Name: platformAPIEnvVar, - Value: platformAPI.Original(), - }, + platformApiVersionEnvVar, serviceBindingRootEnv, }, ImagePullPolicy: corev1.PullIfNotPresent, @@ -332,10 +331,7 @@ func (b *Build) BuildPod(images BuildPodImages, buildContext BuildContext) (*cor }, bindingVolumeMounts), ImagePullPolicy: corev1.PullIfNotPresent, Env: []corev1.EnvVar{ - { - Name: platformAPIEnvVar, - Value: platformAPI.Original(), - }, + platformApiVersionEnvVar, }, } detectContainerMods := ifWindows(buildContext.os(), addNetworkWaitLauncherVolume(), useNetworkWaitLauncher(dnsProbeHost)) @@ -503,10 +499,7 @@ func (b *Build) BuildPod(images BuildPodImages, buildContext BuildContext) (*cor }, cacheVolumes), Env: []corev1.EnvVar{ homeEnv, - { - Name: platformAPIEnvVar, - Value: platformAPI.Original(), - }, + platformApiVersionEnvVar, }, ImagePullPolicy: corev1.PullIfNotPresent, }, @@ -535,10 +528,7 @@ func (b *Build) BuildPod(images BuildPodImages, buildContext BuildContext) (*cor }, bindingVolumeMounts), ImagePullPolicy: corev1.PullIfNotPresent, Env: []corev1.EnvVar{ - { - Name: platformAPIEnvVar, - Value: platformAPI.Original(), - }, + platformApiVersionEnvVar, serviceBindingRootEnv, }, }, @@ -592,10 +582,7 @@ func (b *Build) BuildPod(images BuildPodImages, buildContext BuildContext) (*cor Env: envs( []corev1.EnvVar{ homeEnv, - { - Name: platformAPIEnvVar, - Value: platformAPI.Original(), - }, + platformApiVersionEnvVar, }, func() corev1.EnvVar { return corev1.EnvVar{ @@ -1015,14 +1002,17 @@ var ( supportedPlatformAPIVersions = append(supportedPlatformAPIVersionsWithWindowsAndReportToml, semver.MustParse("0.3")) ) -func (bc *BuildPodBuilderConfig) highestSupportedPlatformAPI(b *Build) (*semver.Version, error) { +func (bc BuildContext) highestSupportedPlatformAPI(b *Build) (*semver.Version, error) { for _, supportedVersion := range func() []*semver.Version { - if b.NotaryV1Config() != nil || bc.OS == "windows" { + if b.NotaryV1Config() != nil || bc.BuildPodBuilderConfig.OS == "windows" { return supportedPlatformAPIVersionsWithWindowsAndReportToml } return supportedPlatformAPIVersions }() { - for _, v := range bc.PlatformAPIs { + if bc.MaximumPlatformApiVersion != nil && bc.MaximumPlatformApiVersion.LessThan(supportedVersion) { + continue + } + for _, v := range bc.BuildPodBuilderConfig.PlatformAPIs { version, err := semver.NewVersion(v) if err != nil { return nil, errors.Wrapf(err, "unexpected platform version %s", v) @@ -1034,7 +1024,7 @@ func (bc *BuildPodBuilderConfig) highestSupportedPlatformAPI(b *Build) (*semver. } } - return nil, errors.Errorf("unsupported builder platform API versions: %s", strings.Join(bc.PlatformAPIs, ",")) + return nil, errors.Errorf("unsupported builder platform API versions: %s", strings.Join(bc.BuildPodBuilderConfig.PlatformAPIs, ",")) } func (b Build) nodeSelector(os string) map[string]string { diff --git a/pkg/apis/build/v1alpha2/build_pod_test.go b/pkg/apis/build/v1alpha2/build_pod_test.go index 1c863b1f9..59064234d 100644 --- a/pkg/apis/build/v1alpha2/build_pod_test.go +++ b/pkg/apis/build/v1alpha2/build_pod_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/Masterminds/semver/v3" "github.com/sclevine/spec" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -2570,6 +2571,57 @@ func testBuildPod(t *testing.T, when spec.G, it spec.S) { assert.Equal(t, "-cache-image=some-tag", pod.Spec.InitContainers[5].Args[8]) }) }) + + when("selecting platform api version", func() { + it("chooses the configured maximum version when less than highest version from builder", func() { + buildContext.BuildPodBuilderConfig.PlatformAPIs = []string{"0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8"} + + platformApi, err := semver.NewVersion("0.7") + require.NoError(t, err) + buildContext.MaximumPlatformApiVersion = platformApi + + pod, err := build.BuildPod(config, buildContext) + require.NoError(t, err) + + expectedEnv := corev1.EnvVar{Name: "CNB_PLATFORM_API", Value: "0.7"} + + for _, container := range pod.Spec.InitContainers { + if envVar, ok := fetchEnvVar(container.Env, expectedEnv.Name); ok { + assert.Equal(t, expectedEnv, envVar) + } + } + }) + it("chooses the highest version from builder when less than configured maximum version", func() { + buildContext.BuildPodBuilderConfig.PlatformAPIs = []string{"0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8"} + + platformApi, err := semver.NewVersion("1.0") + require.NoError(t, err) + buildContext.MaximumPlatformApiVersion = platformApi + + pod, err := build.BuildPod(config, buildContext) + require.NoError(t, err) + + expectedEnv := corev1.EnvVar{Name: "CNB_PLATFORM_API", Value: "0.8"} + for _, container := range pod.Spec.InitContainers { + if envVar, ok := fetchEnvVar(container.Env, expectedEnv.Name); ok { + assert.Equal(t, expectedEnv, envVar) + } + } + }) + it("chooses the highest version from builder when maximum version is not configured", func() { + buildContext.BuildPodBuilderConfig.PlatformAPIs = []string{"0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8"} + + pod, err := build.BuildPod(config, buildContext) + require.NoError(t, err) + + expectedEnv := corev1.EnvVar{Name: "CNB_PLATFORM_API", Value: "0.8"} + for _, container := range pod.Spec.InitContainers { + if envVar, ok := fetchEnvVar(container.Env, expectedEnv.Name); ok { + assert.Equal(t, expectedEnv, envVar) + } + } + }) + }) }) } @@ -2616,3 +2668,13 @@ func names(mounts []corev1.VolumeMount) (names []string) { } return } + +func fetchEnvVar(envVars []corev1.EnvVar, name string) (corev1.EnvVar, bool) { + for _, envVar := range envVars { + if envVar.Name == name { + return envVar, true + } + } + + return corev1.EnvVar{}, false +} diff --git a/pkg/buildpod/generator.go b/pkg/buildpod/generator.go index d5ce6a7bb..b7b3dcf2d 100644 --- a/pkg/buildpod/generator.go +++ b/pkg/buildpod/generator.go @@ -5,6 +5,7 @@ import ( "fmt" "strconv" + "github.com/Masterminds/semver/v3" "github.com/buildpacks/lifecycle/platform" "github.com/google/go-containerregistry/pkg/authn" ggcrv1 "github.com/google/go-containerregistry/pkg/v1" @@ -36,11 +37,12 @@ type ImageFetcher interface { } type Generator struct { - BuildPodConfig buildapi.BuildPodImages - K8sClient k8sclient.Interface - KeychainFactory registry.KeychainFactory - ImageFetcher ImageFetcher - DynamicClient dynamic.Interface + BuildPodConfig buildapi.BuildPodImages + K8sClient k8sclient.Interface + KeychainFactory registry.KeychainFactory + ImageFetcher ImageFetcher + DynamicClient dynamic.Interface + MaximumPlatformApiVersion *semver.Version } type BuildPodable interface { @@ -71,10 +73,11 @@ func (g *Generator) Generate(ctx context.Context, build BuildPodable) (*v1.Pod, } return build.BuildPod(g.BuildPodConfig, buildapi.BuildContext{ - BuildPodBuilderConfig: buildPodBuilderConfig, - Secrets: secrets, - Bindings: bindings, - ImagePullSecrets: imagePullSecrets, + BuildPodBuilderConfig: buildPodBuilderConfig, + Secrets: secrets, + Bindings: bindings, + ImagePullSecrets: imagePullSecrets, + MaximumPlatformApiVersion: g.MaximumPlatformApiVersion, }) } diff --git a/pkg/buildpod/generator_test.go b/pkg/buildpod/generator_test.go index d39df2384..f5ae1ab28 100644 --- a/pkg/buildpod/generator_test.go +++ b/pkg/buildpod/generator_test.go @@ -5,6 +5,7 @@ import ( "fmt" "testing" + "github.com/Masterminds/semver/v3" "github.com/buildpacks/lifecycle/platform" ggcrv1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/mutate" @@ -507,6 +508,26 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) { _, err := generator.Generate(context.TODO(), build) require.EqualError(t, err, "provisionedservices \"some-provisioned-service\" not found") }) + + it("passes the maximum supported platform api through the build context", func() { + var build = &testBuildPodable{ + serviceAccount: serviceAccountName, + namespace: namespace, + buildBuilderSpec: corev1alpha1.BuildBuilderSpec{ + Image: linuxBuilderImage, + ImagePullSecrets: builderPullSecrets, + }, + } + version, err := semver.NewVersion("0.8") + require.NoError(t, err) + generator.MaximumPlatformApiVersion = version + + _, err = generator.Generate(context.TODO(), build) + require.NoError(t, err) + + require.Len(t, build.buildPodCalls, 1) + assert.Equal(t, version, build.buildPodCalls[0].BuildContext.MaximumPlatformApiVersion) + }) }) }