mirror of
https://github.com/yannh/kubeconform.git
synced 2026-02-18 17:37:03 +00:00
Adding Helm plugin and pre-commit
This commit is contained in:
parent
752a33eaeb
commit
2097831d98
7 changed files with 1146 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
dist/
|
||||
bin/
|
||||
.idea/
|
||||
**/*.pyc
|
||||
|
|
|
|||
7
.pre-commit-hooks.yaml
Normal file
7
.pre-commit-hooks.yaml
Normal 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
239
Readme.md
|
|
@ -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
14
plugin.yaml
Normal 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
216
scripts/helm/plugin_binloader.sh
Executable 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
552
scripts/helm/plugin_wrapper.py
Executable 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
117
scripts/helm/pre-commit.py
Executable 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()
|
||||
Loading…
Reference in a new issue