diff --git a/pkg/pod/pod.go b/pkg/pod/pod.go index 3bdd2fc49a6..7af61ca85c4 100644 --- a/pkg/pod/pod.go +++ b/pkg/pod/pod.go @@ -31,6 +31,7 @@ import ( "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" "github.com/tektoncd/pipeline/pkg/internal/computeresources/tasklevel" "github.com/tektoncd/pipeline/pkg/names" + "github.com/tektoncd/pipeline/pkg/spire" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -53,6 +54,9 @@ const ( // deadlineFactor is the factor we multiply the taskrun timeout with to determine the activeDeadlineSeconds of the Pod. // It has to be higher than the timeout (to not be killed before) deadlineFactor = 1.5 + + // csiDriver is the CSI storage plugin needed for injection of SPIFFE workload api. + SpiffeCsiDriver = "csi.spiffe.io" ) // These are effectively const, but Go doesn't have such an annotation. @@ -132,6 +136,10 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec // Secrets, along with any arguments needed by Step entrypoints to process // those secrets. commonExtraEntrypointArgs := []string{} + // Entrypoint arg to enable or disable spire + if config.IsSpireEnabled(ctx) { + commonExtraEntrypointArgs = append(commonExtraEntrypointArgs, "-enable_spire") + } credEntrypointArgs, credVolumes, credVolumeMounts, err := credsInit(ctx, taskRun.Spec.ServiceAccountName, taskRun.Namespace, b.KubeClient) if err != nil { return nil, err @@ -322,6 +330,39 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec return nil, err } + readonly := true + if config.IsSpireEnabled(ctx) { + // add SPIRE's CSI volume to the explicitly declared use volumes + volumes = append(volumes, corev1.Volume{ + Name: spire.WorkloadAPI, + VolumeSource: corev1.VolumeSource{ + CSI: &corev1.CSIVolumeSource{ + Driver: SpiffeCsiDriver, + ReadOnly: &readonly, + }, + }, + }) + + // mount SPIRE's CSI volume to each Step Container + for i := range stepContainers { + c := &stepContainers[i] + c.VolumeMounts = append(c.VolumeMounts, corev1.VolumeMount{ + Name: spire.WorkloadAPI, + MountPath: spire.VolumeMountPath, + ReadOnly: readonly, + }) + } + for i := range initContainers { + // mount SPIRE's CSI volume to each Init Container + c := &initContainers[i] + c.VolumeMounts = append(c.VolumeMounts, corev1.VolumeMount{ + Name: spire.WorkloadAPI, + MountPath: spire.VolumeMountPath, + ReadOnly: readonly, + }) + } + } + mergedPodContainers := stepContainers // Merge sidecar containers with step containers. diff --git a/pkg/pod/pod_test.go b/pkg/pod/pod_test.go index 37f62924c00..961e81dbdef 100644 --- a/pkg/pod/pod_test.go +++ b/pkg/pod/pod_test.go @@ -30,6 +30,7 @@ import ( "github.com/tektoncd/pipeline/pkg/apis/pipeline" "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "github.com/tektoncd/pipeline/pkg/spire" "github.com/tektoncd/pipeline/test/diff" "github.com/tektoncd/pipeline/test/names" corev1 "k8s.io/api/core/v1" @@ -2455,6 +2456,167 @@ func TestPodBuild_TaskLevelResourceRequirements(t *testing.T) { } } +func TestPodBuildwithSpireEnabled(t *testing.T) { + initContainers := []corev1.Container{entrypointInitContainer(images.EntrypointImage, []v1beta1.Step{{Name: "name"}})} + readonly := true + for i := range initContainers { + c := &initContainers[i] + c.VolumeMounts = append(c.VolumeMounts, corev1.VolumeMount{ + Name: spire.WorkloadAPI, + MountPath: spire.VolumeMountPath, + ReadOnly: true, + }) + } + + for _, c := range []struct { + desc string + trs v1beta1.TaskRunSpec + trAnnotation map[string]string + ts v1beta1.TaskSpec + want *corev1.PodSpec + wantAnnotations map[string]string + }{{ + desc: "simple", + ts: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Name: "name", + Image: "image", + Command: []string{"cmd"}, // avoid entrypoint lookup. + }}, + }, + want: &corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + InitContainers: initContainers, + Containers: []corev1.Container{{ + Name: "step-name", + Image: "image", + Command: []string{"/tekton/bin/entrypoint"}, + Args: []string{ + "-wait_file", + "/tekton/downward/ready", + "-wait_file_content", + "-post_file", + "/tekton/run/0/out", + "-termination_path", + "/tekton/termination", + "-step_metadata_dir", + "/tekton/run/0/status", + "-enable_spire", + "-entrypoint", + "cmd", + "--", + }, + VolumeMounts: append([]corev1.VolumeMount{binROMount, runMount(0, false), downwardMount, { + Name: "tekton-creds-init-home-0", + MountPath: "/tekton/creds", + }, { + Name: spire.WorkloadAPI, + MountPath: spire.VolumeMountPath, + ReadOnly: true, + }}, implicitVolumeMounts...), + TerminationMessagePath: "/tekton/termination", + }}, + Volumes: append(implicitVolumes, binVolume, runVolume(0), downwardVolume, corev1.Volume{ + Name: "tekton-creds-init-home-0", + VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{Medium: corev1.StorageMediumMemory}}, + }, corev1.Volume{ + Name: spire.WorkloadAPI, + VolumeSource: corev1.VolumeSource{ + CSI: &corev1.CSIVolumeSource{ + Driver: "csi.spiffe.io", + ReadOnly: &readonly, + }, + }, + }), + ActiveDeadlineSeconds: &defaultActiveDeadlineSeconds, + }, + }} { + t.Run(c.desc, func(t *testing.T) { + featureFlags := map[string]string{ + "enable-api-fields": "alpha", + "enforce-nonfalsifiability": "spire", + } + names.TestingSeed() + store := config.NewStore(logtesting.TestLogger(t)) + store.OnConfigChanged( + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: config.GetFeatureFlagsConfigName(), Namespace: system.Namespace()}, + Data: featureFlags, + }, + ) + kubeclient := fakek8s.NewSimpleClientset( + &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "default"}}, + &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "service-account", Namespace: "default"}, + Secrets: []corev1.ObjectReference{{ + Name: "multi-creds", + }}, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "multi-creds", + Namespace: "default", + Annotations: map[string]string{ + "tekton.dev/docker-0": "https://us.gcr.io", + "tekton.dev/docker-1": "https://docker.io", + "tekton.dev/git-0": "github.com", + "tekton.dev/git-1": "gitlab.com", + }}, + Type: "kubernetes.io/basic-auth", + Data: map[string][]byte{ + "username": []byte("foo"), + "password": []byte("BestEver"), + }, + }, + ) + var trAnnotations map[string]string + if c.trAnnotation == nil { + trAnnotations = map[string]string{ + ReleaseAnnotation: fakeVersion, + } + } else { + trAnnotations = c.trAnnotation + trAnnotations[ReleaseAnnotation] = fakeVersion + } + tr := &v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "taskrun-name", + Namespace: "default", + Annotations: trAnnotations, + }, + Spec: c.trs, + } + + // No entrypoints should be looked up. + entrypointCache := fakeCache{} + builder := Builder{ + Images: images, + KubeClient: kubeclient, + EntrypointCache: entrypointCache, + } + + got, err := builder.Build(store.ToContext(context.Background()), tr, c.ts) + if err != nil { + t.Fatalf("builder.Build: %v", err) + } + + want := kmeta.ChildName(tr.Name, "-pod") + if d := cmp.Diff(got.Name, want); d != "" { + t.Errorf("got %v; want %v", got.Name, want) + } + + if d := cmp.Diff(c.want, &got.Spec, resourceQuantityCmp, volumeSort, volumeMountSort); d != "" { + t.Errorf("Diff %s", diff.PrintWantGot(d)) + } + + if c.wantAnnotations != nil { + if d := cmp.Diff(c.wantAnnotations, got.ObjectMeta.Annotations, cmpopts.IgnoreMapEntries(ignoreReleaseAnnotation)); d != "" { + t.Errorf("Annotation Diff(-want, +got):\n%s", d) + } + } + }) + } +} + // verifyTaskLevelComputeResources verifies that the given TaskRun's containers have the expected compute resources. func verifyTaskLevelComputeResources(expectedComputeResources []ExpectedComputeResources, containers []corev1.Container) error { if len(expectedComputeResources) != len(containers) {