1
0
mirror of https://github.com/helm/chart-testing.git synced 2026-02-05 09:45:14 +01:00

Re-write it in Go (#35)

* Re-write it in Go

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Fix loading config from home dir

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Print config

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Remove git gc test code

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Remove year in copyright header

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Add alias for lint-and-install

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Fix examples

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Remove OWNERS file

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Add docs generation

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Update CircleCI

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Update readme

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Document building and releasing

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>
Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Remove Makefile

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Hide doc-gen command

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Add support for Helm extra args

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Update tool dependencies

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Update Goreleaser

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Upgrade pip

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Update Gopkg.lock

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Add log messages

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Fix CircleCI env var for tag

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Add Docker login

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Readme update for MacOS (#1)

* Add build.sh mac prerequisites, and README markdown linting fixes

Signed-off-by: Scott Rigby <scott@r6by.com>

* Update README.md

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>
Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Update Gopkg.lock

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Update config search locations

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Add config files to distro

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Add debug flag

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Add note on config files for linting

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Fix link

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Revert "Update Gopkg.lock"

This reverts commit fcbfbdc9db.

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Fix link

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>

* Fix readme

Signed-off-by: Reinhard Nägele <unguiculus@gmail.com>
This commit is contained in:
Reinhard Nägele
2018-11-07 19:06:20 +01:00
committed by Scott Rigby
parent 8e23ec2d74
commit f632cd5081
50 changed files with 3075 additions and 1021 deletions

View File

@@ -1,6 +1,6 @@
version: 2
jobs:
build:
lint:
docker:
- image: koalaman/shellcheck-alpine
steps:
@@ -8,7 +8,50 @@ 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
build:
docker:
- image: golang:1.11.1-alpine3.8
working_directory: /go/src/github.com/helm/chart-testing
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.93.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
curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
- checkout
- run:
name: Build
command: |
set -e
set -u
if [[ -z "${CIRCLE_TAG:-}" ]]; then
echo "Building snapshot..."
./build.sh
else
echo "Building release $CIRCLE_TAG..."
echo $DOCKER_PASSWORD | docker login --username $DOCKER_USERNAME --password-stdin quay.io
./build.sh --release
fi
workflows:
version: 2
untagged-build:
jobs:
- lint
- build
tagged-build:
jobs:
- build:
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/

View File

@@ -1,6 +1,4 @@
.circleci
.history
.idea
.editorconfig
.gitignore
.git
Dockerfile
.vscode

View File

@@ -9,6 +9,11 @@ trim_trailing_whitespace = true
insert_final_newline = true
charset = utf-8
[*.{yml,yaml}]
[*.{yml, yaml}]
indent_size = 2
[*.go]
indent_style = tab
[Makefile]
indent_style = tab

9
.gitignore vendored
View File

@@ -1,5 +1,8 @@
.history
.idea
.settings
.project
.classpath
.settings
.vscode
/dist
/config.*
/ct
vendor

53
.goreleaser.yml Normal file
View File

@@ -0,0 +1,53 @@
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
files:
- LICENSE
- README.md
- etc/chart_schema.yaml
- etc/lintconf.yaml
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
dockers:
- goos: linux
goarch: amd64
binary: ct
skip_push: false
dockerfile: Dockerfile
image_templates:
- quay.io/helmpack/chart-testing:{{ .Tag }}
- quay.io/helmpack/chart-testing: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

View File

@@ -1,23 +1,8 @@
# 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 \
@@ -25,16 +10,6 @@ RUN apk --no-cache add \
py-pip && \
pip install --upgrade pip==18.1
# Install YQ command line reader
ARG YQ_VERSION=2.7.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.12.1
RUN pip install "yamllint==$YAML_LINT_VERSION"
@@ -44,22 +19,23 @@ ARG YAMALE_VERSION=1.7.0
RUN pip install "yamale==$YAMALE_VERSION"
# Install kubectl
ARG KUBECTL_VERSION=1.12.2
RUN curl -LO "https://storage.googleapis.com/kubernetes-release/release/v$KUBECTL_VERSION/bin/linux/amd64/kubectl" && \
ARG KUBECTL_VERSION=v1.12.2
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.11.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"
rm -f "helm-$HELM_VERSION-linux-amd64.tar.gz"
COPY etc /testing/etc/
COPY lib /testing/lib/
COPY chart_test.sh /testing/
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

240
Gopkg.lock generated Normal file
View File

@@ -0,0 +1,240 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
digest = "1:6b250e53b3b7b9abd7e62f55875c234d2f8b77b846bff200fecafa2dc09af09a"
name = "github.com/MakeNowJust/heredoc"
packages = ["."]
pruneopts = ""
revision = "e9091a26100e9cfb2b6a8f470085bfa541931a91"
[[projects]]
digest = "1:b856d8248663c39265a764561c1a1a149783f6cc815feb54a1f3a591b91f6eca"
name = "github.com/Masterminds/semver"
packages = ["."]
pruneopts = ""
revision = "c7af12943936e8c39859482e61f0574c2fd7fc75"
version = "v1.4.2"
[[projects]]
digest = "1:982e2547680f9fd2212c6443ab73ea84eef40ee1cdcecb61d997de838445214c"
name = "github.com/cpuguy83/go-md2man"
packages = ["md2man"]
pruneopts = ""
revision = "20f5889cbdc3c73dbd2862796665e7c465ade7d1"
version = "v1.0.8"
[[projects]]
digest = "1:0deddd908b6b4b768cfc272c16ee61e7088a60f7fe2f06c547bd3d8e1f8b8e77"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
pruneopts = ""
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
version = "v1.1.1"
[[projects]]
digest = "1:eb53021a8aa3f599d29c7102e65026242bdedce998a54837dc67f14b6a97c5fd"
name = "github.com/fsnotify/fsnotify"
packages = ["."]
pruneopts = ""
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
version = "v1.4.7"
[[projects]]
digest = "1:d14365c51dd1d34d5c79833ec91413bfbb166be978724f15701e17080dc06dec"
name = "github.com/hashicorp/hcl"
packages = [
".",
"hcl/ast",
"hcl/parser",
"hcl/printer",
"hcl/scanner",
"hcl/strconv",
"hcl/token",
"json/parser",
"json/scanner",
"json/token",
]
pruneopts = ""
revision = "8cb6e5b959231cc1119e43259c4a608f9c51a241"
version = "v1.0.0"
[[projects]]
digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be"
name = "github.com/inconshreveable/mousetrap"
packages = ["."]
pruneopts = ""
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
version = "v1.0"
[[projects]]
digest = "1:961dc3b1d11f969370533390fdf203813162980c858e1dabe827b60940c909a5"
name = "github.com/magiconair/properties"
packages = ["."]
pruneopts = ""
revision = "c2353362d570a7bfa228149c62842019201cfb71"
version = "v1.8.0"
[[projects]]
digest = "1:096a8a9182648da3d00ff243b88407838902b6703fc12657f76890e08d1899bf"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
pruneopts = ""
revision = "ae18d6b8b3205b561c79e8e5f69bff09736185f4"
version = "v1.0.0"
[[projects]]
digest = "1:bcc46a0fbd9e933087bef394871256b5c60269575bb661935874729c65bbbf60"
name = "github.com/mitchellh/mapstructure"
packages = ["."]
pruneopts = ""
revision = "3536a929edddb9a5b34bd6861dc4a9647cb459fe"
version = "v1.1.2"
[[projects]]
digest = "1:894aef961c056b6d85d12bac890bf60c44e99b46292888bfa66caf529f804457"
name = "github.com/pelletier/go-toml"
packages = ["."]
pruneopts = ""
revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194"
version = "v1.2.0"
[[projects]]
digest = "1:7365acd48986e205ccb8652cc746f09c8b7876030d53710ea6ef7d0bd0dcd7ca"
name = "github.com/pkg/errors"
packages = ["."]
pruneopts = ""
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
pruneopts = ""
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
digest = "1:2761e287c811d0948d47d0252b82281eca3801eb3c9d5f9530956643d5b9f430"
name = "github.com/russross/blackfriday"
packages = ["."]
pruneopts = ""
revision = "05f3235734ad95d0016f6a23902f06461fcf567a"
version = "v1.5.2"
[[projects]]
digest = "1:d0431c2fd72e39ee43ea7742322abbc200c3e704c9102c5c3c2e2e667095b0ca"
name = "github.com/spf13/afero"
packages = [
".",
"mem",
]
pruneopts = ""
revision = "d40851caa0d747393da1ffb28f7f9d8b4eeffebd"
version = "v1.1.2"
[[projects]]
digest = "1:ae3493c780092be9d576a1f746ab967293ec165e8473425631f06658b6212afc"
name = "github.com/spf13/cast"
packages = ["."]
pruneopts = ""
revision = "8c9545af88b134710ab1cd196795e7f2388358d7"
version = "v1.3.0"
[[projects]]
digest = "1:a1403cc8a94b8d7956ee5e9694badef0e7b051af289caad1cf668331e3ffa4f6"
name = "github.com/spf13/cobra"
packages = [
".",
"doc",
]
pruneopts = ""
revision = "ef82de70bb3f60c65fb8eebacbb2d122ef517385"
version = "v0.0.3"
[[projects]]
digest = "1:9ceffa4ab5f7195ecf18b3a7fff90c837a9ed5e22e66d18069e4bccfe1f52aa0"
name = "github.com/spf13/jwalterweatherman"
packages = ["."]
pruneopts = ""
revision = "4a4406e478ca629068e7768fc33f3f044173c0a6"
version = "v1.0.0"
[[projects]]
digest = "1:cbaf13cdbfef0e4734ed8a7504f57fe893d471d62a35b982bf6fb3f036449a66"
name = "github.com/spf13/pflag"
packages = ["."]
pruneopts = ""
revision = "298182f68c66c05229eb03ac171abe6e309ee79a"
version = "v1.0.3"
[[projects]]
digest = "1:1ed7a19588d3b74fc6a45fe89f95aa980a34870342fb9c3183b19cad08b18a1e"
name = "github.com/spf13/viper"
packages = ["."]
pruneopts = ""
revision = "2c12c60302a5a0e62ee102ca9bc996277c2f64f5"
version = "v1.2.1"
[[projects]]
digest = "1:c587772fb8ad29ad4db67575dad25ba17a51f072ff18a22b4f0257a4d9c24f75"
name = "github.com/stretchr/testify"
packages = [
"assert",
"require",
]
pruneopts = ""
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
version = "v1.2.2"
[[projects]]
branch = "master"
digest = "1:ff65401e40494de90fddaecf09b2954179d872a45d9c68b9b31da60f35e4ea73"
name = "golang.org/x/sys"
packages = ["unix"]
pruneopts = ""
revision = "7e31e0c00fa05cb5fbf4347b585621d6709e19a4"
[[projects]]
digest = "1:5acd3512b047305d49e8763eef7ba423901e85d5dd2fd1e71778a0ea8de10bd4"
name = "golang.org/x/text"
packages = [
"internal/gen",
"internal/triegen",
"internal/ucd",
"transform",
"unicode/cldr",
"unicode/norm",
]
pruneopts = ""
revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
version = "v0.3.0"
[[projects]]
digest = "1:f0620375dd1f6251d9973b5f2596228cc8042e887cd7f827e4220bc1ce8c30e2"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = ""
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
version = "v2.2.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/MakeNowJust/heredoc",
"github.com/Masterminds/semver",
"github.com/mitchellh/go-homedir",
"github.com/pkg/errors",
"github.com/spf13/cobra",
"github.com/spf13/cobra/doc",
"github.com/spf13/pflag",
"github.com/spf13/viper",
"github.com/stretchr/testify/assert",
"github.com/stretchr/testify/require",
"gopkg.in/yaml.v2",
]
solver-name = "gps-cdcl"
solver-version = 1

32
Gopkg.toml Normal file
View File

@@ -0,0 +1,32 @@
[[constraint]]
name = "github.com/Masterminds/semver"
version = "v1.4.2"
[[constraint]]
name = "github.com/mitchellh/go-homedir"
version = "v1.0.0"
[[constraint]]
name = "github.com/pkg/errors"
version = "v0.8.0"
[[constraint]]
name = "github.com/spf13/cobra"
version = "v0.0.3"
[[constraint]]
name = "github.com/spf13/pflag"
version = "v1.0.2"
[[constraint]]
name = "github.com/spf13/viper"
version = "v1.2.0"
[[constraint]]
name = "github.com/stretchr/testify"
version = "v1.2.2"
[[constraint]]
name = "gopkg.in/yaml.v2"
version = "v2.2.1"

278
README.md
View File

@@ -1,213 +1,141 @@
# 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 charts.
It is meant to be used for linting and testing pull requests.
It automatically detects charts changed against the target branch.
[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.
## Installation
As a convenience, [chart_test.sh](chart_test.sh) is provided.
It supports linting and testing charts that have changed against a target branch.
## 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!
### Binary Distribution
## Installation
Download the release distribution for your OS from the Releases page:
Clone the repository and add it to the `PATH`.
The script must be run in the root directory of a Git repository.
https://github.com/helm/chart-testing/releases
```shell
$ chart_test.sh --help
Usage: chart_test.sh <options>
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
```
Unpack the `ct` binary, add it to your PATH, and you are good to go!
## Configuration
### Docker Image
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 (`<name>=<url>`) | `()` |
| `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.
A Docker image is available at `quay.io/helmpack/chart-testing`.
## 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`.
See documentation for individual commands:
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.
* [ct](doc/ct.md)
* [ct install](doc/ct_install.md)
* [ct lint](doc/ct_lint.md)
* [ct lint-and-install](doc/ct_lint-and-install.md)
* [ct version](doc/ct_version.md)
```shell
REMOTE=myremote
```
```shell
git remote add myremote <repo_url></repo_url>
git fetch myremote
chart-test.sh
## Configuration
`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.
CLI flags, environment variables, and a config file can be mixed. The following order of precedence applies:
1. CLI flags
1. Environment variables
1. Config file
Note that linting requires config file for [yamllint](https://github.com/adrienverge/yamllint) and [yamale](https://github.com/23andMe/Yamale).
If not specified, these files are search in the current directory, `$HOME/.ct`, and `/etc/ct`, in that order.
Samples are provided in the [etc](etc) folder.
### Examples
The following example show various way of configuring the same thing:
#### CLI
ct install --remote upstream --chart-dirs stable,incubator --build-id pr-42
#### Environment Variables
export CT_REMOTE=upstream
export CT_CHART_DIRS=stable,incubator
export CT_BUILD_ID
ct install
#### Config File
`config.yaml`:
```yaml
remote: upstream
chart-dirs:
- stable
- incubator
build-id: pr-42
```
### Linting Charts
`ct install --config config.yaml`
```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` supports any format [Viper](https://github.com/spf13/viper) can read, i. e. JSON, TOML, YAML, HCL, and Java properties files.
## Building from Source
`ct` is built using Go 1.11. Older versions may work but have not been tested.
`build.sh` is used to build and release the tool. It uses [Goreleaser](https://goreleaser.com/) under the covers.
Note: on MacOS you will need `GNU Coreutils readlink`. You can install it with:
```console
brew install coreutils
```
*Sample Output*
Then add `gnubin` to your `$PATH`, with:
```
-----------------------------------------------------------------------
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.
```console
echo 'export PATH="$(brew --prefix coreutils)/libexec/gnubin:$PATH"' >> ~/.bash_profile
bash --login
```
#### Linting Unchanged Charts
To use the build script:
You can lint all charts with `--all` flag (chart version bump check will be ignored):
```console
$ ./build.sh -h
Usage: build.sh <options>
```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
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
```
You can lint a list of charts (separated by comma) with `--charts` flag (chart version bump check will be ignored):
## Releasing
```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
CircleCI creates releases automatically when a new tag is pushed. Tags are created using `tag.sh`.
```console
$ ./tag.sh -h
Usage: tag.sh <options>
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
```
You can lint a single chart with `--charts` flag (chart version bump check will be ignored):
```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
```
### Installing and Testing Charts
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`.
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
```
#### Installing Unchanged Charts
You can force to install all charts with `--all` flag:
```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
```
You can force to install a list of charts (separated by comma) with `--charts` flag:
```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:
```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
```
#### GKE Example
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.
#### Docker for Mac Example
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.

47
app/cmd/docGen.go Normal file
View File

@@ -0,0 +1,47 @@
// Copyright 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"
"github.com/spf13/cobra/doc"
"os"
)
func newGenerateDocsCmd() *cobra.Command {
return &cobra.Command{
Use: "doc-gen",
Short: "Generate documentation",
Long: heredoc.Doc(`
Generate documentation for all commands
to the 'docs' directory.`),
Hidden: true,
Run: generateDocs,
}
}
func generateDocs(cmd *cobra.Command, args []string) {
fmt.Println("Generating docs...")
err := doc.GenMarkdownTree(NewRootCmd(), "doc")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
fmt.Println("Done.")
}

94
app/cmd/install.go Normal file
View File

@@ -0,0 +1,94 @@
// Copyright 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/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.String("helm-extra-args", "", heredoc.Doc(`
Additional arguments for Helm. Must be passed as a single quoted string
(e. g. "--timeout 500 --tiller-namespace tiller"`))
}
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", "helm-extra-args"}
return bindFlags(options, flagSet, v)
}

100
app/cmd/lint.go Normal file
View File

@@ -0,0 +1,100 @@
// Copyright 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"
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 the current directory, '$HOME/.ct', and '/etc/ct', in
that order`))
flags.String("chart-yaml-schema", "", heredoc.Doc(`
The schema for chart.yml validation. If not specified, 'chart_schema.yaml'
is searched in the current directory, '$HOME/.ct', and '/etc/ct', in
that order.`))
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"}
return bindFlags(options, flagSet, v)
}

65
app/cmd/lintAndInstall.go Normal file
View File

@@ -0,0 +1,65 @@
// Copyright 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/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",
Aliases: []string{"li"},
Short: "Lint, install, and test a chart",
Long: "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)
}
}

