From 49d50d761d3ffd15879aa76ed955d7e81ba96187 Mon Sep 17 00:00:00 2001 From: Scott Date: Fri, 6 Mar 2020 14:46:01 -0500 Subject: [PATCH] Creds-init writes to fixed location when HOME override is disabled When the disable-home-env-overwrite flag is set to "true" each Step in a Task can conceivably have its own HOME directory. The concept of "HOME" is further muddied in systems that randomize the UID of containers. So now creds-init will write to a shared volumeMount, /tekton/creds, when the disable-home-env-overwrite flag is "true". When the flag is "false" creds-init will behave exactly the same as before, writing the credentials to /tekton/home, and no extra volume mount will be needed. This change should be mostly transparent to users: the entrypoint binary in each Step will now try and copy credentials out of /tekton/creds into $HOME/. The net result is the same as before the flag was introduced, it's just that entrypoint does the final copy into $HOME instead of creds-init. To support users who were in some way depending on the location of credentials, the path to where creds-init writes is now exposed for Tasks via the $(credentials.path) variable. This will be replaced with the directory that creds-init writes to: either "/tekton/home" or "/tekton/creds" depending on the state of the disable-home-env-overwrite flag. --- cmd/entrypoint/main.go | 8 ++ .../pipeline/v1alpha1/task_validation_test.go | 9 ++ .../v1alpha1/taskrun_validation_test.go | 13 +++ .../pipeline/v1beta1/task_validation_test.go | 9 ++ .../v1beta1/taskrun_validation_test.go | 13 +++ pkg/credentials/initialize.go | 100 +++++++++++++++++ pkg/credentials/initialize_test.go | 102 ++++++++++++++++++ pkg/pod/creds_init.go | 66 +++++++++++- pkg/pod/creds_init_test.go | 70 ++++++++++-- pkg/pod/pod.go | 27 +++-- pkg/pod/pod_test.go | 4 +- pkg/reconciler/taskrun/resources/apply.go | 9 ++ .../taskrun/resources/apply_test.go | 48 +++++++++ pkg/reconciler/taskrun/taskrun.go | 8 +- pkg/reconciler/taskrun/taskrun_test.go | 46 +++++++- 15 files changed, 509 insertions(+), 23 deletions(-) create mode 100644 pkg/credentials/initialize_test.go diff --git a/cmd/entrypoint/main.go b/cmd/entrypoint/main.go index 33e6e9b151f..8799e443471 100644 --- a/cmd/entrypoint/main.go +++ b/cmd/entrypoint/main.go @@ -25,6 +25,7 @@ import ( "syscall" "time" + "github.com/tektoncd/pipeline/pkg/credentials" "github.com/tektoncd/pipeline/pkg/entrypoint" ) @@ -53,6 +54,13 @@ func main() { PostWriter: &realPostWriter{}, Results: strings.Split(*results, ","), } + + // Copy any creds injected by creds-init into the $HOME directory of the current + // user so that they're discoverable by git / ssh. + if err := credentials.CopyCredsToHome(credentials.CredsInitCredentials); err != nil { + log.Printf("non-fatal error copying credentials: %q", err) + } + if err := e.Go(); err != nil { switch t := err.(type) { case skipError: diff --git a/pkg/apis/pipeline/v1alpha1/task_validation_test.go b/pkg/apis/pipeline/v1alpha1/task_validation_test.go index e847f95d8ed..3aedfd74ffa 100644 --- a/pkg/apis/pipeline/v1alpha1/task_validation_test.go +++ b/pkg/apis/pipeline/v1alpha1/task_validation_test.go @@ -255,6 +255,15 @@ func TestTaskSpecValidate(t *testing.T) { WorkingDir: "/foo/bar/$(outputs.resources.source)", }}}, }, + }, { + name: "valid creds-init path variable", + fields: fields{ + Steps: []v1alpha1.Step{{Container: corev1.Container{ + Name: "mystep", + Image: "echo", + Args: []string{"$(credentials.path)"}, + }}}, + }, }, { name: "step template included in validation", fields: fields{ diff --git a/pkg/apis/pipeline/v1alpha1/taskrun_validation_test.go b/pkg/apis/pipeline/v1alpha1/taskrun_validation_test.go index 3b8e50e9194..dbad70e7a6d 100644 --- a/pkg/apis/pipeline/v1alpha1/taskrun_validation_test.go +++ b/pkg/apis/pipeline/v1alpha1/taskrun_validation_test.go @@ -191,6 +191,19 @@ func TestTaskRunSpec_Validate(t *testing.T) { }}}, }}, }, + }, { + name: "task spec with credentials.path variable", + spec: v1alpha1.TaskRunSpec{ + TaskSpec: &v1alpha1.TaskSpec{TaskSpec: v1beta1.TaskSpec{ + Steps: []v1alpha1.Step{{ + Container: corev1.Container{ + Name: "mystep", + Image: "myimage", + }, + Script: `echo "creds-init writes to $(credentials.path)"`, + }}, + }}, + }, }} for _, ts := range tests { t.Run(ts.name, func(t *testing.T) { diff --git a/pkg/apis/pipeline/v1beta1/task_validation_test.go b/pkg/apis/pipeline/v1beta1/task_validation_test.go index 6809a9b4ca2..71cf8744e14 100644 --- a/pkg/apis/pipeline/v1beta1/task_validation_test.go +++ b/pkg/apis/pipeline/v1beta1/task_validation_test.go @@ -160,6 +160,15 @@ func TestTaskSpecValidate(t *testing.T) { WorkingDir: "/foo/bar/src/", }}}, }, + }, { + name: "valid creds-init path variable", + fields: fields{ + Steps: []v1beta1.Step{{Container: corev1.Container{ + Name: "mystep", + Image: "echo", + Args: []string{"$(credentials.path)"}, + }}}, + }, }, { name: "step template included in validation", fields: fields{ diff --git a/pkg/apis/pipeline/v1beta1/taskrun_validation_test.go b/pkg/apis/pipeline/v1beta1/taskrun_validation_test.go index 0d2035849a5..e2565887e8a 100644 --- a/pkg/apis/pipeline/v1beta1/taskrun_validation_test.go +++ b/pkg/apis/pipeline/v1beta1/taskrun_validation_test.go @@ -241,6 +241,19 @@ func TestTaskRunSpec_Validate(t *testing.T) { }}}, }, }, + }, { + name: "task spec with credentials.path variable", + spec: v1beta1.TaskRunSpec{ + TaskSpec: &v1beta1.TaskSpec{ + Steps: []v1alpha1.Step{{ + Container: corev1.Container{ + Name: "mystep", + Image: "myimage", + }, + Script: `echo "creds-init writes to $(credentials.path)"`, + }}, + }, + }, }} for _, ts := range tests { t.Run(ts.name, func(t *testing.T) { diff --git a/pkg/credentials/initialize.go b/pkg/credentials/initialize.go index 49ad7951f6a..59fd886dd5c 100644 --- a/pkg/credentials/initialize.go +++ b/pkg/credentials/initialize.go @@ -18,12 +18,34 @@ package credentials import ( "fmt" + "io" + "log" + "os" + "path/filepath" "sort" "strings" + "github.com/mitchellh/go-homedir" corev1 "k8s.io/api/core/v1" ) +const ( + // credsPath is the path where creds-init will store credentials + // when HOME is not being explicitly set to /tekton/home. + credsPath = "/tekton/creds" + + // credsDirPermissions are the persmission bits assigned to the directories + // copied out of the /tekton/creds and into a Step's HOME. + credsDirPermissions = 0700 + + // credsFilePermissions are the persmission bits assigned to the files + // copied out of /tekton/creds and into a Step's HOME. + credsFilePermissions = 0600 +) + +// CredsInitCredentials is the complete list of credentials that creds-init can write to /tekton/creds. +var CredsInitCredentials = []string{".docker", ".gitconfig", ".git-credentials", ".ssh"} + // VolumePath is the path where build secrets are written. // It is mutable and exported for testing. var VolumePath = "/tekton/creds-secrets" @@ -56,3 +78,81 @@ func SortAnnotations(secrets map[string]string, annotationPrefix string) []strin sort.Strings(mk) return mk } + +// CopyCredsToHome copies credentials from the /tekton/creds directory into +// the current Step's HOME directory. A list of credential paths to try and +// copy is given as an arg, for example, []string{".docker", ".ssh"}. A missing +// /tekton/creds directory is not considered an error. +func CopyCredsToHome(credPaths []string) error { + if info, err := os.Stat(credsPath); err != nil || !info.IsDir() { + return nil + } + + homepath, err := homedir.Dir() + if err != nil { + return fmt.Errorf("error getting the user's home directory: %w", err) + } + + for _, cred := range credPaths { + source := filepath.Join(credsPath, cred) + destination := filepath.Join(homepath, cred) + err := tryCopyCred(source, destination) + if err != nil { + log.Printf("unsuccessful cred copy: %q from %q to %q: %v", cred, credsPath, homepath, err) + } + } + return nil +} + +// tryCopyCred will recursively copy a given source path to a given +// destination path. A missing source file is treated as normal behaviour +// and no error is returned. +func tryCopyCred(source, destination string) error { + fromInfo, err := os.Lstat(source) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return fmt.Errorf("unable to read source file info: %w", err) + } + + fromFile, err := os.Open(filepath.Clean(source)) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return fmt.Errorf("unable to open source: %w", err) + } + defer fromFile.Close() + + if fromInfo.IsDir() { + err := os.MkdirAll(destination, credsDirPermissions) + if err != nil { + return fmt.Errorf("unable to create destination directory: %w", err) + } + subdirs, err := fromFile.Readdirnames(0) + if err != nil { + return fmt.Errorf("unable to read subdirectories of source: %w", err) + } + for _, subdir := range subdirs { + src := filepath.Join(source, subdir) + dst := filepath.Join(destination, subdir) + if err := tryCopyCred(src, dst); err != nil { + return err + } + } + } else { + flags := os.O_RDWR | os.O_CREATE | os.O_TRUNC + toFile, err := os.OpenFile(destination, flags, credsFilePermissions) + if err != nil { + return fmt.Errorf("unable to open destination: %w", err) + } + defer toFile.Close() + + _, err = io.Copy(toFile, fromFile) + if err != nil { + return fmt.Errorf("error copying from source to destination: %w", err) + } + } + return nil +} diff --git a/pkg/credentials/initialize_test.go b/pkg/credentials/initialize_test.go new file mode 100644 index 00000000000..cb2dd6332e7 --- /dev/null +++ b/pkg/credentials/initialize_test.go @@ -0,0 +1,102 @@ +package credentials + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" +) + +const credContents string = "hello, world!" + +func TestTryCopyCredDir(t *testing.T) { + dir, cleanup := createTempDir(t) + defer cleanup() + + fakeCredDir := filepath.Join(dir, ".docker") + err := os.Mkdir(fakeCredDir, 0700) + if err != nil { + t.Fatalf("unexpected error creating fake credential directory: %v", err) + } + credFilename := "important-credential.json" + writeFakeCred(t, fakeCredDir, credFilename, credContents) + destination := filepath.Join(dir, ".docker-copy") + + copiedFile := filepath.Join(destination, credFilename) + if err := tryCopyCred(fakeCredDir, destination); err != nil { + t.Fatalf("error creating copy of credential directory: %v", err) + } + if _, err := os.Lstat(filepath.Join(destination, credFilename)); err != nil { + t.Fatalf("error accessing copied credential: %v", err) + } + b, err := ioutil.ReadFile(copiedFile) + if err != nil { + t.Fatalf("unexpected error opening copied file: %v", err) + } + if string(b) != credContents { + t.Fatalf("mismatching file contents, expected %q received %q", credContents, string(b)) + } +} + +func TestTryCopyCredFile(t *testing.T) { + dir, cleanup := createTempDir(t) + defer cleanup() + fakeCredFile := writeFakeCred(t, dir, ".git-credentials", credContents) + destination := filepath.Join(dir, ".git-credentials-copy") + + if err := tryCopyCred(fakeCredFile, destination); err != nil { + t.Fatalf("error creating copy of credential file: %v", err) + } + if _, err := os.Lstat(destination); err != nil { + t.Fatalf("error accessing copied credential: %v", err) + } + b, err := ioutil.ReadFile(destination) + if err != nil { + t.Fatalf("unexpected error opening copied file: %v", err) + } + if string(b) != credContents { + t.Fatalf("mismatching file contents, expected %q received %q", credContents, string(b)) + } +} + +func TestTryCopyCredFileMissing(t *testing.T) { + dir, cleanup := createTempDir(t) + defer cleanup() + fakeCredFile := filepath.Join(dir, "foo") + destination := filepath.Join(dir, "foo-copy") + + if err := tryCopyCred(fakeCredFile, destination); err != nil { + t.Fatalf("error creating copy of credential file: %v", err) + } + if _, err := os.Lstat(destination); err != nil && !os.IsNotExist(err) { + t.Fatalf("error accessing copied credential: %v", err) + } + _, err := ioutil.ReadFile(destination) + if !os.IsNotExist(err) { + t.Fatalf("destination file exists but should not have been copied: %v", err) + } +} + +func writeFakeCred(t *testing.T, dir, name, contents string) string { + flags := os.O_RDWR | os.O_CREATE | os.O_TRUNC + path := filepath.Join(dir, name) + cred, err := os.OpenFile(path, flags, 0600) + if err != nil { + t.Fatalf("unexpected error writing fake credential: %v", err) + } + _, _ = cred.Write([]byte(credContents)) + _ = cred.Close() + return path +} + +func createTempDir(t *testing.T) (string, func()) { + dir, err := ioutil.TempDir("", "cred-test-fs-") + if err != nil { + t.Fatalf("unexpected error creating temp directory: %v", err) + } + return dir, func() { + if err := os.RemoveAll(dir); err != nil { + t.Errorf("unexpected error cleaning up temp directory: %v", err) + } + } +} diff --git a/pkg/pod/creds_init.go b/pkg/pod/creds_init.go index eb3b853fbf4..d9742c31a43 100644 --- a/pkg/pod/creds_init.go +++ b/pkg/pod/creds_init.go @@ -28,6 +28,22 @@ import ( "k8s.io/client-go/kubernetes" ) +const homeEnvVar = "HOME" +const credsInitHomeMountName = "tekton-creds-init-home" +const credsInitHomeDir = "/tekton/creds" + +var credsInitHomeVolume = corev1.Volume{ + Name: credsInitHomeMountName, + VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{ + Medium: corev1.StorageMediumMemory, + }}, +} + +var credsInitHomeVolumeMount = corev1.VolumeMount{ + Name: credsInitHomeMountName, + MountPath: credsInitHomeDir, +} + // credsInit returns an init container that initializes credentials based on // annotated secrets available to the service account. // @@ -86,12 +102,60 @@ func credsInit(credsImage string, serviceAccountName, namespace string, kubeclie return nil, nil, nil } + env := ensureCredsInitHomeEnv(implicitEnvVars) + return &corev1.Container{ Name: "credential-initializer", Image: credsImage, Command: []string{"/ko-app/creds-init"}, Args: args, - Env: implicitEnvVars, + Env: env, VolumeMounts: volumeMounts, }, volumes, nil } + +// CredentialsPath returns a string path to the location that the creds-init +// helper binary will write its credentials to. The only argument is a boolean +// true if Tekton will overwrite Steps' default HOME environment variable +// with /tekton/home. +func CredentialsPath(shouldOverrideHomeEnv bool) string { + if shouldOverrideHomeEnv { + return homeDir + } + return credsInitHomeDir +} + +// ensureCredsInitHomeEnv ensures that creds-init always has its HOME environment +// variable set to /tekton/creds unless it's already been explicitly set to +// something else. +// +// We do this because Tekton's HOME override is being deprecated: +// creds-init doesn't know the HOME directories of every Step in +// the Task, and may not even be able to tell this in advance because +// of randomized container UIDs like those of OpenShift. So, instead, +// creds-init writes credentials to a single known location (/tekton/creds) +// and leaves it up to the user's Steps to put those credentials in the +// correct place. +func ensureCredsInitHomeEnv(existingEnvVars []corev1.EnvVar) []corev1.EnvVar { + env := []corev1.EnvVar{} + setHome := true + for _, e := range existingEnvVars { + if e.Name == homeEnvVar { + setHome = false + } + env = append(env, e) + } + if setHome { + env = append(env, corev1.EnvVar{ + Name: homeEnvVar, + Value: credsInitHomeDir, + }) + } + return env +} + +// getCredsInitVolume returns the Volume and VolumeMount configuration needed +// to mount the creds-init volume in Steps. +func getCredsInitVolume(volumes []corev1.Volume) (corev1.Volume, corev1.VolumeMount) { + return credsInitHomeVolume, credsInitHomeVolumeMount +} diff --git a/pkg/pod/creds_init_test.go b/pkg/pod/creds_init_test.go index b72681bb5fe..5ff16d60c8e 100644 --- a/pkg/pod/creds_init_test.go +++ b/pkg/pod/creds_init_test.go @@ -36,15 +36,24 @@ func TestCredsInit(t *testing.T) { volumeMounts := []corev1.VolumeMount{{ Name: "implicit-volume-mount", }} - envVars := []corev1.EnvVar{{ + fooEnvVar := corev1.EnvVar{ Name: "FOO", Value: "bar", - }} + } + credsInitHomeEnvVar := corev1.EnvVar{ + Name: "HOME", + Value: credsInitHomeDir, + } + customHomeEnvVar := corev1.EnvVar{ + Name: "HOME", + Value: "/users/home/my-test-user", + } for _, c := range []struct { - desc string - want *corev1.Container - objs []runtime.Object + desc string + want *corev1.Container + objs []runtime.Object + envVars []corev1.EnvVar }{{ desc: "service account exists with no secrets; nothing to initialize", objs: []runtime.Object{ @@ -72,7 +81,51 @@ func TestCredsInit(t *testing.T) { }, want: nil, }, { - desc: "service account has annotated secret; initialize creds", + desc: "service account has annotated secret and no HOME env var passed in; initialize creds in /tekton/creds", + objs: []runtime.Object{ + &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{Name: serviceAccountName, Namespace: namespace}, + Secrets: []corev1.ObjectReference{{ + Name: "my-creds", + }}, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-creds", + Namespace: namespace, + 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"), + }, + }, + }, + envVars: []corev1.EnvVar{fooEnvVar}, + want: &corev1.Container{ + Name: "credential-initializer", + Image: images.CredsImage, + Command: []string{"/ko-app/creds-init"}, + Args: []string{ + "-basic-docker=my-creds=https://docker.io", + "-basic-docker=my-creds=https://us.gcr.io", + "-basic-git=my-creds=github.com", + "-basic-git=my-creds=gitlab.com", + }, + Env: []corev1.EnvVar{fooEnvVar, credsInitHomeEnvVar}, + VolumeMounts: append(volumeMounts, corev1.VolumeMount{ + Name: "tekton-internal-secret-volume-my-creds-9l9zj", + MountPath: "/tekton/creds-secrets/my-creds", + }), + }, + }, { + desc: "service account with secret and HOME env var passed in", objs: []runtime.Object{ &corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{Name: serviceAccountName, Namespace: namespace}, @@ -98,6 +151,7 @@ func TestCredsInit(t *testing.T) { }, }, }, + envVars: []corev1.EnvVar{customHomeEnvVar}, want: &corev1.Container{ Name: "credential-initializer", Image: images.CredsImage, @@ -108,7 +162,7 @@ func TestCredsInit(t *testing.T) { "-basic-git=my-creds=github.com", "-basic-git=my-creds=gitlab.com", }, - Env: envVars, + Env: []corev1.EnvVar{customHomeEnvVar}, VolumeMounts: append(volumeMounts, corev1.VolumeMount{ Name: "tekton-internal-secret-volume-my-creds-9l9zj", MountPath: "/tekton/creds-secrets/my-creds", @@ -118,7 +172,7 @@ func TestCredsInit(t *testing.T) { t.Run(c.desc, func(t *testing.T) { names.TestingSeed() kubeclient := fakek8s.NewSimpleClientset(c.objs...) - got, volumes, err := credsInit(images.CredsImage, serviceAccountName, namespace, kubeclient, volumeMounts, envVars) + got, volumes, err := credsInit(images.CredsImage, serviceAccountName, namespace, kubeclient, volumeMounts, c.envVars) if err != nil { t.Fatalf("credsInit: %v", err) } diff --git a/pkg/pod/pod.go b/pkg/pod/pod.go index 5d700059b1d..ad6f0811919 100644 --- a/pkg/pod/pod.go +++ b/pkg/pod/pod.go @@ -79,24 +79,31 @@ var ( // MakePod converts TaskRun and TaskSpec objects to a Pod which implements the taskrun specified // by the supplied CRD. -func MakePod(images pipeline.Images, taskRun *v1alpha1.TaskRun, taskSpec v1alpha1.TaskSpec, kubeclient kubernetes.Interface, entrypointCache EntrypointCache) (*corev1.Pod, error) { +func MakePod(images pipeline.Images, taskRun *v1alpha1.TaskRun, taskSpec v1alpha1.TaskSpec, kubeclient kubernetes.Interface, entrypointCache EntrypointCache, overrideHomeEnv bool) (*corev1.Pod, error) { var initContainers []corev1.Container var volumes []corev1.Volume - + var volumeMounts []corev1.VolumeMount implicitEnvVars := []corev1.EnvVar{} - if shouldOverrideHomeEnv(kubeclient) { + // Add our implicit volumes first, so they can be overridden by the user if they prefer. + volumes = append(volumes, implicitVolumes...) + volumeMounts = append(volumeMounts, implicitVolumeMounts...) + + if overrideHomeEnv { implicitEnvVars = append(implicitEnvVars, corev1.EnvVar{ Name: "HOME", Value: homeDir, }) + } else { + // Add the volume that creds-init will write to when + // there's no consistent $HOME for Steps. + v, vm := getCredsInitVolume(volumes) + volumes = append(volumes, v) + volumeMounts = append(volumeMounts, vm) } - // Add our implicit volumes first, so they can be overridden by the user if they prefer. - volumes = append(volumes, implicitVolumes...) - // Inititalize any credentials found in annotated Secrets. - if credsInitContainer, secretsVolumes, err := credsInit(images.CredsImage, taskRun.Spec.ServiceAccountName, taskRun.Namespace, kubeclient, implicitVolumeMounts, implicitEnvVars); err != nil { + if credsInitContainer, secretsVolumes, err := credsInit(images.CredsImage, taskRun.Spec.ServiceAccountName, taskRun.Namespace, kubeclient, volumeMounts, implicitEnvVars); err != nil { return nil, err } else if credsInitContainer != nil { initContainers = append(initContainers, *credsInitContainer) @@ -162,7 +169,7 @@ func MakePod(images pipeline.Images, taskRun *v1alpha1.TaskRun, taskSpec v1alpha requestedVolumeMounts[filepath.Clean(vm.MountPath)] = true } var toAdd []corev1.VolumeMount - for _, imp := range implicitVolumeMounts { + for _, imp := range volumeMounts { if !requestedVolumeMounts[filepath.Clean(imp.MountPath)] { toAdd = append(toAdd, imp) } @@ -309,13 +316,13 @@ func getLimitRangeMinimum(namespace string, kubeclient kubernetes.Interface) (co return min, nil } -// shouldOverrideHomeEnv returns a bool indicating whether a Pod should have its +// ShouldOverrideHomeEnv returns a bool indicating whether a Pod should have its // $HOME environment variable overwritten with /tekton/home or if it should be // left unmodified. The default behaviour is to overwrite the $HOME variable // but this is planned to change in an upcoming release. // // For further reference see https://github.com/tektoncd/pipeline/issues/2013 -func shouldOverrideHomeEnv(kubeclient kubernetes.Interface) bool { +func ShouldOverrideHomeEnv(kubeclient kubernetes.Interface) bool { configMap, err := kubeclient.CoreV1().ConfigMaps(system.GetNamespace()).Get(featureFlagConfigMapName, metav1.GetOptions{}) if err == nil && configMap != nil && configMap.Data != nil && configMap.Data[featureFlagDisableHomeEnvKey] == "true" { return false diff --git a/pkg/pod/pod_test.go b/pkg/pod/pod_test.go index 5d7d7e2d01b..d43b04281e6 100644 --- a/pkg/pod/pod_test.go +++ b/pkg/pod/pod_test.go @@ -768,7 +768,7 @@ script-heredoc-randomly-generated-78c5n // No entrypoints should be looked up. entrypointCache := fakeCache{} - got, err := MakePod(images, tr, c.ts, kubeclient, entrypointCache) + got, err := MakePod(images, tr, c.ts, kubeclient, entrypointCache, true) if err != nil { t.Fatalf("MakePod: %v", err) } @@ -840,7 +840,7 @@ func TestShouldOverrideHomeEnv(t *testing.T) { kubeclient := fakek8s.NewSimpleClientset( tc.configMap, ) - if result := shouldOverrideHomeEnv(kubeclient); result != tc.expected { + if result := ShouldOverrideHomeEnv(kubeclient); result != tc.expected { t.Errorf("Expected %t Received %t", tc.expected, result) } }) diff --git a/pkg/reconciler/taskrun/resources/apply.go b/pkg/reconciler/taskrun/resources/apply.go index 2618e7d8f43..8bfc6fc515b 100644 --- a/pkg/reconciler/taskrun/resources/apply.go +++ b/pkg/reconciler/taskrun/resources/apply.go @@ -125,6 +125,15 @@ func ApplyTaskResults(spec *v1alpha1.TaskSpec) *v1alpha1.TaskSpec { return ApplyReplacements(spec, stringReplacements, map[string][]string{}) } +// ApplyCredentialsPath applies a substitution of the key $(credentials.path) with the path that the creds-init +// helper will write its credentials to. +func ApplyCredentialsPath(spec *v1alpha1.TaskSpec, path string) *v1alpha1.TaskSpec { + stringReplacements := map[string]string{ + "credentials.path": path, + } + return ApplyReplacements(spec, stringReplacements, map[string][]string{}) +} + // ApplyReplacements replaces placeholders for declared parameters with the specified replacements. func ApplyReplacements(spec *v1alpha1.TaskSpec, stringReplacements map[string]string, arrayReplacements map[string][]string) *v1alpha1.TaskSpec { spec = spec.DeepCopy() diff --git a/pkg/reconciler/taskrun/resources/apply_test.go b/pkg/reconciler/taskrun/resources/apply_test.go index 24c7224601f..7d38f388190 100644 --- a/pkg/reconciler/taskrun/resources/apply_test.go +++ b/pkg/reconciler/taskrun/resources/apply_test.go @@ -748,3 +748,51 @@ func TestTaskResults(t *testing.T) { t.Errorf("ApplyTaskResults() got diff %s", d) } } + +func TestApplyCredentialsPath(t *testing.T) { + for _, tc := range []struct { + description string + spec v1alpha1.TaskSpec + path string + want v1alpha1.TaskSpec + }{{ + description: "replacement in spec container", + spec: v1alpha1.TaskSpec{TaskSpec: v1beta1.TaskSpec{ + Steps: []v1alpha1.Step{{ + Container: corev1.Container{ + Command: []string{"cp"}, + Args: []string{"-R", "$(credentials.path)/", "$HOME"}, + }, + }}, + }}, + path: "/tekton/creds", + want: v1alpha1.TaskSpec{TaskSpec: v1beta1.TaskSpec{ + Steps: []v1alpha1.Step{{ + Container: corev1.Container{ + Command: []string{"cp"}, + Args: []string{"-R", "/tekton/creds/", "$HOME"}, + }, + }}, + }}, + }, { + description: "replacement in spec Script", + spec: v1alpha1.TaskSpec{TaskSpec: v1beta1.TaskSpec{ + Steps: []v1alpha1.Step{{ + Script: `cp -R "$(credentials.path)/" $HOME`, + }}, + }}, + path: "/tekton/home", + want: v1alpha1.TaskSpec{TaskSpec: v1beta1.TaskSpec{ + Steps: []v1alpha1.Step{{ + Script: `cp -R "/tekton/home/" $HOME`, + }}, + }}, + }} { + t.Run(tc.description, func(t *testing.T) { + got := resources.ApplyCredentialsPath(&tc.spec, tc.path) + if diff := cmp.Diff(&tc.want, got); diff != "" { + t.Errorf("(-want, +got): %s", diff) + } + }) + } +} diff --git a/pkg/reconciler/taskrun/taskrun.go b/pkg/reconciler/taskrun/taskrun.go index 634c27298b3..89122bc2271 100644 --- a/pkg/reconciler/taskrun/taskrun.go +++ b/pkg/reconciler/taskrun/taskrun.go @@ -564,7 +564,13 @@ func (c *Reconciler) createPod(tr *v1alpha1.TaskRun, rtr *resources.ResolvedTask return nil, err } - pod, err := podconvert.MakePod(c.Images, tr, *ts, c.KubeClientSet, c.entrypointCache) + // Check if the HOME env var of every Step should be set to /tekton/home. + shouldOverrideHomeEnv := podconvert.ShouldOverrideHomeEnv(c.KubeClientSet) + + // Apply creds-init path substitutions. + ts = resources.ApplyCredentialsPath(ts, podconvert.CredentialsPath(shouldOverrideHomeEnv)) + + pod, err := podconvert.MakePod(c.Images, tr, *ts, c.KubeClientSet, c.entrypointCache, shouldOverrideHomeEnv) if err != nil { return nil, fmt.Errorf("translating TaskSpec to Pod: %w", err) } diff --git a/pkg/reconciler/taskrun/taskrun_test.go b/pkg/reconciler/taskrun/taskrun_test.go index 1bb34e7d610..f7cb51bffb2 100644 --- a/pkg/reconciler/taskrun/taskrun_test.go +++ b/pkg/reconciler/taskrun/taskrun_test.go @@ -538,11 +538,18 @@ func TestReconcile(t *testing.T) { tb.TaskRunStatus(tb.PodName("some-pod-abcdethat-no-longer-exists")), ) + taskRunWithCredentialsVariable := tb.TaskRun("test-taskrun-with-credentials-variable", "foo", tb.TaskRunSpec( + tb.TaskRunTaskSpec( + tb.Step("myimage", tb.StepName("mycontainer"), tb.StepCommand("/mycmd $(credentials.path)")), + ), + )) + taskruns := []*v1alpha1.TaskRun{ taskRunSuccess, taskRunWithSaSuccess, taskRunSubstitution, taskRunInputOutput, taskRunWithTaskSpec, taskRunWithClusterTask, taskRunWithResourceSpecAndTaskSpec, taskRunWithLabels, taskRunWithAnnotations, taskRunWithPod, + taskRunWithCredentialsVariable, } d := test.Data{ @@ -899,6 +906,43 @@ func TestReconcile(t *testing.T) { ), ), ), + }, { + name: "taskrun-with-credentials-variable-default-tekton-home", + taskRun: taskRunWithCredentialsVariable, + wantPod: tb.Pod("test-taskrun-with-credentials-variable-pod-9l9zj", "foo", + tb.PodAnnotation(podconvert.ReleaseAnnotation, podconvert.ReleaseAnnotationValue), + tb.PodLabel(taskRunNameLabelKey, "test-taskrun-with-credentials-variable"), + tb.PodLabel("app.kubernetes.io/managed-by", "tekton-pipelines"), + tb.PodOwnerReference("TaskRun", "test-taskrun-with-credentials-variable", + tb.OwnerReferenceAPIVersion(currentAPIVersion)), + tb.PodSpec( + tb.PodVolumes(workspaceVolume, homeVolume, resultsVolume, toolsVolume, downwardVolume), + tb.PodRestartPolicy(corev1.RestartPolicyNever), + getPlaceToolsInitContainer(), + tb.PodContainer("step-mycontainer", "myimage", + tb.Command("/tekton/tools/entrypoint"), + tb.Args("-wait_file", + "/tekton/downward/ready", + "-wait_file_content", + "-post_file", + "/tekton/tools/0", + "-termination_path", + "/tekton/termination", + "-entrypoint", + // Important bit here: /tekton/home + "/mycmd /tekton/home", + "--"), + tb.WorkingDir(workspaceDir), + tb.EnvVar("HOME", "/tekton/home"), + tb.VolumeMount("tekton-internal-tools", "/tekton/tools"), + tb.VolumeMount("tekton-internal-downward", "/tekton/downward"), + tb.VolumeMount("tekton-internal-workspace", workspaceDir), + tb.VolumeMount("tekton-internal-home", "/tekton/home"), + tb.VolumeMount("tekton-internal-results", "/tekton/results"), + tb.TerminationMessagePath("/tekton/termination"), + ), + ), + ), }} { t.Run(tc.name, func(t *testing.T) { names.TestingSeed() @@ -1197,7 +1241,7 @@ func makePod(taskRun *v1alpha1.TaskRun, task *v1alpha1.Task) (*corev1.Pod, error return nil, err } - return podconvert.MakePod(images, taskRun, task.Spec, kubeclient, entrypointCache) + return podconvert.MakePod(images, taskRun, task.Spec, kubeclient, entrypointCache, true) } func TestReconcilePodUpdateStatus(t *testing.T) {