From c01f7fc70cc92b9525e4fa3d8774d3b466dc5ac0 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Thu, 30 Apr 2020 17:19:26 -0400 Subject: [PATCH 1/7] Add CNB Service Bindings to Image and Build resources The CNB Service Bindings spec provides a structured way to expose service metadata and credentials into a build. For example, the build can use the binding information to install a database driver, configure an APM agent for the built image, or discover any other capability outside of the built container. Each binding reference has a name, a reference to a k8s ConfigMap holding service metadata, and optionally a k8s Secret holding service credentials. Builds generally should not have access to the credentials so that the values are not backed into the built image, but instead bound fresh at runtime. For images, the bindings are located at `.spec.build.bindings`: ``` kind: Image ... spec: ... build: bindings: - name: my-binding metadataRef: name: provider-binding-metadata secretRef: name: provider-binding-secret ``` For builds, the bindings are located at `.spec.bindings`: ``` kind: Build ... spec: ... bindings: - name: my-binding metadataRef: name: provider-binding-metadata secretRef: name: provider-binding-secret ``` In the build the bindings appear as a directory tree under the path specified by the `$CNB_BINDINGS` env var. Each binding is a directory, which in turn has a `metadata` directory with files and if credentials are exposed a `secret` directory with more files. The environment variable and volume mounts are available during the detect and build steps. Currently, the value of `$CNB_BINDINGS` is `/var/bindings` but should not be presumed to be stable or consistent with the value at runtime. There is no guarantee the same services, or types of services, will be bound at runtime. Refs #369 --- pkg/apis/build/v1alpha1/build_pod.go | 67 +++++++++++- pkg/apis/build/v1alpha1/build_pod_test.go | 103 +++++++++++++++++- pkg/apis/build/v1alpha1/build_types.go | 12 ++ pkg/apis/build/v1alpha1/build_validation.go | 49 +++++++++ .../build/v1alpha1/build_validation_test.go | 65 +++++++++++ pkg/apis/build/v1alpha1/image_builds.go | 8 ++ pkg/apis/build/v1alpha1/image_types.go | 2 + pkg/apis/build/v1alpha1/image_validation.go | 11 +- .../build/v1alpha1/image_validation_test.go | 8 ++ .../build/v1alpha1/zz_generated.deepcopy.go | 62 +++++++++++ 10 files changed, 378 insertions(+), 9 deletions(-) diff --git a/pkg/apis/build/v1alpha1/build_pod.go b/pkg/apis/build/v1alpha1/build_pod.go index 8c14c28b2..ad88fcb27 100644 --- a/pkg/apis/build/v1alpha1/build_pod.go +++ b/pkg/apis/build/v1alpha1/build_pod.go @@ -84,6 +84,10 @@ var ( MountPath: "/builderPullSecrets", ReadOnly: true, } + bindingsEnv = corev1.EnvVar{ + Name: "CNB_BINDINGS", + Value: "/var/bindings", + } ) func (b *Build) BuildPod(config BuildPodImages, secrets []corev1.Secret, bc BuildPodBuilderConfig) (*corev1.Pod, error) { @@ -110,6 +114,51 @@ func (b *Build) BuildPod(config BuildPodImages, secrets []corev1.Secret, bc Buil SubPath: b.Spec.Source.SubPath, // empty string is a nop } + bindingVolumes := []corev1.Volume{} + bindingVolumeMounts := []corev1.VolumeMount{} + for _, binding := range b.Spec.Bindings { + metadataVolume := fmt.Sprintf("binding-metadata-%s", binding.Name) + bindingVolumes = append(bindingVolumes, + corev1.Volume{ + Name: metadataVolume, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: binding.MetadataRef.Name, + }, + }, + }, + }, + ) + bindingVolumeMounts = append(bindingVolumeMounts, + corev1.VolumeMount{ + Name: metadataVolume, + MountPath: fmt.Sprintf("%s/%s/metadata", bindingsEnv.Value, binding.Name), + ReadOnly: true, + }, + ) + if binding.SecretRef != nil { + secretVolume := fmt.Sprintf("binding-secret-%s", binding.Name) + bindingVolumes = append(bindingVolumes, + corev1.Volume{ + Name: secretVolume, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: binding.SecretRef.Name, + }, + }, + }, + ) + bindingVolumeMounts = append(bindingVolumeMounts, + corev1.VolumeMount{ + Name: secretVolume, + MountPath: fmt.Sprintf("%s/%s/secret", bindingsEnv.Value, binding.Name), + ReadOnly: true, + }, + ) + } + } + return &corev1.Pod{ ObjectMeta: v1.ObjectMeta{ Name: b.PodName(), @@ -188,11 +237,14 @@ func (b *Build) BuildPod(config BuildPodImages, secrets []corev1.Secret, bc Buil "-group=/layers/group.toml", "-plan=/layers/plan.toml", }, - VolumeMounts: []corev1.VolumeMount{ + Env: []corev1.EnvVar{ + bindingsEnv, + }, + VolumeMounts: append([]corev1.VolumeMount{ layersVolume, platformVolume, workspaceVolume, - }, + }, bindingVolumeMounts...), ImagePullPolicy: corev1.PullIfNotPresent, }, ) @@ -253,11 +305,14 @@ func (b *Build) BuildPod(config BuildPodImages, secrets []corev1.Secret, bc Buil "-group=/layers/group.toml", "-plan=/layers/plan.toml", }, - VolumeMounts: []corev1.VolumeMount{ + Env: []corev1.EnvVar{ + bindingsEnv, + }, + VolumeMounts: append([]corev1.VolumeMount{ layersVolume, platformVolume, workspaceVolume, - }, + }, bindingVolumeMounts...), ImagePullPolicy: corev1.PullIfNotPresent, }, ) @@ -315,7 +370,7 @@ func (b *Build) BuildPod(config BuildPodImages, secrets []corev1.Secret, bc Buil } }), ServiceAccountName: b.Spec.ServiceAccount, - Volumes: append( + Volumes: append(append( secretVolumes, corev1.Volume{ Name: cacheDirName, @@ -347,7 +402,7 @@ func (b *Build) BuildPod(config BuildPodImages, secrets []corev1.Secret, bc Buil }, b.Spec.Source.Source().ImagePullSecretsVolume(), builderSecretVolume(b.Spec.Builder), - ), + ), bindingVolumes...), ImagePullSecrets: b.Spec.Builder.ImagePullSecrets, }, }, nil diff --git a/pkg/apis/build/v1alpha1/build_pod_test.go b/pkg/apis/build/v1alpha1/build_pod_test.go index ad5784fbc..f2834c5c8 100644 --- a/pkg/apis/build/v1alpha1/build_pod_test.go +++ b/pkg/apis/build/v1alpha1/build_pod_test.go @@ -68,6 +68,23 @@ func testBuildPod(t *testing.T, when spec.G, it spec.S) { }, }, CacheName: "some-cache-name", + Bindings: []v1alpha1.Binding{ + { + Name: "database", + MetadataRef: &corev1.LocalObjectReference{ + Name: "database-configmap", + }, + }, + { + Name: "apm", + MetadataRef: &corev1.LocalObjectReference{ + Name: "apm-configmap", + }, + SecretRef: &corev1.LocalObjectReference{ + Name: "apm-secret", + }, + }, + }, Env: []corev1.EnvVar{ {Name: "keyA", Value: "valueA"}, {Name: "keyB", Value: "valueB"}, @@ -222,6 +239,71 @@ func testBuildPod(t *testing.T, when spec.G, it spec.S) { } }) + it("configures the bindings", func() { + pod, err := build.BuildPod(config, secrets, buildPodBuilderConfig) + require.NoError(t, err) + + assert.Contains(t, + pod.Spec.Volumes, + corev1.Volume{ + Name: "binding-metadata-database", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "database-configmap", + }, + }, + }, + }, + corev1.Volume{ + Name: "binding-metadata-apm", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "apm-configmap", + }, + }, + }, + }, + corev1.Volume{ + Name: "binding-secret-apm", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "apm-secret", + }, + }, + }, + ) + + for _, containerIdx := range []int{1 /* detect */, 4 /* build */} { + assert.Contains(t, + pod.Spec.InitContainers[containerIdx].Env, + corev1.EnvVar{ + Name: "CNB_BINDINGS", + Value: "/var/bindings", + }, + ) + assert.Contains(t, + pod.Spec.InitContainers[containerIdx].VolumeMounts, + corev1.VolumeMount{ + Name: "binding-metadata-database", + MountPath: "/var/bindings/database/metadata", + ReadOnly: true, + }, + corev1.VolumeMount{ + Name: "binding-metadata-apm", + MountPath: "/var/bindings/apm/metadata", + ReadOnly: true, + }, + corev1.VolumeMount{ + Name: "binding-secret-apm", + MountPath: "/var/bindings/apm/secret", + ReadOnly: true, + }, + ) + } + }) + it("configures prepare with docker and git credentials", func() { pod, err := build.BuildPod(config, secrets, buildPodBuilderConfig) require.NoError(t, err) @@ -416,10 +498,16 @@ func testBuildPod(t *testing.T, when spec.G, it spec.S) { assert.Equal(t, pod.Spec.InitContainers[1].Name, "detect") assert.Equal(t, pod.Spec.InitContainers[1].Image, builderImage) + assert.Equal(t, pod.Spec.InitContainers[1].Env, []corev1.EnvVar{ + {Name: "CNB_BINDINGS", Value: "/var/bindings"}, + }) assert.Equal(t, []string{ "layers-dir", "platform-dir", "workspace-dir", + "binding-metadata-database", + "binding-metadata-apm", + "binding-secret-apm", }, names(pod.Spec.InitContainers[1].VolumeMounts)) }) @@ -504,6 +592,9 @@ func testBuildPod(t *testing.T, when spec.G, it spec.S) { "layers-dir", "platform-dir", "workspace-dir", + "binding-metadata-database", + "binding-metadata-apm", + "binding-secret-apm", })) }) @@ -554,7 +645,7 @@ func testBuildPod(t *testing.T, when spec.G, it spec.S) { pod, err := build.BuildPod(config, nil, buildPodBuilderConfig) require.NoError(t, err) - require.Len(t, pod.Spec.Volumes, 7) + require.Len(t, pod.Spec.Volumes, 10) assert.Equal(t, corev1.Volume{ Name: "cache-dir", VolumeSource: corev1.VolumeSource{ @@ -568,7 +659,7 @@ func testBuildPod(t *testing.T, when spec.G, it spec.S) { pod, err := build.BuildPod(config, nil, buildPodBuilderConfig) require.NoError(t, err) - require.Len(t, pod.Spec.Volumes, 7) + require.Len(t, pod.Spec.Volumes, 10) assert.Equal(t, corev1.Volume{ Name: "cache-dir", VolumeSource: corev1.VolumeSource{ @@ -596,6 +687,14 @@ func testBuildPod(t *testing.T, when spec.G, it spec.S) { require.Len(t, pod.Spec.ImagePullSecrets, 1) assert.Equal(t, corev1.LocalObjectReference{Name: "some-image-secret"}, pod.Spec.ImagePullSecrets[0]) }) + + it("mounts volumes for bindings", func() { + pod, err := build.BuildPod(config, secrets, buildPodBuilderConfig) + require.NoError(t, err) + + require.Len(t, pod.Spec.ImagePullSecrets, 1) + assert.Equal(t, corev1.LocalObjectReference{Name: "some-image-secret"}, pod.Spec.ImagePullSecrets[0]) + }) }) when("0.3 platform api", func() { diff --git a/pkg/apis/build/v1alpha1/build_types.go b/pkg/apis/build/v1alpha1/build_types.go index d8d1000b6..dd45ce614 100644 --- a/pkg/apis/build/v1alpha1/build_types.go +++ b/pkg/apis/build/v1alpha1/build_types.go @@ -61,11 +61,23 @@ type BuildSpec struct { Source SourceConfig `json:"source"` CacheName string `json:"cacheName,omitempty"` // +listType + Bindings Bindings `json:"bindings,omitempty"` + // +listType Env []corev1.EnvVar `json:"env,omitempty"` Resources corev1.ResourceRequirements `json:"resources,omitempty"` LastBuild *LastBuild `json:"lastBuild,omitempty"` } +// +k8s:openapi-gen=true +type Bindings []Binding + +// +k8s:openapi-gen=true +type Binding struct { + Name string `json:"name",omitempty"` + MetadataRef *corev1.LocalObjectReference `json:"metadataRef,omitempty"` + SecretRef *corev1.LocalObjectReference `json:"secretRed,omitempty"` +} + // +k8s:openapi-gen=true type LastBuild struct { Image string `json:"image,omitempty"` diff --git a/pkg/apis/build/v1alpha1/build_validation.go b/pkg/apis/build/v1alpha1/build_validation.go index 8298ed24e..a0156827d 100644 --- a/pkg/apis/build/v1alpha1/build_validation.go +++ b/pkg/apis/build/v1alpha1/build_validation.go @@ -2,6 +2,8 @@ package v1alpha1 import ( "context" + "fmt" + "regexp" "knative.dev/pkg/apis" "knative.dev/pkg/kmp" @@ -24,6 +26,7 @@ func (bs *BuildSpec) Validate(ctx context.Context) *apis.FieldError { Also(validate.Tags(bs.Tags)). Also(bs.Builder.Validate(ctx).ViaField("builder")). Also(bs.Source.Validate(ctx).ViaField("source")). + Also(bs.Bindings.Validate(ctx).ViaField("bindings")). Also(bs.LastBuild.Validate(ctx).ViaField("lastBuild")). Also(bs.validateImmutableFields(ctx)) } @@ -54,6 +57,52 @@ func (bbs *BuildBuilderSpec) Validate(ctx context.Context) *apis.FieldError { return validate.Image(bbs.Image) } +func (bs Bindings) Validate(ctx context.Context) *apis.FieldError { + var errs *apis.FieldError + names := map[string]int{} + for i, b := range bs { + // check name uniqueness + if n, ok := names[b.Name]; ok { + errs = errs.Also( + apis.ErrGeneric( + fmt.Sprintf("duplicate binding name %q", b.Name), + fmt.Sprintf("[%d].name", n), + fmt.Sprintf("[%d].name", i), + ), + ) + } + names[b.Name] = i + errs = errs.Also(b.Validate(ctx).ViaIndex(i)) + } + return errs +} + +var bindingNameRE = regexp.MustCompile(`^[a-z0-9\-\.]{1,253}$`) + +func (b *Binding) Validate(context context.Context) *apis.FieldError { + var errs *apis.FieldError + + if b.Name == "" { + errs = errs.Also(apis.ErrMissingField("name")) + } else if !bindingNameRE.MatchString(b.Name) { + errs = errs.Also(apis.ErrInvalidValue(b.Name, "name")) + } + + if b.MetadataRef == nil { + // metadataRef is required + errs = errs.Also(apis.ErrMissingField("metadataRef")) + } else if b.MetadataRef.Name == "" { + errs = errs.Also(apis.ErrMissingField("metadataRef.name")) + } + + if b.SecretRef != nil && b.SecretRef.Name == "" { + // secretRef is optional + errs = errs.Also(apis.ErrMissingField("secretRef.name")) + } + + return errs +} + func (lb *LastBuild) Validate(context context.Context) *apis.FieldError { if lb == nil || lb.Image == "" { return nil diff --git a/pkg/apis/build/v1alpha1/build_validation_test.go b/pkg/apis/build/v1alpha1/build_validation_test.go index 61e4e9405..7ef29479c 100644 --- a/pkg/apis/build/v1alpha1/build_validation_test.go +++ b/pkg/apis/build/v1alpha1/build_validation_test.go @@ -6,6 +6,7 @@ import ( "github.com/sclevine/spec" "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "knative.dev/pkg/apis" ) @@ -145,6 +146,70 @@ func testBuildValidation(t *testing.T, when spec.G, it spec.S) { assertValidationError(build, apis.ErrInvalidValue(build.Spec.LastBuild.Image, "image").ViaField("spec", "lastBuild")) }) + it("validates bindings have a name", func() { + build.Spec.Bindings = []Binding{ + {MetadataRef: &corev1.LocalObjectReference{Name: "metadata"}}, + } + + assertValidationError(build, apis.ErrMissingField("spec.bindings[0].name")) + }) + + it("validates bindings have a valid name", func() { + build.Spec.Bindings = []Binding{ + {Name: "&", MetadataRef: &corev1.LocalObjectReference{Name: "metadata"}}, + } + + assertValidationError(build, apis.ErrInvalidValue("&", "spec.bindings[0].name")) + }) + + it("validates bindings have metadata", func() { + build.Spec.Bindings = []Binding{ + {Name: "apm"}, + } + + assertValidationError(build, apis.ErrMissingField("spec.bindings[0].metadataRef")) + }) + + it("validates bindings have non-empty metadata", func() { + build.Spec.Bindings = []Binding{ + {Name: "apm", MetadataRef: &corev1.LocalObjectReference{}}, + } + + assertValidationError(build, apis.ErrMissingField("spec.bindings[0].metadataRef.name")) + }) + + it("validates bindings have non-empty secrets", func() { + build.Spec.Bindings = []Binding{ + { + Name: "apm", + MetadataRef: &corev1.LocalObjectReference{Name: "metadata"}, + SecretRef: &corev1.LocalObjectReference{}, + }, + } + + assertValidationError(build, apis.ErrMissingField("spec.bindings[0].secretRef.name")) + }) + + it("validates bindings name uniqueness", func() { + build.Spec.Bindings = []Binding{ + { + Name: "apm", + MetadataRef: &corev1.LocalObjectReference{Name: "metadata"}, + }, + { + Name: "not-apm", + MetadataRef: &corev1.LocalObjectReference{Name: "metadata"}, + SecretRef: &corev1.LocalObjectReference{Name: "secret"}, + }, + { + Name: "apm", + MetadataRef: &corev1.LocalObjectReference{Name: "metadata"}, + }, + } + + assertValidationError(build, apis.ErrGeneric("duplicate binding name \"apm\"", "spec.bindings[0].name", "spec.bindings[2].name")) + }) + it("combining errors", func() { build.Spec.Tags = []string{} build.Spec.Builder.Image = "" diff --git a/pkg/apis/build/v1alpha1/image_builds.go b/pkg/apis/build/v1alpha1/image_builds.go index f1245a760..b233b7114 100644 --- a/pkg/apis/build/v1alpha1/image_builds.go +++ b/pkg/apis/build/v1alpha1/image_builds.go @@ -87,6 +87,7 @@ func (im *Image) build(sourceResolver *SourceResolver, builder BuilderResource, Builder: builder.BuildBuilderSpec(), Env: im.env(), Resources: im.resources(), + Bindings: im.bindings(), ServiceAccount: im.Spec.ServiceAccount, Source: sourceResolver.SourceConfig(), CacheName: im.Status.BuildCacheName, @@ -118,6 +119,13 @@ func (im *Image) latestForImage(build *Build) string { return latestImage } +func (im *Image) bindings() Bindings { + if im.Spec.Build == nil { + return nil + } + return im.Spec.Build.Bindings +} + func (im *Image) env() []corev1.EnvVar { if im.Spec.Build == nil { return nil diff --git a/pkg/apis/build/v1alpha1/image_types.go b/pkg/apis/build/v1alpha1/image_types.go index ead7aa585..e4e3f7ef6 100644 --- a/pkg/apis/build/v1alpha1/image_types.go +++ b/pkg/apis/build/v1alpha1/image_types.go @@ -66,6 +66,8 @@ const ( // +k8s:openapi-gen=true type ImageBuild struct { + // +listType + Bindings Bindings `json:"bindings,omitempty"` // +listType Env []corev1.EnvVar `json:"env,omitempty"` Resources corev1.ResourceRequirements `json:"resources,omitempty"` diff --git a/pkg/apis/build/v1alpha1/image_validation.go b/pkg/apis/build/v1alpha1/image_validation.go index 38adc4954..257996232 100644 --- a/pkg/apis/build/v1alpha1/image_validation.go +++ b/pkg/apis/build/v1alpha1/image_validation.go @@ -43,7 +43,8 @@ func (s *Image) Validate(ctx context.Context) *apis.FieldError { func (is *ImageSpec) Validate(ctx context.Context) *apis.FieldError { return is.validateTag(ctx). Also(validateBuilder(is.Builder).ViaField("builder")). - Also(is.Source.Validate(ctx).ViaField("source")) + Also(is.Source.Validate(ctx).ViaField("source")). + Also(is.Build.Validate(ctx).ViaField("build")) } func (im *ImageSpec) validateTag(ctx context.Context) *apis.FieldError { @@ -120,3 +121,11 @@ func (r *Registry) Validate(ctx context.Context) *apis.FieldError { return validate.Image(r.Image) } + +func (ib *ImageBuild) Validate(ctx context.Context) *apis.FieldError { + if ib == nil { + return nil + } + + return ib.Bindings.Validate(ctx).ViaField("bindings") +} diff --git a/pkg/apis/build/v1alpha1/image_validation_test.go b/pkg/apis/build/v1alpha1/image_validation_test.go index e04187f16..47e8fcd12 100644 --- a/pkg/apis/build/v1alpha1/image_validation_test.go +++ b/pkg/apis/build/v1alpha1/image_validation_test.go @@ -193,6 +193,14 @@ func testImageValidation(t *testing.T, when spec.G, it spec.S) { assertValidationError(image, apis.ErrInvalidValue(image.Spec.Source.Registry.Image, "image").ViaField("spec", "source", "registry")) }) + it("validates build bindings", func() { + image.Spec.Build.Bindings = []Binding{ + {MetadataRef: &corev1.LocalObjectReference{Name: "metadata"}}, + } + + assertValidationError(image, apis.ErrMissingField("spec.build.bindings[0].name")) + }) + it("combining errors", func() { image.Spec.Tag = "" image.Spec.Builder.Kind = "FakeBuilder" diff --git a/pkg/apis/build/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/build/v1alpha1/zz_generated.deepcopy.go index 12b584d48..2121d09f6 100644 --- a/pkg/apis/build/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/build/v1alpha1/zz_generated.deepcopy.go @@ -27,6 +27,54 @@ 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 *Binding) DeepCopyInto(out *Binding) { + *out = *in + if in.MetadataRef != nil { + in, out := &in.MetadataRef, &out.MetadataRef + *out = new(v1.LocalObjectReference) + **out = **in + } + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(v1.LocalObjectReference) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Binding. +func (in *Binding) DeepCopy() *Binding { + if in == nil { + return nil + } + out := new(Binding) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in Bindings) DeepCopyInto(out *Bindings) { + { + in := &in + *out = make(Bindings, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Bindings. +func (in Bindings) DeepCopy() Bindings { + if in == nil { + return nil + } + out := new(Bindings) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Blob) DeepCopyInto(out *Blob) { *out = *in @@ -175,6 +223,13 @@ func (in *BuildSpec) DeepCopyInto(out *BuildSpec) { } } in.Resources.DeepCopyInto(&out.Resources) + if in.Bindings != nil { + in, out := &in.Bindings, &out.Bindings + *out = make(Bindings, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.LastBuild != nil { in, out := &in.LastBuild, &out.LastBuild *out = new(LastBuild) @@ -556,6 +611,13 @@ func (in *ImageBuild) DeepCopyInto(out *ImageBuild) { } } in.Resources.DeepCopyInto(&out.Resources) + if in.Bindings != nil { + in, out := &in.Bindings, &out.Bindings + *out = make(Bindings, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } From 4ed73aca589fc71cfc39649b4a232ce619893725 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Tue, 5 May 2020 13:43:39 -0400 Subject: [PATCH 2/7] Mount bindings under platform-dir --- pkg/apis/build/v1alpha1/build_pod.go | 14 ++------------ pkg/apis/build/v1alpha1/build_pod_test.go | 16 +++------------- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/pkg/apis/build/v1alpha1/build_pod.go b/pkg/apis/build/v1alpha1/build_pod.go index ad88fcb27..fc1286ae0 100644 --- a/pkg/apis/build/v1alpha1/build_pod.go +++ b/pkg/apis/build/v1alpha1/build_pod.go @@ -84,10 +84,6 @@ var ( MountPath: "/builderPullSecrets", ReadOnly: true, } - bindingsEnv = corev1.EnvVar{ - Name: "CNB_BINDINGS", - Value: "/var/bindings", - } ) func (b *Build) BuildPod(config BuildPodImages, secrets []corev1.Secret, bc BuildPodBuilderConfig) (*corev1.Pod, error) { @@ -133,7 +129,7 @@ func (b *Build) BuildPod(config BuildPodImages, secrets []corev1.Secret, bc Buil bindingVolumeMounts = append(bindingVolumeMounts, corev1.VolumeMount{ Name: metadataVolume, - MountPath: fmt.Sprintf("%s/%s/metadata", bindingsEnv.Value, binding.Name), + MountPath: fmt.Sprintf("%s/bindings/%s/metadata", platformVolume.MountPath, binding.Name), ReadOnly: true, }, ) @@ -152,7 +148,7 @@ func (b *Build) BuildPod(config BuildPodImages, secrets []corev1.Secret, bc Buil bindingVolumeMounts = append(bindingVolumeMounts, corev1.VolumeMount{ Name: secretVolume, - MountPath: fmt.Sprintf("%s/%s/secret", bindingsEnv.Value, binding.Name), + MountPath: fmt.Sprintf("%s/bindings/%s/secret", platformVolume.MountPath, binding.Name), ReadOnly: true, }, ) @@ -237,9 +233,6 @@ func (b *Build) BuildPod(config BuildPodImages, secrets []corev1.Secret, bc Buil "-group=/layers/group.toml", "-plan=/layers/plan.toml", }, - Env: []corev1.EnvVar{ - bindingsEnv, - }, VolumeMounts: append([]corev1.VolumeMount{ layersVolume, platformVolume, @@ -305,9 +298,6 @@ func (b *Build) BuildPod(config BuildPodImages, secrets []corev1.Secret, bc Buil "-group=/layers/group.toml", "-plan=/layers/plan.toml", }, - Env: []corev1.EnvVar{ - bindingsEnv, - }, VolumeMounts: append([]corev1.VolumeMount{ layersVolume, platformVolume, diff --git a/pkg/apis/build/v1alpha1/build_pod_test.go b/pkg/apis/build/v1alpha1/build_pod_test.go index f2834c5c8..639efb6d1 100644 --- a/pkg/apis/build/v1alpha1/build_pod_test.go +++ b/pkg/apis/build/v1alpha1/build_pod_test.go @@ -276,28 +276,21 @@ func testBuildPod(t *testing.T, when spec.G, it spec.S) { ) for _, containerIdx := range []int{1 /* detect */, 4 /* build */} { - assert.Contains(t, - pod.Spec.InitContainers[containerIdx].Env, - corev1.EnvVar{ - Name: "CNB_BINDINGS", - Value: "/var/bindings", - }, - ) assert.Contains(t, pod.Spec.InitContainers[containerIdx].VolumeMounts, corev1.VolumeMount{ Name: "binding-metadata-database", - MountPath: "/var/bindings/database/metadata", + MountPath: "/platform/bindings/database/metadata", ReadOnly: true, }, corev1.VolumeMount{ Name: "binding-metadata-apm", - MountPath: "/var/bindings/apm/metadata", + MountPath: "/platform/bindings/apm/metadata", ReadOnly: true, }, corev1.VolumeMount{ Name: "binding-secret-apm", - MountPath: "/var/bindings/apm/secret", + MountPath: "/platform/bindings/apm/secret", ReadOnly: true, }, ) @@ -498,9 +491,6 @@ func testBuildPod(t *testing.T, when spec.G, it spec.S) { assert.Equal(t, pod.Spec.InitContainers[1].Name, "detect") assert.Equal(t, pod.Spec.InitContainers[1].Image, builderImage) - assert.Equal(t, pod.Spec.InitContainers[1].Env, []corev1.EnvVar{ - {Name: "CNB_BINDINGS", Value: "/var/bindings"}, - }) assert.Equal(t, []string{ "layers-dir", "platform-dir", From b01248ae36887bbfcbc6227c4e63797c32554e38 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Wed, 6 May 2020 11:53:46 -0400 Subject: [PATCH 3/7] Add sample Image resource with bindings --- pkg/apis/build/v1alpha1/image_builds.go | 2 +- samples/image_with_service_bindings.yaml | 64 ++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 samples/image_with_service_bindings.yaml diff --git a/pkg/apis/build/v1alpha1/image_builds.go b/pkg/apis/build/v1alpha1/image_builds.go index b233b7114..d105ee74a 100644 --- a/pkg/apis/build/v1alpha1/image_builds.go +++ b/pkg/apis/build/v1alpha1/image_builds.go @@ -85,9 +85,9 @@ func (im *Image) build(sourceResolver *SourceResolver, builder BuilderResource, Spec: BuildSpec{ Tags: im.generateTags(buildNumber), Builder: builder.BuildBuilderSpec(), + Bindings: im.bindings(), Env: im.env(), Resources: im.resources(), - Bindings: im.bindings(), ServiceAccount: im.Spec.ServiceAccount, Source: sourceResolver.SourceConfig(), CacheName: im.Status.BuildCacheName, diff --git a/samples/image_with_service_bindings.yaml b/samples/image_with_service_bindings.yaml new file mode 100644 index 000000000..99eaaf38e --- /dev/null +++ b/samples/image_with_service_bindings.yaml @@ -0,0 +1,64 @@ +apiVersion: build.pivotal.io/v1alpha1 +kind: Image +metadata: + name: sample-binding +spec: + tag: sample/image-with-binding + builder: + kind: Builder + name: sample-builder + serviceAccount: service-account + source: + git: + url: https://github.com/buildpack/sample-java-app.git + revision: 0eccc6c2f01d9f055087ebbf03526ed0623e014a + build: + bindings: + - name: sample + metadataRef: + name: sample-binding-metadata +--- +apiVersion: build.pivotal.io/v1alpha1 +kind: Image +metadata: + name: sample-binding-with-secret +spec: + tag: sample/image-with-binding-secret + builder: + kind: Builder + name: sample-builder + serviceAccount: service-account + source: + git: + url: https://github.com/buildpack/sample-java-app.git + revision: 0eccc6c2f01d9f055087ebbf03526ed0623e014a + build: + bindings: + - name: sample + metadataRef: + name: sample-binding-metadata + secretRef: + name: sample-binding-secret +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: sample-binding-metadata +data: + kind: mysql + provider: sample + tags: "" +--- +apiVersion: v1 +kind: Secret +metadata: + name: sample-binding-secret +type: Opaque +stringData: + hostname: localhost + jdbcUrl: jdbc:mysql://localhost:3306/default?user=root&password= + name: default + password: "" + port: "3306" + uri: mysql://root:@localhost:3306/default?reconnect=true + username: root From 990504fa09e21c1434412c3f131b2fefd10ee2d5 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Thu, 7 May 2020 09:43:02 -0400 Subject: [PATCH 4/7] Disable secret binding There are questions that need to be further explored about the implications of supporting secret binding. Exposing the secret name would potentially expose an enumeration attack again other secrets in the namespace. --- pkg/apis/build/v1alpha1/build_validation.go | 4 +++ .../build/v1alpha1/build_validation_test.go | 8 +++-- samples/image_with_service_bindings.yaml | 36 ------------------- 3 files changed, 10 insertions(+), 38 deletions(-) diff --git a/pkg/apis/build/v1alpha1/build_validation.go b/pkg/apis/build/v1alpha1/build_validation.go index a0156827d..eb8b1a13d 100644 --- a/pkg/apis/build/v1alpha1/build_validation.go +++ b/pkg/apis/build/v1alpha1/build_validation.go @@ -95,6 +95,10 @@ func (b *Binding) Validate(context context.Context) *apis.FieldError { errs = errs.Also(apis.ErrMissingField("metadataRef.name")) } + if b.SecretRef != nil { + // TODO(scothis) allow secrets once the security implications are better understood + errs = errs.Also(apis.ErrDisallowedFields("secretRef")) + } if b.SecretRef != nil && b.SecretRef.Name == "" { // secretRef is optional errs = errs.Also(apis.ErrMissingField("secretRef.name")) diff --git a/pkg/apis/build/v1alpha1/build_validation_test.go b/pkg/apis/build/v1alpha1/build_validation_test.go index 7ef29479c..0419623ff 100644 --- a/pkg/apis/build/v1alpha1/build_validation_test.go +++ b/pkg/apis/build/v1alpha1/build_validation_test.go @@ -187,7 +187,12 @@ func testBuildValidation(t *testing.T, when spec.G, it spec.S) { }, } - assertValidationError(build, apis.ErrMissingField("spec.bindings[0].secretRef.name")) + assertValidationError(build, + (&apis.FieldError{}).Also( + apis.ErrDisallowedFields("spec.bindings[0].secretRef"), + apis.ErrMissingField("spec.bindings[0].secretRef.name"), + ), + ) }) it("validates bindings name uniqueness", func() { @@ -199,7 +204,6 @@ func testBuildValidation(t *testing.T, when spec.G, it spec.S) { { Name: "not-apm", MetadataRef: &corev1.LocalObjectReference{Name: "metadata"}, - SecretRef: &corev1.LocalObjectReference{Name: "secret"}, }, { Name: "apm", diff --git a/samples/image_with_service_bindings.yaml b/samples/image_with_service_bindings.yaml index 99eaaf38e..0b54b58a7 100644 --- a/samples/image_with_service_bindings.yaml +++ b/samples/image_with_service_bindings.yaml @@ -18,28 +18,6 @@ spec: metadataRef: name: sample-binding-metadata --- -apiVersion: build.pivotal.io/v1alpha1 -kind: Image -metadata: - name: sample-binding-with-secret -spec: - tag: sample/image-with-binding-secret - builder: - kind: Builder - name: sample-builder - serviceAccount: service-account - source: - git: - url: https://github.com/buildpack/sample-java-app.git - revision: 0eccc6c2f01d9f055087ebbf03526ed0623e014a - build: - bindings: - - name: sample - metadataRef: - name: sample-binding-metadata - secretRef: - name: sample-binding-secret ---- apiVersion: v1 kind: ConfigMap metadata: @@ -48,17 +26,3 @@ data: kind: mysql provider: sample tags: "" ---- -apiVersion: v1 -kind: Secret -metadata: - name: sample-binding-secret -type: Opaque -stringData: - hostname: localhost - jdbcUrl: jdbc:mysql://localhost:3306/default?user=root&password= - name: default - password: "" - port: "3306" - uri: mysql://root:@localhost:3306/default?reconnect=true - username: root From 1b1064f529708098b5d0648abdee16958bfe3c2c Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Thu, 7 May 2020 18:27:22 -0400 Subject: [PATCH 5/7] Revert "Disable secret binding" This reverts commit 990504fa09e21c1434412c3f131b2fefd10ee2d5. --- pkg/apis/build/v1alpha1/build_validation.go | 4 --- .../build/v1alpha1/build_validation_test.go | 8 ++--- samples/image_with_service_bindings.yaml | 36 +++++++++++++++++++ 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/pkg/apis/build/v1alpha1/build_validation.go b/pkg/apis/build/v1alpha1/build_validation.go index eb8b1a13d..a0156827d 100644 --- a/pkg/apis/build/v1alpha1/build_validation.go +++ b/pkg/apis/build/v1alpha1/build_validation.go @@ -95,10 +95,6 @@ func (b *Binding) Validate(context context.Context) *apis.FieldError { errs = errs.Also(apis.ErrMissingField("metadataRef.name")) } - if b.SecretRef != nil { - // TODO(scothis) allow secrets once the security implications are better understood - errs = errs.Also(apis.ErrDisallowedFields("secretRef")) - } if b.SecretRef != nil && b.SecretRef.Name == "" { // secretRef is optional errs = errs.Also(apis.ErrMissingField("secretRef.name")) diff --git a/pkg/apis/build/v1alpha1/build_validation_test.go b/pkg/apis/build/v1alpha1/build_validation_test.go index 0419623ff..7ef29479c 100644 --- a/pkg/apis/build/v1alpha1/build_validation_test.go +++ b/pkg/apis/build/v1alpha1/build_validation_test.go @@ -187,12 +187,7 @@ func testBuildValidation(t *testing.T, when spec.G, it spec.S) { }, } - assertValidationError(build, - (&apis.FieldError{}).Also( - apis.ErrDisallowedFields("spec.bindings[0].secretRef"), - apis.ErrMissingField("spec.bindings[0].secretRef.name"), - ), - ) + assertValidationError(build, apis.ErrMissingField("spec.bindings[0].secretRef.name")) }) it("validates bindings name uniqueness", func() { @@ -204,6 +199,7 @@ func testBuildValidation(t *testing.T, when spec.G, it spec.S) { { Name: "not-apm", MetadataRef: &corev1.LocalObjectReference{Name: "metadata"}, + SecretRef: &corev1.LocalObjectReference{Name: "secret"}, }, { Name: "apm", diff --git a/samples/image_with_service_bindings.yaml b/samples/image_with_service_bindings.yaml index 0b54b58a7..99eaaf38e 100644 --- a/samples/image_with_service_bindings.yaml +++ b/samples/image_with_service_bindings.yaml @@ -18,6 +18,28 @@ spec: metadataRef: name: sample-binding-metadata --- +apiVersion: build.pivotal.io/v1alpha1 +kind: Image +metadata: + name: sample-binding-with-secret +spec: + tag: sample/image-with-binding-secret + builder: + kind: Builder + name: sample-builder + serviceAccount: service-account + source: + git: + url: https://github.com/buildpack/sample-java-app.git + revision: 0eccc6c2f01d9f055087ebbf03526ed0623e014a + build: + bindings: + - name: sample + metadataRef: + name: sample-binding-metadata + secretRef: + name: sample-binding-secret +--- apiVersion: v1 kind: ConfigMap metadata: @@ -26,3 +48,17 @@ data: kind: mysql provider: sample tags: "" +--- +apiVersion: v1 +kind: Secret +metadata: + name: sample-binding-secret +type: Opaque +stringData: + hostname: localhost + jdbcUrl: jdbc:mysql://localhost:3306/default?user=root&password= + name: default + password: "" + port: "3306" + uri: mysql://root:@localhost:3306/default?reconnect=true + username: root From d196f92634217e05a675831c50e9dfcc6e67d59d Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Thu, 7 May 2020 19:48:03 -0400 Subject: [PATCH 6/7] Reject a build if a binding references a secret attached to a service account --- pkg/apis/build/v1alpha1/build.go | 4 ++++ pkg/buildpod/generator.go | 38 +++++++++++++++++++++++++++++++- pkg/buildpod/generator_test.go | 31 ++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/pkg/apis/build/v1alpha1/build.go b/pkg/apis/build/v1alpha1/build.go index bf99e3ea9..8f784ca8b 100644 --- a/pkg/apis/build/v1alpha1/build.go +++ b/pkg/apis/build/v1alpha1/build.go @@ -25,6 +25,10 @@ func (b *Build) BuilderSpec() BuildBuilderSpec { return b.Spec.Builder } +func (b *Build) Bindings() []Binding { + return b.Spec.Bindings +} + func (b *Build) IsRunning() bool { if b == nil { return false diff --git a/pkg/buildpod/generator.go b/pkg/buildpod/generator.go index 0d479605f..f8405e29d 100644 --- a/pkg/buildpod/generator.go +++ b/pkg/buildpod/generator.go @@ -1,14 +1,15 @@ package buildpod import ( + "fmt" "strconv" "github.com/buildpacks/lifecycle" "github.com/google/go-containerregistry/pkg/authn" ggcrv1 "github.com/google/go-containerregistry/pkg/v1" "github.com/pkg/errors" - "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8sclient "k8s.io/client-go/kubernetes" @@ -39,11 +40,16 @@ type BuildPodable interface { GetNamespace() string ServiceAccount() string BuilderSpec() v1alpha1.BuildBuilderSpec + Bindings() []v1alpha1.Binding BuildPod(v1alpha1.BuildPodImages, []corev1.Secret, v1alpha1.BuildPodBuilderConfig) (*corev1.Pod, error) } func (g *Generator) Generate(build BuildPodable) (*v1.Pod, error) { + if err := g.buildAllowed(build); err != nil { + return nil, fmt.Errorf("build rejected: %w", err) + } + secrets, err := g.fetchBuildSecrets(build) if err != nil { return nil, err @@ -57,6 +63,36 @@ func (g *Generator) Generate(build BuildPodable) (*v1.Pod, error) { return build.BuildPod(g.BuildPodConfig, secrets, buildPodBuilderConfig) } +func (g *Generator) buildAllowed(build BuildPodable) error { + serviceAccounts, err := g.fetchServiceAccounts(build) + if err != nil { + return err + } + + var forbiddenSecrets = map[string]bool{} + for _, serviceAccount := range serviceAccounts { + for _, secret := range serviceAccount.Secrets { + forbiddenSecrets[secret.Name] = true + } + } + + for _, binding := range build.Bindings() { + if binding.SecretRef != nil && forbiddenSecrets[binding.SecretRef.Name] { + return fmt.Errorf("binding %q uses forbidden secret %q", binding.Name, binding.SecretRef.Name) + } + } + + return nil +} + +func (g *Generator) fetchServiceAccounts(build BuildPodable) ([]corev1.ServiceAccount, error) { + serviceAccounts, err := g.K8sClient.CoreV1().ServiceAccounts(build.GetNamespace()).List(metav1.ListOptions{}) + if err != nil { + return []v1.ServiceAccount{}, err + } + return serviceAccounts.Items, nil +} + func (g *Generator) fetchBuildSecrets(build BuildPodable) ([]corev1.Secret, error) { var secrets []corev1.Secret serviceAccount, err := g.K8sClient.CoreV1().ServiceAccounts(build.GetNamespace()).Get(build.ServiceAccount(), metav1.GetOptions{}) diff --git a/pkg/buildpod/generator_test.go b/pkg/buildpod/generator_test.go index 6a6b9a535..24687e8ed 100644 --- a/pkg/buildpod/generator_test.go +++ b/pkg/buildpod/generator_test.go @@ -1,6 +1,7 @@ package buildpod_test import ( + "fmt" "testing" "github.com/buildpacks/lifecycle" @@ -184,6 +185,31 @@ func testGenerator(t *testing.T, when spec.G, it spec.S) { }, }}, build.buildPodCalls) }) + + it("rejects a build with a binding secret that is attached to a service account", func() { + buildPodConfig := v1alpha1.BuildPodImages{} + generator := &buildpod.Generator{ + BuildPodConfig: buildPodConfig, + K8sClient: fakeK8sClient, + KeychainFactory: keychainFactory, + ImageFetcher: imageFetcher, + } + + var build = &testBuildPodable{ + namespace: namespace, + bindings: []v1alpha1.Binding{ + { + Name: "naughty", + MetadataRef: &corev1.LocalObjectReference{Name: "binding-configmap"}, + SecretRef: &corev1.LocalObjectReference{Name: dockerSecret.Name}, + }, + }, + } + + pod, err := generator.Generate(build) + require.EqualError(t, err, fmt.Sprintf("build rejected: binding %q uses forbidden secret %q", "naughty", dockerSecret.Name)) + require.Nil(t, pod) + }) }) } @@ -198,6 +224,7 @@ type testBuildPodable struct { serviceAccount string namespace string buildPodCalls []buildPodCall + bindings []v1alpha1.Binding } type buildPodCall struct { @@ -230,3 +257,7 @@ func (tb *testBuildPodable) BuildPod(images v1alpha1.BuildPodImages, secrets []c }) return &corev1.Pod{}, nil } + +func (tb *testBuildPodable) Bindings() []v1alpha1.Binding { + return tb.bindings +} From 2e7928720b027d48f2d6a17dcee46fd4598bd16d Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Mon, 11 May 2020 11:07:10 -0400 Subject: [PATCH 7/7] Move binding volume setup into a helper --- pkg/apis/build/v1alpha1/build_pod.go | 96 +++++++++++++++------------- 1 file changed, 51 insertions(+), 45 deletions(-) diff --git a/pkg/apis/build/v1alpha1/build_pod.go b/pkg/apis/build/v1alpha1/build_pod.go index fc1286ae0..157e36272 100644 --- a/pkg/apis/build/v1alpha1/build_pod.go +++ b/pkg/apis/build/v1alpha1/build_pod.go @@ -102,6 +102,8 @@ func (b *Build) BuildPod(config BuildPodImages, secrets []corev1.Secret, bc Buil secretVolumes, secretVolumeMounts, secretArgs := b.setupSecretVolumesAndArgs(secrets, gitAndDockerSecrets) + bindingVolumes, bindingVolumeMounts := b.setupBindings() + builderImage := b.Spec.Builder.Image workspaceVolume := corev1.VolumeMount{ @@ -110,51 +112,6 @@ func (b *Build) BuildPod(config BuildPodImages, secrets []corev1.Secret, bc Buil SubPath: b.Spec.Source.SubPath, // empty string is a nop } - bindingVolumes := []corev1.Volume{} - bindingVolumeMounts := []corev1.VolumeMount{} - for _, binding := range b.Spec.Bindings { - metadataVolume := fmt.Sprintf("binding-metadata-%s", binding.Name) - bindingVolumes = append(bindingVolumes, - corev1.Volume{ - Name: metadataVolume, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: binding.MetadataRef.Name, - }, - }, - }, - }, - ) - bindingVolumeMounts = append(bindingVolumeMounts, - corev1.VolumeMount{ - Name: metadataVolume, - MountPath: fmt.Sprintf("%s/bindings/%s/metadata", platformVolume.MountPath, binding.Name), - ReadOnly: true, - }, - ) - if binding.SecretRef != nil { - secretVolume := fmt.Sprintf("binding-secret-%s", binding.Name) - bindingVolumes = append(bindingVolumes, - corev1.Volume{ - Name: secretVolume, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: binding.SecretRef.Name, - }, - }, - }, - ) - bindingVolumeMounts = append(bindingVolumeMounts, - corev1.VolumeMount{ - Name: secretVolume, - MountPath: fmt.Sprintf("%s/bindings/%s/secret", platformVolume.MountPath, binding.Name), - ReadOnly: true, - }, - ) - } - } - return &corev1.Pod{ ObjectMeta: v1.ObjectMeta{ Name: b.PodName(), @@ -517,6 +474,55 @@ func (b *Build) setupSecretVolumesAndArgs(secrets []corev1.Secret, filter func(s return volumes, volumeMounts, args } +func (b *Build) setupBindings() ([]corev1.Volume, []corev1.VolumeMount) { + volumes := []corev1.Volume{} + volumeMounts := []corev1.VolumeMount{} + for _, binding := range b.Spec.Bindings { + metadataVolume := fmt.Sprintf("binding-metadata-%s", binding.Name) + volumes = append(volumes, + corev1.Volume{ + Name: metadataVolume, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: binding.MetadataRef.Name, + }, + }, + }, + }, + ) + volumeMounts = append(volumeMounts, + corev1.VolumeMount{ + Name: metadataVolume, + MountPath: fmt.Sprintf("%s/bindings/%s/metadata", platformVolume.MountPath, binding.Name), + ReadOnly: true, + }, + ) + if binding.SecretRef != nil { + secretVolume := fmt.Sprintf("binding-secret-%s", binding.Name) + volumes = append(volumes, + corev1.Volume{ + Name: secretVolume, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: binding.SecretRef.Name, + }, + }, + }, + ) + volumeMounts = append(volumeMounts, + corev1.VolumeMount{ + Name: secretVolume, + MountPath: fmt.Sprintf("%s/bindings/%s/secret", platformVolume.MountPath, binding.Name), + ReadOnly: true, + }, + ) + } + } + + return volumes, volumeMounts +} + func (bc *BuildPodBuilderConfig) legacy() bool { return bc.PlatformAPI == "0.2" }