diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..e7472921 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,97 @@ +version: 2 + +jobs: + + build: + docker: + - image: circleci/golang:1.7 + working_directory: /go/src/github.com/roboll/helmfile + steps: + - checkout + - run: make build + - persist_to_workspace: + root: /go/src/github.com/roboll/helmfile + paths: + - . + + test: + docker: + - image: circleci/golang:1.7 + working_directory: /go/src/github.com/roboll/helmfile + steps: + - checkout + - run: make check + - run: make pristine + - run: make test + + # thanks to https://raw.githubusercontent.com/weaveworks/launcher/master/.circleci/config.yml + integration_tests: + machine: true + environment: + CHANGE_MINIKUBE_NONE_USER: true + steps: + - checkout + - run: mkdir ~/build + - attach_workspace: + at: ~/build + - run: cp ~/build/helmfile ~/project/helmfile + - run: + name: Install kubectl + command: | + curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/v1.8.4/bin/linux/amd64/kubectl + chmod +x kubectl + sudo mv kubectl /usr/local/bin/ + - run: + name: Install minikube + command: | + curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.25.0/minikube-linux-amd64 + chmod +x minikube + sudo mv minikube /usr/local/bin/ + - run: + name: Install helm + command: | + HELM_VERSION=v2.8.2 + HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz" + curl -Lo ${HELM_FILENAME} "https://kubernetes-helm.storage.googleapis.com/${HELM_FILENAME}" + tar zxf ${HELM_FILENAME} linux-amd64/helm + chmod +x linux-amd64/helm + sudo mv linux-amd64/helm /usr/local/bin/ + - run: + name: Start minikube + command: | + sudo minikube start --vm-driver=none + sudo chown -R $USER.$USER ~/.minikube + sudo chown -R $USER.$USER ~/.kube + minikube update-context + - run: + name: Wait for nodes to become ready + command: JSONPATH='{range .items[*]}{@.metadata.name}:{range @.status.conditions[*]}{@.type}={@.status};{end}{end}'; until kubectl get nodes -o jsonpath="$JSONPATH" 2>&1 | grep -q "Ready=True"; do sleep 1; done + - run: + name: Execute integration tests + command: | + export TERM=xterm + make integration + +# GITHUB_TOKEN env var must be setup in circleci console +deployment: + release: + tag: /v.*/ + commands: + - docker login -e="." -u="$DOCKER_USER" -p="$DOCKER_PASS" quay.io + - cd "$WORK" && make tools + - cd "$WORK" && BUILD_URL="$CIRCLE_BUILD_URL" make push release + +workflows: + version: 2 + build_and_test: + jobs: + - build + - test + - integration_tests: + requires: + - build + filters: + branches: + only: + - master + - /pull.*/ \ No newline at end of file diff --git a/Makefile b/Makefile index 63c2a314..9ad1ed53 100644 --- a/Makefile +++ b/Makefile @@ -21,10 +21,18 @@ test: go test -v ${PKGS} -cover -race -p=1 .PHONY: test +integration: + bash test/integration/run.sh +.PHONY: integration + cross: gox -os '!freebsd !netbsd' -arch '!arm' -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" -ldflags '-X main.Version=${TAG}' ${TARGETS} .PHONY: cross +clean: + rm dist/helmfile_* +.PHONY: clean + pristine: generate fmt git ls-files --exclude-standard --modified --deleted --others | diff /dev/null - .PHONY: pristine diff --git a/circle.yml b/circle.yml deleted file mode 100644 index ea75d861..00000000 --- a/circle.yml +++ /dev/null @@ -1,34 +0,0 @@ -machine: - services: [ docker ] - post: - - mkdir -p download - - test -e download/$GODIST || curl -o download/$GODIST https://storage.googleapis.com/golang/$GODIST - - sudo rm -rf /usr/local/go - - sudo tar -C /usr/local -xzf download/$GODIST - environment: - GODIST: "go1.7.linux-amd64.tar.gz" - GOPATH: "$HOME/go" - PATH: "$PATH:$GOPATH/bin" - WORK: "$GOPATH/src/github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/" - -dependencies: - override: - - mkdir -p "$WORK" - - rsync -az --delete ./ "$WORK" - -test: - pre: - - cd "$WORK" && make check - - cd "$WORK" && make pristine - - override: - - cd "$WORK" && make test - -# GITHUB_TOKEN env var must be setup in circleci console -deployment: - release: - tag: /v.*/ - commands: - - docker login -e="." -u="$DOCKER_USER" -p="$DOCKER_PASS" quay.io - - cd "$WORK" && make tools - - cd "$WORK" && BUILD_URL="$CIRCLE_BUILD_URL" make push release diff --git a/test/integration/charts/httpbin/.helmignore b/test/integration/charts/httpbin/.helmignore new file mode 100644 index 00000000..f0c13194 --- /dev/null +++ b/test/integration/charts/httpbin/.helmignore @@ -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 diff --git a/test/integration/charts/httpbin/Chart.yaml b/test/integration/charts/httpbin/Chart.yaml new file mode 100644 index 00000000..f4e846c3 --- /dev/null +++ b/test/integration/charts/httpbin/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +description: HTTP Request & Response Service, written in Python + Flask. https://httpbin.org +name: httpbin +version: 0.1.0 + diff --git a/test/integration/charts/httpbin/templates/_helpers.tpl b/test/integration/charts/httpbin/templates/_helpers.tpl new file mode 100644 index 00000000..cf15b710 --- /dev/null +++ b/test/integration/charts/httpbin/templates/_helpers.tpl @@ -0,0 +1,16 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "httpbin.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 "httpbin.fullname" -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/test/integration/charts/httpbin/templates/deployment.yaml b/test/integration/charts/httpbin/templates/deployment.yaml new file mode 100644 index 00000000..d24b7ed9 --- /dev/null +++ b/test/integration/charts/httpbin/templates/deployment.yaml @@ -0,0 +1,38 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: +metadata: + name: {{ template "httpbin.fullname" . }} + labels: + app: {{ template "httpbin.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: 1 + strategy: {} + template: + metadata: + labels: + app: {{ template "httpbin.name" . }} + release: {{ .Release.Name }} + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + livenessProbe: + httpGet: + path: / + port: {{ .Values.service.internalPort }} + readinessProbe: + httpGet: + path: / + port: {{ .Values.service.internalPort }} + ports: + - containerPort: {{ .Values.service.internalPort }} + resources: +{{ toYaml .Values.resources | indent 10 }} + +status: {} +--- diff --git a/test/integration/charts/httpbin/templates/ingress.yaml b/test/integration/charts/httpbin/templates/ingress.yaml new file mode 100644 index 00000000..bcd4311f --- /dev/null +++ b/test/integration/charts/httpbin/templates/ingress.yaml @@ -0,0 +1,33 @@ +{{- if .Values.ingress.enabled -}} +{{- $serviceName := include "httpbin.fullname" . -}} +{{- $servicePort := .Values.service.externalPort -}} +{{- $prefix := .Values.ingress.prefix -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ template "httpbin.fullname" . }} + labels: + app: {{ template "httpbin.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} + annotations: + {{- range $key, $value := .Values.ingress.annotations }} + {{ $key }}: {{ $value | quote }} + {{- end }} +spec: + rules: + {{- range $host := .Values.ingress.hosts }} + - host: {{ $host }} + http: + paths: + - path: / + backend: + serviceName: {{ $serviceName }} + servicePort: {{ $servicePort }} + {{- end -}} + {{- if .Values.ingress.tls }} + tls: +{{ toYaml .Values.ingress.tls | indent 4 }} + {{- end -}} +{{- end -}} diff --git a/test/integration/charts/httpbin/templates/service.yaml b/test/integration/charts/httpbin/templates/service.yaml new file mode 100644 index 00000000..cc71903d --- /dev/null +++ b/test/integration/charts/httpbin/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "httpbin.fullname" . }} + labels: + app: {{ template "httpbin.name" . }} + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.externalPort }} + targetPort: {{ .Values.service.internalPort }} + protocol: TCP + name: {{ .Values.service.name }} + selector: + app: {{ template "httpbin.name" . }} + release: {{ .Release.Name }} diff --git a/test/integration/charts/httpbin/values.yaml b/test/integration/charts/httpbin/values.yaml new file mode 100644 index 00000000..34fe6360 --- /dev/null +++ b/test/integration/charts/httpbin/values.yaml @@ -0,0 +1,14 @@ +image: + repository: docker.io/citizenstig/httpbin + tag: latest + pullPolicy: IfNotPresent +service: + name: httpbin + type: LoadBalancer + externalPort: 8000 + internalPort: 8000 +ingress: + enabled: false + hosts: + - httpbin.local +resources: {} diff --git a/test/integration/happypath.yaml b/test/integration/happypath.yaml new file mode 100644 index 00000000..5dac162e --- /dev/null +++ b/test/integration/happypath.yaml @@ -0,0 +1,13 @@ +repositories: + - name: stable + url: https://kubernetes-charts.storage.googleapis.com/ + +context: minikube + +releases: + + - name: httpbin + chart: ./charts/httpbin + set: + - name: ingress.enabled + value: false diff --git a/test/integration/lib/ensure.sh b/test/integration/lib/ensure.sh new file mode 100644 index 00000000..bf3e1798 --- /dev/null +++ b/test/integration/lib/ensure.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# Check environment is correctly setup + +if ! hash minikube 2>/dev/null; then + fail "Minikube needs to be installed." +fi +if [ ! $(minikube status --format '{{.MinikubeStatus}}') == "Running" ]; then + fail "Minikube is not running." +fi +if [ ! $(minikube status --format '{{.ClusterStatus}}') == "Running" ]; then + fail "Minikube Cluster is not running." +fi +if ! kubectl version --short 1> /dev/null; then + fail "Could not connect to minikube apiserver" +fi +if ! hash curl 1>/dev/null; then + fail "curl needs to be installed." +fi +if ! hash docker 1>/dev/null; then + fail "Docker needs to be installed." +fi +if ! docker version 1> /dev/null; then + fail "Could not connect to Docker daemon" +fi +if ! hash helm 1>/dev/null; then + fail "Helm needs to be installed." +fi diff --git a/test/integration/lib/output.sh b/test/integration/lib/output.sh new file mode 100644 index 00000000..05db3d5b --- /dev/null +++ b/test/integration/lib/output.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +declare -i tests_total=0 + +function info () { + tput bold; tput setaf 4; echo -n "INFO: "; tput sgr0; echo "${@}" +} +function warn () { + tput bold; tput setaf 3; echo -n "WARN: "; tput sgr0; echo "${@}" +} +function fail () { + tput bold; tput setaf 1; echo -n "FAIL: "; tput sgr0; echo "${@}" + exit 1 +} +function test_start () { + tput bold; tput setaf 6; echo -n "TEST: "; tput sgr0; echo "${@}" +} +function test_pass () { + tests_total=$((tests_total+1)) + tput bold; tput setaf 2; echo -n "PASS: "; tput sgr0; echo "${@}" +} +function all_tests_passed () { + tput bold; tput setaf 2; echo -n "PASS: "; tput sgr0; echo "${tests_total} tests passed" +} diff --git a/test/integration/run.sh b/test/integration/run.sh new file mode 100755 index 00000000..5dc5395d --- /dev/null +++ b/test/integration/run.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +# IMPORTS ----------------------------------------------------------------------------------------------------------- + +# determine working directory to use to relative paths irrespective of starting directory +dir="${BASH_SOURCE%/*}" +if [[ ! -d "${dir}" ]]; then dir="${PWD}"; fi + +. "${dir}/lib/output.sh" +. "${dir}/lib/ensure.sh" + + +# GLOBALS ----------------------------------------------------------------------------------------------------------- + +test_ns="helmfile-tests-$(date +"%Y%m%d-%H%M%S")" +helmfile="./helmfile --namespace=${test_ns}" +helm="helm --kube-context=minikube" +kubectl="kubectl --context=minikube --namespace=${test_ns}" + +# FUNCTIONS ---------------------------------------------------------------------------------------------------------- + +function wait_deploy_ready() { + $kubectl rollout status deployment ${1} + while [ "$($kubectl get deploy ${1} -o=jsonpath='{.status.readyReplicas}')" == "0" ]; do + info "Waiting for deployment ${1} to be ready" + sleep 1 + done +} + +# SETUP -------------------------------------------------------------------------------------------------------------- + +set -e +info "Using namespace: ${test_ns}" +info "Using Helm version: $(helm version --short --client | grep -o v.*$)" +$helm init --wait +$helmfile -v +$kubectl get namespace ${test_ns} &> /dev/null && warn "Namespace ${test_ns} exists, from a previous test run?" +trap "{ $kubectl delete namespace ${test_ns}; }" EXIT # remove namespace whenever we exit this script + + +# TEST CASES---------------------------------------------------------------------------------------------------------- + +test_start "happypath - simple rollout of httpbin chart" +$helmfile -f ${dir}/happypath.yaml sync +wait_deploy_ready httpbin-httpbin +curl --fail $(minikube service --url --namespace=${test_ns} httpbin-httpbin)/status/200 \ + || fail "httpbin failed to return 200 OK" +$helmfile -f ${dir}/happypath.yaml delete +$helm status --namespace=${test_ns} httpbin &> /dev/null \ + && fail "release should not exist anymore after a delete" +test_pass "happypath" + + +# ALL DONE ----------------------------------------------------------------------------------------------------------- + +all_tests_passed \ No newline at end of file