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