From f4130e120fe626805434fded3adba3b09b3de0c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20Na=CC=88gele?= Date: Fri, 19 Oct 2018 18:16:22 +0200 Subject: [PATCH] Re-write it in Go --- .circleci/config.yml | 45 +- .dockerignore | 6 +- .editorconfig | 5 + .gitignore | 8 +- .goreleaser.yml | 49 ++ Dockerfile | 50 +- Makefile | 13 + OWNERS | 9 + README.md | 227 ++----- app/cmd/install.go | 106 ++++ app/cmd/lint.go | 108 ++++ app/cmd/lintAndInstall.go | 72 +++ app/cmd/root.go | 91 +++ app/cmd/version.go | 53 ++ app/main.go | 23 + build.sh | 54 +- chart_test.sh | 188 ------ code-of-conduct.md | 3 - examples/docker-for-mac/my_test.sh | 7 +- examples/gke/Dockerfile | 4 +- examples/gke/my_test.sh | 3 +- go.mod | 15 + go.sum | 46 ++ lib/chartlib.sh | 565 ----------------- pkg/chart/chart.go | 597 ++++++++++++++++++ pkg/chart/chart_test.go | 147 +++++ .../testdata/empty_maintainers/Chart.yaml | 1 + .../testdata/invalid_maintainers/Chart.yaml | 5 + pkg/chart/testdata/no_maintainers/Chart.yaml | 0 .../no_maintainers_deprecated/Chart.yaml | 1 + .../testdata/valid_maintainers/Chart.yaml | 5 + .../valid_maintainers_deprecated/Chart.yaml | 6 + pkg/config/config.go | 121 ++++ pkg/config/config_test.go | 49 ++ pkg/config/test_config.json | 23 + pkg/config/test_config.yaml | 18 + pkg/exec/exec.go | 86 +++ pkg/tool/account.go | 45 ++ pkg/tool/account_test.go | 28 + pkg/tool/git.go | 61 ++ pkg/tool/helm.go | 89 +++ pkg/tool/kubectl.go | 138 ++++ pkg/tool/linter.go | 33 + pkg/util/util.go | 166 +++++ pkg/util/util_test.go | 67 ++ tag.sh | 109 ++++ 46 files changed, 2543 insertions(+), 1002 deletions(-) create mode 100644 .goreleaser.yml create mode 100644 Makefile create mode 100644 OWNERS create mode 100644 app/cmd/install.go create mode 100644 app/cmd/lint.go create mode 100644 app/cmd/lintAndInstall.go create mode 100644 app/cmd/root.go create mode 100644 app/cmd/version.go create mode 100644 app/main.go delete mode 100755 chart_test.sh delete mode 100644 code-of-conduct.md create mode 100644 go.mod create mode 100644 go.sum delete mode 100644 lib/chartlib.sh create mode 100644 pkg/chart/chart.go create mode 100644 pkg/chart/chart_test.go create mode 100644 pkg/chart/testdata/empty_maintainers/Chart.yaml create mode 100644 pkg/chart/testdata/invalid_maintainers/Chart.yaml create mode 100644 pkg/chart/testdata/no_maintainers/Chart.yaml create mode 100644 pkg/chart/testdata/no_maintainers_deprecated/Chart.yaml create mode 100644 pkg/chart/testdata/valid_maintainers/Chart.yaml create mode 100644 pkg/chart/testdata/valid_maintainers_deprecated/Chart.yaml create mode 100644 pkg/config/config.go create mode 100644 pkg/config/config_test.go create mode 100644 pkg/config/test_config.json create mode 100644 pkg/config/test_config.yaml create mode 100644 pkg/exec/exec.go create mode 100644 pkg/tool/account.go create mode 100644 pkg/tool/account_test.go create mode 100644 pkg/tool/git.go create mode 100644 pkg/tool/helm.go create mode 100644 pkg/tool/kubectl.go create mode 100644 pkg/tool/linter.go create mode 100644 pkg/util/util.go create mode 100644 pkg/util/util_test.go create mode 100755 tag.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 48a988cd..0ef4c8c6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,6 +1,6 @@ version: 2 jobs: - build: + lint: docker: - image: koalaman/shellcheck-alpine steps: @@ -8,7 +8,42 @@ jobs: - run: name: lint command: | - shellcheck -x lib/chartlib.sh - shellcheck -x chart_test.sh - shellcheck -x examples/docker-for-mac/my_test.sh - shellcheck -x examples/gke/my_test.sh + shellcheck -x build.sh + shellcheck -x tag.sh + release: + docker: + - image: golang:1.11.1-alpine3.8 + working_directory: /workdir + steps: + - setup_remote_docker + - run: + name: Install tools + command: | + apk add bash build-base ca-certificates curl docker git openssh + curl -SLO https://github.com/goreleaser/goreleaser/releases/download/v0.89.0/goreleaser_Linux_x86_64.tar.gz + mkdir -p /usr/local/goreleaser + tar -xzf goreleaser_Linux_x86_64.tar.gz -C /usr/local/goreleaser + ln -s /usr/local/goreleaser/goreleaser /usr/local/bin/goreleaser + rm -rf goreleaser_Linux_x86_64.tar.gz + - checkout + - run: + name: Checkout tag + command: git gc --aggressive --prune=all && git checkout -b "$CIRCLE_TAG" "$CIRCLE_TAG" + - run: + name: Create release + command: | + go mod download + ./build.sh --debug --release +workflows: + version: 2 + untagged-build: + jobs: + - lint + tagged-build: + jobs: + - release: + filters: + tags: + only: /^v.*/ + branches: + ignore: /.*/ diff --git a/.dockerignore b/.dockerignore index 04fc4254..c8ca6a20 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,4 @@ +.circleci .history .idea -.editorconfig -.gitignore -.git -Dockerfile +.vscode diff --git a/.editorconfig b/.editorconfig index bab0e99c..7b06f12d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,3 +12,8 @@ charset = utf-8 [*.{yml,yaml}] indent_size = 2 +[*.go] +indent_style = tab + +[Makefile] +indent_style = tab diff --git a/.gitignore b/.gitignore index 707ddf0c..4068e9a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ -.history .idea -.settings .project -.classpath +.settings +.vscode +/dist +/config.* +/ct diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 00000000..c76afc38 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,49 @@ +project_name: chart-testing +builds: + - main: app/main.go + binary: ct + env: + - CGO_ENABLED=0 + goarch: + - amd64 + - arm + goos: + - linux + - darwin + - windows + ldflags: + - >- + -X github.com/helm/chart-testing/app/cmd.Version={{ .Tag }} + -X github.com/helm/chart-testing/app/cmd.GitCommit={{ .Commit }} + -X github.com/helm/chart-testing/app/cmd.BuildDate={{ .Date }} +archive: + format_overrides: + - goos: windows + format: zip +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc +dockers: + - goos: linux + goarch: amd64 + binary: ct + image: quay.io/helmpack/chart-testing + skip_push: false + dockerfile: Dockerfile + tag_templates: + - "{{ .Tag }}" + - latest + build_flag_templates: + - "--build-arg=dist_dir=" + - "--label=org.label-schema.schema-version=1.0" + - "--label=org.label-schema.version={{ .Version }}" + - "--label=org.label-schema.name={{ .ProjectName }}" + - "--label=org.label-schema.build-date={{ .Date }}" + - "--label=org.label-schema.description='ct - The chart testing tool'" + - "--label=org.label-schema.vendor=Helm" + extra_files: + - etc/chart_schema.yaml + - etc/lintconf.yaml diff --git a/Dockerfile b/Dockerfile index e0fc8116..24adcc31 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,39 +1,14 @@ -# Copyright 2018 The Helm Authors. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -#     http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -FROM bash:4.4 +FROM alpine:3.8 RUN apk --no-cache add \ curl \ git \ - jq \ libc6-compat \ openssh-client \ python \ py-crcmod \ py-pip -# Install YQ command line reader -ARG YQ_VERSION=2.5.0 -RUN pip install "yq==$YQ_VERSION" - -# Install SemVer testing tool -ARG VERT_VERSION=0.1.0 -RUN curl -Lo vert "https://github.com/Masterminds/vert/releases/download/v$VERT_VERSION/vert-v$VERT_VERSION-linux-amd64" && \ - chmod +x vert && \ - mv vert /usr/local/bin/ - # Install a YAML Linter ARG YAML_LINT_VERSION=1.8.1 RUN pip install "yamllint==$YAML_LINT_VERSION" @@ -43,22 +18,23 @@ ARG YAMALE_VERSION=1.7.0 RUN pip install "yamale==$YAMALE_VERSION" # Install kubectl -ARG KUBECTL_VERSION=1.10.2 -RUN curl -LO "https://storage.googleapis.com/kubernetes-release/release/v$KUBECTL_VERSION/bin/linux/amd64/kubectl" && \ +ARG KUBECTL_VERSION=v1.12.0 +RUN curl -LO "https://storage.googleapis.com/kubernetes-release/release/$KUBECTL_VERSION/bin/linux/amd64/kubectl" && \ chmod +x kubectl && \ mv kubectl /usr/local/bin/ # Install Helm -ARG HELM_VERSION=2.10.0 -RUN curl -LO "https://kubernetes-helm.storage.googleapis.com/helm-v$HELM_VERSION-linux-amd64.tar.gz" && \ +ARG HELM_VERSION=v2.11.0 +RUN curl -LO "https://kubernetes-helm.storage.googleapis.com/helm-$HELM_VERSION-linux-amd64.tar.gz" && \ mkdir -p "/usr/local/helm-$HELM_VERSION" && \ - tar -xzf "helm-v$HELM_VERSION-linux-amd64.tar.gz" -C "/usr/local/helm-$HELM_VERSION" && \ + tar -xzf "helm-$HELM_VERSION-linux-amd64.tar.gz" -C "/usr/local/helm-$HELM_VERSION" && \ ln -s "/usr/local/helm-$HELM_VERSION/linux-amd64/helm" /usr/local/bin/helm && \ - rm -f "helm-v$HELM_VERSION-linux-amd64.tar.gz" - -COPY etc /testing/etc/ -COPY lib /testing/lib/ -COPY chart_test.sh /testing/ + rm -f "helm-$HELM_VERSION-linux-amd64.tar.gz" -RUN ln -s /testing/chart_test.sh /usr/local/bin/chart_test.sh +# Goreleaser needs to override this because it builds the +# Dockerfile from a tmp dir with all files to be copied in the root +ARG dist_dir=dist/linux_amd64 +COPY "$dist_dir/chart_schema.yaml" /etc/ct/chart_schema.yaml +COPY "$dist_dir/lintconf.yaml" /etc/ct/lintconf.yaml +COPY "$dist_dir/ct" /usr/local/bin/ct diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..e6d50025 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ + + +.PHONY: build +build: + go build -o ct app/main.go + +.PHONY: test +test: + go test ./... + +.PHONY: release +release: test + ./tag.sh diff --git a/OWNERS b/OWNERS new file mode 100644 index 00000000..49aba12b --- /dev/null +++ b/OWNERS @@ -0,0 +1,9 @@ +approvers: + - lachie83 + - prydonius + - viglesiasce + - unguiculus + - scottrigby + - mattfarina + - davidkarlsen + - paulczar diff --git a/README.md b/README.md index 4b7619c2..a7ceb0df 100644 --- a/README.md +++ b/README.md @@ -1,213 +1,114 @@ # Chart Testing -Bash library for linting and testing Helm charts. -Comes prepackaged as Docker image for easy use. +`ct` is the the tool for testing Helm chart written in Go. -[chartlib.sh](lib/chartlib.sh) is a Bash library with useful function for linting and testing charts. -It is well documented and should be easily usable. -The script is meant to be sourced and can be configured via environment variables. +## Features -As a convenience, [chart_test.sh](chart_test.sh) is provided. -It supports linting and testing charts that have changed against a target branch. +It is meant to be used for linting and testing pull requests. +It automatically detects charts changed against the target branch. + +## Installation -## Prerequisites +### Prerequisites It is recommended to use the provided Docker image which can be [found on Quay](quay.io/helmpack/chart-testing/). It comes with all necessary tools installed. -* Bash 4.4 (https://tiswww.case.edu/php/chet/bash/bashtop.html) * Helm (http://helm.sh) -* yq (https://github.com/kislyuk/yq) -* vert (https://github.com/Masterminds/vert) * yamllint (https://github.com/adrienverge/yamllint) * yamale (https://github.com/23andMe/Yamale) * kubectl (https://kubernetes.io/docs/reference/kubectl/overview/) * Tooling for your cluster -Note that older Bash versions may not work! -## Installation +### Binary Distribution -Clone the repository and add it to the `PATH`. -The script must be run in the root directory of a Git repository. - -```shell -$ chart_test.sh --help -Usage: chart_test.sh - Lint, install, and test Helm charts. - -h, --help Display help - --verbose Display verbose output - --no-lint Skip chart linting - --no-install Skip chart installation - --all Lint/install all charts - --charts Lint/install: - a standalone chart (e. g. stable/nginx) - a list of charts (e. g. stable/nginx,stable/cert-manager) - --config Path to the config file (optional) - -- End of all options -``` +Download the release distribution for your OS from the Releases page: -## Configuration +https://github.com/helm/chart-testing/releases -The following environment variables can be set to configure [chartlib.sh](lib/chartlib.sh). -Note that this must be done before the script is sourced. - -| Variable | Description | Default | -| - | - | - | -| `REMOTE` | The name of the Git remote to check against for changed charts | `origin` | -| `TARGET_BRANCH` | The name of the Git target branch to check against for changed charts | `master` | -| `CHART_DIRS` | Array of directories relative to the repo root containing charts | `(charts)` | -| `EXCLUDED_CHARTS` | Array of directories of charts that should be skipped | `()` | -| `CHART_REPOS` | Array of additional chart repos to add (`=`) | `()` | -| `TIMEOUT` | Timeout for chart installation in seconds | `300` | -| `LINT_CONF` | Config file for YAML linter | `/testing/etc/lintconf.yaml` (path of default config file in Docker image) | -| `CHART_YAML_SCHEMA` | YAML schema for `Chart.yaml` | `/testing/etc/chart_schema.yaml` (path of default schema file in Docker image) | -| `VALIDATE_MAINTAINERS`| If `true`, maintainer names in `Chart.yaml` are validated to be existing Github accounts | `true` | -| `GITHUB_INSTANCE`| Url of Github instance for maintainer validation | `https://github.com` | -| `CHECK_VERSION_INCREMENT`| If `true`, the chart version is checked to be incremented from the version on the remote target branch | `true` | - -Note that `CHART_DIRS`, `EXCLUDED_CHARTS`, and `CHART_REPOS` must be configured as Bash arrays. +Unpack the `ct` binary, add it to your PATH, and you are good to go! -## Usage -The library is meant to be used for linting and testing pull requests. -It automatically detects charts changed against the target branch. -The environment variables mentioned in the configuration section above can be set in a config file for `chart_test.sh`. +### Docker Image -By default, changes are detected against `origin/master`. -Depending on your CI setup, it may be necessary to configure and fetch a separate remote for this. +A Docker image is available at `quay.io/helmpack/chart-testing`. -```shell -REMOTE=myremote -``` -```shell -git remote add myremote -git fetch myremote -chart-test.sh -``` -### Linting Charts +## Usage -```shell -docker run --rm -v "$(pwd):/workdir" --workdir /workdir quay.io/helmpack/chart-testing:v1.1.0 chart_test.sh --no-install --config .mytestenv ``` +$ ./ct + __ + _____/ /_ + / ___/ __/ +/ /__/ /_ +\___/\__/ -*Sample Output* +Lint and test -``` ------------------------------------------------------------------------ -Environment: -REMOTE=k8s -TARGET_BRANCH=master -CHART_DIRS=stable -EXCLUDED_CHARTS= -CHART_REPOS= -TIMEOUT=600 -LINT_CONF=/testing/etc/lintconf.yaml -CHART_YAML_SCHEMA=/testing/etc/chart_schema.yaml -VALIDATE_MAINTAINERS=true ------------------------------------------------------------------------ -Charts to be installed and tested: stable/dummy -Initializing Helm client... -Creating /home/testing/.helm -Creating /home/testing/.helm/repository -Creating /home/testing/.helm/repository/cache -Creating /home/testing/.helm/repository/local -Creating /home/testing/.helm/plugins -Creating /home/testing/.helm/starters -Creating /home/testing/.helm/cache/archive -Creating /home/testing/.helm/repository/repositories.yaml -Adding stable repo with URL: https://kubernetes-charts.storage.googleapis.com -Adding local repo with URL: http://127.0.0.1:8879/charts -$HELM_HOME has been configured at /home/testing/.helm. -Not installing Tiller due to 'client-only' flag having been set -Happy Helming! - ------------------------------------------------------------------------ -Processing chart 'stable/dummy'... ------------------------------------------------------------------------ - -Validating chart 'stable/dummy'... -Checking chart 'stable/dummy' for a version bump... -Unable to find chart on master. New chart detected. -Linting 'stable/dummy/Chart.yaml'... -Linting 'stable/dummy/values.yaml'... -Validating Chart.yaml -Validating /workdir/stable/dummy/Chart.yaml... -Validation success! 👍 -Validating maintainers -Verifying maintainer 'unguiculus'... -Using custom values file 'stable/dummy/ci/ci-values.yaml'... -Linting chart 'stable/dummy'... -==> Linting stable/dummy -[INFO] Chart.yaml: icon is recommended - -1 chart(s) linted, no failures -Done. -``` +* changed charts +* specific charts +* all charts -#### Linting Unchanged Charts +in given chart directories. -You can lint all charts with `--all` flag (chart version bump check will be ignored): +Usage: + ct [command] -```shell -docker run --rm -v "$(pwd):/workdir" --workdir /workdir quay.io/helmpack/chart-testing:v1.1.0 chart_test.sh --no-install --config .mytestenv --all -``` +Available Commands: + help Help about any command + install Install and test a chart + lint Lint and validate a chart + lint-and-install Lint, install, and test a chart + version Print version information -You can lint a list of charts (separated by comma) with `--charts` flag (chart version bump check will be ignored): +Flags: + -h, --help help for ct -```shell -docker run --rm -v "$(pwd):/workdir" --workdir /workdir quay.io/helmpack/chart-testing:v1.1.0 chart_test.sh --no-install --config .mytestenv --charts stable/nginx,stable/cert-manager +Use "ct [command] --help" for more information about a command. ``` -You can lint a single chart with `--charts` flag (chart version bump check will be ignored): +## Configuration -```shell -docker run --rm -v "$(pwd):/workdir" --workdir /workdir quay.io/helmpack/chart-testing:v1.1.0 chart_test.sh --no-install --config .mytestenv --charts stable/nginx -``` +`ct` is a command-line application. +All command-line flags can also be set via environment variables or config file. +Environment variables must be prefixed with `CT_`. Underscores must be used instead of hyphens. -### Installing and Testing Charts +CLI flags, environment variables, and a config file can be mixed. The following order of precedence applies: -Installing a chart requires access to a Kubernetes cluster. -You may have to create your own Docker image that extends from `quay.io/helmpack/chart-testing:v1.1.0` in order to install additional tools (e. g. `google-cloud-sdk` for GKE). -A container from such an image could run steps to authenticate to a Kubernetes cluster, where it initializes the `kubectl` context, before running `chart_test.sh`. +1. CLI flags +1. Environment variables +1. Config file -Charts are installed into newly created namespaces that will be deleted again afterwards. -By default, they are named by the chart, which may not be a good idea, especially when multiple PR jobs could be running for the same chart. -`chart_lib.sh` looks for an environment variable `BUILD_ID` and uses it to name the namespace. -Make sure you set it based on the pull request number. -```shell -docker run --rm -v "$(pwd):/workdir" --workdir /workdir quay.io/helmpack/chart-testing:v1.1.0 chart_test.sh --no-lint --config .mytestenv -``` +### Examples -#### Installing Unchanged Charts +The following example show various way of configuring the same thing: -You can force to install all charts with `--all` flag: +#### CLI -```shell -docker run --rm -v "$(pwd):/workdir" --workdir /workdir quay.io/helmpack/chart-testing:v1.1.0 chart_test.sh --no-lint --config .mytestenv --all -``` + ct install --remote upstream --chart-dirs stable,incubator --build-id pr-42 -You can force to install a list of charts (separated by comma) with `--charts` flag: +#### Environment Variables -```shell -docker run --rm -v "$(pwd):/workdir" --workdir /workdir quay.io/helmpack/chart-testing:v1.1.0 chart_test.sh --no-lint --config .mytestenv --charts stable/nginx,stable/cert-manager -``` - -You can force to install one chart with `--charts` flag: + export CT_REMOTE=upstream + export CT_CHART_DIRS=stable,incubator + export CT_BUILD_ID -```shell -docker run --rm -v "$(pwd):/workdir" --workdir /workdir quay.io/helmpack/chart-testing:v1.1.0 chart_test.sh --no-lint --config .mytestenv --charts stable/nginx -``` + ct install -#### GKE Example +#### Config File -An example for GKE is available in the [examples/gke](examples/gke) directory. -A custom `Dockerfile` additionally installs the `google-cloud-sdk` and a custom shell script puts everything together. +*config.yaml* +``` +remote: upstream +chart-dirs: + - stable + - incubator +build-id: pr-42 +``` -#### Docker for Mac Example +`ct install --config config.yaml` -An example for Docker for Mac is available in the [examples/docker-for-mac](examples/docker-for-mac) directory. -This script can be run as is in the [charts](https://github.com/helm/charts) repo. -Make sure `Show system containers` is active for Docker's Kubernetes distribution, so the script can find the API server and configure `kubectl` so it can access the API server from within the container. +`ct` supports any format [Viper](https://github.com/spf13/viper) can read, i. e. JSON, TOML, YAML, HCL, and Java properties files. diff --git a/app/cmd/install.go b/app/cmd/install.go new file mode 100644 index 00000000..650e8672 --- /dev/null +++ b/app/cmd/install.go @@ -0,0 +1,106 @@ +// Copyright © 2018 The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "os" + "time" + + "github.com/spf13/viper" + + "github.com/MakeNowJust/heredoc" + "github.com/helm/chart-testing/pkg/chart" + "github.com/helm/chart-testing/pkg/config" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" +) + +func newInstallCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "install", + Short: "Install and test a chart", + Long: heredoc.Doc(` + __ + _____/ /_ + / ___/ __/ + / /__/ /_ + \___/\__/ + + Run 'helm install' and ' helm test' on + + * changed charts (default) + * specific charts (--charts) + * all charts (--all) + + in given chart directories. + + Charts may have multiple custom values files matching the glob pattern + '*-values.yaml' in a directory named 'ci' in the root of the chart's + directory. The chart is installed and tested for each of these files. + If no custom values file is present, the chart is installed and + tested with defaults.`), + Run: install, + } + + flags := cmd.Flags() + addInstallFlags(flags) + addCommonLintAndInstallFlags(flags) + return cmd +} + +func addInstallFlags(flags *flag.FlagSet) { + flags.String("build-id", "", heredoc.Doc(` + An optional, arbitrary identifier that is added to the name of the namespace a + chart is installed into. In a CI environment, this could be the build number or + the ID of a pull request. If not specified, the name of the chart is used.`)) + flags.Duration("timeout", 5*time.Minute, "The timeout for Helm in seconds (default: 5m)") + flags.String("tiller-namespace", "kube-system", "The namespace of Tiller (default: kube-system)") +} + +func install(cmd *cobra.Command, args []string) { + fmt.Println("Installing charts...") + + configuration, err := config.LoadConfiguration(cfgFile, cmd, bindRootFlags, bindInstallFlags) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + testing := chart.NewTesting(*configuration) + results, err := testing.InstallCharts() + if err != nil { + fmt.Println("Error installing charts:", err) + } else { + fmt.Println("All charts installed successfully") + } + + testing.PrintResults(results) + + if err != nil { + os.Exit(1) + } +} + +func bindInstallFlags(flagSet *flag.FlagSet, v *viper.Viper) error { + options := []string{"build-id", "timeout", "tiller-namespace"} + for _, option := range options { + if err := v.BindPFlag(option, flagSet.Lookup(option)); err != nil { + return err + } + } + return nil +} diff --git a/app/cmd/lint.go b/app/cmd/lint.go new file mode 100644 index 00000000..6956a3eb --- /dev/null +++ b/app/cmd/lint.go @@ -0,0 +1,108 @@ +// Copyright © 2018 The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "github.com/MakeNowJust/heredoc" + "os" + + "github.com/helm/chart-testing/pkg/chart" + "github.com/helm/chart-testing/pkg/config" + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +func newLintCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "lint", + Short: "Lint and validate a chart", + Long: heredoc.Doc(` + __ + _____/ /_ + / ___/ __/ + / /__/ /_ + \___/\__/ + + Run 'helm lint', version checking, YAML schema validation + on 'Chart.yaml', YAML linting on 'Chart.yaml' and 'values.yaml', + and maintainer validation on + + * changed charts (default) + * specific charts (--charts) + * all charts (--all) + + in given chart directories. + + Charts may have multiple custom values files matching the glob pattern + '*-values.yaml' in a directory named 'ci' in the root of the chart's + directory. The chart is linted for each of these files. If no custom + values file is present, the chart is linted with defaults.`), + Run: lint, + } + + flags := cmd.Flags() + addLintFlags(flags) + addCommonLintAndInstallFlags(flags) + return cmd +} + +func addLintFlags(flags *flag.FlagSet) { + flags.String("lint-conf", "", heredoc.Doc(` + The config file for YAML linting. If not specified, 'lintconf.yaml' is + searched in '/etc/ct', '$HOME/ct' and the current directory`)) + flags.String("chart-yaml-schema", "", heredoc.Doc(` + The schema for chart.yml validation. If not specified, 'chart_schema.yaml' + is searched in '/etc/ct', '$HOME/ct' and the current directory`)) + flags.Bool("validate-maintainers", true, heredoc.Doc(` + Enabled validation of maintainer account names in chart.yml (default: true). + Works for GitHub, GitLab, and Bitbucket`)) + flags.Bool("check-version-increment", true, "Activates a check for chart version increments (default: true)") +} + +func lint(cmd *cobra.Command, args []string) { + fmt.Println("Linting charts...") + + configuration, err := config.LoadConfiguration(cfgFile, cmd, bindRootFlags, bindLintFlags) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + testing := chart.NewTesting(*configuration) + results, err := testing.LintCharts() + if err != nil { + fmt.Println("Error linting charts") + } else { + fmt.Println("All charts linted successfully") + } + + testing.PrintResults(results) + + if err != nil { + os.Exit(1) + } +} + +func bindLintFlags(flagSet *flag.FlagSet, v *viper.Viper) error { + options := []string{"lint-conf", "chart-yaml-schema", "validate-maintainers", "check-version-increment"} + for _, option := range options { + if err := v.BindPFlag(option, flagSet.Lookup(option)); err != nil { + return err + } + } + return nil +} diff --git a/app/cmd/lintAndInstall.go b/app/cmd/lintAndInstall.go new file mode 100644 index 00000000..3d6a6057 --- /dev/null +++ b/app/cmd/lintAndInstall.go @@ -0,0 +1,72 @@ +// Copyright © 2018 The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "os" + + "github.com/MakeNowJust/heredoc" + "github.com/helm/chart-testing/pkg/chart" + "github.com/helm/chart-testing/pkg/config" + + "github.com/spf13/cobra" +) + +func newLintAndInstallCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "lint-and-install", + Short: "Lint, install, and test a chart", + Long: heredoc.Doc(` + __ + _____/ /_ + / ___/ __/ + / /__/ /_ + \___/\__/ + + Combines 'lint' and 'install' commands.`), + Run: lintAndInstall, + } + + flags := cmd.Flags() + addLintFlags(flags) + addInstallFlags(flags) + addCommonLintAndInstallFlags(flags) + return cmd +} + +func lintAndInstall(cmd *cobra.Command, args []string) { + fmt.Println("Linting and installing charts...") + + configuration, err := config.LoadConfiguration(cfgFile, cmd, bindRootFlags, bindLintFlags, bindInstallFlags) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + testing := chart.NewTesting(*configuration) + results, err := testing.LintAndInstallCharts() + if err != nil { + fmt.Println("Error linting and installing charts") + } else { + fmt.Println("All charts linted and installed successfully") + } + + testing.PrintResults(results) + + if err != nil { + os.Exit(1) + } +} diff --git a/app/cmd/root.go b/app/cmd/root.go new file mode 100644 index 00000000..dd9e3456 --- /dev/null +++ b/app/cmd/root.go @@ -0,0 +1,91 @@ +// Copyright © 2018 The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "github.com/spf13/viper" + "os" + + "github.com/MakeNowJust/heredoc" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + flag "github.com/spf13/pflag" +) + +var ( + cfgFile string +) + +func newRootCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "ct", + Short: "The Helm chart testing tool", + Long: heredoc.Doc(` + __ + _____/ /_ + / ___/ __/ + / /__/ /_ + \___/\__/ + + Lint and test + + * changed charts + * specific charts + * all charts + + in given chart directories.`), + } + + cmd.AddCommand(newLintCmd()) + cmd.AddCommand(newInstallCmd()) + cmd.AddCommand(newLintAndInstallCmd()) + cmd.AddCommand(newVersionCmd()) + + return cmd +} + +// Execute runs the application +func Execute() { + if err := newRootCmd().Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func addCommonLintAndInstallFlags(flags *pflag.FlagSet) { + flags.StringVar(&cfgFile, "config", "", "Config file") + flags.String("remote", "origin", "The name of the Git remote used to identify changed charts") + flags.String("target-branch", "master", "The name of the target branch used to identify changed charts") + flags.Bool("all", false, heredoc.Doc(` + Process all charts except those explicitly excluded. + Disables changed charts detection and version increment checking`)) + flags.StringSlice("charts", []string{}, heredoc.Doc(` + Specific charts to test. + Disables changed charts detection and version increment checking`)) + flags.StringSlice("chart-dirs", []string{"charts"}, "Directories containing Helm charts") + flags.StringSlice("chart-repos", []string{}, "Additional chart repos to add so dependencies can be resolved") + flags.StringSlice("excluded-charts", []string{}, "Charts that should be skipped") +} + +func bindRootFlags(flagSet *flag.FlagSet, v *viper.Viper) error { + options := []string{"remote", "target-branch", "all", "charts", "chart-dirs", "chart-repos", "excluded-charts"} + for _, option := range options { + if err := v.BindPFlag(option, flagSet.Lookup(option)); err != nil { + return err + } + } + return nil +} diff --git a/app/cmd/version.go b/app/cmd/version.go new file mode 100644 index 00000000..ef2510a7 --- /dev/null +++ b/app/cmd/version.go @@ -0,0 +1,53 @@ +// Copyright © 2018 The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "github.com/MakeNowJust/heredoc" + "github.com/spf13/cobra" +) + +var ( + // GitCommit is updated with the Git tag by the Goreleaser build + GitCommit = "unknown" + // BuildDate is updated with the current ISO timestamp by the Goreleaser build + BuildDate = "unknown" + // Version is updated with the latest tag by the Goreleaser build + Version = "unreleased" +) + +func newVersionCmd() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print version information", + Long: heredoc.Doc(` + __ + _____/ /_ + / ___/ __/ + / /__/ /_ + \___/\__/ + + Print version information.`), + Run: version, + } +} + +func version(cmd *cobra.Command, args []string) { + fmt.Println("Version:\t", Version) + fmt.Println("Git commit:\t", GitCommit) + fmt.Println("Date:\t\t", BuildDate) + fmt.Println("License:\t Apache 2.0") +} diff --git a/app/main.go b/app/main.go new file mode 100644 index 00000000..d0311332 --- /dev/null +++ b/app/main.go @@ -0,0 +1,23 @@ +// Copyright © 2018 The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "github.com/helm/chart-testing/app/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/build.sh b/build.sh index f1fdaa7b..df5280bd 100755 --- a/build.sh +++ b/build.sh @@ -18,26 +18,25 @@ set -o errexit set -o nounset set -o pipefail -readonly IMAGE_TAG=v1.1.0 - -# The image goes into two repositories. quay.io/helmpack/chart-testing is used -# for public consumption and is built by Quay via a webhook. The below image -# is close to the CI environment used by charts where we also push it. -readonly IMAGE_REPOSITORY="gcr.io/kubernetes-charts-ci/chart-testing" readonly SCRIPT_DIR=$(dirname "$(readlink -f "$0")") show_help() { cat << EOF -Usage: ${0##*/} - -h, --help Display help - -v, --verbose Display verbose output - -p, --push Push image to registry +Usage: $(basename "$0") + +Build ct using Goreleaser. + + -h, --help Display help + -d, --debug Display verbose output and run Goreleaser with --debug + -r, --release Create a release using Goreleaser. This includes the creation + of a GitHub release and building and pushing the Docker image. + If this flag is not specified, Goreleaser is run with --snapshot EOF } main() { - local verbose= - local push= + local debug= + local release= while :; do case "${1:-}" in @@ -45,14 +44,11 @@ main() { show_help exit ;; - -v|--verbose) - verbose=true - ;; - -p|--push) - push=true + -d|--debug) + debug=true ;; - -?*) - printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2 + -r|--release) + release=true ;; *) break @@ -62,17 +58,23 @@ main() { shift done - [[ -n "$verbose" ]] && set -o xtrace + local goreleaser_args=(--rm-dist) - pushd "$SCRIPT_DIR" - - docker build --tag "$IMAGE_REPOSITORY:$IMAGE_TAG" . + if [[ -n "$debug" ]]; then + goreleaser_args+=( --debug) + set -x + fi - if [[ -n "$push" ]]; then - docker push "$IMAGE_REPOSITORY:$IMAGE_TAG" + if [[ -z "$release" ]]; then + goreleaser_args+=( --snapshot) fi - popd + pushd "$SCRIPT_DIR" > /dev/null + + go test ./... + goreleaser "${goreleaser_args[@]}" + + popd > /dev/null } main "$@" diff --git a/chart_test.sh b/chart_test.sh deleted file mode 100755 index 3fd5a1f4..00000000 --- a/chart_test.sh +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 2018 The Helm Authors. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -#     http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -o errexit -set -o nounset -set -o pipefail - -readonly REPO_ROOT=$(git rev-parse --show-toplevel) -readonly SCRIPT_DIR=$(dirname "$(readlink -f "$0")") - -show_help() { -cat << EOF -Usage: $(basename "$0") - Lint, install, and test Helm charts. - -h, --help Display help - --verbose Display verbose output - --no-lint Skip chart linting - --no-install Skip chart installation - --all Lint/install all charts - --charts Lint/install: - a standalone chart (e. g. stable/nginx) - a list of charts (e. g. stable/nginx,stable/cert-manager) - --config Path to the config file (optional) - -- End of all options -EOF -} - -main() { - local verbose= - local no_install= - local no_lint= - local config= - local all= - local charts= - - while :; do - case "${1:-}" in - -h|--help) - show_help - exit - ;; - --verbose) - verbose=true - ;; - --no-install) - no_install=true - ;; - --no-lint) - no_lint=true - ;; - --all) - all=true - ;; - --charts) - if [ -n "$2" ]; then - charts="$2" - shift - else - echo "ERROR: '--charts' cannot be empty." >&2 - exit 1 - fi - ;; - --config) - if [ -n "$2" ]; then - config="$2" - shift - else - echo "ERROR: '--config' cannot be empty." >&2 - exit 1 - fi - ;; - -?*) - echo "WARN: Unknown option (ignored): $1" >&2 - ;; - *) - break - ;; - esac - - shift - done - - if [[ -n "$config" ]]; then - if [[ -f "$config" ]]; then - # shellcheck disable=SC1090 - source "$config" - else - echo "ERROR: Specified config file does not exist: $config" >&2 - exit 1 - fi - fi - - if [[ "$all" == "true" || -n "$charts" ]]; then - export CHECK_VERSION_INCREMENT=false - fi - - # shellcheck source=lib/chartlib.sh - source "$SCRIPT_DIR/lib/chartlib.sh" - - [[ -n "$verbose" ]] && set -o xtrace - - pushd "$REPO_ROOT" > /dev/null - - for dir in "${CHART_DIRS[@]}"; do - if [[ ! -d "$dir" ]]; then - chartlib::error "Configured charts directory '$dir' does not exist" - exit 1 - fi - done - - local exit_code=0 - - if [[ "$all" == "true" ]]; then - read -ra changed_dirs <<< "$(chartlib::read_directories)" - elif [[ -n "$charts" ]]; then - charts="${charts//,/ }" - read -ra changed_dirs <<< "${charts}" - else - read -ra changed_dirs <<< "$(chartlib::detect_changed_directories)" - fi - - if [[ -n "${changed_dirs[*]}" ]]; then - echo "Charts to be installed and tested: ${changed_dirs[*]}" - - chartlib::init_helm - - local summary=() - - for chart_dir in "${changed_dirs[@]}"; do - echo '' - echo '--------------------------------------------------------------------------------' - echo " Processing chart '$chart_dir'..." - echo '--------------------------------------------------------------------------------' - echo '' - - local error= - - if [[ -z "$no_lint" ]]; then - if ! chartlib::validate_chart "$chart_dir"; then - error=true - fi - if ! chartlib::lint_chart_with_all_configs "$chart_dir"; then - error=true - fi - fi - - if [[ -z "$no_install" && -z "$error" ]]; then - if ! chartlib::install_chart_with_all_configs "$chart_dir"; then - error=true - fi - fi - - if [[ -z "$error" ]]; then - summary+=(" ✔︎ $chart_dir") - else - summary+=(" ✖︎ $chart_dir") - exit_code=1 - fi - done - else - summary+=('No chart changes detected.') - fi - - echo '--------------------------------------------------------------------------------' - for line in "${summary[@]}"; do - echo "$line" - done - echo '--------------------------------------------------------------------------------' - - popd > /dev/null - - exit "$exit_code" -} - -main "$@" diff --git a/code-of-conduct.md b/code-of-conduct.md deleted file mode 100644 index 0d15c00c..00000000 --- a/code-of-conduct.md +++ /dev/null @@ -1,3 +0,0 @@ -# Kubernetes Community Code of Conduct - -Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) diff --git a/examples/docker-for-mac/my_test.sh b/examples/docker-for-mac/my_test.sh index 9a6a811e..bbc6fcd2 100755 --- a/examples/docker-for-mac/my_test.sh +++ b/examples/docker-for-mac/my_test.sh @@ -18,7 +18,7 @@ set -o errexit set -o nounset set -o pipefail -readonly IMAGE_TAG=v1.1.0 +readonly IMAGE_TAG=v0.0.0 readonly IMAGE_REPOSITORY="quay.io/helmpack/chart-testing" main() { @@ -74,9 +74,8 @@ configure_kubectl() { run_test() { git remote add k8s https://github.com/helm/charts.git &> /dev/null || true git fetch k8s - docker exec "$testcontainer_id" chart_test.sh --config test/.testenv - - echo "Done Testing!" + docker exec "$testcontainer_id" ct lint --chart-dirs stable,incubator --remote k8s + docker exec "$testcontainer_id" ct install --chart-dirs stable,incubator --remote k8s } main diff --git a/examples/gke/Dockerfile b/examples/gke/Dockerfile index 16aaf258..3241a2e8 100644 --- a/examples/gke/Dockerfile +++ b/examples/gke/Dockerfile @@ -12,10 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM quay.io/helmpack/chart-testing:v1.1.0 +FROM quay.io/helmpack/chart-testing:v2.0.0-beta.1 ENV PATH /google-cloud-sdk/bin:$PATH -ARG CLOUD_SDK_VERSION=200.0.0 +ARG CLOUD_SDK_VERSION=221.0.0 RUN curl -LO "https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-$CLOUD_SDK_VERSION-linux-x86_64.tar.gz" && \ tar xzf "google-cloud-sdk-$CLOUD_SDK_VERSION-linux-x86_64.tar.gz" && \ rm "google-cloud-sdk-$CLOUD_SDK_VERSION-linux-x86_64.tar.gz" && \ diff --git a/examples/gke/my_test.sh b/examples/gke/my_test.sh index ce537211..e7847170 100755 --- a/examples/gke/my_test.sh +++ b/examples/gke/my_test.sh @@ -19,6 +19,7 @@ set -o nounset set -o pipefail readonly IMAGE_REPOSITORY="myrepo/chart-testing" +readonly IMAGE_REPOSITORY="v1.0.0" readonly REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel)}" main() { @@ -36,8 +37,6 @@ main() { docker exec "$config_container_id" gcloud container clusters get-credentials my-cluster --project my-project --zone us-west1-a docker exec "$config_container_id" kubectl cluster-info docker exec "$config_container_id" chart_test.sh --config /workdir/.testenv - - echo "Done Testing!" } main diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..65dfa1fe --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/helm/chart-testing + +require ( + github.com/BurntSushi/toml v0.3.1 // indirect + github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e + github.com/Masterminds/semver v1.4.2 + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/pkg/errors v0.8.0 + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/cobra v0.0.3 + github.com/spf13/pflag v1.0.2 + github.com/spf13/viper v1.2.0 + github.com/stretchr/testify v1.2.2 + gopkg.in/yaml.v2 v2.2.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..4a45407f --- /dev/null +++ b/go.sum @@ -0,0 +1,46 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e h1:eb0Pzkt15Bm7f2FFYv7sjY7NPFi3cPkS3tv1CcrFBWA= +github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= +github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +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/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KHeTRY+7I= +github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg= +github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= +github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc= +github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.2.0 h1:M4Rzxlu+RgU4pyBRKhKaVN1VeYOm8h2jgyXnAseDgCc= +github.com/spf13/viper v1.2.0/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 h1:BH3eQWeGbwRU2+wxxuuPOdFBmaiBH81O8BugSjHeTFg= +golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/lib/chartlib.sh b/lib/chartlib.sh deleted file mode 100644 index dd3c4a82..00000000 --- a/lib/chartlib.sh +++ /dev/null @@ -1,565 +0,0 @@ -#!/usr/bin/env bash - -# Copyright 2018 The Helm Authors. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -#     http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -o errexit -set -o nounset -set -o pipefail -shopt -s nullglob - - -readonly REMOTE="${REMOTE:-origin}" -readonly TARGET_BRANCH="${TARGET_BRANCH:-master}" -readonly TIMEOUT="${TIMEOUT:-300}" -readonly LINT_CONF="${LINT_CONF:-/testing/etc/lintconf.yaml}" -readonly CHART_YAML_SCHEMA="${CHART_YAML_SCHEMA:-/testing/etc/chart_schema.yaml}" -readonly VALIDATE_MAINTAINERS="${VALIDATE_MAINTAINERS:-true}" -readonly GITHUB_INSTANCE="${GITHUB_INSTANCE:-https://github.com}" -readonly CHECK_VERSION_INCREMENT="${CHECK_VERSION_INCREMENT:-true}" - -# Special handling for arrays -[[ -z "${CHART_DIRS[*]}" ]] && CHART_DIRS=(charts); readonly CHART_DIRS -[[ -z "${EXCLUDED_CHARTS[*]}" ]] && EXCLUDED_CHARTS=(); readonly EXCLUDED_CHARTS -[[ -z "${CHART_REPOS[*]}" ]] && CHART_REPOS=(); readonly CHART_REPOS - -echo -if [[ "$CHECK_VERSION_INCREMENT" == false ]]; then - echo '--------------------------------------------------------------------------------' - echo " SKIPPING VERSION INCREMENT CHECK!" -fi -echo '--------------------------------------------------------------------------------' -echo ' Environment:' -echo " REMOTE=$REMOTE" -echo " TARGET_BRANCH=$TARGET_BRANCH" -echo " CHART_DIRS=${CHART_DIRS[*]}" -echo " EXCLUDED_CHARTS=${EXCLUDED_CHARTS[*]}" -echo " CHART_REPOS=${CHART_REPOS[*]}" -echo " TIMEOUT=$TIMEOUT" -echo " LINT_CONF=$LINT_CONF" -echo " CHART_YAML_SCHEMA=$CHART_YAML_SCHEMA" -echo " VALIDATE_MAINTAINERS=$VALIDATE_MAINTAINERS" -echo " GITHUB_INSTANCE=$GITHUB_INSTANCE" -echo " CHECK_VERSION_INCREMENT=$CHECK_VERSION_INCREMENT" -echo '--------------------------------------------------------------------------------' -echo - -# Read chart directories to be used with --force -chartlib::read_directories() { - local dir - - while read -r dir; do - local excluded= - for excluded_dir in "${EXCLUDED_CHARTS[@]}"; do - if [[ "$dir" == "$excluded_dir" ]]; then - excluded=true - break - fi - done - if [[ -z "$excluded" && -d "$dir" ]]; then - changed_dirs=("${changed_dirs[@]}" "$dir") - fi - done < <(find "${CHART_DIRS[@]}" -mindepth 1 -maxdepth 1 -type d | awk -F/ '{ print $1"/"$2 }' | uniq) - - echo "${changed_dirs[@]}" -} - -# Detects chart directories that have changes against the -# target branch ("$REMOTE/$TARGET_BRANCH"). -chartlib::detect_changed_directories() { - local merge_base - merge_base="$(git merge-base "$REMOTE/$TARGET_BRANCH" HEAD)" - - local changed_dirs=() - local dir - - while read -r dir; do - local excluded= - for excluded_dir in "${EXCLUDED_CHARTS[@]}"; do - if [[ "$dir" == "$excluded_dir" ]]; then - excluded=true - break - fi - done - if [[ -z "$excluded" && -d "$dir" ]]; then - changed_dirs=("${changed_dirs[@]}" "$dir") - fi - done < <(git diff --find-renames --name-only "$merge_base" "${CHART_DIRS[@]}" | awk -F/ '{ print $1"/"$2 }' | uniq) - - echo "${changed_dirs[@]}" -} - -# Initializes the Helm client and add configured repos. -chartlib::init_helm() { - echo 'Initializing Helm client...' - - helm init --client-only - - for repo in "${CHART_REPOS[@]}"; do - local name="${repo%%=*}" - local url="${repo#*=}" - - helm repo add "$name" "$url" - done -} - -# Checks a chart for a version bump comparing the version from Chart.yaml -# with that from the target branch. -# Args: -# $1 The chart directory -chartlib::check_for_version_bump() { - local chart_dir="${1?Chart directory is required}" - - echo "Checking chart '$chart_dir' for a version bump..." - - # Check if chart exists on taget branch - if ! git cat-file -e "$REMOTE/$TARGET_BRANCH:$chart_dir/Chart.yaml" > /dev/null 2>&1; then - echo "Unable to find chart on master. New chart detected." - return 0 - fi - - # Compare version of chart under test with that on the target branch - - local old_version - old_version=$(yq -r .version <(git show "$REMOTE/$TARGET_BRANCH:$chart_dir/Chart.yaml")) - echo "Chart version on" "$REMOTE/$TARGET_BRANCH" ":" "$old_version" - - local new_version - new_version=$(yq -r .version "$chart_dir/Chart.yaml") - echo "New chart version: " "$new_version" - - # Pre-releases may not be API compatible. So, when tools compare versions - # they often skip pre-releases. vert can force looking at pre-releases by - # adding a dash on the end followed by pre-release. -0 on the end will force - # looking for all valid pre-releases since a pre-release cannot start with a 0. - # For example, 1.2.3-0 will include looking for pre-releases. - if [[ $old_version == *-* ]]; then # Found the - to denote it has a pre-release - if vert ">$old_version" "$new_version"; then - echo "Chart version ok. Version bumped." - return 0 - fi - else - # No pre-release was found so we increment the patch version and attach a - # -0 to enable pre-releases being found. - local old_version_array - read -ra old_version_array <<< "${old_version//./ }" # Turn the version into an array - - (( old_version_array[2] += 1 )) # Increment the patch release - if vert ">${old_version_array[0]}.${old_version_array[1]}.${old_version_array[2]}-0" "$new_version"; then - echo "Chart version ok. Version bumped." - return 0 - fi - fi - - chartlib::error "Chart version not ok. Needs a version bump." - return 1 -} - -# Validates the Chart.yaml against a YAML schema. -# Args: -# $1 The chart directory -chartlib::validate_chart_yaml() { - local chart_dir="${1?Chart directory is required}" - - echo "Validating Chart.yaml" - yamale --schema "$CHART_YAML_SCHEMA" "$chart_dir/Chart.yaml" -} - -# Validates maintainer names in Chart.yaml to be valid Github users. -# Args: -# $1 The chart directory -chartlib::validate_maintainers() { - local chart_dir="${1?Chart directory is required}" - - echo "Validating maintainers" - - # We require maintainers for non-deprecated charts - local deprecated - deprecated=$(yq -r '.deprecated // empty' "$chart_dir/Chart.yaml") - - local maintainers - maintainers=$(yq -r '.maintainers // empty' "$chart_dir/Chart.yaml") - - if [[ -n "$deprecated" ]]; then - if [[ -n "$maintainers" ]]; then - chartlib::error "Deprecated charts must not have any maintainers in 'Chart.yaml'." - return 1 - else - return 0 - fi - else - if [[ -z "$maintainers" ]]; then - echo "No maintainers found in 'Chart.yaml'." - fi - fi - - while read -r name; do - echo "Verifying maintainer '$name'..." - if [[ $(curl --silent --output /dev/null --write-out "%{http_code}" --fail --head "$GITHUB_INSTANCE/$name") -ne 200 ]]; then - chartlib::error "'$name' is not a valid GitHub account. Please use a valid Github account to help us communicate with maintainers in PRs/issues." - return 1 - fi - done < <(yq -r '.maintainers[].name' "$chart_dir/Chart.yaml") -} - -# Lints a YAML file. -# Args: -# $1 The YAML file to lint -chartlib::lint_yaml_file() { - local file="${1?Specify YAML file for linting}" - - echo "Linting '$file'..." - - if [[ -f "$file" ]]; then - yamllint --config-file "$LINT_CONF" "$file" - else - chartlib::error "File '$file' does not exist." - return 1 - fi -} - -# Validates a chart: -# - Checks for a version bump -# - Lints Chart.yaml and values.yaml -# - Validates Chart.yaml against schema -# - Validates maintainers -# Args: -# $1 The chart directory -chartlib::validate_chart() { - local chart_dir="${1?Chart directory is required}" - local error= - - echo "Validating chart '$chart_dir'..." - - if [[ "$CHECK_VERSION_INCREMENT" == true ]]; then - chartlib::check_for_version_bump "$chart_dir" || error=true - else - echo "Skipping version increment check!" - fi - - chartlib::lint_yaml_file "$chart_dir/Chart.yaml" || error=true - chartlib::lint_yaml_file "$chart_dir/values.yaml" || error=true - chartlib::validate_chart_yaml "$chart_dir" || error=true - - if [[ "$VALIDATE_MAINTAINERS" == true ]]; then - chartlib::validate_maintainers "$chart_dir" || error=true - fi - - if [[ -n "$error" ]]; then - chartlib::error 'Chart validation failed.' - return 1 - fi -} - -# Lints a chart. -# Args: -# $1 The chart directory -# $2 A custom values file for the chart installation (optional) -chartlib::lint_chart_with_single_config() { - local chart_dir="${1?Chart directory is required}" - local values_file="${2:-}" - - echo "Building dependencies for chart '$chart_dir'..." - helm dependency build "$chart_dir" - - if [[ -n "$values_file" ]]; then - echo "Using custom values file '$values_file'..." - - echo "Linting chart '$chart_dir'..." - helm lint "$chart_dir" --values "$values_file" - else - echo "Chart does not provide test values. Using defaults..." - - echo "Linting chart '$chart_dir'..." - helm lint "$chart_dir" - fi -} - -# Installs and tests a chart. The release and the namespace are -# automatically deleted afterwards. -# Args: -# $1 The chart directory -# $2 The release name for the chart to be installed -# $3 The namespace to install the chart in -# $4 A custom values file for the chart installation (optional) -chartlib::install_chart_with_single_config() { - local chart_dir="${1?Chart directory is required}" - local release="${2?Release is required}" - local namespace="${3?Namespace is required}" - local values_file="${4:-}" - - # Capture subshell output - exec 3>&1 - - if ! ( - set -o errexit - - # Run in subshell so we can use a trap within the function. - trap 'chartlib::print_pod_details_and_logs "$namespace" || true; chartlib::delete_release "$release" || true; chartlib::delete_namespace "$namespace" || true' EXIT - - echo "Building dependencies for chart '$chart_dir'..." - helm dependency build "$chart_dir" - - echo "Installing chart '$chart_dir' into namespace '$namespace'..." - - if [[ -n "$values_file" ]]; then - echo "Using custom values file '$values_file'..." - helm install "$chart_dir" --name "$release" --namespace "$namespace" --wait --timeout "$TIMEOUT" --values "$values_file" - else - echo "Chart does not provide test values. Using defaults..." - helm install "$chart_dir" --name "$release" --namespace "$namespace" --wait --timeout "$TIMEOUT" - fi - - local error= - - # For deployments --wait may not be sufficient because it looks at 'maxUnavailable' which is 0 by default. - for deployment in $(kubectl get deployments --namespace "$namespace" --output jsonpath='{.items[*].metadata.name}'); do - kubectl rollout status "deployment/$deployment" --namespace "$namespace" - - # 'kubectl rollout status' does not return a non-zero exit code when rollouts fail. - # We, thus, need to double-check here. - - local jsonpath='{.status.conditions[?(@.type=="Ready")].status}' - - for pod in $(chartlib::get_pods_for_deployment "$deployment" "$namespace"); do - ready=$(kubectl get pod "$pod" --namespace "$namespace" --output jsonpath="$jsonpath") - if [[ "$ready" != "True" ]]; then - chartlib::error "Pod '$pod' did not reach ready state!" - error=true - fi - done - done - - if [[ -n "$error" ]]; then - return 1 - fi - - echo "Testing chart '$chart_dir' in namespace '$namespace'..." - helm test "$release" --cleanup --timeout "$TIMEOUT" - - ) >&3; then - - chartlib::error "Chart installation failed: $chart_dir" - return 1 - fi -} - -# Returns the pods that are governed by a deployment. -# Args: -# $1 The name of the deployment -# $2 The namespace -chartlib::get_pods_for_deployment() { - local deployment="${1?Deployment is required}" - local namespace="${2?Namespace is required}" - - local jq_filter='.spec.selector.matchLabels | to_entries | .[] | "\(.key)=\(.value)"' - - local selectors - mapfile -t selectors < <(kubectl get deployment "$deployment" --namespace "$namespace" --output=json | jq -r "$jq_filter") - - local selector - selector=$(chartlib::join_by , "${selectors[@]}") - - kubectl get pods --selector "$selector" --namespace "$namespace" --output jsonpath='{.items[*].metadata.name}' -} - -# Lints a chart for all custom values files matching '*.values.yaml' -# in the 'ci' subdirectory. -# Args: -# $1 The chart directory -chartlib::lint_chart_with_all_configs() { - local chart_dir="${1?Chart directory is required}" - local error= - - local has_test_values= - for values_file in "$chart_dir"/ci/*-values.yaml; do - has_test_values=true - chartlib::lint_chart_with_single_config "$chart_dir" "$values_file" || error=true - done - - if [[ -z "$has_test_values" ]]; then - chartlib::lint_chart_with_single_config "$chart_dir" || error=true - fi - - if [[ -n "$error" ]]; then - return 1 - fi -} - -# Installs a chart for all custom values files matching '*.values.yaml' -# in the 'ci' subdirectory. If no custom values files are found, the chart -# is installed with defaults. If $BUILD_ID is set, it is used as -# name for the namespace to install the chart in. Otherwise, the chart -# name is taken as the namespace name. Namespace and release are suffixed with -# an index. Releases and namespaces are automatically deleted afterwards. -# Args: -# $1 The chart directory -chartlib::install_chart_with_all_configs() { - local chart_dir="${1?Chart directory is required}" - local error= - local index=0 - - # Generate suffix 10 long and cut release name to 16 long, as in case of long release name - # it was causing StatefulSet with long names to create pods - # bug https://github.com/kubernetes/kubernetes/issues/64023 - local release - release=$(yq -r .name < "$chart_dir/Chart.yaml" | cut -c-16) - - local random_suffix - random_suffix=$(tr -dc a-z0-9 < /dev/urandom | fold -w 10 | head -n 1) - - local namespace="${BUILD_ID:-"$release"}-$random_suffix" - local release="$release-$random_suffix" - - local has_test_values= - for values_file in "$chart_dir"/ci/*-values.yaml; do - has_test_values=true - chartlib::install_chart_with_single_config "$chart_dir" "$release-$index" "$namespace-$index" "$values_file" || error=true - ((index += 1)) - done - - if [[ -z "$has_test_values" ]]; then - chartlib::install_chart_with_single_config "$chart_dir" "$release" "$namespace" || error=true - fi - - if [[ -n "$error" ]]; then - return 1 - fi -} - -# Prints log for all pods in the specified namespace. -# Args: -# $1 The namespace -chartlib::print_pod_details_and_logs() { - local namespace="${1?Namespace is required}" - - kubectl get pods --show-all --no-headers --namespace "$namespace" | awk '{ print $1 }' | while read -r pod; do - if [[ -n "$pod" ]]; then - printf '\n================================================================================\n' - printf ' Details from pod %s\n' "$pod" - printf '================================================================================\n' - - printf '\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n' - printf ' Description of pod %s\n' "$pod" - printf '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n' - - kubectl describe pod --namespace "$namespace" "$pod" || true - - printf '\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n' - printf ' End of description for pod %s\n' "$pod" - printf '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n' - - local init_containers - init_containers=$(kubectl get pods --show-all --output jsonpath="{.spec.initContainers[*].name}" --namespace "$namespace" "$pod") - for container in $init_containers; do - printf -- '\n--------------------------------------------------------------------------------\n' - printf ' Logs of init container %s in pod %s\n' "$container" "$pod" - printf -- '--------------------------------------------------------------------------------\n\n' - - kubectl logs --namespace "$namespace" --container "$container" "$pod" || true - - printf -- '\n--------------------------------------------------------------------------------\n' - printf ' End of logs of init container %s in pod %s\n' "$container" "$pod" - printf -- '--------------------------------------------------------------------------------\n' - done - - local containers - containers=$(kubectl get pods --show-all --output jsonpath="{.spec.containers[*].name}" --namespace "$namespace" "$pod") - for container in $containers; do - printf '\n--------------------------------------------------------------------------------\n' - printf -- ' Logs of container %s in pod %s\n' "$container" "$pod" - printf -- '--------------------------------------------------------------------------------\n\n' - - kubectl logs --namespace "$namespace" --container "$container" "$pod" || true - - printf -- '\n--------------------------------------------------------------------------------\n' - printf ' End of logs of container %s in pod %s\n' "$container" "$pod" - printf -- '--------------------------------------------------------------------------------\n' - done - - printf '\n================================================================================\n' - printf ' End of details for pod %s\n' "$pod" - printf '================================================================================\n\n' - fi - done -} - -# Deletes a release. -# Args: -# $1 The name of the release to delete -chartlib::delete_release() { - local release="${1?Release is required}" - - echo "Deleting release '$release'..." - helm delete --purge "$release" --timeout "$TIMEOUT" -} - -# Deletes a namespace. -# Args: -# $1 The namespace to delete -chartlib::delete_namespace() { - local namespace="${1?Namespace is required}" - - echo "Deleting namespace '$namespace'..." - kubectl delete namespace "$namespace" - - echo -n "Waiting for namespace '$namespace' to terminate..." - - local max_retries=30 - local retry=0 - local sleep_time_sec=3 - while ((retry < max_retries)); do - sleep "$sleep_time_sec" - ((retry++)) - - if ! kubectl get namespace "$namespace" &> /dev/null; then - echo - echo "Namespace '$namespace' terminated." - return 0 - fi - - echo -n '.' - done - - echo - - chartlib::error "Namespace '$namespace' not terminated after $((max_retries * sleep_time_sec)) s." - - echo "Force-deleting pods..." - kubectl delete pods --namespace "$namespace" --all --force --grace-period 0 || true - - sleep 3 - - if ! kubectl get namespace "$namespace" &> /dev/null; then - echo "Force-deleting namespace '$namespace'..." - kubectl delete namespace "$namespace" --ignore-not-found --force --grace-period 0 || true - fi -} - -# Logs an error. -# Args: -# $1 The error message -chartlib::error() { - printf '\e[31mERROR: %s\n\e[39m' "$1" >&2 -} - -# Joins strings by a delimiters -# Args: -# $1 The delimiter -# $* Additional args to join by the delimiter -chartlib::join_by() { - local IFS="$1" - shift - echo "$*" -} diff --git a/pkg/chart/chart.go b/pkg/chart/chart.go new file mode 100644 index 00000000..1df268ea --- /dev/null +++ b/pkg/chart/chart.go @@ -0,0 +1,597 @@ +// Copyright © 2018 The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package chart + +import ( + "fmt" + "path" + "path/filepath" + "strings" + + "github.com/helm/chart-testing/pkg/config" + "github.com/helm/chart-testing/pkg/tool" + "github.com/helm/chart-testing/pkg/util" + "github.com/pkg/errors" +) + +// Git is the Interface that wraps Git operations. +// +// FileExistsOnBranch checks whether file exists on the specified remote/branch. +// +// Show returns the contents of file on the specified remote/branch. +// +// MergeBase returns the SHA1 of the merge base of commit1 and commit2. +// +// ListChangedFilesInDirs diffs commit against HEAD and returns changed files for the specified dirs. +// +// GetUrlForRemote returns the repo URL for the specified remote. +type Git interface { + FileExistsOnBranch(file string, remote string, branch string) bool + Show(file string, remote string, branch string) (string, error) + MergeBase(commit1 string, commit2 string) (string, error) + ListChangedFilesInDirs(commit string, dirs ...string) ([]string, error) + GetUrlForRemote(remote string) (string, error) +} + +// Helm is the interface that wraps Helm operations +// +// Init runs client-side Helm initialization +// +// AddRepo adds a chart repository to the local Helm configuration +// +// BuildDependencies builds the chart's dependencies +// +// Lint runs `helm lint` for the given chart +// +// LintWithValues runs `helm lint` for the given chart using the specified values file +// +// Install runs `helm install` for the given chart +// +// InstallWithValues runs `helm install` for the given chart using the specified values file +// +// DeleteRelease purges the specified Helm release. +type Helm interface { + Init() error + AddRepo(name string, url string) error + BuildDependencies(chart string) error + Lint(chart string) error + LintWithValues(chart string, valuesFile string) error + Install(chart string, namespace string, release string) error + InstallWithValues(chart string, valuesFile string, namespace string, release string) error + DeleteRelease(release string) +} + +// Kubectl is the interface that wraps kubectl operations +// +// DeleteNamespace deletes a namespace +// +// WaitForDeployments waits for a deployment to become ready +// +// GetPodsforDeployment gets all pods for a deployment +// +// GetPods gets pods for the given args +// +// DescribePod prints the pod's description +// +// Logs prints the logs of container +// +// GetInitContainers gets all init containers of pod +// +// GetContainers gets all containers of pod +type Kubectl interface { + DeleteNamespace(namespace string) + WaitForDeployments(namespace string) error + GetPodsforDeployment(namespace string, deployment string) ([]string, error) + GetPods(args ...string) ([]string, error) + DescribePod(namespace string, pod string) error + Logs(namespace string, pod string, container string) error + GetInitContainers(namespace string, pod string) ([]string, error) + GetContainers(namespace string, pod string) ([]string, error) +} + +// Linter is the interface that wrap linting operations +// +// YamlLint runs `yamllint` on the specified file with the specified configuration +// +// Yamale runs `yamale` on the specified file with the specified schema file +type Linter interface { + YamlLint(yamlFile string, configFile string) error + Yamale(yamlFile string, schemaFile string) error +} + +// DiretoryLister is the interface +// +// ListChildDirs lists direct child directories of parentDir given they pass the test function +type DirectoryLister interface { + ListChildDirs(parentDir string, test func(string) bool) ([]string, error) +} + +// ChartUtils is the interface that wraps chart-related methods +// +// IsChartdir checks if a directory is a chart directory +// +// ReadChartYaml reads the `Chart.yaml` from the specified directory +type ChartUtils interface { + IsChartDir(dir string) bool + ReadChartYaml(dir string) (*util.ChartYaml, error) +} + +// AccountValidator is the interface that wraps Git account validation +// +// Validate checks if account is valid on repoDomain +type AccountValidator interface { + Validate(repoDomain string, account string) error +} + +type Testing struct { + config config.Configuration + helm Helm + kubectl Kubectl + git Git + linter Linter + accountValidator AccountValidator + directoryLister DirectoryLister + chartUtils ChartUtils +} + +// TestResults holds results and overall status +type TestResults struct { + OverallSuccess bool + TestResults []TestResult +} + +// TestResult holds test results for a specific chart +type TestResult struct { + Chart string + Error error +} + +// NewTesting creates a new Testing struct with the given config. +func NewTesting(config config.Configuration) Testing { + kubectl := tool.NewKubectl() + testing := Testing{ + config: config, + helm: tool.NewHelm(kubectl, config.Timeout, config.TillerNamespace), + git: tool.NewGit(), + kubectl: kubectl, + linter: tool.NewLinter(), + accountValidator: tool.AccountValidator{}, + directoryLister: util.DirectoryLister{}, + chartUtils: util.ChartUtils{}, + } + return testing +} + +func (t *Testing) processCharts(action func(chart string, valuesFiles []string) TestResult) ([]TestResult, error) { + var results []TestResult + charts, err := t.FindChartsToBeProcessed() + if err != nil { + return nil, errors.Wrap(err, "Error identifying charts to process") + } else if len(charts) == 0 { + return results, nil + } + + fmt.Println() + printDelimiterLine("-") + fmt.Println(" Charts to be processed:") + printDelimiterLine("-") + for _, chart := range charts { + fmt.Printf(" %s\n", chart) + } + printDelimiterLine("-") + fmt.Println() + + if err := t.helm.Init(); err != nil { + return nil, errors.Wrap(err, "Error initializing Helm") + } + + for _, repo := range t.config.ChartRepos { + repoSlice := strings.SplitN(repo, "=", 2) + name := repoSlice[0] + url := repoSlice[1] + if err := t.helm.AddRepo(name, url); err != nil { + return nil, errors.Wrapf(err, "Error adding repo: %s=%s", name, url) + } + } + + testResults := TestResults{ + OverallSuccess: true, + TestResults: results, + } + + for _, chart := range charts { + valuesFiles := t.FindValuesFilesForCI(chart) + + if err := t.helm.BuildDependencies(chart); err != nil { + return nil, errors.Wrapf(err, "Error building dependencies for chart '%s'", chart) + } + + result := action(chart, valuesFiles) + if result.Error != nil { + testResults.OverallSuccess = false + } + results = append(results, result) + } + if testResults.OverallSuccess { + return results, nil + } + + return results, errors.New("Error processing charts") +} + +// LintCharts lints charts (changed, all, specific) depending on the configuration. +func (t *Testing) LintCharts() ([]TestResult, error) { + return t.processCharts(t.LintChart) +} + +// InstallCharts install charts (changed, all, specific) depending on the configuration. +func (t *Testing) InstallCharts() ([]TestResult, error) { + return t.processCharts(t.InstallChart) +} + +// LintAndInstallChart first lints and then installs charts (changed, all, specific) depending on the configuration. +func (t *Testing) LintAndInstallCharts() ([]TestResult, error) { + return t.processCharts(t.LintAndInstallChart) +} + +// PrintResults writes test results to stdout. +func (t *Testing) PrintResults(results []TestResult) { + printDelimiterLine("-") + if results != nil { + for _, result := range results { + err := result.Error + if err != nil { + fmt.Printf(" %s %s > %s\n", "✖︎", result.Chart, err) + } else { + fmt.Printf(" %s %s\n", "✔︎", result.Chart) + } + } + } else { + fmt.Println("No chart changes detected.") + } + printDelimiterLine("-") +} + +// LintChart lints the specified chart. +func (t *Testing) LintChart(chart string, valuesFiles []string) TestResult { + fmt.Printf("Linting chart '%s'\n", chart) + + result := TestResult{Chart: chart} + + if t.config.CheckVersionIncrement { + if err := t.CheckVersionIncrement(chart); err != nil { + result.Error = err + return result + } + } + + chartYaml := path.Join(chart, "Chart.yaml") + valuesYaml := path.Join(chart, "values.yaml") + + if err := t.linter.Yamale(chartYaml, t.config.ChartYamlSchema); err != nil { + result.Error = err + return result + } + if err := t.linter.YamlLint(chartYaml, t.config.LintConf); err != nil { + result.Error = err + return result + } + if err := t.linter.YamlLint(valuesYaml, t.config.LintConf); err != nil { + result.Error = err + return result + } + + if err := t.ValidateMaintainers(chart); err != nil { + result.Error = err + return result + } + + if len(valuesFiles) > 0 { + for _, valuesFile := range valuesFiles { + if err := t.helm.LintWithValues(chart, valuesFile); err != nil { + result.Error = err + break + } + } + } else { + if err := t.helm.Lint(chart); err != nil { + result.Error = err + } + } + + return result +} + +// InstallChart installs the specified chart into a new namespace, waits for resources to become ready, and eventually +// uninstalls it and deletes the namespace again. +func (t *Testing) InstallChart(chart string, valuesFiles []string) TestResult { + fmt.Printf("Installing chart '%s'...\n", chart) + + result := TestResult{Chart: chart} + + if len(valuesFiles) > 0 { + for _, valuesFile := range valuesFiles { + release, namespace := util.CreateInstallParams(chart, t.config.BuildId) + + defer t.kubectl.DeleteNamespace(namespace) + defer t.helm.DeleteRelease(release) + defer t.PrintPodDetailsAndLogs(namespace) + + if err := t.helm.InstallWithValues(chart, valuesFile, namespace, release); err != nil { + result.Error = err + break + } + } + } else { + release, namespace := util.CreateInstallParams(chart, t.config.BuildId) + + defer t.kubectl.DeleteNamespace(namespace) + defer t.helm.DeleteRelease(release) + defer t.PrintPodDetailsAndLogs(namespace) + + if err := t.helm.Install(chart, namespace, release); err != nil { + result.Error = err + } + } + + return result +} + +// LintAndInstallChart first lints and then installs the specified chart. +func (t *Testing) LintAndInstallChart(chart string, valuesFiles []string) TestResult { + result := t.LintChart(chart, valuesFiles) + if result.Error != nil { + return result + } + return t.InstallChart(chart, valuesFiles) +} + +// FindChartsToBeProcessed identifies charts to be processed depending on the configuration +// (changed charts, all charts, or specific charts). +func (t *Testing) FindChartsToBeProcessed() ([]string, error) { + cfg := t.config + if cfg.ProcessAllCharts { + return t.ReadAllChartDirectories() + } else if len(cfg.Charts) > 0 { + return t.config.Charts, nil + } + return t.ComputeChangedChartDirectories() +} + +// FindValuesFilesForCI returns all files in the 'ci' subfolder of the chart directory matching the pattern '*-values.yaml' +func (t *Testing) FindValuesFilesForCI(chart string) []string { + ciDir := path.Join(chart, "ci/*-values.yaml") + matches, _ := filepath.Glob(ciDir) + return matches +} + +// ComputeChangedChartDirectories takes the merge base of HEAD and the configured remote and target branch and computes a +// slice of changed charts from that in the configured chart directories excluding those configured to be excluded. +func (t *Testing) ComputeChangedChartDirectories() ([]string, error) { + cfg := t.config + + mergeBase, err := t.git.MergeBase(fmt.Sprintf("%s/%s", cfg.Remote, cfg.TargetBranch), "HEAD") + if err != nil { + return nil, errors.Wrap(err, "Could not determined changed charts: Error identifying merge base.") + } + allChangedChartFiles, err := t.git.ListChangedFilesInDirs(mergeBase, cfg.ChartDirs...) + if err != nil { + return nil, errors.Wrap(err, "Could not determined changed charts: Error icreating diff.") + } + + var changedChartDirs []string + for _, file := range allChangedChartFiles { + pathElements := strings.SplitN(filepath.ToSlash(file), "/", 3) + if util.StringSliceContains(cfg.ExcludedCharts, pathElements[1]) { + continue + } + dir := path.Join(pathElements[0], pathElements[1]) + // Only add if not already in list and double-check if it is a chart directory + if !util.StringSliceContains(changedChartDirs, dir) && t.chartUtils.IsChartDir(dir) { + changedChartDirs = append(changedChartDirs, dir) + } + } + + return changedChartDirs, nil +} + +// ReadAllChartDirectories returns a slice of all charts in the configured chart directories except those +// configured to be excluded. +func (t *Testing) ReadAllChartDirectories() ([]string, error) { + cfg := t.config + + var chartDirs []string + for _, chartParentDir := range cfg.ChartDirs { + dirs, err := t.directoryLister.ListChildDirs(chartParentDir, + func(dir string) bool { + return t.chartUtils.IsChartDir(dir) && !util.StringSliceContains(cfg.ExcludedCharts, path.Base(dir)) + }) + if err != nil { + return nil, errors.Wrap(err, "Error reading chart directories") + } + chartDirs = append(chartDirs, dirs...) + } + + return chartDirs, nil +} + +// CheckVersionIncrement checks that the new chart version is greater than the old one using semantic version comparison. +func (t *Testing) CheckVersionIncrement(chart string) error { + fmt.Printf("Checking chart '%s' for a version bump...\n", chart) + + oldVersion, err := t.GetOldChartVersion(chart) + if err != nil { + return err + } + if oldVersion == "" { + // new chart, skip version check + return nil + } + + fmt.Println("Old chart version:", oldVersion) + + newVersion, err := t.GetNewChartVersion(chart) + if err != nil { + return err + } + fmt.Println("New chart version:", newVersion) + + result, err := util.CompareVersions(oldVersion, newVersion) + if err != nil { + return err + } + + if result >= 0 { + return errors.New("Chart version not ok. Needs a version bump!") + } + + fmt.Println("Chart version ok.") + return nil +} + +// GetOldChartVersion gets the version of the old Chart.yaml file from the target branch. +func (t *Testing) GetOldChartVersion(chart string) (string, error) { + cfg := t.config + + chartYamlFile := path.Join(chart, "Chart.yaml") + if !t.git.FileExistsOnBranch(chartYamlFile, cfg.Remote, cfg.TargetBranch) { + fmt.Printf("Unable to find chart on %s. New chart detected.\n", cfg.TargetBranch) + return "", nil + } + + chartYamlContents, err := t.git.Show(chartYamlFile, cfg.Remote, cfg.TargetBranch) + if err != nil { + return "", errors.Wrap(err, "Error reading old Chart.yaml") + } + + chartYaml, err := util.ReadChartYaml([]byte(chartYamlContents)) + if err != nil { + return "", errors.Wrap(err, "Error reading old chart version") + } + + return chartYaml.Version, nil +} + +// GetNewChartVersion gets the new version from the currently checked out Chart.yaml file. +func (t *Testing) GetNewChartVersion(chart string) (string, error) { + chartYaml, err := t.chartUtils.ReadChartYaml(chart) + if err != nil { + return "", errors.Wrap(err, "Error reading new chart version") + } + return chartYaml.Version, nil +} + +// ValidateMaintainers validates maintainers in the Chart.yaml file. Maintainer names must be valid accounts +// (GitHub, Bitbucket, GitLab) names. Deprecated charts must not have maintainers. +func (t *Testing) ValidateMaintainers(chart string) error { + fmt.Println("Validating maintainers...") + + chartYaml, err := t.chartUtils.ReadChartYaml(chart) + if err != nil { + return err + } + + if chartYaml.Deprecated { + if len(chartYaml.Maintainers) > 0 { + return errors.New("Deprecated chart must not have maintainers") + } + return nil + } + + if len(chartYaml.Maintainers) == 0 { + return errors.New("Chart doesn't have maintainers") + } + + repoUrl, err := t.git.GetUrlForRemote(t.config.Remote) + if err != nil { + return err + } + + for _, maintainer := range chartYaml.Maintainers { + if err := t.accountValidator.Validate(repoUrl, maintainer.Name); err != nil { + return err + } + } + + return nil +} + +func (t *Testing) PrintPodDetailsAndLogs(namespace string) { + pods, err := t.kubectl.GetPods("--no-headers", "--namespace", namespace, "--output", "jsonpath={.items[*].metadata.name}") + if err != nil { + fmt.Println("Error printing logs:", err) + return + } + + printDelimiterLine("=") + + for _, pod := range pods { + printDetails(pod, "Description of pod", "~", func(item string) error { + return t.kubectl.DescribePod(namespace, pod) + }, pod) + + initContainers, err := t.kubectl.GetInitContainers(namespace, pod) + if err != nil { + fmt.Println("Error printing logs:", err) + return + } + + printDetails(pod, "Logs of init container", "-", + func(item string) error { + return t.kubectl.Logs(namespace, pod, item) + }, initContainers...) + + containers, err := t.kubectl.GetContainers(namespace, pod) + if err != nil { + fmt.Println("Error printing logs:", err) + return + } + + printDetails(pod, "Logs of container", "-", + func(item string) error { + return t.kubectl.Logs(namespace, pod, item) + }, + containers...) + } + + printDelimiterLine("=") +} + +func printDetails(pod string, text string, delimiterChar string, printFunc func(item string) error, items ...string) { + for _, item := range items { + item = strings.Trim(item, "'") + + printDelimiterLine(delimiterChar) + fmt.Printf("==> %s %s\n", text, pod) + printDelimiterLine(delimiterChar) + + if err := printFunc(item); err != nil { + fmt.Println("Error printing details:", err) + return + } + + printDelimiterLine(delimiterChar) + fmt.Printf("<== %s %s\n", text, pod) + printDelimiterLine(delimiterChar) + } +} + +func printDelimiterLine(delimiterChar string) { + delim := make([]string, 120) + for i := 0; i < 120; i++ { + delim[i] = delimiterChar + } + fmt.Println(strings.Join(delim, "")) +} diff --git a/pkg/chart/chart_test.go b/pkg/chart/chart_test.go new file mode 100644 index 00000000..c65a7587 --- /dev/null +++ b/pkg/chart/chart_test.go @@ -0,0 +1,147 @@ +// Copyright © 2018 The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package chart + +import ( + "fmt" + "strings" + "testing" + + "github.com/pkg/errors" + + "github.com/helm/chart-testing/pkg/util" + + "github.com/helm/chart-testing/pkg/config" + "github.com/stretchr/testify/assert" +) + +type fakeGit struct{} + +func (g fakeGit) FileExistsOnBranch(file string, remote string, branch string) bool { + return true +} + +func (g fakeGit) Show(file string, remote string, branch string) (string, error) { + return "", nil +} + +func (g fakeGit) MergeBase(commit1 string, commit2 string) (string, error) { + return "", nil +} + +func (g fakeGit) ListChangedFilesInDirs(commit string, dirs ...string) ([]string, error) { + return []string{ + "incubator/excluded/Chart.yaml", + "incubator/excluded/values.yaml", + "incubator/bar/README.md", + "incubator/bar/README.md", + "incubator/excluded/templates/configmap.yaml", + "incubator/excluded/values.yaml", + "stable/blah/Chart.yaml", + "stable/blah/README.md", + "stable/this-is-no-chart-dir/foo.md", + }, nil +} + +func (g fakeGit) GetUrlForRemote(remote string) (string, error) { + return "git@github.com/helm/chart-testing", nil +} + +type fakeDirLister struct{} + +func (l fakeDirLister) ListChildDirs(parentDir string, test func(dir string) bool) ([]string, error) { + if parentDir == "stable" { + var dirs []string + for _, dir := range []string{"stable/foo", "stable/excluded"} { + if test(dir) { + dirs = append(dirs, dir) + } + } + return dirs, nil + } + return []string{"incubator/bar"}, nil +} + +type fakeChartUtils struct{} + +func (v fakeChartUtils) IsChartDir(dir string) bool { + return dir != "stable/this-is-no-chart-dir" +} + +func (v fakeChartUtils) ReadChartYaml(dir string) (*util.ChartYaml, error) { + chartUtils := util.ChartUtils{} + return chartUtils.ReadChartYaml(dir) +} + +type fakeAccountValidator struct{} + +func (v fakeAccountValidator) Validate(repoDomain string, account string) error { + if strings.HasPrefix(account, "valid") { + return nil + } + return errors.New(fmt.Sprintf("Error validating account: %s", account)) +} + +var ct Testing + +func init() { + cfg := config.Configuration{ + ExcludedCharts: []string{"excluded"}, + ChartDirs: []string{"stable", "incubator"}, + } + ct = Testing{ + config: cfg, + directoryLister: fakeDirLister{}, + git: fakeGit{}, + chartUtils: fakeChartUtils{}, + accountValidator: fakeAccountValidator{}, + } +} + +func TestComputeChangedChartDirectories(t *testing.T) { + actual, err := ct.ComputeChangedChartDirectories() + expected := []string{"incubator/bar", "stable/blah"} + assert.Nil(t, err) + assert.Equal(t, actual, expected) +} + +func TestReadAllChartDirectories(t *testing.T) { + actual, err := ct.ReadAllChartDirectories() + expected := []string{"stable/foo", "incubator/bar"} + assert.Nil(t, err) + assert.Equal(t, actual, expected) +} + +func TestValidateMaintainers(t *testing.T) { + var testDataSlice = []struct { + name string + chartDir string + expected bool + }{ + {"valid", "testdata/valid_maintainers", true}, + {"invalid", "testdata/invalid_maintainers", false}, + {"no-maintainers", "testdata/no_maintainers", false}, + {"empty-maintainers", "testdata/empty_maintainers", false}, + {"valid-deprecated", "testdata/valid_maintainers_deprecated", false}, + {"no-maintainers-deprecated", "testdata/no_maintainers_deprecated", true}, + } + + for _, testData := range testDataSlice { + t.Run(testData.name, func(t *testing.T) { + err := ct.ValidateMaintainers(testData.chartDir) + assert.Equal(t, testData.expected, err == nil) + }) + } +} diff --git a/pkg/chart/testdata/empty_maintainers/Chart.yaml b/pkg/chart/testdata/empty_maintainers/Chart.yaml new file mode 100644 index 00000000..d2983d52 --- /dev/null +++ b/pkg/chart/testdata/empty_maintainers/Chart.yaml @@ -0,0 +1 @@ +maintainers: [] diff --git a/pkg/chart/testdata/invalid_maintainers/Chart.yaml b/pkg/chart/testdata/invalid_maintainers/Chart.yaml new file mode 100644 index 00000000..2b909340 --- /dev/null +++ b/pkg/chart/testdata/invalid_maintainers/Chart.yaml @@ -0,0 +1,5 @@ +maintainers: + - name: invalid + email: invalid@example.com + - name: valid + email: valid@example.com diff --git a/pkg/chart/testdata/no_maintainers/Chart.yaml b/pkg/chart/testdata/no_maintainers/Chart.yaml new file mode 100644 index 00000000..e69de29b diff --git a/pkg/chart/testdata/no_maintainers_deprecated/Chart.yaml b/pkg/chart/testdata/no_maintainers_deprecated/Chart.yaml new file mode 100644 index 00000000..6274ee99 --- /dev/null +++ b/pkg/chart/testdata/no_maintainers_deprecated/Chart.yaml @@ -0,0 +1 @@ +deprecated: true diff --git a/pkg/chart/testdata/valid_maintainers/Chart.yaml b/pkg/chart/testdata/valid_maintainers/Chart.yaml new file mode 100644 index 00000000..bf964e7f --- /dev/null +++ b/pkg/chart/testdata/valid_maintainers/Chart.yaml @@ -0,0 +1,5 @@ +maintainers: + - name: valid + email: valid@example.com + - name: valid-too + email: valid-too@example.com diff --git a/pkg/chart/testdata/valid_maintainers_deprecated/Chart.yaml b/pkg/chart/testdata/valid_maintainers_deprecated/Chart.yaml new file mode 100644 index 00000000..788d468a --- /dev/null +++ b/pkg/chart/testdata/valid_maintainers_deprecated/Chart.yaml @@ -0,0 +1,6 @@ +deprecated: true +maintainers: + - name: valid + email: valid@example.com + - name: valid-too + email: valid-too@example.com diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 00000000..a849bb02 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,121 @@ +// Copyright © 2018 The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "fmt" + "path" + "strings" + "time" + + "github.com/helm/chart-testing/pkg/util" + "github.com/pkg/errors" + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + "github.com/spf13/viper" +) + +var configSearchLocations = []string{"/etc/ct", "$HOME/ct", "."} + +type Configuration struct { + Remote string `mapstructure:"remote"` + TargetBranch string `mapstructure:"target-branch"` + BuildId string `mapstructure:"build-id"` + TillerNamespace string `mapstructure:"tiller-namespace"` + LintConf string `mapstructure:"lint-conf"` + ChartYamlSchema string `mapstructure:"chart-yaml-schema"` + ValidateMaintainers bool `mapstructure:"validate-maintainers"` + CheckVersionIncrement bool `mapstructure:"check-version-increment"` + ProcessAllCharts bool `mapstructure:"all"` + Charts []string `mapstructure:"charts"` + ChartRepos []string `mapstructure:"chart-repos"` + ChartDirs []string `mapstructure:"chart-dirs"` + ExcludedCharts []string `mapstructure:"excluded-charts"` + Timeout time.Duration `mapstructure:"timeout"` +} + +func LoadConfiguration(cfgFile string, cmd *cobra.Command, bindFlagsFunc ...func(flagSet *flag.FlagSet, viper *viper.Viper) error) (*Configuration, error) { + v := viper.New() + for _, bindFunc := range bindFlagsFunc { + if err := bindFunc(cmd.Flags(), v); err != nil { + return nil, errors.Wrap(err, "Error binding flags") + } + } + + v.AutomaticEnv() + v.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + v.SetEnvPrefix("CT") + + if cfgFile != "" { + v.SetConfigFile(cfgFile) + } else { + v.SetConfigName("ct") + for _, searchLocation := range configSearchLocations { + v.AddConfigPath(searchLocation) + } + } + + if err := v.ReadInConfig(); err != nil { + if cfgFile != "" { + // Only error out for specified config file. Ignore for default locations. + return nil, errors.Wrap(err, "Error loading config file") + } + } else { + fmt.Println("Using config file: ", v.ConfigFileUsed()) + } + + cfg := &Configuration{} + if err := v.Unmarshal(cfg); err != nil { + return nil, errors.Wrap(err, "Error unmarshaling configuration") + } + + if cfg.ProcessAllCharts && len(cfg.Charts) > 0 { + return nil, errors.New("Specifying both, '--all' and '--charts', is not allowed!") + } + + if strings.Contains(cmd.Use, "lint") { + chartYamlSchemaPath := cfg.ChartYamlSchema + if chartYamlSchemaPath == "" { + var err error + cfgFile, err = findConfigFile("chart_schema.yaml") + if err != nil { + return nil, errors.New("'chart_schema.yaml' neither specified nor found in default locations") + } + cfg.ChartYamlSchema = cfgFile + } + + lintConfPath := cfg.LintConf + if lintConfPath == "" { + var err error + cfgFile, err = findConfigFile("lintconf.yaml") + if err != nil { + return nil, errors.New("'lintconf.yaml' neither specified nor found in default locations") + } + cfg.LintConf = cfgFile + } + } + + return cfg, nil +} + +func findConfigFile(fileName string) (string, error) { + for _, location := range configSearchLocations { + filePath := path.Join(location, fileName) + if util.FileExists(filePath) { + return filePath, nil + } + } + return "", errors.New("config file not found") +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 00000000..59e9d74c --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,49 @@ +// Copyright © 2018 The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package config + +import ( + "testing" + "time" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalYaml(t *testing.T) { + loadAndAssertConfigFromFile(t, "test_config.yaml") +} + +func TestUnmarshalJson(t *testing.T) { + loadAndAssertConfigFromFile(t, "test_config.json") +} + +func loadAndAssertConfigFromFile(t *testing.T, configFile string) { + cfg, _ := LoadConfiguration(configFile, &cobra.Command{}) + + require.Equal(t, "origin", cfg.Remote) + require.Equal(t, "master", cfg.TargetBranch) + require.Equal(t, "pr-42", cfg.BuildId) + require.Equal(t, "tiller", cfg.TillerNamespace) + require.Equal(t, "my-lint-conf.yaml", cfg.LintConf) + require.Equal(t, "my-chart-yaml-schema.yaml", cfg.ChartYamlSchema) + require.Equal(t, true, cfg.ValidateMaintainers) + require.Equal(t, true, cfg.CheckVersionIncrement) + require.Equal(t, false, cfg.ProcessAllCharts) + require.Equal(t, []string{"incubator=https://incubator"}, cfg.ChartRepos) + require.Equal(t, []string{"stable", "incubator"}, cfg.ChartDirs) + require.Equal(t, []string{"common"}, cfg.ExcludedCharts) + require.Equal(t, 5*time.Minute, cfg.Timeout) +} diff --git a/pkg/config/test_config.json b/pkg/config/test_config.json new file mode 100644 index 00000000..e2f3eda0 --- /dev/null +++ b/pkg/config/test_config.json @@ -0,0 +1,23 @@ +{ + "remote": "origin", + "target-branch": "master", + "tiller-namespace": "tiller", + "build-id": "pr-42", + "lint-conf": "my-lint-conf.yaml", + "chart-yaml-schema": "my-chart-yaml-schema.yaml", + "github-instance": "https://github.com", + "validate-maintainers": true, + "check-version-increment": true, + "all": false, + "chart-repos": [ + "incubator=https://incubator" + ], + "chart-dirs": [ + "stable", + "incubator" + ], + "excluded-charts": [ + "common" + ], + "timeout": "300s" +} diff --git a/pkg/config/test_config.yaml b/pkg/config/test_config.yaml new file mode 100644 index 00000000..2465bdd8 --- /dev/null +++ b/pkg/config/test_config.yaml @@ -0,0 +1,18 @@ +remote: origin +target-branch: master +tiller-namespace: tiller +build-id: pr-42 +lint-conf: my-lint-conf.yaml +chart-yaml-schema: my-chart-yaml-schema.yaml +github-instance: https://github.com +validate-maintainers: true +check-version-increment: true +all: false +chart-repos: + - incubator=https://incubator +chart-dirs: + - stable + - incubator +excluded-charts: + - common +timeout: 300s diff --git a/pkg/exec/exec.go b/pkg/exec/exec.go new file mode 100644 index 00000000..58030445 --- /dev/null +++ b/pkg/exec/exec.go @@ -0,0 +1,86 @@ +// Copyright © 2018 The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package exec + +import ( + "bufio" + "fmt" + "io" + "os/exec" + "strings" + + "github.com/helm/chart-testing/pkg/util" + "github.com/pkg/errors" +) + +type ProcessExecutor struct{} + +func (p ProcessExecutor) RunProcessAndCaptureOutput(executable string, execArgs ...interface{}) (string, error) { + return p.RunProcessInDirAndCaptureOutput("", executable, execArgs) +} + +func (p ProcessExecutor) RunProcessInDirAndCaptureOutput(workingDirectory string, executable string, execArgs ...interface{}) (string, error) { + args, err := util.Flatten(execArgs) + fmt.Println(">>>", executable, strings.Join(args, " ")) + if err != nil { + return "", errors.Wrap(err, "Invalid arguments supplied") + } + cmd := exec.Command(executable, args...) + cmd.Dir = workingDirectory + bytes, err := cmd.CombinedOutput() + + if err != nil { + return "", errors.Wrap(err, "Error running process") + } + return strings.TrimSpace(string(bytes)), nil +} + +func (p ProcessExecutor) RunProcess(executable string, execArgs ...interface{}) error { + args, err := util.Flatten(execArgs) + fmt.Println(">>>", executable, strings.Join(args, " ")) + if err != nil { + return errors.Wrap(err, "Invalid arguments supplied") + } + cmd := exec.Command(executable, args...) + + outReader, err := cmd.StdoutPipe() + if err != nil { + return errors.Wrap(err, "Error getting StdoutPipe for command") + } + + errReader, err := cmd.StderrPipe() + if err != nil { + return errors.Wrap(err, "Error getting StderrPipe for command") + } + + scanner := bufio.NewScanner(io.MultiReader(outReader, errReader)) + go func() { + for scanner.Scan() { + fmt.Println(scanner.Text()) + } + }() + + err = cmd.Start() + if err != nil { + return errors.Wrap(err, "Error running process") + } + + err = cmd.Wait() + if err != nil { + return errors.Wrap(err, "Error waiting for process") + } + + return nil +} diff --git a/pkg/tool/account.go b/pkg/tool/account.go new file mode 100644 index 00000000..ffa39645 --- /dev/null +++ b/pkg/tool/account.go @@ -0,0 +1,45 @@ +// Copyright © 2018 The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tool + +import ( + "fmt" + "github.com/pkg/errors" + "net/http" + "regexp" +) + +type AccountValidator struct{} + +var repoDomainPattern = regexp.MustCompile("(?:https://|git@)([^/:]+)") + +func (v AccountValidator) Validate(repoUrl string, account string) error { + domain := parseOutGitRepoDomain(repoUrl) + url := fmt.Sprintf("https://%s/%s", domain, account) + response, err := http.Head(url) + if err != nil { + return errors.Wrap(err, "Error validating maintainers") + } + if response.StatusCode != 200 { + return errors.New(fmt.Sprintf("Error validating maintainer '%s': %s", account, response.Status)) + } + return nil +} + +func parseOutGitRepoDomain(repoUrl string) string { + // This works for GitHub, Bitbucket, and Gitlab + submatch := repoDomainPattern.FindStringSubmatch(repoUrl) + return submatch[1] +} diff --git a/pkg/tool/account_test.go b/pkg/tool/account_test.go new file mode 100644 index 00000000..59eaae2c --- /dev/null +++ b/pkg/tool/account_test.go @@ -0,0 +1,28 @@ +package tool + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestParseOutGitDomain(t *testing.T) { + var testDataSlice = []struct { + name string + repoUrl string + expected string + }{ + {"GitHub SSH", "git@github.com:foo/bar", "github.com"}, + {"GitHub HTTPS", "https://github.com/foo/bar", "github.com"}, + {"Gitlab SSH", "git@gitlab.com:foo/bar", "gitlab.com"}, + {"Gitlab HTTPS", "https://gitlab.com/foo/bar", "gitlab.com"}, + {"Bitbucket SSH", "git@bitbucket.com:foo/bar", "bitbucket.com"}, + {"Bitbucket HTTPS", "https://bitbucket.com/foo/bar", "bitbucket.com"}, + } + + for _, testData := range testDataSlice { + t.Run(testData.name, func(t *testing.T) { + actual := parseOutGitRepoDomain(testData.repoUrl) + assert.Equal(t, testData.expected, actual) + }) + } +} diff --git a/pkg/tool/git.go b/pkg/tool/git.go new file mode 100644 index 00000000..9de5c6bc --- /dev/null +++ b/pkg/tool/git.go @@ -0,0 +1,61 @@ +// Copyright © 2018 The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tool + +import ( + "fmt" + "github.com/helm/chart-testing/pkg/exec" + "github.com/pkg/errors" + "strings" +) + +type Git struct { + exec exec.ProcessExecutor +} + +func NewGit() Git { + return Git{exec: exec.ProcessExecutor{}} +} + +func (g Git) FileExistsOnBranch(file string, remote string, branch string) bool { + fileSpec := fmt.Sprintf("%s/%s:%s", remote, branch, file) + _, err := g.exec.RunProcessAndCaptureOutput("git", "cat-file", "-e", fileSpec) + return err == nil +} + +func (g Git) Show(file string, remote string, branch string) (string, error) { + fileSpec := fmt.Sprintf("%s/%s:%s", remote, branch, file) + return g.exec.RunProcessAndCaptureOutput("git", "show", fileSpec) +} + +func (g Git) MergeBase(commit1 string, commit2 string) (string, error) { + return g.exec.RunProcessAndCaptureOutput("git", "merge-base", commit1, commit2) +} + +func (g Git) ListChangedFilesInDirs(commit string, dirs ...string) ([]string, error) { + changedChartFilesString, err := + g.exec.RunProcessAndCaptureOutput("git", "diff", "--find-renames", "--name-only", commit, "--", dirs) + if err != nil { + return nil, errors.Wrap(err, "Could not determined changed charts: Error creating diff.") + } + if changedChartFilesString == "" { + return nil, nil + } + return strings.Split(changedChartFilesString, "\n"), nil +} + +func (g Git) GetUrlForRemote(remote string) (string, error) { + return g.exec.RunProcessAndCaptureOutput("git", "ls-remote", "--get-url", remote) +} diff --git a/pkg/tool/helm.go b/pkg/tool/helm.go new file mode 100644 index 00000000..61571ddd --- /dev/null +++ b/pkg/tool/helm.go @@ -0,0 +1,89 @@ +// Copyright © 2018 The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tool + +import ( + "fmt" + "strconv" + "time" + + "github.com/helm/chart-testing/pkg/exec" +) + +type Helm struct { + exec exec.ProcessExecutor + kubectl Kubectl + timeout time.Duration + tillerNamespace string +} + +func NewHelm(kubectl Kubectl, timeout time.Duration, tillerNamespace string) Helm { + return Helm{ + exec: exec.ProcessExecutor{}, + kubectl: kubectl, + timeout: timeout, + tillerNamespace: tillerNamespace, + } +} + +func (h Helm) Init() error { + return h.exec.RunProcess("helm", "init", "--client-only") +} + +func (h Helm) AddRepo(name string, url string) error { + return h.exec.RunProcess("helm", "repo", "add", name, url) +} + +func (h Helm) BuildDependencies(chart string) error { + return h.exec.RunProcess("helm", "dependency", "build", chart) +} + +func (h Helm) Lint(chart string) error { + return h.exec.RunProcess("helm", "lint", chart) +} + +func (h Helm) LintWithValues(chart string, valuesFile string) error { + return h.exec.RunProcess("helm", "lint", chart, "--values", valuesFile) +} + +func (h Helm) Install(chart string, namespace string, release string) error { + return h.InstallWithValues(chart, "", namespace, release) +} + +func (h Helm) InstallWithValues(chart string, valuesFile string, namespace string, release string) error { + timeoutSec := strconv.FormatFloat(h.timeout.Seconds(), 'f', 0, 64) + var values []string + if valuesFile != "" { + values = []string{"--values", valuesFile} + } + + if err := h.exec.RunProcess("helm", "install", chart, "--name", release, "--namespace", namespace, + "--tiller-namespace", h.tillerNamespace, "--wait", "--timeout", timeoutSec, values); err != nil { + return err + } + + if err := h.exec.RunProcess("helm", "test", release, "--tiller-namespace", h.tillerNamespace, "--timeout", timeoutSec); err != nil { + return err + } + + return h.kubectl.WaitForDeployments(namespace) +} + +func (h Helm) DeleteRelease(release string) { + fmt.Printf("Deleting release '%s'...\n", release) + if err := h.exec.RunProcess("helm", "delete", "--purge", release); err != nil { + fmt.Println("Error deleting Helm release:", err) + } +} diff --git a/pkg/tool/kubectl.go b/pkg/tool/kubectl.go new file mode 100644 index 00000000..3fcdd38b --- /dev/null +++ b/pkg/tool/kubectl.go @@ -0,0 +1,138 @@ +package tool + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/helm/chart-testing/pkg/exec" + "github.com/pkg/errors" +) + +type Kubectl struct { + exec exec.ProcessExecutor +} + +func NewKubectl() Kubectl { + return Kubectl{ + exec: exec.ProcessExecutor{}, + } +} + +// DeleteNamespace deletes the specified namespace. If the namespace does not terminate within 90s, pods running in the +// namespace and, eventually, the namespace itself are force-deleted. +func (k Kubectl) DeleteNamespace(namespace string) { + fmt.Printf("Deleting namespace '%s'...\n", namespace) + timeoutSec := "120s" + if err := k.exec.RunProcess("kubectl", "delete", "namespace", namespace, "--timeout", timeoutSec); err != nil { + fmt.Printf("Namespace '%s' did not terminate after %s.", namespace, timeoutSec) + } + + if _, err := k.exec.RunProcessAndCaptureOutput("kubectl", "get", "namespace", namespace); err != nil { + fmt.Printf("Namespace '%s' terminated.\n", namespace) + return + } + + fmt.Printf("Namespace '%s' did not terminate after %s.", namespace, timeoutSec) + + fmt.Println("Force-deleting pods...") + if err := k.exec.RunProcess("kubectl", "delete", "pods", "--namespace", namespace, "--all", "--force", "--grace-period=0"); err != nil { + fmt.Println("Error deleting pods:", err) + } + + time.Sleep(3 * time.Second) + + if err := k.exec.RunProcess("kubectl", "get", "namespace", namespace); err != nil { + fmt.Printf("Force-deleting namespace '%s'...\n", namespace) + if err := k.exec.RunProcess("kubectl", "delete", "namespace", namespace, "--force", "--grace-period=0"); err != nil { + fmt.Println("Error deleting namespace:", err) + } + } +} + +func (k Kubectl) WaitForDeployments(namespace string) error { + output, err := k.exec.RunProcessAndCaptureOutput( + "kubectl", "get", "deployments", "--namespace", namespace, "--output", "jsonpath={.items[*].metadata.name}") + if err != nil { + return err + } + + deployments := strings.Fields(output) + for _, deployment := range deployments { + deployment = strings.Trim(deployment, "'") + err := k.exec.RunProcess("kubectl", "rollout", "status", "deployment", deployment, "--namespace", namespace) + if err != nil { + return err + } + + // 'kubectl rollout status' does not return a non-zero exit code when rollouts fail. + // We, thus, need to double-check here. + + pods, err := k.GetPodsforDeployment(namespace, deployment) + if err != nil { + return err + } + for _, pod := range pods { + pod = strings.Trim(pod, "'") + ready, err := k.exec.RunProcessAndCaptureOutput("kubectl", "get", "pod", pod, "--namespace", namespace, "--output", + `jsonpath={.status.conditions[?(@.type=="Ready")].status}`) + if err != nil { + return err + } + if ready != "True" { + return errors.New(fmt.Sprintf("Pods '%s' did not reach ready state!", pod)) + } + } + } + + return nil +} + +func (k Kubectl) GetPodsforDeployment(namespace string, deployment string) ([]string, error) { + jsonString, _ := k.exec.RunProcessAndCaptureOutput("kubectl", "get", "deployment", deployment, "--namespace", namespace, "--output=json") + var deploymentMap map[string]interface{} + err := json.Unmarshal([]byte(jsonString), &deploymentMap) + if err != nil { + return nil, err + } + + spec := deploymentMap["spec"].(map[string]interface{}) + selector := spec["selector"].(map[string]interface{}) + matchLabels := selector["matchLabels"].(map[string]interface{}) + var ls string + for name, value := range matchLabels { + if ls != "" { + ls += "," + } + ls += fmt.Sprintf("%s=%s", name, value) + } + + return k.GetPods("--selector", ls, "--namespace", namespace, "--output", "jsonpath={.items[*].metadata.name}") +} + +func (k Kubectl) GetPods(args ...string) ([]string, error) { + kubectlArgs := []string{"get", "pods"} + kubectlArgs = append(kubectlArgs, args...) + pods, err := k.exec.RunProcessAndCaptureOutput("kubectl", kubectlArgs) + if err != nil { + return nil, err + } + return strings.Fields(pods), nil +} + +func (k Kubectl) DescribePod(namespace string, pod string) error { + return k.exec.RunProcess("kubectl", "describe", "pod", pod, "--namespace", namespace) +} + +func (k Kubectl) Logs(namespace string, pod string, container string) error { + return k.exec.RunProcess("kubectl", "logs", pod, "--namespace", namespace, "--container", container) +} + +func (k Kubectl) GetInitContainers(namespace string, pod string) ([]string, error) { + return k.GetPods(pod, "--no-headers", "--namespace", namespace, "--output", "jsonpath={.spec.initContainers[*].name}") +} + +func (k Kubectl) GetContainers(namespace string, pod string) ([]string, error) { + return k.GetPods(pod, "--no-headers", "--namespace", namespace, "--output", "jsonpath={.spec.containers[*].name}") +} diff --git a/pkg/tool/linter.go b/pkg/tool/linter.go new file mode 100644 index 00000000..eccf6096 --- /dev/null +++ b/pkg/tool/linter.go @@ -0,0 +1,33 @@ +// Copyright © 2018 The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tool + +import "github.com/helm/chart-testing/pkg/exec" + +type Linter struct { + exec exec.ProcessExecutor +} + +func NewLinter() Linter { + return Linter{exec: exec.ProcessExecutor{}} +} + +func (l Linter) YamlLint(yamlFile string, configFile string) error { + return l.exec.RunProcess("yamllint", "--config-file", configFile, yamlFile) +} + +func (l Linter) Yamale(yamlFile string, schemaFile string) error { + return l.exec.RunProcess("yamale", "--schema", schemaFile, yamlFile) +} diff --git a/pkg/util/util.go b/pkg/util/util.go new file mode 100644 index 00000000..27db420c --- /dev/null +++ b/pkg/util/util.go @@ -0,0 +1,166 @@ +// Copyright © 2018 The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "fmt" + "github.com/Masterminds/semver" + "github.com/pkg/errors" + "gopkg.in/yaml.v2" + "io/ioutil" + "math/rand" + "os" + "path" + "time" +) + +const chars = "1234567890abcdefghijklmnopqrstuvwxyz" + +type Maintainer struct { + Name string `yaml:"name"` + Email string `yaml:"email"` +} + +type ChartYaml struct { + Name string `yaml:"name"` + Version string `yaml:"version"` + Deprecated bool `yaml:"deprecated"` + Maintainers []Maintainer +} + +func Flatten(items []interface{}) ([]string, error) { + return doFlatten([]string{}, items) +} + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +func doFlatten(result []string, items interface{}) ([]string, error) { + var err error + + switch v := items.(type) { + case string: + result = append(result, v) + case []string: + result = append(result, v...) + case []interface{}: + for _, item := range v { + result, err = doFlatten(result, item) + if err != nil { + return nil, err + } + } + default: + return nil, errors.New(fmt.Sprintf("Flatten does not support %T", v)) + } + + return result, err +} + +func StringSliceContains(slice []string, s string) bool { + for _, element := range slice { + if s == element { + return true + } + } + return false +} + +func FileExists(file string) bool { + if _, err := os.Stat(file); err != nil { + return false + } + return true +} + +// RandomString string creates a random string of numbers and lower-case ascii characters with the specified length. +func RandomString(length int) string { + n := len(chars) + bytes := make([]byte, length) + for i := range bytes { + bytes[i] = chars[rand.Intn(n)] + } + return string(bytes) +} + +type DirectoryLister struct{} + +// ListChildDirs lists subdirectories of parentDir matching the test function. +func (l DirectoryLister) ListChildDirs(parentDir string, test func(dir string) bool) ([]string, error) { + fileInfos, err := ioutil.ReadDir(parentDir) + if err != nil { + return nil, err + } + + var dirs []string + for _, dir := range fileInfos { + dirName := dir.Name() + parentSlashChildDir := path.Join(parentDir, dirName) + if test(parentSlashChildDir) { + dirs = append(dirs, parentSlashChildDir) + } + } + + return dirs, nil +} + +type ChartUtils struct{} + +func (u ChartUtils) IsChartDir(dir string) bool { + return FileExists(path.Join(dir, "Chart.yaml")) +} + +func (u ChartUtils) ReadChartYaml(dir string) (*ChartYaml, error) { + yamlBytes, err := ioutil.ReadFile(path.Join(dir, "Chart.yaml")) + if err != nil { + return nil, errors.Wrap(err, "Could not read 'Chart.yaml'") + } + return ReadChartYaml(yamlBytes) +} + +func ReadChartYaml(yamlBytes []byte) (*ChartYaml, error) { + chartYaml := &ChartYaml{} + + if err := yaml.Unmarshal(yamlBytes, chartYaml); err != nil { + return nil, errors.Wrap(err, "Could not unmarshal 'Chart.yaml'") + } + + return chartYaml, nil +} + +func CompareVersions(left string, right string) (int, error) { + leftVersion, err := semver.NewVersion(left) + if err != nil { + return 0, errors.Wrap(err, "Error parsing semantic version") + } + rightVersion, err := semver.NewVersion(right) + if err != nil { + return 0, errors.Wrap(err, "Error parsing semantic version") + } + return leftVersion.Compare(rightVersion), nil +} + +func CreateInstallParams(chart string, buildId string) (release string, namespace string) { + release = path.Base(chart) + namespace = release + if buildId != "" { + namespace += buildId + } + randomSuffix := RandomString(10) + release = fmt.Sprintf("%s-%s", release, randomSuffix) + namespace = fmt.Sprintf("%s-%s", namespace, randomSuffix) + return +} diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go new file mode 100644 index 00000000..758d0ebd --- /dev/null +++ b/pkg/util/util_test.go @@ -0,0 +1,67 @@ +// Copyright © 2018 The Helm Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestFlatten(t *testing.T) { + var testDataSlice = []struct { + input []interface{} + expected []string + }{ + {[]interface{}{"foo", "bar", []string{"bla", "blubb"}}, []string{"foo", "bar", "bla", "blubb"}}, + {[]interface{}{"foo", "bar", "bla", "blubb"}, []string{"foo", "bar", "bla", "blubb"}}, + {[]interface{}{"foo", "bar", []interface{}{"bla", []string{"blubb"}}}, []string{"foo", "bar", "bla", "blubb"}}, + {[]interface{}{"foo", 42, []interface{}{"bla", []string{"blubb"}}}, nil}, + } + + for index, testData := range testDataSlice { + t.Run(string(index), func(t *testing.T) { + actual, err := Flatten(testData.input) + assert.Equal(t, testData.expected, actual) + if testData.expected != nil { + assert.Nil(t, err) + } else { + assert.NotNil(t, err) + } + }) + } +} + +func TestCompareVersions(t *testing.T) { + var testDataSlice = []struct { + oldVersion string + newVersion string + expected int + }{ + {"1.2.3", "1.2.4+2", -1}, + {"1+foo", "1+bar", 0}, + {"1.4-beta", "1.3", 1}, + {"1.3-beta", "1.3", -1}, + {"1", "2", -1}, + {"3", "3", 0}, + {"3-alpha", "3-beta", -1}, + } + + for index, testData := range testDataSlice { + t.Run(string(index), func(t *testing.T) { + actual, _ := CompareVersions(testData.oldVersion, testData.newVersion) + assert.Equal(t, testData.expected, actual) + }) + } +} diff --git a/tag.sh b/tag.sh new file mode 100755 index 00000000..efbcd3fc --- /dev/null +++ b/tag.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash + +# Copyright 2018 The Helm Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +#     http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -o errexit +set -o nounset +set -o pipefail + +readonly SCRIPT_DIR=$(dirname "$(readlink -f "$0")") + +show_help() { +cat << EOF +Usage: $(basename "$0") + +Create and push a tag. + + -h, --help Display help + -d, --debug Display verbose output + -r, --remote The name of the remote to push the tag to (default: upstream) + -f, --force Force an existing tag to be overwritten + -t, --tag The name of the tag to create + -s, --skip-push Skip pushing the tag +EOF +} + +main() { + local debug= + local tag= + local remote=upstream + local force=() + local skip_push= + + while :; do + case "${1:-}" in + -h|--help) + show_help + exit + ;; + -d|--debug) + debug=true + ;; + -t|--tag) + if [ -n "${2:-}" ]; then + tag="$2" + shift + else + echo "ERROR: '--tag' cannot be empty." >&2 + show_help + exit 1 + fi + ;; + -r|--remote) + if [ -n "${2:-}" ]; then + remote="$2" + shift + else + echo "ERROR: '--remote' cannot be empty." >&2 + show_help + exit 1 + fi + ;; + -f|--force) + force+=(--force) + ;; + -s|--skip-push) + skip_push=true + ;; + *) + break + ;; + esac + + shift + done + + if [[ -z "$tag" ]]; then + echo "ERROR: --tag is required!" >&2 + show_help + exit 1 + fi + + if [[ -n "$debug" ]]; then + set -x + fi + + pushd "$SCRIPT_DIR" > /dev/null + + git tag -a -m "Release $tag" "$tag" "${force[@]}" + + if [[ -z "$skip_push" ]]; then + git push "$remote" "refs/tags/$tag" "${force[@]}" + fi + + popd > /dev/null +} + +main "$@"