Add integration test for Kustomize inetgration (#1288)

Summary of changes:

* Output any error from Mkdir in `helmfile template`

* Add failing test for .Release.Name interpolation

* Add golden files for testing

* Parse resources with kustomize to compare them structure by structure

* Decode resources into plain maps

The RNode type from kustomize uses yaml.Node under the hood,
which carries extra information like line numbers, which
become noisy when comparing with deep.Equal.
This commit is contained in:
ento 2020-06-15 16:06:52 -08:00 committed by GitHub
parent 3a2a460fe7
commit face92536c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 579 additions and 4 deletions

View File

@ -16,6 +16,7 @@ jobs:
- go-mod-cache-v1-
- run: go env
- run: make build
- run: make build-test-tools
- save_cache:
key: go-mod-cache-v1-{{ checksum "./go.sum" }}
paths:
@ -87,7 +88,10 @@ jobs:
- run: mkdir ~/build
- attach_workspace:
at: ~/build
- run: cp ~/build/helmfile ~/project/helmfile
- run:
command: |
cp ~/build/helmfile ~/project/helmfile
cp ~/build/diff-yamls ~/project/diff-yamls
- run:
name: Install helm
environment:
@ -98,6 +102,16 @@ jobs:
tar zxf ${HELM_FILENAME} linux-amd64/helm
chmod +x linux-amd64/helm
sudo mv linux-amd64/helm /usr/local/bin/
- run:
name: Install kustomize
environment:
KUSTOMIZE_VERSION: v3.6.1
command: |
KUSTOMIZE_FILENAME="kustomize_${KUSTOMIZE_VERSION}_linux_amd64.tar.gz"
curl -Lo ${KUSTOMIZE_FILENAME} "https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2F${KUSTOMIZE_VERSION}/${KUSTOMIZE_FILENAME}"
tar zxf ${KUSTOMIZE_FILENAME} kustomize
chmod +x kustomize
sudo mv kustomize /usr/local/bin/
- run:
name: Deploy minikube
environment:
@ -132,7 +146,10 @@ jobs:
- run: mkdir ~/build
- attach_workspace:
at: ~/build
- run: cp ~/build/helmfile ~/project/helmfile
- run:
command: |
cp ~/build/helmfile ~/project/helmfile
cp ~/build/diff-yamls ~/project/diff-yamls
- run:
name: Install helm
environment:
@ -143,6 +160,16 @@ jobs:
tar zxf ${HELM_FILENAME} linux-amd64/helm
chmod +x linux-amd64/helm
sudo mv linux-amd64/helm /usr/local/bin/
- run:
name: Install kustomize
environment:
KUSTOMIZE_VERSION: v3.6.1
command: |
KUSTOMIZE_FILENAME="kustomize_${KUSTOMIZE_VERSION}_linux_amd64.tar.gz"
curl -Lo ${KUSTOMIZE_FILENAME} "https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2F${KUSTOMIZE_VERSION}/${KUSTOMIZE_FILENAME}"
tar zxf ${KUSTOMIZE_FILENAME} kustomize
chmod +x kustomize
sudo mv kustomize /usr/local/bin/
- run:
name: Deploy minikube
environment:

1
.gitignore vendored
View File

@ -2,4 +2,5 @@ dist/
.idea/
helmfile
helmfile.lock
test/integration/tmp
vendor/

View File

@ -17,6 +17,10 @@ check:
go vet ${PKGS}
.PHONY: check
build-test-tools:
go build test/diff-yamls.go
.PHONY: build-test-tools
test:
go test -v ${PKGS} -cover -race -p=1
.PHONY: test

2
go.mod
View File

@ -21,6 +21,7 @@ require (
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
github.com/pierrec/lz4 v2.3.0+incompatible // indirect
github.com/r3labs/diff v0.0.0-20190801153147-a71de73c46ad
github.com/spf13/cobra v0.0.3
github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939
github.com/urfave/cli v1.20.0
github.com/variantdev/chartify v0.3.7
@ -35,4 +36,5 @@ require (
gopkg.in/yaml.v2 v2.2.4
gotest.tools v2.2.0+incompatible
gotest.tools/v3 v3.0.3-0.20200410202438-4e4a41b7851a
k8s.io/apimachinery v0.0.0-20190409092423-760d1845f48b
)

1
go.sum
View File

@ -755,6 +755,7 @@ github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945/go.mod h1:s
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=

View File

@ -898,7 +898,10 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, outputDir string,
flags = append(flags, "--output-dir", releaseOutputDir)
st.logger.Debugf("Generating templates to : %s\n", releaseOutputDir)
os.Mkdir(releaseOutputDir, 0755)
err = os.MkdirAll(releaseOutputDir, 0755)
if err != nil {
errs = append(errs, err)
}
}
if validate {

275
test/diff-yamls.go Normal file
View File

@ -0,0 +1,275 @@
package main
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/go-test/deep"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/yaml"
)
type diffSource string
const (
diffSourceLeft diffSource = "left"
diffSourceRight diffSource = "right"
)
var (
rootCmd = &cobra.Command{
Use: "diff-yamls dir-with-yamls/ dir-with-yamls/",
Short: "Print any diff between the given directories",
Long: `Similar to the 'diff' command, but file contents
are compared after being parsed as a set of Kubernetes manifests.`,
Args: cobra.ExactArgs(2),
Run: run,
}
)
func run(cmd *cobra.Command, args []string) {
left := args[0]
right := args[1]
leftYamls, err := globYamlFilenames(left)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
rightYamls, err := globYamlFilenames(right)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
exitCode := 0
onlyInLeft := leftYamls.Difference(rightYamls)
if onlyInLeft.Len() > 0 {
exitCode = 1
for _, f := range onlyInLeft.List() {
fmt.Fprintf(os.Stderr, "Only in %s: %s\n", left, f)
}
}
onlyInRight := rightYamls.Difference(leftYamls)
if onlyInRight.Len() > 0 {
exitCode = 1
for _, f := range onlyInRight.List() {
fmt.Fprintf(os.Stderr, "Only in %s: %s\n", right, f)
}
}
inBoth := leftYamls.Intersection(rightYamls)
for _, f := range inBoth.List() {
leftPath := filepath.Join(left, f)
leftNodes, err := readManifest(leftPath)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
rightPath := filepath.Join(right, f)
rightNodes, err := readManifest(rightPath)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
ps := pairs{}
for _, node := range leftNodes {
if err := ps.add(node, diffSourceLeft); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
for _, node := range rightNodes {
if err := ps.add(node, diffSourceRight); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
for _, p := range ps.list {
switch {
case p.left != nil && p.right == nil:
fmt.Fprintf(os.Stderr, "Only in %s: %s\n", leftPath, p.left.getId())
exitCode = 1
case p.left == nil && p.right != nil:
fmt.Fprintf(os.Stderr, "Only in %s: %s\n", rightPath, p.right.getId())
exitCode = 1
default:
diff := deep.Equal(p.left, p.right)
if diff != nil {
id := p.left.getId()
fmt.Fprintf(os.Stderr, "< %s %s\n", id, leftPath)
fmt.Fprintf(os.Stderr, "> %s %s\n", id, rightPath)
for _, d := range diff {
fmt.Fprintf(os.Stderr, "%s\n", d)
}
exitCode = 1
}
}
}
}
os.Exit(exitCode)
}
func globYamlFilenames(dir string) (sets.String, error) {
matches, err := filepath.Glob(filepath.Join(dir, "*.yaml"))
if err != nil {
return nil, err
}
set := sets.String{}
for _, f := range matches {
set.Insert(filepath.Base(f))
}
return set, nil
}
type resource map[string]interface{}
type meta struct {
apiVersion string
kind string
name string
namespace string
}
func (res resource) getMeta() (meta, error) {
if len(res) == 0 {
return meta{}, nil
}
m := meta{}
apiVersion, _ := res["apiVersion"].(string)
m.apiVersion = apiVersion
kind, _ := res["kind"].(string)
m.kind = kind
metadata, _ := res["metadata"].(map[string]interface{})
name, _ := metadata["name"].(string)
m.name = name
namespace, _ := metadata["namespace"].(string)
m.namespace = namespace
return m, nil
}
func readManifest(path string) ([]resource, error) {
var err error
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
decoder := yaml.NewYAMLToJSONDecoder(f)
resources := []resource{}
for {
r := make(resource)
err = decoder.Decode(&r)
if err != nil {
break
}
if len(r) > 0 {
resources = append(resources, r)
}
}
if err != nil && err != io.EOF {
return nil, err
}
return resources, nil
}
func (res resource) getId() string {
meta, err := res.getMeta()
if err != nil {
return fmt.Sprintf("%v", res)
}
ns := meta.namespace
if ns == "" {
ns = "~X"
}
nm := meta.name
if nm == "" {
nm = "~N"
}
gv := meta.apiVersion
if gv == "" {
gv = "~G_~V"
}
k := meta.kind
if k == "" {
k = "~K"
}
gvk := strings.Join([]string{gv, k}, "_")
return strings.Join([]string{gvk, ns, nm}, "|")
}
// lifted from kustomize/kyaml/kio/filters/merge3.go
type pairs struct {
list []*pair
}
func (ps *pairs) isSameResource(meta1, meta2 meta) bool {
if meta1.name != meta2.name {
return false
}
if meta1.namespace != meta2.namespace {
return false
}
if meta1.apiVersion != meta2.apiVersion {
return false
}
if meta1.kind != meta2.kind {
return false
}
return true
}
func (ps *pairs) add(node resource, source diffSource) error {
nodeMeta, err := node.getMeta()
if err != nil {
return err
}
for i := range ps.list {
p := ps.list[i]
if ps.isSameResource(p.meta, nodeMeta) {
return p.add(node, source)
}
}
p := &pair{meta: nodeMeta}
if err := p.add(node, source); err != nil {
return err
}
ps.list = append(ps.list, p)
return nil
}
type pair struct {
meta meta
left resource
right resource
}
func (p *pair) add(node resource, source diffSource) error {
switch source {
case diffSourceLeft:
if p.left != nil {
return fmt.Errorf("left source already specified")
}
p.left = node
case diffSourceRight:
if p.right != nil {
return fmt.Errorf("right source already specified")
}
p.right = node
default:
return fmt.Errorf("unknown diff source value: %s", source)
}
return nil
}
func main() {
rootCmd.Execute()
}

View File

@ -0,0 +1,21 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*~
# Various IDEs
.project
.idea/
*.tmproj

View File

@ -0,0 +1,4 @@
apiVersion: v1
description: Chart for testing helmx features
name: helmx
version: 0.1.0

View File

@ -0,0 +1,16 @@
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "helmx.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
*/}}
{{- define "helmx.fullname" -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}

View File

@ -0,0 +1,13 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: release-name
data:
name: {{ .Release.Name }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: release-namespace
data:
namespace: {{ .Release.Namespace }}

View File

@ -25,3 +25,17 @@ releases:
values:
- mysecret: {{ .Environment.Values.mysecret }}
- values.yaml
- name: helmx
chart: ./charts/helmx
namespace: helmx-system
jsonPatches:
- target:
version: v1
kind: ConfigMap
name: release-name
patch:
- op: add
path: /metadata/annotations
value:
foo: bar

View File

@ -47,12 +47,15 @@ set -e
info "Using namespace: ${test_ns}"
# helm v2
if helm version --client 2>/dev/null | grep '"v2\.'; then
helm_major_version=2
info "Using Helm version: $(helm version --short --client | grep -o v.*$)"
${helm} init --wait --override spec.template.spec.automountServiceAccountToken=true
# helm v3
else
helm_major_version=3
info "Using Helm version: $(helm version --short | grep -o v.*$)"
fi
info "Using Kustomize version: $(kustomize version --short | grep -o 'v[^ ]+')"
${helm} plugin install https://github.com/databus23/helm-diff --version v3.0.0-rc.7
${kubectl} get namespace ${test_ns} &> /dev/null && warn "Namespace ${test_ns} exists, from a previous test run?"
$kubectl create namespace ${test_ns} || fail "Could not create namespace ${test_ns}"
@ -73,9 +76,19 @@ info "Diffing ${dir}/happypath.yaml with limited context"
bash -c "${helmfile} -f ${dir}/happypath.yaml diff --context 3 --detailed-exitcode; code="'$?'"; [ "'${code}'" -eq 2 ]" || fail "unexpected exit code returned by helmfile diff"
info "Templating ${dir}/happypath.yaml"
${helmfile} -f ${dir}/happypath.yaml template
rm -rf ${dir}/tmp
${helmfile} -f ${dir}/happypath.yaml --debug template --output-dir tmp
code=$?
[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile template: ${code}"
for output in $(ls -d ${dir}/tmp/*); do
# e.g. test/integration/tmp/happypath-877c0dd4-helmx/helmx
for release_dir in $(ls -d ${output}/*); do
release_name=$(basename ${release_dir})
golden_dir=${dir}/templates-golden/v${helm_major_version}/${release_name}
info "Comparing template output ${release_dir}/templates with ${golden_dir}"
./diff-yamls ${golden_dir} ${release_dir}/templates || fail "unexpected diff in template result for ${release_name}"
done
done
info "Applying ${dir}/happypath.yaml"
bash -c "${helmfile} -f ${dir}/happypath.yaml apply --detailed-exitcode; code="'$?'"; echo Code: "'$code'"; [ "'${code}'" -eq 2 ]" || fail "unexpected exit code returned by helmfile apply"

View File

@ -0,0 +1,17 @@
---
# Source: helmx/templates/helmx.all.yaml
apiVersion: v1
data:
namespace: helmx-system
kind: ConfigMap
metadata:
name: release-namespace
---
apiVersion: v1
data:
name: helmx
kind: ConfigMap
metadata:
name: release-name
annotations:
foo: bar

View File

@ -0,0 +1,39 @@
---
# Source: httpbin/templates/deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
metadata:
name: httpbin-httpbin
labels:
app: httpbin
chart: httpbin-0.1.0
release: httpbin
heritage: Tiller
spec:
replicas: 1
strategy: {}
template:
metadata:
labels:
app: httpbin
release: httpbin
spec:
containers:
- name: httpbin
image: "docker.io/citizenstig/httpbin:latest"
imagePullPolicy: IfNotPresent
livenessProbe:
httpGet:
path: /
port: 8000
readinessProbe:
httpGet:
path: /
port: 8000
ports:
- containerPort: 8000
resources:
{}
status: {}

View File

@ -0,0 +1,21 @@
---
# Source: httpbin/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: httpbin-httpbin
labels:
app: httpbin
chart: httpbin-0.1.0
release: httpbin
heritage: Tiller
spec:
type: LoadBalancer
ports:
- port: 8000
targetPort: 8000
protocol: TCP
name: httpbin
selector:
app: httpbin
release: httpbin

View File

@ -0,0 +1,13 @@
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: Secret
metadata:
labels:
app: raw
chart: raw-0.2.3
heritage: Tiller
release: raw
name: common-secret
stringData:
mykey: MYSECRET

View File

@ -0,0 +1,18 @@
---
# Source: helmx/templates/helmx.all.yaml
apiVersion: v1
data:
namespace: helmx-system
kind: ConfigMap
metadata:
name: release-namespace
---
# Source: helmx/templates/helmx.all.yaml
apiVersion: v1
data:
name: helmx
kind: ConfigMap
metadata:
name: release-name
annotations:
foo: bar

View File

@ -0,0 +1,39 @@
---
# Source: httpbin/templates/deployment.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
metadata:
name: httpbin-httpbin
labels:
app: httpbin
chart: httpbin-0.1.0
release: httpbin
heritage: Helm
spec:
replicas: 1
strategy: {}
template:
metadata:
labels:
app: httpbin
release: httpbin
spec:
containers:
- name: httpbin
image: "docker.io/citizenstig/httpbin:latest"
imagePullPolicy: IfNotPresent
livenessProbe:
httpGet:
path: /
port: 8000
readinessProbe:
httpGet:
path: /
port: 8000
ports:
- containerPort: 8000
resources:
{}
status: {}

View File

@ -0,0 +1,21 @@
---
# Source: httpbin/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: httpbin-httpbin
labels:
app: httpbin
chart: httpbin-0.1.0
release: httpbin
heritage: Helm
spec:
type: LoadBalancer
ports:
- port: 8000
targetPort: 8000
protocol: TCP
name: httpbin
selector:
app: httpbin
release: httpbin

View File

@ -0,0 +1,13 @@
---
# Source: raw/templates/resources.yaml
apiVersion: v1
kind: Secret
metadata:
labels:
app: raw
chart: raw-0.2.3
heritage: Helm
release: raw
name: common-secret
stringData:
mykey: MYSECRET