Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creds-init writes to fixed location when HOME override is disabled #2180

Merged
merged 1 commit into from Mar 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
vdemeester marked this conversation as resolved.
Show resolved Hide resolved
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