Adding Helm plugin and pre-commit

This commit is contained in:
Jiri Tyr 2022-12-07 09:44:58 +00:00
parent 752a33eaeb
commit 2097831d98
7 changed files with 1146 additions and 0 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
dist/
bin/
.idea/
**/*.pyc

7
.pre-commit-hooks.yaml Normal file
View file

@ -0,0 +1,7 @@
- id: kubeconform-helm
name: Kubeconform Helm
description: Run kubeconform for Helm charts
language: script
entry: scripts/helm/pre-commit.py
files: ^charts/[^/]+/(\.kubeconform|\.helmignore|templates/NOTES.txt|.*\.(ya?ml|json|tpl))$
require_serial: true

239
Readme.md
View file

@ -49,6 +49,9 @@ sys 0m1,069s
* [Integrating Kubeconform in the CI](#Integrating-Kubeconform-in-the-CI)
* [Github Workflow](#Github-Workflow)
* [Gitlab-CI](#Gitlab-CI)
* [Helm charts](#helm-charts)
* [Helm plugin](#helm-plugin)
* [Helm `pre-commit` hook](#helm-pre-commit-hook)
* [Using kubeconform as a Go Module](#Using-kubeconform-as-a-Go-Module)
* [Credits](#Credits)
@ -325,6 +328,242 @@ lint-kubeconform:
See [issue 106](https://github.com/yannh/kubeconform/issues/106) for more details.
## Helm charts
`kubeconform` supports automation for [Helm charts](https://helm.sh) in the form
of [Helm plugin](https://helm.sh/docs/topics/plugins/) and [`pre-commit`
hook](https://pre-commit.com/).
### Helm plugin
The `kubeconform` [Helm plugin](https://helm.sh/docs/topics/plugins/) can be
installed using this command:
```shell
helm plugin install https://github.com/yannh/kubeconform
```
Once installed, the plugin can be used from any Helm chart directory:
```shell
# Enter the chart directory
cd charts/mychart
# Run kubeconform plugin
helm kubeconform .
```
The plugin uses `helm template` internally and passes its output to the
`kubeconform`. There is several `helm template` command line options supported
by the plugin that can be specified:
```shell
helm kubeconform --namespace myns .
```
There is also several `kubeconform` command line options supported by the plugin
that can be specified:
```shell
# Kubeconform options
helm kubeconform --verbose --summary .
```
It's also possible to create `.kubeconform` file in the Helm chart directory
that can contain default `kubeconform` settings:
```yaml
# Command line options that can be set multiple times can be defined as an array
schema-location:
- default
- https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json
# Command line options that can be specified without a value must have boolean
# value in the config file
summary: true
verbose: true
```
The full list of options for the
[plugin](https://github.com/yannh/kubeconform/blob/master/scripts/helm/plugin_wrapper.py)
is as follows:
```text
$ ./plugin_wrapper.py --help
usage: plugin_wrapper.py [-h] [--cache] [--cache-dir DIR] [--config FILE] [--values-dir DIR]
[--values-pattern PATTERN] [--debug] [--skip-refresh] [--verify]
[-f FILE] [-n NAME] [-r NAME] [--ignore-missing-schemas]
[--insecure-skip-tls-verify] [--kubernetes-version VERSION]
[--goroutines NUMBER] [--output {json,junit,tap,text}]
[--reject LIST] [--schema-location LOCATION] [--skip LIST]
[--strict] [--summary] [--verbose]
CHART
Wrapper to run kubeconform for a Helm chart.
options:
-h, --help show this help message and exit
--cache whether to use kubeconform cache
--cache-dir DIR path to the cache directory (default: ~/.cache/kubeconform)
--config FILE config file name (default: .kubeconform)
--values-dir DIR directory with optional values files for the tests (default: ci)
--values-pattern PATTERN
pattern to select the values files (default: *-values.yaml)
--debug debug output
helm build:
Options passed to the 'helm build' command
--skip-refresh do not refresh the local repository cache
--verify verify the packages against signatures
helm template:
Options passed to the 'helm template' command
-f FILE, --values FILE
values YAML file or URL (can specified multiple)
-n NAME, --namespace NAME
namespace
-r NAME, --release NAME
release name
CHART chart path (e.g. '.')
kubeconform:
Options passsed to the 'kubeconform' command
--ignore-missing-schemas
skip files with missing schemas instead of failing
--insecure-skip-tls-verify
disable verification of the server's SSL certificate
--kubernetes-version VERSION
version of Kubernetes to validate against, e.g. 1.18.0 (default:
master)
--goroutines NUMBER number of goroutines to run concurrently (default: 4)
--output {json,junit,tap,text}
output format (default: text)
--reject LIST comma-separated list of kinds or GVKs to reject
--schema-location LOCATION
override schemas location search path (can specified multiple)
--skip LIST comma-separated list of kinds or GVKs to ignore
--strict disallow additional properties not in schema or duplicated keys
--summary print a summary at the end (ignored for junit output)
--verbose print results for all resources (ignored for tap and junit output)
```
## Helm `pre-commit` hook
The `kubeconform` [`pre-commit` hook](https://pre-commit.com) can be added into the
`.pre-commit-config.yaml` file like this:
```yaml
repos:
- repo: https://github.com/yannh/kubeconform
rev: v0.5.0
hooks:
- id: kubeconform-helm
```
The hook uses `helm template` internally and passes its output to the
`kubeconform`. There is several `helm template` command line options supported
by the hook that can be specified:
```yaml
- repo: https://github.com/yannh/kubeconform
rev: v0.5.0
hooks:
- id: kubeconform-helm
args:
- --namespace=myns
- --release=myrelease
```
There is also several `kubeconform` command line options supported by the hook
that can be specified:
```yaml
- repo: https://github.com/yannh/kubeconform
rev: v0.5.0
hooks:
- id: kubeconform-helm
args:
- --kubernetes-version=1.24.0
- --verbose
- --summary
```
The full list of options for the
[hook](https://github.com/yannh/kubeconform/blob/master/scripts/helm/pre-commit.py)
is as follows:
```text
$ ./pre-commit.py --help
usage: pre-commit.py [-h] [--charts-path PATH] [--include-charts LIST]
[--exclude-charts LIST] [--cache] [--cache-dir DIR] [--config FILE]
[--values-dir DIR] [--values-pattern PATTERN] [--debug]
[--skip-refresh] [--verify] [-f FILE] [-n NAME] [-r NAME]
[--ignore-missing-schemas] [--insecure-skip-tls-verify]
[--kubernetes-version VERSION] [--goroutines NUMBER]
[--output {json,junit,tap,text}] [--reject LIST]
[--schema-location LOCATION] [--skip LIST] [--strict] [--summary]
[--verbose]
FILES [FILES ...]
Wrapper to run kubeconform for a Helm chart.
positional arguments:
FILES files that have changed
options:
-h, --help show this help message and exit
--charts-path PATH path to the directory with charts (default: charts)
--include-charts LIST
comma-separated list of chart names to include in the testing
--exclude-charts LIST
comma-separated list of chart names to exclude from the testing
--cache whether to use kubeconform cache
--cache-dir DIR path to the cache directory (default: ~/.cache/kubeconform)
--config FILE config file name (default: .kubeconform)
--values-dir DIR directory with optional values files for the tests (default: ci)
--values-pattern PATTERN
pattern to select the values files (default: *-values.yaml)
--debug debug output
helm build:
Options passed to the 'helm build' command
--skip-refresh do not refresh the local repository cache
--verify verify the packages against signatures
helm template:
Options passed to the 'helm template' command
-f FILE, --values FILE
values YAML file or URL (can specified multiple)
-n NAME, --namespace NAME
namespace
-r NAME, --release NAME
release name
kubeconform:
Options passsed to the 'kubeconform' command
--ignore-missing-schemas
skip files with missing schemas instead of failing
--insecure-skip-tls-verify
disable verification of the server's SSL certificate
--kubernetes-version VERSION
version of Kubernetes to validate against, e.g. 1.18.0 (default:
master)
--goroutines NUMBER number of goroutines to run concurrently (default: 4)
--output {json,junit,tap,text}
output format (default: text)
--reject LIST comma-separated list of kinds or GVKs to reject
--schema-location LOCATION
override schemas location search path (can specified multiple)
--skip LIST comma-separated list of kinds or GVKs to ignore
--strict disallow additional properties not in schema or duplicated keys
--summary print a summary at the end (ignored for junit output)
--verbose print results for all resources (ignored for tap and junit output)
```
## Using kubeconform as a Go Module
**Warning**: This is a work-in-progress, the interface is not yet considered stable. Feedback is encouraged.

14
plugin.yaml Normal file
View file

@ -0,0 +1,14 @@
name: kubeconform
version: 0.5.0
usage: Kubernetes manifest validation tool for Helm charts
description: Kubernetes manifest validation tool for Helm charts
ignoreFlags: false
command: >-
$HELM_PLUGIN_DIR/scripts/helm/plugin_wrapper.py
hooks:
install: >-
cd $HELM_PLUGIN_DIR;
./scripts/helm/plugin_binloader.sh
update: >-
cd $HELM_PLUGIN_DIR;
HELM_PLUGIN_UPDATE=1 ./scripts/helm/plugin_binloader.sh

216
scripts/helm/plugin_binloader.sh Executable file
View file

@ -0,0 +1,216 @@
#!/usr/bin/env sh
# This script was borrowed from https://github.com/quintush/helm-unittest
if [ -z "$HELM_PLUGIN_DIR" ]; then
echo "No HELM_PLUGIN_DIR defined"
exit 1
fi
PROJECT_NAME='kubeconform'
PROJECT_GH="yannh/$PROJECT_NAME"
PROJECT_CHECKSUM_FILE='CHECKSUMS'
HELM_PLUGIN_PATH="$HELM_PLUGIN_DIR"
# Convert the HELM_PLUGIN_PATH to unix if cygpath is
# available. This is the case when using MSYS2 or Cygwin
# on Windows where helm returns a Windows path but we
# need a Unix path
if type cygpath >/dev/null 2>&1; then
echo 'Use Sygpath'
HELM_PLUGIN_PATH=$(cygpath -u "$HELM_PLUGIN_PATH")
fi
if [ "$SKIP_BIN_INSTALL" = '1' ]; then
echo 'Skipping binary install'
exit
fi
# fail_trap is executed if an error occurs.
fail_trap() {
result=$?
if [ "$result" != '0' ]; then
echo "Failed to install $PROJECT_NAME"
echo 'For support, go to https://github.com/kubernetes/helm'
fi
exit $result
}
# initArch discovers the architecture for this system.
initArch() {
ARCH=$(uname -m)
case "$ARCH" in
armv5*) ARCH='armv5';;
armv6*) ARCH='armv6';;
armv7*) ARCH='armv7';;
aarch64) ARCH='arm64';;
x86) ARCH='386';;
x86_64) ARCH='amd64';;
i686) ARCH='386';;
i386) ARCH='386';;
esac
}
# initOS discovers the operating system for this system.
initOS() {
OS=$(uname | tr '[:upper:]' '[:lower:]')
case "$OS" in
# Msys support
msys*) OS='windows';;
# Minimalist GNU for Windows
mingw*) OS='windows';;
# MacOS
darwin) OS='darwin';;
esac
}
# verifySupported checks that the os/arch combination is supported for
# binary builds.
verifySupported() {
supported='darwin-arm64,darwin-amd64,linux-amd64,linux-armv6,linux-arm64,linux-386,windows-arm64,windows-armv6,windows-amd64,windows-386'
if ! echo "$supported" | grep -q "$OS-$ARCH"; then
echo "No prebuild binary for $OS-$ARCH"
exit 1
fi
if type curl >/dev/null 2>&1; then
DOWNLOADER='curl'
elif type wget >/dev/null 2>&1; then
DOWNLOADER='wget'
else
echo 'Either curl or wget is required'
exit 1
fi
echo "Support $OS-$ARCH"
}
# getDownloadURL checks the latest available version.
getDownloadURLs() {
# Use the GitHub API to find the latest version for this project.
latest_url="https://api.github.com/repos/$PROJECT_GH/releases/latest"
if [ -z "$HELM_PLUGIN_UPDATE" ]; then
version=$(git describe --tags --exact-match 2>/dev/null || true)
if [ -n "$version" ]; then
latest_url="https://api.github.com/repos/$PROJECT_GH/releases/tags/$version"
fi
fi
echo "Retrieving $latest_url"
if [ $DOWNLOADER = 'curl' ]; then
DOWNLOAD_URL=$(curl -sL "$latest_url" | grep "$OS-$ARCH" | awk '/\"browser_download_url\":/{gsub(/[,\"]/,"", $2); print $2}' 2>/dev/null)
PROJECT_CHECKSUM=$(curl -sL "$latest_url" | grep "$PROJECT_CHECKSUM_FILE" | awk '/\"browser_download_url\":/{gsub(/[,\"]/,"", $2); print $2}' 2>/dev/null)
elif [ $DOWNLOADER = 'wget' ]; then
DOWNLOAD_URL=$(wget -q -O - "$latest_url" | grep "$OS-$ARCH" | awk '/\"browser_download_url\":/{gsub(/[,\"]/,"", $2); print $2}' 2>/dev/null)
PROJECT_CHECKSUM=$(wget -q -O - "$latest_url" | grep "$PROJECT_CHECKSUM_FILE" | awk '/\"browser_download_url\":/{gsub(/[,\"]/,"", $2); print $2}' 2>/dev/null)
fi
if [ -z "$DOWNLOAD_URL" ]; then
echo 'Failed to get DOWNLOAD_URL'
exit 1
elif [ -z "$PROJECT_CHECKSUM" ]; then
echo 'Failed to get PROJECT_CHECKSUM'
exit 1
fi
}
# downloadFiles downloads the latest binary package and also the checksum
# for that binary.
downloadFiles() {
PLUGIN_TMP_FOLDER=$(mktemp -d)
CHECKSUM_FILE_PATH="$PLUGIN_TMP_FOLDER/$PROJECT_CHECKSUM_FILE"
echo "Downloading '$DOWNLOAD_URL' and '$PROJECT_CHECKSUM' to location $PLUGIN_TMP_FOLDER"
if [ $DOWNLOADER = 'curl' ]; then
(cd "$PLUGIN_TMP_FOLDER" && curl -sLO "$DOWNLOAD_URL")
curl -s -L -o "$CHECKSUM_FILE_PATH" "$PROJECT_CHECKSUM"
elif [ $DOWNLOADER = 'wget' ]; then
wget -P "$PLUGIN_TMP_FOLDER" "$DOWNLOAD_URL"
wget -q -O "$CHECKSUM_FILE_PATH" "$PROJECT_CHECKSUM"
fi
}
# installFile verifies the SHA256 for the file, then unpacks and
# installs it.
installFile() {
echo 'Verifying SHA for the file'
DOWNLOAD_FILE=$(find "$PLUGIN_TMP_FOLDER" -name "*.tar.gz")
if [ -z "$DOWNLOAD_FILE" ]; then
DOWNLOAD_FILE=$(find "$PLUGIN_TMP_FOLDER" -name "*.zip")
fi
DOWNLOAD_FILE_NAME=$(basename "$DOWNLOAD_FILE")
(
cd "$PLUGIN_TMP_FOLDER"
if type shasum >/dev/null 2>&1; then
grep "$DOWNLOAD_FILE_NAME" "$CHECKSUM_FILE_PATH" | shasum -a 256 -c -s
elif type sha256sum >/dev/null 2>&1; then
if grep -q 'ID=alpine' /etc/os-release; then
grep "$DOWNLOAD_FILE_NAME" "$CHECKSUM_FILE_PATH" | sha256sum -c -s
else
grep "$DOWNLOAD_FILE_NAME" "$CHECKSUM_FILE_PATH" | sha256sum -c --status
fi
else
echo 'No Checksum as there is no shasum or sha256sum found'
fi
)
HELM_TMP="$PLUGIN_TMP_FOLDER/$PROJECT_NAME"
mkdir "$HELM_TMP"
tar -C "$HELM_TMP" -xf "$DOWNLOAD_FILE"
echo "Preparing to install into $HELM_PLUGIN_PATH"
HELM_TMP_BIN="$HELM_TMP/$PROJECT_NAME"
# Use * to also copy the file with the exe suffix on Windows
cp "$HELM_TMP_BIN"* "$HELM_PLUGIN_PATH/bin/"
rm -r "$HELM_TMP"
rm -r "$PLUGIN_TMP_FOLDER"
echo "$PROJECT_NAME installed into $HELM_PLUGIN_PATH/bin"
}
# testVersion tests the installed client to make sure it is working.
testVersion() {
# To avoid to keep track of the Windows suffix,
# call the plugin assuming it is in the PATH
PATH="$HELM_PLUGIN_PATH/bin:$PATH"
kubeconform -v
}
# Stop execution on any error
trap "fail_trap" EXIT
set -e
# Execution
initArch
initOS
verifySupported
getDownloadURLs
downloadFiles
installFile
testVersion

552
scripts/helm/plugin_wrapper.py Executable file
View file

@ -0,0 +1,552 @@
#!/usr/bin/env python3
import argparse
import logging
import os
import subprocess
import sys
import yaml
from glob import glob
def parse_args(add_chart=True, add_files=False, add_path=False, add_incl_excl=False):
# Command line options for helm, kubeconform and the plugin itself
args = {
"helm_tmpl": [],
"helm_build": [],
"kubeconform": [],
"wrapper": [],
}
# Define parser
parser = argparse.ArgumentParser(
description="Wrapper to run kubeconform for a Helm chart."
)
if add_path:
parser.add_argument(
"--charts-path",
metavar="PATH",
help="path to the directory with charts (default: charts)",
default="charts",
)
if add_incl_excl:
parser.add_argument(
"--include-charts",
metavar="LIST",
help="comma-separated list of chart names to include in the testing",
)
parser.add_argument(
"--exclude-charts",
metavar="LIST",
help="comma-separated list of chart names to exclude from the testing",
)
parser.add_argument(
"--cache", help="whether to use kubeconform cache", action="store_true"
)
parser.add_argument(
"--cache-dir",
metavar="DIR",
help="path to the cache directory (default: ~/.cache/kubeconform)",
default="~/.cache/kubeconform",
)
parser.add_argument(
"--config",
metavar="FILE",
help="config file name (default: .kubeconform)",
default=".kubeconform",
)
parser.add_argument(
"--values-dir",
metavar="DIR",
help="directory with optional values files for the tests (default: ci)",
default="ci",
)
parser.add_argument(
"--values-pattern",
metavar="PATTERN",
help="pattern to select the values files (default: *-values.yaml)",
default="*-values.yaml",
)
parser.add_argument(
"--debug",
help="debug output",
action="store_true",
)
group_helm_build = parser.add_argument_group(
"helm build", "Options passed to the 'helm build' command"
)
group_helm_build.add_argument(
"--skip-refresh",
help="do not refresh the local repository cache",
action="store_true",
)
group_helm_build.add_argument(
"--verify", help="verify the packages against signatures", action="store_true"
)
group_helm_tmpl = parser.add_argument_group(
"helm template", "Options passed to the 'helm template' command"
)
group_helm_tmpl.add_argument(
"-f",
"--values",
metavar="FILE",
help="values YAML file or URL (can specified multiple)",
action="append",
)
group_helm_tmpl.add_argument(
"-n",
"--namespace",
metavar="NAME",
help="namespace",
)
group_helm_tmpl.add_argument(
"-r",
"--release",
metavar="NAME",
help="release name",
)
if add_chart:
group_helm_tmpl.add_argument(
"CHART",
help="chart path (e.g. '.')",
)
group_kubeconform = parser.add_argument_group(
"kubeconform", "Options passsed to the 'kubeconform' command"
)
group_kubeconform.add_argument(
"--ignore-missing-schemas",
help="skip files with missing schemas instead of failing",
action="store_true",
)
group_kubeconform.add_argument(
"--insecure-skip-tls-verify",
help="disable verification of the server's SSL certificate",
action="store_true",
)
group_kubeconform.add_argument(
"--kubernetes-version",
metavar="VERSION",
help="version of Kubernetes to validate against, e.g. 1.18.0 (default: master)",
)
group_kubeconform.add_argument(
"--goroutines",
metavar="NUMBER",
help="number of goroutines to run concurrently (default: 4)",
)
group_kubeconform.add_argument(
"--output",
help="output format (default: text)",
choices=["json", "junit", "tap", "text"],
)
group_kubeconform.add_argument(
"--reject",
metavar="LIST",
help="comma-separated list of kinds or GVKs to reject",
)
group_kubeconform.add_argument(
"--schema-location",
metavar="LOCATION",
help="override schemas location search path (can specified multiple)",
action="append",
)
group_kubeconform.add_argument(
"--skip",
metavar="LIST",
help="comma-separated list of kinds or GVKs to ignore",
)
group_kubeconform.add_argument(
"--strict",
help="disallow additional properties not in schema or duplicated keys",
action="store_true",
)
group_kubeconform.add_argument(
"--summary",
help="print a summary at the end (ignored for junit output)",
action="store_true",
)
group_kubeconform.add_argument(
"--verbose",
help="print results for all resources (ignored for tap and junit output)",
action="store_true",
)
if add_files:
parser.add_argument(
"FILES",
help="files that have changed",
nargs="+",
)
# Parse the args
a = parser.parse_args()
# ### Populate the helm build options
if a.skip_refresh:
args["helm_build"] = ["--skip-refresh"]
if a.verify:
args["helm_build"] = ["--verify"]
# This must stay the last item from 'helm_build'!
if add_chart:
args["helm_build"] += [a.CHART]
# ### Populate the helm template options
if a.values:
for v in a.values:
args["helm_tmpl"] += ["--values", v]
if a.namespace is not None:
args["helm_tmpl"] += ["--namespace", a.namespace]
if a.release is not None:
args["helm_tmpl"] += [a.release]
# This must stay the last item from 'helm_tmpl'!
if add_chart:
args["helm_tmpl"] += [a.CHART]
# ### Polulate the kubeconform options
if a.cache:
args["kubeconform"] += ["-cache", os.path.expanduser(a.cache_dir)]
if a.ignore_missing_schemas is True:
args["kubeconform"] += ["-ignore-missing-schemas"]
if a.insecure_skip_tls_verify is True:
args["kubeconform"] += ["-insecure-skip-tls-verify"]
if a.kubernetes_version is not None:
args["kubeconform"] += ["-kubernetes-version", a.kubernetes_version]
if a.goroutines is not None:
args["kubeconform"] += ["-n", a.goroutines]
if a.output is not None:
args["kubeconform"] += ["-output", a.output]
if a.reject is True:
args["kubeconform"] += ["-reject"]
if a.schema_location:
for v in a.schema_location:
args["kubeconform"] += ["-schema-location", v]
if a.skip is True:
args["kubeconform"] += ["-skip"]
if a.strict is True:
args["kubeconform"] += ["-strict"]
if a.summary is True:
args["kubeconform"] += ["-summary"]
if a.verbose is True:
args["kubeconform"] += ["-verbose"]
# ### All args are wrapper options
args["wrapper"] = a
return args
def get_logger(debug):
if debug:
level = logging.DEBUG
else:
level = logging.ERROR
format = "%(levelname)s: %(message)s"
logging.basicConfig(level=level, format=format)
return logging.getLogger(__name__)
def parse_config(filename):
args = []
# Check if file exists
if not os.path.isfile(filename):
return args
# Read and parse the file
try:
with open(filename, "r") as stream:
try:
data = yaml.load(stream, Loader=yaml.Loader)
except yaml.YAMLError as e:
raise Exception("cannot parse YAML file '%s': %s" % (filename, e))
except IOError as e:
raise Exception("cannot open file '%s': %s" % (filename, e))
# Produce extra args out of the config file
if isinstance(data, dict):
for key, val in data.items():
if isinstance(val, list):
for v in val:
args.append("-%s=%s" % (key, v))
elif isinstance(v, dict):
# No deep dicts allowed in the config
continue
else:
args.append("-%s=%s" % (key, val))
return args
def get_values_files(values_dir, values_pattern):
values_files = []
# Get values files
if os.path.isdir(values_dir):
values_files = glob(os.path.join(values_dir, values_pattern))
return values_files
def run_helm_dependecy_build(args):
# Check if it's local chart
if not os.path.isfile(os.path.join(args[-1], "Chart.yaml")):
return
charts_dir = os.path.join(args[-1], "charts")
# Check if the dependency charts are already there
if os.path.isdir(charts_dir):
with open(os.path.join(args[-1], "Chart.yaml"), "r") as f:
try:
data = yaml.safe_load(f)
except yaml.YAMLError as e:
raise Exception("failed to parse Chart.yaml: %s" % e)
if "dependencies" in data:
for d in data["dependencies"]:
if not (
"name" in d
and "version" in d
and os.path.isfile(
os.path.join(
charts_dir, "%s-%s.tgz" % (d["name"], d["version"])
)
)
):
# Dependency missing, let's get it
break
else:
# All dependencies seem to be there so don't run anything
return
# Check if there is Chart.lock
if os.path.isfile(os.path.join(args[-1], "Chart.yaml")):
action = "update"
else:
action = "build"
# Run process
result = subprocess.run(
[
os.getenv("HELM_BIN", "helm"),
"dependency",
action,
]
+ args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
# Check for errors
if result.returncode != 0:
raise Exception(
"failed to run helm dependency build: rc=%d %s"
% (result.returncode, result.stderr)
)
def run_helm_template(args):
# Run process
result = subprocess.run(
[
os.getenv("HELM_BIN", "helm"),
"template",
]
+ args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
# Check for errors
if result.returncode != 0:
raise Exception(
"failed to run helm template: rc=%d %s" % (result.returncode, result.stderr)
)
return result
def run_kubeconform(args, input):
bin_file = "kubeconform"
# Try to use `HELM_PLUGIN_DIR` env var to determine location of kubeconform
helm_plugin_dir = os.getenv("HELM_PLUGIN_DIR", "")
helm_plugin_bin = os.path.join(helm_plugin_dir, "bin", bin_file)
if os.path.isfile(helm_plugin_bin):
bin_file = helm_plugin_bin
else:
helm_error = False
# Try to use `helm env` to determine location of kubeconform
try:
result = subprocess.run(
[
"helm",
"env",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
except Exception:
helm_error = True
if not helm_error:
for line in result.stdout.split("\n"):
if line.startswith("HELM_PLUGINS") and "=" in line:
_, plugins_path = line.split("=")
helm_plugin_bin = os.path.join(
plugins_path.strip('"'), bin_file, "bin", bin_file
)
if os.path.isfile(helm_plugin_bin):
bin_file = helm_plugin_bin
# Create the cache dir
if "-cache" in args:
c_idx = args.index("-cache")
c_dir = args[c_idx + 1]
if not os.path.isdir(c_dir):
try:
os.mkdir(c_dir, 0o755)
except OSError as e:
raise Exception("failed to create cache directory: %s" % e)
# Run process
result = subprocess.run(
[
bin_file,
]
+ args,
input=input,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
# Check for errors
if result.returncode != 0:
raise Exception(
"failed to run kubeconform: rc=%d\n%s%s"
% (result.returncode, result.stderr, result.stdout)
)
return result
def run_test(args, values_file=None):
values_args = []
# Add extra values parameter if any file is specified
if values_file:
values_args = [
"--values",
values_file,
]
# Build Helm dependencies
try:
run_helm_dependecy_build(
args["helm_build"],
)
except Exception as e:
raise Exception("dependency build failed: %s" % e)
# Get templated output
try:
helm_result = run_helm_template(
args["helm_tmpl"] + values_args,
)
except Exception as e:
raise Exception("templating failed: %s" % e)
# Get kubeconform output
try:
kubeconform_result = run_kubeconform(
args["kubeconform"],
helm_result.stdout,
)
except Exception as e:
raise Exception("kubeconform failed: %s" % e)
# Print results
if kubeconform_result.stdout:
print(kubeconform_result.stdout.rstrip())
def main():
# Parse args
args = parse_args()
# Ger logger
log = get_logger(args["wrapper"].debug)
# Parse config file
config_args = parse_config(
args["wrapper"].config,
)
# Merge the args from config file and from command line
if config_args:
args["kubeconform"] = config_args + args["kubeconform"]
# Get list of values files
values_files = get_values_files(
args["wrapper"].values_dir,
args["wrapper"].values_pattern,
)
# Run tests
try:
if values_files:
for values_file in values_files:
log.debug("Testing with CI values file %s" % values_file)
run_test(args, values_file)
else:
log.debug("Testing without CI values files")
run_test(args)
except Exception as e:
log.error("Testing failed: %s" % e)
sys.exit(1)
if __name__ == "__main__":
main()

117
scripts/helm/pre-commit.py Executable file
View file

@ -0,0 +1,117 @@
#!/usr/bin/env python3
import os
import sys
from contextlib import contextmanager
import plugin_wrapper as pw
@contextmanager
def cd(newdir):
prevdir = os.getcwd()
os.chdir(os.path.expanduser(newdir))
try:
yield
finally:
os.chdir(prevdir)
def main():
# Parse args
args = pw.parse_args(
add_chart=False,
add_files=True,
add_path=True,
add_incl_excl=True,
)
# We gonna change directory into the chart directory so we add it as local
# path for helm dependency build and helm template
args["helm_build"].append(".")
args["helm_tmpl"].append(".")
# Ger logger
log = pw.get_logger(
args["wrapper"].debug,
)
# Here we store paths fo the changed charts
charts = {}
# Calculate length of the path to the directory with charts
path_items = args["wrapper"].charts_path.split(os.sep)
# Take only paths pointing to files in the chart
path_items_len = len(path_items) + 1
# Includes and excludes
if args["wrapper"].include_charts is not None:
include_charts = list(map(str.strip, args["wrapper"].include_charts.split(",")))
else:
include_charts = []
if args["wrapper"].exclude_charts is not None:
exclude_charts = list(map(str.strip, args["wrapper"].exclude_charts.split(",")))
else:
exclude_charts = []
for f in args["wrapper"].FILES:
if f.startswith("%s%s" % (args["wrapper"].charts_path, os.sep)):
items = f.split(os.sep)
name = items[path_items_len - 1]
# Skip chart if it's not included or is excluded
if (
include_charts and name not in include_charts
) or name in exclude_charts:
continue
if len(items) > path_items_len:
path = os.sep.join(items[0:path_items_len])
if path not in charts:
charts[name] = path
# Change directory to the chart and run tests
for name, path in charts.items():
print("Testing chart '%s'" % name)
with cd(path):
# Parse config file
config_args = pw.parse_config(
args["wrapper"].config,
)
# Merge the args from config file and from command line
if config_args:
args["kubeconform"] = config_args + args["kubeconform"]
# Get list of values files
values_files = pw.get_values_files(
args["wrapper"].values_dir,
args["wrapper"].values_pattern,
)
# Run tests
try:
if values_files:
for values_file in values_files:
log.debug("Testing with an extra values file %s" % values_file)
pw.run_test(args, values_file)
else:
log.debug("Testing without any extra values files")
pw.run_test(args)
except Exception as e:
log.error("Testing failed: %s" % e)
sys.exit(1)
if __name__ == "__main__":
main()