1
0
mirror of synced 2026-03-01 11:16:56 +00:00

Compare commits

..

32 Commits

Author SHA1 Message Date
Stefan Prodan
62e4b03342 Merge pull request #40 from fluxcd/gh-deploy-key
Rotate GitHub deploy key during bootstrap
2020-06-15 10:25:37 +03:00
stefanprodan
dca7b0ba91 Recreate GitHub deploy key during bootstrap 2020-06-12 18:05:00 +03:00
Stefan Prodan
2a61befbfd Merge pull request #39 from fluxcd/bootstrap-path
Add target path to the bootstrap cmd
2020-06-12 16:11:09 +03:00
Stefan Prodan
287be26190 Merge pull request #38 from fluxcd/uninstall-ks
Add kustomizations removal to uninstall cmd
2020-06-12 15:59:42 +03:00
stefanprodan
4992e11383 Add target path to the bootstrap cmd 2020-06-12 15:57:34 +03:00
stefanprodan
727734850e Add kustomizations removal to uninstall cmd 2020-06-10 18:49:38 +03:00
Hidde Beydals
e042d25062 Merge pull request #37 from fluxcd/go-license
Add Go license to forked package
2020-06-10 12:19:14 +02:00
Hidde Beydals
ba7a11d0e5 Add Go license to forked package 2020-06-10 12:10:04 +02:00
Stefan Prodan
ecbcc8b6f1 Merge pull request #35 from fluxcd/release-v0.0.1-beta.2
Update controllers to v0.0.1-beta.2
2020-06-10 12:20:34 +03:00
stefanprodan
94f320336c Update controllers to v0.0.1-beta.2 2020-06-10 12:12:35 +03:00
Hidde Beydals
c27f53b5b1 Merge pull request #34 from fluxcd/inmemory-knownhosts
ssh: add in-memory knownhosts utilities
2020-06-09 18:50:33 +02:00
Hidde Beydals
134d819bdf ssh: add in-memory knownhosts utilities 2020-06-09 18:23:45 +02:00
Stefan Prodan
dc7c2ce56b Merge pull request #31 from fluxcd/bootstrap-github
Implement GitHub repository bootstrap
2020-06-09 17:00:27 +03:00
stefanprodan
0cd820e923 Add export for public git repos 2020-06-09 16:22:35 +03:00
stefanprodan
c047a4476e Add bootstrap docs 2020-06-09 16:14:04 +03:00
stefanprodan
51ca2ee9d8 Improve cluster sync wait 2020-06-09 16:12:40 +03:00
stefanprodan
e612a8a496 Implement SSH deploy key bootstrap 2020-06-09 15:24:36 +03:00
stefanprodan
e70d86e843 Fix SSH secret upsert 2020-06-09 15:23:19 +03:00
stefanprodan
1bc637a43f Add export flag to create commands 2020-06-09 13:52:21 +03:00
stefanprodan
f5b738198f Add support for GitHub Enterprise 2020-06-09 13:52:21 +03:00
stefanprodan
34ea89c3be Create GitHub repo if it doesn't exists 2020-06-09 13:52:21 +03:00
stefanprodan
d4cbc45e16 Add personal and public options to bootstrap 2020-06-09 13:52:21 +03:00
stefanprodan
59ae2f327c Implement bootstrap upgrade 2020-06-09 13:52:21 +03:00
stefanprodan
4ec5b7fc69 Implement GitHub repository bootstrap 2020-06-09 13:52:21 +03:00
Hidde Beydals
9e31bbe716 Merge pull request #32 from fluxcd/go-native-ssh 2020-06-09 12:49:51 +02:00
Hidde Beydals
65cfa6d347 Move port check to scanHostKey func 2020-06-09 12:42:37 +02:00
Hidde Beydals
9099ad3d8b Split cmd SSH/auth functions 2020-06-09 12:30:47 +02:00
Hidde Beydals
43876b5ab9 Make SSH host key utilities publicly accessible 2020-06-09 11:31:01 +02:00
Hidde Beydals
6017946144 Improve host key scanner, add Ed25519 generator 2020-06-09 11:30:57 +02:00
Hidde Beydals
3a8151bcc0 Add various custom flags 2020-06-08 20:06:54 +02:00
Hidde Beydals
2dfe88b82d Move ssh package from internal to pkg 2020-06-08 13:44:51 +02:00
Hidde Beydals
a332e12338 Replace SSH shell-outs with Go implementation 2020-06-06 21:42:54 +02:00
49 changed files with 2074 additions and 151 deletions

20
cmd/tk/bootstrap.go Normal file
View File

@@ -0,0 +1,20 @@
package main
import (
"github.com/spf13/cobra"
)
var bootstrapCmd = &cobra.Command{
Use: "bootstrap",
Short: "Bootstrap commands",
}
var (
bootstrapVersion string
)
func init() {
bootstrapCmd.PersistentFlags().StringVar(&bootstrapVersion, "version", "master", "toolkit tag or branch")
rootCmd.AddCommand(bootstrapCmd)
}

582
cmd/tk/bootstrap_github.go Normal file
View File

