Skip to content

Commit

Permalink
Merge pull request #290 from utilitywarehouse/default-ssh-key
Browse files Browse the repository at this point in the history
Use the git ssh key passed to KA as the default way of pulling via ssh
  • Loading branch information
ffilippopoulos authored Mar 22, 2024
2 parents eefd564 + 79cd203 commit 1d14042
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 66 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,14 @@ to prevent kube-applier from pruning it. However, since it is a secret itself
and would need be encrypted as well in git, it must be created manually the
first time (or after any changes to its contents).

#### Private Kustomize Bases

If not overridden via Waybill.Spec.gitSSHSecretRef configuration (see below),
Kube-Applier will use the SSH key passed via `-git-ssh-key-path` as the default
key flag to try fetching remote `kustomize` bases from private repositories.
Since this relies on using SSH to fetch from upstreams, the bases should be
defined with the `ssh://` scheme in `kustomization.yaml`.

#### Custom SSH Keys

You can specify custom SSH keys to be used for fetching remote `kustomize` bases
Expand Down
6 changes: 4 additions & 2 deletions apis/kubeapplier/v1alpha1/waybill_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ type WaybillSpec struct {
// +kubebuilder:default=false
DryRun bool `json:"dryRun,omitempty"`

// GitSSHSecretRef references a Secret that contains an item named `key` and
// GitSSHSecretRef will override the default Git SSH key passed as a
// flag. It references a Secret that contains an item named `key` and
// optionally an item named `known_hosts`. If present, these are passed to
// the apply runtime and are used by `kustomize` when cloning remote bases.
// This allows the use of bases from private repositories.
// This allows the use of bases from private repositories that the default
// key will not have access to.
// +optional
GitSSHSecretRef *ObjectReference `json:"gitSSHSecretRef,omitempty"`

Expand Down
21 changes: 11 additions & 10 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ var (
fDryRun = flag.Bool("dry-run", getBoolEnv("DRY_RUN", false), "Whether kube-applier operates in dry-run mode globally")
fGitPollWait = flag.Duration("git-poll-wait", getDurationEnv("GIT_POLL_WAIT", time.Second*5), "How long kube-applier waits before checking for changes in the repository")
fGitKnownHostsPath = flag.String("git-ssh-known-hosts-path", getStringEnv("GIT_KNOWN_HOSTS_PATH", ""), "Path to the known hosts file used for fetching the repository")
fGitSSHKeyPath = flag.String("git-ssh-key-path", getStringEnv("GIT_SSH_KEY_PATH", ""), "Path to the SSH key file used for fetching the repository")
fGitSSHKeyPath = flag.String("git-ssh-key-path", getStringEnv("GIT_SSH_KEY_PATH", ""), "Path to the SSH key file used for fetching the repository. This will also be used for any Kustomize bases fetched via ssh, unless overridden by Waybill.Spec.GitSSHSecretRef config")
fListenPort = flag.Int("listen-port", getIntEnv("LISTEN_PORT", 8080), "Port that the http server is listening on")
fLogLevel = flag.String("log-level", getStringEnv("LOG_LEVEL", "warn"), "Logging level: trace, debug, info, warn, error, off")
fOidcCallbackURL = flag.String("oidc-callback-url", getStringEnv("OIDC_CALLBACK_URL", ""), "OIDC callback url should be the root URL where kube-applier is exposed")
Expand Down Expand Up @@ -161,15 +161,16 @@ func main() {
}

runner := &run.Runner{
Clock: clock,
DryRun: *fDryRun,
KubeClient: kubeClient,
KubeCtlClient: kubeCtlClient,
PruneBlacklist: pruneBlacklistSlice,
Repository: repo,
RepoPath: *fRepoPath,
Strongbox: &run.Strongboxer{},
WorkerCount: *fWorkerCount,
Clock: clock,
DefaultGitSSHKeyPath: *fGitSSHKeyPath,
DryRun: *fDryRun,
KubeClient: kubeClient,
KubeCtlClient: kubeCtlClient,
PruneBlacklist: pruneBlacklistSlice,
Repository: repo,
RepoPath: *fRepoPath,
Strongbox: &run.Strongboxer{},
WorkerCount: *fWorkerCount,
}

runQueue := runner.Start()
Expand Down
24 changes: 7 additions & 17 deletions manifests/base/cluster/kube-applier.io_waybills.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.8.0
controller-gen.kubebuilder.io/version: v0.11.3
creationTimestamp: null
name: waybills.kube-applier.io
spec:
Expand Down Expand Up @@ -80,11 +80,6 @@ spec:
description: AutoApply determines whether this Waybill will be automatically
applied by scheduled or polling runs.
type: boolean
autoTest:
default: true
description: AutoTest determines whether this Waybill will be automatically
applied by scheduled or polling runs.
type: boolean
delegateServiceAccountSecretRef:
default: kube-applier-delegate-token
description: DelegateServiceAccountSecretRef references a Secret of
Expand All @@ -98,11 +93,12 @@ spec:
description: DryRun enables the dry-run flag when applying this Waybill.
type: boolean
gitSSHSecretRef:
description: GitSSHSecretRef references a Secret that contains an
item named `key` and optionally an item named `known_hosts`. If
present, these are passed to the apply runtime and are used by `kustomize`
when cloning remote bases. This allows the use of bases from private
repositories.
description: GitSSHSecretRef will override the default Git SSH key
passed as a flag. It references a Secret that contains an item named
`key` and optionally an item named `known_hosts`. If present, these
are passed to the apply runtime and are used by `kustomize` when
cloning remote bases. This allows the use of bases from private
repositories that the default key will not have access to.
properties:
name:
description: Name of the resource being referred to.
Expand Down Expand Up @@ -225,9 +221,3 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
35 changes: 20 additions & 15 deletions run/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,18 @@ func uniqueStrings(in []string) []string {
// Runner manages the full process of an apply run, including getting the
// appropriate files, running apply commands on them, and handling the results.
type Runner struct {
Clock sysutil.ClockInterface
DryRun bool
KubeClient *client.Client
KubeCtlClient *kubectl.Client
PruneBlacklist []string
RepoPath string
Repository *git.Repository
Strongbox StrongboxInterface
WorkerCount int
workerGroup *sync.WaitGroup
workerQueue chan Request
Clock sysutil.ClockInterface
DefaultGitSSHKeyPath string
DryRun bool
KubeClient *client.Client
KubeCtlClient *kubectl.Client
PruneBlacklist []string
RepoPath string
Repository *git.Repository
Strongbox StrongboxInterface
WorkerCount int
workerGroup *sync.WaitGroup
workerQueue chan Request
}

// Start runs a continuous loop that starts a new run when a request comes into the queue channel.
Expand Down Expand Up @@ -410,10 +411,14 @@ func (r *Runner) setupGitSSH(ctx context.Context, waybill *kubeapplierv1alpha1.W
sshDir := filepath.Join(tmpHomeDir, ".ssh")
os.Mkdir(sshDir, 0700)
if waybill.Spec.GitSSHSecretRef == nil {
// Even when there is no git SSH secret defined, we still override the
// git ssh command (pointing the key to /dev/null) in order to avoid
// using ssh keys in default system locations and to surface the error
// if bases over ssh have been configured.
// If there is no SSH secret defined, fall back to using the one
// provided to kube-applier as a flag to clone the root repo.
if r.DefaultGitSSHKeyPath != "" {
log.Logger("runner").Debug("No GitSSHSecretRef set, falling back to root repo ssh config path", "path", r.DefaultGitSSHKeyPath)
return fmt.Sprintf("GIT_SSH_COMMAND=ssh -q -F none -o IdentitiesOnly=yes -o User=git -o IdentityFile=%s -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no", r.DefaultGitSSHKeyPath), nil
}
// Else override the git ssh command (pointing the key to /dev/null) to surface the error if bases over ssh have been configured.
log.Logger("runner").Debug("No Git SSH key found, pointing identity file to /dev/null")
return `GIT_SSH_COMMAND=ssh -q -F none -o IdentitiesOnly=yes -o IdentityFile=/dev/null -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no`, nil
}
gsNamespace := waybill.Spec.GitSSHSecretRef.Namespace
Expand Down
110 changes: 88 additions & 22 deletions run/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"context"
"encoding/base64"
"fmt"
"io/ioutil"
"path/filepath"
"regexp"
"strings"
"syscall"
"testing"
"time"

Expand All @@ -26,6 +28,30 @@ import (
"github.com/utilitywarehouse/kube-applier/metrics"
)

// The ssh keys below are base64-encoded in order to work around
// GitHub's notifications about committing a private key in a public
// repo, which triggers for the deploy key. This key is a read-only
// deploy key for a public repository and is safe to commit.
var (
randomKey, _ = base64.StdEncoding.DecodeString(
`LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFB
QUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhP
UUFBQUNDbjArQUw1bzNDU1g3U2UwOTY5SUgvYWc4b2hlUkJkUXlwd1dXN1M0N1NMUUFBQUpBYVNL
MmxHa2l0CnBRQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQ24wK0FMNW8zQ1NYN1NlMDk2OUlIL2Fn
OG9oZVJCZFF5cHdXVzdTNDdTTFEKQUFBRUJTMUpJNnhwa0lYN1JxK3Nnc1YyM2FrY1FBeGFDaUI4
SjM3b0ZKVkViUHhLZlQ0QXZtamNKSmZ0SjdUM3IwZ2Y5cQpEeWlGNUVGMURLbkJaYnRManRJdEFB
QUFER0ZzYTJGeVFHdDFhbWx5WVFFPQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K`)

deployKey, _ = base64.StdEncoding.DecodeString(
`LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFB
QUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhP
UUFBQUNEMnlBVGFaZHZGOXFvQU9QWnkrejBSaHI3dm1IdVZ3WldvUkFwYjhuZ3hLQUFBQUpCMm1j
VlZkcG5GClZRQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDRDJ5QVRhWmR2Rjlxb0FPUFp5K3owUmhy
N3ZtSHVWd1pXb1JBcGI4bmd4S0EKQUFBRUI1VDBoKzNGV0J0M0xaZXpyL00rZzd5Q2NtaHFjYWRQ
V0dTRjltUDh1L21mYklCTnBsMjhYMnFnQTQ5bkw3UFJHRwp2dStZZTVYQmxhaEVDbHZ5ZURFb0FB
QUFER0ZzYTJGeVFHdDFhbWx5WVFFPQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K`)
)

func TestApplyOptions_pruneWhitelist(t *testing.T) {
assert := assert.New(t)

Expand Down Expand Up @@ -425,28 +451,6 @@ Some error output has been omitted because it may contain sensitive data

testEnsureWaybills(wbList)

// The ssh keys below are base64-encoded in order to work around
// GitHub's notifications about committing a private key in a public
// repo, which triggers for the deploy key. This key is a read-only
// deploy key for a public repository and is safe to commit.
randomKey, _ := base64.StdEncoding.DecodeString(
`LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFB
QUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhP
UUFBQUNDbjArQUw1bzNDU1g3U2UwOTY5SUgvYWc4b2hlUkJkUXlwd1dXN1M0N1NMUUFBQUpBYVNL
MmxHa2l0CnBRQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQ24wK0FMNW8zQ1NYN1NlMDk2OUlIL2Fn
OG9oZVJCZFF5cHdXVzdTNDdTTFEKQUFBRUJTMUpJNnhwa0lYN1JxK3Nnc1YyM2FrY1FBeGFDaUI4
SjM3b0ZKVkViUHhLZlQ0QXZtamNKSmZ0SjdUM3IwZ2Y5cQpEeWlGNUVGMURLbkJaYnRManRJdEFB
QUFER0ZzYTJGeVFHdDFhbWx5WVFFPQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K`)

deployKey, _ := base64.StdEncoding.DecodeString(
`LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFB
QUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhP
UUFBQUNEMnlBVGFaZHZGOXFvQU9QWnkrejBSaHI3dm1IdVZ3WldvUkFwYjhuZ3hLQUFBQUpCMm1j
VlZkcG5GClZRQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDRDJ5QVRhWmR2Rjlxb0FPUFp5K3owUmhy
N3ZtSHVWd1pXb1JBcGI4bmd4S0EKQUFBRUI1VDBoKzNGV0J0M0xaZXpyL00rZzd5Q2NtaHFjYWRQ
V0dTRjltUDh1L21mYklCTnBsMjhYMnFnQTQ5bkw3UFJHRwp2dStZZTVYQmxhaEVDbHZ5ZURFb0FB
QUFER0ZzYTJGeVFHdDFhbWx5WVFFPQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K`)

Expect(k8sClient.GetClient().Create(context.TODO(), &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "git-ssh",
Expand Down Expand Up @@ -605,6 +609,68 @@ deployment.apps/test-deployment created
})
})

Context("When operating on a Waybill that uses kustomize with no git secret it should fall back to KA ssh key", func() {
It("Should be able to build and apply", func() {
sshKey, err := ioutil.TempFile("", "testGitSSHKey")
if err != nil {
panic(err)
}
defer syscall.Unlink(sshKey.Name())
ioutil.WriteFile(sshKey.Name(), []byte(deployKey), 0700)
runner.DefaultGitSSHKeyPath = sshKey.Name()

waybill := kubeapplierv1alpha1.Waybill{
TypeMeta: metav1.TypeMeta{APIVersion: "kube-applier.io/v1alpha1", Kind: "Waybill"},
ObjectMeta: metav1.ObjectMeta{
Name: "app-d",
Namespace: "app-d-kustomize",
},
Spec: kubeapplierv1alpha1.WaybillSpec{
AutoApply: pointer.BoolPtr(true),
Prune: pointer.BoolPtr(true),
},
}

testEnsureWaybills([]*kubeapplierv1alpha1.Waybill{&waybill})

repositoryPath := waybill.Spec.RepositoryPath
if repositoryPath == "" {
repositoryPath = waybill.Namespace
}
headCommitHash, err := runner.Repository.HashForPath(context.TODO(), filepath.Join(runner.RepoPath, repositoryPath))
Expect(err).To(BeNil())
expected := waybill
expected.Status = kubeapplierv1alpha1.WaybillStatus{
LastRun: &kubeapplierv1alpha1.WaybillStatusRun{
Command: "",
Commit: headCommitHash,
ErrorMessage: "",
Finished: metav1.Time{},
Output: `namespace/app-d-kustomize configured
deployment.apps/test-deployment created
`,
Started: metav1.Time{},
Success: true,
Type: PollingRun.String(),
},
}

Enqueue(runQueue, PollingRun, &waybill)
runner.Stop()

waybill.Status.LastRun.Output = testStripKubectlWarnings(waybill.Status.LastRun.Output)
Expect(waybill).Should(matchWaybill(expected, kubeCtlPath, kustomizePath, runner.RepoPath, applyOptions.pruneWhitelist(&waybill, runner.PruneBlacklist)))

testMetrics([]string{
`kube_applier_kubectl_exit_code_count{exit_code="0",namespace="app-d-kustomize"} 1`,
`kube_applier_last_run_timestamp_seconds{namespace="app-d-kustomize"}`,
`kube_applier_namespace_apply_count{namespace="app-d-kustomize",success="true"} 1`,
`kube_applier_run_latency_seconds`,
`kube_applier_run_queue{namespace="app-d-kustomize",type="Git polling run"} 0`,
})
})
})

Context("When operating on a Waybill that defines a strongbox keyring", func() {
It("Should be able to apply encrypted files, given a strongbox keyring secret", func() {
wbList := []*kubeapplierv1alpha1.Waybill{
Expand Down
4 changes: 4 additions & 0 deletions testdata/manifests/app-d-kustomize/00-namespace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
kind: Namespace
apiVersion: v1
metadata:
name: app-d-kustomize
4 changes: 4 additions & 0 deletions testdata/manifests/app-d-kustomize/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
bases:
- ssh://github.com/utilitywarehouse/kube-applier//testdata/bases/simple-deployment?ref=default-ssh-key
resources:
- 00-namespace.yaml

0 comments on commit 1d14042

Please sign in to comment.