diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..618d93c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM gcr.io/google_containers/ubuntu-slim:0.1 + +ENV GIT_SYNC_DEST /git +VOLUME ["/git"] + +RUN apt-get update && \ + apt-get install -y git ca-certificates --no-install-recommends && \ + apt-get install -y openssh-client && \ + apt-get clean -y && \ + rm -rf /var/lib/apt/lists/* + +COPY git-sync /git-sync + +# Move the existing SSH binary, then replace it with the wrapper script +RUN mv /usr/bin/ssh /usr/bin/ssh-binary +COPY ssh-wrapper.sh /usr/bin/ssh +RUN chmod 755 /usr/bin/ssh + +RUN mkdir /nonexistent && chmod 777 /nonexistent + +ENTRYPOINT ["/git-sync"] diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json new file mode 100644 index 0000000..be1078e --- /dev/null +++ b/Godeps/Godeps.json @@ -0,0 +1,8 @@ +{ + "ImportPath": "k8s.io/git-sync", + "GoVersion": "go1.5", + "Packages": [ + "./..." + ], + "Deps": [] +} diff --git a/Godeps/Readme b/Godeps/Readme new file mode 100644 index 0000000..4cdaa53 --- /dev/null +++ b/Godeps/Readme @@ -0,0 +1,5 @@ +This directory tree is generated automatically by godep. + +Please do not edit. + +See https://github.com/tools/godep for more information. diff --git a/Godeps/_workspace/.gitignore b/Godeps/_workspace/.gitignore new file mode 100644 index 0000000..f037d68 --- /dev/null +++ b/Godeps/_workspace/.gitignore @@ -0,0 +1,2 @@ +/pkg +/bin diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..73bfcc7 --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +all: push + +# 0.0 shouldn't clobber any released builds +TAG = 0.0 +PREFIX = gcr.io/google_containers/git-sync + +binary: main.go + CGO_ENABLED=0 GOOS=linux godep go build -a -installsuffix cgo -ldflags '-w' -o git-sync + +container: binary + docker build -t $(PREFIX):$(TAG) . + +push: container + gcloud docker push $(PREFIX):$(TAG) + +clean: + docker rmi -f $(PREFIX):$(TAG) || true diff --git a/OWNERS b/OWNERS new file mode 100644 index 0000000..966dbdf --- /dev/null +++ b/OWNERS @@ -0,0 +1,4 @@ +assignees: +- mikedanese +- paulbakker + diff --git a/README.md b/README.md new file mode 100644 index 0000000..c6b9f75 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# git-sync + +git-sync is a command that pull a git repository to a local directory. + +It can be used to source a container volume with the content of a git repo. + +In order to ensure that the git repository content is cloned and updated atomically, you cannot use a volume root directory as the directory for your local repository. + +The local repository is created in a subdirectory of /git, with the subdirectory name specified by GIT_SYNC_DEST. + +## Usage + +``` +# build the container +docker build -t git-sync . +# run the git-sync container +docker run -d GIT_SYNC_REPO=https://github.com/GoogleCloudPlatform/kubernetes GIT_SYNC_DEST=/git -e GIT_SYNC_BRANCH=gh-pages -r HEAD -v /git-data:/git git-sync +# run a nginx container to serve sync'ed content +docker run -d -p 8080:80 -v /git-data:/usr/share/nginx/html nginx +``` + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/git-sync/README.md?pixel)]() diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..7917055 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,33 @@ +# git-blog-demo + +This demo shows how to use the `git-sync` sidekick container along side `volumes` and `volumeMounts` to create a markdown powered blog. + +## How it works + +The pod is composed of 3 containers that share directories using 2 volumes: + +- The `git-sync` container clones a git repo into the `markdown` volume +- The `hugo` container read from the `markdown` volume and render it into the `html` volume. +- The `nginx` container serve the content from the `html` volume. + +## Usage + +Build the demo containers, and push them to a registry + +``` +docker build -t /git-sync .. +docker build -t /hugo hugo/ +docker push /hugo /git-sync +``` + +Create the pod and the service for the blog + +``` +kubectl pods create config/pod.html +kubectl services create config/pod.html +``` + +Open the service external ip in your browser + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/git-sync/demo/README.md?pixel)]() diff --git a/demo/blog/archetypes/.keep b/demo/blog/archetypes/.keep new file mode 100644 index 0000000..e69de29 diff --git a/demo/blog/config.toml b/demo/blog/config.toml new file mode 100644 index 0000000..ab3aa9e --- /dev/null +++ b/demo/blog/config.toml @@ -0,0 +1,3 @@ +baseurl = "http://example.com" +languageCode = "en-us" +title = "example blog" diff --git a/demo/blog/content/about.md b/demo/blog/content/about.md new file mode 100644 index 0000000..e632d28 --- /dev/null +++ b/demo/blog/content/about.md @@ -0,0 +1,12 @@ ++++ +date = "2014-12-19T15:29:48-08:00" +draft = true +title = "about" ++++ + +## A headline + +Some content about the blog. + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/git-sync/demo/blog/content/about.md?pixel)]() diff --git a/demo/blog/content/post/first.md b/demo/blog/content/post/first.md new file mode 100644 index 0000000..6dc8c9c --- /dev/null +++ b/demo/blog/content/post/first.md @@ -0,0 +1,12 @@ ++++ +date = "2014-12-19T15:30:18-08:00" +draft = true +title = "first" ++++ + +## first port + +This is the first post. + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/git-sync/demo/blog/content/post/first.md?pixel)]() diff --git a/demo/blog/layouts/.keep b/demo/blog/layouts/.keep new file mode 100644 index 0000000..e69de29 diff --git a/demo/blog/static/.keep b/demo/blog/static/.keep new file mode 100644 index 0000000..e69de29 diff --git a/demo/config/pod.yaml b/demo/config/pod.yaml new file mode 100644 index 0000000..e6b0ac3 --- /dev/null +++ b/demo/config/pod.yaml @@ -0,0 +1,46 @@ +apiVersion: v1 +kind: Pod +metadata: + labels: + name: blog + name: blog-pod +spec: + containers: + - name: git-sync + image: gcr.io/google_containers/git-sync + imagePullPolicy: Always + volumeMounts: + - name: markdown + mountPath: /git + env: + - name: GIT_SYNC_REPO + value: https://github.com/GoogleCloudPlatform/kubernetes.git + - name: GIT_SYNC_DEST + value: /git + - name: hugo + image: gcr.io/google_containers/hugo + imagePullPolicy: Always + volumeMounts: + - name: markdown + mountPath: /src + - name: html + mountPath: /dest + env: + - name: HUGO_SRC + value: /src/git-sync/demo/blog + - name: HUGO_BUILD_DRAFT + value: "true" + - name: HUGO_BASE_URL + value: example.com + - name: nginx + image: nginx + volumeMounts: + - name: html + mountPath: /usr/share/nginx/html + ports: + - containerPort: 80 + volumes: + - name: markdown + emptyDir: {} + - name: html + emptyDir: {} diff --git a/demo/config/service.yaml b/demo/config/service.yaml new file mode 100644 index 0000000..e9d30b4 --- /dev/null +++ b/demo/config/service.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + name: blog-service +spec: + type: "LoadBalancer" + ports: + - port: 80 + selector: + name: blog diff --git a/demo/hugo/Dockerfile b/demo/hugo/Dockerfile new file mode 100644 index 0000000..88d3b43 --- /dev/null +++ b/demo/hugo/Dockerfile @@ -0,0 +1,26 @@ +# Copyright 2014 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +FROM golang +RUN go get -v github.com/spf13/hugo +RUN git clone --recursive https://github.com/spf13/hugoThemes.git /themes +VOLUME ["/src", "/dest"] +EXPOSE 1313 +ENV HUGO_SRC /src +ENV HUGO_DEST /dest +ENV HUGO_THEME hyde +ENV HUGO_BUILD_DRAFT false +ENV HUGO_BASE_URL "" +ADD run-hugo /run-hugo +ENTRYPOINT ["/run-hugo"] +CMD ["server", "--source=${HUGO_SRC}", "--theme=${HUGO_THEME}", "--buildDrafts=${HUGO_BUILD_DRAFT}", "--baseUrl=${HUGO_BASE_URL}", "--watch", "--destination=${HUGO_DEST}", "--appendPort=false"] diff --git a/demo/hugo/README.md b/demo/hugo/README.md new file mode 100644 index 0000000..57511f7 --- /dev/null +++ b/demo/hugo/README.md @@ -0,0 +1,15 @@ +# hugo + +`hugo` is a container that convert markdown into html using [hugo static site generator](http://gohugo.io/). + +## Usage + +``` +# build the container +docker build -t hugo . +# run the hugo container +docker run -e HUGO_BASE_URL=example.com -v /path/to/md:/src -v /path/to/html:/dest hugo +``` + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/git-sync/demo/hugo/README.md?pixel)]() diff --git a/demo/hugo/run-hugo b/demo/hugo/run-hugo new file mode 100755 index 0000000..784efad --- /dev/null +++ b/demo/hugo/run-hugo @@ -0,0 +1,21 @@ +#!/bin/bash + +# Copyright 2014 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -ex +if [ ! -d ${HUGO_SRC}/themes ]; then + ln -s /themes ${HUGO_SRC}/themes +fi +hugo $(eval echo $*) # force default CMD env expansion diff --git a/docs/ssh.md b/docs/ssh.md new file mode 100644 index 0000000..2d663c3 --- /dev/null +++ b/docs/ssh.md @@ -0,0 +1,78 @@ +# Using SSH with git-sync + +Git-sync supports using the SSH protocol for pulling git content. + +## Step 1: Create Secret +Create a Secret to store your SSH private key, with the Secret keyed as "ssh". This can be done one of two ways: + +***Method 1:*** + +Use the ``kubectl create secret`` command and point to the file on your filesystem that stores the key. Ensure that the file is mapped to "ssh" as shown (the file can be located anywhere). +``` +kubectl create secret generic git-creds --from-file=ssh=~/.ssh/id_rsa +``` + +***Method 2:*** + +Write a config file for a Secret that holds your SSH private key, with the key (pasted as plaintext) mapped to the "ssh" field. +``` +{ + "kind": "Secret", + "apiVersion": "v1", + "metadata": { + "name": "git-creds" + }, + "data": { + "ssh": +} +``` + +Create the Secret using ``kubectl create -f``. +``` +kubectl create -f /path/to/secret-config.json +``` + +## Step 2: Configure Pod/Deployment Volume + +In your Pod or Deployment configuration, specify a Volume for mounting the Secret. Ensure that secretName matches the name you used when creating the Secret (e.g. "git-creds" used in both above examples). +``` +volumes: [ + { + "name": "git-secret", + "secret": { + "secretName": "git-creds" + } + }, + ... +], +``` + +## Step 3: Configure git-sync container + +In your git-sync container configuration, mount the Secret Volume at "/etc/git-secret". Ensure that the environment variable GIT_SYNC_REPO is set to use a URL with the SSH protocol, and set GIT_SYNC_SSH to true. +``` +{ + name: "git-sync", + ... + env: [ + { + name: "GIT_SYNC_REPO", + value: "git@github.com:kubernetes/kubernetes.git", + }, { + name: "GIT_SYNC_SSH", + value: "true", + }, + ... + ] + volumeMounts: [ + { + "name": "git-secret", + "mountPath": "/etc/git-secret" + }, + ... + ], +} +``` +**Note: Do not mount the Secret Volume with "readOnly: true".** Kubernetes mounts the Secret with permissions 0444 by default (not restrictive enough to be used as an SSH key), so the container runs a chmod command on the Secret. Mounting the Secret Volume as a read-only filesystem prevents chmod and thus prevents the use of the Secret as an SSH key. + +***TODO***: Remove the chmod command once Kubernetes allows for specifying permissions for a Secret Volume. See https://github.com/kubernetes/kubernetes/pull/28936. diff --git a/main.go b/main.go new file mode 100644 index 0000000..2fd8671 --- /dev/null +++ b/main.go @@ -0,0 +1,355 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// git-sync is a command that pull a git repository to a local directory. + +package main // import "k8s.io/git-sync" + +import ( + "bytes" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "math/rand" + "os" + "os/exec" + "path" + "path/filepath" + "strconv" + "strings" + "time" +) + +const volMount = "/git" + +var flRepo = flag.String("repo", envString("GIT_SYNC_REPO", ""), "git repo url") +var flBranch = flag.String("branch", envString("GIT_SYNC_BRANCH", "master"), "git branch") +var flRev = flag.String("rev", envString("GIT_SYNC_REV", "HEAD"), "git rev") +var flDest = flag.String("dest", envString("GIT_SYNC_DEST", ""), "destination subdirectory path within volume") +var flWait = flag.Int("wait", envInt("GIT_SYNC_WAIT", 0), "number of seconds to wait before next sync") +var flOneTime = flag.Bool("one-time", envBool("GIT_SYNC_ONE_TIME", false), "exit after the initial checkout") +var flDepth = flag.Int("depth", envInt("GIT_SYNC_DEPTH", 0), "shallow clone with a history truncated to the specified number of commits") + +var flMaxSyncFailures = flag.Int("max-sync-failures", envInt("GIT_SYNC_MAX_SYNC_FAILURES", 0), + `number of consecutive failures allowed before aborting (the first pull must succeed)`) + +var flUsername = flag.String("username", envString("GIT_SYNC_USERNAME", ""), "username") +var flPassword = flag.String("password", envString("GIT_SYNC_PASSWORD", ""), "password") + +var flSSH = flag.Bool("ssh", envBool("GIT_SYNC_SSH", false), "use SSH protocol") + +var flChmod = flag.Int("change-permissions", envInt("GIT_SYNC_PERMISSIONS", 0), `If set it will change the permissions of the directory + that contains the git repository. Example: 744`) + +func envString(key, def string) string { + if env := os.Getenv(key); env != "" { + return env + } + return def +} + +func envBool(key string, def bool) bool { + if env := os.Getenv(key); env != "" { + res, err := strconv.ParseBool(env) + if err != nil { + return def + } + + return res + } + return def +} + +func envInt(key string, def int) int { + if env := os.Getenv(key); env != "" { + val, err := strconv.Atoi(env) + if err != nil { + log.Printf("invalid value for %q: using default: %q", key, def) + return def + } + return val + } + return def +} + +const usage = "usage: GIT_SYNC_REPO= GIT_SYNC_DEST= [GIT_SYNC_BRANCH= GIT_SYNC_WAIT= GIT_SYNC_DEPTH= GIT_SYNC_USERNAME= GIT_SYNC_PASSWORD= GIT_SYNC_SSH= GIT_SYNC_ONE_TIME= GIT_SYNC_MAX_SYNC_FAILURES=] git-sync -repo GIT_REPO_URL -dest PATH [-branch -wait -username -password -ssh -depth -one-time -max-sync-failures]" + +func main() { + flag.Parse() + if *flRepo == "" || *flDest == "" { + flag.Usage() + log.Fatal(usage) + } + if _, err := exec.LookPath("git"); err != nil { + log.Fatalf("required git executable not found: %v", err) + } + + if *flUsername != "" && *flPassword != "" { + if err := setupGitAuth(*flUsername, *flPassword, *flRepo); err != nil { + log.Fatalf("error creating .netrc file: %v", err) + } + } + + if *flSSH { + if err := setupGitSSH(); err != nil { + log.Fatalf("error configuring SSH: %v", err) + } + } + + initialSync := true + failCount := 0 + for { + if err := syncRepo(*flRepo, *flDest, *flBranch, *flRev, *flDepth); err != nil { + if initialSync || failCount >= *flMaxSyncFailures { + log.Fatalf("error syncing repo: %v", err) + } + + failCount++ + log.Printf("unexpected error syncing repo: %v", err) + log.Printf("waiting %d seconds before retryng", *flWait) + time.Sleep(time.Duration(*flWait) * time.Second) + continue + } + + initialSync = false + failCount = 0 + + if *flOneTime { + os.Exit(0) + } + + log.Printf("waiting %d seconds", *flWait) + time.Sleep(time.Duration(*flWait) * time.Second) + log.Println("done") + } +} + +// updateSymlink atomically swaps the symlink to point at the specified directory and cleans up the previous worktree. +func updateSymlink(link, newDir string) error { + // Get currently-linked repo directory (to be removed), unless it doesn't exist + currentDir, err := filepath.EvalSymlinks(path.Join(volMount, link)) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("error accessing symlink: %v", err) + } + + // newDir is /git/rev-..., we need to change it to relative path. + // Volume in other container may not be mounted at /git, so the symlink can't point to /git. + newDirRelative, err := filepath.Rel(volMount, newDir) + if err != nil { + return fmt.Errorf("error converting to relative path: %v", err) + } + + if _, err := runCommand("ln", volMount, []string{"-snf", newDirRelative, "tmp-link"}); err != nil { + return fmt.Errorf("error creating symlink: %v", err) + } + + log.Printf("create symlink %v->%v", "tmp-link", newDirRelative) + + if _, err := runCommand("mv", volMount, []string{"-T", "tmp-link", link}); err != nil { + return fmt.Errorf("error replacing symlink: %v", err) + } + + log.Printf("rename symlink %v to %v", "tmp-link", link) + + // Clean up previous worktree + if len(currentDir) > 0 { + if err = os.RemoveAll(currentDir); err != nil { + return fmt.Errorf("error removing directory: %v", err) + } + + log.Printf("remove %v", currentDir) + + output, err := runCommand("git", volMount, []string{"worktree", "prune"}) + if err != nil { + return err + } + + log.Printf("worktree prune %v", string(output)) + } + + return nil +} + +// addWorktreeAndSwap creates a new worktree and calls updateSymlink to swap the symlink to point to the new worktree +func addWorktreeAndSwap(dest, branch, rev string) error { + // fetch branch + output, err := runCommand("git", volMount, []string{"fetch", "origin", branch}) + if err != nil { + return err + } + + log.Printf("fetch %q: %s", branch, string(output)) + + // add worktree in subdir + rand.Seed(time.Now().UnixNano()) + worktreePath := path.Join(volMount, "rev-"+strconv.Itoa(rand.Int())) + output, err = runCommand("git", volMount, []string{"worktree", "add", worktreePath, "origin/" + branch}) + if err != nil { + return err + } + + log.Printf("add worktree origin/%q: %v", branch, string(output)) + + // .git file in worktree directory holds a reference to /git/.git/worktrees/ + // Replace it with a reference using relative paths, so that other containers can use a different volume mount name + worktreePathRelative, err := filepath.Rel(volMount, worktreePath) + if err != nil { + return err + } + gitDirRef := []byte(path.Join("gitdir: ../.git/worktrees", worktreePathRelative) + "\n") + if err = ioutil.WriteFile(path.Join(worktreePath, ".git"), gitDirRef, 0644); err != nil { + return err + } + + // reset working copy + output, err = runCommand("git", worktreePath, []string{"reset", "--hard", rev}) + if err != nil { + return err + } + + log.Printf("reset %q: %v", rev, string(output)) + + if *flChmod != 0 { + // set file permissions + _, err = runCommand("chmod", "", []string{"-R", string(*flChmod), worktreePath}) + if err != nil { + return err + } + } + + return updateSymlink(dest, worktreePath) +} + +func initRepo(repo, dest, branch, rev string, depth int) error { + // clone repo + args := []string{"clone", "--no-checkout", "-b", branch} + if depth != 0 { + args = append(args, "-depth") + args = append(args, string(depth)) + } + args = append(args, repo) + args = append(args, volMount) + output, err := runCommand("git", "", args) + if err != nil { + return err + } + + log.Printf("clone %q: %s", repo, string(output)) + + return nil +} + +// syncRepo syncs the branch of a given repository to the destination at the given rev. +func syncRepo(repo, dest, branch, rev string, depth int) error { + target := path.Join(volMount, dest) + gitRepoPath := path.Join(target, ".git") + _, err := os.Stat(gitRepoPath) + switch { + case os.IsNotExist(err): + err = initRepo(repo, target, branch, rev, depth) + if err != nil { + return err + } + case err != nil: + return fmt.Errorf("error checking if repo exist %q: %v", gitRepoPath, err) + default: + needUpdate, err := gitRemoteChanged(target, branch) + if err != nil { + return err + } + if !needUpdate { + log.Printf("No change") + return nil + } + } + + return addWorktreeAndSwap(dest, branch, rev) +} + +// gitRemoteChanged returns true if the remote HEAD is different from the local HEAD, false otherwise +func gitRemoteChanged(localDir, branch string) (bool, error) { + _, err := runCommand("git", localDir, []string{"remote", "update"}) + if err != nil { + return false, err + } + localHead, err := runCommand("git", localDir, []string{"rev-parse", "HEAD"}) + if err != nil { + return false, err + } + remoteHead, err := runCommand("git", localDir, []string{"rev-parse", fmt.Sprintf("origin/%v", branch)}) + if err != nil { + return false, err + } + return (strings.Compare(string(localHead), string(remoteHead)) != 0), nil +} + +func runCommand(command, cwd string, args []string) ([]byte, error) { + cmd := exec.Command(command, args...) + if cwd != "" { + cmd.Dir = cwd + } + output, err := cmd.CombinedOutput() + if err != nil { + return []byte{}, fmt.Errorf("error running command %q : %v: %s", strings.Join(cmd.Args, " "), err, string(output)) + } + + return output, nil +} + +func setupGitAuth(username, password, gitURL string) error { + log.Println("setting up the git credential cache") + cmd := exec.Command("git", "config", "--global", "credential.helper", "cache") + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("error setting up git credentials %v: %s", err, string(output)) + } + + cmd = exec.Command("git", "credential", "approve") + stdin, err := cmd.StdinPipe() + if err != nil { + return err + } + creds := fmt.Sprintf("url=%v\nusername=%v\npassword=%v\n", gitURL, username, password) + io.Copy(stdin, bytes.NewBufferString(creds)) + stdin.Close() + output, err = cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("error setting up git credentials %v: %s", err, string(output)) + } + + return nil +} + +func setupGitSSH() error { + log.Println("setting up git SSH credentials") + + if _, err := os.Stat("/etc/git-secret/ssh"); err != nil { + return fmt.Errorf("error: could not find SSH key Secret: %v", err) + } + + // Kubernetes mounts Secret as 0444 by default, which is not restrictive enough to use as an SSH key. + // TODO: Remove this command once Kubernetes allows for specifying permissions for a Secret Volume. + // See https://github.com/kubernetes/kubernetes/pull/28936. + if err := os.Chmod("/etc/git-secret/ssh", 0400); err != nil { + + // If the Secret Volume is mounted as readOnly, the read-only filesystem nature prevents the necessary chmod. + return fmt.Errorf("error running chmod on Secret (make sure Secret Volume is NOT mounted with readOnly=true): %v", err) + } + + return nil +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..7e93d4c --- /dev/null +++ b/main_test.go @@ -0,0 +1,100 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "os" + "testing" +) + +const ( + testKey = "KEY" +) + +func TestEnvBool(t *testing.T) { + cases := []struct { + value string + def bool + exp bool + }{ + {"true", true, true}, + {"true", false, true}, + {"", true, true}, + {"", false, false}, + {"false", true, false}, + {"false", false, false}, + {"", true, true}, + {"", false, false}, + {"no true", true, true}, + {"no false", true, true}, + } + + for _, testCase := range cases { + os.Setenv(testKey, testCase.value) + val := envBool(testKey, testCase.def) + if val != testCase.exp { + t.Fatalf("expected %v but %v returned", testCase.exp, val) + } + } +} + +func TestEnvString(t *testing.T) { + cases := []struct { + value string + def string + exp string + }{ + {"true", "true", "true"}, + {"true", "false", "true"}, + {"", "true", "true"}, + {"", "false", "false"}, + {"false", "true", "false"}, + {"false", "false", "false"}, + {"", "true", "true"}, + {"", "false", "false"}, + } + + for _, testCase := range cases { + os.Setenv(testKey, testCase.value) + val := envString(testKey, testCase.def) + if val != testCase.exp { + t.Fatalf("expected %v but %v returned", testCase.exp, val) + } + } +} + +func TestEnvInt(t *testing.T) { + cases := []struct { + value string + def int + exp int + }{ + {"0", 1, 0}, + {"", 0, 0}, + {"-1", 0, -1}, + {"abcd", 0, 0}, + {"abcd", 1, 1}, + } + + for _, testCase := range cases { + os.Setenv(testKey, testCase.value) + val := envInt(testKey, testCase.def) + if val != testCase.exp { + t.Fatalf("expected %v but %v returned", testCase.exp, val) + } + } +} diff --git a/ssh-wrapper.sh b/ssh-wrapper.sh new file mode 100644 index 0000000..d5408b4 --- /dev/null +++ b/ssh-wrapper.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +# Copyright 2016 The Kubernetes Authors All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This script wraps the standard SSH binary so that the mounted SSH key can be used without user confirmation. +# In the Dockerfile, the original SSH binary is moved to /usr/bin/ssh-binary (and is then used as the base command here). +# This script is moved to /usr/bin/ssh so that Git uses it by default. + +# The "UserKnownHostsFile" and "StrictHostKeyChecking" options avoid the user confirmation check. +# The -i flag specifies where the SSH key is located. + +secret_path=/etc/git-secret/ssh +ssh-binary -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i $secret_path "$@"