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:
committed by
Scott Rigby
parent
8e23ec2d74
commit
f632cd5081
@@ -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: /.*/
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
.circleci
|
||||
.history
|
||||
.idea
|
||||
.editorconfig
|
||||
.gitignore
|
||||
.git
|
||||
Dockerfile
|
||||
.vscode
|
||||
|
||||
@@ -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
9
.gitignore
vendored
@@ -1,5 +1,8 @@
|
||||
.history
|
||||
.idea
|
||||
.settings
|
||||
.project
|
||||
.classpath
|
||||
.settings
|
||||
.vscode
|
||||
/dist
|
||||
/config.*
|
||||
/ct
|
||||
vendor
|
||||
|
||||
53
.goreleaser.yml
Normal file
53
.goreleaser.yml
Normal 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
|
||||
50
Dockerfile
50
Dockerfile
@@ -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
240
Gopkg.lock
generated
Normal 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
32
Gopkg.toml
Normal 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
278
README.md
@@ -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
47
app/cmd/docGen.go
Normal 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
94
app/cmd/install.go
Normal 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
100
app/cmd/lint.go
Normal 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
65
app/cmd/lintAndInstall.go
Normal 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
101
app/cmd/root.go
Normal 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
44
app/cmd/version.go
Normal 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
23
app/main.go
Normal 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()
|
||||
}
|
||||
59
build.sh
59
build.sh
@@ -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 "$@"
|
||||
|
||||
188
chart_test.sh
188
chart_test.sh
@@ -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 "$@"
|
||||
@@ -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
28
doc/ct.md
Normal 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
56
doc/ct_install.md
Normal 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
|
||||
53
doc/ct_lint-and-install.md
Normal file
53
doc/ct_lint-and-install.md
Normal 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
61
doc/ct_lint.md
Normal 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
23
doc/ct_version.md
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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" && \
|
||||
|
||||
@@ -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
|
||||
|
||||
565
lib/chartlib.sh
565
lib/chartlib.sh
@@ -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
592
pkg/chart/chart.go
Normal 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
147
pkg/chart/chart_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
1
pkg/chart/testdata/empty_maintainers/Chart.yaml
vendored
Normal file
1
pkg/chart/testdata/empty_maintainers/Chart.yaml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
maintainers: []
|
||||
5
pkg/chart/testdata/invalid_maintainers/Chart.yaml
vendored
Normal file
5
pkg/chart/testdata/invalid_maintainers/Chart.yaml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
maintainers:
|
||||
- name: invalid
|
||||
email: invalid@example.com
|
||||
- name: valid
|
||||
email: valid@example.com
|
||||
0
pkg/chart/testdata/no_maintainers/Chart.yaml
vendored
Normal file
0
pkg/chart/testdata/no_maintainers/Chart.yaml
vendored
Normal file
1
pkg/chart/testdata/no_maintainers_deprecated/Chart.yaml
vendored
Normal file
1
pkg/chart/testdata/no_maintainers_deprecated/Chart.yaml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
deprecated: true
|
||||
5
pkg/chart/testdata/valid_maintainers/Chart.yaml
vendored
Normal file
5
pkg/chart/testdata/valid_maintainers/Chart.yaml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
maintainers:
|
||||
- name: valid
|
||||
email: valid@example.com
|
||||
- name: valid-too
|
||||
email: valid-too@example.com
|
||||
6
pkg/chart/testdata/valid_maintainers_deprecated/Chart.yaml
vendored
Normal file
6
pkg/chart/testdata/valid_maintainers_deprecated/Chart.yaml
vendored
Normal 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
152
pkg/config/config.go
Normal 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
46
pkg/config/config_test.go
Normal 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)
|
||||
}
|
||||
22
pkg/config/test_config.json
Normal file
22
pkg/config/test_config.json
Normal 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"
|
||||
}
|
||||
17
pkg/config/test_config.yaml
Normal file
17
pkg/config/test_config.yaml
Normal 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
98
pkg/exec/exec.go
Normal 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
45
pkg/tool/account.go
Normal 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
28
pkg/tool/account_test.go
Normal 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
64
pkg/tool/git.go
Normal 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
83
pkg/tool/helm.go
Normal 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
138
pkg/tool/kubectl.go
Normal 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
35
pkg/tool/linter.go
Normal 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
175
pkg/util/util.go
Normal 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
67
pkg/util/util_test.go
Normal 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
109
tag.sh
Executable 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 "$@"
|
||||
Reference in New Issue
Block a user