Merge pull request #1 from thockin/master
Move git-sync to a new repo of its very own
This commit is contained in:
commit
2ed0e3fcba
|
|
@ -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"]
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"ImportPath": "k8s.io/git-sync",
|
||||
"GoVersion": "go1.5",
|
||||
"Packages": [
|
||||
"./..."
|
||||
],
|
||||
"Deps": []
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
/pkg
|
||||
/bin
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
```
|
||||
|
||||
[]()
|
||||
|
|
@ -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 <some-registry>/git-sync ..
|
||||
docker build -t <some-registry>/hugo hugo/
|
||||
docker push <some-registry>/hugo <some-registry>/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
|
||||
|
||||
|
||||
[]()
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
baseurl = "http://example.com"
|
||||
languageCode = "en-us"
|
||||
title = "example blog"
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
+++
|
||||
date = "2014-12-19T15:29:48-08:00"
|
||||
draft = true
|
||||
title = "about"
|
||||
+++
|
||||
|
||||
## A headline
|
||||
|
||||
Some content about the blog.
|
||||
|
||||
|
||||
[]()
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
+++
|
||||
date = "2014-12-19T15:30:18-08:00"
|
||||
draft = true
|
||||
title = "first"
|
||||
+++
|
||||
|
||||
## first port
|
||||
|
||||
This is the first post.
|
||||
|
||||
|
||||
[]()
|
||||
|
|
@ -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: {}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: blog-service
|
||||
spec:
|
||||
type: "LoadBalancer"
|
||||
ports:
|
||||
- port: 80
|
||||
selector:
|
||||
name: blog
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
```
|
||||
|
||||
|
||||
[]()
|
||||
|
|
@ -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
|
||||
|
|
@ -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": <private-key>
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
|
@ -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/<worktree-dir-name>
|
||||
// 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 "$@"
|
||||
Loading…
Reference in New Issue