101
app/cmd/root.go Normal file
View File

@@ -0,0 +1,101 @@
// Copyright 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/spf13/viper"
"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())
cmd.AddCommand(newGenerateDocsCmd())
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. May be specified multiple times
or separate values with commas`))
flags.StringSlice("chart-dirs", []string{"charts"}, heredoc.Doc(`
Directories containing Helm charts. May be specified multiple times
or separate values with commas`))
flags.StringSlice("chart-repos", []string{}, heredoc.Doc(`
Additional chart repos to add so dependencies can be resolved. May be
specified multiple times or separate values with commas`))
flags.StringSlice("excluded-charts", []string{}, heredoc.Doc(`
Charts that should be skipped. May be specified multiple times
or separate values with commas`))
flags.Bool("debug", false, heredoc.Doc(`
Print CLI calls of external tools to stdout (Note: depending on helm-extra-args
passed, this may reveal sensitive data)`))
}
func bindFlags(options []string, flagSet *flag.FlagSet, v *viper.Viper) error {
for _, option := range options {
if err := v.BindPFlag(option, flagSet.Lookup(option)); err != nil {
return err
}
}
return nil
}
func bindRootFlags(flagSet *flag.FlagSet, v *viper.Viper) error {
options := []string{"remote", "target-branch", "all", "charts", "chart-dirs", "chart-repos", "excluded-charts"}
return bindFlags(options, flagSet, v)
}

44
app/cmd/version.go Normal file
View File

@@ -0,0 +1,44 @@
// Copyright 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/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",
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")
}

23
app/main.go Normal file
View File

@@ -0,0 +1,23 @@
// Copyright 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()
}

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Copyright 2018 The Helm Authors. All rights reserved.
# Copyright 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.
@@ -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##*/} <args>
-h, --help Display help
-v, --verbose Display verbose output
-p, --push Push image to registry
Usage: $(basename "$0") <options>
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
-d|--debug)
debug=true
;;
-p|--push)
push=true
;;
-?*)
printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2
-r|--release)
release=true
;;
*)
break
@@ -62,17 +58,24 @@ 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 "$push" ]]; then
docker push "$IMAGE_REPOSITORY:$IMAGE_TAG"
if [[ -n "$debug" ]]; then
goreleaser_args+=( --debug)
set -x
fi
popd
if [[ -z "$release" ]]; then
goreleaser_args+=( --snapshot)
fi
pushd "$SCRIPT_DIR" > /dev/null
dep ensure -v
go test ./...
goreleaser "${goreleaser_args[@]}"
popd > /dev/null
}
main "$@"

View File

@@ -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") <options>
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 "$@"

View File

@@ -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)

28
doc/ct.md Normal file
View File

@@ -0,0 +1,28 @@
## ct
The Helm chart testing tool
### Synopsis
Lint and test
* changed charts
* specific charts
* all charts
in given chart directories.
### Options
```
-h, --help help for ct
```
### SEE ALSO
* [ct install](ct_install.md) - Install and test a chart
* [ct lint](ct_lint.md) - Lint and validate a chart
* [ct lint-and-install](ct_lint-and-install.md) - Lint, install, and test a chart
* [ct version](ct_version.md) - Print version information
###### Auto generated by spf13/cobra on 6-Nov-2018

56
doc/ct_install.md Normal file
View File

@@ -0,0 +1,56 @@
## ct install
Install and test a chart
### Synopsis
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.
```
ct install [flags]
```
### Options
```
--all Process all charts except those explicitly excluded.
Disables changed charts detection and version increment checking
--build-id string 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
--chart-dirs strings Directories containing Helm charts. May be specified multiple times
or separate values with commas (default [charts])
--chart-repos strings Additional chart repos to add so dependencies can be resolved. May be
specified multiple times or separate values with commas
--charts strings Specific charts to test. Disables changed charts detection and
version increment checking. May be specified multiple times
or separate values with commas
--config string Config file
--debug Print CLI calls of external tools to stdout (Note: depending on helm-extra-args
passed, this may reveal sensitive data)
--excluded-charts strings Charts that should be skipped. May be specified multiple times
or separate values with commas
--helm-extra-args string Additional arguments for Helm. Must be passed as a single quoted string
(e. g. "--timeout 500 --tiller-namespace tiller"
-h, --help help for install
--remote string The name of the Git remote used to identify changed charts (default "origin")
--target-branch string The name of the target branch used to identify changed charts (default "master")
```
### SEE ALSO
* [ct](ct.md) - The Helm chart testing tool
###### Auto generated by spf13/cobra on 6-Nov-2018

View File

@@ -0,0 +1,53 @@
## ct lint-and-install
Lint, install, and test a chart
### Synopsis
Combines 'lint' and 'install' commands.
```
ct lint-and-install [flags]
```
### Options
```
--all Process all charts except those explicitly excluded.
Disables changed charts detection and version increment checking
--build-id string 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
--chart-dirs strings Directories containing Helm charts. May be specified multiple times
or separate values with commas (default [charts])
--chart-repos strings Additional chart repos to add so dependencies can be resolved. May be
specified multiple times or separate values with commas
--chart-yaml-schema string The schema for chart.yml validation. If not specified, 'chart_schema.yaml'
is searched in the current directory, '$HOME/.ct', and '/etc/ct', in
that order.
--charts strings Specific charts to test. Disables changed charts detection and
version increment checking. May be specified multiple times
or separate values with commas
--check-version-increment Activates a check for chart version increments (default: true) (default true)
--config string Config file
--debug Print CLI calls of external tools to stdout (Note: depending on helm-extra-args
passed, this may reveal sensitive data)
--excluded-charts strings Charts that should be skipped. May be specified multiple times
or separate values with commas
--helm-extra-args string Additional arguments for Helm. Must be passed as a single quoted string
(e. g. "--timeout 500 --tiller-namespace tiller"
-h, --help help for lint-and-install
--lint-conf string The config file for YAML linting. If not specified, 'lintconf.yaml'
is searched in the current directory, '$HOME/.ct', and '/etc/ct', in
that order
--remote string The name of the Git remote used to identify changed charts (default "origin")
--target-branch string The name of the target branch used to identify changed charts (default "master")
--validate-maintainers Enabled validation of maintainer account names in chart.yml (default: true).
Works for GitHub, GitLab, and Bitbucket (default true)
```
### SEE ALSO
* [ct](ct.md) - The Helm chart testing tool
###### Auto generated by spf13/cobra on 6-Nov-2018

61
doc/ct_lint.md Normal file
View File

@@ -0,0 +1,61 @@
## ct lint
Lint and validate a chart
### Synopsis
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.
```
ct lint [flags]
```
### Options
```
--all Process all charts except those explicitly excluded.
Disables changed charts detection and version increment checking
--chart-dirs strings Directories containing Helm charts. May be specified multiple times
or separate values with commas (default [charts])
--chart-repos strings Additional chart repos to add so dependencies can be resolved. May be
specified multiple times or separate values with commas
--chart-yaml-schema string The schema for chart.yml validation. If not specified, 'chart_schema.yaml'
is searched in the current directory, '$HOME/.ct', and '/etc/ct', in
that order.
--charts strings Specific charts to test. Disables changed charts detection and
version increment checking. May be specified multiple times
or separate values with commas
--check-version-increment Activates a check for chart version increments (default: true) (default true)
--config string Config file
--debug Print CLI calls of external tools to stdout (Note: depending on helm-extra-args
passed, this may reveal sensitive data)
--excluded-charts strings Charts that should be skipped. May be specified multiple times
or separate values with commas
-h, --help help for lint
--lint-conf string The config file for YAML linting. If not specified, 'lintconf.yaml'
is searched in the current directory, '$HOME/.ct', and '/etc/ct', in
that order
--remote string The name of the Git remote used to identify changed charts (default "origin")
--target-branch string The name of the target branch used to identify changed charts (default "master")
--validate-maintainers Enabled validation of maintainer account names in chart.yml (default: true).
Works for GitHub, GitLab, and Bitbucket (default true)
```
### SEE ALSO
* [ct](ct.md) - The Helm chart testing tool
###### Auto generated by spf13/cobra on 6-Nov-2018

23
doc/ct_version.md Normal file
View File

@@ -0,0 +1,23 @@
## ct version
Print version information
### Synopsis
Print version information
```
ct version [flags]
```
### Options
```
-h, --help help for version
```
### SEE ALSO
* [ct](ct.md) - The Helm chart testing tool
###### Auto generated by spf13/cobra on 6-Nov-2018

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Copyright 2018 The Helm Authors. All rights reserved.
# Copyright 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.
@@ -18,7 +18,7 @@ set -o errexit
set -o nounset
set -o pipefail
readonly IMAGE_TAG=v1.1.0
readonly IMAGE_TAG=v3.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

View File

@@ -1,4 +1,4 @@
# Copyright 2018 The Helm Authors. All rights reserved.
# Copyright 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.
@@ -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" && \

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Copyright 2018 The Helm Authors. All rights reserved.
# Copyright 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.
@@ -19,6 +19,7 @@ set -o nounset
set -o pipefail
readonly IMAGE_REPOSITORY="myrepo/chart-testing"
readonly IMAGE_TAG="v1.0.0"
readonly REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel)}"
main() {
@@ -35,9 +36,7 @@ main() {
docker exec "$config_container_id" gcloud auth activate-service-account --key-file /service-account.json
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!"
docker exec "$config_container_id" ct lint-and-install --chart-dirs stable,incubator
}
main

View File

@@ -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 "$*"
}

592
pkg/chart/chart.go Normal file
View File

@@ -0,0 +1,592 @@
// Copyright 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"
"github.com/helm/chart-testing/pkg/exec"
"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 {
procExec := exec.NewProcessExecutor(config.Debug)
kubectl := tool.NewKubectl(procExec)
extraArgs := strings.Fields(config.HelmExtraArgs)
testing := Testing{
config: config,
helm: tool.NewHelm(procExec, kubectl, extraArgs),
git: tool.NewGit(procExec),
kubectl: kubectl,
linter: tool.NewLinter(procExec),
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()
util.PrintDelimiterLine("-")
fmt.Println(" Charts to be processed:")
util.PrintDelimiterLine("-")
for _, chart := range charts {
fmt.Printf(" %s\n", chart)
}
util.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) {
util.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.")
}
util.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
}
util.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...)
}
util.PrintDelimiterLine("=")
}
func printDetails(pod string, text string, delimiterChar string, printFunc func(item string) error, items ...string) {
for _, item := range items {
item = strings.Trim(item, "'")
util.PrintDelimiterLine(delimiterChar)
fmt.Printf("==> %s %s\n", text, pod)
util.PrintDelimiterLine(delimiterChar)
if err := printFunc(item); err != nil {
fmt.Println("Error printing details:", err)
return
}
util.PrintDelimiterLine(delimiterChar)
fmt.Printf("<== %s %s\n", text, pod)
util.PrintDelimiterLine(delimiterChar)
}
}

147
pkg/chart/chart_test.go Normal file
View File

@@ -0,0 +1,147 @@
// Copyright 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)
})
}
}

View File

@@ -0,0 +1 @@
maintainers: []

View File

@@ -0,0 +1,5 @@
maintainers:
- name: invalid
email: invalid@example.com
- name: valid
email: valid@example.com

View File

View File

@@ -0,0 +1 @@
deprecated: true

View File

@@ -0,0 +1,5 @@
maintainers:
- name: valid
email: valid@example.com
- name: valid-too
email: valid-too@example.com

View File

@@ -0,0 +1,6 @@
deprecated: true
maintainers:
- name: valid
email: valid@example.com
- name: valid-too
email: valid-too@example.com

152
pkg/config/config.go Normal file
View File

@@ -0,0 +1,152 @@
// Copyright 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"
"github.com/mitchellh/go-homedir"
"path"
"reflect"
"strings"
"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 (
homeDir, _ = homedir.Dir()
configSearchLocations = []string{
".",
path.Join(homeDir, ".ct"),
"/etc/ct",
}
)
type Configuration struct {
Remote string `mapstructure:"remote"`
TargetBranch string `mapstructure:"target-branch"`
BuildId string `mapstructure:"build-id"`
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"`
HelmExtraArgs string `mapstructure:"helm-extra-args"`
Debug bool `mapstructure:"debug"`
}
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!")
}
isLint := strings.Contains(cmd.Use, "lint")
chartYamlSchemaPath := cfg.ChartYamlSchema
if chartYamlSchemaPath == "" {
var err error
cfgFile, err = findConfigFile("chart_schema.yaml")
if err != nil && isLint {
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 && isLint {
return nil, errors.New("'lintconf.yaml' neither specified nor found in default locations")
}
cfg.LintConf = cfgFile
}
printCfg(cfg)
return cfg, nil
}
func printCfg(cfg *Configuration) {
util.PrintDelimiterLine("-")
fmt.Println(" Configuration")
util.PrintDelimiterLine("-")
e := reflect.ValueOf(cfg).Elem()
typeOfCfg := e.Type()
for i := 0; i < e.NumField(); i++ {
var pattern string
switch e.Field(i).Kind() {
case reflect.Bool:
pattern = "%s: %t\n"
default:
pattern = "%s: %s\n"
}
fmt.Printf(pattern, typeOfCfg.Field(i).Name, e.Field(i).Interface())
}
util.PrintDelimiterLine("-")
}
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(fmt.Sprintf("Config file not found: %s", fileName))
}