@@ -0,0 +1,582 @@
package main
import (
"context"
"fmt"
"io/ioutil"
"net/url"
"os"
"path"
"path/filepath"
"sigs.k8s.io/yaml"
"strings"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/google/go-github/v32/github"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1"
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
)
var bootstrapGitHubCmd = &cobra.Command{
Use: "github",
Short: "Bootstrap GitHub repository",
Long: `
The bootstrap command creates the GitHub repository if it doesn't exists and
commits the toolkit components manifests to the master branch.
Then it configure the target cluster to synchronize with the repository.
If the toolkit components are present on the cluster,
the bootstrap command will perform an upgrade if needed.`,
Example: ` # Create a GitHub personal access token and export it as an env var
export GITHUB_TOKEN=<my-token>
# Run bootstrap for a private repo owned by a GitHub organization
bootstrap github --owner=<organization> --repository=<repo name>
# Run bootstrap for a repository path
bootstrap github --owner=<organization> --repository=<repo name> --path=dev-cluster
# Run bootstrap for a public repository on a personal account
bootstrap github --owner=<user> --repository=<repo name> --private=false --personal=true
# Run bootstrap for a private repo hosted on GitHub Enterprise
bootstrap github --owner=<organization> --repository=<repo name> --hostname=<domain>
`,
RunE: bootstrapGitHubCmdRun,
}
var (
ghOwner string
ghRepository string
ghInterval time.Duration
ghPersonal bool
ghPrivate bool
ghHostname string
ghPath string
)
const (
ghTokenName = "GITHUB_TOKEN"
ghBranch = "master"
ghInstallManifest = "toolkit-components.yaml"
ghSourceManifest = "toolkit-source.yaml"
ghKustomizationManifest = "toolkit-kustomization.yaml"
ghDefaultHostname = "github.com"
)
func init() {
bootstrapGitHubCmd.Flags().StringVar(&ghOwner, "owner", "", "GitHub user or organization name")
bootstrapGitHubCmd.Flags().StringVar(&ghRepository, "repository", "", "GitHub repository name")
bootstrapGitHubCmd.Flags().BoolVar(&ghPersonal, "personal", false, "is personal repository")
bootstrapGitHubCmd.Flags().BoolVar(&ghPrivate, "private", true, "is private repository")
bootstrapGitHubCmd.Flags().DurationVar(&ghInterval, "interval", time.Minute, "sync interval")
bootstrapGitHubCmd.Flags().StringVar(&ghHostname, "hostname", ghDefaultHostname, "GitHub hostname")
bootstrapGitHubCmd.Flags().StringVar(&ghPath, "path", "", "repository path, when specified the cluster sync will be scoped to this path")
bootstrapCmd.AddCommand(bootstrapGitHubCmd)
}
func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
ghToken := os.Getenv(ghTokenName)
if ghToken == "" {
return fmt.Errorf("%s environment variable not found", ghTokenName)
}
ghURL := fmt.Sprintf("https://%s/%s/%s", ghHostname, ghOwner, ghRepository)
sshURL := fmt.Sprintf("ssh://git@%s/%s/%s", ghHostname, ghOwner, ghRepository)
if ghOwner == "" || ghRepository == "" {
return fmt.Errorf("owner and repository are required")
}
kubeClient, err := utils.kubeClient(kubeconfig)
if err != nil {
return err
}
tmpDir, err := ioutil.TempDir("", namespace)
if err != nil {
return err
}
defer os.RemoveAll(tmpDir)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// create GitHub repository if doesn't exists
logAction("connecting to %s", ghHostname)
if err := createGitHubRepository(ctx, ghHostname, ghOwner, ghRepository, ghToken, ghPrivate, ghPersonal); err != nil {
return err
}
// clone repository and checkout the master branch
repo, err := checkoutGitHubRepository(ctx, ghURL, ghBranch, ghToken, tmpDir)
if err != nil {
return err
}
logSuccess("repository cloned")
// generate install manifests
logGenerate("generating manifests")
manifest, err := generateGitHubInstall(ghPath, namespace, tmpDir)
if err != nil {
return err
}
// stage install manifests
changed, err := commitGitHubManifests(repo, ghPath, namespace)
if err != nil {
return err
}
// push install manifests
if changed {
if err := pushGitHubRepository(ctx, repo, ghToken); err != nil {
return err
}
logSuccess("components manifests pushed")
} else {
logSuccess("components are up to date")
}
// determine if repo synchronization is working
isInstall := shouldInstallGitHub(ctx, kubeClient, namespace)
if isInstall {
// apply install manifests
logAction("installing components in %s namespace", namespace)
command := fmt.Sprintf("kubectl apply -f %s", manifest)
if _, err := utils.execCommand(ctx, ModeOS, command); err != nil {
return fmt.Errorf("install failed")
}
logSuccess("install completed")
// check installation
logWaiting("verifying installation")
for _, deployment := range components {
command = fmt.Sprintf("kubectl -n %s rollout status deployment %s --timeout=%s",
namespace, deployment, timeout.String())
if _, err := utils.execCommand(ctx, ModeOS, command); err != nil {
return fmt.Errorf("install failed")
} else {
logSuccess("%s ready", deployment)
}
}
}
// setup SSH deploy key
if shouldCreateGitHubDeployKey(ctx, kubeClient, namespace) {
logAction("configuring deploy key")
u, err := url.Parse(sshURL)
if err != nil {
return fmt.Errorf("git URL parse failed: %w", err)
}
key, err := generateGitHubDeployKey(ctx, kubeClient, u, namespace)
if err != nil {
return fmt.Errorf("generating deploy key failed: %w", err)
}
if err := createGitHubDeployKey(ctx, key, ghHostname, ghOwner, ghRepository, ghPath, ghToken); err != nil {
return err
}
logSuccess("deploy key configured")
}
// configure repo synchronization
if isInstall {
// generate source and kustomization manifests
logAction("generating sync manifests")
if err := generateGitHubKustomization(sshURL, namespace, namespace, ghPath, tmpDir, ghInterval); err != nil {
return err
}
// stage manifests
changed, err = commitGitHubManifests(repo, ghPath, namespace)
if err != nil {
return err
}
// push manifests
if changed {
if err := pushGitHubRepository(ctx, repo, ghToken); err != nil {
return err
}
}
logSuccess("sync manifests pushed")
// apply manifests and waiting for sync
logAction("applying sync manifests")
if err := applyGitHubKustomization(ctx, kubeClient, namespace, namespace, ghPath, tmpDir); err != nil {
return err
}
}
logSuccess("bootstrap finished")
return nil
}
func makeGitHubClient(hostname, token string) (*github.Client, error) {
auth := github.BasicAuthTransport{
Username: "git",
Password: token,
}
gh := github.NewClient(auth.Client())
if hostname != ghDefaultHostname {
baseURL := fmt.Sprintf("https://%s/api/v3/", hostname)
uploadURL := fmt.Sprintf("https://%s/api/uploads/", hostname)
if g, err := github.NewEnterpriseClient(baseURL, uploadURL, auth.Client()); err == nil {
gh = g
} else {
return nil, fmt.Errorf("github client error: %w", err)
}
}
return gh, nil
}
func createGitHubRepository(ctx context.Context, hostname, owner, name, token string, isPrivate, isPersonal bool) error {
gh, err := makeGitHubClient(hostname, token)
if err != nil {
return err
}
org := ""
if !isPersonal {
org = owner
}
if _, _, err := gh.Repositories.Get(ctx, org, name); err == nil {
return nil
}
autoInit := true
_, _, err = gh.Repositories.Create(ctx, org, &github.Repository{
AutoInit: &autoInit,
Name: &name,
Private: &isPrivate,
})
if err != nil {
if !strings.Contains(err.Error(), "name already exists on this account") {
return fmt.Errorf("github create repository error: %w", err)
}
} else {
logSuccess("repository created")
}
return nil
}
func checkoutGitHubRepository(ctx context.Context, url, branch, token, path string) (*git.Repository, error) {
auth := &http.BasicAuth{
Username: "git",
Password: token,
}
repo, err := git.PlainCloneContext(ctx, path, false, &git.CloneOptions{
URL: url,
Auth: auth,
RemoteName: git.DefaultRemoteName,
ReferenceName: plumbing.NewBranchReferenceName(branch),
SingleBranch: true,
NoCheckout: false,
Progress: nil,
Tags: git.NoTags,
})
if err != nil {
return nil, fmt.Errorf("git clone error: %w", err)
}
_, err = repo.Head()
if err != nil {
return nil, fmt.Errorf("git resolve HEAD error: %w", err)
}
return repo, nil
}
func generateGitHubInstall(targetPath, namespace, tmpDir string) (string, error) {
tkDir := path.Join(tmpDir, ".tk")
defer os.RemoveAll(tkDir)
if err := os.MkdirAll(tkDir, os.ModePerm); err != nil {
return "", fmt.Errorf("generating manifests failed: %w", err)
}
if err := genInstallManifests(bootstrapVersion, namespace, components, tkDir); err != nil {
return "", fmt.Errorf("generating manifests failed: %w", err)
}
manifestsDir := path.Join(tmpDir, targetPath, namespace)
if err := os.MkdirAll(manifestsDir, os.ModePerm); err != nil {
return "", fmt.Errorf("generating manifests failed: %w", err)
}
manifest := path.Join(manifestsDir, ghInstallManifest)
if err := buildKustomization(tkDir, manifest); err != nil {
return "", fmt.Errorf("build kustomization failed: %w", err)
}
return manifest, nil
}
func commitGitHubManifests(repo *git.Repository, targetPath, namespace string) (bool, error) {
w, err := repo.Worktree()
if err != nil {
return false, err
}
_, err = w.Add(path.Join(targetPath, namespace))
if err != nil {
return false, err
}
status, err := w.Status()
if err != nil {
return false, err
}
if !status.IsClean() {
if _, err := w.Commit("Add manifests", &git.CommitOptions{
Author: &object.Signature{
Name: "tk",
Email: "tk@users.noreply.github.com",
When: time.Now(),
},
}); err != nil {
return false, err
}
return true, nil
}
return false, nil
}
func pushGitHubRepository(ctx context.Context, repo *git.Repository, token string) error {
auth := &http.BasicAuth{
Username: "git",
Password: token,
}
err := repo.PushContext(ctx, &git.PushOptions{
Auth: auth,
Progress: nil,
})
if err != nil {
return fmt.Errorf("git push error: %w", err)
}
return nil
}
func generateGitHubKustomization(url, name, namespace, targetPath, tmpDir string, interval time.Duration) error {
gvk := sourcev1.GroupVersion.WithKind("GitRepository")
gitRepository := sourcev1.GitRepository{
TypeMeta: metav1.TypeMeta{
Kind: gvk.Kind,
APIVersion: gvk.GroupVersion().String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: sourcev1.GitRepositorySpec{
URL: url,
Interval: metav1.Duration{
Duration: interval,
},
Reference: &sourcev1.GitRepositoryRef{
Branch: "master",
},
SecretRef: &corev1.LocalObjectReference{
Name: name,
},
},
}
gitData, err := yaml.Marshal(gitRepository)
if err != nil {
return err
}
if err := utils.writeFile(string(gitData), filepath.Join(tmpDir, targetPath, namespace, ghSourceManifest)); err != nil {
return err
}
gvk = kustomizev1.GroupVersion.WithKind("Kustomization")
emptyAPIGroup := ""
kustomization := kustomizev1.Kustomization{
TypeMeta: metav1.TypeMeta{
Kind: gvk.Kind,
APIVersion: gvk.GroupVersion().String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: kustomizev1.KustomizationSpec{
Interval: metav1.Duration{
Duration: 10 * time.Minute,
},
Path: "./" + strings.TrimPrefix("./", targetPath),
Prune: true,
SourceRef: corev1.TypedLocalObjectReference{
APIGroup: &emptyAPIGroup,
Kind: "GitRepository",
Name: name,
},
},
}
ksData, err := yaml.Marshal(kustomization)
if err != nil {
return err
}
if err := utils.writeFile(string(ksData), filepath.Join(tmpDir, targetPath, namespace, ghKustomizationManifest)); err != nil {
return err
}
return nil
}
func applyGitHubKustomization(ctx context.Context, kubeClient client.Client, name, namespace, targetPath, tmpDir string) error {
command := fmt.Sprintf("kubectl apply -f %s", filepath.Join(tmpDir, targetPath, namespace))
if _, err := utils.execCommand(ctx, ModeStderrOS, command); err != nil {
return err
}
logWaiting("waiting for cluster sync")
if err := wait.PollImmediate(pollInterval, timeout,
isGitRepositoryReady(ctx, kubeClient, name, namespace)); err != nil {
return err
}
if err := wait.PollImmediate(pollInterval, timeout,
isKustomizationReady(ctx, kubeClient, name, namespace)); err != nil {
return err
}
return nil
}
func shouldInstallGitHub(ctx context.Context, kubeClient client.Client, namespace string) bool {
namespacedName := types.NamespacedName{
Namespace: namespace,
Name: namespace,
}
var kustomization kustomizev1.Kustomization
if err := kubeClient.Get(ctx, namespacedName, &kustomization); err != nil {
return true
}
return kustomization.Status.LastAppliedRevision == ""
}
func shouldCreateGitHubDeployKey(ctx context.Context, kubeClient client.Client, namespace string) bool {
namespacedName := types.NamespacedName{
Namespace: namespace,
Name: namespace,
}
var existing corev1.Secret
if err := kubeClient.Get(ctx, namespacedName, &existing); err != nil {
return true
}
return false
}
func generateGitHubDeployKey(ctx context.Context, kubeClient client.Client, url *url.URL, namespace string) (string, error) {
pair, err := generateKeyPair(ctx)
if err != nil {
return "", err
}
hostKey, err := scanHostKey(ctx, url)
if err != nil {
return "", err
}
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
Namespace: namespace,
},
StringData: map[string]string{
"identity": string(pair.PrivateKey),
"identity.pub": string(pair.PublicKey),
"known_hosts": string(hostKey),
},
}
if err := upsertSecret(ctx, kubeClient, secret); err != nil {
return "", err
}
return string(pair.PublicKey), nil
}
func createGitHubDeployKey(ctx context.Context, key, hostname, owner, repository, targetPath, token string) error {
gh, err := makeGitHubClient(hostname, token)
if err != nil {
return err
}
keyName := "tk"
if targetPath != "" {
keyName = fmt.Sprintf("tk-%s", targetPath)
}
// list deploy keys
keys, resp, err := gh.Repositories.ListKeys(ctx, owner, repository, nil)
if err != nil {
return fmt.Errorf("github list deploy keys error: %w", err)
}
if resp.StatusCode >= 300 {
return fmt.Errorf("github list deploy keys failed with status code: %s", resp.Status)
}
// check if the key exists
shouldCreateKey := true
var existingKey *github.Key
for _, k := range keys {
if k.Title != nil && k.Key != nil && *k.Title == keyName {
if *k.Key != key {
existingKey = k
} else {
shouldCreateKey = false
}
break
}
}
// delete existing key if the value differs
if existingKey != nil {
resp, err := gh.Repositories.DeleteKey(ctx, owner, repository, *existingKey.ID)
if err != nil {
return fmt.Errorf("github delete deploy key error: %w", err)
}
if resp.StatusCode >= 300 {
return fmt.Errorf("github delete deploy key failed with status code: %s", resp.Status)
}
}
// create key
if shouldCreateKey {
isReadOnly := true
_, _, err = gh.Repositories.CreateKey(ctx, owner, repository, &github.Key{
Title: &keyName,
Key: &key,
ReadOnly: &isReadOnly,
})
if err != nil {
return fmt.Errorf("github create deploy key error: %w", err)
}
}
return nil
}

