Skip to content

Commit

Permalink
Creds-init writes to fixed location when HOME override is disabled
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Scott authored and tekton-robot committed Mar 24, 2020
1 parent a65caec commit 1325eea
Show file tree
Hide file tree
Showing 15 changed files with 509 additions and 23 deletions.
8 changes: 8 additions & 0 deletions cmd/entrypoint/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"syscall"
"time"

"github.com/tektoncd/pipeline/pkg/credentials"
"github.com/tektoncd/pipeline/pkg/entrypoint"
)

Expand Down Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions pkg/apis/pipeline/v1alpha1/task_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
13 changes: 13 additions & 0 deletions pkg/apis/pipeline/v1alpha1/taskrun_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
9 changes: 9 additions & 0 deletions pkg/apis/pipeline/v1beta1/task_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
13 changes: 13 additions & 0 deletions pkg/apis/pipeline/v1beta1/taskrun_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
100 changes: 100 additions & 0 deletions pkg/credentials/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
102 changes: 102 additions & 0 deletions pkg/credentials/initialize_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
66 changes: 65 additions & 1 deletion pkg/pod/creds_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down Expand Up @@ -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
}
Loading

0 comments on commit 1325eea

Please sign in to comment.