46
pkg/config/config_test.go Normal file
View File

@@ -0,0 +1,46 @@
// Copyright 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 (
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
"testing"
)
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, "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, "--timeout 300", cfg.HelmExtraArgs)
}

View File

@@ -0,0 +1,22 @@
{
"remote": "origin",
"target-branch": "master",
"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"
],
"helm-extra-args": "--timeout 300"
}

View File

@@ -0,0 +1,17 @@
remote: origin
target-branch: master
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
helm-extra-args: --timeout 300

98
pkg/exec/exec.go Normal file
View File

@@ -0,0 +1,98 @@
// Copyright 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 {
debug bool
}
func NewProcessExecutor(debug bool) ProcessExecutor {
return ProcessExecutor{
debug: debug,
}
}
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)
if p.debug {
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)
if p.debug {
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
}

45
pkg/tool/account.go Normal file
View File

@@ -0,0 +1,45 @@
// Copyright 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]
}

28
pkg/tool/account_test.go Normal file
View File

@@ -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)
})
}
}

64
pkg/tool/git.go Normal file
View File

@@ -0,0 +1,64 @@
// Copyright 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"
"strings"
"github.com/helm/chart-testing/pkg/exec"
"github.com/pkg/errors"
)
type Git struct {
exec exec.ProcessExecutor
}
func NewGit(exec exec.ProcessExecutor) Git {
return Git{
exec: exec,
}
}
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)
}