View File

@@ -45,9 +45,6 @@ func runCheckCmd(cmd *cobra.Command, args []string) error {
logAction("checking prerequisites")
checkFailed := false
if !sshCheck() {
checkFailed = true
}
if !kubectlCheck(ctx, ">=1.18.0") {
checkFailed = true
@@ -76,21 +73,6 @@ func runCheckCmd(cmd *cobra.Command, args []string) error {
return nil
}
func sshCheck() bool {
ok := true
for _, cmd := range []string{"ssh-keygen", "ssh-keyscan"} {
_, err := exec.LookPath(cmd)
if err != nil {
logFailure("%s not found", cmd)
ok = false
} else {
logSuccess("%s found", cmd)
}
}
return ok
}
func kubectlCheck(ctx context.Context, version string) bool {
_, err := exec.LookPath("kubectl")
if err != nil {

View File

@@ -13,9 +13,11 @@ var createCmd = &cobra.Command{
var (
interval time.Duration
export bool
)
func init() {
createCmd.PersistentFlags().DurationVarP(&interval, "interval", "", time.Minute, "source sync interval")
createCmd.PersistentFlags().BoolVar(&export, "export", false, "export in yaml format to stdout")
rootCmd.AddCommand(createCmd)
}

View File

@@ -3,11 +3,9 @@ package main
import (
"context"
"fmt"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1"
"strings"
"time"
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
@@ -15,6 +13,9 @@ import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1"
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
)
var createKsCmd = &cobra.Command{
@@ -106,7 +107,9 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
return err
}
logGenerate("generating kustomization")
if !export {
logGenerate("generating kustomization")
}
emptyAPIGroup := ""
kustomization := kustomizev1.Kustomization{
@@ -171,6 +174,10 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
}
}
if export {
return exportKs(kustomization)
}
logAction("applying kustomization")
if err := upsertKustomization(ctx, kubeClient, kustomization); err != nil {
return err

View File

@@ -2,20 +2,24 @@ package main
import (
"context"
"crypto/elliptic"
"fmt"
"io/ioutil"
"net/url"
"os"
"time"
sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
"io/ioutil"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"net/url"
"os"
"sigs.k8s.io/controller-runtime/pkg/client"
"strings"
"github.com/fluxcd/toolkit/pkg/ssh"
)
var createSourceGitCmd = &cobra.Command{
@@ -40,11 +44,19 @@ For private Git repositories, the basic authentication credentials are stored in
--url=https://github.com/stefanprodan/podinfo \
--tag-semver=">=3.2.0 <3.3.0"
# Create a source from a Git repository using SSH authentication
# Create a source from a Git repository using SSH authentication
create source git podinfo \
--url=ssh://git@github.com/stefanprodan/podinfo \
--branch=master
# Create a source from a Git repository using SSH authentication and an
# ECDSA P-521 curve public key
create source git podinfo \
--url=ssh://git@github.com/stefanprodan/podinfo \
--branch=master \
--ssh-key-algorithm=ecdsa \
--ssh-ecdsa-curve=p521
# Create a source from a Git repository using basic authentication
create source git podinfo \
--url=https://github.com/stefanprodan/podinfo \
@@ -55,12 +67,15 @@ For private Git repositories, the basic authentication credentials are stored in
}
var (
sourceGitURL string
sourceGitBranch string
sourceGitTag string
sourceGitSemver string
sourceGitUsername string
sourceGitPassword string
sourceGitURL string
sourceGitBranch string
sourceGitTag string
sourceGitSemver string
sourceGitUsername string
sourceGitPassword string
sourceGitKeyAlgorithm PublicKeyAlgorithm = "rsa"
sourceGitRSABits RSAKeyBits = 2048
sourceGitECDSACurve = ECDSACurve{elliptic.P384()}
)
func init() {
@@ -70,6 +85,9 @@ func init() {
createSourceGitCmd.Flags().StringVar(&sourceGitSemver, "tag-semver", "", "git tag semver range")
createSourceGitCmd.Flags().StringVarP(&sourceGitUsername, "username", "u", "", "basic authentication username")
createSourceGitCmd.Flags().StringVarP(&sourceGitPassword, "password", "p", "", "basic authentication password")
createSourceGitCmd.Flags().Var(&sourceGitKeyAlgorithm, "ssh-key-algorithm", sourceGitKeyAlgorithm.Description())
createSourceGitCmd.Flags().Var(&sourceGitRSABits, "ssh-rsa-bits", sourceGitRSABits.Description())
createSourceGitCmd.Flags().Var(&sourceGitECDSACurve, "ssh-ecdsa-curve", sourceGitECDSACurve.Description())
createSourceCmd.AddCommand(createSourceGitCmd)
}
@@ -98,25 +116,11 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
withAuth := false
if strings.HasPrefix(sourceGitURL, "ssh") {
if err := generateSSH(ctx, name, u.Host, tmpDir); err != nil {
return err
}
withAuth = true
} else if sourceGitUsername != "" && sourceGitPassword != "" {
if err := generateBasicAuth(ctx, name); err != nil {
return err
}
withAuth = true
kubeClient, err := utils.kubeClient(kubeconfig)
if err != nil {
return err
}
if withAuth {
logSuccess("authentication configured")
}
logGenerate("generating source")
gitRepository := sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{
Name: name,
@@ -131,12 +135,6 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
},
}
if withAuth {
gitRepository.Spec.SecretRef = &corev1.LocalObjectReference{
Name: name,
}
}
if sourceGitSemver != "" {
gitRepository.Spec.Reference.SemVer = sourceGitSemver
} else if sourceGitTag != "" {
@@ -145,9 +143,80 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
gitRepository.Spec.Reference.Branch = sourceGitBranch
}
kubeClient, err := utils.kubeClient(kubeconfig)
if err != nil {
return err
if export {
return exportGit(gitRepository)
}
withAuth := false
// TODO(hidde): move all auth prep to separate func?
if u.Scheme == "ssh" {
logAction("generating deploy key pair")
pair, err := generateKeyPair(ctx)
if err != nil {
return err
}
fmt.Printf("%s", pair.PublicKey)
prompt := promptui.Prompt{
Label: "Have you added the deploy key to your repository",
IsConfirm: true,
}
if _, err := prompt.Run(); err != nil {
return fmt.Errorf("aborting")
}
logAction("collecting preferred public key from SSH server")
hostKey, err := scanHostKey(ctx, u)
if err != nil {
return err
}
logSuccess("collected public key from SSH server:")
fmt.Printf("%s", hostKey)
logAction("applying secret with keys")
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
StringData: map[string]string{
"identity": string(pair.PrivateKey),
"identity.pub": string(pair.PublicKey),
"known_hosts": string(hostKey),
},
}
if err := upsertSecret(ctx, kubeClient, secret); err != nil {
return err
}
withAuth = true
} else if sourceGitUsername != "" && sourceGitPassword != "" {
logAction("applying secret with basic auth credentials")
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
StringData: map[string]string{
"username": sourceGitUsername,
"password": sourceGitPassword,
},
}
if err := upsertSecret(ctx, kubeClient, secret); err != nil {
return err
}
withAuth = true
}
if withAuth {
logSuccess("authentication configured")
}
logGenerate("generating source")
if withAuth {
gitRepository.Spec.SecretRef = &corev1.LocalObjectReference{
Name: name,
}
}
logAction("applying source")
@@ -181,55 +250,59 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
return nil
}
func generateBasicAuth(ctx context.Context, name string) error {
logAction("saving credentials")
credentials := fmt.Sprintf("--from-literal=username='%s' --from-literal=password='%s'",
sourceGitUsername, sourceGitPassword)
secret := fmt.Sprintf("kubectl -n %s create secret generic %s %s --dry-run=client -oyaml | kubectl apply -f-",
namespace, name, credentials)
if _, err := utils.execCommand(ctx, ModeOS, secret); err != nil {
return fmt.Errorf("kubectl create secret failed")
func generateKeyPair(ctx context.Context) (*ssh.KeyPair, error) {
var keyGen ssh.KeyPairGenerator
switch sourceGitKeyAlgorithm.String() {
case "rsa":
keyGen = ssh.NewRSAGenerator(int(sourceGitRSABits))
case "ecdsa":
keyGen = ssh.NewECDSAGenerator(sourceGitECDSACurve.Curve)
case "ed25519":
keyGen = ssh.NewEd25519Generator()
default:
return nil, fmt.Errorf("unsupported public key algorithm '%s'", sourceGitKeyAlgorithm.String())
}
return nil
pair, err := keyGen.Generate()
if err != nil {
return nil, fmt.Errorf("key pair generation failed: %w", err)
}
return pair, nil
}
func generateSSH(ctx context.Context, name, host, tmpDir string) error {
logGenerate("generating host key for %s", host)
func scanHostKey(ctx context.Context, url *url.URL) ([]byte, error) {
host := url.Host
if url.Port() == "" {
host = host + ":22"
}
hostKey, err := ssh.ScanHostKey(host, 30*time.Second)
if err != nil {
return nil, fmt.Errorf("SSH key scan for host '%s' failed: %w", host, err)
}
return hostKey, nil
}
command := fmt.Sprintf("ssh-keyscan %s > %s/known_hosts", host, tmpDir)
if _, err := utils.execCommand(ctx, ModeStderrOS, command); err != nil {
return fmt.Errorf("ssh-keyscan failed")
func upsertSecret(ctx context.Context, kubeClient client.Client, secret corev1.Secret) error {
namespacedName := types.NamespacedName{
Namespace: secret.GetNamespace(),
Name: secret.GetName(),
}
logGenerate("generating deploy key")
command = fmt.Sprintf("ssh-keygen -b 2048 -t rsa -f %s/identity -q -N \"\"", tmpDir)
if _, err := utils.execCommand(ctx, ModeStderrOS, command); err != nil {
return fmt.Errorf("ssh-keygen failed")
var existing corev1.Secret
err := kubeClient.Get(ctx, namespacedName, &existing)
if err != nil {
if errors.IsNotFound(err) {
if err := kubeClient.Create(ctx, &secret); err != nil {
return err
} else {
return nil
}
}
return err
}
command = fmt.Sprintf("cat %s/identity.pub", tmpDir)
if deployKey, err := utils.execCommand(ctx, ModeCapture, command); err != nil {
return fmt.Errorf("unable to read identity.pub: %w", err)
} else {
fmt.Print(deployKey)
}
prompt := promptui.Prompt{
Label: "Have you added the deploy key to your repository",
IsConfirm: true,
}
if _, err := prompt.Run(); err != nil {
return fmt.Errorf("aborting")
}
logAction("saving keys")
files := fmt.Sprintf("--from-file=%s/identity --from-file=%s/identity.pub --from-file=%s/known_hosts",
tmpDir, tmpDir, tmpDir)
secret := fmt.Sprintf("kubectl -n %s create secret generic %s %s --dry-run=client -oyaml | kubectl apply -f-",
namespace, name, files)
if _, err := utils.execCommand(ctx, ModeOS, secret); err != nil {
return fmt.Errorf("create secret failed")
existing.StringData = secret.StringData
if err := kubeClient.Update(ctx, &existing); err != nil {
return err
}
return nil
}

112
cmd/tk/flags.go Normal file
View File

@@ -0,0 +1,112 @@
package main
import (
"crypto/elliptic"
"fmt"
"strconv"
"strings"
)
var supportedPublicKeyAlgorithms = []string{"rsa", "ecdsa", "ed25519"}
type PublicKeyAlgorithm string
func (a *PublicKeyAlgorithm) String() string {
return string(*a)
}
func (a *PublicKeyAlgorithm) Set(str string) error {
if strings.TrimSpace(str) == "" {
return fmt.Errorf("no public key algorithm given, must be one of: %s",
strings.Join(supportedPublicKeyAlgorithms, ", "))
}
for _, v := range supportedPublicKeyAlgorithms {
if str == v {
*a = PublicKeyAlgorithm(str)
return nil
}
}
return fmt.Errorf("unsupported public key algorithm '%s', must be one of: %s",
str, strings.Join(supportedPublicKeyAlgorithms, ", "))
}
func (a *PublicKeyAlgorithm) Type() string {
return "publicKeyAlgorithm"
}
func (a *PublicKeyAlgorithm) Description() string {
return fmt.Sprintf("SSH public key algorithm (%s)", strings.Join(supportedPublicKeyAlgorithms, ", "))
}
var defaultRSAKeyBits = 2048
type RSAKeyBits int
func (b *RSAKeyBits) String() string {
return strconv.Itoa(int(*b))
}
func (b *RSAKeyBits) Set(str string) error {
if strings.TrimSpace(str) == "" {
*b = RSAKeyBits(defaultRSAKeyBits)
return nil
}
bits, err := strconv.Atoi(str)
if err != nil {
return err
}
if bits%8 != 0 {
return fmt.Errorf("RSA key bit size should be a multiples of 8")
}
*b = RSAKeyBits(bits)
return nil
}
func (b *RSAKeyBits) Type() string {
return "rsaKeyBits"
}
func (b *RSAKeyBits) Description() string {
return "SSH RSA public key bit size (multiplies of 8)"
}
type ECDSACurve struct {
elliptic.Curve
}
var supportedECDSACurves = map[string]elliptic.Curve{
"p256": elliptic.P256(),
"p384": elliptic.P384(),
"p521": elliptic.P521(),
}
func (c *ECDSACurve) String() string {
if c.Curve == nil {
return ""
}
return strings.ToLower(strings.Replace(c.Curve.Params().Name, "-", "", 1))
}
func (c *ECDSACurve) Set(str string) error {
if v, ok := supportedECDSACurves[str]; ok {
*c = ECDSACurve{v}
return nil
}
return fmt.Errorf("unsupported curve '%s', should be one of: %s", str, strings.Join(ecdsaCurves(), ", "))
}
func (c *ECDSACurve) Type() string {
return "ecdsaCurve"
}
func (c *ECDSACurve) Description() string {
return fmt.Sprintf("SSH ECDSA public key curve (%s)", strings.Join(ecdsaCurves(), ", "))
}
func ecdsaCurves() []string {
keys := make([]string, 0, len(supportedECDSACurves))
for k := range supportedECDSACurves {
keys = append(keys, k)
}
return keys
}

View File

@@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"time"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
@@ -24,12 +25,15 @@ cluster role bindings and CRDs.`,
}
var (
uninstallCRDs bool
uninstallDryRun bool
uninstallSilent bool
uninstallCRDs bool
uninstallKustomizations bool
uninstallDryRun bool
uninstallSilent bool
)
func init() {
uninstallCmd.Flags().BoolVarP(&uninstallKustomizations, "kustomizations", "", false,
"removes all kustomizations previously installed")
uninstallCmd.Flags().BoolVarP(&uninstallCRDs, "crds", "", false,
"removes all CRDs previously installed")
uninstallCmd.Flags().BoolVarP(&uninstallDryRun, "dry-run", "", false,
@@ -57,6 +61,20 @@ func uninstallCmdRun(cmd *cobra.Command, args []string) error {
}
}
if uninstallKustomizations {
logAction("uninstalling kustomizations")
command := fmt.Sprintf("kubectl -n %s delete kustomizations --all --timeout=%s %s",
namespace, timeout.String(), dryRun)
if _, err := utils.execCommand(ctx, ModeOS, command); err != nil {
return fmt.Errorf("uninstall failed")
}
// TODO: use the kustomizations snapshots to create a list of objects
// that are subject to deletion and wait for all of them to be terminated
logWaiting("waiting on GC")
time.Sleep(30 * time.Second)
}
kinds := "namespace,clusterroles,clusterrolebindings"
if uninstallCRDs {
kinds += ",crds"

View File

@@ -112,3 +112,18 @@ func (*Utils) kubeClient(config string) (client.Client, error) {
return kubeClient, nil
}
func (*Utils) writeFile(content, filename string) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
_, err = io.WriteString(file, content)
if err != nil {
return err
}
return file.Sync()
}

View File

@@ -77,6 +77,7 @@ Command line utility for assembling Kubernetes CD pipelines the GitOps way.
### SEE ALSO
* [tk bootstrap](tk_bootstrap.md) - Bootstrap commands
* [tk check](tk_check.md) - Check requirements and installation
* [tk completion](tk_completion.md) - Generates bash completion scripts
* [tk create](tk_create.md) - Create commands
@@ -89,4 +90,4 @@ Command line utility for assembling Kubernetes CD pipelines the GitOps way.
* [tk sync](tk_sync.md) - Synchronize commands
* [tk uninstall](tk_uninstall.md) - Uninstall the toolkit components
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

31
docs/cmd/tk_bootstrap.md Normal file
View File

@@ -0,0 +1,31 @@
## tk bootstrap
Bootstrap commands
### Synopsis
Bootstrap commands
### Options
```
-h, --help help for bootstrap
--version string toolkit tag or branch (default "master")
```
### Options inherited from parent commands
```
--components strings list of components, accepts comma-separated values (default [source-controller,kustomize-controller])
--kubeconfig string path to the kubeconfig file (default "~/.kube/config")
--namespace string the namespace scope for this operation (default "gitops-system")
--timeout duration timeout for this operation (default 5m0s)
--verbose print generated objects
```
### SEE ALSO
* [tk](tk.md) - Command line utility for assembling Kubernetes CD pipelines
* [tk bootstrap github](tk_bootstrap_github.md) - Bootstrap GitHub repository
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -0,0 +1,62 @@
## tk bootstrap github
Bootstrap GitHub repository
### Synopsis
The bootstrap command creates the GitHub repository if it doesn't exists and
commits the toolkit components manifests to the master branch.
Then it configure the target cluster to synchronize with the repository.
If the toolkit components are present on the cluster,
the bootstrap command will perform an upgrade if needed.
```
tk bootstrap github [flags]
```
### Examples
```
# Create a GitHub personal access token and export it as an env var
export GITHUB_TOKEN=<my-token>
# Run bootstrap for a private repo owned by a GitHub organization
bootstrap github --owner=<organization> --repository=<repo name>
# Run bootstrap for a public repository on a personal account
bootstrap github --owner=<user> --repository=<repo name> --private=false --personal=true
# Run bootstrap for a private repo hosted on GitHub Enterprise
bootstrap github --owner=<organization> --repository=<repo name> --hostname=<domain>
```
### Options
```
-h, --help help for github
--hostname string GitHub hostname (default "github.com")
--interval duration sync interval (default 1m0s)
--owner string GitHub user or organization name
--personal is personal repository
--private is private repository (default true)
--repository string GitHub repository name
```
### Options inherited from parent commands
```
--components strings list of components, accepts comma-separated values (default [source-controller,kustomize-controller])
--kubeconfig string path to the kubeconfig file (default "~/.kube/config")
--namespace string the namespace scope for this operation (default "gitops-system")
--timeout duration timeout for this operation (default 5m0s)
--verbose print generated objects
--version string toolkit tag or branch (default "master")
```
### SEE ALSO
* [tk bootstrap](tk_bootstrap.md) - Bootstrap commands
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -44,4 +44,4 @@ tk check [flags]
* [tk](tk.md) - Command line utility for assembling Kubernetes CD pipelines
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -44,4 +44,4 @@ To configure your bash shell to load completions for each session add to your ba
* [tk](tk.md) - Command line utility for assembling Kubernetes CD pipelines
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -9,6 +9,7 @@ Create commands
### Options
```
--export export in yaml format to stdout
-h, --help help for create
--interval duration source sync interval (default 1m0s)
```
@@ -29,4 +30,4 @@ Create commands
* [tk create kustomization](tk_create_kustomization.md) - Create or update a kustomization resource
* [tk create source](tk_create_source.md) - Create source commands
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -66,6 +66,7 @@ tk create kustomization [name] [flags]
```
--components strings list of components, accepts comma-separated values (default [source-controller,kustomize-controller])
--export export in yaml format to stdout
--interval duration source sync interval (default 1m0s)
--kubeconfig string path to the kubeconfig file (default "~/.kube/config")
--namespace string the namespace scope for this operation (default "gitops-system")
@@ -77,4 +78,4 @@ tk create kustomization [name] [flags]
* [tk create](tk_create.md) - Create commands
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -16,6 +16,7 @@ Create source commands
```
--components strings list of components, accepts comma-separated values (default [source-controller,kustomize-controller])
--export export in yaml format to stdout
--interval duration source sync interval (default 1m0s)
--kubeconfig string path to the kubeconfig file (default "~/.kube/config")
--namespace string the namespace scope for this operation (default "gitops-system")
@@ -28,4 +29,4 @@ Create source commands
* [tk create](tk_create.md) - Create commands
* [tk create source git](tk_create_source_git.md) - Create or update a git source
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -31,11 +31,19 @@ tk create source git [name] [flags]
--url=https://github.com/stefanprodan/podinfo \
--tag-semver=">=3.2.0 <3.3.0"
# Create a source from a Git repository using SSH authentication
# Create a source from a Git repository using SSH authentication
create source git podinfo \
--url=ssh://git@github.com/stefanprodan/podinfo \
--branch=master
# Create a source from a Git repository using SSH authentication and an
# ECDSA P-521 curve public key
create source git podinfo \
--url=ssh://git@github.com/stefanprodan/podinfo \
--branch=master \
--ssh-key-algorithm=ecdsa \
--ssh-ecdsa-curve=p521
# Create a source from a Git repository using basic authentication
create source git podinfo \
--url=https://github.com/stefanprodan/podinfo \
@@ -47,19 +55,23 @@ tk create source git [name] [flags]
### Options
```
--branch string git branch (default "master")
-h, --help help for git
-p, --password string basic authentication password
--tag string git tag
--tag-semver string git tag semver range
--url string git address, e.g. ssh://git@host/org/repository
-u, --username string basic authentication username
--branch string git branch (default "master")
-h, --help help for git
-p, --password string basic authentication password
--ssh-ecdsa-curve ecdsaCurve SSH ECDSA public key curve (p521, p256, p384) (default p384)
--ssh-key-algorithm publicKeyAlgorithm SSH public key algorithm (rsa, ecdsa, ed25519) (default rsa)
--ssh-rsa-bits rsaKeyBits SSH RSA public key bit size (multiplies of 8) (default 2048)
--tag string git tag
--tag-semver string git tag semver range
--url string git address, e.g. ssh://git@host/org/repository
-u, --username string basic authentication username
```
### Options inherited from parent commands
```
--components strings list of components, accepts comma-separated values (default [source-controller,kustomize-controller])
--export export in yaml format to stdout
--interval duration source sync interval (default 1m0s)
--kubeconfig string path to the kubeconfig file (default "~/.kube/config")
--namespace string the namespace scope for this operation (default "gitops-system")
@@ -71,4 +83,4 @@ tk create source git [name] [flags]
* [tk create source](tk_create_source.md) - Create source commands
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -29,4 +29,4 @@ Delete commands
* [tk delete kustomization](tk_delete_kustomization.md) - Delete kustomization
* [tk delete source](tk_delete_source.md) - Delete sources commands
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -31,4 +31,4 @@ tk delete kustomization [name] [flags]
* [tk delete](tk_delete.md) - Delete commands
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -28,4 +28,4 @@ Delete sources commands
* [tk delete](tk_delete.md) - Delete commands
* [tk delete source git](tk_delete_source_git.md) - Delete git source
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -31,4 +31,4 @@ tk delete source git [name] [flags]
* [tk delete source](tk_delete_source.md) - Delete sources commands
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -29,4 +29,4 @@ Export commands
* [tk export kustomization](tk_export_kustomization.md) - Export kustomization in YAML format
* [tk export source](tk_export_source.md) - Export source commands
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -42,4 +42,4 @@ tk export kustomization [name] [flags]
* [tk export](tk_export.md) - Export commands
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -29,4 +29,4 @@ Export source commands
* [tk export](tk_export.md) - Export commands
* [tk export source git](tk_export_source_git.md) - Export git sources in YAML format
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -43,4 +43,4 @@ tk export source git [name] [flags]
* [tk export source](tk_export_source.md) - Export source commands
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -28,4 +28,4 @@ Get commands
* [tk get kustomizations](tk_get_kustomizations.md) - Get kustomizations status
* [tk get sources](tk_get_sources.md) - Get sources commands
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -31,4 +31,4 @@ tk get kustomizations [flags]
* [tk get](tk_get.md) - Get commands
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -27,4 +27,4 @@ Get sources commands
* [tk get](tk_get.md) - Get commands
* [tk get sources git](tk_get_sources_git.md) - Get git sources status
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -31,4 +31,4 @@ tk get sources git [flags]
* [tk get sources](tk_get_sources.md) - Get sources commands
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -49,4 +49,4 @@ tk install [flags]
* [tk](tk.md) - Command line utility for assembling Kubernetes CD pipelines
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -27,4 +27,4 @@ Resume commands
* [tk](tk.md) - Command line utility for assembling Kubernetes CD pipelines
* [tk resume kustomization](tk_resume_kustomization.md) - Resume kustomization
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -30,4 +30,4 @@ tk resume kustomization [name] [flags]
* [tk resume](tk_resume.md) - Resume commands
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -27,4 +27,4 @@ Suspend commands
* [tk](tk.md) - Command line utility for assembling Kubernetes CD pipelines
* [tk suspend kustomization](tk_suspend_kustomization.md) - Suspend kustomization
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -30,4 +30,4 @@ tk suspend kustomization [name] [flags]
* [tk suspend](tk_suspend.md) - Suspend commands
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -28,4 +28,4 @@ Synchronize commands
* [tk sync kustomization](tk_sync_kustomization.md) - Synchronize kustomization
* [tk sync source](tk_sync_source.md) - Synchronize source commands
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -43,4 +43,4 @@ tk sync kustomization [name] [flags]
* [tk sync](tk_sync.md) - Synchronize commands
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -27,4 +27,4 @@ Synchronize source commands
* [tk sync](tk_sync.md) - Synchronize commands
* [tk sync source git](tk_sync_source_git.md) - Synchronize git source
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -39,4 +39,4 @@ tk sync source git [name] [flags]
* [tk sync source](tk_sync_source.md) - Synchronize source commands
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

View File

@@ -46,4 +46,4 @@ tk uninstall [flags]
* [tk](tk.md) - Command line utility for assembling Kubernetes CD pipelines
###### Auto generated by spf13/cobra on 5-May-2020
###### Auto generated by spf13/cobra on 9-Jun-2020

7
go.mod
View File

@@ -4,10 +4,13 @@ go 1.14
require (
github.com/blang/semver v3.5.1+incompatible
github.com/fluxcd/kustomize-controller v0.0.1-beta.1
github.com/fluxcd/source-controller v0.0.1-beta.1
github.com/fluxcd/kustomize-controller v0.0.1-beta.2
github.com/fluxcd/source-controller v0.0.1-beta.2
github.com/go-git/go-git/v5 v5.0.0
github.com/google/go-github/v32 v32.0.0
github.com/manifoldco/promptui v0.7.0
github.com/spf13/cobra v1.0.0
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073
k8s.io/api v0.18.2
k8s.io/apimachinery v0.18.2
k8s.io/client-go v0.18.2

27
go.sum
View File

@@ -48,13 +48,16 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
@@ -92,6 +95,7 @@ github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:z
github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.3.2 h1:ForxmXkA6tPIvffbrDAcPUIB32QgXkt2XFj+F0UxetA=
github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20200107194136-26c1120b8d41/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY=
@@ -122,6 +126,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE=
github.com/deislabs/oras v0.8.1 h1:If674KraJVpujYR00rzdi0QAmW4BxzMJPVAZJKuhQ0c=
github.com/deislabs/oras v0.8.1/go.mod h1:Mx0rMSbBNaNfY9hjpccEnxkOqJL6KGjtxNHPLC4G4As=
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
@@ -163,12 +168,11 @@ github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwC
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fluxcd/kustomize-controller v0.0.1-beta.1 h1:RneBBMGl98jTJlaMnHVL79QEjWuu05bJd57rgTpT6w4=
github.com/fluxcd/kustomize-controller v0.0.1-beta.1/go.mod h1:U90yHjFfKFDZHA7rZtLNUpH1/gZgY/SM4FlqQz9a0Fg=
github.com/fluxcd/source-controller v0.0.1-alpha.6 h1:CpSH1mA9bRjifRwmcVS98ejY2MA9ofTyLbl2xwT0MqM=
github.com/fluxcd/source-controller v0.0.1-alpha.6/go.mod h1:gvU+sR6yH9kbyWZAJ9NEBQJMsJ+S3yCyOdPIhM+8FUI=
github.com/fluxcd/source-controller v0.0.1-beta.1 h1:1lNFrAwnrZG2qWbm9/6qgDsY4F6go+6HZ4XSCmmpAiY=
github.com/fluxcd/source-controller v0.0.1-beta.1/go.mod h1:tmscNdCxEt7+Xt2g1+bI38hMPw2leYMFAaCn4UlMGuw=
github.com/fluxcd/kustomize-controller v0.0.1-beta.2 h1:3EM2OFR9z8FiILDgzqHHqzSx3mO4KT8xG1gKTevD42E=
github.com/fluxcd/kustomize-controller v0.0.1-beta.2/go.mod h1:mLeipvpQkyof6b5IHNtqeA8CmbjfVIf92UyKkpeBY98=
github.com/fluxcd/source-controller v0.0.1-beta.2 h1:XOtc+tSf/8Q0bTVla2L5FdCMMjJWFSPWX/o4/h4OUv0=
github.com/fluxcd/source-controller v0.0.1-beta.2/go.mod h1:tmscNdCxEt7+Xt2g1+bI38hMPw2leYMFAaCn4UlMGuw=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -176,6 +180,7 @@ github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYis
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
@@ -186,6 +191,7 @@ github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc=
github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-git/go-git/v5 v5.0.0 h1:k5RWPm4iJwYtfWoxIJy4wJX9ON7ihPeZZYC1fLYDnpg=
github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmClfZwtUVA=
@@ -313,6 +319,10 @@ github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github/v32 v32.0.0 h1:q74KVb22spUq0U5HqZ9VCYqQz8YRuOtL/39ZnfwO+NM=
github.com/google/go-github/v32 v32.0.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
@@ -481,8 +491,10 @@ github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34=
github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
@@ -705,6 +717,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -793,6 +806,7 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
@@ -801,6 +815,7 @@ google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=

View File

@@ -1,5 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- github.com/fluxcd/kustomize-controller/config//crd?ref=v0.0.1-beta.1
- github.com/fluxcd/kustomize-controller/config//manager?ref=v0.0.1-beta.1
- github.com/fluxcd/kustomize-controller/config//crd?ref=v0.0.1-beta.2
- github.com/fluxcd/kustomize-controller/config//manager?ref=v0.0.1-beta.2

View File

@@ -1,5 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- github.com/fluxcd/source-controller/config//crd?ref=v0.0.1-beta.1
- github.com/fluxcd/source-controller/config//manager?ref=v0.0.1-beta.1
- github.com/fluxcd/source-controller/config//crd?ref=v0.0.1-beta.2
- github.com/fluxcd/source-controller/config//manager?ref=v0.0.1-beta.2

55
pkg/ssh/host_key.go Normal file
View File

@@ -0,0 +1,55 @@
package ssh
import (
"encoding/base64"
"fmt"
"net"
"time"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
)
// ScanHostKey collects the given host's preferred public key for the
// Any errors (e.g. authentication failures) are ignored, except if
// no key could be collected from the host.
func ScanHostKey(host string, timeout time.Duration) ([]byte, error) {
col := &HostKeyCollector{}
config := &ssh.ClientConfig{
HostKeyCallback: col.StoreKey(),
Timeout: timeout,
}
client, err := ssh.Dial("tcp", host, config)
if err == nil {
defer client.Close()
}
if len(col.knownKeys) > 0 {
return col.knownKeys, nil
}
return col.knownKeys, err
}
// HostKeyCollector offers a StoreKey method which provides an
// HostKeyCallBack to collect public keys from an SSH server.
type HostKeyCollector struct {
knownKeys []byte
}
// StoreKey stores the public key in bytes as returned by the host.
// To collect multiple public key types from the host, multiple
// SSH dials need with the ClientConfig HostKeyAlgorithms set to
// the algorithm you want to collect.
func (c *HostKeyCollector) StoreKey() ssh.HostKeyCallback {
return func(hostname string, remote net.Addr, key ssh.PublicKey) error {
c.knownKeys = append(
c.knownKeys,
fmt.Sprintf("%s %s %s\n", knownhosts.Normalize(hostname), key.Type(), base64.StdEncoding.EncodeToString(key.Marshal()))...,
)
return nil
}
}
// GetKnownKeys returns the collected public keys in bytes.
func (c *HostKeyCollector) GetKnownKeys() []byte {
return c.knownKeys
}

130
pkg/ssh/key_pair.go Normal file
View File

@@ -0,0 +1,130 @@
package ssh
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"golang.org/x/crypto/ssh"
)
// KeyPair holds the public and private key PEM block bytes.
type KeyPair struct {
PublicKey []byte
PrivateKey []byte
}
type KeyPairGenerator interface {
Generate() (*KeyPair, error)
}
type RSAGenerator struct {
bits int
}
func NewRSAGenerator(bits int) KeyPairGenerator {
return &RSAGenerator{bits}
}
func (g *RSAGenerator) Generate() (*KeyPair, error) {
pk, err := rsa.GenerateKey(rand.Reader, g.bits)
if err != nil {
return nil, err
}
err = pk.Validate()
if err != nil {
return nil, err
}
pub, err := generatePublicKey(&pk.PublicKey)
if err != nil {
return nil, err
}
priv, err := encodePrivateKeyToPEM(pk)
if err != nil {
return nil, err
}
return &KeyPair{
PublicKey: pub,
PrivateKey: priv,
}, nil
}
type ECDSAGenerator struct {
c elliptic.Curve
}
func NewECDSAGenerator(c elliptic.Curve) KeyPairGenerator {
return &ECDSAGenerator{c}
}
func (g *ECDSAGenerator) Generate() (*KeyPair, error) {
pk, err := ecdsa.GenerateKey(g.c, rand.Reader)
if err != nil {
return nil, err
}
pub, err := generatePublicKey(&pk.PublicKey)
if err != nil {
return nil, err
}
priv, err := encodePrivateKeyToPEM(pk)
if err != nil {
return nil, err
}
return &KeyPair{
PublicKey: pub,
PrivateKey: priv,
}, nil
}
type Ed25519Generator struct{}
func NewEd25519Generator() KeyPairGenerator {
return &Ed25519Generator{}
}
func (g *Ed25519Generator) Generate() (*KeyPair, error) {
pk, pv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
pub, err := generatePublicKey(pk)
if err != nil {
return nil, err
}
priv, err := encodePrivateKeyToPEM(pv)
if err != nil {
return nil, err
}
return &KeyPair{
PublicKey: pub,
PrivateKey: priv,
}, nil
}
func generatePublicKey(pk interface{}) ([]byte, error) {
b, err := ssh.NewPublicKey(pk)
if err != nil {
return nil, err
}
k := ssh.MarshalAuthorizedKey(b)
return k, nil
}
// encodePrivateKeyToPEM encodes the given private key to a PEM block.
// The encoded format is PKCS#8 for universal support of the most
// common key types (rsa, ecdsa, ed25519).
func encodePrivateKeyToPEM(pk interface{}) ([]byte, error) {
b, err := x509.MarshalPKCS8PrivateKey(pk)
if err != nil {
return nil, err
}
block := pem.Block{
Type: "PRIVATE KEY",
Bytes: b,
}
return pem.EncodeToMemory(&block), nil
}

View File

@@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,446 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Copyright 2020 The FluxCD contributors. All rights reserved.
// This package provides an in-memory known hosts database
// derived from the golang.org/x/crypto/ssh/knownhosts
// package.
// It has been slightly modified and adapted to work with
// in-memory host keys not related to any known_hosts files
// on disk, and the database can be initialized with just a
// known_hosts byte blob.
// https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts
package knownhosts
import (
"bufio"
"bytes"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"strings"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
)
// See the sshd manpage
// (http://man.openbsd.org/sshd#SSH_KNOWN_HOSTS_FILE_FORMAT) for
// background.
type addr struct{ host, port string }
func (a *addr) String() string {
h := a.host
if strings.Contains(h, ":") {
h = "[" + h + "]"
}
return h + ":" + a.port
}
type matcher interface {
match(addr) bool
}
type hostPattern struct {
negate bool
addr addr
}
func (p *hostPattern) String() string {
n := ""
if p.negate {
n = "!"
}
return n + p.addr.String()
}
type hostPatterns []hostPattern
func (ps hostPatterns) match(a addr) bool {
matched := false
for _, p := range ps {
if !p.match(a) {
continue
}
if p.negate {
return false
}
matched = true
}
return matched
}
// See
// https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/addrmatch.c
// The matching of * has no regard for separators, unlike filesystem globs
func wildcardMatch(pat []byte, str []byte) bool {
for {
if len(pat) == 0 {
return len(str) == 0
}
if len(str) == 0 {
return false
}
if pat[0] == '*' {
if len(pat) == 1 {
return true
}
for j := range str {
if wildcardMatch(pat[1:], str[j:]) {
return true
}
}
return false
}
if pat[0] == '?' || pat[0] == str[0] {
pat = pat[1:]
str = str[1:]
} else {
return false
}
}
}
func (p *hostPattern) match(a addr) bool {
return wildcardMatch([]byte(p.addr.host), []byte(a.host)) && p.addr.port == a.port
}
type inMemoryHostKeyDB struct {
hostKeys []hostKey
revoked map[string]*ssh.PublicKey
}
func newInMemoryHostKeyDB() *inMemoryHostKeyDB {
db := &inMemoryHostKeyDB{
revoked: make(map[string]*ssh.PublicKey),
}
return db
}
func keyEq(a, b ssh.PublicKey) bool {
return bytes.Equal(a.Marshal(), b.Marshal())
}
type hostKey struct {
matcher matcher
cert bool
key ssh.PublicKey
}
func (l *hostKey) match(a addr) bool {
return l.matcher.match(a)
}
// IsAuthorityForHost can be used as a callback in ssh.CertChecker
func (db *inMemoryHostKeyDB) IsHostAuthority(remote ssh.PublicKey, address string) bool {
h, p, err := net.SplitHostPort(address)
if err != nil {
return false
}
a := addr{host: h, port: p}
for _, l := range db.hostKeys {
if l.cert && keyEq(l.key, remote) && l.match(a) {
return true
}
}
return false
}
// IsRevoked can be used as a callback in ssh.CertChecker
func (db *inMemoryHostKeyDB) IsRevoked(key *ssh.Certificate) bool {
_, ok := db.revoked[string(key.Marshal())]
return ok
}
const markerCert = "@cert-authority"
const markerRevoked = "@revoked"
func nextWord(line []byte) (string, []byte) {
i := bytes.IndexAny(line, "\t ")
if i == -1 {
return string(line), nil
}
return string(line[:i]), bytes.TrimSpace(line[i:])
}
func parseLine(line []byte) (marker, host string, key ssh.PublicKey, err error) {
if w, next := nextWord(line); w == markerCert || w == markerRevoked {
marker = w
line = next
}
host, line = nextWord(line)
if len(line) == 0 {
return "", "", nil, errors.New("knownhosts: missing host pattern")
}
// ignore the keytype as it's in the key blob anyway.
_, line = nextWord(line)
if len(line) == 0 {
return "", "", nil, errors.New("knownhosts: missing key type pattern")
}
keyBlob, _ := nextWord(line)
keyBytes, err := base64.StdEncoding.DecodeString(keyBlob)
if err != nil {
return "", "", nil, err
}
key, err = ssh.ParsePublicKey(keyBytes)
if err != nil {
return "", "", nil, err
}
return marker, host, key, nil
}
func (db *inMemoryHostKeyDB) parseLine(line []byte) error {
marker, pattern, key, err := parseLine(line)
if err != nil {
return err
}
if marker == markerRevoked {
db.revoked[string(key.Marshal())] = &key
return nil
}
entry := hostKey{
key: key,
cert: marker == markerCert,
}
if pattern[0] == '|' {
entry.matcher, err = newHashedHost(pattern)
} else {
entry.matcher, err = newHostnameMatcher(pattern)
}
if err != nil {
return err
}
db.hostKeys = append(db.hostKeys, entry)
return nil
}
func newHostnameMatcher(pattern string) (matcher, error) {
var hps hostPatterns
for _, p := range strings.Split(pattern, ",") {
if len(p) == 0 {
continue
}
var a addr
var negate bool
if p[0] == '!' {
negate = true
p = p[1:]
}
if len(p) == 0 {
return nil, errors.New("knownhosts: negation without following hostname")
}
var err error
if p[0] == '[' {
a.host, a.port, err = net.SplitHostPort(p)
if err != nil {
return nil, err
}
} else {
a.host, a.port, err = net.SplitHostPort(p)
if err != nil {
a.host = p
a.port = "22"
}
}
hps = append(hps, hostPattern{
negate: negate,
addr: a,
})
}
return hps, nil
}
// check checks a key against the host database. This should not be
// used for verifying certificates.
func (db *inMemoryHostKeyDB) check(address string, remote net.Addr, remoteKey ssh.PublicKey) error {
if revoked := db.revoked[string(remoteKey.Marshal())]; revoked != nil {
return &knownhosts.RevokedError{Revoked: knownhosts.KnownKey{Key: *revoked}}
}
host, port, err := net.SplitHostPort(remote.String())
if err != nil {
return fmt.Errorf("knownhosts: SplitHostPort(%s): %v", remote, err)
}
hostToCheck := addr{host, port}
if address != "" {
// Give preference to the hostname if available.
host, port, err := net.SplitHostPort(address)
if err != nil {
return fmt.Errorf("knownhosts: SplitHostPort(%s): %v", address, err)
}
hostToCheck = addr{host, port}
}
return db.checkAddr(hostToCheck, remoteKey)
}
// checkAddr checks if we can find the given public key for the
// given address. If we only find an entry for the IP address,
// or only the hostname, then this still succeeds.
func (db *inMemoryHostKeyDB) checkAddr(a addr, remoteKey ssh.PublicKey) error {
// TODO(hanwen): are these the right semantics? What if there
// is just a key for the IP address, but not for the
// hostname?
// Algorithm => key.
knownKeys := map[string]ssh.PublicKey{}
for _, l := range db.hostKeys {
if l.match(a) {
typ := l.key.Type()
if _, ok := knownKeys[typ]; !ok {
knownKeys[typ] = l.key
}
}
}
keyErr := &knownhosts.KeyError{}
for _, v := range knownKeys {
keyErr.Want = append(keyErr.Want, knownhosts.KnownKey{Key: v})
}
// Unknown remote host.
if len(knownKeys) == 0 {
return keyErr
}
// If the remote host starts using a different, unknown key type, we
// also interpret that as a mismatch.
if known, ok := knownKeys[remoteKey.Type()]; !ok || !keyEq(known, remoteKey) {
return keyErr
}
return nil
}
// The Read function parses file contents.
func (db *inMemoryHostKeyDB) Read(r io.Reader) error {
scanner := bufio.NewScanner(r)
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Bytes()
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '#' {
continue
}
if err := db.parseLine(line); err != nil {
return fmt.Errorf("knownhosts: %v", err)
}
}
return scanner.Err()
}
// New creates a host key callback from the given OpenSSH host key
// file bytes. The returned callback is for use in
// ssh.ClientConfig.HostKeyCallback. By preference, the key check
// operates on the hostname if available, i.e. if a server changes its
// IP address, the host key check will still succeed, even though a
// record of the new IP address is not available.
func New(b []byte) (ssh.HostKeyCallback, error) {
db := newInMemoryHostKeyDB()
r := bytes.NewReader(b)
if err := db.Read(r); err != nil {
return nil, err
}
var certChecker ssh.CertChecker
certChecker.IsHostAuthority = db.IsHostAuthority
certChecker.IsRevoked = db.IsRevoked
certChecker.HostKeyFallback = db.check
return certChecker.CheckHostKey, nil
}
func decodeHash(encoded string) (hashType string, salt, hash []byte, err error) {
if len(encoded) == 0 || encoded[0] != '|' {
err = errors.New("knownhosts: hashed host must start with '|'")
return
}
components := strings.Split(encoded, "|")
if len(components) != 4 {
err = fmt.Errorf("knownhosts: got %d components, want 3", len(components))
return
}
hashType = components[1]
if salt, err = base64.StdEncoding.DecodeString(components[2]); err != nil {
return
}
if hash, err = base64.StdEncoding.DecodeString(components[3]); err != nil {
return
}
return
}
func encodeHash(typ string, salt []byte, hash []byte) string {
return strings.Join([]string{"",
typ,
base64.StdEncoding.EncodeToString(salt),
base64.StdEncoding.EncodeToString(hash),
}, "|")
}
// See https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/hostfile.c#120
func hashHost(hostname string, salt []byte) []byte {
mac := hmac.New(sha1.New, salt)
mac.Write([]byte(hostname))
return mac.Sum(nil)
}
type hashedHost struct {
salt []byte
hash []byte
}
const sha1HashType = "1"
func newHashedHost(encoded string) (*hashedHost, error) {
typ, salt, hash, err := decodeHash(encoded)
if err != nil {
return nil, err
}
// The type field seems for future algorithm agility, but it's
// actually hardcoded in openssh currently, see
// https://android.googlesource.com/platform/external/openssh/+/ab28f5495c85297e7a597c1ba62e996416da7c7e/hostfile.c#120
if typ != sha1HashType {
return nil, fmt.Errorf("knownhosts: got hash type %s, must be '1'", typ)
}
return &hashedHost{salt: salt, hash: hash}, nil
}
func (h *hashedHost) match(a addr) bool {
return bytes.Equal(hashHost(knownhosts.Normalize(a.String()), h.salt), h.hash)
}

View File

@@ -0,0 +1,327 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Copyright 2020 The FluxCD contributors. All rights reserved.
// This package provides an in-memory known hosts database
// derived from the golang.org/x/crypto/ssh/knownhosts
// package.
// It has been slightly modified and adapted to work with
// in-memory host keys not related to any known_hosts files
// on disk, and the database can be initialized with just a
// known_hosts byte blob.
// https://pkg.go.dev/golang.org/x/crypto/ssh/knownhosts
package knownhosts
import (
"bytes"
"fmt"
"net"
"reflect"
"testing"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
)
const edKeyStr = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGBAarftlLeoyf+v+nVchEZII/vna2PCV8FaX4vsF5BX"
const alternateEdKeyStr = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIXffBYeYL+WVzVru8npl5JHt2cjlr4ornFTWzoij9sx"
const ecKeyStr = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNLCu01+wpXe3xB5olXCN4SqU2rQu0qjSRKJO4Bg+JRCPU+ENcgdA5srTU8xYDz/GEa4dzK5ldPw4J/gZgSXCMs="
var ecKey, alternateEdKey, edKey ssh.PublicKey
var testAddr = &net.TCPAddr{
IP: net.IP{198, 41, 30, 196},
Port: 22,
}
var testAddr6 = &net.TCPAddr{
IP: net.IP{198, 41, 30, 196,
1, 2, 3, 4,
1, 2, 3, 4,
1, 2, 3, 4,
},
Port: 22,
}
func init() {
var err error
ecKey, _, _, _, err = ssh.ParseAuthorizedKey([]byte(ecKeyStr))
if err != nil {
panic(err)
}
edKey, _, _, _, err = ssh.ParseAuthorizedKey([]byte(edKeyStr))
if err != nil {
panic(err)
}
alternateEdKey, _, _, _, err = ssh.ParseAuthorizedKey([]byte(alternateEdKeyStr))
if err != nil {
panic(err)
}
}
func testDB(t *testing.T, s string) *inMemoryHostKeyDB {
db := newInMemoryHostKeyDB()
if err := db.Read(bytes.NewBufferString(s)); err != nil {
t.Fatalf("Read: %v", err)
}
return db
}
func TestRevoked(t *testing.T) {
db := testDB(t, "\n\n@revoked * "+edKeyStr+"\n")
want := &knownhosts.RevokedError{
Revoked: knownhosts.KnownKey{
Key: edKey,
},
}
if err := db.check("", &net.TCPAddr{
Port: 42,
}, edKey); err == nil {
t.Fatal("no error for revoked key")
} else if !reflect.DeepEqual(want, err) {
t.Fatalf("got %#v, want %#v", want, err)
}
}
func TestHostAuthority(t *testing.T) {
for _, m := range []struct {
authorityFor string
address string
good bool
}{
{authorityFor: "localhost", address: "localhost:22", good: true},
{authorityFor: "localhost", address: "localhost", good: false},
{authorityFor: "localhost", address: "localhost:1234", good: false},
{authorityFor: "[localhost]:1234", address: "localhost:1234", good: true},
{authorityFor: "[localhost]:1234", address: "localhost:22", good: false},
{authorityFor: "[localhost]:1234", address: "localhost", good: false},
} {
db := testDB(t, `@cert-authority `+m.authorityFor+` `+edKeyStr)
if ok := db.IsHostAuthority(db.hostKeys[0].key, m.address); ok != m.good {
t.Errorf("IsHostAuthority: authority %s, address %s, wanted good = %v, got good = %v",
m.authorityFor, m.address, m.good, ok)
}
}
}
func TestBracket(t *testing.T) {
db := testDB(t, `[git.eclipse.org]:29418,[198.41.30.196]:29418 `+edKeyStr)
if err := db.check("git.eclipse.org:29418", &net.TCPAddr{
IP: net.IP{198, 41, 30, 196},
Port: 29418,
}, edKey); err != nil {
t.Errorf("got error %v, want none", err)
}
if err := db.check("git.eclipse.org:29419", &net.TCPAddr{
Port: 42,
}, edKey); err == nil {
t.Fatalf("no error for unknown address")
} else if ke, ok := err.(*knownhosts.KeyError); !ok {
t.Fatalf("got type %T, want *KeyError", err)
} else if len(ke.Want) > 0 {
t.Fatalf("got Want %v, want []", ke.Want)
}
}
func TestNewKeyType(t *testing.T) {
str := fmt.Sprintf("%s %s", testAddr, edKeyStr)
db := testDB(t, str)
if err := db.check("", testAddr, ecKey); err == nil {
t.Fatalf("no error for unknown address")
} else if ke, ok := err.(*knownhosts.KeyError); !ok {
t.Fatalf("got type %T, want *KeyError", err)
} else if len(ke.Want) == 0 {
t.Fatalf("got empty KeyError.Want")
}
}
func TestSameKeyType(t *testing.T) {
str := fmt.Sprintf("%s %s", testAddr, edKeyStr)
db := testDB(t, str)
if err := db.check("", testAddr, alternateEdKey); err == nil {
t.Fatalf("no error for unknown address")
} else if ke, ok := err.(*knownhosts.KeyError); !ok {
t.Fatalf("got type %T, want *KeyError", err)
} else if len(ke.Want) == 0 {
t.Fatalf("got empty KeyError.Want")
} else if got, want := ke.Want[0].Key.Marshal(), edKey.Marshal(); !bytes.Equal(got, want) {
t.Fatalf("got key %q, want %q", got, want)
}
}
func TestIPAddress(t *testing.T) {
str := fmt.Sprintf("%s %s", testAddr, edKeyStr)
db := testDB(t, str)
if err := db.check("", testAddr, edKey); err != nil {
t.Errorf("got error %q, want none", err)
}
}
func TestIPv6Address(t *testing.T) {
str := fmt.Sprintf("%s %s", testAddr6, edKeyStr)
db := testDB(t, str)
if err := db.check("", testAddr6, edKey); err != nil {
t.Errorf("got error %q, want none", err)
}
}
func TestBasic(t *testing.T) {
str := fmt.Sprintf("#comment\n\nserver.org,%s %s\notherhost %s", testAddr, edKeyStr, ecKeyStr)
db := testDB(t, str)
if err := db.check("server.org:22", testAddr, edKey); err != nil {
t.Errorf("got error %v, want none", err)
}
want := knownhosts.KnownKey{
Key: edKey,
}
if err := db.check("server.org:22", testAddr, ecKey); err == nil {
t.Errorf("succeeded, want KeyError")
} else if ke, ok := err.(*knownhosts.KeyError); !ok {
t.Errorf("got %T, want *KeyError", err)
} else if len(ke.Want) != 1 {
t.Errorf("got %v, want 1 entry", ke)
} else if !reflect.DeepEqual(ke.Want[0], want) {
t.Errorf("got %v, want %v", ke.Want[0], want)
}
}
func TestHostNamePrecedence(t *testing.T) {
var evilAddr = &net.TCPAddr{
IP: net.IP{66, 66, 66, 66},
Port: 22,
}
str := fmt.Sprintf("server.org,%s %s\nevil.org,%s %s", testAddr, edKeyStr, evilAddr, ecKeyStr)
db := testDB(t, str)
if err := db.check("server.org:22", evilAddr, ecKey); err == nil {
t.Errorf("check succeeded")
} else if _, ok := err.(*knownhosts.KeyError); !ok {
t.Errorf("got %T, want *KeyError", err)
}
}
func TestDBOrderingPrecedenceKeyType(t *testing.T) {
str := fmt.Sprintf("server.org,%s %s\nserver.org,%s %s", testAddr, edKeyStr, testAddr, alternateEdKeyStr)
db := testDB(t, str)
if err := db.check("server.org:22", testAddr, alternateEdKey); err == nil {
t.Errorf("check succeeded")
} else if _, ok := err.(*knownhosts.KeyError); !ok {
t.Errorf("got %T, want *KeyError", err)
}
}
func TestNegate(t *testing.T) {
str := fmt.Sprintf("%s,!server.org %s", testAddr, edKeyStr)
db := testDB(t, str)
if err := db.check("server.org:22", testAddr, ecKey); err == nil {
t.Errorf("succeeded")
} else if ke, ok := err.(*knownhosts.KeyError); !ok {
t.Errorf("got error type %T, want *KeyError", err)
} else if len(ke.Want) != 0 {
t.Errorf("got expected keys %d (first of type %s), want []", len(ke.Want), ke.Want[0].Key.Type())
}
}
func TestWildcard(t *testing.T) {
str := fmt.Sprintf("server*.domain %s", edKeyStr)
db := testDB(t, str)
want := &knownhosts.KeyError{
Want: []knownhosts.KnownKey{{
Key: edKey,
}},
}
got := db.check("server.domain:22", &net.TCPAddr{}, ecKey)
if !reflect.DeepEqual(got, want) {
t.Errorf("got %s, want %s", got, want)
}
}
func TestWildcardMatch(t *testing.T) {
for _, c := range []struct {
pat, str string
want bool
}{
{"a?b", "abb", true},
{"ab", "abc", false},
{"abc", "ab", false},
{"a*b", "axxxb", true},
{"a*b", "axbxb", true},
{"a*b", "axbxbc", false},
{"a*?", "axbxc", true},
{"a*b*", "axxbxxxxxx", true},
{"a*b*c", "axxbxxxxxxc", true},
{"a*b*?", "axxbxxxxxxc", true},
{"a*b*z", "axxbxxbxxxz", true},
{"a*b*z", "axxbxxzxxxz", true},
{"a*b*z", "axxbxxzxxx", false},
} {
got := wildcardMatch([]byte(c.pat), []byte(c.str))
if got != c.want {
t.Errorf("wildcardMatch(%q, %q) = %v, want %v", c.pat, c.str, got, c.want)
}
}
}
// TODO(hanwen): test coverage for certificates.
const testHostname = "hostname"
// generated with keygen -H -f
const encodedTestHostnameHash = "|1|IHXZvQMvTcZTUU29+2vXFgx8Frs=|UGccIWfRVDwilMBnA3WJoRAC75Y="
func TestHostHash(t *testing.T) {
testHostHash(t, testHostname, encodedTestHostnameHash)
}
func TestHashList(t *testing.T) {
encoded := knownhosts.HashHostname(testHostname)
testHostHash(t, testHostname, encoded)
}
func testHostHash(t *testing.T, hostname, encoded string) {
typ, salt, hash, err := decodeHash(encoded)
if err != nil {
t.Fatalf("decodeHash: %v", err)
}
if got := encodeHash(typ, salt, hash); got != encoded {
t.Errorf("got encoding %s want %s", got, encoded)
}
if typ != sha1HashType {
t.Fatalf("got hash type %q, want %q", typ, sha1HashType)
}
got := hashHost(hostname, salt)
if !bytes.Equal(got, hash) {
t.Errorf("got hash %x want %x", got, hash)
}
}
func TestHashedHostkeyCheck(t *testing.T) {
str := fmt.Sprintf("%s %s", knownhosts.HashHostname(testHostname), edKeyStr)
db := testDB(t, str)
if err := db.check(testHostname+":22", testAddr, edKey); err != nil {
t.Errorf("check(%s): %v", testHostname, err)
}
want := &knownhosts.KeyError{
Want: []knownhosts.KnownKey{{
Key: edKey,
}},
}
if got := db.check(testHostname+":22", testAddr, alternateEdKey); !reflect.DeepEqual(got, want) {
t.Errorf("got error %v, want %v", got, want)
}
}