From 42d2ed51cbb2266f98b59a140d30ebd160685f5d Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Fri, 24 Apr 2020 18:16:31 +0300 Subject: [PATCH 01/15] Add install manifests --- .../kustomize-controller/kustomization.yaml | 5 ++++ .../source-controller/kustomization.yaml | 5 ++++ manifests/install/kustomization.yaml | 11 +++++++++ manifests/install/labels.yaml | 9 +++++++ manifests/install/namespace.yaml | 4 ++++ manifests/policies/deny-ingress.yaml | 8 +++++++ manifests/policies/kustomization.yaml | 4 ++++ manifests/rbac/cluster_role.yaml | 23 ++++++++++++++++++ manifests/rbac/kustomization.yaml | 5 ++++ manifests/rbac/role.yaml | 24 +++++++++++++++++++ 10 files changed, 98 insertions(+) create mode 100644 manifests/bases/kustomize-controller/kustomization.yaml create mode 100644 manifests/bases/source-controller/kustomization.yaml create mode 100644 manifests/install/kustomization.yaml create mode 100644 manifests/install/labels.yaml create mode 100644 manifests/install/namespace.yaml create mode 100644 manifests/policies/deny-ingress.yaml create mode 100644 manifests/policies/kustomization.yaml create mode 100644 manifests/rbac/cluster_role.yaml create mode 100644 manifests/rbac/kustomization.yaml create mode 100644 manifests/rbac/role.yaml diff --git a/manifests/bases/kustomize-controller/kustomization.yaml b/manifests/bases/kustomize-controller/kustomization.yaml new file mode 100644 index 00000000..9492b8d0 --- /dev/null +++ b/manifests/bases/kustomize-controller/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- github.com/fluxcd/kustomize-controller/config//crd?ref=v0.0.1-alpha.4 +- github.com/fluxcd/kustomize-controller/config//manager?ref=v0.0.1-alpha.4 diff --git a/manifests/bases/source-controller/kustomization.yaml b/manifests/bases/source-controller/kustomization.yaml new file mode 100644 index 00000000..39ede655 --- /dev/null +++ b/manifests/bases/source-controller/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- github.com/fluxcd/source-controller/config//crd?ref=v0.0.1-alpha.2 +- github.com/fluxcd/source-controller/config//manager?ref=v0.0.1-alpha.2 diff --git a/manifests/install/kustomization.yaml b/manifests/install/kustomization.yaml new file mode 100644 index 00000000..f5e991b0 --- /dev/null +++ b/manifests/install/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: gitops-system +resources: + - namespace.yaml + - ../bases/source-controller + - ../bases/kustomize-controller + - ../rbac + - ../policies +transformers: + - labels.yaml diff --git a/manifests/install/labels.yaml b/manifests/install/labels.yaml new file mode 100644 index 00000000..aecb8e3a --- /dev/null +++ b/manifests/install/labels.yaml @@ -0,0 +1,9 @@ +apiVersion: builtin +kind: LabelTransformer +metadata: + name: labels +labels: + app.kubernetes.io/instance: gitops-system +fieldSpecs: + - path: metadata/labels + create: true diff --git a/manifests/install/namespace.yaml b/manifests/install/namespace.yaml new file mode 100644 index 00000000..ab45ab3c --- /dev/null +++ b/manifests/install/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: gitops-system diff --git a/manifests/policies/deny-ingress.yaml b/manifests/policies/deny-ingress.yaml new file mode 100644 index 00000000..d9d0d0a3 --- /dev/null +++ b/manifests/policies/deny-ingress.yaml @@ -0,0 +1,8 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: deny-ingress +spec: + podSelector: {} + policyTypes: + - Ingress diff --git a/manifests/policies/kustomization.yaml b/manifests/policies/kustomization.yaml new file mode 100644 index 00000000..f535811d --- /dev/null +++ b/manifests/policies/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - deny-ingress.yaml diff --git a/manifests/rbac/cluster_role.yaml b/manifests/rbac/cluster_role.yaml new file mode 100644 index 00000000..9ce30d91 --- /dev/null +++ b/manifests/rbac/cluster_role.yaml @@ -0,0 +1,23 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cluster-reconciler +rules: + - apiGroups: ['*'] + resources: ['*'] + verbs: ['*'] + - nonResourceURLs: ['*'] + verbs: ['*'] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: cluster-reconciler +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-reconciler +subjects: + - kind: ServiceAccount + name: default + namespace: system diff --git a/manifests/rbac/kustomization.yaml b/manifests/rbac/kustomization.yaml new file mode 100644 index 00000000..ea165a8f --- /dev/null +++ b/manifests/rbac/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - cluster_role.yaml + - role.yaml diff --git a/manifests/rbac/role.yaml b/manifests/rbac/role.yaml new file mode 100644 index 00000000..4e79d185 --- /dev/null +++ b/manifests/rbac/role.yaml @@ -0,0 +1,24 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: crd-controller +rules: +- apiGroups: ['source.fluxcd.io'] + resources: ['*'] + verbs: ['*'] +- apiGroups: ['kustomize.fluxcd.io'] + resources: ['*'] + verbs: ['*'] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: crd-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: crd-controller +subjects: + - kind: ServiceAccount + name: default + namespace: system From 94e0b3c9c3de040d75805b3fd03f4abcd37a80b7 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Fri, 24 Apr 2020 18:18:41 +0300 Subject: [PATCH 02/15] Add install command with dry-run option - build kustomization - apply output on the cluster - check rollout status of source and kustomize controllers --- cmd/tk/install.go | 97 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 cmd/tk/install.go diff --git a/cmd/tk/install.go b/cmd/tk/install.go new file mode 100644 index 00000000..af455e96 --- /dev/null +++ b/cmd/tk/install.go @@ -0,0 +1,97 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "github.com/spf13/cobra" + "io" + "os" + "os/exec" + "strings" + "time" +) + +var installCmd = &cobra.Command{ + Use: "install", + Short: "Install the toolkit components", + Long: ` +The Install command deploys the toolkit components +on the configured Kubernetes cluster in ~/.kube/config`, + Example: ` install --manifests github.com/fluxcd/toolkit//manifests/install --dry-run`, + RunE: installCmdRun, +} + +var ( + installDryRun bool + installManifestsPath string + installNamespace string +) + +func init() { + installCmd.Flags().BoolVarP(&installDryRun, "dry-run", "", false, + "only print the object that would be applied") + installCmd.Flags().StringVarP(&installManifestsPath, "manifests", "", "", + "path to the manifest directory") + installCmd.Flags().StringVarP(&installNamespace, "namespace", "", "gitops-system", + "the namespace scope for this installation") + + rootCmd.AddCommand(installCmd) +} + +func installCmdRun(cmd *cobra.Command, args []string) error { + if installManifestsPath == "" { + return fmt.Errorf("no manifests specified") + } + + if !strings.HasPrefix(installManifestsPath, "github.com/") { + if _, err := os.Stat(installManifestsPath); err != nil { + return fmt.Errorf("manifests not found: %w", err) + } + } + + timeout := time.Minute * 5 + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + dryRun := "" + if installDryRun { + dryRun = "--dry-run=client" + } + command := fmt.Sprintf("kustomize build %s | kubectl apply -f- %s", + installManifestsPath, dryRun) + c := exec.CommandContext(ctx, "/bin/sh", "-c", command) + + var stdoutBuf, stderrBuf bytes.Buffer + c.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) + c.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) + + fmt.Println(`✚`, "installing...") + err := c.Run() + if err != nil { + fmt.Println(`✗`, "install failed") + os.Exit(1) + } + + if installDryRun { + fmt.Println(`✔`, "install dry-run finished") + return nil + } + + fmt.Println(`✚`, "verifying installation...") + for _, deployment := range []string{"source-controller", "kustomize-controller"} { + command = fmt.Sprintf("kubectl -n %s rollout status deployment %s --timeout=2m", + installNamespace, deployment) + c = exec.CommandContext(ctx, "/bin/sh", "-c", command) + c.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) + c.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) + err := c.Run() + if err != nil { + fmt.Println(`✗`, "install failed") + os.Exit(1) + } + } + + fmt.Println(`✔`, "install finished") + return nil +} From 403a0f2882c4e77a03a2a8ca707711f768cdebc3 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Fri, 24 Apr 2020 19:43:12 +0300 Subject: [PATCH 03/15] Add check command - verify minimum supported version of kubectl, kustomize and Kubernetes API --- cmd/tk/check.go | 98 ++++++++++++++++++++++++++++++++++++++++++++++--- go.mod | 1 + go.sum | 3 ++ 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/cmd/tk/check.go b/cmd/tk/check.go index 4ebb03ab..1a3f75c4 100644 --- a/cmd/tk/check.go +++ b/cmd/tk/check.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" + "github.com/blang/semver" "github.com/spf13/cobra" ) @@ -30,19 +31,24 @@ func init() { } func runCheckCmd(cmd *cobra.Command, args []string) error { - if !checkLocal() { + if !kubectlCheck(">=1.14.0") { os.Exit(1) } + + if !kustomizeCheck(">=3.5.0") { + os.Exit(1) + } + if checkPre { fmt.Println(`✔`, "all prerequisites checks passed") return nil } - if !checkRemote() { + if !kubernetesCheck(">=1.14.0") { os.Exit(1) - } else { - fmt.Println(`✔`, "all checks passed") } + + fmt.Println(`✔`, "all checks passed") return nil } @@ -57,10 +63,69 @@ func checkLocal() bool { fmt.Println(`✔`, cmd, "found") } } + return ok } -func checkRemote() bool { +func kubectlCheck(version string) bool { + _, err := exec.LookPath("kubectl") + if err != nil { + fmt.Println(`✗`, "kubectl not found") + return false + } + + output, err := execCommand("kubectl version --client --short | awk '{ print $3 }'") + if err != nil { + fmt.Println(`✗`, "kubectl version can't be determined") + return false + } + + v, err := semver.ParseTolerant(output) + if err != nil { + fmt.Println(`✗`, "kubectl version can't be determined") + return false + } + + rng, _ := semver.ParseRange(version) + if !rng(v) { + fmt.Println(`✗`, "kubectl version must be", version) + return false + } + + fmt.Println(`✔`, "kubectl", v.String()) + return true +} + +func kustomizeCheck(version string) bool { + _, err := exec.LookPath("kustomize") + if err != nil { + fmt.Println(`✗`, "kustomize not found") + return false + } + + output, err := execCommand("kustomize version --short | awk '{ print $1 }' | cut -c2-") + if err != nil { + fmt.Println(`✗`, "kustomize version can't be determined") + return false + } + + v, err := semver.ParseTolerant(output) + if err != nil { + fmt.Println(`✗`, "kustomize version can't be determined") + return false + } + + rng, _ := semver.ParseRange(version) + if !rng(v) { + fmt.Println(`✗`, "kustomize version must be", version) + return false + } + + fmt.Println(`✔`, "kustomize", v.String()) + return true +} + +func kubernetesCheck(version string) bool { client, err := NewKubernetesClient() if err != nil { fmt.Println(`✗`, "kubernetes client initialization failed", err.Error()) @@ -73,6 +138,27 @@ func checkRemote() bool { return false } - fmt.Println(`✔`, "kubernetes version", ver.String()) + v, err := semver.ParseTolerant(ver.String()) + if err != nil { + fmt.Println(`✗`, "kubernetes version can't be determined") + return false + } + + rng, _ := semver.ParseRange(version) + if !rng(v) { + fmt.Println(`✗`, "kubernetes version must be", version) + return false + } + + fmt.Println(`✔`, "kubernetes", v.String()) return true } + +func execCommand(command string) (string, error) { + c := exec.Command("/bin/sh", "-c", command) + output, err := c.CombinedOutput() + if err != nil { + return "", err + } + return string(output), nil +} diff --git a/go.mod b/go.mod index dedccacb..b95b3c7b 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/fluxcd/toolkit go 1.14 require ( + github.com/blang/semver v3.5.1+incompatible github.com/spf13/cobra v0.0.6 k8s.io/client-go v0.18.0 ) diff --git a/go.sum b/go.sum index 4ebe84a9..7a5d5664 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,9 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/blang/semver v1.1.0 h1:ol1rO7QQB5uy7umSNV7VAmLugfLRD+17sYJujRNYPhg= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= From 35163fa9eb72dfa282b9fec6c1c9c4ac7c631800 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Fri, 24 Apr 2020 19:54:45 +0300 Subject: [PATCH 04/15] Add e2e tests for install command --- .github/actions/kustomize/Dockerfile | 6 ++++ .github/actions/kustomize/action.yml | 9 +++++ .github/actions/kustomize/entrypoint.sh | 12 +++++++ .github/workflows/e2e.yaml | 46 +++++++++++++++++++++++++ Makefile | 16 +++++---- README.md | 9 ++++- go.sum | 1 - 7 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 .github/actions/kustomize/Dockerfile create mode 100644 .github/actions/kustomize/action.yml create mode 100644 .github/actions/kustomize/entrypoint.sh create mode 100644 .github/workflows/e2e.yaml diff --git a/.github/actions/kustomize/Dockerfile b/.github/actions/kustomize/Dockerfile new file mode 100644 index 00000000..2ebd6334 --- /dev/null +++ b/.github/actions/kustomize/Dockerfile @@ -0,0 +1,6 @@ +FROM giantswarm/tiny-tools + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/.github/actions/kustomize/action.yml b/.github/actions/kustomize/action.yml new file mode 100644 index 00000000..bd53d1d7 --- /dev/null +++ b/.github/actions/kustomize/action.yml @@ -0,0 +1,9 @@ +name: 'kustomize' +description: 'A GitHub Action to run kustomize commands' +author: 'Stefan Prodan' +branding: + icon: 'command' + color: 'blue' +runs: + using: 'docker' + image: 'Dockerfile' diff --git a/.github/actions/kustomize/entrypoint.sh b/.github/actions/kustomize/entrypoint.sh new file mode 100644 index 00000000..b9d560c9 --- /dev/null +++ b/.github/actions/kustomize/entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh -l + +VERSION=3.5.4 +curl -sL https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${VERSION}/kustomize_v${VERSION}_linux_amd64.tar.gz | tar xz + +mkdir -p $GITHUB_WORKSPACE/bin +cp ./kustomize $GITHUB_WORKSPACE/bin +chmod +x $GITHUB_WORKSPACE/bin/kustomize +ls -lh $GITHUB_WORKSPACE/bin + +echo "::add-path::$GITHUB_WORKSPACE/bin" +echo "::add-path::$RUNNER_WORKSPACE/$(basename $GITHUB_REPOSITORY)/bin" diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml new file mode 100644 index 00000000..6b1dae6e --- /dev/null +++ b/.github/workflows/e2e.yaml @@ -0,0 +1,46 @@ +name: e2e + +on: + pull_request: + push: + branches: + - master + +jobs: + kind: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Restore Go cache + uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: Setup Go + uses: actions/setup-go@v2-beta + with: + go-version: 1.14.x + - name: Setup Kubernetes + uses: engineerd/setup-kind@v0.3.0 + - name: Setup Kustomize + uses: ./.github/actions/kustomize + - name: Run test + run: make test + - name: Check if working tree is dirty + run: | + if [[ $(git diff --stat) != '' ]]; then + echo 'run make test and commit changes' + exit 1 + fi + - name: Build + run: sudo go build -o ./bin/tk ./cmd/tk + - name: Run integration tests + run: | + ./bin/tk install --manifests ./manifests/install/ + - name: Debug failure + if: failure() + run: | + kubectl -n gitops-system get all diff --git a/Makefile b/Makefile index 2ada45ec..b5c78f2c 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,9 @@ VERSION?=$(shell grep 'VERSION' cmd/tk/main.go | awk '{ print $$4 }' | tr -d '"') -all: tidy fmt vet test build +all: test build -build: - CGO_ENABLED=0 go build -o ./bin/tk ./cmd/tk +tidy: + go mod tidy fmt: go fmt ./... @@ -11,9 +11,11 @@ fmt: vet: go vet ./... -tidy: - go mod tidy - -test: +test: tidy fmt vet go test ./... -coverprofile cover.out +build: + CGO_ENABLED=0 go build -o ./bin/tk ./cmd/tk + +install: + go install cmd/tk diff --git a/README.md b/README.md index c2765f6a..9a2d2859 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,9 @@ # toolkit -Experimental toolkit for assembling CD pipelines + +[![e2e](https://github.com/fluxcd/toolkit/workflows/e2e/badge.svg)](https://github.com/fluxcd/toolkit/actions) + +Experimental toolkit for assembling CD pipelines. + +Components: +* [source-controller](https://github.com/fluxcd/source-controller) +* [kustomize-controller](https://github.com/fluxcd/kustomize-controller) diff --git a/go.sum b/go.sum index 7a5d5664..333dd9c9 100644 --- a/go.sum +++ b/go.sum @@ -26,7 +26,6 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/blang/semver v1.1.0 h1:ol1rO7QQB5uy7umSNV7VAmLugfLRD+17sYJujRNYPhg= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= From bba9ac0aa17230734fe665a2a6e92996cedc0741 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Fri, 24 Apr 2020 20:39:04 +0300 Subject: [PATCH 05/15] Add ssh tools to requirements check --- .github/workflows/e2e.yaml | 3 +++ cmd/tk/check.go | 25 +++++++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 6b1dae6e..c1c32110 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -39,8 +39,11 @@ jobs: run: sudo go build -o ./bin/tk ./cmd/tk - name: Run integration tests run: | + ./bin/tk check ./bin/tk install --manifests ./manifests/install/ - name: Debug failure if: failure() run: | + kubectl version --client --short + kustomize version --short kubectl -n gitops-system get all diff --git a/cmd/tk/check.go b/cmd/tk/check.go index 1a3f75c4..a016e941 100644 --- a/cmd/tk/check.go +++ b/cmd/tk/check.go @@ -4,14 +4,15 @@ import ( "fmt" "os" "os/exec" + "strings" "github.com/blang/semver" "github.com/spf13/cobra" ) var checkCmd = &cobra.Command{ - Use: "check --pre", - Short: "Check for potential problems", + Use: "check", + Short: "Check requirements", Long: ` The check command will perform a series of checks to validate that the local environment is configured correctly.`, @@ -31,6 +32,10 @@ func init() { } func runCheckCmd(cmd *cobra.Command, args []string) error { + if !sshCheck() { + os.Exit(1) + } + if !kubectlCheck(">=1.14.0") { os.Exit(1) } @@ -52,9 +57,9 @@ func runCheckCmd(cmd *cobra.Command, args []string) error { return nil } -func checkLocal() bool { +func sshCheck() bool { ok := true - for _, cmd := range []string{"kubectl", "kustomize"} { + for _, cmd := range []string{"ssh-keygen", "ssh-keyscan"} { _, err := exec.LookPath(cmd) if err != nil { fmt.Println(`✗`, cmd, "not found") @@ -82,7 +87,7 @@ func kubectlCheck(version string) bool { v, err := semver.ParseTolerant(output) if err != nil { - fmt.Println(`✗`, "kubectl version can't be determined") + fmt.Println(`✗`, "kubectl version can't be parsed") return false } @@ -109,9 +114,17 @@ func kustomizeCheck(version string) bool { return false } + if strings.Contains(output, "kustomize/") { + output, err = execCommand("kustomize version --short | awk '{ print $1 }' | cut -c12-") + if err != nil { + fmt.Println(`✗`, "kustomize version can't be determined") + return false + } + } + v, err := semver.ParseTolerant(output) if err != nil { - fmt.Println(`✗`, "kustomize version can't be determined") + fmt.Println(`✗`, "kustomize version can't be parsed") return false } From 1371b4d4d3bd4a30dbec9d73dd75e7120d4393ec Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Fri, 24 Apr 2020 22:32:17 +0300 Subject: [PATCH 06/15] Implement create source for ssh git repos - generate host keys and SSH keys - prompt for deploy key setup - generate gitrepo source - wait for source to sync --- cmd/tk/check.go | 11 +-- cmd/tk/create.go | 19 +++++ cmd/tk/create_source.go | 156 ++++++++++++++++++++++++++++++++++++++++ cmd/tk/install.go | 5 +- cmd/tk/main.go | 17 ++++- go.mod | 1 + go.sum | 17 +++++ 7 files changed, 210 insertions(+), 16 deletions(-) create mode 100644 cmd/tk/create.go create mode 100644 cmd/tk/create_source.go diff --git a/cmd/tk/check.go b/cmd/tk/check.go index a016e941..be552e22 100644 --- a/cmd/tk/check.go +++ b/cmd/tk/check.go @@ -139,7 +139,7 @@ func kustomizeCheck(version string) bool { } func kubernetesCheck(version string) bool { - client, err := NewKubernetesClient() + client, err := kubernetesClient() if err != nil { fmt.Println(`✗`, "kubernetes client initialization failed", err.Error()) return false @@ -166,12 +166,3 @@ func kubernetesCheck(version string) bool { fmt.Println(`✔`, "kubernetes", v.String()) return true } - -func execCommand(command string) (string, error) { - c := exec.Command("/bin/sh", "-c", command) - output, err := c.CombinedOutput() - if err != nil { - return "", err - } - return string(output), nil -} diff --git a/cmd/tk/create.go b/cmd/tk/create.go new file mode 100644 index 00000000..f63c1c5f --- /dev/null +++ b/cmd/tk/create.go @@ -0,0 +1,19 @@ +package main + +import ( + "github.com/spf13/cobra" +) + +var createCmd = &cobra.Command{ + Use: "create", + Short: "Create commands", +} + +var ( + interval string +) + +func init() { + createCmd.PersistentFlags().StringVar(&interval, "interval", "1m", "source sync interval") + rootCmd.AddCommand(createCmd) +} diff --git a/cmd/tk/create_source.go b/cmd/tk/create_source.go new file mode 100644 index 00000000..8d73f628 --- /dev/null +++ b/cmd/tk/create_source.go @@ -0,0 +1,156 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "net/url" + "os" + "text/template" + + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" +) + +var createSourceCmd = &cobra.Command{ + Use: "source [name]", + Short: "Create source resource", + Long: ` +The create source command generates a source.fluxcd.io resource and waits for it to sync. +If a Git repository is specified, it will create a SSH deploy key.`, + Example: ` create source podinfo --git-url ssh://git@github.com/stefanprodan/podinfo-deploy`, + RunE: createSourceCmdRun, +} + +var ( + sourceGitURL string + sourceGitBranch string +) + +func init() { + createSourceCmd.Flags().StringVar(&sourceGitURL, "git-url", "", "git SSH address, in the format ssh://git@host/org/repository") + createSourceCmd.Flags().StringVar(&sourceGitBranch, "git-branch", "master", "git branch") + + createCmd.AddCommand(createSourceCmd) +} + +func createSourceCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("source name is required") + } + name := args[0] + + if sourceGitURL == "" { + return fmt.Errorf("git-url is required") + } + + tmpDir, err := ioutil.TempDir("", name) + if err != nil { + return err + } + defer os.RemoveAll(tmpDir) + + u, err := url.Parse(sourceGitURL) + if err != nil { + return fmt.Errorf("git URL parse failed: %w", err) + } + + fmt.Println(`✚`, "generating host key for", u.Host) + + keyscan := fmt.Sprintf("ssh-keyscan %s > %s/known_hosts", u.Host, tmpDir) + if output, err := execCommand(keyscan); err != nil { + return fmt.Errorf("ssh-keyscan failed: %s", output) + } + + fmt.Println(`✚`, "generating deploy key") + + keygen := fmt.Sprintf("ssh-keygen -b 2048 -t rsa -f %s/identity -q -N \"\"", tmpDir) + if output, err := execCommand(keygen); err != nil { + return fmt.Errorf("ssh-keygen failed: %s", output) + } + + deployKey, err := execCommand(fmt.Sprintf("cat %s/identity.pub", tmpDir)) + if err != nil { + return fmt.Errorf("unable to read identity.pub: %w", err) + } + + fmt.Print(deployKey) + prompt := promptui.Prompt{ + Label: "Have you added the deploy key to your repository", + IsConfirm: true, + } + if _, err := prompt.Run(); err != nil { + fmt.Println(`✗`, "aborting") + return nil + } + + fmt.Println(`✚`, "saving deploy key") + 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 output, err := execCommand(secret); err != nil { + return fmt.Errorf("kubectl create secret failed: %s", output) + } else { + fmt.Print(output) + } + + fmt.Println(`✚`, "generating source resource") + + t, err := template.New("tmpl").Parse(gitSource) + if err != nil { + return fmt.Errorf("template parse error: %w", err) + } + + source := struct { + Name string + Namespace string + GitURL string + Interval string + }{ + Name: name, + Namespace: namespace, + GitURL: sourceGitURL, + Interval: interval, + } + + var data bytes.Buffer + writer := bufio.NewWriter(&data) + if err := t.Execute(writer, source); err != nil { + return fmt.Errorf("template execution failed: %w", err) + } + if err := writer.Flush(); err != nil { + return fmt.Errorf("source flush failed: %w", err) + } + + if output, err := execCommand(fmt.Sprintf("echo '%s' | kubectl apply -f-", data.String())); err != nil { + return fmt.Errorf("kubectl create source failed: %s", output) + } else { + fmt.Print(output) + } + + fmt.Println(`✚`, "waiting for source sync") + if output, err := execCommand(fmt.Sprintf( + "kubectl -n %s wait gitrepository/%s --for=condition=ready --timeout=1m", + namespace, name)); err != nil { + return fmt.Errorf("source sync failed: %s", output) + } else { + fmt.Print(output) + } + + return nil +} + +var gitSource = `--- +apiVersion: source.fluxcd.io/v1alpha1 +kind: GitRepository +metadata: + name: {{.Name}} + namespace: {{.Namespace}} +spec: + interval: {{.Interval}} + url: {{.GitURL}} + secretRef: + name: {{.Name}} +` diff --git a/cmd/tk/install.go b/cmd/tk/install.go index af455e96..f7244987 100644 --- a/cmd/tk/install.go +++ b/cmd/tk/install.go @@ -25,7 +25,6 @@ on the configured Kubernetes cluster in ~/.kube/config`, var ( installDryRun bool installManifestsPath string - installNamespace string ) func init() { @@ -33,8 +32,6 @@ func init() { "only print the object that would be applied") installCmd.Flags().StringVarP(&installManifestsPath, "manifests", "", "", "path to the manifest directory") - installCmd.Flags().StringVarP(&installNamespace, "namespace", "", "gitops-system", - "the namespace scope for this installation") rootCmd.AddCommand(installCmd) } @@ -81,7 +78,7 @@ func installCmdRun(cmd *cobra.Command, args []string) error { fmt.Println(`✚`, "verifying installation...") for _, deployment := range []string{"source-controller", "kustomize-controller"} { command = fmt.Sprintf("kubectl -n %s rollout status deployment %s --timeout=2m", - installNamespace, deployment) + namespace, deployment) c = exec.CommandContext(ctx, "/bin/sh", "-c", command) c.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) c.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) diff --git a/cmd/tk/main.go b/cmd/tk/main.go index e8542631..3c6ac728 100644 --- a/cmd/tk/main.go +++ b/cmd/tk/main.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "os" + "os/exec" "path/filepath" "strings" @@ -23,6 +24,7 @@ var rootCmd = &cobra.Command{ var ( kubeconfig string + namespace string ) func init() { @@ -30,9 +32,11 @@ func init() { rootCmd.PersistentFlags().StringVarP(&kubeconfig, "kubeconfig", "", filepath.Join(home, ".kube", "config"), "path to the kubeconfig file") } else { - checkCmd.PersistentFlags().StringVarP(&kubeconfig, "kubeconfig", "", "", + rootCmd.PersistentFlags().StringVarP(&kubeconfig, "kubeconfig", "", "", "absolute path to the kubeconfig file") } + rootCmd.PersistentFlags().StringVarP(&namespace, "namespace", "", "gitops-system", + "the namespace scope for this operation") } func main() { @@ -53,7 +57,7 @@ func homeDir() string { return os.Getenv("USERPROFILE") // windows } -func NewKubernetesClient() (*kubernetes.Clientset, error) { +func kubernetesClient() (*kubernetes.Clientset, error) { config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) if err != nil { return nil, err @@ -66,3 +70,12 @@ func NewKubernetesClient() (*kubernetes.Clientset, error) { return client, nil } + +func execCommand(command string) (string, error) { + c := exec.Command("/bin/sh", "-c", command) + output, err := c.CombinedOutput() + if err != nil { + return "", err + } + return string(output), nil +} diff --git a/go.mod b/go.mod index b95b3c7b..826f0689 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.14 require ( github.com/blang/semver v3.5.1+incompatible + github.com/manifoldco/promptui v0.7.0 github.com/spf13/cobra v0.0.6 k8s.io/client-go v0.18.0 ) diff --git a/go.sum b/go.sum index 333dd9c9..221b5a3e 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,12 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -109,6 +115,8 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= @@ -120,8 +128,16 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4= +github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -228,6 +244,7 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= From 9cec671f6c7c9c56b86a85cc54274cd243b180e6 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Sat, 25 Apr 2020 00:02:09 +0300 Subject: [PATCH 07/15] Add public repos and semver support to create cmd --- cmd/tk/create_source.go | 143 ++++++++++++++++++++++++++-------------- cmd/tk/main.go | 23 +++---- 2 files changed, 104 insertions(+), 62 deletions(-) diff --git a/cmd/tk/create_source.go b/cmd/tk/create_source.go index 8d73f628..22cb42a7 100644 --- a/cmd/tk/create_source.go +++ b/cmd/tk/create_source.go @@ -4,9 +4,12 @@ import ( "bufio" "bytes" "fmt" + "io" "io/ioutil" "net/url" "os" + "os/exec" + "strings" "text/template" "github.com/manifoldco/promptui" @@ -18,19 +21,29 @@ var createSourceCmd = &cobra.Command{ Short: "Create source resource", Long: ` The create source command generates a source.fluxcd.io resource and waits for it to sync. -If a Git repository is specified, it will create a SSH deploy key.`, - Example: ` create source podinfo --git-url ssh://git@github.com/stefanprodan/podinfo-deploy`, - RunE: createSourceCmdRun, +For Git over SSH, host and SSH keys are automatically generated.`, + Example: ` # Create a gitrepository.source.fluxcd.io for a public repository + create source podinfo --git-url https://github.com/stefanprodan/podinfo-deploy --git-branch master + + # Create a gitrepository.source.fluxcd.io that syncs tags based on a semver range + create source podinfo --git-url https://github.com/stefanprodan/podinfo-deploy --git-semver=">=0.0.1-rc.1 <0.1.0" + + # Create a gitrepository.source.fluxcd.io with SSH authentication + create source podinfo --git-url ssh://git@github.com/stefanprodan/podinfo-deploy +`, + RunE: createSourceCmdRun, } var ( sourceGitURL string sourceGitBranch string + sourceGitSemver string ) func init() { - createSourceCmd.Flags().StringVar(&sourceGitURL, "git-url", "", "git SSH address, in the format ssh://git@host/org/repository") + createSourceCmd.Flags().StringVar(&sourceGitURL, "git-url", "", "git address, e.g. ssh://git@host/org/repository") createSourceCmd.Flags().StringVar(&sourceGitBranch, "git-branch", "master", "git branch") + createSourceCmd.Flags().StringVar(&sourceGitSemver, "git-semver", "", "git tag semver range") createCmd.AddCommand(createSourceCmd) } @@ -56,44 +69,11 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("git URL parse failed: %w", err) } - fmt.Println(`✚`, "generating host key for", u.Host) - - keyscan := fmt.Sprintf("ssh-keyscan %s > %s/known_hosts", u.Host, tmpDir) - if output, err := execCommand(keyscan); err != nil { - return fmt.Errorf("ssh-keyscan failed: %s", output) - } - - fmt.Println(`✚`, "generating deploy key") - - keygen := fmt.Sprintf("ssh-keygen -b 2048 -t rsa -f %s/identity -q -N \"\"", tmpDir) - if output, err := execCommand(keygen); err != nil { - return fmt.Errorf("ssh-keygen failed: %s", output) - } - - deployKey, err := execCommand(fmt.Sprintf("cat %s/identity.pub", tmpDir)) - if err != nil { - return fmt.Errorf("unable to read identity.pub: %w", err) - } - - fmt.Print(deployKey) - prompt := promptui.Prompt{ - Label: "Have you added the deploy key to your repository", - IsConfirm: true, - } - if _, err := prompt.Run(); err != nil { - fmt.Println(`✗`, "aborting") - return nil - } - - fmt.Println(`✚`, "saving deploy key") - 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 output, err := execCommand(secret); err != nil { - return fmt.Errorf("kubectl create secret failed: %s", output) - } else { - fmt.Print(output) + isSSH := strings.HasPrefix(sourceGitURL, "ssh") + if isSSH { + if err := generateSSH(name, u.Host, tmpDir); err != nil { + return err + } } fmt.Println(`✚`, "generating source resource") @@ -106,13 +86,19 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error { source := struct { Name string Namespace string - GitURL string + URL string + Branch string + Semver string Interval string + IsSSH bool }{ Name: name, Namespace: namespace, - GitURL: sourceGitURL, + URL: sourceGitURL, + Branch: sourceGitBranch, + Semver: sourceGitSemver, Interval: interval, + IsSSH: isSSH, } var data bytes.Buffer @@ -123,11 +109,19 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error { if err := writer.Flush(); err != nil { return fmt.Errorf("source flush failed: %w", err) } + fmt.Print(data.String()) - if output, err := execCommand(fmt.Sprintf("echo '%s' | kubectl apply -f-", data.String())); err != nil { - return fmt.Errorf("kubectl create source failed: %s", output) - } else { - fmt.Print(output) + command := fmt.Sprintf("echo '%s' | kubectl apply -f-", data.String()) + c := exec.Command("/bin/sh", "-c", command) + + var stdoutBuf, stderrBuf bytes.Buffer + c.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) + c.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) + + err = c.Run() + if err != nil { + fmt.Println(`✗`, "source apply failed") + os.Exit(1) } fmt.Println(`✚`, "waiting for source sync") @@ -142,6 +136,49 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error { return nil } +func generateSSH(name, host, tmpDir string) error { + fmt.Println(`✚`, "generating host key for", host) + + keyscan := fmt.Sprintf("ssh-keyscan %s > %s/known_hosts", host, tmpDir) + if output, err := execCommand(keyscan); err != nil { + return fmt.Errorf("ssh-keyscan failed: %s", output) + } + + fmt.Println(`✚`, "generating deploy key") + + keygen := fmt.Sprintf("ssh-keygen -b 2048 -t rsa -f %s/identity -q -N \"\"", tmpDir) + if output, err := execCommand(keygen); err != nil { + return fmt.Errorf("ssh-keygen failed: %s", output) + } + + deployKey, err := execCommand(fmt.Sprintf("cat %s/identity.pub", tmpDir)) + if err != nil { + return fmt.Errorf("unable to read identity.pub: %w", err) + } + + fmt.Print(deployKey) + prompt := promptui.Prompt{ + Label: "Have you added the deploy key to your repository", + IsConfirm: true, + } + if _, err := prompt.Run(); err != nil { + fmt.Println(`✗`, "aborting") + os.Exit(1) + } + + fmt.Println(`✚`, "saving deploy key") + 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 output, err := execCommand(secret); err != nil { + return fmt.Errorf("kubectl create secret failed: %s", output) + } else { + fmt.Print(output) + } + return nil +} + var gitSource = `--- apiVersion: source.fluxcd.io/v1alpha1 kind: GitRepository @@ -150,7 +187,15 @@ metadata: namespace: {{.Namespace}} spec: interval: {{.Interval}} - url: {{.GitURL}} + url: {{.URL}} + ref: +{{- if .Semver }} + semver: "{{.Semver}}" +{{- else }} + branch: {{.Branch}} +{{- end }} +{{- if .IsSSH }} secretRef: name: {{.Name}} +{{- end }} ` diff --git a/cmd/tk/main.go b/cmd/tk/main.go index 3c6ac728..af1e8423 100644 --- a/cmd/tk/main.go +++ b/cmd/tk/main.go @@ -2,24 +2,24 @@ package main import ( "fmt" - "log" - "os" - "os/exec" - "path/filepath" - "strings" - "github.com/spf13/cobra" "k8s.io/client-go/kubernetes" _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/client-go/tools/clientcmd" + "log" + "os" + "os/exec" + "path/filepath" ) var VERSION = "0.0.1" var rootCmd = &cobra.Command{ - Use: "tk", - Short: "Kubernetes CD assembler", - Version: VERSION, + Use: "tk", + Short: "Kubernetes CD assembler", + Version: VERSION, + SilenceUsage: true, + SilenceErrors: true, } var ( @@ -41,11 +41,8 @@ func init() { func main() { log.SetFlags(0) - - rootCmd.SetArgs(os.Args[1:]) if err := rootCmd.Execute(); err != nil { - e := err.Error() - fmt.Println(strings.ToUpper(e[:1]) + e[1:]) + fmt.Println(err) os.Exit(1) } } From 63b58e7dad53747e11632049183eb709880c261e Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Sat, 25 Apr 2020 00:02:33 +0300 Subject: [PATCH 08/15] Add e2e test for create source cmd --- .github/workflows/e2e.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index c1c32110..699015f2 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -41,6 +41,7 @@ jobs: run: | ./bin/tk check ./bin/tk install --manifests ./manifests/install/ + ./bin/tk create source podinfo --git-url https://github.com/stefanprodan/podinfo-deploy --git-semver=">=0.0.1-rc.1 <0.1.0" - name: Debug failure if: failure() run: | From 4d8800ad62dce61e8caa2f5fa52844275bcf13a6 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Sat, 25 Apr 2020 10:29:54 +0300 Subject: [PATCH 09/15] Add basic-auth option to create source --- cmd/tk/create_source.go | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/cmd/tk/create_source.go b/cmd/tk/create_source.go index 22cb42a7..c6226e0f 100644 --- a/cmd/tk/create_source.go +++ b/cmd/tk/create_source.go @@ -30,6 +30,9 @@ For Git over SSH, host and SSH keys are automatically generated.`, # Create a gitrepository.source.fluxcd.io with SSH authentication create source podinfo --git-url ssh://git@github.com/stefanprodan/podinfo-deploy + + # Create a gitrepository.source.fluxcd.io with basic authentication + create source podinfo --git-url https://github.com/stefanprodan/podinfo-deploy -u username -p password `, RunE: createSourceCmdRun, } @@ -38,13 +41,16 @@ var ( sourceGitURL string sourceGitBranch string sourceGitSemver string + sourceUsername string + sourcePassword string ) func init() { createSourceCmd.Flags().StringVar(&sourceGitURL, "git-url", "", "git address, e.g. ssh://git@host/org/repository") createSourceCmd.Flags().StringVar(&sourceGitBranch, "git-branch", "master", "git branch") createSourceCmd.Flags().StringVar(&sourceGitSemver, "git-semver", "", "git tag semver range") - + createSourceCmd.Flags().StringVarP(&sourceUsername, "username", "u", "", "basic authentication username") + createSourceCmd.Flags().StringVarP(&sourcePassword, "password", "p", "", "basic authentication password") createCmd.AddCommand(createSourceCmd) } @@ -69,11 +75,17 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("git URL parse failed: %w", err) } - isSSH := strings.HasPrefix(sourceGitURL, "ssh") - if isSSH { + withAuth := false + if strings.HasPrefix(sourceGitURL, "ssh") { if err := generateSSH(name, u.Host, tmpDir); err != nil { return err } + withAuth = true + } else if sourceUsername != "" && sourcePassword != "" { + if err := generateBasicAuth(name); err != nil { + return err + } + withAuth = true } fmt.Println(`✚`, "generating source resource") @@ -90,7 +102,7 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error { Branch string Semver string Interval string - IsSSH bool + WithAuth bool }{ Name: name, Namespace: namespace, @@ -98,7 +110,7 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error { Branch: sourceGitBranch, Semver: sourceGitSemver, Interval: interval, - IsSSH: isSSH, + WithAuth: withAuth, } var data bytes.Buffer @@ -136,6 +148,20 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error { return nil } +func generateBasicAuth(name string) error { + fmt.Println(`✚`, "saving credentials") + credentials := fmt.Sprintf("--from-literal=username='%s' --from-literal=password='%s'", + sourceUsername, sourcePassword) + secret := fmt.Sprintf("kubectl -n %s create secret generic %s %s --dry-run=client -oyaml | kubectl apply -f-", + namespace, name, credentials) + if output, err := execCommand(secret); err != nil { + return fmt.Errorf("kubectl create secret failed: %s", output) + } else { + fmt.Print(output) + } + return nil +} + func generateSSH(name, host, tmpDir string) error { fmt.Println(`✚`, "generating host key for", host) @@ -194,7 +220,7 @@ spec: {{- else }} branch: {{.Branch}} {{- end }} -{{- if .IsSSH }} +{{- if .WithAuth }} secretRef: name: {{.Name}} {{- end }} From 7d1a14d09ae0c79b012a09416d71c25291c32c97 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Sat, 25 Apr 2020 11:11:43 +0300 Subject: [PATCH 10/15] Add uninstall cmd --- cmd/tk/uninstall.go | 81 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 cmd/tk/uninstall.go diff --git a/cmd/tk/uninstall.go b/cmd/tk/uninstall.go new file mode 100644 index 00000000..9062bb2b --- /dev/null +++ b/cmd/tk/uninstall.go @@ -0,0 +1,81 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "os/exec" + "time" + + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" +) + +var uninstallCmd = &cobra.Command{ + Use: "uninstall", + Short: "Uninstall the toolkit components", + Long: ` +The uninstall command removes the namespace, cluster roles, +cluster role bindings and CRDs`, + Example: ` uninstall --namespace=gitops-system --crds --dry-run`, + RunE: uninstallCmdRun, +} + +var ( + uninstallCRDs bool + uninstallDryRun bool +) + +func init() { + uninstallCmd.Flags().BoolVarP(&uninstallCRDs, "crds", "", false, + "removes all CRDs previously installed") + uninstallCmd.Flags().BoolVarP(&uninstallDryRun, "dry-run", "", false, + "only print the object that would be deleted") + + rootCmd.AddCommand(uninstallCmd) +} + +func uninstallCmdRun(cmd *cobra.Command, args []string) error { + timeout := time.Minute * 5 + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + dryRun := "" + if uninstallDryRun { + dryRun = "--dry-run=client" + } else { + prompt := promptui.Prompt{ + Label: fmt.Sprintf("Are you sure you want to delete the %s namespace", namespace), + IsConfirm: true, + } + if _, err := prompt.Run(); err != nil { + fmt.Println(`✗`, "aborting") + os.Exit(1) + } + } + + kinds := "namespace,clusterroles,clusterrolebindings" + if uninstallCRDs { + kinds += ",crds" + } + + command := fmt.Sprintf("kubectl delete %s -l app.kubernetes.io/instance=%s %s", + kinds, namespace, dryRun) + c := exec.CommandContext(ctx, "/bin/sh", "-c", command) + + var stdoutBuf, stderrBuf bytes.Buffer + c.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) + c.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) + + fmt.Println(`✚`, "uninstalling...") + err := c.Run() + if err != nil { + fmt.Println(`✗`, "uninstall failed") + os.Exit(1) + } + + fmt.Println(`✔`, "uninstall finished") + return nil +} From e308097785e5422f8e6fe4d8e2af815bd8d44d9e Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Sat, 25 Apr 2020 11:50:42 +0300 Subject: [PATCH 11/15] Add timeout option to install/uninstall --- cmd/tk/install.go | 9 ++++----- cmd/tk/main.go | 13 +++++++++---- cmd/tk/uninstall.go | 6 ++---- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/cmd/tk/install.go b/cmd/tk/install.go index f7244987..ee3df1b1 100644 --- a/cmd/tk/install.go +++ b/cmd/tk/install.go @@ -4,12 +4,12 @@ import ( "bytes" "context" "fmt" - "github.com/spf13/cobra" "io" "os" "os/exec" "strings" - "time" + + "github.com/spf13/cobra" ) var installCmd = &cobra.Command{ @@ -47,7 +47,6 @@ func installCmdRun(cmd *cobra.Command, args []string) error { } } - timeout := time.Minute * 5 ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() @@ -77,8 +76,8 @@ func installCmdRun(cmd *cobra.Command, args []string) error { fmt.Println(`✚`, "verifying installation...") for _, deployment := range []string{"source-controller", "kustomize-controller"} { - command = fmt.Sprintf("kubectl -n %s rollout status deployment %s --timeout=2m", - namespace, deployment) + command = fmt.Sprintf("kubectl -n %s rollout status deployment %s --timeout=%s", + namespace, deployment, timeout.String()) c = exec.CommandContext(ctx, "/bin/sh", "-c", command) c.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) c.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) diff --git a/cmd/tk/main.go b/cmd/tk/main.go index af1e8423..c0ceb44c 100644 --- a/cmd/tk/main.go +++ b/cmd/tk/main.go @@ -2,14 +2,16 @@ package main import ( "fmt" - "github.com/spf13/cobra" - "k8s.io/client-go/kubernetes" - _ "k8s.io/client-go/plugin/pkg/client/auth" - "k8s.io/client-go/tools/clientcmd" "log" "os" "os/exec" "path/filepath" + "time" + + "github.com/spf13/cobra" + "k8s.io/client-go/kubernetes" + _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/client-go/tools/clientcmd" ) var VERSION = "0.0.1" @@ -25,6 +27,7 @@ var rootCmd = &cobra.Command{ var ( kubeconfig string namespace string + timeout time.Duration ) func init() { @@ -37,6 +40,8 @@ func init() { } rootCmd.PersistentFlags().StringVarP(&namespace, "namespace", "", "gitops-system", "the namespace scope for this operation") + rootCmd.PersistentFlags().DurationVarP(&timeout, "timeout", "", 5*time.Minute, + "timeout for this operation") } func main() { diff --git a/cmd/tk/uninstall.go b/cmd/tk/uninstall.go index 9062bb2b..1459d011 100644 --- a/cmd/tk/uninstall.go +++ b/cmd/tk/uninstall.go @@ -7,7 +7,6 @@ import ( "io" "os" "os/exec" - "time" "github.com/manifoldco/promptui" "github.com/spf13/cobra" @@ -38,7 +37,6 @@ func init() { } func uninstallCmdRun(cmd *cobra.Command, args []string) error { - timeout := time.Minute * 5 ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() @@ -61,8 +59,8 @@ func uninstallCmdRun(cmd *cobra.Command, args []string) error { kinds += ",crds" } - command := fmt.Sprintf("kubectl delete %s -l app.kubernetes.io/instance=%s %s", - kinds, namespace, dryRun) + command := fmt.Sprintf("kubectl delete %s -l app.kubernetes.io/instance=%s --timeout=%s %s", + kinds, namespace, timeout.String(), dryRun) c := exec.CommandContext(ctx, "/bin/sh", "-c", command) var stdoutBuf, stderrBuf bytes.Buffer From 13760258ab6022847e127f26effa02a458af92ea Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Sat, 25 Apr 2020 12:06:25 +0300 Subject: [PATCH 12/15] Add verbose option to create source --- cmd/tk/create_source.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/tk/create_source.go b/cmd/tk/create_source.go index c6226e0f..c3cb26cd 100644 --- a/cmd/tk/create_source.go +++ b/cmd/tk/create_source.go @@ -43,6 +43,7 @@ var ( sourceGitSemver string sourceUsername string sourcePassword string + sourceVerbose bool ) func init() { @@ -51,6 +52,8 @@ func init() { createSourceCmd.Flags().StringVar(&sourceGitSemver, "git-semver", "", "git tag semver range") createSourceCmd.Flags().StringVarP(&sourceUsername, "username", "u", "", "basic authentication username") createSourceCmd.Flags().StringVarP(&sourcePassword, "password", "p", "", "basic authentication password") + createSourceCmd.Flags().BoolVarP(&sourceVerbose, "verbose", "", false, "print generated source object") + createCmd.AddCommand(createSourceCmd) } @@ -121,7 +124,10 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error { if err := writer.Flush(); err != nil { return fmt.Errorf("source flush failed: %w", err) } - fmt.Print(data.String()) + + if sourceVerbose { + fmt.Print(data.String()) + } command := fmt.Sprintf("echo '%s' | kubectl apply -f-", data.String()) c := exec.Command("/bin/sh", "-c", command) From 2d300d734431f121b87d4cf585999059ec2d55ab Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Sat, 25 Apr 2020 12:08:20 +0300 Subject: [PATCH 13/15] Add semver condition to check output --- cmd/tk/check.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/cmd/tk/check.go b/cmd/tk/check.go index be552e22..02e6f434 100644 --- a/cmd/tk/check.go +++ b/cmd/tk/check.go @@ -32,27 +32,34 @@ func init() { } func runCheckCmd(cmd *cobra.Command, args []string) error { + checkFailed := false if !sshCheck() { - os.Exit(1) + checkFailed = true } - if !kubectlCheck(">=1.14.0") { - os.Exit(1) + if !kubectlCheck(">=1.18.0") { + checkFailed = true } if !kustomizeCheck(">=3.5.0") { - os.Exit(1) + checkFailed = true } if checkPre { + if checkFailed { + os.Exit(1) + } fmt.Println(`✔`, "all prerequisites checks passed") return nil } if !kubernetesCheck(">=1.14.0") { - os.Exit(1) + checkFailed = true } + if checkFailed { + os.Exit(1) + } fmt.Println(`✔`, "all checks passed") return nil } @@ -97,7 +104,7 @@ func kubectlCheck(version string) bool { return false } - fmt.Println(`✔`, "kubectl", v.String()) + fmt.Println(`✔`, "kubectl", v.String(), version) return true } @@ -134,7 +141,7 @@ func kustomizeCheck(version string) bool { return false } - fmt.Println(`✔`, "kustomize", v.String()) + fmt.Println(`✔`, "kustomize", v.String(), version) return true } @@ -163,6 +170,6 @@ func kubernetesCheck(version string) bool { return false } - fmt.Println(`✔`, "kubernetes", v.String()) + fmt.Println(`✔`, "kubernetes", v.String(), version) return true } From 8392c4fe245fd5ee1e82f45ed61c390b63c73856 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Sat, 25 Apr 2020 13:16:34 +0300 Subject: [PATCH 14/15] Refactor logging --- cmd/tk/check.go | 42 ++++++++++++++++++++--------------------- cmd/tk/create_source.go | 23 ++++++++++++---------- cmd/tk/install.go | 12 ++++++------ cmd/tk/main.go | 14 +++++++++++++- cmd/tk/uninstall.go | 8 ++++---- 5 files changed, 57 insertions(+), 42 deletions(-) diff --git a/cmd/tk/check.go b/cmd/tk/check.go index 02e6f434..b201c05f 100644 --- a/cmd/tk/check.go +++ b/cmd/tk/check.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "os" "os/exec" "strings" @@ -32,6 +31,7 @@ func init() { } func runCheckCmd(cmd *cobra.Command, args []string) error { + logAction("starting verification") checkFailed := false if !sshCheck() { checkFailed = true @@ -49,7 +49,7 @@ func runCheckCmd(cmd *cobra.Command, args []string) error { if checkFailed { os.Exit(1) } - fmt.Println(`✔`, "all prerequisites checks passed") + logSuccess("all prerequisites checks passed") return nil } @@ -60,7 +60,7 @@ func runCheckCmd(cmd *cobra.Command, args []string) error { if checkFailed { os.Exit(1) } - fmt.Println(`✔`, "all checks passed") + logSuccess("all checks passed") return nil } @@ -69,10 +69,10 @@ func sshCheck() bool { for _, cmd := range []string{"ssh-keygen", "ssh-keyscan"} { _, err := exec.LookPath(cmd) if err != nil { - fmt.Println(`✗`, cmd, "not found") + logFailure("%s not found", cmd) ok = false } else { - fmt.Println(`✔`, cmd, "found") + logSuccess("%s found", cmd) } } @@ -82,94 +82,94 @@ func sshCheck() bool { func kubectlCheck(version string) bool { _, err := exec.LookPath("kubectl") if err != nil { - fmt.Println(`✗`, "kubectl not found") + logFailure("kubectl not found") return false } output, err := execCommand("kubectl version --client --short | awk '{ print $3 }'") if err != nil { - fmt.Println(`✗`, "kubectl version can't be determined") + logFailure("kubectl version can't be determined") return false } v, err := semver.ParseTolerant(output) if err != nil { - fmt.Println(`✗`, "kubectl version can't be parsed") + logFailure("kubectl version can't be parsed") return false } rng, _ := semver.ParseRange(version) if !rng(v) { - fmt.Println(`✗`, "kubectl version must be", version) + logFailure("kubectl version must be %s", version) return false } - fmt.Println(`✔`, "kubectl", v.String(), version) + logSuccess("kubectl %s %s", v.String(), version) return true } func kustomizeCheck(version string) bool { _, err := exec.LookPath("kustomize") if err != nil { - fmt.Println(`✗`, "kustomize not found") + logFailure("kustomize not found") return false } output, err := execCommand("kustomize version --short | awk '{ print $1 }' | cut -c2-") if err != nil { - fmt.Println(`✗`, "kustomize version can't be determined") + logFailure("kustomize version can't be determined") return false } if strings.Contains(output, "kustomize/") { output, err = execCommand("kustomize version --short | awk '{ print $1 }' | cut -c12-") if err != nil { - fmt.Println(`✗`, "kustomize version can't be determined") + logFailure("kustomize version can't be determined") return false } } v, err := semver.ParseTolerant(output) if err != nil { - fmt.Println(`✗`, "kustomize version can't be parsed") + logFailure("kustomize version can't be parsed") return false } rng, _ := semver.ParseRange(version) if !rng(v) { - fmt.Println(`✗`, "kustomize version must be", version) + logFailure("kustomize version must be %s", version) return false } - fmt.Println(`✔`, "kustomize", v.String(), version) + logSuccess("kustomize %s %s", v.String(), version) return true } func kubernetesCheck(version string) bool { client, err := kubernetesClient() if err != nil { - fmt.Println(`✗`, "kubernetes client initialization failed", err.Error()) + logFailure("kubernetes client initialization failed: %s", err.Error()) return false } ver, err := client.Discovery().ServerVersion() if err != nil { - fmt.Println(`✗`, "kubernetes API call failed", err.Error()) + logFailure("kubernetes API call failed %s", err.Error()) return false } v, err := semver.ParseTolerant(ver.String()) if err != nil { - fmt.Println(`✗`, "kubernetes version can't be determined") + logFailure("kubernetes version can't be determined") return false } rng, _ := semver.ParseRange(version) if !rng(v) { - fmt.Println(`✗`, "kubernetes version must be", version) + logFailure("kubernetes version must be %s", version) return false } - fmt.Println(`✔`, "kubernetes", v.String(), version) + logSuccess("kubernetes %s %s", v.String(), version) return true } diff --git a/cmd/tk/create_source.go b/cmd/tk/create_source.go index c3cb26cd..543a6777 100644 --- a/cmd/tk/create_source.go +++ b/cmd/tk/create_source.go @@ -3,6 +3,7 @@ package main import ( "bufio" "bytes" + "context" "fmt" "io" "io/ioutil" @@ -78,6 +79,9 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("git URL parse failed: %w", err) } + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + withAuth := false if strings.HasPrefix(sourceGitURL, "ssh") { if err := generateSSH(name, u.Host, tmpDir); err != nil { @@ -91,7 +95,7 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error { withAuth = true } - fmt.Println(`✚`, "generating source resource") + logAction("generating source %s in %s namespace", name, namespace) t, err := template.New("tmpl").Parse(gitSource) if err != nil { @@ -130,7 +134,7 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error { } command := fmt.Sprintf("echo '%s' | kubectl apply -f-", data.String()) - c := exec.Command("/bin/sh", "-c", command) + c := exec.CommandContext(ctx, "/bin/sh", "-c", command) var stdoutBuf, stderrBuf bytes.Buffer c.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) @@ -138,11 +142,10 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error { err = c.Run() if err != nil { - fmt.Println(`✗`, "source apply failed") - os.Exit(1) + return fmt.Errorf("source apply failed") } - fmt.Println(`✚`, "waiting for source sync") + logAction("waiting for source sync") if output, err := execCommand(fmt.Sprintf( "kubectl -n %s wait gitrepository/%s --for=condition=ready --timeout=1m", namespace, name)); err != nil { @@ -155,7 +158,7 @@ func createSourceCmdRun(cmd *cobra.Command, args []string) error { } func generateBasicAuth(name string) error { - fmt.Println(`✚`, "saving credentials") + logAction("saving credentials") credentials := fmt.Sprintf("--from-literal=username='%s' --from-literal=password='%s'", sourceUsername, sourcePassword) secret := fmt.Sprintf("kubectl -n %s create secret generic %s %s --dry-run=client -oyaml | kubectl apply -f-", @@ -169,14 +172,14 @@ func generateBasicAuth(name string) error { } func generateSSH(name, host, tmpDir string) error { - fmt.Println(`✚`, "generating host key for", host) + logAction("generating host key for %s", host) keyscan := fmt.Sprintf("ssh-keyscan %s > %s/known_hosts", host, tmpDir) if output, err := execCommand(keyscan); err != nil { return fmt.Errorf("ssh-keyscan failed: %s", output) } - fmt.Println(`✚`, "generating deploy key") + logAction("generating deploy key") keygen := fmt.Sprintf("ssh-keygen -b 2048 -t rsa -f %s/identity -q -N \"\"", tmpDir) if output, err := execCommand(keygen); err != nil { @@ -194,11 +197,11 @@ func generateSSH(name, host, tmpDir string) error { IsConfirm: true, } if _, err := prompt.Run(); err != nil { - fmt.Println(`✗`, "aborting") + logFailure("aborting") os.Exit(1) } - fmt.Println(`✚`, "saving deploy key") + logAction("saving deploy key") 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-", diff --git a/cmd/tk/install.go b/cmd/tk/install.go index ee3df1b1..1cb12467 100644 --- a/cmd/tk/install.go +++ b/cmd/tk/install.go @@ -62,19 +62,19 @@ func installCmdRun(cmd *cobra.Command, args []string) error { c.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) c.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) - fmt.Println(`✚`, "installing...") + logAction("installing components in %s namespace", namespace) err := c.Run() if err != nil { - fmt.Println(`✗`, "install failed") + logFailure("install failed") os.Exit(1) } if installDryRun { - fmt.Println(`✔`, "install dry-run finished") + logSuccess("install dry-run finished") return nil } - fmt.Println(`✚`, "verifying installation...") + logAction("verifying installation") for _, deployment := range []string{"source-controller", "kustomize-controller"} { command = fmt.Sprintf("kubectl -n %s rollout status deployment %s --timeout=%s", namespace, deployment, timeout.String()) @@ -83,11 +83,11 @@ func installCmdRun(cmd *cobra.Command, args []string) error { c.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) err := c.Run() if err != nil { - fmt.Println(`✗`, "install failed") + logFailure("install failed") os.Exit(1) } } - fmt.Println(`✔`, "install finished") + logSuccess("install finished") return nil } diff --git a/cmd/tk/main.go b/cmd/tk/main.go index c0ceb44c..33aa2178 100644 --- a/cmd/tk/main.go +++ b/cmd/tk/main.go @@ -47,7 +47,7 @@ func init() { func main() { log.SetFlags(0) if err := rootCmd.Execute(); err != nil { - fmt.Println(err) + logFailure("%v", err) os.Exit(1) } } @@ -81,3 +81,15 @@ func execCommand(command string) (string, error) { } return string(output), nil } + +func logAction(format string, a ...interface{}) { + fmt.Println(`✚`, fmt.Sprintf(format, a...)) +} + +func logSuccess(format string, a ...interface{}) { + fmt.Println(`✔`, fmt.Sprintf(format, a...)) +} + +func logFailure(format string, a ...interface{}) { + fmt.Println(`✗`, fmt.Sprintf(format, a...)) +} diff --git a/cmd/tk/uninstall.go b/cmd/tk/uninstall.go index 1459d011..aeea68cf 100644 --- a/cmd/tk/uninstall.go +++ b/cmd/tk/uninstall.go @@ -49,7 +49,7 @@ func uninstallCmdRun(cmd *cobra.Command, args []string) error { IsConfirm: true, } if _, err := prompt.Run(); err != nil { - fmt.Println(`✗`, "aborting") + logFailure("aborting") os.Exit(1) } } @@ -67,13 +67,13 @@ func uninstallCmdRun(cmd *cobra.Command, args []string) error { c.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf) c.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) - fmt.Println(`✚`, "uninstalling...") + logAction("uninstalling components") err := c.Run() if err != nil { - fmt.Println(`✗`, "uninstall failed") + logFailure("uninstall failed") os.Exit(1) } - fmt.Println(`✔`, "uninstall finished") + logSuccess("uninstall finished") return nil } From 70072f300a1272bb184f271948ff1303c93cf833 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Sat, 25 Apr 2020 19:28:51 +0300 Subject: [PATCH 15/15] Add release workflow to CI --- .github/workflows/release.yaml | 34 ++++++++++++++++++++++++++++++++++ .goreleaser.yml | 14 ++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 .github/workflows/release.yaml create mode 100644 .goreleaser.yml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 00000000..f0deead1 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,34 @@ +name: release + +on: + push: + tags: + - '*' + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Unshallow + run: git fetch --prune --unshallow + - name: Setup Go + uses: actions/setup-go@v2-beta + with: + go-version: 1.14.x + - name: Download release notes utility + env: + GH_REL_URL: https://github.com/buchanae/github-release-notes/releases/download/0.2.0/github-release-notes-linux-amd64-0.2.0.tar.gz + run: cd /tmp && curl -sSL ${GH_REL_URL} | tar xz && sudo mv github-release-notes /usr/local/bin/ + - name: Generate release notes + run: | + echo 'CHANGELOG' > /tmp/release.txt + github-release-notes -org fluxcd -repo toolkit -since-latest-release >> /tmp/release.txt + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v1 + with: + version: latest + args: release --release-notes=/tmp/release.txt + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 00000000..279671ec --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,14 @@ +builds: + - main: ./cmd/tk + binary: tk + goos: + - darwin + - linux + goarch: + - amd64 + env: + - CGO_ENABLED=0 +archives: + - name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + files: + - none*