83
pkg/tool/helm.go Normal file
View File

@@ -0,0 +1,83 @@
// Copyright 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"
)
type Helm struct {
exec exec.ProcessExecutor
kubectl Kubectl
extraArgs []string
}
func NewHelm(exec exec.ProcessExecutor, kubectl Kubectl, extraArgs []string) Helm {
return Helm{
exec: exec,
kubectl: kubectl,
extraArgs: extraArgs,
}
}
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 {
var values []string
if valuesFile != "" {
values = []string{"--values", valuesFile}
}
if err := h.exec.RunProcess("helm", "install", chart, "--name", release, "--namespace", namespace,
"--wait", values, h.extraArgs); err != nil {
return err
}
if err := h.exec.RunProcess("helm", "test", release, h.extraArgs); 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, h.extraArgs); err != nil {
fmt.Println("Error deleting Helm release:", err)
}
}

138
pkg/tool/kubectl.go Normal file
View File

@@ -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(exec exec.ProcessExecutor) Kubectl {
return Kubectl{
exec: exec,
}
}
// 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}")
}

35
pkg/tool/linter.go Normal file
View File

@@ -0,0 +1,35 @@
// Copyright 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(exec exec.ProcessExecutor) Linter {
return Linter{
exec: exec,
}
}
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)
}

175
pkg/util/util.go Normal file
View File

@@ -0,0 +1,175 @@
// Copyright 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"
"strings"
"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
}
func PrintDelimiterLine(delimiterChar string) {
delim := make([]string, 120)
for i := 0; i < 120; i++ {
delim[i] = delimiterChar
}
fmt.Println(strings.Join(delim, ""))
}

67
pkg/util/util_test.go Normal file
View File

@@ -0,0 +1,67 @@
// Copyright 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)
})
}
}

109
tag.sh Executable file
View File

@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# Copyright 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") <options>
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 "$@"