mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-22 16:37:08 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b86408500 | ||
|
|
bdbaa00628 | ||
|
|
87e86dcfd2 | ||
|
|
995b201e47 | ||
|
|
0663831dd5 | ||
|
|
afb2653452 | ||
|
|
9ec4a85821 | ||
|
|
4d868794fd | ||
|
|
1aca099457 | ||
|
|
f5d098876a | ||
|
|
080080f57f | ||
|
|
cd4afc3269 | ||
|
|
8f8669778c | ||
|
|
563fce4adf | ||
|
|
b287101625 | ||
|
|
ab9fb2c9dc | ||
|
|
c170b5a621 | ||
|
|
d9286ed8f6 | ||
|
|
3018e82902 | ||
|
|
34acf14d7c | ||
|
|
5d8eba9b29 | ||
|
|
b284b7be83 | ||
|
|
b293ac1f83 | ||
|
|
8d9d635b8b | ||
|
|
954673d742 | ||
|
|
942b9a64c0 | ||
|
|
b176408eb2 | ||
|
|
ff81b2f279 | ||
|
|
8db03f979c | ||
|
|
988c218096 | ||
|
|
028bcc51dc | ||
|
|
5dd65e8d9a | ||
|
|
134d5beba2 | ||
|
|
87d04f0202 | ||
|
|
2d7c8e7525 | ||
|
|
0fad9f0544 | ||
|
|
832dcf47a5 | ||
|
|
1d2c8b8a3a | ||
|
|
0482ba3b6f | ||
|
|
9d2c0d4285 | ||
|
|
efd26f288a | ||
|
|
5f1698d0ed | ||
|
|
2ecf5780f3 | ||
|
|
3e6542e341 | ||
|
|
d3daea3137 | ||
|
|
19d79427a1 | ||
|
|
cd0ecc5263 | ||
|
|
dc6c59dc14 | ||
|
|
0fc0869671 | ||
|
|
5ca7ce15bc | ||
|
|
6b4b76e2bc | ||
|
|
7b11ce851a | ||
|
|
c575587e1f | ||
|
|
f2dfa7ce64 | ||
|
|
41cd1fe4a6 | ||
|
|
9e54af5c2c | ||
|
|
8a43e8f782 | ||
|
|
94e01b79e0 | ||
|
|
85e77982bf | ||
|
|
b42e847154 | ||
|
|
14e2b9ed93 | ||
|
|
90a41222e6 | ||
|
|
a9aa7af572 | ||
|
|
738409ee0e | ||
|
|
ec87a39015 | ||
|
|
ab1c118022 | ||
|
|
c8c444debc | ||
|
|
9a03d79a84 | ||
|
|
0ef7e65f02 | ||
|
|
61b61d3009 | ||
|
|
2710cb382f | ||
|
|
4d7fcd846e | ||
|
|
b85243a6b4 | ||
|
|
53c3fe9f37 | ||
|
|
2ce57ba2ee | ||
|
|
cc9b7f3942 | ||
|
|
85a2024669 | ||
|
|
87573089e8 | ||
|
|
4fb47a833a | ||
|
|
9cffef50b0 | ||
|
|
b4857937fb | ||
|
|
1e260e4a5e | ||
|
|
ee8ea507a6 | ||
|
|
b5830a3011 | ||
|
|
4fde6e13db | ||
|
|
a5e790caf1 | ||
|
|
1e956ae8a5 | ||
|
|
1671b8ba7d | ||
|
|
608ba19c3c | ||
|
|
daf7d3b876 | ||
|
|
34f6d4d568 | ||
|
|
1784b02d03 | ||
|
|
68c51d8c16 | ||
|
|
4bbb1699a3 | ||
|
|
753f65a56d | ||
|
|
43adbeaaca | ||
|
|
df6489ab43 | ||
|
|
31d0fce6fd | ||
|
|
cd427d9714 | ||
|
|
6d4f0984b6 | ||
|
|
ac40eec423 | ||
|
|
8de9b70b33 | ||
|
|
face92536c | ||
|
|
3a2a460fe7 | ||
|
|
f16d96bc8f | ||
|
|
0f86cc9b87 | ||
|
|
ca844aa044 | ||
|
|
604af3f1d2 | ||
|
|
28e13c3a18 | ||
|
|
7db31846cf | ||
|
|
63c8df9873 | ||
|
|
eff2a7bf84 | ||
|
|
3284df2752 | ||
|
|
e607bae5ff | ||
|
|
930f91a70d | ||
|
|
1cfce32e0d | ||
|
|
29fdb57add | ||
|
|
68432b1848 | ||
|
|
16288dfa7d | ||
|
|
969de1ebc9 | ||
|
|
ceb108a24f | ||
|
|
576a07f497 | ||
|
|
eb8ab54865 | ||
|
|
9e4bc6e7fc | ||
|
|
ef2d8853c2 | ||
|
|
66f34580c3 | ||
|
|
6d5f8c71cb | ||
|
|
23be9af2db | ||
|
|
1e9e228a9a | ||
|
|
c72cab80b5 | ||
|
|
923bd54db0 | ||
|
|
a0e30c5193 | ||
|
|
daf8c2f12b | ||
|
|
d71ac15748 | ||
|
|
eeb61e6174 | ||
|
|
b1190508b2 | ||
|
|
3a19a394ab | ||
|
|
dcb19f942b | ||
|
|
458385d971 | ||
|
|
b89fba1cc2 | ||
|
|
6b6b86a0b5 | ||
|
|
e0a793b7c5 | ||
|
|
71bb7354e7 | ||
|
|
3a34d28d7c | ||
|
|
f39d7928bf | ||
|
|
e0811dffa2 | ||
|
|
7d11f5dedc | ||
|
|
870cc03c70 | ||
|
|
98886df5d2 | ||
|
|
71635caace | ||
|
|
486be0970d | ||
|
|
9d7d2de6f5 | ||
|
|
04c963d050 | ||
|
|
a2c62c9e73 | ||
|
|
1654ce4c18 | ||
|
|
25aae679b1 | ||
|
|
d404356230 | ||
|
|
51ecd12360 | ||
|
|
6643a41ea3 | ||
|
|
35e5454994 | ||
|
|
69feadc360 | ||
|
|
f676c61425 | ||
|
|
20a39e9412 | ||
|
|
048c79ad34 | ||
|
|
994e4b66fc | ||
|
|
499f6ec6e3 | ||
|
|
bf22502a2f | ||
|
|
7c80a859fa | ||
|
|
00c4422a64 | ||
|
|
af44965949 | ||
|
|
0186254e79 | ||
|
|
8acbbc596d | ||
|
|
f1bdb65777 | ||
|
|
322ed4d1d8 | ||
|
|
c4e6160220 | ||
|
|
10f2a576c5 | ||
|
|
2579a876a6 | ||
|
|
6b3277ce71 | ||
|
|
27425bc4d2 | ||
|
|
05add478c1 | ||
|
|
757e748e3f | ||
|
|
e6f1db128c | ||
|
|
5fb037c8e3 | ||
|
|
5abbdea059 | ||
|
|
9fab6851f0 | ||
|
|
c8ba676e19 | ||
|
|
fc75f25293 | ||
|
|
b044820607 | ||
|
|
0e1aeb43ce | ||
|
|
88adc56551 | ||
|
|
9cf6b59cd8 | ||
|
|
fb7cc60afe | ||
|
|
90c7b04dbf | ||
|
|
ac534a897b | ||
|
|
314557c824 | ||
|
|
4b2d6946be | ||
|
|
03898b7a98 | ||
|
|
4b1b19f8a6 | ||
|
|
07c42474cc | ||
|
|
27c098c17c | ||
|
|
e745a2602b | ||
|
|
04379cee80 | ||
|
|
25599eae89 | ||
|
|
8ea144e31f | ||
|
|
f508b9091e | ||
|
|
7ec92c0520 | ||
|
|
06a3b558e0 | ||
|
|
8a2311d033 | ||
|
|
312ca76751 | ||
|
|
80dcdfb821 | ||
|
|
f08da43e6c | ||
|
|
468b9b659d | ||
|
|
3ccb43fbb6 | ||
|
|
b99daa3605 | ||
|
|
990d31c667 | ||
|
|
45943f9306 | ||
|
|
dc2961a493 | ||
|
|
d15c4aad2d | ||
|
|
c099f69d94 | ||
|
|
43f1188325 | ||
|
|
f466800e1a | ||
|
|
ada5f05b36 | ||
|
|
a136c46c06 | ||
|
|
e2e4e8440d | ||
|
|
77082cef58 | ||
|
|
8d7c79a323 | ||
|
|
ed7a6d9051 | ||
|
|
35adb8ae22 | ||
|
|
a0c902d6d1 | ||
|
|
ebad0ad20f | ||
|
|
4751613f39 | ||
|
|
f41fe86452 | ||
|
|
dad8e72bc5 | ||
|
|
d0dd4a29af | ||
|
|
8fc546ae86 | ||
|
|
fbbd7630e7 | ||
|
|
30751e94f7 | ||
|
|
f47d8ea256 | ||
|
|
6955d07ea8 | ||
|
|
3f02b86640 | ||
|
|
e258052933 | ||
|
|
4c892edb7e | ||
|
|
464e6bc782 | ||
|
|
78bc481675 | ||
|
|
a42c098727 | ||
|
|
fc0a7b68a8 | ||
|
|
88caa7514f | ||
|
|
2c7810a623 | ||
|
|
7666e95690 | ||
|
|
b8f24948bb | ||
|
|
730ae35c73 | ||
|
|
4680010c60 | ||
|
|
849ed63999 | ||
|
|
9990242d60 | ||
|
|
4294a5de06 | ||
|
|
d6bd317c9c | ||
|
|
fdca10678b | ||
|
|
f51949ac3f | ||
|
|
76b3a17427 | ||
|
|
2264da3ac9 | ||
|
|
cf9bbc7603 |
+116
-11
@@ -4,7 +4,7 @@ jobs:
|
||||
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.12.4
|
||||
- image: circleci/golang:1.14.2
|
||||
environment:
|
||||
GOPATH: /go
|
||||
working_directory: /home/circleci/workspace/helmfile
|
||||
@@ -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:
|
||||
@@ -24,12 +25,45 @@ jobs:
|
||||
root: ./
|
||||
paths:
|
||||
- .
|
||||
image:
|
||||
docker:
|
||||
- image: circleci/golang:1.14.2
|
||||
working_directory: /home/circleci/workspace/helmfile
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-mod-cache-v1-{{ checksum "./go.sum" }}
|
||||
- go-mod-cache-v1-
|
||||
- setup_remote_docker
|
||||
- run:
|
||||
command: |
|
||||
make tools
|
||||
go mod vendor
|
||||
ORG=roboll BUILD_URL="$CIRCLE_BUILD_URL" make image
|
||||
|
||||
image_helm3:
|
||||
docker:
|
||||
- image: circleci/golang:1.14.2
|
||||
working_directory: /home/circleci/workspace/helmfile
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-mod-cache-v1-{{ checksum "./go.sum" }}
|
||||
- go-mod-cache-v1-
|
||||
- setup_remote_docker
|
||||
- run:
|
||||
command: |
|
||||
make tools
|
||||
go mod vendor
|
||||
ORG=roboll BUILD_URL="$CIRCLE_BUILD_URL" make image/helm3
|
||||
|
||||
test:
|
||||
environment:
|
||||
GOPATH: /go
|
||||
docker:
|
||||
- image: circleci/golang:1.12.4
|
||||
- image: circleci/golang:1.14.2
|
||||
working_directory: /home/circleci/workspace/helmfile
|
||||
steps:
|
||||
- checkout
|
||||
@@ -54,17 +88,30 @@ 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:
|
||||
HELM_VERSION: v2.13.0
|
||||
HELM_VERSION: v2.16.12
|
||||
command: |
|
||||
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: 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:
|
||||
@@ -87,11 +134,69 @@ jobs:
|
||||
export TERM=xterm
|
||||
make integration
|
||||
|
||||
integration_tests_helm3:
|
||||
machine:
|
||||
image: circleci/classic:201808-01
|
||||
environment:
|
||||
CHANGE_MINIKUBE_NONE_USER: true
|
||||
MINIKUBE_WANTUPDATENOTIFICATION: false
|
||||
MINIKUBE_WANTREPORTERRORPROMPT: false
|
||||
steps:
|
||||
- checkout
|
||||
- run: mkdir ~/build
|
||||
- attach_workspace:
|
||||
at: ~/build
|
||||
- run:
|
||||
command: |
|
||||
cp ~/build/helmfile ~/project/helmfile
|
||||
cp ~/build/diff-yamls ~/project/diff-yamls
|
||||
- run:
|
||||
name: Install helm
|
||||
environment:
|
||||
HELM_VERSION: v3.3.3
|
||||
command: |
|
||||
HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
|
||||
curl -Lo ${HELM_FILENAME} "https://get.helm.sh/${HELM_FILENAME}"
|
||||
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:
|
||||
CHANGE_MINIKUBE_NONE_USER: true
|
||||
K8S_VERSION: v1.12.3
|
||||
MINIKUBE_VERSION: v0.30.0
|
||||
command: |
|
||||
curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/${K8S_VERSION}/bin/linux/amd64/kubectl
|
||||
chmod +x kubectl && sudo mv kubectl /usr/local/bin/
|
||||
curl -Lo minikube https://storage.googleapis.com/minikube/releases/${MINIKUBE_VERSION}/minikube-linux-amd64
|
||||
chmod +x minikube && sudo mv minikube /usr/local/bin/
|
||||
sudo -E minikube start --vm-driver=none --kubernetes-version=${K8S_VERSION}
|
||||
sudo -E 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
|
||||
HELMFILE_HELM3=1 make integration
|
||||
|
||||
# GITHUB_TOKEN env var must be setup in circleci console
|
||||
|
||||
release:
|
||||
docker:
|
||||
- image: circleci/golang:1.12.4
|
||||
- image: circleci/golang:1.14.2
|
||||
working_directory: /home/circleci/workspace/helmfile
|
||||
steps:
|
||||
- checkout
|
||||
@@ -105,22 +210,22 @@ jobs:
|
||||
docker login -u="$DOCKER_USER" -p="$DOCKER_PASS" quay.io
|
||||
make tools
|
||||
go mod vendor
|
||||
ORG=roboll BUILD_URL="$CIRCLE_BUILD_URL" make push release
|
||||
ORG=roboll BUILD_URL="$CIRCLE_BUILD_URL" make push push/helm3 release
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build_and_test:
|
||||
jobs:
|
||||
- build
|
||||
- image
|
||||
- image_helm3
|
||||
- test
|
||||
- integration_tests:
|
||||
requires:
|
||||
- build
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /pull.*/
|
||||
- integration_tests_helm3:
|
||||
requires:
|
||||
- build
|
||||
- release:
|
||||
filters:
|
||||
branches:
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
root = true
|
||||
|
||||
[*.md]
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
@@ -2,4 +2,5 @@ dist/
|
||||
.idea/
|
||||
helmfile
|
||||
helmfile.lock
|
||||
test/integration/tmp
|
||||
vendor/
|
||||
|
||||
+17
-10
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.12.4-alpine3.9 as builder
|
||||
FROM golang:1.14.2-alpine3.11 as builder
|
||||
|
||||
RUN apk add --no-cache make git
|
||||
WORKDIR /workspace/helmfile
|
||||
@@ -7,30 +7,37 @@ RUN make static-linux
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
FROM alpine:3.8
|
||||
FROM alpine:3.11
|
||||
|
||||
RUN apk add --no-cache ca-certificates git bash curl jq
|
||||
|
||||
ARG HELM_VERSION=v2.13.0
|
||||
ARG HELM_VERSION="v2.16.12"
|
||||
ARG HELM_LOCATION="https://kubernetes-helm.storage.googleapis.com"
|
||||
ARG HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
|
||||
ARG HELM_SHA256="15eca6ad225a8279de80c7ced42305e24bc5ac60bb7d96f2d2fa4af86e02c794"
|
||||
RUN wget ${HELM_LOCATION}/${HELM_FILENAME} && \
|
||||
ARG HELM_SHA256="756ab375314329b66b452c0f9d569f74b0760141670217c07b79890ad314c214"
|
||||
RUN set -x && \
|
||||
wget ${HELM_LOCATION}/${HELM_FILENAME} && \
|
||||
echo Verifying ${HELM_FILENAME}... && \
|
||||
sha256sum ${HELM_FILENAME} | grep -q "${HELM_SHA256}" && \
|
||||
tar zxf ${HELM_FILENAME} && mv /linux-amd64/helm /usr/local/bin/ && \
|
||||
echo Extracting ${HELM_FILENAME}... && \
|
||||
tar zxvf ${HELM_FILENAME} && mv /linux-amd64/helm /usr/local/bin/ && \
|
||||
mv /linux-amd64/tiller /usr/local/bin/ && \
|
||||
rm ${HELM_FILENAME} && rm -r /linux-amd64
|
||||
|
||||
# using the install documentation found at https://kubernetes.io/docs/tasks/tools/install-kubectl/
|
||||
# for now but in a future version of alpine (in the testing version at the time of writing)
|
||||
# we should be able to install using apk add.
|
||||
ENV KUBECTL_VERSION="v1.14.5"
|
||||
ENV KUBECTL_SHA256="26681319de56820a8467c9407e9203d5b15fb010ffc75ac5b99c9945ad0bd28c"
|
||||
RUN curl -LO "https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" && \
|
||||
# the sha256 sum can be found at https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl.sha256
|
||||
# maybe a good idea to automate in the future?
|
||||
ENV KUBECTL_VERSION="v1.18.9"
|
||||
ENV KUBECTL_SHA256="6a68756a2d3d04b4d0f52b00de6493ba2c1fcb28b32f3e4a0e99b3d9f6c4e8ed"
|
||||
RUN set -x & \
|
||||
curl --retry 5 --retry-connrefused -LO "https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" && \
|
||||
sha256sum kubectl | grep ${KUBECTL_SHA256} && \
|
||||
chmod +x kubectl && \
|
||||
mv kubectl /usr/local/bin/kubectl
|
||||
|
||||
RUN mkdir -p "$(helm home)/plugins"
|
||||
RUN ["helm", "init", "--client-only"]
|
||||
RUN helm plugin install https://github.com/databus23/helm-diff && \
|
||||
helm plugin install https://github.com/futuresimple/helm-secrets && \
|
||||
helm plugin install https://github.com/hypnoglow/helm-s3.git && \
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
FROM golang:1.14.2-alpine3.11 as builder
|
||||
|
||||
RUN apk add --no-cache make git
|
||||
WORKDIR /workspace/helmfile
|
||||
COPY . /workspace/helmfile
|
||||
RUN make static-linux
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
FROM alpine:3.11
|
||||
|
||||
RUN apk add --no-cache ca-certificates git bash curl jq
|
||||
|
||||
ARG HELM_VERSION="v3.3.4"
|
||||
ARG HELM_LOCATION="https://get.helm.sh"
|
||||
ARG HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
|
||||
ARG HELM_SHA256="b664632683c36446deeb85c406871590d879491e3de18978b426769e43a1e82c"
|
||||
RUN set -x && \
|
||||
wget ${HELM_LOCATION}/${HELM_FILENAME} && \
|
||||
echo Verifying ${HELM_FILENAME}... && \
|
||||
sha256sum ${HELM_FILENAME} | grep -q "${HELM_SHA256}" && \
|
||||
echo Extracting ${HELM_FILENAME}... && \
|
||||
tar zxvf ${HELM_FILENAME} && mv /linux-amd64/helm /usr/local/bin/ && \
|
||||
rm ${HELM_FILENAME} && rm -r /linux-amd64
|
||||
|
||||
# using the install documentation found at https://kubernetes.io/docs/tasks/tools/install-kubectl/
|
||||
# for now but in a future version of alpine (in the testing version at the time of writing)
|
||||
# we should be able to install using apk add.
|
||||
# the sha256 sum can be found at https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl.sha256
|
||||
# maybe a good idea to automate in the future?
|
||||
ENV KUBECTL_VERSION="v1.18.9"
|
||||
ENV KUBECTL_SHA256="6a68756a2d3d04b4d0f52b00de6493ba2c1fcb28b32f3e4a0e99b3d9f6c4e8ed"
|
||||
RUN set -x && \
|
||||
curl --retry 5 --retry-connrefused -LO "https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl" && \
|
||||
sha256sum kubectl | grep ${KUBECTL_SHA256} && \
|
||||
chmod +x kubectl && \
|
||||
mv kubectl /usr/local/bin/kubectl
|
||||
|
||||
RUN helm plugin install https://github.com/databus23/helm-diff --version v3.1.3 && \
|
||||
helm plugin install https://github.com/futuresimple/helm-secrets && \
|
||||
helm plugin install https://github.com/hypnoglow/helm-s3.git && \
|
||||
helm plugin install https://github.com/aslafy-z/helm-git.git
|
||||
|
||||
COPY --from=builder /workspace/helmfile/dist/helmfile_linux_amd64 /usr/local/bin/helmfile
|
||||
|
||||
CMD ["/usr/local/bin/helmfile"]
|
||||
@@ -2,7 +2,7 @@ ORG ?= $(shell basename $(realpath ..))
|
||||
PKGS := $(shell go list ./... | grep -v /vendor/)
|
||||
|
||||
build:
|
||||
go build ${TARGETS}
|
||||
go build -ldflags '-X github.com/roboll/helmfile/pkg/app/version.Version=${TAG}' ${TARGETS}
|
||||
.PHONY: build
|
||||
|
||||
generate:
|
||||
@@ -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
|
||||
@@ -26,15 +30,15 @@ integration:
|
||||
.PHONY: integration
|
||||
|
||||
cross:
|
||||
env CGO_ENABLED=0 gox -os '!openbsd !freebsd !netbsd' -arch '!arm !mips !mipsle !mips64 !mips64le !s390x' -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" -ldflags '-X main.Version=${TAG}' ${TARGETS}
|
||||
env CGO_ENABLED=0 gox -os '!openbsd !freebsd !netbsd' -arch '!arm !mips !mipsle !mips64 !mips64le !s390x' -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" -ldflags '-X github.com/roboll/helmfile/pkg/app/version.Version=${TAG}' ${TARGETS}
|
||||
.PHONY: cross
|
||||
|
||||
static-linux:
|
||||
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOFLAGS=-mod=vendor go build -o "dist/helmfile_linux_amd64" -ldflags '-X main.Version=${TAG}' ${TARGETS}
|
||||
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOFLAGS=-mod=vendor go build -o "dist/helmfile_linux_amd64" -ldflags '-X github.com/roboll/helmfile/pkg/app/version.Version=${TAG}' ${TARGETS}
|
||||
.PHONY: static-linux
|
||||
|
||||
install:
|
||||
env CGO_ENABLED=0 go install -ldflags '-X main.Version=${TAG}' ${TARGETS}
|
||||
env CGO_ENABLED=0 go install -ldflags '-X github.com/roboll/helmfile/pkg/app/version.Version=${TAG}' ${TARGETS}
|
||||
.PHONY: install
|
||||
|
||||
clean:
|
||||
@@ -59,6 +63,13 @@ run: image
|
||||
push: image
|
||||
docker push quay.io/${ORG}/helmfile:${TAG}
|
||||
|
||||
|
||||
image/helm3:
|
||||
docker build -f Dockerfile.helm3 -t quay.io/${ORG}/helmfile:helm3-${TAG} .
|
||||
|
||||
push/helm3: image/helm3
|
||||
docker push quay.io/${ORG}/helmfile:helm3-${TAG}
|
||||
|
||||
tools:
|
||||
go get -u github.com/tcnksm/ghr github.com/mitchellh/gox
|
||||
.PHONY: tools
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
# Paths Overivew
|
||||
Using manifest files in conjunction with command line argument can be a bit confusing.
|
||||
# Paths Overivew
|
||||
|
||||
A few rules to clear up this ambiguity:
|
||||
Using manifest files in conjunction with command line argument can be a bit confusing.
|
||||
|
||||
A few rules to clear up this ambiguity:
|
||||
|
||||
- Absolute paths are always resolved as absolute paths
|
||||
- Relative paths referenced *in* the helmfile manifest itself are relative to that manifest
|
||||
- Relative paths referenced on the command line are relative to the current working directory the user is in
|
||||
|
||||
### Examples
|
||||
There are several examples that we can go through in the `/examples` folder which demonstrate this.
|
||||
|
||||
There are several examples that we can go through in the [`/examples`](examples) folder which demonstrate this.
|
||||
|
||||
**Local Execution**
|
||||
|
||||
This is an example of a Helmfile manifest referencing a local value directly.
|
||||
This is an example of a Helmfile manifest referencing a local value directly.
|
||||
|
||||
Indirect:
|
||||
```
|
||||
helmfile -f examples/deployments/local/charts.yaml sync
|
||||
```
|
||||
|
||||
Direct:
|
||||
Direct:
|
||||
```
|
||||
cd examples/deployments/local/
|
||||
helmfile sync
|
||||
@@ -27,14 +29,14 @@ helmfile sync
|
||||
|
||||
**Relative Paths in Helmfile**
|
||||
|
||||
This is an example of a Helmfile manifest using relative paths for values.
|
||||
This is an example of a Helmfile manifest using relative paths for values.
|
||||
|
||||
Indirect:
|
||||
```
|
||||
helmfile -f examples/deployments/dev/charts.yaml sync
|
||||
```
|
||||
|
||||
Direct:
|
||||
Direct:
|
||||
```
|
||||
cd examples/deployments/dev/
|
||||
helmfile sync
|
||||
@@ -42,7 +44,7 @@ helmfile sync
|
||||
|
||||
**Relative Paths in Helmfile w/ --values overrides**
|
||||
|
||||
This is an example of a Helmfile manifest using relative paths for values including an additional `--values` from the command line.
|
||||
This is an example of a Helmfile manifest using relative paths for values including an additional `--values` from the command line.
|
||||
|
||||
NOTE: The `--values` is resolved relative to the CWD of the terminal *not* the Helmfile manifest. You can see this with the `replicas` being adjusted to 3 now for the deployment.
|
||||
|
||||
@@ -51,7 +53,7 @@ Indirect:
|
||||
helmfile -f examples/deployments/dev/charts.yaml sync --values values/replica-values.yaml
|
||||
```
|
||||
|
||||
Direct:
|
||||
Direct:
|
||||
```
|
||||
cd examples/deployments/dev/
|
||||
helmfile sync --values ../../values/replica-values.yaml
|
||||
|
||||
@@ -35,9 +35,9 @@ To avoid upgrades for each iteration of `helm`, the `helmfile` executable delega
|
||||
|
||||
## Configuration
|
||||
|
||||
**CAUTION**: This documentation is for the development version of Helmfile. If you are looking for the documentation for any of releases, please switch to the corresponding release tag like [v0.79.1](https://github.com/roboll/helmfile/tree/v0.79.1).
|
||||
**CAUTION**: This documentation is for the development version of Helmfile. If you are looking for the documentation for any of releases, please switch to the corresponding release tag like [v0.92.1](https://github.com/roboll/helmfile/tree/v0.92.1).
|
||||
|
||||
The default helmfile is `helmfile.yaml`:
|
||||
The default name for a helmfile is `helmfile.yaml`:
|
||||
|
||||
```yaml
|
||||
# Chart repositories used from within this state file
|
||||
@@ -69,21 +69,28 @@ repositories:
|
||||
|
||||
# context: kube-context # this directive is deprecated, please consider using helmDefaults.kubeContext
|
||||
|
||||
#default values to set for args along with dedicated keys that can be set by contributers, cli args take precedence over these
|
||||
# Default values to set for args along with dedicated keys that can be set by contributors, cli args take precedence over these.
|
||||
# In other words, unset values results in no flags passed to helm.
|
||||
# See the helm usage (helm SUBCOMMAND -h) for more info on default values when those flags aren't provided.
|
||||
helmDefaults:
|
||||
tillerNamespace: tiller-namespace #dedicated default key for tiller-namespace
|
||||
tillerless: false #dedicated default key for tillerless
|
||||
kubeContext: kube-context #dedicated default key for kube-context (--kube-context)
|
||||
# additional and global args passed to helm
|
||||
cleanupOnFail: false #dedicated default key for helm flag --cleanup-on-fail
|
||||
# additional and global args passed to helm (default "")
|
||||
args:
|
||||
- "--set k=v"
|
||||
# defaults for verify, wait, force, timeout and recreatePods under releases[]
|
||||
# verify the chart before upgrading (only works with packaged charts not directories) (default false)
|
||||
verify: true
|
||||
# wait for k8s resources via --wait. (default false)
|
||||
wait: true
|
||||
# time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks, and waits on pod/pvc/svc/deployment readiness) (default 300)
|
||||
timeout: 600
|
||||
# performs pods restart for the resource if applicable (default false)
|
||||
recreatePods: true
|
||||
force: true
|
||||
# enable TLS for request to Tiller
|
||||
# forces resource update through delete/recreate if needed (default false)
|
||||
force: false
|
||||
# enable TLS for request to Tiller (default false)
|
||||
tls: true
|
||||
# path to TLS CA certificate file (default "$HELM_HOME/ca.pem")
|
||||
tlsCACert: "path/to/ca.pem"
|
||||
@@ -91,6 +98,19 @@ helmDefaults:
|
||||
tlsCert: "path/to/cert.pem"
|
||||
# path to TLS key file (default "$HELM_HOME/key.pem")
|
||||
tlsKey: "path/to/key.pem"
|
||||
# limit the maximum number of revisions saved per release. Use 0 for no limit. (default 10)
|
||||
historyMax: 10
|
||||
# when using helm 3.2+, automatically create release namespaces if they do not exist (default true)
|
||||
createNamespace: true
|
||||
# if used with charts museum allows to pull unstable charts for deployment, for example: if 1.2.3 and 1.2.4-dev versions exist and set to true, 1.2.4-dev will be pulled (default false)
|
||||
devel: true
|
||||
# When set to `true`, skips running `helm dep up` and `helm dep build` on this release's chart.
|
||||
# Useful when the chart is broken, like seen in https://github.com/roboll/helmfile/issues/1547
|
||||
skipDeps: false
|
||||
|
||||
# these labels will be applied to all releases in a Helmfile. Useful in templating if you have a helmfile per environment or customer and don't want to copy the same label to each release
|
||||
commonLabels:
|
||||
hello: world
|
||||
|
||||
# The desired states of Helm releases.
|
||||
#
|
||||
@@ -99,10 +119,12 @@ releases:
|
||||
# Published chart example
|
||||
- name: vault # name of this release
|
||||
namespace: vault # target namespace
|
||||
labels: # Arbitrary key value pairs for filtering releases
|
||||
createNamespace: true # helm 3.2+ automatically create release namespace (default true)
|
||||
labels: # Arbitrary key value pairs for filtering releases
|
||||
foo: bar
|
||||
chart: roboll/vault-secret-manager # the chart being installed to create this release, referenced by `repository/chart` syntax
|
||||
version: ~1.24.1 # the semver of the chart. range constraint is supported
|
||||
condition: vault.enabled # The values lookup key for filtering releases. Corresponds to the boolean value of `vault.enabled`, where `vault` is an arbitrary value
|
||||
missingFileHandler: Warn # set to either "Error" or "Warn". "Error" instructs helmfile to fail when unable to find a values or secrets file. When "Warn", it prints the file and continues.
|
||||
# Values files used for rendering the chart
|
||||
values:
|
||||
@@ -143,25 +165,23 @@ releases:
|
||||
# will attempt to decrypt it using helm-secrets plugin
|
||||
secrets:
|
||||
- vault_secret.yaml
|
||||
# verify the chart before upgrading (only works with packaged charts not directories)
|
||||
# Override helmDefaults options for verify, wait, timeout, recreatePods and force.
|
||||
verify: true
|
||||
# wait for k8s resources via --wait. Defaults to `false`
|
||||
wait: true
|
||||
# time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks, and waits on pod/pvc/svc/deployment readiness) (default 300)
|
||||
timeout: 60
|
||||
# performs pods restart for the resource if applicable
|
||||
recreatePods: true
|
||||
# forces resource update through delete/recreate if needed
|
||||
force: true
|
||||
# set `false` to uninstall on sync
|
||||
force: false
|
||||
# set `false` to uninstall this release on sync. (default true)
|
||||
installed: true
|
||||
# restores previous state in case of failed release
|
||||
# restores previous state in case of failed release (default false)
|
||||
atomic: true
|
||||
# name of the tiller namespace
|
||||
# when true, cleans up any new resources created during a failed release (default false)
|
||||
cleanupOnFail: false
|
||||
# name of the tiller namespace (default "")
|
||||
tillerNamespace: vault
|
||||
# if true, will use the helm-tiller plugin
|
||||
# if true, will use the helm-tiller plugin (default false)
|
||||
tillerless: false
|
||||
# enable TLS for request to Tiller
|
||||
# enable TLS for request to Tiller (default false)
|
||||
tls: true
|
||||
# path to TLS CA certificate file (default "$HELM_HOME/ca.pem")
|
||||
tlsCACert: "path/to/ca.pem"
|
||||
@@ -172,7 +192,13 @@ releases:
|
||||
# --kube-context to be passed to helm commands
|
||||
# CAUTION: this doesn't work as expected for `tilerless: true`.
|
||||
# See https://github.com/roboll/helmfile/issues/642
|
||||
# (default "", which means the standard kubeconfig, either ~/kubeconfig or the file pointed by $KUBECONFIG environment variable)
|
||||
kubeContext: kube-context
|
||||
# limit the maximum number of revisions saved per release. Use 0 for no limit (default 10)
|
||||
historyMax: 10
|
||||
# When set to `true`, skips running `helm dep up` and `helm dep build` on this release's chart.
|
||||
# Useful when the chart is broken, like seen in https://github.com/roboll/helmfile/issues/1547
|
||||
skipDeps: false
|
||||
|
||||
# Local chart example
|
||||
- name: grafana # name of this release
|
||||
@@ -209,6 +235,9 @@ helmfiles:
|
||||
# The nested-state file is locally checked-out along with the remote directory containing it.
|
||||
# Therefore all the local paths in the file are resolved relative to the file
|
||||
path: git::https://github.com/cloudposse/helmfiles.git@releases/kiam.yaml?ref=0.40.0
|
||||
# If set to "Error", return an error when a subhelmfile points to a
|
||||
# non-existent path. The default behavior is to print a warning and continue.
|
||||
missingFileHandler: Error
|
||||
|
||||
#
|
||||
# Advanced Configuration: Environments
|
||||
@@ -237,7 +266,10 @@ environments:
|
||||
values:
|
||||
- environment/production/values.yaml
|
||||
- myChartVer: 1.0.0
|
||||
## `secrets.yaml` is decrypted by `helm-secrets` and available via `{{ .Environment.Secrets.KEY }}`
|
||||
# disable vault release processing
|
||||
- vault:
|
||||
enabled: false
|
||||
## `secrets.yaml` is decrypted by `helm-secrets` and available via `{{ .Environment.Values.KEY }}`
|
||||
secrets:
|
||||
- environment/production/secrets.yaml
|
||||
# Instructs helmfile to fail when unable to find a environment values file listed under `environments.NAME.values`.
|
||||
@@ -259,12 +291,38 @@ bases:
|
||||
- environments.yaml
|
||||
- defaults.yaml
|
||||
- templates.yaml
|
||||
|
||||
#
|
||||
# Advanced Configuration: API Capabilities
|
||||
#
|
||||
# 'helmfile template' renders releases locally without querying an actual cluster,
|
||||
# and in this case `.Capabilities.APIVersions` cannot be populated.
|
||||
# When a chart queries for a specific CRD, this can lead to unexpected results.
|
||||
#
|
||||
# Configure a fixed list of api versions to pass to 'helm template' via the --api-versions flag:
|
||||
apiVersions:
|
||||
- example/v1
|
||||
|
||||
```
|
||||
|
||||
## Templating
|
||||
|
||||
Helmfile uses [Go templates](https://godoc.org/text/template) for templating your helmfile.yaml. While go ships several built-in functions, we have added all of the functions in the [Sprig library](https://godoc.org/github.com/Masterminds/sprig).
|
||||
|
||||
We also added the following functions:
|
||||
|
||||
- `requiredEnv`
|
||||
- `exec`
|
||||
- `readFile`
|
||||
- `toYaml`
|
||||
- `fromYaml`
|
||||
- `setValueAtPath`
|
||||
- `get` (Sprig's original `get` is available as `sprigGet`)
|
||||
- `tpl`
|
||||
- `required`
|
||||
- `fetchSecretValue`
|
||||
- `expandSecretRefs`
|
||||
|
||||
We also added one special template function: `requiredEnv`.
|
||||
The `requiredEnv` function allows you to declare a particular environment variable as required for template rendering.
|
||||
If the environment variable is unset or empty, the template rendering will fail with an error message.
|
||||
@@ -297,15 +355,19 @@ releases:
|
||||
value: {{ env "SCHEME" | default "https" }}
|
||||
```
|
||||
|
||||
## installation
|
||||
### Note
|
||||
|
||||
If you wish to treat your enviroment variables as strings always, even if they are boolean or numeric values you can use `{{ env "ENV_NAME" | quote }}` or `"{{ env "ENV_NAME" }}"`. These approaches also work with `requiredEnv`.
|
||||
|
||||
## Installation
|
||||
|
||||
- download one of [releases](https://github.com/roboll/helmfile/releases) or
|
||||
- run as a [container](https://quay.io/roboll/helmfile) or
|
||||
- install from [AUR](https://aur.archlinux.org/packages/kubernetes-helmfile-bin/) for Archlinux or
|
||||
- Archlinux: install via `pacman -S helmfile` or from [AUR](https://aur.archlinux.org/packages/kubernetes-helmfile-bin/)
|
||||
- Windows (using [scoop](https://scoop.sh/)): `scoop install helmfile`
|
||||
- macOS (using [homebrew](https://brew.sh/)): `brew install helmfile`
|
||||
|
||||
## getting started
|
||||
## Getting Started
|
||||
|
||||
Let's start with a simple `helmfile` and gradually improve it to fit your use-case!
|
||||
|
||||
@@ -331,21 +393,21 @@ Congratulations! You now have your first Prometheus deployment running inside yo
|
||||
|
||||
Iterate on the `helmfile.yaml` by referencing:
|
||||
|
||||
- [Configuration syntax](#configuration-syntax)
|
||||
- [Configuration](#configuration)
|
||||
- [CLI reference](#cli-reference).
|
||||
- [Helmfile Best Practices Guide](https://github.com/roboll/helmfile/blob/master/docs/writing-helmfile.md)
|
||||
|
||||
## cli reference
|
||||
## CLI Reference
|
||||
|
||||
```
|
||||
NAME:
|
||||
helmfile -
|
||||
helmfile
|
||||
|
||||
USAGE:
|
||||
helmfile [global options] command [command options] [arguments...]
|
||||
|
||||
VERSION:
|
||||
v0.70.0
|
||||
v0.92.1
|
||||
|
||||
COMMANDS:
|
||||
deps update charts based on the contents of requirements.yaml
|
||||
@@ -360,15 +422,19 @@ COMMANDS:
|
||||
delete DEPRECATED: delete releases from state file (helm delete)
|
||||
destroy deletes and then purges releases
|
||||
test test releases from state file (helm test)
|
||||
build output compiled helmfile state(s) as YAML
|
||||
list list releases defined in state file
|
||||
help, h Shows a list of commands or help for one command
|
||||
|
||||
GLOBAL OPTIONS:
|
||||
--helm-binary value, -b value path to helm binary
|
||||
--helm-binary value, -b value path to helm binary (default: "helm")
|
||||
--file helmfile.yaml, -f helmfile.yaml load config from file or directory. defaults to helmfile.yaml or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference
|
||||
--environment default, -e default specify the environment name. defaults to default
|
||||
--state-values-set value set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)
|
||||
--state-values-file value specify state values in a YAML file
|
||||
--quiet, -q Silence output. Equivalent to log-level warn
|
||||
--kube-context value Set kubectl context. Uses current context by default
|
||||
--no-color Output without color
|
||||
--log-level value Set log level, default info
|
||||
--namespace value, -n value Set namespace. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}
|
||||
--selector value, -l value Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar.
|
||||
@@ -379,6 +445,9 @@ GLOBAL OPTIONS:
|
||||
--interactive, -i Request confirmation before attempting to modify clusters
|
||||
--help, -h show help
|
||||
--version, -v print the version
|
||||
|
||||
Environment variables:
|
||||
HELMFILE_ENVIRONMENT specify the environment name, the command line option '-e' have precedence
|
||||
```
|
||||
|
||||
### sync
|
||||
@@ -435,7 +504,6 @@ The `helmfile delete` sub-command deletes all the releases defined in the manife
|
||||
|
||||
Note that `delete` doesn't purge releases. So `helmfile delete && helmfile sync` results in sync failed due to that releases names are not deleted but preserved for future references. If you really want to remove releases for reuse, add `--purge` flag to run it like `helmfile delete --purge`.
|
||||
|
||||
|
||||
### secrets
|
||||
|
||||
The `secrets` parameter in a `helmfile.yaml` causes the [helm-secrets](https://github.com/futuresimple/helm-secrets) plugin to be executed to decrypt the file.
|
||||
@@ -455,6 +523,7 @@ Use `--cleanup` to delete pods upon completion.
|
||||
The `helmfile lint` sub-command runs a `helm lint` across all of the charts/releases defined in the manifest. Non local charts will be fetched into a temporary folder which will be deleted once the task is completed.
|
||||
|
||||
## Paths Overview
|
||||
|
||||
Using manifest files in conjunction with command line argument can be a bit confusing.
|
||||
|
||||
A few rules to clear up this ambiguity:
|
||||
@@ -463,9 +532,10 @@ A few rules to clear up this ambiguity:
|
||||
- Relative paths referenced *in* the Helmfile manifest itself are relative to that manifest
|
||||
- Relative paths referenced on the command line are relative to the current working directory the user is in
|
||||
|
||||
For additional context, take a look at [paths examples](PATHS.md)
|
||||
For additional context, take a look at [paths examples](PATHS.md).
|
||||
|
||||
## Labels Overview
|
||||
|
||||
A selector can be used to only target a subset of releases when running Helmfile. This is useful for large helmfiles with releases that are logically grouped together.
|
||||
|
||||
Labels are simple key value pairs that are an optional field of the release spec. When selecting by label, the search can be inverted. `tier!=backend` would match all releases that do NOT have the `tier: backend` label. `tier=fronted` would only match releases with the `tier: frontend` label.
|
||||
@@ -474,10 +544,45 @@ Multiple labels can be specified using `,` as a separator. A release must match
|
||||
|
||||
The `selector` parameter can be specified multiple times. Each parameter is resolved independently so a release that matches any parameter will be used.
|
||||
|
||||
`--selector tier=frontend --selector tier=backend` will select all the charts
|
||||
`--selector tier=frontend --selector tier=backend` will select all the charts.
|
||||
|
||||
In addition to user supplied labels, the name, the namespace, and the chart are available to be used as selectors. The chart will just be the chart name excluding the repository (Example `stable/filebeat` would be selected using `--selector chart=filebeat`).
|
||||
|
||||
`commonLabels` can be used when you want to apply the same label to all releases and use [templating](##Templates) based on that.
|
||||
For instance, you install a number of charts on every customer but need to provide different values file per customer.
|
||||
|
||||
templates/common.yaml:
|
||||
|
||||
```yaml
|
||||
templates:
|
||||
nginx: &nginx
|
||||
name: nginx
|
||||
chart: stable/nginx-ingress
|
||||
values:
|
||||
- ../values/common/{{ .Release.Name }}.yaml
|
||||
- ../values/{{ .Release.Labels.customer }}/{{ .Release.Name }}.yaml
|
||||
|
||||
cert-manager: &cert-manager
|
||||
name: cert-manager
|
||||
chart: jetstack/cert-manager
|
||||
values:
|
||||
- ../values/common/{{ .Release.Name }}.yaml
|
||||
- ../values/{{ .Release.Labels.customer }}/{{ .Release.Name }}.yaml
|
||||
```
|
||||
|
||||
helmfile.yaml:
|
||||
|
||||
```yaml
|
||||
{{ readFile "templates/common.yaml" }}
|
||||
|
||||
commonLabels:
|
||||
customer: company
|
||||
|
||||
releases:
|
||||
- <<: *nginx
|
||||
- <<: *cert-manager
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||
You can use go's text/template expressions in `helmfile.yaml` and `values.yaml.gotmpl` (templated helm values files). `values.yaml` references will be used verbatim. In other words:
|
||||
@@ -491,13 +596,14 @@ In addition to built-in ones, the following custom template functions are availa
|
||||
- `fromYaml` reads a golang string and generates a map
|
||||
- `setValueAtPath PATH NEW_VALUE` traverses a golang map, replaces the value at the PATH with NEW_VALUE
|
||||
- `toYaml` marshals a map into a string
|
||||
- `get` returns the value of the specified key if present in the `.Values` object, otherwise will return the default value defined in the function
|
||||
|
||||
### Values Files Templates
|
||||
|
||||
You can reference a template of values file in your `helmfile.yaml` like below:
|
||||
|
||||
```yaml
|
||||
releases
|
||||
releases:
|
||||
- name: myapp
|
||||
chart: mychart
|
||||
values:
|
||||
@@ -519,7 +625,7 @@ foo:
|
||||
bar: ""
|
||||
```
|
||||
|
||||
The resulting, temporary values.yaml that is generated from `values.yaml.tpl` would become:
|
||||
The resulting, temporary values.yaml that is generated from `values.yaml.gotmpl` would become:
|
||||
|
||||
```yaml
|
||||
foo:
|
||||
@@ -592,11 +698,9 @@ environments:
|
||||
production:
|
||||
|
||||
releases:
|
||||
|
||||
{{ if eq .Environment.Name "production" }}
|
||||
- name: newrelic-agent
|
||||
installed: {{ eq .Environment.Name "production" | toYaml }}
|
||||
# snip
|
||||
{{ end }}
|
||||
- name: myapp
|
||||
# snip
|
||||
```
|
||||
@@ -632,7 +736,7 @@ releaseName: prod
|
||||
`values.yaml.gotmpl`
|
||||
|
||||
```yaml
|
||||
domain: {{ .Values | getOrNil "my.domain" | default "dev.example.com" }}
|
||||
domain: {{ .Values | get "domain" "dev.example.com" }}
|
||||
```
|
||||
|
||||
`helmfile sync` installs `myapp` with the value `domain=dev.example.com`,
|
||||
@@ -662,15 +766,13 @@ releases:
|
||||
- name: myapp-{{ .Values.releaseName }} # release name will be one of `dev` or `prod` depending on selected environment
|
||||
values:
|
||||
- values.yaml.gotmpl
|
||||
|
||||
{{ if eq (.Values.releaseName "prod" ) }}
|
||||
# this release would be installed only if selected environment is `production`
|
||||
- name: production-specific-release
|
||||
# this release would be installed only if selected environment is `production`
|
||||
installed: {{ eq .Values.releaseName "prod" | toYaml }}
|
||||
...
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
### Note
|
||||
### Note on Environment.Values vs Values
|
||||
|
||||
The `{{ .Values.foo }}` syntax is the recommended way of using environment values.
|
||||
|
||||
@@ -679,6 +781,27 @@ This is still working but is **deprecated** and the new `{{ .Values.foo }}` synt
|
||||
|
||||
You can read more infos about the feature proposal [here](https://github.com/roboll/helmfile/issues/640).
|
||||
|
||||
### Loading remote environment values files
|
||||
|
||||
Since #1296 and Helmfile v0.118.8, you can use `go-getter`-style URLs to refer to remote values files:
|
||||
|
||||
```yaml
|
||||
environments:
|
||||
cluster-azure-us-west:
|
||||
values:
|
||||
- git::https://git.company.org/helmfiles/global/azure.yaml?ref=master
|
||||
- git::https://git.company.org/helmfiles/global/us-west.yaml?ref=master
|
||||
cluster-gcp-europe-west:
|
||||
values:
|
||||
- git::https://git.company.org/helmfiles/global/gcp.yaml?ref=master
|
||||
- git::https://git.company.org/helmfiles/global/europe-west.yaml?ref=master
|
||||
|
||||
releases:
|
||||
- ...
|
||||
```
|
||||
|
||||
This is particularly useful when you co-locate helmfiles within your project repo but want to reuse the definitions in a global repo.
|
||||
|
||||
## Environment Secrets
|
||||
|
||||
Environment Secrets (not to be confused with Kubernetes Secrets) are encrypted versions of `Environment Values`.
|
||||
@@ -723,6 +846,52 @@ With the [helm-tiller](https://github.com/rimusz/helm-tiller) plugin installed,
|
||||
To enable this mode, you need to define `tillerless: true` and set the `tillerNamespace` in the `helmDefaults` section
|
||||
or in the `releases` entries.
|
||||
|
||||
## DAG-aware installation/deletion ordering
|
||||
|
||||
`needs` controls the order of the installation/deletion of the release:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: somerelease
|
||||
needs:
|
||||
- [TILLER_NAMESPACE/][NAMESPACE/]anotherelease
|
||||
```
|
||||
|
||||
Be aware that you have to specify the namespace name if you configured one for the release(s).
|
||||
|
||||
All the releases listed under `needs` are installed before(or deleted after) the release itself.
|
||||
|
||||
For the following example, `helmfile [sync|apply]` installs releases in this order:
|
||||
|
||||
1. logging
|
||||
2. servicemesh
|
||||
3. myapp1 and myapp2
|
||||
|
||||
```yaml
|
||||
- name: myapp1
|
||||
chart: charts/myapp
|
||||
needs:
|
||||
- servicemesh
|
||||
- logging
|
||||
- name: myapp2
|
||||
chart: charts/myapp
|
||||
needs:
|
||||
- servicemesh
|
||||
- logging
|
||||
- name: servicemesh
|
||||
chart: charts/istio
|
||||
needs:
|
||||
- logging
|
||||
- name: logging
|
||||
chart: charts/fluentd
|
||||
```
|
||||
|
||||
Note that all the releases in a same group is installed concurrently. That is, myapp1 and myapp2 are installed concurrently.
|
||||
|
||||
On `helmfile [delete|destroy]`, deletions happen in the reverse order.
|
||||
|
||||
That is, `myapp1` and `myapp2` are deleted first, then `servicemesh`, and finally `logging`.
|
||||
|
||||
## Separating helmfile.yaml into multiple independent files
|
||||
|
||||
Once your `helmfile.yaml` got to contain too many releases,
|
||||
@@ -824,14 +993,14 @@ that is accessible by running a command:
|
||||
|
||||
A usual usage of `exec` would look like this:
|
||||
|
||||
```
|
||||
```yaml
|
||||
mysetting: |
|
||||
{{ exec "./mycmd" (list "arg1" "arg2" "--flag1") | indent 2 }}
|
||||
```
|
||||
|
||||
Or even with a pipeline:
|
||||
|
||||
```
|
||||
```yaml
|
||||
mysetting: |
|
||||
{{ yourinput | exec "./mycmd-consume-stdin" (list "arg1" "arg2") | indent 2 }}
|
||||
```
|
||||
@@ -854,20 +1023,30 @@ Currently supported `events` are:
|
||||
|
||||
- `prepare`
|
||||
- `presync`
|
||||
- `preuninstall`
|
||||
- `postuninstall`
|
||||
- `postsync`
|
||||
- `cleanup`
|
||||
|
||||
Hooks associated to `prepare` events are triggered after each release in your helmfile is loaded from YAML, before execution.
|
||||
`prepare` hooks are triggered on the release as long as it is not excluded by the helmfile selector(e.g. `helmfile -l key=value`).
|
||||
|
||||
Hooks associated to `cleanup` events are triggered after each release is processed.
|
||||
Hooks associated to `presync` events are triggered before each release is applied to the remote cluster.
|
||||
This is the ideal event to execute any commands that may mutate the cluster state as it will not be run for read-only operations like `lint`, `diff` or `template`.
|
||||
|
||||
Hooks associated to `presync` events are triggered before each release is applied to the remote cluster. This is the ideal event to execute any commands that may mutate the cluster state as it will not be run for read-only operations like `lint`, `diff` or `template`.
|
||||
`preuninstall` hooks are triggered immediately before a release is uninstalled as part of `helmfile apply`, `helmfile sync`, `helmfile delete`, and `helmfile destroy`.
|
||||
|
||||
Hooks associated to `postsync` events are triggered after each release is applied to the remote cluster. This is the ideal event to execute any commands that may mutate the cluster state as it will not be run for read-only operations like `lint`, `diff` or `template`.
|
||||
`postuninstall` hooks are triggered immediately after successful uninstall of a release while running `helmfile apply`, `helmfile sync`, `helmfile delete`, `helmfile destroy`.
|
||||
|
||||
`postsync` hooks are triggered after each release is synced(installed, updated, or uninstalled) to/from the cluster, regardless of the sync was successful or not.
|
||||
This is the ideal place to execute any commands that may mutate the cluster state as it will not be run for read-only operations like `lint`, `diff` or `template`.
|
||||
|
||||
`cleanup` hooks are triggered after each release is processed.
|
||||
This is the counterpart to `prepare`, as any release on which `prepare` has been triggered gets `cleanup` triggered as well.
|
||||
|
||||
The following is an example hook that just prints the contextual information provided to hook:
|
||||
|
||||
```
|
||||
```yaml
|
||||
releases:
|
||||
- name: myapp
|
||||
chart: mychart
|
||||
@@ -901,6 +1080,20 @@ Now, replace `echo` with any command you like, and rewrite `args` that actually
|
||||
For templating, imagine that you created a hook that generates a helm chart on-the-fly by running an external tool like ksonnet, kustomize, or your own template engine.
|
||||
It will allow you to write your helm releases with any language you like, while still leveraging goodies provided by helm.
|
||||
|
||||
### Global Hooks
|
||||
|
||||
In contrast to the per release hooks mentioned above these are run only once at the very beginning and end of the execution of a helmfile command and only the `prepare` and `cleanup` hooks are available respectively.
|
||||
|
||||
They use the same syntax as per release hooks, but at the top level of your helmfile:
|
||||
```yaml
|
||||
hooks:
|
||||
- events: ["prepare", "cleanup"]
|
||||
showlogs: true
|
||||
command: "echo"
|
||||
args: ["{{`{{.Environment.Name}}`}}", "{{`{{.HelmfileCommand}}`}}\
|
||||
"]
|
||||
```
|
||||
|
||||
### Helmfile + Kustomize
|
||||
|
||||
Do you prefer `kustomize` to write and organize your Kubernetes apps, but still want to leverage helm's useful features
|
||||
@@ -909,7 +1102,7 @@ like rollback, history, and so on? This section is for you!
|
||||
The combination of `hooks` and [helmify-kustomize](https://gist.github.com/mumoshu/f9d0bd98e0eb77f636f79fc2fb130690)
|
||||
enables you to integrate [kustomize](https://github.com/kubernetes-sigs/kustomize) into Helmfile.
|
||||
|
||||
That is, you can use `kustommize` to build a local helm chart from a kustomize overlay.
|
||||
That is, you can use `kustomize` to build a local helm chart from a kustomize overlay.
|
||||
|
||||
Let's assume you have a kustomize project named `foo-kustomize` like this:
|
||||
|
||||
@@ -963,9 +1156,9 @@ We also have dedicated documentation on the following topics which might interes
|
||||
|
||||
Or join our friendly slack community in the [`#helmfile`](https://slack.sweetops.com) channel to ask questions and get help. Check out our [slack archive](https://archive.sweetops.com/helmfile/) for good examples of how others are using it.
|
||||
|
||||
## Using env files
|
||||
## Using .env files
|
||||
|
||||
Helmfile itself doesn't have an ability to load env files. But you can write some bash script to achieve the goal:
|
||||
Helmfile itself doesn't have an ability to load .env files. But you can write some bash script to achieve the goal:
|
||||
|
||||
```console
|
||||
set -a; . .env; set +a; helmfile sync
|
||||
@@ -973,7 +1166,7 @@ set -a; . .env; set +a; helmfile sync
|
||||
|
||||
Please see #203 for more context.
|
||||
|
||||
## Running helmfile interactively
|
||||
## Running Helmfile interactively
|
||||
|
||||
`helmfile --interactive [apply|destroy]` requests confirmation from you before actually modifying your cluster.
|
||||
|
||||
@@ -987,7 +1180,8 @@ Once you download all required charts into your machine, you can run `helmfile c
|
||||
It basically run only `helm upgrade --install` with your already-downloaded charts, hence no Internet connection is required.
|
||||
See #155 for more information on this topic.
|
||||
|
||||
## Experimental features
|
||||
## Experimental Features
|
||||
|
||||
Some experimental features may be available for testing in perspective of being (or not) included in a future release.
|
||||
Those features are set using the environment variable `HELMFILE_EXPERIMENTAL`. Here is the current experimental feature :
|
||||
* `explicit-selector-inheritance` : remove today implicit cli selectors inheritance for composed helmfiles, see [composition selector](#selectors)
|
||||
@@ -998,7 +1192,79 @@ If you want to enable all experimental features set the env var to `HELMFILE_EXP
|
||||
|
||||
For more examples, see the [examples/README.md](https://github.com/roboll/helmfile/blob/master/examples/README.md) or the [`helmfile`](https://github.com/cloudposse/helmfiles/tree/master/releases) distribution by [Cloud Posse](https://github.com/cloudposse/).
|
||||
|
||||
# Attribution
|
||||
## Integrations
|
||||
|
||||
- [renovate](https://github.com/renovatebot/renovate) automates chart version updates. See [this PR for more information](https://github.com/renovatebot/renovate/pull/5257).
|
||||
- For updating container image tags and git tags embedded within helmfile.yaml and values, you can use [renovate's regexManager](https://docs.renovatebot.com/modules/manager/regex/). Please see [this comment in the renovate repository](https://github.com/renovatebot/renovate/issues/6130#issuecomment-624061289) for more information.
|
||||
- [ArgoCD Integration](#argocd-integration)
|
||||
- [Azure ACR Integration](#azure-acr-integration)
|
||||
|
||||
### ArgoCD Integration
|
||||
|
||||
Use [ArgoCD](https://argoproj.github.io/argo-cd/) with `helmfile template` for GitOps.
|
||||
|
||||
ArgoCD has support for kustomize/manifests/helm chart by itself. Why bother with Helmfile?
|
||||
|
||||
The reasons may vary:
|
||||
|
||||
1. You do want to manage applications with ArgoCD, while letting Helmfile manage infrastructure-related components like Calico/Cilium/WeaveNet, Linkerd/Istio, and ArgoCD itself.
|
||||
- This way, any application deployed by ArgoCD has access to all the infrastructure.
|
||||
- Of course, you can use ArgoCD's [Sync Waves and Phases](https://argoproj.github.io/argo-cd/user-guide/sync-waves/) for ordering the infrastructure and application installations. But it may be difficult to separate the concern between the infrastructure and apps and annotate K8s resources consistently when you have different teams for managing infra and apps.
|
||||
2. You want to review the exact K8s manifests being applied on pull-request time, before ArgoCD syncs.
|
||||
- This is often better than using a kind of `HelmRelease` custom resources that obfuscates exactly what manifests are being applied, which makes reviewing harder.
|
||||
3. Use Helmfile as the single-pane of glass for all the K8s resources deployed to your cluster(s).
|
||||
- Helmfile can reduce repetition in K8s manifests across ArgoCD application
|
||||
|
||||
For 1, you run `helmfile apply` on CI to deploy ArgoCD and the infrastructure components.
|
||||
|
||||
> helmfile config for this phase often reside within the same directory as your Terraform project. So connecting the two with [terraform-provider-helmfile](https://github.com/mumoshu/terraform-provider-helmfile) may be helpful
|
||||
|
||||
For 2, another app-centric CI or bot should render/commit manifests by running:
|
||||
|
||||
```
|
||||
helmfile template --output-dir-template $(pwd)/gitops//{{.Release.Name}}
|
||||
cd gitops
|
||||
git add .
|
||||
git commit -m 'some message'
|
||||
git push origin $BRANCH
|
||||
```
|
||||
|
||||
> Note that `$(pwd)` is necessary when `hemlfile.yaml` has one or more sub-helmfiles in nested directories,
|
||||
> because setting a relative file path in `--output-dir` or `--output-dir-template` results in each sub-helmfile render
|
||||
> to the directory relative to the specified path.
|
||||
|
||||
so that they can be deployed by Argo CD as usual.
|
||||
|
||||
|
||||
The CI or bot can optionally submit a PR to be review by human, running:
|
||||
|
||||
```
|
||||
hub pull-request -b main -l gitops -m 'some description'
|
||||
```
|
||||
|
||||
Recommendations:
|
||||
|
||||
- Do create ArgoCD `Application` custom resource per Helm/Helmfile release, each point to respective sub-directory generated by `helmfile template --output-dir-template`
|
||||
- If you don't directly push it to the main Git branch and instead go through a pull-request, do lint rendered manifests on your CI, so that you can catch easy mistakes earlier/before ArgoCD finally deploys it
|
||||
- See [this ArgoCD issue](https://github.com/argoproj/argo-cd/issues/2143#issuecomment-570478329) for why you may want this, and see [this helmfile issue](https://github.com/roboll/helmfile/pull/1357) for how `--output-dir-template` works.
|
||||
|
||||
### Azure ACR Integration
|
||||
|
||||
Azure offers helm repository [support for Azure Container Registry](https://docs.microsoft.com/en-us/azure/container-registry/container-registry-helm-repos) as a preview feature.
|
||||
|
||||
To use this you must first `az login` and then `az acr helm repo add -n <MyRegistry>`. This will extract a token for the given ACR and configure `helm` to use it, e.g. `helm repo update` should work straight away.
|
||||
|
||||
To use `helmfile` with ACR, on the other hand, you must either include a username/password in the repository definition for the ACR in your `helmfile.yaml` or use the `--skip-deps` switch, e.g. `helmfile template --skip-deps`.
|
||||
|
||||
An ACR repository definition in `helmfile.yaml` looks like this:
|
||||
|
||||
```yaml
|
||||
repositories:
|
||||
- name: <MyRegistry>
|
||||
url: https://<MyRegistry>.azurecr.io/helm/v1/repo
|
||||
```
|
||||
|
||||
## Attribution
|
||||
|
||||
We use:
|
||||
|
||||
|
||||
@@ -11,11 +11,20 @@ information to this file.
|
||||
|---|---|---|---|---|
|
||||
| [reddit.com](https://www.reddit.com/) | production | [GitHub issue](https://github.com/roboll/helmfile/issues/96#issue-312012249), [Talk](https://www.slideshare.net/GregoryTaylor11/helm-at-reddit-from-local-dev-staging-to-production) | San Francisco, CA | April 2018 |
|
||||
| [ricardo.ch](https://www.ricardo.ch/) | production | We're deploying our complete application platform using Helmfile. | Zug, Switzerland | April 2018 |
|
||||
| [Hellofresh](https://engineering.hellofresh.com/) | production | We've been using helmfile in production since April 2018 for deploying all our infrastructure applications | Berlin, Germany | April 2018 |
|
||||
| [Cherre](https://cherre.com/) | production | We have no public posts about using Helmfile, but we have been using it for a long time now | New York, NY | October 2018 |
|
||||
| [Sight Machine](https://sightmachine.com/) | production | We don't have anything publicly posted about it, but have been using it for quite a while in production. | San Francisco, CA and Ann Arbor, MI | December 2018 |
|
||||
| [TailorMed](https://tailormed.co/) | production | We're deploying all of our platform and all K8s resources using Helmfile. The power of Helmfile are more then amazing. Thank you! | Tel-Aviv, Israel | September 2020 |
|
||||
| [VSHN – The DevOps Company](https://vshn.ch) | production | | Zurich, Switzerland | March 2019 |
|
||||
| [Vlocity](https://vlocity.com/) | proof-of-concept | | Melbourne, Australia | March 2019 |
|
||||
| [transit](https://transit.app/) | production | [Blog post](https://medium.com/@naseem_60378/helmfile-its-like-a-helm-for-your-helm-74a908581599). | Montreal, Canada | March 2019 |
|
||||
| [uniqkey](https://uniqkey.eu/) | production | [Wiki Page](https://ocd-scm.github.io/ocd-meta/) | Copenhagen, Denmark | April 2019 |
|
||||
| [bitsofinfo](https://github.com/bitsofinfo/helmfile-deploy) | proof-of-concept, stage | Used with [helmfile-deploy](https://github.com/bitsofinfo/helmfile-deploy) to manage releases for dozens of apps | USA | July 2019 |
|
||||
| [bitsofinfo](https://github.com/bitsofinfo/helmfile-deploy) | production | Used with [helmfile-deploy](https://github.com/bitsofinfo/helmfile-deploy) to manage releases for dozens of apps | USA | July 2019 |
|
||||
| [kloeckner-i](https://www.kloeckner-i.com/) | production | We are deploying our standard tools via helmfile to all our clusters. | Berlin, Germany | September 2019 |
|
||||
| [American Express](https://www.americanexpress.com) | proof-of-concept | Orchestration of both internal cluster workloads and local developer environments. | London, GB | January 2020 |
|
||||
| [Sportradar](https://www.sportradar.com) | production | Since mid-2019, we've been deploying our core infrastructure and several application stacks with Helmfile. | St. Gallen, Switzerland | March 2020 |
|
||||
| [PedidosYa](https://www.pedidosya.com) | production | | Montevideo, Uruguay | June 2020 |
|
||||
| [Jenkins OSS](https://jenkins.io) | production | [jenkins-infra/charts](https://github.com/jenkins-infra/charts) | * | July 2020 |
|
||||
| [SettleMint](https://settlemint.com) | production | The SettleMint platform allows enterprises to spin up k8s clusters and deploy production grade blockchain networks and additional services. Helmfile is in charge of deploying these networks and services on demand out of the self service management ui. | Belgium, Singapore, UAE, India | October 2020 |
|
||||
| [AutoTrader (UK)](https://www.autotrader.co.uk) | production | We've used Helmfile for 2+ years to deploy 400+ services | UK | October 2020 |
|
||||
<!-- TABLE_END -->
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
## Advanced Features
|
||||
|
||||
- [Import Configuration Parameters into Helmfile](#import-configuration-parameters-into-helmfile)
|
||||
|
||||
### Import Configuration Parameters into Helmfile
|
||||
|
||||
Helmfile integrates [vals]() to import configuration parameters from following backends:
|
||||
|
||||
- AWS SSM Parameter Store
|
||||
- AWS SecretsManager
|
||||
- Vault
|
||||
- SOPS
|
||||
|
||||
See [Vals "Suported Backends"](https://github.com/variantdev/vals#suported-backends) for the full list of available backends.
|
||||
|
||||
This feature was implemented in https://github.com/roboll/helmfile/pull/906.
|
||||
If you're curious how it's designed and how it works, please consult the pull request.
|
||||
|
||||
### Deploy Kustomizations with Helmfile
|
||||
|
||||
You can deploy [kustomize](https://github.com/kubernetes-sigs/kustomize) "kustomization"s with Helmfile.
|
||||
|
||||
Most of Kustomize operations that is usually done with `kustomize edit` can be done declaratively via Helm values.yaml files.
|
||||
|
||||
Under the hood, Helmfile transforms the kustomization into a local chart in a temporary directory so that it can be `helm upgrade --install`ed.
|
||||
|
||||
The transformation is done by generating (1)a temporary kustomization from various options and (2)temporary chart from the temporary kustomization.
|
||||
|
||||
An example pseudo code for the transformation logic can be written as:
|
||||
|
||||
```console
|
||||
$ TMPCHART=/tmp/sometmpdir
|
||||
$ mkdir -p ${TMPCHART}/templates
|
||||
$ somehow_generate_chart_yaml ${TMPCHART}/Chart.yaml
|
||||
|
||||
$ TMPKUSTOMIZATION=/tmp/sometmpdir2
|
||||
$ somehow_generate_temp_kustomization_yaml ${TMPKUSTOMIZATION}/kustomization.yaml
|
||||
$ kustomize build ${TMPKUSTOMIZATION}/kustomization.yaml > ${TMPCHART}/templates/all.yaml
|
||||
```
|
||||
|
||||
Let's say you have a `helmfile.yaml` that looks like the below:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: myapp
|
||||
chart: mykustomization
|
||||
values:
|
||||
- values.yaml
|
||||
```
|
||||
|
||||
Helmfile firstly generates a temporary `kustomization.yaml` that looks like:
|
||||
|
||||
```yaml
|
||||
bases:
|
||||
- $(ABS_PATH_TO_HELMFILE_YAML}/mykustomization
|
||||
```
|
||||
|
||||
Followed by the below steps:
|
||||
|
||||
- Running `kustomize edit set image $IMAGE` for every `$IMAGE` generated from your values.yaml
|
||||
- Running `kustomize edit set nameprefix $NAMEPREFIX` with the nameprefix specified in your values.yaml
|
||||
- Running `kustomize edit set namesuffix $NAMESUFFIX` with the namesuffix specified in your values.yaml
|
||||
- Running `kustomize edit set namespace $NS` with the namespace specified in your values.yaml
|
||||
|
||||
A `values.yaml` file for kustomization would look like the below:
|
||||
|
||||
```yaml
|
||||
images:
|
||||
# kustomize edit set image mysql=eu.gcr.io/my-project/mysql@canary
|
||||
- name: mysql
|
||||
newName: eu.gcr.io/my-project/mysql
|
||||
newTag: canary
|
||||
# kustomize edit set image myapp=my-registry/my-app@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3
|
||||
- name: myapp
|
||||
digest: sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3
|
||||
newName: my-registry/my-app
|
||||
|
||||
# kustomize edit set nameprefix foo-
|
||||
namePrefix: foo-
|
||||
|
||||
# kustomize edit set namesuffix -bar
|
||||
nameSuffix: -bar
|
||||
|
||||
# kustomize edit set namespace myapp
|
||||
namespace: myapp
|
||||
```
|
||||
|
||||
At this point, Helmfile can generate a complete kustomization from the base kustomization you specified in `releases[].chart` of your helmfile.yaml and `values.yaml`,
|
||||
which can be included in the temporary chart.
|
||||
|
||||
After all, Helmfile just installs the temporary chart like standard charts, which allows you to manage everything with Helmfile regardless of each app is declared using a Helm chart or a kustomization.
|
||||
|
||||
Please also see [test/advanced/helmfile.yaml](https://github.com/roboll/helmfile/tree/master/test/advanced/helmfile.yaml) for an example of kustomization support and more.
|
||||
|
||||
## Adhoc Customization of Helm charts
|
||||
|
||||
You can add/update any Kubernetes resource field rendered from a Helm chart by specifying `releases[].strategicMergePatches`:
|
||||
|
||||
```
|
||||
repositories:
|
||||
- name: incubator
|
||||
url: https://kubernetes-charts-incubator.storage.googleapis.com
|
||||
|
||||
releases:
|
||||
- name: raw1
|
||||
chart: incubator/raw
|
||||
values:
|
||||
- resources:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: raw1
|
||||
namespace: default
|
||||
data:
|
||||
foo: FOO
|
||||
strategicMergePatches:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: raw1
|
||||
namespace: default
|
||||
data:
|
||||
bar: BAR
|
||||
```
|
||||
|
||||
Running `helmfile template` on the above example results in a ConfigMap called `raw` whose `data` is:
|
||||
|
||||
```yaml
|
||||
foo: FOO
|
||||
bar: BAR
|
||||
```
|
||||
|
||||
Please note that the second `data` field `bar` is coming from the strategic-merge patch defined in the above helmfile.yaml.
|
||||
|
||||
There's also `releases[].jsonPatches` that works similarly to `strategicMergePatches` but has additional capability to remove fields.
|
||||
|
||||
Please also see [test/advanced/helmfile.yaml](https://github.com/roboll/helmfile/tree/master/test/advanced/helmfile.yaml) for an example of patching support and more.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Secrets
|
||||
|
||||
helmfile can handle secrets using [helm-secrets](https://github.com/zendesk/helm-secrets) plugin or using remote secrets storage
|
||||
(everything that package [vals](https://github.com/variantdev/vals) can handle vault, AWS SSM etc)
|
||||
This section will describe the second use case.
|
||||
|
||||
# Remote secrets
|
||||
|
||||
This paragraph will describe how to use remote secrets storage (vault, SSM etc) in helmfile
|
||||
|
||||
## Fetching single key
|
||||
|
||||
To fetch single key from remote secret storage you can use `fetchSecretValue` template function example below
|
||||
|
||||
```yaml
|
||||
# helmfile.yaml
|
||||
|
||||
repositories:
|
||||
- name: stable
|
||||
url: https://kubernetes-charts.storage.googleapis.com
|
||||
|
||||
environments:
|
||||
default:
|
||||
values:
|
||||
- service:
|
||||
password: ref+vault://svc/#pass
|
||||
login: ref+vault://svc/#login
|
||||
releases:
|
||||
- name: service
|
||||
namespace: default
|
||||
labels:
|
||||
cluster: services
|
||||
secrets: vault
|
||||
chart: stable/svc
|
||||
version: 0.1.0
|
||||
values:
|
||||
- service:
|
||||
login: {{ .Values.service.login | fetchSecretValue }} # this will resolve ref+vault://svc/#pass and fetch secret from vault
|
||||
password: {{ .Values.service.password | fetchSecretValue | quote }}
|
||||
# - values/service.yaml.gotmpl # alternatively
|
||||
```
|
||||
## Fetching multiple keys
|
||||
Alternatively you can use `expandSecretRefs` to fetch a map of secrets
|
||||
```yaml
|
||||
# values/service.yaml.gotmpl
|
||||
service:
|
||||
{{ .Values.service | expandSecretRefs | toYaml | nindent 2 }}
|
||||
```
|
||||
|
||||
This will produce
|
||||
```yaml
|
||||
# values/service.yaml
|
||||
service:
|
||||
login: svc-login # fetched from vault
|
||||
password: pass
|
||||
|
||||
```
|
||||
|
||||
+11
-11
@@ -14,10 +14,10 @@ That is, the following example let `helmfile` fail when you have no `eventApi.re
|
||||
{{ .Values.eventApi.replicas | default 1 }}
|
||||
```
|
||||
|
||||
In case it isn't a mistake and you do want to allow missing keys, use the `getOrNil` template function:
|
||||
In case it isn't a mistake and you do want to allow missing keys, use the `get` template function:
|
||||
|
||||
```
|
||||
{{ .Values | getOrNil "eventApi.replicas" }}
|
||||
{{ .Values | get "eventApi.replicas" nil }}
|
||||
```
|
||||
|
||||
This result in printing `<no value` in your template, that may or may not result in a failure.
|
||||
@@ -25,14 +25,14 @@ This result in printing `<no value` in your template, that may or may not result
|
||||
If you want a kind of default values that is used when a missing key was referenced, use `default` like:
|
||||
|
||||
```
|
||||
{{ .Values | getOrNil "eventApi.replicas" | default 1 }}
|
||||
{{ .Values | get "eventApi.replicas" 1 }}
|
||||
```
|
||||
|
||||
Now, you get `1` when there is no `eventApi.replicas` defined in environment values.
|
||||
|
||||
## Release Template / Conventional Directory Structure
|
||||
|
||||
Introducing helmfile into a large-scale project that involes dozens of releases often results in a lot of repetitions in `helmfile.yaml` files.
|
||||
Introducing helmfile into a large-scale project that involves dozens of releases often results in a lot of repetitions in `helmfile.yaml` files.
|
||||
|
||||
The example below shows repetitions in `namespace`, `chart`, `values`, and `secrets`:
|
||||
|
||||
@@ -57,7 +57,7 @@ releases:
|
||||
values:
|
||||
- "./config/kubernetes-dashboard/values.yaml"
|
||||
- "./config/kubernetes-dashboard/{{ .Environment.Name }}.yaml"
|
||||
values:
|
||||
secrets:
|
||||
- "./config/kubernetes-dashboard/secrets.yaml"
|
||||
- "./config/kubernetes-dashboard/{{ .Environment.Name }}-secrets.yaml"
|
||||
```
|
||||
@@ -146,7 +146,7 @@ bases:
|
||||
- environments.yaml
|
||||
|
||||
releases:
|
||||
- name: metricbaet
|
||||
- name: metricbeat
|
||||
chart: stable/metricbeat
|
||||
- name: myapp
|
||||
chart: mychart
|
||||
@@ -173,7 +173,7 @@ environments:
|
||||
releases:
|
||||
- name: myapp
|
||||
chart: mychart
|
||||
- name: metricbaet
|
||||
- name: metricbeat
|
||||
chart: stable/metricbeat
|
||||
```
|
||||
|
||||
@@ -186,7 +186,7 @@ environments:
|
||||
production:
|
||||
|
||||
releases:
|
||||
- name: metricbaet
|
||||
- name: metricbeat
|
||||
chart: stable/metricbeat
|
||||
- name: myapp
|
||||
chart: mychart
|
||||
@@ -204,7 +204,7 @@ Helmfile doesn't merge arrays across layers. That is, the below example doesn't
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: metricbaet
|
||||
- name: metricbeat
|
||||
chart: stable/metricbeat
|
||||
---
|
||||
releases:
|
||||
@@ -216,7 +216,7 @@ Helmfile overrides the `releases` array with the latest layer so the resulting s
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
# metricbeat release disappeared! but that's how helmfiel works
|
||||
# metricbeat release disappeared! but that's how helmfile works
|
||||
- name: myapp
|
||||
chart: mychart
|
||||
```
|
||||
@@ -228,7 +228,7 @@ A work-around is to treat the state file as a go template and use `readFile` tem
|
||||
```yaml
|
||||
templates:
|
||||
metricbeat: &metricbeat
|
||||
- name: metricbeat
|
||||
name: metricbeat
|
||||
chart: stable/metricbeat
|
||||
```
|
||||
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
module github.com/roboll/helmfile
|
||||
|
||||
go 1.12
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/Masterminds/goutils v1.1.0 // indirect
|
||||
github.com/Masterminds/semver v1.4.1
|
||||
github.com/Masterminds/sprig v2.20.0+incompatible
|
||||
github.com/Azure/azure-sdk-for-go v35.0.0+incompatible // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.0
|
||||
github.com/Masterminds/sprig/v3 v3.1.0
|
||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a
|
||||
github.com/go-test/deep v1.0.3
|
||||
github.com/google/go-cmp v0.3.0
|
||||
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c // indirect
|
||||
github.com/golang/mock v1.4.3
|
||||
github.com/google/go-cmp v0.4.0
|
||||
github.com/gosuri/uitable v0.0.3
|
||||
github.com/hashicorp/go-getter v1.3.0
|
||||
github.com/huandu/xstrings v1.2.0 // indirect
|
||||
github.com/imdario/mergo v0.3.6
|
||||
github.com/pkg/errors v0.8.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.6.3 // indirect
|
||||
github.com/hashicorp/go-version v1.2.0
|
||||
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c // indirect
|
||||
github.com/imdario/mergo v0.3.11
|
||||
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 v0.0.0-20160620154522-6011f165dc28
|
||||
go.uber.org/atomic v1.3.2 // indirect
|
||||
github.com/urfave/cli v1.20.0
|
||||
github.com/variantdev/chartify v0.4.8
|
||||
github.com/variantdev/dag v0.0.0-20191028002400-bb0b3c785363
|
||||
github.com/variantdev/vals v0.10.3
|
||||
go.uber.org/multierr v1.1.0
|
||||
go.uber.org/zap v1.8.0
|
||||
gopkg.in/yaml.v2 v2.2.1
|
||||
go.uber.org/zap v1.9.1
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
|
||||
gopkg.in/square/go-jose.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
gotest.tools v2.2.0+incompatible
|
||||
gotest.tools/v3 v3.0.3-0.20200410202438-4e4a41b7851a
|
||||
k8s.io/apimachinery v0.0.0-20190409092423-760d1845f48b
|
||||
)
|
||||
|
||||
replace git.apache.org/thrift.git => github.com/apache/thrift v0.12.0
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "=> Building container"
|
||||
docker build \
|
||||
--tag "$IMAGE_NAME" \
|
||||
--file Dockerfile .
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
go()
|
||||
{
|
||||
docker run --rm -v "$(pwd):/workspace" -w /workspace golang:latest go $@;
|
||||
}
|
||||
|
||||
go get -u github.com/tcnksm/ghr github.com/mitchellh/gox
|
||||
go mod vendor
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "=> Fetch unshallow origin"
|
||||
git fetch --unshallow origin || true
|
||||
|
||||
echo "=> Tag image:"
|
||||
docker tag "${IMAGE_NAME}" "${DOCKER_REPO}:$(git describe --tags --abbrev=0 HEAD^)"
|
||||
docker tag "${IMAGE_NAME}" "${DOCKER_REPO}:$(git describe --tags --abbrev=0 HEAD)"
|
||||
|
||||
echo "=> Push images"
|
||||
docker push "${DOCKER_REPO}"
|
||||
@@ -5,31 +5,27 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/app/version"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/app"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/maputil"
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
"github.com/urfave/cli"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
var Version string
|
||||
|
||||
var logger *zap.SugaredLogger
|
||||
|
||||
func configureLogging(c *cli.Context) error {
|
||||
// Valid levels:
|
||||
// https://github.com/uber-go/zap/blob/7e7e266a8dbce911a49554b945538c5b950196b8/zapcore/level.go#L126
|
||||
logLevel := c.GlobalString("log-level")
|
||||
if c.GlobalBool("quiet") {
|
||||
if c.GlobalBool("debug") {
|
||||
logLevel = "debug"
|
||||
} else if c.GlobalBool("quiet") {
|
||||
logLevel = "warn"
|
||||
}
|
||||
var level zapcore.Level
|
||||
err := level.Set(logLevel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger = helmexec.NewLogger(os.Stderr, logLevel)
|
||||
if c.App.Metadata == nil {
|
||||
// Auto-initialised in 1.19.0
|
||||
@@ -45,11 +41,12 @@ func main() {
|
||||
cliApp := cli.NewApp()
|
||||
cliApp.Name = "helmfile"
|
||||
cliApp.Usage = ""
|
||||
cliApp.Version = Version
|
||||
cliApp.Version = version.Version
|
||||
cliApp.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "helm-binary, b",
|
||||
Usage: "path to helm binary",
|
||||
Value: app.DefaultHelmBinary,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "file, f",
|
||||
@@ -75,6 +72,10 @@ func main() {
|
||||
Name: "kube-context",
|
||||
Usage: "Set kubectl context. Uses current context by default",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "Enable verbose output for Helm and set log-level to debug, this disables --quiet/-q effect",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-color",
|
||||
Usage: "Output without color",
|
||||
@@ -190,6 +191,10 @@ func main() {
|
||||
Name: "detailed-exitcode",
|
||||
Usage: "return a non-zero exit code when there are changes",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "include-tests",
|
||||
Usage: "enable the diffing of the helm test hooks",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "suppress-secrets",
|
||||
Usage: "suppress secrets in the output. highly recommended to specify on CI/CD use-cases",
|
||||
@@ -230,6 +235,48 @@ func main() {
|
||||
Name: "output-dir",
|
||||
Usage: "output directory to pass to helm template (helm template --output-dir)",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "output-dir-template",
|
||||
Usage: "go text template for generating the output directory. Default: {{ .OutputDir }}/{{ .State.BaseName }}-{{ .State.AbsPathSHA1 }}-{{ .Release.Name}}",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "concurrency",
|
||||
Value: 0,
|
||||
Usage: "maximum number of concurrent downloads of release charts",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "validate",
|
||||
Usage: "validate your manifests against the Kubernetes cluster you are currently pointing at",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "include-crds",
|
||||
Usage: "include CRDs in the templated output",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "skip-deps",
|
||||
Usage: "skip running `helm repo update` and `helm dependency build`",
|
||||
},
|
||||
},
|
||||
Action: action(func(run *app.App, c configImpl) error {
|
||||
return run.Template(c)
|
||||
}),
|
||||
},
|
||||
{
|
||||
Name: "write-values",
|
||||
Usage: "write values files for releases. Similar to `helmfile template`, write values files instead of manifests.",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringSliceFlag{
|
||||
Name: "set",
|
||||
Usage: "additional values to be merged into the command",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "values",
|
||||
Usage: "additional value files to be merged into the command",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "output-file-template",
|
||||
Usage: "go text template for generating the output file. Default: {{ .State.BaseName }}-{{ .State.AbsPathSHA1 }}/{{ .Release.Name}}.yaml",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "concurrency",
|
||||
Value: 0,
|
||||
@@ -241,7 +288,7 @@ func main() {
|
||||
},
|
||||
},
|
||||
Action: action(func(run *app.App, c configImpl) error {
|
||||
return run.Template(c)
|
||||
return run.WriteValues(c)
|
||||
}),
|
||||
},
|
||||
{
|
||||
@@ -328,15 +375,31 @@ func main() {
|
||||
Value: 0,
|
||||
Usage: "output NUM lines of context around changes",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "detailed-exitcode",
|
||||
Usage: "return a non-zero exit code 2 instead of 0 when there were changes detected AND the changes are synced successfully",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "args",
|
||||
Value: "",
|
||||
Usage: "pass args to helm exec",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "retain-values-files",
|
||||
Usage: "Stop cleaning up values files passed to Helm. Together with --log-level=debug, you can manually rerun helm commands as Helmfile did for debugging purpose",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "include-tests",
|
||||
Usage: "enable the diffing of the helm test hooks",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "suppress-secrets",
|
||||
Usage: "suppress secrets in the diff output. highly recommended to specify on CI/CD use-cases",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "suppress-diff",
|
||||
Usage: "suppress diff in the output. Usable in new installs",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "skip-deps",
|
||||
Usage: "skip running `helm repo update` and `helm dependency build`",
|
||||
@@ -438,7 +501,12 @@ func main() {
|
||||
{
|
||||
Name: "build",
|
||||
Usage: "output compiled helmfile state(s) as YAML",
|
||||
Flags: []cli.Flag{},
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "embed-values",
|
||||
Usage: "Read all the values files for every release and embed into the output helmfile.yaml",
|
||||
},
|
||||
},
|
||||
Action: action(func(run *app.App, c configImpl) error {
|
||||
return run.PrintState(c)
|
||||
}),
|
||||
@@ -446,11 +514,26 @@ func main() {
|
||||
{
|
||||
Name: "list",
|
||||
Usage: "list releases defined in state file",
|
||||
Flags: []cli.Flag{},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "output",
|
||||
Value: "",
|
||||
Usage: "output releases list as a json string",
|
||||
},
|
||||
},
|
||||
Action: action(func(run *app.App, c configImpl) error {
|
||||
return run.ListReleases(c)
|
||||
}),
|
||||
},
|
||||
{
|
||||
Name: "version",
|
||||
Usage: "Show the version for Helmfile.",
|
||||
ArgsUsage: "[command]",
|
||||
Action: func(c *cli.Context) error {
|
||||
cli.ShowVersion(c)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := cliApp.Run(os.Args)
|
||||
@@ -483,10 +566,10 @@ func NewUrfaveCliConfigImpl(c *cli.Context) (configImpl, error) {
|
||||
ops := strings.Split(optsSet[i], ",")
|
||||
for j := range ops {
|
||||
op := strings.SplitN(ops[j], "=", 2)
|
||||
k := strings.Split(op[0], ".")
|
||||
k := maputil.ParseKey(op[0])
|
||||
v := op[1]
|
||||
|
||||
set = maputil.Set(set, k, v)
|
||||
maputil.Set(set, k, v)
|
||||
}
|
||||
}
|
||||
conf.set = set
|
||||
@@ -508,13 +591,31 @@ func (c configImpl) Values() []string {
|
||||
}
|
||||
|
||||
func (c configImpl) Args() string {
|
||||
return c.c.String("args")
|
||||
args := c.c.String("args")
|
||||
enableHelmDebug := c.c.GlobalBool("debug")
|
||||
|
||||
if enableHelmDebug {
|
||||
args = fmt.Sprintf("%s %s", args, "--debug")
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func (c configImpl) OutputDir() string {
|
||||
return c.c.String("output-dir")
|
||||
}
|
||||
|
||||
func (c configImpl) OutputDirTemplate() string {
|
||||
return c.c.String("output-dir-template")
|
||||
}
|
||||
|
||||
func (c configImpl) OutputFileTemplate() string {
|
||||
return c.c.String("output-file-template")
|
||||
}
|
||||
|
||||
func (c configImpl) Validate() bool {
|
||||
return c.c.Bool("validate")
|
||||
}
|
||||
|
||||
func (c configImpl) Concurrency() int {
|
||||
return c.c.Int("concurrency")
|
||||
}
|
||||
@@ -533,10 +634,22 @@ func (c configImpl) DetailedExitcode() bool {
|
||||
return c.c.Bool("detailed-exitcode")
|
||||
}
|
||||
|
||||
func (c configImpl) RetainValuesFiles() bool {
|
||||
return c.c.Bool("retain-values-files")
|
||||
}
|
||||
|
||||
func (c configImpl) IncludeTests() bool {
|
||||
return c.c.Bool("include-tests")
|
||||
}
|
||||
|
||||
func (c configImpl) SuppressSecrets() bool {
|
||||
return c.c.Bool("suppress-secrets")
|
||||
}
|
||||
|
||||
func (c configImpl) SuppressDiff() bool {
|
||||
return c.c.Bool("suppress-diff")
|
||||
}
|
||||
|
||||
// DeleteConfig
|
||||
|
||||
func (c configImpl) Purge() bool {
|
||||
@@ -550,9 +663,18 @@ func (c configImpl) Cleanup() bool {
|
||||
}
|
||||
|
||||
func (c configImpl) Timeout() int {
|
||||
if !c.c.IsSet("timeout") {
|
||||
return state.EmptyTimeout
|
||||
}
|
||||
return c.c.Int("timeout")
|
||||
}
|
||||
|
||||
// ListConfig
|
||||
|
||||
func (c configImpl) Output() string {
|
||||
return c.c.String("output")
|
||||
}
|
||||
|
||||
// GlobalConfig
|
||||
|
||||
func (c configImpl) HelmBinary() string {
|
||||
@@ -595,6 +717,14 @@ func (c configImpl) Context() int {
|
||||
return c.c.Int("context")
|
||||
}
|
||||
|
||||
func (c configImpl) EmbedValues() bool {
|
||||
return c.c.Bool("embed-values")
|
||||
}
|
||||
|
||||
func (c configImpl) IncludeCRDs() bool {
|
||||
return c.c.Bool("include-crds")
|
||||
}
|
||||
|
||||
func (c configImpl) Logger() *zap.SugaredLogger {
|
||||
return c.c.App.Metadata["logger"].(*zap.SugaredLogger)
|
||||
}
|
||||
@@ -602,7 +732,10 @@ func (c configImpl) Logger() *zap.SugaredLogger {
|
||||
func (c configImpl) Env() string {
|
||||
env := c.c.GlobalString("environment")
|
||||
if env == "" {
|
||||
env = state.DefaultEnv
|
||||
env = os.Getenv("HELMFILE_ENVIRONMENT")
|
||||
if env == "" {
|
||||
env = state.DefaultEnv
|
||||
}
|
||||
}
|
||||
return env
|
||||
}
|
||||
@@ -616,11 +749,11 @@ func action(do func(*app.App, configImpl) error) func(*cli.Context) error {
|
||||
|
||||
a := app.New(conf)
|
||||
|
||||
a.ErrorHandler = func(err error) error {
|
||||
if err := do(a, conf); err != nil {
|
||||
return toCliError(implCtx, err)
|
||||
}
|
||||
|
||||
return do(a, conf)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1169
-208
File diff suppressed because it is too large
Load Diff
+2392
-282
File diff suppressed because it is too large
Load Diff
@@ -40,11 +40,18 @@ type ApplyConfigProvider interface {
|
||||
Set() []string
|
||||
SkipDeps() bool
|
||||
|
||||
IncludeTests() bool
|
||||
|
||||
SuppressSecrets() bool
|
||||
SuppressDiff() bool
|
||||
|
||||
DetailedExitcode() bool
|
||||
|
||||
NoColor() bool
|
||||
Context() int
|
||||
|
||||
RetainValuesFiles() bool
|
||||
|
||||
concurrencyConfig
|
||||
interactive
|
||||
loggingConfig
|
||||
@@ -68,7 +75,10 @@ type DiffConfigProvider interface {
|
||||
Set() []string
|
||||
SkipDeps() bool
|
||||
|
||||
IncludeTests() bool
|
||||
|
||||
SuppressSecrets() bool
|
||||
SuppressDiff() bool
|
||||
|
||||
DetailedExitcode() bool
|
||||
NoColor() bool
|
||||
@@ -119,12 +129,22 @@ type TemplateConfigProvider interface {
|
||||
|
||||
Values() []string
|
||||
Set() []string
|
||||
OutputDirTemplate() string
|
||||
Validate() bool
|
||||
SkipDeps() bool
|
||||
OutputDir() string
|
||||
IncludeCRDs() bool
|
||||
|
||||
concurrencyConfig
|
||||
}
|
||||
|
||||
type WriteValuesConfigProvider interface {
|
||||
Values() []string
|
||||
Set() []string
|
||||
OutputFileTemplate() string
|
||||
SkipDeps() bool
|
||||
}
|
||||
|
||||
type StatusesConfigProvider interface {
|
||||
Args() string
|
||||
|
||||
@@ -132,6 +152,7 @@ type StatusesConfigProvider interface {
|
||||
}
|
||||
|
||||
type StateConfigProvider interface {
|
||||
EmbedValues() bool
|
||||
}
|
||||
|
||||
type concurrencyConfig interface {
|
||||
@@ -145,3 +166,7 @@ type loggingConfig interface {
|
||||
type interactive interface {
|
||||
Interactive() bool
|
||||
}
|
||||
|
||||
type ListConfigProvider interface {
|
||||
Output() string
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"gotest.tools/assert"
|
||||
is "gotest.tools/assert/cmp"
|
||||
"gotest.tools/env"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/env"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
+7
-17
@@ -5,31 +5,21 @@ import (
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
updatedRepos map[string]struct{}
|
||||
updatedRepos map[string]bool
|
||||
}
|
||||
|
||||
func NewContext() Context {
|
||||
return Context{
|
||||
updatedRepos: map[string]struct{}{},
|
||||
updatedRepos: map[string]bool{},
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx Context) SyncReposOnce(st *state.HelmState, helm state.RepoUpdater) []error {
|
||||
var errs []error
|
||||
func (ctx Context) SyncReposOnce(st *state.HelmState, helm state.RepoUpdater) error {
|
||||
updated, err := st.SyncRepos(helm, ctx.updatedRepos)
|
||||
|
||||
allUpdated := true
|
||||
for _, r := range st.Repositories {
|
||||
_, exists := ctx.updatedRepos[r.Name]
|
||||
allUpdated = allUpdated && exists
|
||||
for _, r := range updated {
|
||||
ctx.updatedRepos[r] = true
|
||||
}
|
||||
|
||||
if !allUpdated {
|
||||
errs = st.SyncRepos(helm)
|
||||
|
||||
for _, r := range st.Repositories {
|
||||
ctx.updatedRepos[r.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,30 +4,37 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/remote"
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
"github.com/variantdev/vals"
|
||||
"go.uber.org/zap"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultHelmBinary = state.DefaultHelmBinary
|
||||
)
|
||||
|
||||
type desiredStateLoader struct {
|
||||
KubeContext string
|
||||
Reverse bool
|
||||
overrideKubeContext string
|
||||
overrideHelmBinary string
|
||||
|
||||
env string
|
||||
namespace string
|
||||
|
||||
readFile func(string) ([]byte, error)
|
||||
deleteFile func(string) error
|
||||
fileExists func(string) (bool, error)
|
||||
abs func(string) (string, error)
|
||||
glob func(string) ([]string, error)
|
||||
getHelm func(*state.HelmState) helmexec.Interface
|
||||
|
||||
logger *zap.SugaredLogger
|
||||
helm helmexec.Interface
|
||||
remote *remote.Remote
|
||||
logger *zap.SugaredLogger
|
||||
valsRuntime vals.Evaluator
|
||||
}
|
||||
|
||||
func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, error) {
|
||||
@@ -40,7 +47,7 @@ func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, e
|
||||
return nil, fmt.Errorf("bug: opts.CalleePath was nil: f=%s, opts=%v", f, opts)
|
||||
}
|
||||
storage := state.NewStorage(opts.CalleePath, ld.logger, ld.glob)
|
||||
envld := state.NewEnvironmentValuesLoader(storage, ld.readFile, ld.logger)
|
||||
envld := state.NewEnvironmentValuesLoader(storage, ld.readFile, ld.logger, ld.remote)
|
||||
handler := state.MissingFileHandlerError
|
||||
vals, err := envld.LoadEnvironmentValues(&handler, args)
|
||||
if err != nil {
|
||||
@@ -58,26 +65,22 @@ func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ld.Reverse {
|
||||
rev := func(i, j int) bool {
|
||||
return j < i
|
||||
}
|
||||
sort.Slice(st.Releases, rev)
|
||||
sort.Slice(st.Helmfiles, rev)
|
||||
if opts.Reverse {
|
||||
st.Reverse()
|
||||
}
|
||||
|
||||
if ld.KubeContext != "" {
|
||||
if ld.overrideKubeContext != "" {
|
||||
if st.HelmDefaults.KubeContext != "" {
|
||||
return nil, errors.New("err: Cannot use option --kube-context and set attribute helmDefaults.kubeContext.")
|
||||
}
|
||||
st.HelmDefaults.KubeContext = ld.KubeContext
|
||||
st.HelmDefaults.KubeContext = ld.overrideKubeContext
|
||||
}
|
||||
|
||||
if ld.namespace != "" {
|
||||
if st.Namespace != "" {
|
||||
if st.OverrideNamespace != "" {
|
||||
return nil, errors.New("err: Cannot use option --namespace and set attribute namespace.")
|
||||
}
|
||||
st.Namespace = ld.namespace
|
||||
st.OverrideNamespace = ld.namespace
|
||||
}
|
||||
|
||||
return st, nil
|
||||
@@ -124,11 +127,25 @@ func (ld *desiredStateLoader) loadFileWithOverrides(inheritedEnv, overrodeEnv *e
|
||||
)
|
||||
}
|
||||
|
||||
return self, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, h := range self.Helmfiles {
|
||||
if h.Path == f {
|
||||
return nil, fmt.Errorf("%s contains a recursion into the same sub-helmfile at helmfiles[%d]", f, i)
|
||||
}
|
||||
if h.Path == "." {
|
||||
return nil, fmt.Errorf("%s contains a recursion into the the directory containing this helmfile at helmfiles[%d]", f, i)
|
||||
}
|
||||
}
|
||||
|
||||
return self, nil
|
||||
}
|
||||
|
||||
func (a *desiredStateLoader) underlying() *state.StateCreator {
|
||||
c := state.NewCreator(a.logger, a.readFile, a.fileExists, a.abs, a.glob, a.helm)
|
||||
c := state.NewCreator(a.logger, a.readFile, a.fileExists, a.abs, a.glob, a.valsRuntime, a.getHelm, a.overrideHelmBinary, a.remote)
|
||||
c.DeleteFile = a.deleteFile
|
||||
c.LoadFile = a.loadFile
|
||||
return c
|
||||
}
|
||||
@@ -154,7 +171,9 @@ func (a *desiredStateLoader) load(yaml []byte, baseDir, file string, evaluateBas
|
||||
}
|
||||
|
||||
func (ld *desiredStateLoader) renderAndLoad(env, overrodeEnv *environment.Environment, baseDir, filename string, content []byte, evaluateBases bool) (*state.HelmState, error) {
|
||||
parts := bytes.Split(content, []byte("\n---\n"))
|
||||
// Allows part-splitting to work with CLRF-ed content
|
||||
normalizedContent := bytes.ReplaceAll(content, []byte("\r\n"), []byte("\n"))
|
||||
parts := bytes.Split(normalizedContent, []byte("\n---\n"))
|
||||
|
||||
var finalState *state.HelmState
|
||||
|
||||
@@ -188,12 +207,20 @@ func (ld *desiredStateLoader) renderAndLoad(env, overrodeEnv *environment.Enviro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, r := range currentState.Releases {
|
||||
if r.Chart == "" {
|
||||
return nil, fmt.Errorf("error during %s parsing: encountered empty chart while reading release %q at index %d", id, r.Name, i)
|
||||
}
|
||||
}
|
||||
|
||||
if finalState == nil {
|
||||
finalState = currentState
|
||||
} else {
|
||||
if err := mergo.Merge(finalState, currentState, mergo.WithOverride); err != nil {
|
||||
if err := mergo.Merge(&finalState.ReleaseSetSpec, ¤tState.ReleaseSetSpec, mergo.WithOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
finalState.RenderedValues = currentState.RenderedValues
|
||||
}
|
||||
|
||||
env = &finalState.Env
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"github.com/roboll/helmfile/pkg/exectest"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/testhelper"
|
||||
"github.com/variantdev/vals"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type destroyConfig struct {
|
||||
args string
|
||||
concurrency int
|
||||
interactive bool
|
||||
logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func (d destroyConfig) Args() string {
|
||||
return d.args
|
||||
}
|
||||
|
||||
func (d destroyConfig) Interactive() bool {
|
||||
return d.interactive
|
||||
}
|
||||
|
||||
func (d destroyConfig) Logger() *zap.SugaredLogger {
|
||||
return d.logger
|
||||
}
|
||||
|
||||
func (d destroyConfig) Concurrency() int {
|
||||
return d.concurrency
|
||||
}
|
||||
|
||||
func TestDestroy(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
loc string
|
||||
ns string
|
||||
concurrency int
|
||||
error string
|
||||
files map[string]string
|
||||
selectors []string
|
||||
lists map[exectest.ListKey]string
|
||||
diffs map[exectest.DiffKey]error
|
||||
upgraded []exectest.Release
|
||||
deleted []exectest.Release
|
||||
log string
|
||||
}{
|
||||
//
|
||||
// complex test cases for smoke testing
|
||||
//
|
||||
{
|
||||
name: "smoke",
|
||||
loc: location(),
|
||||
files: map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
releases:
|
||||
- name: database
|
||||
chart: charts/mysql
|
||||
needs:
|
||||
- logging
|
||||
- name: frontend-v1
|
||||
chart: charts/frontend
|
||||
installed: false
|
||||
needs:
|
||||
- servicemesh
|
||||
- logging
|
||||
- backend-v1
|
||||
- name: frontend-v2
|
||||
chart: charts/frontend
|
||||
needs:
|
||||
- servicemesh
|
||||
- logging
|
||||
- backend-v2
|
||||
- name: frontend-v3
|
||||
chart: charts/frontend
|
||||
needs:
|
||||
- servicemesh
|
||||
- logging
|
||||
- backend-v2
|
||||
- name: backend-v1
|
||||
chart: charts/backend
|
||||
installed: false
|
||||
needs:
|
||||
- servicemesh
|
||||
- logging
|
||||
- database
|
||||
- anotherbackend
|
||||
- name: backend-v2
|
||||
chart: charts/backend
|
||||
needs:
|
||||
- servicemesh
|
||||
- logging
|
||||
- database
|
||||
- anotherbackend
|
||||
- name: anotherbackend
|
||||
chart: charts/anotherbackend
|
||||
needs:
|
||||
- servicemesh
|
||||
- logging
|
||||
- database
|
||||
- name: servicemesh
|
||||
chart: charts/istio
|
||||
needs:
|
||||
- logging
|
||||
- name: logging
|
||||
chart: charts/fluent-bit
|
||||
- name: front-proxy
|
||||
chart: stable/envoy
|
||||
`,
|
||||
},
|
||||
diffs: map[exectest.DiffKey]error{},
|
||||
lists: map[exectest.ListKey]string{
|
||||
exectest.ListKey{Filter: "^frontend-v1$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||
`,
|
||||
exectest.ListKey{Filter: "^frontend-v2$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||
frontend-v2 4 Fri Nov 1 08:40:07 2019 DEPLOYED frontend-3.1.0 3.1.0 default
|
||||
`,
|
||||
exectest.ListKey{Filter: "^frontend-v3$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||
frontend-v3 4 Fri Nov 1 08:40:07 2019 DEPLOYED frontend-3.1.0 3.1.0 default
|
||||
`,
|
||||
exectest.ListKey{Filter: "^backend-v1$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||
`,
|
||||
exectest.ListKey{Filter: "^backend-v2$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||
backend-v2 4 Fri Nov 1 08:40:07 2019 DEPLOYED backend-3.1.0 3.1.0 default
|
||||
`,
|
||||
exectest.ListKey{Filter: "^logging$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||
logging 4 Fri Nov 1 08:40:07 2019 DEPLOYED fluent-bit-3.1.0 3.1.0 default
|
||||
`,
|
||||
exectest.ListKey{Filter: "^front-proxy$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||
front-proxy 4 Fri Nov 1 08:40:07 2019 DEPLOYED envoy-3.1.0 3.1.0 default
|
||||
`,
|
||||
exectest.ListKey{Filter: "^servicemesh$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||
servicemesh 4 Fri Nov 1 08:40:07 2019 DEPLOYED istio-3.1.0 3.1.0 default
|
||||
`,
|
||||
exectest.ListKey{Filter: "^database$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||
database 4 Fri Nov 1 08:40:07 2019 DEPLOYED mysql-3.1.0 3.1.0 default
|
||||
`,
|
||||
exectest.ListKey{Filter: "^anotherbackend$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||
anotherbackend 4 Fri Nov 1 08:40:07 2019 DEPLOYED anotherbackend-3.1.0 3.1.0 default
|
||||
`,
|
||||
},
|
||||
// Disable concurrency to avoid in-deterministic result
|
||||
concurrency: 1,
|
||||
upgraded: []exectest.Release{},
|
||||
deleted: []exectest.Release{
|
||||
{Name: "frontend-v3", Flags: []string{}},
|
||||
{Name: "frontend-v2", Flags: []string{}},
|
||||
{Name: "frontend-v1", Flags: []string{}},
|
||||
{Name: "backend-v2", Flags: []string{}},
|
||||
{Name: "backend-v1", Flags: []string{}},
|
||||
{Name: "anotherbackend", Flags: []string{}},
|
||||
{Name: "servicemesh", Flags: []string{}},
|
||||
{Name: "database", Flags: []string{}},
|
||||
{Name: "front-proxy", Flags: []string{}},
|
||||
{Name: "logging", Flags: []string{}},
|
||||
},
|
||||
log: `processing file "helmfile.yaml" in directory "."
|
||||
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
|
||||
first-pass uses: &{default map[] map[]}
|
||||
first-pass rendering output of "helmfile.yaml.part.0":
|
||||
0:
|
||||
1: releases:
|
||||
2: - name: database
|
||||
3: chart: charts/mysql
|
||||
4: needs:
|
||||
5: - logging
|
||||
6: - name: frontend-v1
|
||||
7: chart: charts/frontend
|
||||
8: installed: false
|
||||
9: needs:
|
||||
10: - servicemesh
|
||||
11: - logging
|
||||
12: - backend-v1
|
||||
13: - name: frontend-v2
|
||||
14: chart: charts/frontend
|
||||
15: needs:
|
||||
16: - servicemesh
|
||||
17: - logging
|
||||
18: - backend-v2
|
||||
19: - name: frontend-v3
|
||||
20: chart: charts/frontend
|
||||
21: needs:
|
||||
22: - servicemesh
|
||||
23: - logging
|
||||
24: - backend-v2
|
||||
25: - name: backend-v1
|
||||
26: chart: charts/backend
|
||||
27: installed: false
|
||||
28: needs:
|
||||
29: - servicemesh
|
||||
30: - logging
|
||||
31: - database
|
||||
32: - anotherbackend
|
||||
33: - name: backend-v2
|
||||
34: chart: charts/backend
|
||||
35: needs:
|
||||
36: - servicemesh
|
||||
37: - logging
|
||||
38: - database
|
||||
39: - anotherbackend
|
||||
40: - name: anotherbackend
|
||||
41: chart: charts/anotherbackend
|
||||
42: needs:
|
||||
43: - servicemesh
|
||||
44: - logging
|
||||
45: - database
|
||||
46: - name: servicemesh
|
||||
47: chart: charts/istio
|
||||
48: needs:
|
||||
49: - logging
|
||||
50: - name: logging
|
||||
51: chart: charts/fluent-bit
|
||||
52: - name: front-proxy
|
||||
53: chart: stable/envoy
|
||||
54:
|
||||
|
||||
first-pass produced: &{default map[] map[]}
|
||||
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
|
||||
vals:
|
||||
map[]
|
||||
defaultVals:[]
|
||||
second-pass rendering result of "helmfile.yaml.part.0":
|
||||
0:
|
||||
1: releases:
|
||||
2: - name: database
|
||||
3: chart: charts/mysql
|
||||
4: needs:
|
||||
5: - logging
|
||||
6: - name: frontend-v1
|
||||
7: chart: charts/frontend
|
||||
8: installed: false
|
||||
9: needs:
|
||||
10: - servicemesh
|
||||
11: - logging
|
||||
12: - backend-v1
|
||||
13: - name: frontend-v2
|
||||
14: chart: charts/frontend
|
||||
15: needs:
|
||||
16: - servicemesh
|
||||
17: - logging
|
||||
18: - backend-v2
|
||||
19: - name: frontend-v3
|
||||
20: chart: charts/frontend
|
||||
21: needs:
|
||||
22: - servicemesh
|
||||
23: - logging
|
||||
24: - backend-v2
|
||||
25: - name: backend-v1
|
||||
26: chart: charts/backend
|
||||
27: installed: false
|
||||
28: needs:
|
||||
29: - servicemesh
|
||||
30: - logging
|
||||
31: - database
|
||||
32: - anotherbackend
|
||||
33: - name: backend-v2
|
||||
34: chart: charts/backend
|
||||
35: needs:
|
||||
36: - servicemesh
|
||||
37: - logging
|
||||
38: - database
|
||||
39: - anotherbackend
|
||||
40: - name: anotherbackend
|
||||
41: chart: charts/anotherbackend
|
||||
42: needs:
|
||||
43: - servicemesh
|
||||
44: - logging
|
||||
45: - database
|
||||
46: - name: servicemesh
|
||||
47: chart: charts/istio
|
||||
48: needs:
|
||||
49: - logging
|
||||
50: - name: logging
|
||||
51: chart: charts/fluent-bit
|
||||
52: - name: front-proxy
|
||||
53: chart: stable/envoy
|
||||
54:
|
||||
|
||||
merged environment: &{default map[] map[]}
|
||||
10 release(s) found in helmfile.yaml
|
||||
|
||||
processing 5 groups of releases in this order:
|
||||
GROUP RELEASES
|
||||
1 frontend-v3, frontend-v2, frontend-v1
|
||||
2 backend-v2, backend-v1
|
||||
3 anotherbackend
|
||||
4 servicemesh, database
|
||||
5 front-proxy, logging
|
||||
|
||||
processing releases in group 1/5: frontend-v3, frontend-v2, frontend-v1
|
||||
release "frontend-v3" processed
|
||||
release "frontend-v2" processed
|
||||
release "frontend-v1" processed
|
||||
processing releases in group 2/5: backend-v2, backend-v1
|
||||
release "backend-v2" processed
|
||||
release "backend-v1" processed
|
||||
processing releases in group 3/5: anotherbackend
|
||||
release "anotherbackend" processed
|
||||
processing releases in group 4/5: servicemesh, database
|
||||
release "servicemesh" processed
|
||||
release "database" processed
|
||||
processing releases in group 5/5: front-proxy, logging
|
||||
release "front-proxy" processed
|
||||
release "logging" processed
|
||||
|
||||
DELETED RELEASES:
|
||||
NAME
|
||||
frontend-v3
|
||||
frontend-v2
|
||||
frontend-v1
|
||||
backend-v2
|
||||
backend-v1
|
||||
anotherbackend
|
||||
servicemesh
|
||||
database
|
||||
front-proxy
|
||||
logging
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range testcases {
|
||||
tc := testcases[i]
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
wantUpgrades := tc.upgraded
|
||||
wantDeletes := tc.deleted
|
||||
|
||||
var helm = &exectest.Helm{
|
||||
FailOnUnexpectedList: true,
|
||||
FailOnUnexpectedDiff: true,
|
||||
Lists: tc.lists,
|
||||
Diffs: tc.diffs,
|
||||
DiffMutex: &sync.Mutex{},
|
||||
ChartsMutex: &sync.Mutex{},
|
||||
ReleasesMutex: &sync.Mutex{},
|
||||
}
|
||||
|
||||
bs := &bytes.Buffer{}
|
||||
|
||||
func() {
|
||||
logReader, logWriter := io.Pipe()
|
||||
|
||||
logFlushed := &sync.WaitGroup{}
|
||||
// Ensure all the log is consumed into `bs` by calling `logWriter.Close()` followed by `logFlushed.Wait()`
|
||||
logFlushed.Add(1)
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(logReader)
|
||||
for scanner.Scan() {
|
||||
bs.Write(scanner.Bytes())
|
||||
bs.WriteString("\n")
|
||||
}
|
||||
logFlushed.Done()
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
// This is here to avoid data-trace on bytes buffer `bs` to capture logs
|
||||
if err := logWriter.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
logFlushed.Wait()
|
||||
}()
|
||||
|
||||
logger := helmexec.NewLogger(logWriter, "debug")
|
||||
|
||||
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error creating vals runtime: %v", err)
|
||||
}
|
||||
|
||||
app := appWithFs(&App{
|
||||
OverrideHelmBinary: DefaultHelmBinary,
|
||||
glob: filepath.Glob,
|
||||
abs: filepath.Abs,
|
||||
OverrideKubeContext: "default",
|
||||
Env: "default",
|
||||
Logger: logger,
|
||||
helms: map[helmKey]helmexec.Interface{
|
||||
createHelmKey("helm", "default"): helm,
|
||||
},
|
||||
valsRuntime: valsRuntime,
|
||||
}, tc.files)
|
||||
|
||||
if tc.ns != "" {
|
||||
app.Namespace = tc.ns
|
||||
}
|
||||
|
||||
if tc.selectors != nil {
|
||||
app.Selectors = tc.selectors
|
||||
}
|
||||
|
||||
destroyErr := app.Destroy(destroyConfig{
|
||||
// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
|
||||
concurrency: tc.concurrency,
|
||||
logger: logger,
|
||||
})
|
||||
|
||||
if tc.error == "" && destroyErr != nil {
|
||||
t.Fatalf("unexpected error for data defined at %s: %v", tc.loc, destroyErr)
|
||||
} else if tc.error != "" && destroyErr == nil {
|
||||
t.Fatalf("expected error did not occur for data defined at %s", tc.loc)
|
||||
} else if tc.error != "" && destroyErr != nil && tc.error != destroyErr.Error() {
|
||||
t.Fatalf("invalid error: expected %q, got %q", tc.error, destroyErr.Error())
|
||||
}
|
||||
|
||||
if len(wantUpgrades) > len(helm.Releases) {
|
||||
t.Fatalf("insufficient number of upgrades: got %d, want %d", len(helm.Releases), len(wantUpgrades))
|
||||
}
|
||||
|
||||
for relIdx := range wantUpgrades {
|
||||
if wantUpgrades[relIdx].Name != helm.Releases[relIdx].Name {
|
||||
t.Errorf("releases[%d].name: got %q, want %q", relIdx, helm.Releases[relIdx].Name, wantUpgrades[relIdx].Name)
|
||||
}
|
||||
for flagIdx := range wantUpgrades[relIdx].Flags {
|
||||
if wantUpgrades[relIdx].Flags[flagIdx] != helm.Releases[relIdx].Flags[flagIdx] {
|
||||
t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Releases[relIdx].Flags[flagIdx], wantUpgrades[relIdx].Flags[flagIdx])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(wantDeletes) > len(helm.Deleted) {
|
||||
t.Fatalf("insufficient number of deletes: got %d, want %d", len(helm.Deleted), len(wantDeletes))
|
||||
}
|
||||
|
||||
for relIdx := range wantDeletes {
|
||||
if wantDeletes[relIdx].Name != helm.Deleted[relIdx].Name {
|
||||
t.Errorf("releases[%d].name: got %q, want %q", relIdx, helm.Deleted[relIdx].Name, wantDeletes[relIdx].Name)
|
||||
}
|
||||
for flagIdx := range wantDeletes[relIdx].Flags {
|
||||
if wantDeletes[relIdx].Flags[flagIdx] != helm.Deleted[relIdx].Flags[flagIdx] {
|
||||
t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Deleted[relIdx].Flags[flagIdx], wantDeletes[relIdx].Flags[flagIdx])
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if tc.log != "" {
|
||||
actual := bs.String()
|
||||
|
||||
diff, exists := testhelper.Diff(tc.log, actual, 3)
|
||||
if exists {
|
||||
t.Errorf("unexpected log for data defined %s:\nDIFF\n%s\nEOD", tc.loc, diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/gosuri/uitable"
|
||||
)
|
||||
|
||||
func FormatAsTable(releases []*HelmRelease) error {
|
||||
table := uitable.New()
|
||||
table.AddRow("NAME", "NAMESPACE", "ENABLED", "LABELS")
|
||||
|
||||
for _, r := range releases {
|
||||
table.AddRow(r.Name, r.Namespace, fmt.Sprintf("%t", r.Enabled), r.Labels)
|
||||
}
|
||||
|
||||
fmt.Println(table.String())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func FormatAsJson(releases []*HelmRelease) error {
|
||||
output, err := json.Marshal(releases)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating json: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(output))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -9,8 +9,14 @@ type LoadOpts struct {
|
||||
Selectors []string
|
||||
Environment state.SubhelmfileEnvironmentSpec
|
||||
|
||||
RetainValuesFiles bool
|
||||
|
||||
// CalleePath is the absolute path to the file being loaded
|
||||
CalleePath string
|
||||
|
||||
Reverse bool
|
||||
|
||||
Filter bool
|
||||
}
|
||||
|
||||
func (o LoadOpts) DeepCopy() LoadOpts {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package app
|
||||
|
||||
import "github.com/roboll/helmfile/pkg/helmexec"
|
||||
|
||||
type noCallHelmExec struct {
|
||||
}
|
||||
|
||||
type versionOnlyHelmExec struct {
|
||||
*noCallHelmExec
|
||||
isHelm3 bool
|
||||
}
|
||||
|
||||
func (helm *versionOnlyHelmExec) IsHelm3() bool {
|
||||
return helm.isHelm3
|
||||
}
|
||||
|
||||
func (helm *noCallHelmExec) doPanic() {
|
||||
panic("unexpected call to helm")
|
||||
}
|
||||
|
||||
func (helm *noCallHelmExec) TemplateRelease(name, chart string, flags ...string) error {
|
||||
helm.doPanic()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (helm *noCallHelmExec) UpdateDeps(chart string) error {
|
||||
helm.doPanic()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (helm *noCallHelmExec) BuildDeps(name, chart string) error {
|
||||
helm.doPanic()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (helm *noCallHelmExec) SetExtraArgs(args ...string) {
|
||||
helm.doPanic()
|
||||
return
|
||||
}
|
||||
func (helm *noCallHelmExec) SetHelmBinary(bin string) {
|
||||
helm.doPanic()
|
||||
return
|
||||
}
|
||||
func (helm *noCallHelmExec) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string) error {
|
||||
helm.doPanic()
|
||||
return nil
|
||||
}
|
||||
func (helm *noCallHelmExec) UpdateRepo() error {
|
||||
helm.doPanic()
|
||||
return nil
|
||||
}
|
||||
func (helm *noCallHelmExec) SyncRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
|
||||
helm.doPanic()
|
||||
return nil
|
||||
}
|
||||
func (helm *noCallHelmExec) DiffRelease(context helmexec.HelmContext, name, chart string, suppressDiff bool, flags ...string) error {
|
||||
helm.doPanic()
|
||||
return nil
|
||||
}
|
||||
func (helm *noCallHelmExec) ReleaseStatus(context helmexec.HelmContext, release string, flags ...string) error {
|
||||
helm.doPanic()
|
||||
return nil
|
||||
}
|
||||
func (helm *noCallHelmExec) DeleteRelease(context helmexec.HelmContext, name string, flags ...string) error {
|
||||
helm.doPanic()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (helm *noCallHelmExec) List(context helmexec.HelmContext, filter string, flags ...string) (string, error) {
|
||||
helm.doPanic()
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (helm *noCallHelmExec) DecryptSecret(context helmexec.HelmContext, name string, flags ...string) (string, error) {
|
||||
helm.doPanic()
|
||||
return "", nil
|
||||
}
|
||||
func (helm *noCallHelmExec) TestRelease(context helmexec.HelmContext, name string, flags ...string) error {
|
||||
helm.doPanic()
|
||||
return nil
|
||||
}
|
||||
func (helm *noCallHelmExec) Fetch(chart string, flags ...string) error {
|
||||
helm.doPanic()
|
||||
return nil
|
||||
}
|
||||
func (helm *noCallHelmExec) Lint(name, chart string, flags ...string) error {
|
||||
helm.doPanic()
|
||||
return nil
|
||||
}
|
||||
func (helm *noCallHelmExec) IsHelm3() bool {
|
||||
helm.doPanic()
|
||||
return false
|
||||
}
|
||||
|
||||
func (helm *noCallHelmExec) GetVersion() helmexec.Version {
|
||||
helm.doPanic()
|
||||
return helmexec.Version{}
|
||||
}
|
||||
|
||||
func (helm *noCallHelmExec) IsVersionAtLeast(versionStr string) bool {
|
||||
helm.doPanic()
|
||||
return false
|
||||
}
|
||||
+184
-236
@@ -2,11 +2,13 @@ package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/argparser"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Run struct {
|
||||
@@ -14,10 +16,16 @@ type Run struct {
|
||||
helm helmexec.Interface
|
||||
ctx Context
|
||||
|
||||
ReleaseToChart map[string]string
|
||||
|
||||
Ask func(string) bool
|
||||
}
|
||||
|
||||
func NewRun(st *state.HelmState, helm helmexec.Interface, ctx Context) *Run {
|
||||
if helm == nil {
|
||||
panic("Assertion failed: helmexec.Interface must not be nil")
|
||||
}
|
||||
|
||||
return &Run{state: st, helm: helm, ctx: ctx}
|
||||
}
|
||||
|
||||
@@ -28,19 +36,59 @@ func (r *Run) askForConfirmation(msg string) bool {
|
||||
return AskForConfirmation(msg)
|
||||
}
|
||||
|
||||
func (r *Run) Deps(c DepsConfigProvider) []error {
|
||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||
func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepareOptions, f func()) error {
|
||||
if r.ReleaseToChart != nil {
|
||||
panic("Run.PrepareCharts can be called only once")
|
||||
}
|
||||
|
||||
if !c.SkipRepos() {
|
||||
if errs := r.ctx.SyncReposOnce(r.state, r.helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
if !opts.SkipRepos {
|
||||
ctx := r.ctx
|
||||
if err := ctx.SyncReposOnce(r.state, r.helm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create tmp directory and bail immediately if it fails
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
if _, err = r.state.TriggerGlobalPrepareEvent(helmfileCommand); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
releaseToChart, errs := r.state.PrepareCharts(r.helm, dir, 2, helmfileCommand, opts)
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%v", errs)
|
||||
}
|
||||
|
||||
for i := range r.state.Releases {
|
||||
rel := &r.state.Releases[i]
|
||||
|
||||
if chart := releaseToChart[rel.Name]; chart != "" {
|
||||
rel.Chart = chart
|
||||
}
|
||||
}
|
||||
|
||||
r.ReleaseToChart = releaseToChart
|
||||
|
||||
f()
|
||||
|
||||
_, err = r.state.TriggerGlobalCleanupEvent(helmfileCommand)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *Run) Deps(c DepsConfigProvider) []error {
|
||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||
|
||||
return r.state.UpdateDeps(r.helm)
|
||||
}
|
||||
|
||||
func (r *Run) Repos(c ReposConfigProvider) []error {
|
||||
func (r *Run) Repos(c ReposConfigProvider) error {
|
||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||
|
||||
return r.ctx.SyncReposOnce(r.state, r.helm)
|
||||
@@ -64,174 +112,23 @@ func (r *Run) Status(c StatusesConfigProvider) []error {
|
||||
return r.state.ReleaseStatuses(r.helm, workers)
|
||||
}
|
||||
|
||||
func (r *Run) Delete(c DeleteConfigProvider) []error {
|
||||
affectedReleases := state.AffectedReleases{}
|
||||
purge := c.Purge()
|
||||
|
||||
errs := []error{}
|
||||
|
||||
names := make([]string, len(r.state.Releases))
|
||||
for i, r := range r.state.Releases {
|
||||
names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart)
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf(`Affected releases are:
|
||||
%s
|
||||
|
||||
Do you really want to delete?
|
||||
Helmfile will delete all your releases, as shown above.
|
||||
|
||||
`, strings.Join(names, "\n"))
|
||||
interactive := c.Interactive()
|
||||
if !interactive || interactive && r.askForConfirmation(msg) {
|
||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||
|
||||
errs = r.state.DeleteReleases(&affectedReleases, r.helm, c.Concurrency(), purge)
|
||||
}
|
||||
affectedReleases.DisplayAffectedReleases(c.Logger())
|
||||
return errs
|
||||
}
|
||||
|
||||
func (r *Run) Destroy(c DestroyConfigProvider) []error {
|
||||
errs := []error{}
|
||||
affectedReleases := state.AffectedReleases{}
|
||||
|
||||
names := make([]string, len(r.state.Releases))
|
||||
for i, r := range r.state.Releases {
|
||||
names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart)
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf(`Affected releases are:
|
||||
%s
|
||||
|
||||
Do you really want to delete?
|
||||
Helmfile will delete all your releases, as shown above.
|
||||
|
||||
`, strings.Join(names, "\n"))
|
||||
interactive := c.Interactive()
|
||||
if !interactive || interactive && r.askForConfirmation(msg) {
|
||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||
|
||||
errs = r.state.DeleteReleases(&affectedReleases, r.helm, c.Concurrency(), true)
|
||||
}
|
||||
affectedReleases.DisplayAffectedReleases(c.Logger())
|
||||
return errs
|
||||
}
|
||||
|
||||
func (r *Run) Apply(c ApplyConfigProvider) []error {
|
||||
func (a *App) diff(r *Run, c DiffConfigProvider) (*string, bool, bool, []error) {
|
||||
st := r.state
|
||||
helm := r.helm
|
||||
ctx := r.ctx
|
||||
|
||||
affectedReleases := state.AffectedReleases{}
|
||||
if !c.SkipDeps() {
|
||||
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
}
|
||||
if errs := st.PrepareReleases(helm, "apply"); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
allReleases := st.GetReleasesWithOverrides()
|
||||
|
||||
// helm must be 2.11+ and helm-diff should be provided `--detailed-exitcode` in order for `helmfile apply` to work properly
|
||||
detailedExitCode := true
|
||||
|
||||
diffOpts := &state.DiffOpts{
|
||||
NoColor: c.NoColor(),
|
||||
Context: c.Context(),
|
||||
Set: c.Set(),
|
||||
}
|
||||
|
||||
releases, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, c.SuppressSecrets(), false, diffOpts)
|
||||
|
||||
releasesToBeDeleted, err := st.DetectReleasesToBeDeleted(helm)
|
||||
toDiff, err := a.getSelectedReleases(r)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
return nil, false, false, []error{err}
|
||||
}
|
||||
|
||||
fatalErrs := []error{}
|
||||
|
||||
noError := true
|
||||
for _, e := range errs {
|
||||
switch err := e.(type) {
|
||||
case *state.ReleaseError:
|
||||
if err.Code != 2 {
|
||||
noError = false
|
||||
fatalErrs = append(fatalErrs, e)
|
||||
}
|
||||
default:
|
||||
noError = false
|
||||
fatalErrs = append(fatalErrs, e)
|
||||
}
|
||||
if len(toDiff) == 0 {
|
||||
return nil, false, false, nil
|
||||
}
|
||||
|
||||
// sync only when there are changes
|
||||
if noError {
|
||||
if len(releases) == 0 && len(releasesToBeDeleted) == 0 {
|
||||
// TODO better way to get the logger
|
||||
logger := c.Logger()
|
||||
logger.Infof("")
|
||||
logger.Infof("No affected releases")
|
||||
} else {
|
||||
names := []string{}
|
||||
for _, r := range releases {
|
||||
names = append(names, fmt.Sprintf(" %s (%s) UPDATED", r.Name, r.Chart))
|
||||
}
|
||||
for _, r := range releasesToBeDeleted {
|
||||
names = append(names, fmt.Sprintf(" %s (%s) DELETED", r.Name, r.Chart))
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf(`Affected releases are:
|
||||
%s
|
||||
|
||||
Do you really want to apply?
|
||||
Helmfile will apply all your changes, as shown above.
|
||||
|
||||
`, strings.Join(names, "\n"))
|
||||
interactive := c.Interactive()
|
||||
if !interactive || interactive && r.askForConfirmation(msg) {
|
||||
rs := []state.ReleaseSpec{}
|
||||
for _, r := range releases {
|
||||
rs = append(rs, *r)
|
||||
}
|
||||
for _, r := range releasesToBeDeleted {
|
||||
rs = append(rs, *r)
|
||||
}
|
||||
|
||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||
|
||||
st.Releases = rs
|
||||
syncOpts := &state.SyncOpts{
|
||||
Set: c.Set(),
|
||||
}
|
||||
return st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), syncOpts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
affectedReleases.DisplayAffectedReleases(c.Logger())
|
||||
return fatalErrs
|
||||
}
|
||||
|
||||
func (r *Run) Diff(c DiffConfigProvider) []error {
|
||||
st := r.state
|
||||
helm := r.helm
|
||||
ctx := r.ctx
|
||||
|
||||
if !c.SkipDeps() {
|
||||
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
}
|
||||
if errs := st.PrepareReleases(helm, "diff"); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
// Do build deps and prepare only on selected releases so that we won't waste time
|
||||
// on running various helm commands on unnecessary releases
|
||||
st.Releases = toDiff
|
||||
|
||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||
|
||||
@@ -240,93 +137,144 @@ func (r *Run) Diff(c DiffConfigProvider) []error {
|
||||
NoColor: c.NoColor(),
|
||||
Set: c.Set(),
|
||||
}
|
||||
_, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), c.DetailedExitcode(), c.SuppressSecrets(), true, opts)
|
||||
return errs
|
||||
|
||||
// Validate all releases for missing `needs` targets
|
||||
st.Releases = allReleases
|
||||
|
||||
if _, err := st.PlanReleases(false); err != nil {
|
||||
return nil, false, false, []error{err}
|
||||
}
|
||||
|
||||
// Diff only targeted releases
|
||||
|
||||
st.Releases = toDiff
|
||||
|
||||
filtered := &Run{
|
||||
state: st,
|
||||
helm: r.helm,
|
||||
ctx: r.ctx,
|
||||
Ask: r.Ask,
|
||||
}
|
||||
|
||||
infoMsg, updated, deleted, errs := filtered.diff(true, c.DetailedExitcode(), c, opts)
|
||||
|
||||
return infoMsg, true, len(deleted) > 0 || len(updated) > 0, errs
|
||||
}
|
||||
|
||||
func (r *Run) Sync(c SyncConfigProvider) []error {
|
||||
st := r.state
|
||||
helm := r.helm
|
||||
ctx := r.ctx
|
||||
|
||||
affectedReleases := state.AffectedReleases{}
|
||||
if !c.SkipDeps() {
|
||||
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
}
|
||||
if errs := st.PrepareReleases(helm, "sync"); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||
|
||||
opts := &state.SyncOpts{
|
||||
Set: c.Set(),
|
||||
}
|
||||
errs := st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), opts)
|
||||
affectedReleases.DisplayAffectedReleases(c.Logger())
|
||||
return errs
|
||||
}
|
||||
|
||||
func (r *Run) Template(c TemplateConfigProvider) []error {
|
||||
st := r.state
|
||||
helm := r.helm
|
||||
ctx := r.ctx
|
||||
|
||||
if !c.SkipDeps() {
|
||||
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
}
|
||||
if errs := st.PrepareReleases(helm, "template"); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
args := argparser.GetArgs(c.Args(), st)
|
||||
opts := &state.TemplateOpts{
|
||||
Set: c.Set(),
|
||||
}
|
||||
return st.TemplateReleases(helm, c.OutputDir(), c.Values(), args, c.Concurrency(), opts)
|
||||
}
|
||||
|
||||
func (r *Run) Test(c TestConfigProvider) []error {
|
||||
func (a *App) test(r *Run, c TestConfigProvider) []error {
|
||||
cleanup := c.Cleanup()
|
||||
timeout := c.Timeout()
|
||||
concurrency := c.Concurrency()
|
||||
|
||||
st := r.state
|
||||
|
||||
toTest, err := a.getSelectedReleases(r)
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
if len(toTest) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do test only on selected releases, because that's what the user intended
|
||||
// with conditions and selectors
|
||||
st.Releases = toTest
|
||||
|
||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||
|
||||
return r.state.TestReleases(r.helm, cleanup, timeout, concurrency)
|
||||
return st.TestReleases(r.helm, cleanup, timeout, concurrency)
|
||||
}
|
||||
|
||||
func (r *Run) Lint(c LintConfigProvider) []error {
|
||||
st := r.state
|
||||
helm := r.helm
|
||||
ctx := r.ctx
|
||||
|
||||
values := c.Values()
|
||||
args := argparser.GetArgs(c.Args(), st)
|
||||
workers := c.Concurrency()
|
||||
if !c.SkipDeps() {
|
||||
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
}
|
||||
if errs := st.PrepareReleases(helm, "lint"); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
opts := &state.LintOpts{
|
||||
Set: c.Set(),
|
||||
}
|
||||
return st.LintReleases(helm, values, args, workers, opts)
|
||||
}
|
||||
|
||||
func (run *Run) diff(triggerCleanupEvent bool, detailedExitCode bool, c DiffConfigProvider, diffOpts *state.DiffOpts) (*string, map[string]state.ReleaseSpec, map[string]state.ReleaseSpec, []error) {
|
||||
st := run.state
|
||||
helm := run.helm
|
||||
|
||||
var changedReleases []state.ReleaseSpec
|
||||
var deletingReleases []state.ReleaseSpec
|
||||
var planningErrs []error
|
||||
|
||||
// TODO Better way to detect diff on only filtered releases
|
||||
{
|
||||
changedReleases, planningErrs = st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, c.IncludeTests(), c.SuppressSecrets(), c.SuppressDiff(), triggerCleanupEvent, diffOpts)
|
||||
|
||||
var err error
|
||||
deletingReleases, err = st.DetectReleasesToBeDeletedForSync(helm, st.Releases)
|
||||
if err != nil {
|
||||
planningErrs = append(planningErrs, err)
|
||||
}
|
||||
}
|
||||
|
||||
fatalErrs := []error{}
|
||||
|
||||
for _, e := range planningErrs {
|
||||
switch err := e.(type) {
|
||||
case *state.ReleaseError:
|
||||
if err.Code != 2 {
|
||||
fatalErrs = append(fatalErrs, e)
|
||||
}
|
||||
default:
|
||||
fatalErrs = append(fatalErrs, e)
|
||||
}
|
||||
}
|
||||
|
||||
if len(fatalErrs) > 0 {
|
||||
return nil, nil, nil, fatalErrs
|
||||
}
|
||||
|
||||
releasesToBeDeleted := map[string]state.ReleaseSpec{}
|
||||
for _, r := range deletingReleases {
|
||||
id := state.ReleaseToID(&r)
|
||||
releasesToBeDeleted[id] = r
|
||||
}
|
||||
|
||||
releasesToBeUpdated := map[string]state.ReleaseSpec{}
|
||||
for _, r := range changedReleases {
|
||||
id := state.ReleaseToID(&r)
|
||||
|
||||
// If `helm-diff` detected changes but it is not being `helm delete`ed, we should run `helm upgrade`
|
||||
if _, ok := releasesToBeDeleted[id]; !ok {
|
||||
releasesToBeUpdated[id] = r
|
||||
}
|
||||
}
|
||||
|
||||
// sync only when there are changes
|
||||
if len(releasesToBeUpdated) == 0 && len(releasesToBeDeleted) == 0 {
|
||||
var msg *string
|
||||
if c.DetailedExitcode() {
|
||||
// TODO better way to get the logger
|
||||
m := "No affected releases"
|
||||
msg = &m
|
||||
}
|
||||
return msg, nil, nil, nil
|
||||
}
|
||||
|
||||
names := []string{}
|
||||
for _, r := range releasesToBeUpdated {
|
||||
names = append(names, fmt.Sprintf(" %s (%s) UPDATED", r.Name, r.Chart))
|
||||
}
|
||||
for _, r := range releasesToBeDeleted {
|
||||
names = append(names, fmt.Sprintf(" %s (%s) DELETED", r.Name, r.Chart))
|
||||
}
|
||||
// Make the output deterministic for testing purpose
|
||||
sort.Strings(names)
|
||||
|
||||
infoMsg := fmt.Sprintf(`Affected releases are:
|
||||
%s
|
||||
`, strings.Join(names, "\n"))
|
||||
|
||||
return &infoMsg, releasesToBeUpdated, releasesToBeDeleted, nil
|
||||
}
|
||||
|
||||
@@ -31,12 +31,15 @@ func (r *desiredStateLoader) renderPrestate(firstPassEnv *environment.Environmen
|
||||
yamlBuf, err := firstPassRenderer.RenderTemplateContentToBuffer(content)
|
||||
if err != nil && r.logger != nil {
|
||||
r.logger.Debugf("first-pass rendering input of \"%s\":\n%s", filename, prependLineNumbers(string(content)))
|
||||
r.logger.Debugf("template syntax error: %v", err)
|
||||
if yamlBuf == nil { // we have a template syntax error, let the second parse report
|
||||
r.logger.Debugf("template syntax error: %v", err)
|
||||
return firstPassEnv, nil
|
||||
}
|
||||
}
|
||||
yamlData := yamlBuf.String()
|
||||
if r.logger != nil {
|
||||
r.logger.Debugf("first-pass rendering output of \"%s\":\n%s", filename, prependLineNumbers(yamlData))
|
||||
}
|
||||
|
||||
// Work-around for https://github.com/golang/go/issues/24963
|
||||
sanitized := strings.ReplaceAll(yamlData, "<no value>", "")
|
||||
@@ -53,7 +56,7 @@ func (r *desiredStateLoader) renderPrestate(firstPassEnv *environment.Environmen
|
||||
if err != nil && r.logger != nil {
|
||||
switch err.(type) {
|
||||
case *state.StateLoadError:
|
||||
r.logger.Infof("could not deduce `environment:` block, configuring only .Environment.Name. error: %v", err)
|
||||
r.logger.Debugf("could not deduce `environment:` block, configuring only .Environment.Name. error: %v", err)
|
||||
}
|
||||
r.logger.Debugf("error in first-pass rendering: result of \"%s\":\n%s", filename, prependLineNumbers(yamlBuf.String()))
|
||||
}
|
||||
@@ -113,15 +116,13 @@ func (r *desiredStateLoader) twoPassRenderTemplateToYaml(inherited, overrode *en
|
||||
r.logger.Debugf("first-pass rendering result of \"%s\": %v", filename, *finalEnv)
|
||||
}
|
||||
|
||||
vals := map[string]interface{}{}
|
||||
vals, err := finalEnv.GetMergedValues()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if prestate != nil {
|
||||
prestate.Env = *finalEnv
|
||||
vals, err = prestate.Values()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if prestate != nil {
|
||||
r.logger.Debugf("vals:\n%v\ndefaultVals:%v", vals, prestate.DefaultValues)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/roboll/helmfile/pkg/remote"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -14,6 +15,8 @@ import (
|
||||
|
||||
func makeLoader(files map[string]string, env string) (*desiredStateLoader, *testhelper.TestFs) {
|
||||
testfs := testhelper.NewTestFs(files)
|
||||
logger := helmexec.NewLogger(os.Stdout, "debug")
|
||||
r := remote.NewRemote(logger, testfs.Cwd, testfs.ReadFile, testfs.DirectoryExistsAt, testfs.FileExistsAt)
|
||||
return &desiredStateLoader{
|
||||
env: env,
|
||||
namespace: "namespace",
|
||||
@@ -22,6 +25,7 @@ func makeLoader(files map[string]string, env string) (*desiredStateLoader, *test
|
||||
fileExists: testfs.FileExists,
|
||||
abs: testfs.Abs,
|
||||
glob: testfs.Glob,
|
||||
remote: r,
|
||||
}, testfs
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package version
|
||||
|
||||
var Version string
|
||||
@@ -10,7 +10,11 @@ func TestGetArgs(t *testing.T) {
|
||||
args := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false --tiller-namespace ns"
|
||||
defaultArgs := []string{"--recreate-pods", "--force"}
|
||||
Helmdefaults := state.HelmSpec{KubeContext: "test", TillerNamespace: "test-namespace", Args: defaultArgs}
|
||||
testState := &state.HelmState{HelmDefaults: Helmdefaults}
|
||||
testState := &state.HelmState{
|
||||
ReleaseSetSpec: state.ReleaseSetSpec{
|
||||
HelmDefaults: Helmdefaults,
|
||||
},
|
||||
}
|
||||
receivedArgs := GetArgs(args, testState)
|
||||
|
||||
expectedOutput := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false --tiller-namespace ns --recreate-pods --force"
|
||||
@@ -24,7 +28,11 @@ func Test2(t *testing.T) {
|
||||
args := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false,app3.bootstrap=true --tiller-namespace ns"
|
||||
defaultArgs := []string{"--recreate-pods", "--force"}
|
||||
Helmdefaults := state.HelmSpec{KubeContext: "test", TillerNamespace: "test-namespace", Args: defaultArgs}
|
||||
testState := &state.HelmState{HelmDefaults: Helmdefaults}
|
||||
testState := &state.HelmState{
|
||||
ReleaseSetSpec: state.ReleaseSetSpec{
|
||||
HelmDefaults: Helmdefaults,
|
||||
},
|
||||
}
|
||||
receivedArgs := GetArgs(args, testState)
|
||||
|
||||
expectedOutput := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false,app3.bootstrap=true --tiller-namespace ns --recreate-pods --force"
|
||||
|
||||
@@ -58,9 +58,28 @@ func (e *Environment) Merge(other *Environment) (*Environment, error) {
|
||||
}
|
||||
copy := e.DeepCopy()
|
||||
if other != nil {
|
||||
if err := mergo.Merge(©, other, mergo.WithOverride); err != nil {
|
||||
if err := mergo.Merge(©, other, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return ©, nil
|
||||
}
|
||||
|
||||
func (e *Environment) GetMergedValues() (map[string]interface{}, error) {
|
||||
vals := map[string]interface{}{}
|
||||
|
||||
if err := mergo.Merge(&vals, e.Defaults, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := mergo.Merge(&vals, e.Values, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vals, err := maputil.CastKeysToStrings(vals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package environment
|
||||
|
||||
import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// See https://github.com/roboll/helmfile/issues/1150
|
||||
func TestMerge_OverwriteNilValue_Issue1150(t *testing.T) {
|
||||
dst := &Environment{
|
||||
Name: "dst",
|
||||
Values: map[string]interface{}{
|
||||
"components": map[string]interface{}{
|
||||
"etcd-operator": nil,
|
||||
},
|
||||
},
|
||||
Defaults: nil,
|
||||
}
|
||||
|
||||
src := &Environment{
|
||||
Name: "src",
|
||||
Values: map[string]interface{}{
|
||||
"components": map[string]interface{}{
|
||||
"etcd-operator": map[string]interface{}{
|
||||
"version": "0.10.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
Defaults: nil,
|
||||
}
|
||||
|
||||
merged, err := dst.Merge(src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
actual := merged.Values
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"components": map[string]interface{}{
|
||||
"etcd-operator": map[string]interface{}{
|
||||
"version": "0.10.3",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
// See https://github.com/roboll/helmfile/issues/1154
|
||||
func TestMerge_OverwriteWithNilValue_Issue1154(t *testing.T) {
|
||||
dst := &Environment{
|
||||
Name: "dst",
|
||||
Values: map[string]interface{}{
|
||||
"components": map[string]interface{}{
|
||||
"etcd-operator": map[string]interface{}{
|
||||
"version": "0.10.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
Defaults: nil,
|
||||
}
|
||||
|
||||
src := &Environment{
|
||||
Name: "src",
|
||||
Values: map[string]interface{}{
|
||||
"components": map[string]interface{}{
|
||||
"etcd-operator": map[string]interface{}{
|
||||
"version": "0.10.3",
|
||||
},
|
||||
"prometheus": nil,
|
||||
},
|
||||
},
|
||||
Defaults: nil,
|
||||
}
|
||||
|
||||
merged, err := dst.Merge(src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
actual := merged.Values
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"components": map[string]interface{}{
|
||||
"etcd-operator": map[string]interface{}{
|
||||
"version": "0.10.3",
|
||||
},
|
||||
"prometheus": nil,
|
||||
},
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
+7
-4
@@ -20,7 +20,8 @@ type Hook struct {
|
||||
}
|
||||
|
||||
type event struct {
|
||||
Name string
|
||||
Name string
|
||||
Error error
|
||||
}
|
||||
|
||||
type Bus struct {
|
||||
@@ -37,10 +38,11 @@ type Bus struct {
|
||||
Logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func (bus *Bus) Trigger(evt string, context map[string]interface{}) (bool, error) {
|
||||
func (bus *Bus) Trigger(evt string, evtErr error, context map[string]interface{}) (bool, error) {
|
||||
if bus.Runner == nil {
|
||||
bus.Runner = helmexec.ShellRunner{
|
||||
Dir: bus.BasePath,
|
||||
Dir: bus.BasePath,
|
||||
Logger: bus.Logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +70,8 @@ func (bus *Bus) Trigger(evt string, context map[string]interface{}) (bool, error
|
||||
"Environment": bus.Env,
|
||||
"Namespace": bus.Namespace,
|
||||
"Event": event{
|
||||
Name: evt,
|
||||
Name: evt,
|
||||
Error: evtErr,
|
||||
},
|
||||
}
|
||||
for k, v := range context {
|
||||
|
||||
@@ -104,7 +104,7 @@ func TestTrigger(t *testing.T) {
|
||||
"Release": "myrel",
|
||||
"HelmfileCommand": "mycmd",
|
||||
}
|
||||
ok, err := bus.Trigger(c.triggeredEvt, data)
|
||||
ok, err := bus.Trigger(c.triggeredEvt, nil, data)
|
||||
|
||||
if ok != c.expectedResult {
|
||||
t.Errorf("unexpected result for case \"%s\": expected=%v, actual=%v", c.name, c.expectedResult, ok)
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
package exectest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
)
|
||||
|
||||
type ListKey struct {
|
||||
Filter string
|
||||
Flags string
|
||||
}
|
||||
|
||||
type DiffKey struct {
|
||||
Name string
|
||||
Chart string
|
||||
Flags string
|
||||
}
|
||||
|
||||
type Helm struct {
|
||||
Charts []string
|
||||
Repo []string
|
||||
Releases []Release
|
||||
Deleted []Release
|
||||
Lists map[ListKey]string
|
||||
Diffs map[DiffKey]error
|
||||
Diffed []Release
|
||||
FailOnUnexpectedDiff bool
|
||||
FailOnUnexpectedList bool
|
||||
Version *semver.Version
|
||||
|
||||
UpdateDepsCallbacks map[string]func(string) error
|
||||
|
||||
DiffMutex *sync.Mutex
|
||||
ChartsMutex *sync.Mutex
|
||||
ReleasesMutex *sync.Mutex
|
||||
}
|
||||
|
||||
type Release struct {
|
||||
Name string
|
||||
Flags []string
|
||||
}
|
||||
|
||||
type Affected struct {
|
||||
Upgraded []*Release
|
||||
Deleted []*Release
|
||||
Failed []*Release
|
||||
}
|
||||
|
||||
func (helm *Helm) UpdateDeps(chart string) error {
|
||||
if strings.Contains(chart, "error") {
|
||||
return fmt.Errorf("simulated UpdateDeps failure for chart: %s", chart)
|
||||
}
|
||||
helm.Charts = append(helm.Charts, chart)
|
||||
|
||||
if helm.UpdateDepsCallbacks != nil {
|
||||
callback, exists := helm.UpdateDepsCallbacks[chart]
|
||||
if exists {
|
||||
if err := callback(chart); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (helm *Helm) BuildDeps(name, chart string) error {
|
||||
if strings.Contains(chart, "error") {
|
||||
return errors.New("error")
|
||||
}
|
||||
helm.Charts = append(helm.Charts, chart)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (helm *Helm) SetExtraArgs(args ...string) {
|
||||
return
|
||||
}
|
||||
func (helm *Helm) SetHelmBinary(bin string) {
|
||||
return
|
||||
}
|
||||
func (helm *Helm) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string) error {
|
||||
helm.Repo = []string{name, repository, cafile, certfile, keyfile, username, password, managed}
|
||||
return nil
|
||||
}
|
||||
func (helm *Helm) UpdateRepo() error {
|
||||
return nil
|
||||
}
|
||||
func (helm *Helm) SyncRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
|
||||
if strings.Contains(name, "error") {
|
||||
return errors.New("error")
|
||||
}
|
||||
helm.sync(helm.ReleasesMutex, func() {
|
||||
helm.Releases = append(helm.Releases, Release{Name: name, Flags: flags})
|
||||
})
|
||||
helm.sync(helm.ChartsMutex, func() {
|
||||
helm.Charts = append(helm.Charts, chart)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
func (helm *Helm) DiffRelease(context helmexec.HelmContext, name, chart string, suppressDiff bool, flags ...string) error {
|
||||
if helm.DiffMutex != nil {
|
||||
helm.DiffMutex.Lock()
|
||||
}
|
||||
helm.Diffed = append(helm.Diffed, Release{Name: name, Flags: flags})
|
||||
if helm.DiffMutex != nil {
|
||||
helm.DiffMutex.Unlock()
|
||||
}
|
||||
key := DiffKey{Name: name, Chart: chart, Flags: strings.Join(flags, "")}
|
||||
err, ok := helm.Diffs[key]
|
||||
if !ok && helm.FailOnUnexpectedDiff {
|
||||
return fmt.Errorf("unexpected diff with key: %v", key)
|
||||
}
|
||||
return err
|
||||
}
|
||||
func (helm *Helm) ReleaseStatus(context helmexec.HelmContext, release string, flags ...string) error {
|
||||
if strings.Contains(release, "error") {
|
||||
return errors.New("error")
|
||||
}
|
||||
helm.Releases = append(helm.Releases, Release{Name: release, Flags: flags})
|
||||
return nil
|
||||
}
|
||||
func (helm *Helm) DeleteRelease(context helmexec.HelmContext, name string, flags ...string) error {
|
||||
if strings.Contains(name, "error") {
|
||||
return errors.New("error")
|
||||
}
|
||||
helm.Deleted = append(helm.Deleted, Release{Name: name, Flags: flags})
|
||||
return nil
|
||||
}
|
||||
func (helm *Helm) List(context helmexec.HelmContext, filter string, flags ...string) (string, error) {
|
||||
key := ListKey{Filter: filter, Flags: strings.Join(flags, "")}
|
||||
res, ok := helm.Lists[key]
|
||||
if !ok && helm.FailOnUnexpectedList {
|
||||
return "", fmt.Errorf("unexpected list key: %v", key)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
func (helm *Helm) DecryptSecret(context helmexec.HelmContext, name string, flags ...string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (helm *Helm) TestRelease(context helmexec.HelmContext, name string, flags ...string) error {
|
||||
if strings.Contains(name, "error") {
|
||||
return errors.New("error")
|
||||
}
|
||||
helm.Releases = append(helm.Releases, Release{Name: name, Flags: flags})
|
||||
return nil
|
||||
}
|
||||
func (helm *Helm) Fetch(chart string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
func (helm *Helm) Lint(name, chart string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
func (helm *Helm) TemplateRelease(name, chart string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (helm *Helm) IsHelm3() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (helm *Helm) GetVersion() helmexec.Version {
|
||||
return helmexec.Version{
|
||||
Major: int(helm.Version.Major()),
|
||||
Minor: int(helm.Version.Minor()),
|
||||
Patch: int(helm.Version.Patch()),
|
||||
}
|
||||
}
|
||||
|
||||
func (helm *Helm) IsVersionAtLeast(versionStr string) bool {
|
||||
if helm.Version == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
ver := semver.MustParse(versionStr)
|
||||
return helm.Version.Equal(ver) || helm.Version.GreaterThan(ver)
|
||||
}
|
||||
|
||||
func (helm *Helm) sync(m *sync.Mutex, f func()) {
|
||||
if m != nil {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
}
|
||||
|
||||
f()
|
||||
}
|
||||
@@ -8,15 +8,16 @@ import (
|
||||
type HelmContext struct {
|
||||
Tillerless bool
|
||||
TillerNamespace string
|
||||
HistoryMax int
|
||||
WorkerIndex int
|
||||
}
|
||||
|
||||
func (context *HelmContext) GetTillerlessArgs(helmBinary string) []string {
|
||||
if context.Tillerless {
|
||||
func (context *HelmContext) GetTillerlessArgs(helm *execer) []string {
|
||||
if context.Tillerless && !helm.IsHelm3() {
|
||||
if context.TillerNamespace != "" {
|
||||
return []string{"tiller", "run", context.TillerNamespace, "--", helmBinary}
|
||||
return []string{"tiller", "run", context.TillerNamespace, "--", helm.helmBinary}
|
||||
} else {
|
||||
return []string{"tiller", "run", "--", helmBinary}
|
||||
return []string{"tiller", "run", "--", helm.helmBinary}
|
||||
}
|
||||
} else {
|
||||
return []string{}
|
||||
|
||||
+137
-38
@@ -6,17 +6,15 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
const (
|
||||
command = "helm"
|
||||
)
|
||||
|
||||
type decryptedSecret struct {
|
||||
mutex sync.RWMutex
|
||||
bytes []byte
|
||||
@@ -24,6 +22,7 @@ type decryptedSecret struct {
|
||||
|
||||
type execer struct {
|
||||
helmBinary string
|
||||
version semver.Version
|
||||
runner Runner
|
||||
logger *zap.SugaredLogger
|
||||
kubeContext string
|
||||
@@ -49,10 +48,48 @@ func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger {
|
||||
return zap.New(core).Sugar()
|
||||
}
|
||||
|
||||
func parseHelmVersion(versionStr string) (semver.Version, error) {
|
||||
if len(versionStr) == 0 {
|
||||
return semver.Version{}, nil
|
||||
}
|
||||
|
||||
versionStr = strings.TrimLeft(versionStr, "Client: ")
|
||||
versionStr = strings.TrimRight(versionStr, "\n")
|
||||
|
||||
ver, err := semver.NewVersion(versionStr)
|
||||
if err != nil {
|
||||
return semver.Version{}, fmt.Errorf("error parsing helm verion '%s'", versionStr)
|
||||
}
|
||||
|
||||
// Support explicit helm3 opt-in via environment variable
|
||||
if os.Getenv("HELMFILE_HELM3") != "" && ver.Major() < 3 {
|
||||
return *semver.MustParse("v3.0.0"), nil
|
||||
}
|
||||
|
||||
return *ver, nil
|
||||
}
|
||||
|
||||
func getHelmVersion(helmBinary string, runner Runner) (semver.Version, error) {
|
||||
|
||||
// Autodetect from `helm verison`
|
||||
bytes, err := runner.Execute(helmBinary, []string{"version", "--client", "--short"}, nil)
|
||||
if err != nil {
|
||||
return semver.Version{}, fmt.Errorf("error determining helm version: %w", err)
|
||||
}
|
||||
|
||||
return parseHelmVersion(string(bytes))
|
||||
}
|
||||
|
||||
// New for running helm commands
|
||||
func New(logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer {
|
||||
func New(helmBinary string, logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer {
|
||||
// TODO: proper error handling
|
||||
version, err := getHelmVersion(helmBinary, runner)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &execer{
|
||||
helmBinary: command,
|
||||
helmBinary: helmBinary,
|
||||
version: version,
|
||||
logger: logger,
|
||||
kubeContext: kubeContext,
|
||||
runner: runner,
|
||||
@@ -68,20 +105,46 @@ func (helm *execer) SetHelmBinary(bin string) {
|
||||
helm.helmBinary = bin
|
||||
}
|
||||
|
||||
func (helm *execer) AddRepo(name, repository, cafile, certfile, keyfile, username, password string) error {
|
||||
func (helm *execer) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string) error {
|
||||
var args []string
|
||||
args = append(args, "repo", "add", name, repository)
|
||||
if certfile != "" && keyfile != "" {
|
||||
args = append(args, "--cert-file", certfile, "--key-file", keyfile)
|
||||
var out []byte
|
||||
var err error
|
||||
if name == "" && repository != "" {
|
||||
helm.logger.Infof("empty field name\n")
|
||||
return fmt.Errorf("empty field name")
|
||||
}
|
||||
if cafile != "" {
|
||||
args = append(args, "--ca-file", cafile)
|
||||
switch managed {
|
||||
case "acr":
|
||||
helm.logger.Infof("Adding repo %v (acr)", name)
|
||||
out, err = helm.azcli(name)
|
||||
case "":
|
||||
args = append(args, "repo", "add", name, repository)
|
||||
|
||||
// See https://github.com/helm/helm/pull/8777
|
||||
if cons, err := semver.NewConstraint(">= 3.3.2"); err == nil {
|
||||
if cons.Check(&helm.version) {
|
||||
args = append(args, "--force-update")
|
||||
}
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if certfile != "" && keyfile != "" {
|
||||
args = append(args, "--cert-file", certfile, "--key-file", keyfile)
|
||||
}
|
||||
if cafile != "" {
|
||||
args = append(args, "--ca-file", cafile)
|
||||
}
|
||||
if username != "" && password != "" {
|
||||
args = append(args, "--username", username, "--password", password)
|
||||
}
|
||||
helm.logger.Infof("Adding repo %v %v", name, repository)
|
||||
out, err = helm.exec(args, map[string]string{})
|
||||
default:
|
||||
helm.logger.Errorf("ERROR: unknown type '%v' for repository %v", managed, name)
|
||||
out = nil
|
||||
err = nil
|
||||
}
|
||||
if username != "" && password != "" {
|
||||
args = append(args, "--username", username, "--password", password)
|
||||
}
|
||||
helm.logger.Infof("Adding repo %v %v", name, repository)
|
||||
out, err := helm.exec(args, map[string]string{})
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
@@ -109,8 +172,15 @@ func (helm *execer) UpdateDeps(chart string) error {
|
||||
|
||||
func (helm *execer) SyncRelease(context HelmContext, name, chart string, flags ...string) error {
|
||||
helm.logger.Infof("Upgrading release=%v, chart=%v", name, chart)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
preArgs := context.GetTillerlessArgs(helm)
|
||||
env := context.getTillerlessEnv()
|
||||
|
||||
if helm.IsHelm3() {
|
||||
flags = append(flags, "--history-max", strconv.Itoa(context.HistoryMax))
|
||||
} else {
|
||||
env["HELM_TILLER_HISTORY_MAX"] = strconv.Itoa(context.HistoryMax)
|
||||
}
|
||||
|
||||
out, err := helm.exec(append(append(preArgs, "upgrade", "--install", "--reset-values", name, chart), flags...), env)
|
||||
helm.write(out)
|
||||
return err
|
||||
@@ -118,7 +188,7 @@ func (helm *execer) SyncRelease(context HelmContext, name, chart string, flags .
|
||||
|
||||
func (helm *execer) ReleaseStatus(context HelmContext, name string, flags ...string) error {
|
||||
helm.logger.Infof("Getting status %v", name)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
preArgs := context.GetTillerlessArgs(helm)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "status", name), flags...), env)
|
||||
helm.write(out)
|
||||
@@ -127,16 +197,27 @@ func (helm *execer) ReleaseStatus(context HelmContext, name string, flags ...str
|
||||
|
||||
func (helm *execer) List(context HelmContext, filter string, flags ...string) (string, error) {
|
||||
helm.logger.Infof("Listing releases matching %v", filter)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
preArgs := context.GetTillerlessArgs(helm)
|
||||
env := context.getTillerlessEnv()
|
||||
var args []string
|
||||
if helm.isHelm3() {
|
||||
if helm.IsHelm3() {
|
||||
args = []string{"list", "--filter", filter}
|
||||
} else {
|
||||
args = []string{"list", filter}
|
||||
}
|
||||
|
||||
out, err := helm.exec(append(append(preArgs, args...), flags...), env)
|
||||
// In v2 we have been expecting `helm list FILTER` prints nothing.
|
||||
// In v3 helm still prints the header like `NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION`,
|
||||
// which confuses helmfile's existing logic that treats any non-empty output from `helm list` is considered as the indication
|
||||
// of the release to exist.
|
||||
//
|
||||
// This fixes it by removing the header from the v3 output, so that the output is formatted the same as that of v2.
|
||||
if helm.IsHelm3() {
|
||||
lines := strings.Split(string(out), "\n")
|
||||
lines = lines[1:]
|
||||
out = []byte(strings.Join(lines, "\n"))
|
||||
}
|
||||
helm.write(out)
|
||||
return string(out), err
|
||||
}
|
||||
@@ -163,7 +244,7 @@ func (helm *execer) DecryptSecret(context HelmContext, name string, flags ...str
|
||||
helm.decryptedSecretMutex.Unlock()
|
||||
|
||||
helm.logger.Infof("Decrypting secret %v", absPath)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
preArgs := context.GetTillerlessArgs(helm)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "secrets", "dec", absPath), flags...), env)
|
||||
helm.info(out)
|
||||
@@ -212,7 +293,7 @@ func (helm *execer) DecryptSecret(context HelmContext, name string, flags ...str
|
||||
func (helm *execer) TemplateRelease(name string, chart string, flags ...string) error {
|
||||
helm.logger.Infof("Templating release=%v, chart=%v", name, chart)
|
||||
var args []string
|
||||
if helm.isHelm3() {
|
||||
if helm.IsHelm3() {
|
||||
args = []string{"template", name, chart}
|
||||
} else {
|
||||
args = []string{"template", chart, "--name", name}
|
||||
@@ -223,9 +304,9 @@ func (helm *execer) TemplateRelease(name string, chart string, flags ...string)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) DiffRelease(context HelmContext, name, chart string, flags ...string) error {
|
||||
func (helm *execer) DiffRelease(context HelmContext, name, chart string, suppressDiff bool, flags ...string) error {
|
||||
helm.logger.Infof("Comparing release=%v, chart=%v", name, chart)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
preArgs := context.GetTillerlessArgs(helm)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "diff", "upgrade", "--reset-values", "--allow-unreleased", name, chart), flags...), env)
|
||||
// Do our best to write STDOUT only when diff existed
|
||||
@@ -241,11 +322,13 @@ func (helm *execer) DiffRelease(context HelmContext, name, chart string, flags .
|
||||
switch e := err.(type) {
|
||||
case ExitError:
|
||||
if e.ExitStatus() == 2 {
|
||||
helm.write(out)
|
||||
if !(suppressDiff) {
|
||||
helm.write(out)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if !(suppressDiff) {
|
||||
helm.write(out)
|
||||
}
|
||||
return err
|
||||
@@ -267,7 +350,7 @@ func (helm *execer) Fetch(chart string, flags ...string) error {
|
||||
|
||||
func (helm *execer) DeleteRelease(context HelmContext, name string, flags ...string) error {
|
||||
helm.logger.Infof("Deleting %v", name)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
preArgs := context.GetTillerlessArgs(helm)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "delete", name), flags...), env)
|
||||
helm.write(out)
|
||||
@@ -276,14 +359,9 @@ func (helm *execer) DeleteRelease(context HelmContext, name string, flags ...str
|
||||
|
||||
func (helm *execer) TestRelease(context HelmContext, name string, flags ...string) error {
|
||||
helm.logger.Infof("Testing %v", name)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
preArgs := context.GetTillerlessArgs(helm)
|
||||
env := context.getTillerlessEnv()
|
||||
var args []string
|
||||
if helm.isHelm3() {
|
||||
args = []string{"test", "run", name}
|
||||
} else {
|
||||
args = []string{"test", name}
|
||||
}
|
||||
args := []string{"test", name}
|
||||
out, err := helm.exec(append(append(preArgs, args...), flags...), env)
|
||||
helm.write(out)
|
||||
return err
|
||||
@@ -295,11 +373,19 @@ func (helm *execer) exec(args []string, env map[string]string) ([]byte, error) {
|
||||
cmdargs = append(cmdargs, helm.extra...)
|
||||
}
|
||||
if helm.kubeContext != "" {
|
||||
cmdargs = append(cmdargs, "--kube-context", helm.kubeContext)
|
||||
cmdargs = append([]string{"--kube-context", helm.kubeContext}, cmdargs...)
|
||||
}
|
||||
cmd := fmt.Sprintf("exec: %s %s", helm.helmBinary, strings.Join(cmdargs, " "))
|
||||
helm.logger.Debug(cmd)
|
||||
bytes, err := helm.runner.Execute(helm.helmBinary, cmdargs, env)
|
||||
return bytes, err
|
||||
}
|
||||
|
||||
func (helm *execer) azcli(name string) ([]byte, error) {
|
||||
cmdargs := append(strings.Split("acr helm repo add --name", " "), name)
|
||||
cmd := fmt.Sprintf("exec: az %s", strings.Join(cmdargs, " "))
|
||||
helm.logger.Debug(cmd)
|
||||
bytes, err := helm.runner.Execute("az", cmdargs, map[string]string{})
|
||||
helm.logger.Debugf("%s: %s", cmd, bytes)
|
||||
return bytes, err
|
||||
}
|
||||
@@ -316,6 +402,19 @@ func (helm *execer) write(out []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
func (helm *execer) isHelm3() bool {
|
||||
return os.Getenv("HELMFILE_HELM3") != ""
|
||||
func (helm *execer) IsHelm3() bool {
|
||||
return helm.version.Major() == 3
|
||||
}
|
||||
|
||||
func (helm *execer) GetVersion() Version {
|
||||
return Version{
|
||||
Major: int(helm.version.Major()),
|
||||
Minor: int(helm.version.Minor()),
|
||||
Patch: int(helm.version.Patch()),
|
||||
}
|
||||
}
|
||||
|
||||
func (helm *execer) IsVersionAtLeast(versionStr string) bool {
|
||||
ver := semver.MustParse(versionStr)
|
||||
return helm.version.Equal(ver) || helm.version.GreaterThan(ver)
|
||||
}
|
||||
|
||||
+142
-82
@@ -9,6 +9,7 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -20,11 +21,11 @@ type mockRunner struct {
|
||||
}
|
||||
|
||||
func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) {
|
||||
return []byte{}, nil
|
||||
return mock.output, mock.err
|
||||
}
|
||||
|
||||
func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer {
|
||||
execer := New(logger, kubeContext, &mockRunner{})
|
||||
execer := New("helm", logger, kubeContext, &mockRunner{})
|
||||
return execer
|
||||
}
|
||||
|
||||
@@ -32,10 +33,7 @@ func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer {
|
||||
|
||||
func TestNewHelmExec(t *testing.T) {
|
||||
buffer := bytes.NewBufferString("something")
|
||||
logger := NewLogger(buffer, "debug")
|
||||
helm := New(logger, "dev", &ShellRunner{
|
||||
Logger: logger,
|
||||
})
|
||||
helm := MockExecer(NewLogger(buffer, "debug"), "dev")
|
||||
if helm.kubeContext != "dev" {
|
||||
t.Error("helmexec.New() - kubeContext")
|
||||
}
|
||||
@@ -48,11 +46,7 @@ func TestNewHelmExec(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_SetExtraArgs(t *testing.T) {
|
||||
buffer := bytes.NewBufferString("something")
|
||||
logger := NewLogger(buffer, "debug")
|
||||
helm := New(NewLogger(os.Stdout, "info"), "dev", &ShellRunner{
|
||||
Logger: logger,
|
||||
})
|
||||
helm := MockExecer(NewLogger(os.Stdout, "info"), "dev")
|
||||
helm.SetExtraArgs()
|
||||
if len(helm.extra) != 0 {
|
||||
t.Error("helmexec.SetExtraArgs() - passing no arguments should not change extra field")
|
||||
@@ -68,11 +62,7 @@ func Test_SetExtraArgs(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_SetHelmBinary(t *testing.T) {
|
||||
buffer := bytes.NewBufferString("something")
|
||||
logger := NewLogger(buffer, "debug")
|
||||
helm := New(NewLogger(os.Stdout, "info"), "dev", &ShellRunner{
|
||||
Logger: logger,
|
||||
})
|
||||
helm := MockExecer(NewLogger(os.Stdout, "info"), "dev")
|
||||
if helm.helmBinary != "helm" {
|
||||
t.Error("helmexec.command - default command is not helm")
|
||||
}
|
||||
@@ -82,44 +72,86 @@ func Test_SetHelmBinary(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func Test_AddRepo_Helm_3_3_2(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := &execer{
|
||||
helmBinary: "helm",
|
||||
version: *semver.MustParse("3.3.2"),
|
||||
logger: logger,
|
||||
kubeContext: "dev",
|
||||
runner: &mockRunner{},
|
||||
}
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "cert.pem", "key.pem", "", "", "")
|
||||
expected := `Adding repo myRepo https://repo.example.com/
|
||||
exec: helm --kube-context dev repo add myRepo https://repo.example.com/ --force-update --cert-file cert.pem --key-file key.pem
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_AddRepo(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "cert.pem", "key.pem", "", "")
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "cert.pem", "key.pem", "", "", "")
|
||||
expected := `Adding repo myRepo https://repo.example.com/
|
||||
exec: helm repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem --kube-context dev
|
||||
exec: helm repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem --kube-context dev:
|
||||
exec: helm --kube-context dev repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "ca.crt", "", "", "", "")
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "ca.crt", "", "", "", "", "")
|
||||
expected = `Adding repo myRepo https://repo.example.com/
|
||||
exec: helm repo add myRepo https://repo.example.com/ --ca-file ca.crt --kube-context dev
|
||||
exec: helm repo add myRepo https://repo.example.com/ --ca-file ca.crt --kube-context dev:
|
||||
exec: helm --kube-context dev repo add myRepo https://repo.example.com/ --ca-file ca.crt
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "", "")
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "", "", "")
|
||||
expected = `Adding repo myRepo https://repo.example.com/
|
||||
exec: helm repo add myRepo https://repo.example.com/ --kube-context dev
|
||||
exec: helm repo add myRepo https://repo.example.com/ --kube-context dev:
|
||||
exec: helm --kube-context dev repo add myRepo https://repo.example.com/
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "example_user", "example_password")
|
||||
helm.AddRepo("acrRepo", "", "", "", "", "", "", "acr")
|
||||
expected = `Adding repo acrRepo (acr)
|
||||
exec: az acr helm repo add --name acrRepo
|
||||
exec: az acr helm repo add --name acrRepo:
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.AddRepo("otherRepo", "", "", "", "", "", "", "unknown")
|
||||
expected = `ERROR: unknown type 'unknown' for repository otherRepo
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "example_user", "example_password", "")
|
||||
expected = `Adding repo myRepo https://repo.example.com/
|
||||
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password --kube-context dev
|
||||
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password --kube-context dev:
|
||||
exec: helm --kube-context dev repo add myRepo https://repo.example.com/ --username example_user --password example_password
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.AddRepo("", "https://repo.example.com/", "", "", "", "", "", "")
|
||||
expected = `empty field name
|
||||
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -132,8 +164,7 @@ func Test_UpdateRepo(t *testing.T) {
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.UpdateRepo()
|
||||
expected := `Updating repo
|
||||
exec: helm repo update --kube-context dev
|
||||
exec: helm repo update --kube-context dev:
|
||||
exec: helm --kube-context dev repo update
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.UpdateRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -146,8 +177,7 @@ func Test_SyncRelease(t *testing.T) {
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.SyncRelease(HelmContext{}, "release", "chart", "--timeout 10", "--wait")
|
||||
expected := `Upgrading release=release, chart=chart
|
||||
exec: helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev
|
||||
exec: helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev:
|
||||
exec: helm --kube-context dev upgrade --install --reset-values release chart --timeout 10 --wait
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -156,8 +186,7 @@ exec: helm upgrade --install --reset-values release chart --timeout 10 --wait --
|
||||
buffer.Reset()
|
||||
helm.SyncRelease(HelmContext{}, "release", "chart")
|
||||
expected = `Upgrading release=release, chart=chart
|
||||
exec: helm upgrade --install --reset-values release chart --kube-context dev
|
||||
exec: helm upgrade --install --reset-values release chart --kube-context dev:
|
||||
exec: helm --kube-context dev upgrade --install --reset-values release chart
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -171,8 +200,7 @@ func Test_SyncReleaseTillerless(t *testing.T) {
|
||||
helm.SyncRelease(HelmContext{Tillerless: true, TillerNamespace: "foo"}, "release", "chart",
|
||||
"--timeout 10", "--wait")
|
||||
expected := `Upgrading release=release, chart=chart
|
||||
exec: helm tiller run foo -- helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev
|
||||
exec: helm tiller run foo -- helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev:
|
||||
exec: helm --kube-context dev tiller run foo -- helm upgrade --install --reset-values release chart --timeout 10 --wait
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -185,8 +213,7 @@ func Test_UpdateDeps(t *testing.T) {
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.UpdateDeps("./chart/foo")
|
||||
expected := `Updating dependency ./chart/foo
|
||||
exec: helm dependency update ./chart/foo --kube-context dev
|
||||
exec: helm dependency update ./chart/foo --kube-context dev:
|
||||
exec: helm --kube-context dev dependency update ./chart/foo
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.UpdateDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -196,8 +223,7 @@ exec: helm dependency update ./chart/foo --kube-context dev:
|
||||
helm.SetExtraArgs("--verify")
|
||||
helm.UpdateDeps("./chart/foo")
|
||||
expected = `Updating dependency ./chart/foo
|
||||
exec: helm dependency update ./chart/foo --verify --kube-context dev
|
||||
exec: helm dependency update ./chart/foo --verify --kube-context dev:
|
||||
exec: helm --kube-context dev dependency update ./chart/foo --verify
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -210,8 +236,7 @@ func Test_BuildDeps(t *testing.T) {
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.BuildDeps("foo", "./chart/foo")
|
||||
expected := `Building dependency release=foo, chart=./chart/foo
|
||||
exec: helm dependency build ./chart/foo --kube-context dev
|
||||
exec: helm dependency build ./chart/foo --kube-context dev:
|
||||
exec: helm --kube-context dev dependency build ./chart/foo
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -221,8 +246,7 @@ exec: helm dependency build ./chart/foo --kube-context dev:
|
||||
helm.SetExtraArgs("--verify")
|
||||
helm.BuildDeps("foo", "./chart/foo")
|
||||
expected = `Building dependency release=foo, chart=./chart/foo
|
||||
exec: helm dependency build ./chart/foo --verify --kube-context dev
|
||||
exec: helm dependency build ./chart/foo --verify --kube-context dev:
|
||||
exec: helm --kube-context dev dependency build ./chart/foo --verify
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -243,11 +267,10 @@ func Test_DecryptSecret(t *testing.T) {
|
||||
|
||||
expected := fmt.Sprintf(`Preparing to decrypt secret %v/secretName
|
||||
Decrypting secret %s/secretName
|
||||
exec: helm secrets dec %s/secretName --kube-context dev
|
||||
exec: helm secrets dec %s/secretName --kube-context dev:
|
||||
exec: helm --kube-context dev secrets dec %s/secretName
|
||||
Preparing to decrypt secret %s/secretName
|
||||
Found secret in cache %s/secretName
|
||||
`, cwd, cwd, cwd, cwd, cwd, cwd)
|
||||
`, cwd, cwd, cwd, cwd, cwd)
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DecryptSecret()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
@@ -257,20 +280,18 @@ func Test_DiffRelease(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.DiffRelease(HelmContext{}, "release", "chart", "--timeout 10", "--wait")
|
||||
helm.DiffRelease(HelmContext{}, "release", "chart", false, "--timeout 10", "--wait")
|
||||
expected := `Comparing release=release, chart=chart
|
||||
exec: helm diff upgrade --reset-values --allow-unreleased release chart --timeout 10 --wait --kube-context dev
|
||||
exec: helm diff upgrade --reset-values --allow-unreleased release chart --timeout 10 --wait --kube-context dev:
|
||||
exec: helm --kube-context dev diff upgrade --reset-values --allow-unreleased release chart --timeout 10 --wait
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.DiffRelease(HelmContext{}, "release", "chart")
|
||||
helm.DiffRelease(HelmContext{}, "release", "chart", false)
|
||||
expected = `Comparing release=release, chart=chart
|
||||
exec: helm diff upgrade --reset-values --allow-unreleased release chart --kube-context dev
|
||||
exec: helm diff upgrade --reset-values --allow-unreleased release chart --kube-context dev:
|
||||
exec: helm --kube-context dev diff upgrade --reset-values --allow-unreleased release chart
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -281,10 +302,9 @@ func Test_DiffReleaseTillerless(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.DiffRelease(HelmContext{Tillerless: true}, "release", "chart", "--timeout 10", "--wait")
|
||||
helm.DiffRelease(HelmContext{Tillerless: true}, "release", "chart", false, "--timeout 10", "--wait")
|
||||
expected := `Comparing release=release, chart=chart
|
||||
exec: helm tiller run -- helm diff upgrade --reset-values --allow-unreleased release chart --timeout 10 --wait --kube-context dev
|
||||
exec: helm tiller run -- helm diff upgrade --reset-values --allow-unreleased release chart --timeout 10 --wait --kube-context dev:
|
||||
exec: helm --kube-context dev tiller run -- helm diff upgrade --reset-values --allow-unreleased release chart --timeout 10 --wait
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -297,8 +317,7 @@ func Test_DeleteRelease(t *testing.T) {
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.DeleteRelease(HelmContext{}, "release")
|
||||
expected := `Deleting release
|
||||
exec: helm delete release --kube-context dev
|
||||
exec: helm delete release --kube-context dev:
|
||||
exec: helm --kube-context dev delete release
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -310,8 +329,7 @@ func Test_DeleteRelease_Flags(t *testing.T) {
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.DeleteRelease(HelmContext{}, "release", "--purge")
|
||||
expected := `Deleting release
|
||||
exec: helm delete release --purge --kube-context dev
|
||||
exec: helm delete release --purge --kube-context dev:
|
||||
exec: helm --kube-context dev delete release --purge
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -324,8 +342,7 @@ func Test_TestRelease(t *testing.T) {
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.TestRelease(HelmContext{}, "release")
|
||||
expected := `Testing release
|
||||
exec: helm test release --kube-context dev
|
||||
exec: helm test release --kube-context dev:
|
||||
exec: helm --kube-context dev test release
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -337,8 +354,7 @@ func Test_TestRelease_Flags(t *testing.T) {
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.TestRelease(HelmContext{}, "release", "--cleanup", "--timeout", "60")
|
||||
expected := `Testing release
|
||||
exec: helm test release --cleanup --timeout 60 --kube-context dev
|
||||
exec: helm test release --cleanup --timeout 60 --kube-context dev:
|
||||
exec: helm --kube-context dev test release --cleanup --timeout 60
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -351,8 +367,7 @@ func Test_ReleaseStatus(t *testing.T) {
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.ReleaseStatus(HelmContext{}, "myRelease")
|
||||
expected := `Getting status myRelease
|
||||
exec: helm status myRelease --kube-context dev
|
||||
exec: helm status myRelease --kube-context dev:
|
||||
exec: helm --kube-context dev status myRelease
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.ReleaseStatus()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -366,7 +381,6 @@ func Test_exec(t *testing.T) {
|
||||
env := map[string]string{}
|
||||
helm.exec([]string{"version"}, env)
|
||||
expected := `exec: helm version
|
||||
exec: helm version:
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -381,8 +395,7 @@ exec: helm version:
|
||||
buffer.Reset()
|
||||
helm = MockExecer(logger, "dev")
|
||||
helm.exec([]string{"diff", "release", "chart", "--timeout 10", "--wait"}, env)
|
||||
expected = `exec: helm diff release chart --timeout 10 --wait --kube-context dev
|
||||
exec: helm diff release chart --timeout 10 --wait --kube-context dev:
|
||||
expected = `exec: helm --kube-context dev diff release chart --timeout 10 --wait
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -390,8 +403,7 @@ exec: helm diff release chart --timeout 10 --wait --kube-context dev:
|
||||
|
||||
buffer.Reset()
|
||||
helm.exec([]string{"version"}, env)
|
||||
expected = `exec: helm version --kube-context dev
|
||||
exec: helm version --kube-context dev:
|
||||
expected = `exec: helm --kube-context dev version
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -400,8 +412,7 @@ exec: helm version --kube-context dev:
|
||||
buffer.Reset()
|
||||
helm.SetExtraArgs("foo")
|
||||
helm.exec([]string{"version"}, env)
|
||||
expected = `exec: helm version foo --kube-context dev
|
||||
exec: helm version foo --kube-context dev:
|
||||
expected = `exec: helm --kube-context dev version foo
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -412,7 +423,6 @@ exec: helm version foo --kube-context dev:
|
||||
helm.SetHelmBinary("overwritten")
|
||||
helm.exec([]string{"version"}, env)
|
||||
expected = `exec: overwritten version
|
||||
exec: overwritten version:
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -425,8 +435,7 @@ func Test_Lint(t *testing.T) {
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.Lint("release", "path/to/chart", "--values", "file.yml")
|
||||
expected := `Linting release=release, chart=path/to/chart
|
||||
exec: helm lint path/to/chart --values file.yml --kube-context dev
|
||||
exec: helm lint path/to/chart --values file.yml --kube-context dev:
|
||||
exec: helm --kube-context dev lint path/to/chart --values file.yml
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -439,8 +448,7 @@ func Test_Fetch(t *testing.T) {
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.Fetch("chart", "--version", "1.2.3", "--untar", "--untardir", "/tmp/dir")
|
||||
expected := `Fetching chart
|
||||
exec: helm fetch chart --version 1.2.3 --untar --untardir /tmp/dir --kube-context dev
|
||||
exec: helm fetch chart --version 1.2.3 --untar --untardir /tmp/dir --kube-context dev:
|
||||
exec: helm --kube-context dev fetch chart --version 1.2.3 --untar --untardir /tmp/dir
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -450,7 +458,6 @@ exec: helm fetch chart --version 1.2.3 --untar --untardir /tmp/dir --kube-contex
|
||||
var logLevelTests = map[string]string{
|
||||
"debug": `Adding repo myRepo https://repo.example.com/
|
||||
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password
|
||||
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password:
|
||||
`,
|
||||
"info": `Adding repo myRepo https://repo.example.com/
|
||||
`,
|
||||
@@ -463,7 +470,7 @@ func Test_LogLevels(t *testing.T) {
|
||||
buffer.Reset()
|
||||
logger := NewLogger(&buffer, logLevel)
|
||||
helm := MockExecer(logger, "")
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "example_user", "example_password")
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "example_user", "example_password", "")
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
@@ -510,10 +517,63 @@ func Test_Template(t *testing.T) {
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.TemplateRelease("release", "path/to/chart", "--values", "file.yml")
|
||||
expected := `Templating release=release, chart=path/to/chart
|
||||
exec: helm template path/to/chart --name release --values file.yml --kube-context dev
|
||||
exec: helm template path/to/chart --name release --values file.yml --kube-context dev:
|
||||
exec: helm --kube-context dev template path/to/chart --name release --values file.yml
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.Template()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_IsHelm3(t *testing.T) {
|
||||
helm2Runner := mockRunner{output: []byte("Client: v2.16.0+ge13bc94\n")}
|
||||
helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &helm2Runner)
|
||||
if helm.IsHelm3() {
|
||||
t.Error("helmexec.IsHelm3() - Detected Helm 3 with Helm 2 version")
|
||||
}
|
||||
|
||||
helm3Runner := mockRunner{output: []byte("v3.0.0+ge29ce2a\n")}
|
||||
helm = New("helm", NewLogger(os.Stdout, "info"), "dev", &helm3Runner)
|
||||
if !helm.IsHelm3() {
|
||||
t.Error("helmexec.IsHelm3() - Failed to detect Helm 3")
|
||||
}
|
||||
|
||||
os.Setenv("HELMFILE_HELM3", "1")
|
||||
helm2Runner = mockRunner{output: []byte("Client: v2.16.0+ge13bc94\n")}
|
||||
helm = New("helm", NewLogger(os.Stdout, "info"), "dev", &helm2Runner)
|
||||
if !helm.IsHelm3() {
|
||||
t.Error("helmexec.IsHelm3() - Helm3 not detected when HELMFILE_HELM3 is set")
|
||||
}
|
||||
os.Setenv("HELMFILE_HELM3", "")
|
||||
}
|
||||
|
||||
func Test_GetVersion(t *testing.T) {
|
||||
helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94\n")}
|
||||
helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &helm2Runner)
|
||||
ver := helm.GetVersion()
|
||||
if ver.Major != 2 || ver.Minor != 16 || ver.Patch != 1 {
|
||||
t.Error(fmt.Sprintf("helmexec.GetVersion - did not detect correct Helm2 version; it was: %+v", ver))
|
||||
}
|
||||
|
||||
helm3Runner := mockRunner{output: []byte("v3.2.4+ge29ce2a\n")}
|
||||
helm = New("helm", NewLogger(os.Stdout, "info"), "dev", &helm3Runner)
|
||||
ver = helm.GetVersion()
|
||||
if ver.Major != 3 || ver.Minor != 2 || ver.Patch != 4 {
|
||||
t.Error(fmt.Sprintf("helmexec.GetVersion - did not detect correct Helm3 version; it was: %+v", ver))
|
||||
}
|
||||
}
|
||||
|
||||
func Test_IsVersionAtLeast(t *testing.T) {
|
||||
helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94\n")}
|
||||
helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &helm2Runner)
|
||||
if !helm.IsVersionAtLeast("2.1.0") {
|
||||
t.Error("helmexec.IsVersionAtLeast - 2.16.1 not atleast 2.1")
|
||||
}
|
||||
|
||||
if helm.IsVersionAtLeast("2.19.0") {
|
||||
t.Error("helmexec.IsVersionAtLeast - 2.16.1 is atleast 2.19")
|
||||
}
|
||||
|
||||
if helm.IsVersionAtLeast("3.2.0") {
|
||||
t.Error("helmexec.IsVersionAtLeast - 2.16.1 is atleast 3.2")
|
||||
}
|
||||
}
|
||||
|
||||
+47
-12
@@ -2,35 +2,70 @@ package helmexec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func newExitError(helmCmdPath string, exitStatus int, errorMessage string) ExitError {
|
||||
func newExitError(path string, args []string, exitStatus int, err error, stderr, combined string) ExitError {
|
||||
var out string
|
||||
|
||||
out += fmt.Sprintf("PATH:\n%s", Indent(path, " "))
|
||||
|
||||
out += "\n\nARGS:"
|
||||
for i, a := range args {
|
||||
out += fmt.Sprintf("\n%s", Indent(fmt.Sprintf("%d: %s (%d bytes)", i, a, len(a)), " "))
|
||||
}
|
||||
|
||||
out += fmt.Sprintf("\n\nERROR:\n%s", Indent(err.Error(), " "))
|
||||
|
||||
out += fmt.Sprintf("\n\nEXIT STATUS\n%s", Indent(fmt.Sprintf("%d", exitStatus), " "))
|
||||
|
||||
if len(stderr) > 0 {
|
||||
out += fmt.Sprintf("\n\nSTDERR:\n%s", Indent(stderr, " "))
|
||||
}
|
||||
|
||||
if len(combined) > 0 {
|
||||
out += fmt.Sprintf("\n\nCOMBINED OUTPUT:\n%s", Indent(combined, " "))
|
||||
}
|
||||
|
||||
return ExitError{
|
||||
msg: fmt.Sprintf("%s exited with status %d:\n%s", filepath.Base(helmCmdPath), exitStatus, indent(strings.TrimSpace(errorMessage))),
|
||||
exitStatus: exitStatus,
|
||||
Message: fmt.Sprintf("command %q exited with non-zero status:\n\n%s", path, out),
|
||||
Code: exitStatus,
|
||||
}
|
||||
}
|
||||
|
||||
func indent(text string) string {
|
||||
// indents a block of text with an indent string
|
||||
func Indent(text, indent string) string {
|
||||
var b strings.Builder
|
||||
|
||||
b.Grow(len(text) * 2)
|
||||
|
||||
lines := strings.Split(text, "\n")
|
||||
for i := range lines {
|
||||
lines[i] = " " + lines[i]
|
||||
|
||||
last := len(lines) - 1
|
||||
|
||||
for i, j := range lines {
|
||||
if i > 0 && i < last && j != "" {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
if j != "" {
|
||||
b.WriteString(indent + j)
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ExitError is created whenever your shell command exits with a non-zero exit status
|
||||
type ExitError struct {
|
||||
msg string
|
||||
exitStatus int
|
||||
Message string
|
||||
Code int
|
||||
}
|
||||
|
||||
func (e ExitError) Error() string {
|
||||
return e.msg
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func (e ExitError) ExitStatus() int {
|
||||
return e.exitStatus
|
||||
return e.Code
|
||||
}
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
package helmexec
|
||||
|
||||
// Version represents the version of helm
|
||||
type Version struct {
|
||||
Major int
|
||||
Minor int
|
||||
Patch int
|
||||
}
|
||||
|
||||
// Interface for executing helm commands
|
||||
type Interface interface {
|
||||
SetExtraArgs(args ...string)
|
||||
SetHelmBinary(bin string)
|
||||
|
||||
AddRepo(name, repository, cafile, certfile, keyfile, username, password string) error
|
||||
AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string) error
|
||||
UpdateRepo() error
|
||||
BuildDeps(name, chart string) error
|
||||
UpdateDeps(chart string) error
|
||||
SyncRelease(context HelmContext, name, chart string, flags ...string) error
|
||||
DiffRelease(context HelmContext, name, chart string, flags ...string) error
|
||||
DiffRelease(context HelmContext, name, chart string, suppressDiff bool, flags ...string) error
|
||||
TemplateRelease(name, chart string, flags ...string) error
|
||||
Fetch(chart string, flags ...string) error
|
||||
Lint(name, chart string, flags ...string) error
|
||||
@@ -19,8 +26,12 @@ type Interface interface {
|
||||
TestRelease(context HelmContext, name string, flags ...string) error
|
||||
List(context HelmContext, filter string, flags ...string) (string, error)
|
||||
DecryptSecret(context HelmContext, name string, flags ...string) (string, error)
|
||||
IsHelm3() bool
|
||||
GetVersion() Version
|
||||
IsVersionAtLeast(versionStr string) bool
|
||||
}
|
||||
|
||||
type DependencyUpdater interface {
|
||||
UpdateDeps(chart string) error
|
||||
IsHelm3() bool
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package helmexec
|
||||
|
||||
import "math/rand"
|
||||
|
||||
var executionIDComponents = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
func newExecutionID() string {
|
||||
b := make([]rune, 5)
|
||||
for i := range b {
|
||||
b[i] = executionIDComponents[rand.Intn(len(executionIDComponents))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type logWriterGenerator struct {
|
||||
log *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func (g logWriterGenerator) Writer(prefix string) *logWriter {
|
||||
return &logWriter{
|
||||
log: g.log,
|
||||
prefix: prefix,
|
||||
}
|
||||
}
|
||||
|
||||
type logWriter struct {
|
||||
log *zap.SugaredLogger
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (w *logWriter) Write(p []byte) (int, error) {
|
||||
w.log.Debugf("%s%s", w.prefix, strings.TrimSpace(string(p)))
|
||||
return len(p), nil
|
||||
}
|
||||
+34
-11
@@ -4,11 +4,14 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -33,24 +36,35 @@ func (shell ShellRunner) Execute(cmd string, args []string, env map[string]strin
|
||||
preparedCmd := exec.Command(cmd, args...)
|
||||
preparedCmd.Dir = shell.Dir
|
||||
preparedCmd.Env = mergeEnv(os.Environ(), env)
|
||||
return combinedOutput(preparedCmd, shell.Logger)
|
||||
return Output(preparedCmd, &logWriterGenerator{
|
||||
log: shell.Logger,
|
||||
})
|
||||
}
|
||||
|
||||
func combinedOutput(c *exec.Cmd, logger *zap.SugaredLogger) ([]byte, error) {
|
||||
func Output(c *exec.Cmd, logWriterGenerators ...*logWriterGenerator) ([]byte, error) {
|
||||
if c.Stdout != nil {
|
||||
return nil, errors.New("exec: Stdout already set")
|
||||
}
|
||||
if c.Stderr != nil {
|
||||
return nil, errors.New("exec: Stderr already set")
|
||||
}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
c.Stdout = &stdout
|
||||
c.Stderr = &stderr
|
||||
err := c.Run()
|
||||
var combined bytes.Buffer
|
||||
|
||||
o := stdout.Bytes()
|
||||
e := stderr.Bytes()
|
||||
var logWriters []io.Writer
|
||||
|
||||
id := newExecutionID()
|
||||
for _, g := range logWriterGenerators {
|
||||
logPrefix := fmt.Sprintf("%s:%s> ", filepath.Base(c.Path), id)
|
||||
logWriters = append(logWriters, g.Writer(logPrefix))
|
||||
}
|
||||
|
||||
c.Stdout = io.MultiWriter(append([]io.Writer{&stdout, &combined}, logWriters...)...)
|
||||
c.Stderr = io.MultiWriter(append([]io.Writer{&stderr, &combined}, logWriters...)...)
|
||||
|
||||
err := c.Run()
|
||||
|
||||
if err != nil {
|
||||
// TrimSpace is necessary, because otherwise helmfile prints the redundant new-lines after each error like:
|
||||
@@ -64,13 +78,13 @@ func combinedOutput(c *exec.Cmd, logger *zap.SugaredLogger) ([]byte, error) {
|
||||
// so that helmfile could return its own exit code accordingly
|
||||
waitStatus := ee.Sys().(syscall.WaitStatus)
|
||||
exitStatus := waitStatus.ExitStatus()
|
||||
err = newExitError(c.Path, exitStatus, string(e))
|
||||
err = newExitError(c.Path, c.Args, exitStatus, ee, stderr.String(), combined.String())
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected error: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
return o, err
|
||||
return stdout.Bytes(), err
|
||||
}
|
||||
|
||||
func mergeEnv(orig []string, new map[string]string) []string {
|
||||
@@ -93,7 +107,16 @@ func env2map(env []string) map[string]string {
|
||||
wanted := map[string]string{}
|
||||
for _, cur := range env {
|
||||
pair := strings.SplitN(cur, "=", 2)
|
||||
wanted[pair[0]] = pair[1]
|
||||
|
||||
var v string
|
||||
|
||||
// An environment can completely miss `=` and the right side.
|
||||
// If we didn't deal with that, this may fail due to an index-out-of-range error
|
||||
if len(pair) > 1 {
|
||||
v = pair[1]
|
||||
}
|
||||
|
||||
wanted[pair[0]] = v
|
||||
}
|
||||
return wanted
|
||||
}
|
||||
|
||||
+123
-23
@@ -1,6 +1,9 @@
|
||||
package maputil
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CastKeysToStrings(s interface{}) (map[string]interface{}, error) {
|
||||
new := map[string]interface{}{}
|
||||
@@ -60,32 +63,129 @@ func recursivelyStringifyMapKey(v interface{}) (interface{}, error) {
|
||||
return casted_v, nil
|
||||
}
|
||||
|
||||
func Set(m map[string]interface{}, key []string, value string) map[string]interface{} {
|
||||
type arg interface {
|
||||
getMap(map[string]interface{}) map[string]interface{}
|
||||
set(map[string]interface{}, string)
|
||||
}
|
||||
|
||||
type keyArg struct {
|
||||
key string
|
||||
}
|
||||
|
||||
func (a keyArg) getMap(m map[string]interface{}) map[string]interface{} {
|
||||
_, ok := m[a.key]
|
||||
if !ok {
|
||||
m[a.key] = map[string]interface{}{}
|
||||
}
|
||||
switch t := m[a.key].(type) {
|
||||
case map[string]interface{}:
|
||||
return t
|
||||
default:
|
||||
panic(fmt.Errorf("unexpected type: %v(%T)", t, t))
|
||||
}
|
||||
}
|
||||
|
||||
func (a keyArg) set(m map[string]interface{}, value string) {
|
||||
m[a.key] = value
|
||||
}
|
||||
|
||||
type indexedKeyArg struct {
|
||||
key string
|
||||
index int
|
||||
}
|
||||
|
||||
func (a indexedKeyArg) getArray(m map[string]interface{}) []interface{} {
|
||||
_, ok := m[a.key]
|
||||
if !ok {
|
||||
m[a.key] = make([]interface{}, a.index+1)
|
||||
}
|
||||
switch t := m[a.key].(type) {
|
||||
case []interface{}:
|
||||
if len(t) <= a.index {
|
||||
t2 := make([]interface{}, a.index+1)
|
||||
copy(t, t2)
|
||||
t = t2
|
||||
}
|
||||
return t
|
||||
default:
|
||||
panic(fmt.Errorf("unexpected type: %v(%T)", t, t))
|
||||
}
|
||||
}
|
||||
|
||||
func (a indexedKeyArg) getMap(m map[string]interface{}) map[string]interface{} {
|
||||
t := a.getArray(m)
|
||||
if t[a.index] == nil {
|
||||
t[a.index] = map[string]interface{}{}
|
||||
}
|
||||
switch t := t[a.index].(type) {
|
||||
case map[string]interface{}:
|
||||
return t
|
||||
default:
|
||||
panic(fmt.Errorf("unexpected type: %v(%T)", t, t))
|
||||
}
|
||||
}
|
||||
|
||||
func (a indexedKeyArg) set(m map[string]interface{}, value string) {
|
||||
t := a.getArray(m)
|
||||
t[a.index] = value
|
||||
m[a.key] = t
|
||||
}
|
||||
|
||||
func getCursor(key string) arg {
|
||||
key = strings.ReplaceAll(key, "[", " ")
|
||||
key = strings.ReplaceAll(key, "]", " ")
|
||||
k := key
|
||||
idx := 0
|
||||
|
||||
n, err := fmt.Sscanf(key, "%s %d", &k, &idx)
|
||||
|
||||
if n == 2 && err == nil {
|
||||
return indexedKeyArg{
|
||||
key: k,
|
||||
index: idx,
|
||||
}
|
||||
}
|
||||
|
||||
return keyArg{
|
||||
key: key,
|
||||
}
|
||||
}
|
||||
|
||||
func ParseKey(key string) []string {
|
||||
r := []string{}
|
||||
part := ""
|
||||
escaped := false
|
||||
for _, rune := range key {
|
||||
split := false
|
||||
switch {
|
||||
case escaped == false && rune == '\\':
|
||||
escaped = true
|
||||
continue
|
||||
case rune == '.':
|
||||
split = escaped == false
|
||||
}
|
||||
escaped = false
|
||||
if split {
|
||||
r = append(r, part)
|
||||
part = ""
|
||||
} else {
|
||||
part = part + string(rune)
|
||||
}
|
||||
}
|
||||
if len(part) > 0 {
|
||||
r = append(r, part)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func Set(m map[string]interface{}, key []string, value string) {
|
||||
if len(key) == 0 {
|
||||
panic(fmt.Errorf("bug: unexpected length of key: %d", len(key)))
|
||||
}
|
||||
|
||||
k := key[0]
|
||||
|
||||
if len(key) == 1 {
|
||||
m[k] = value
|
||||
return m
|
||||
for len(key) > 1 {
|
||||
m, key = getCursor(key[0]).getMap(m), key[1:]
|
||||
}
|
||||
|
||||
remain := key[1:]
|
||||
|
||||
nested, ok := m[k]
|
||||
if !ok {
|
||||
nested = map[string]interface{}{}
|
||||
}
|
||||
switch t := nested.(type) {
|
||||
case map[string]interface{}:
|
||||
nested = Set(t, remain, value)
|
||||
default:
|
||||
panic(fmt.Errorf("unexpected type: %v(%T)", t, t))
|
||||
}
|
||||
|
||||
m[k] = nested
|
||||
|
||||
return m
|
||||
getCursor(key[0]).set(m, value)
|
||||
}
|
||||
|
||||
@@ -59,3 +59,72 @@ func TestMapUtil_IFKeys(t *testing.T) {
|
||||
t.Errorf("unexpected c: expected=C, got=%s", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapUtil_KeyArg(t *testing.T) {
|
||||
m := map[string]interface{}{}
|
||||
|
||||
key := []string{"a", "b", "c"}
|
||||
|
||||
Set(m, key, "C")
|
||||
|
||||
c := (((m["a"].(map[string]interface{}))["b"]).(map[string]interface{}))["c"]
|
||||
|
||||
if c != "C" {
|
||||
t.Errorf("unexpected c: expected=C, got=%s", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapUtil_IndexedKeyArg(t *testing.T) {
|
||||
m := map[string]interface{}{}
|
||||
|
||||
key := []string{"a", "b[0]", "c"}
|
||||
|
||||
Set(m, key, "C")
|
||||
|
||||
c := (((m["a"].(map[string]interface{}))["b"].([]interface{}))[0].(map[string]interface{}))["c"]
|
||||
|
||||
if c != "C" {
|
||||
t.Errorf("unexpected c: expected=C, got=%s", c)
|
||||
}
|
||||
}
|
||||
|
||||
type parseKeyTc struct {
|
||||
key string
|
||||
result map[int]string
|
||||
}
|
||||
|
||||
func TestMapUtil_ParseKey(t *testing.T) {
|
||||
tcs := []parseKeyTc{
|
||||
{
|
||||
key: `a.b.c`,
|
||||
result: map[int]string{
|
||||
0: "a",
|
||||
1: "b",
|
||||
2: "c",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: `a\.b.c`,
|
||||
result: map[int]string{
|
||||
0: "a.b",
|
||||
1: "c",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: `a\.b\.c`,
|
||||
result: map[int]string{
|
||||
0: "a.b.c",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
parts := ParseKey(tc.key)
|
||||
|
||||
for index, value := range tc.result {
|
||||
if parts[index] != value {
|
||||
t.Errorf("unexpected key part[%d]: expected=%s, got=%s", index, value, parts[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"github.com/variantdev/vals"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// cache size for improving performance of ref+.* secrets rendering
|
||||
valsCacheSize = 512
|
||||
)
|
||||
|
||||
var instance *vals.Runtime
|
||||
var once sync.Once
|
||||
|
||||
func ValsInstance() (*vals.Runtime, error) {
|
||||
var err error
|
||||
once.Do(func() {
|
||||
instance, err = vals.New(vals.Options{CacheSize: valsCacheSize})
|
||||
})
|
||||
|
||||
return instance, err
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package plugins
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValsInstance(t *testing.T) {
|
||||
i, err := ValsInstance()
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
i2, _ := ValsInstance()
|
||||
|
||||
if i != i2 {
|
||||
t.Error("Instances should be equal")
|
||||
}
|
||||
}
|
||||
@@ -260,3 +260,15 @@ func (g *GoGetter) Get(wd, src, dst string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewRemote(logger *zap.SugaredLogger, homeDir string, readFile func(string) ([]byte, error), dirExists func(string) bool, fileExists func(string) bool) *Remote {
|
||||
remote := &Remote{
|
||||
Logger: logger,
|
||||
Home: homeDir,
|
||||
Getter: &GoGetter{Logger: logger},
|
||||
ReadFile: readFile,
|
||||
DirExists: dirExists,
|
||||
FileExists: fileExists,
|
||||
}
|
||||
return remote
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package remote
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/testhelper"
|
||||
"os"
|
||||
@@ -156,6 +157,75 @@ func TestRemote_SShGitHub(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
type testcase struct {
|
||||
input string
|
||||
getter, scheme, dir, file, query string
|
||||
err string
|
||||
}
|
||||
|
||||
testcases := []testcase{
|
||||
{
|
||||
input: "raw/incubator",
|
||||
err: "parse url: missing scheme - probably this is a local file path? raw/incubator",
|
||||
},
|
||||
{
|
||||
input: "git::https://github.com/stakater/Forecastle.git@deployments/kubernetes/chart/forecastle?ref=v1.0.54",
|
||||
getter: "git",
|
||||
scheme: "https",
|
||||
dir: "/stakater/Forecastle.git",
|
||||
file: "deployments/kubernetes/chart/forecastle",
|
||||
query: "ref=v1.0.54",
|
||||
},
|
||||
}
|
||||
|
||||
for i := range testcases {
|
||||
tc := testcases[i]
|
||||
|
||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
||||
src, err := Parse(tc.input)
|
||||
|
||||
var errMsg string
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tc.err, errMsg); diff != "" {
|
||||
t.Fatalf("Unexpected error:\n%s", diff)
|
||||
}
|
||||
|
||||
var getter, scheme, dir, file, query string
|
||||
if src != nil {
|
||||
getter = src.Getter
|
||||
scheme = src.Scheme
|
||||
dir = src.Dir
|
||||
file = src.File
|
||||
query = src.RawQuery
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tc.getter, getter); diff != "" {
|
||||
t.Fatalf("Unexpected getter:\n%s", diff)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tc.scheme, scheme); diff != "" {
|
||||
t.Fatalf("Unexpected scheme:\n%s", diff)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tc.file, file); diff != "" {
|
||||
t.Fatalf("Unexpected file:\n%s", diff)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tc.dir, dir); diff != "" {
|
||||
t.Fatalf("Unexpected dir:\n%s", diff)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tc.query, query); diff != "" {
|
||||
t.Fatalf("Unexpected query:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testGetter struct {
|
||||
get func(wd, src, dst string) error
|
||||
}
|
||||
|
||||
@@ -2,16 +2,19 @@ package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Masterminds/semver"
|
||||
"github.com/r3labs/diff"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
goversion "github.com/hashicorp/go-version"
|
||||
"github.com/r3labs/diff"
|
||||
"github.com/roboll/helmfile/pkg/app/version"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type ChartMeta struct {
|
||||
@@ -50,6 +53,7 @@ type ChartRequirements struct {
|
||||
}
|
||||
|
||||
type ChartLockedRequirements struct {
|
||||
Version string `yaml:"version"`
|
||||
ResolvedDependencies []ResolvedChartDependency `yaml:"dependencies"`
|
||||
Digest string `yaml:"digest"`
|
||||
Generated string `yaml:"generated"`
|
||||
@@ -251,7 +255,7 @@ func updateDependencies(st *HelmState, shell helmexec.DependencyUpdater, unresol
|
||||
|
||||
_, err := depMan.Update(shell, wd, unresolved)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to resolve %d deps: %v", len(unresolved.deps), err)
|
||||
return nil, fmt.Errorf("unable to update %d deps: %v", len(unresolved.deps), err)
|
||||
}
|
||||
|
||||
return resolveDependencies(st, depMan, unresolved)
|
||||
@@ -280,7 +284,7 @@ func (m *chartDependencyManager) lockFileName() string {
|
||||
}
|
||||
|
||||
func (m *chartDependencyManager) Update(shell helmexec.DependencyUpdater, wd string, unresolved *UnresolvedDependencies) (*ResolvedDependencies, error) {
|
||||
if isHelm3() {
|
||||
if shell.IsHelm3() {
|
||||
return m.updateHelm3(shell, wd, unresolved)
|
||||
}
|
||||
return m.updateHelm2(shell, wd, unresolved)
|
||||
@@ -288,7 +292,7 @@ func (m *chartDependencyManager) Update(shell helmexec.DependencyUpdater, wd str
|
||||
|
||||
func (m *chartDependencyManager) updateHelm3(shell helmexec.DependencyUpdater, wd string, unresolved *UnresolvedDependencies) (*ResolvedDependencies, error) {
|
||||
// Generate `Chart.yaml` of the temporary local chart
|
||||
chartMetaContent := fmt.Sprintf("name: %s\nversion: 1.0.0\n", m.Name)
|
||||
chartMetaContent := fmt.Sprintf("name: %s\nversion: 1.0.0\napiVersion: v2\n", m.Name)
|
||||
|
||||
// Generate `requirements.yaml` of the temporary local chart from the helmfile state
|
||||
reqsContent, err := yaml.Marshal(unresolved.ToChartRequirements())
|
||||
@@ -330,7 +334,7 @@ func (m *chartDependencyManager) doUpdate(chartLockFile string, unresolved *Unre
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isHelm3() && originalLockFileContent != nil {
|
||||
if shell.IsHelm3() && originalLockFileContent != nil {
|
||||
if err := m.writeBytes(filepath.Join(wd, chartLockFile), originalLockFileContent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -357,7 +361,7 @@ func (m *chartDependencyManager) doUpdate(chartLockFile string, unresolved *Unre
|
||||
})
|
||||
|
||||
// Don't update lock file if no dependency updated.
|
||||
if !isHelm3() && originalLockFileContent != nil {
|
||||
if !shell.IsHelm3() && originalLockFileContent != nil {
|
||||
originalLockedReqs := &ChartLockedRequirements{}
|
||||
if err := yaml.Unmarshal(originalLockFileContent, originalLockedReqs); err != nil {
|
||||
return nil, err
|
||||
@@ -374,6 +378,8 @@ func (m *chartDependencyManager) doUpdate(chartLockFile string, unresolved *Unre
|
||||
}
|
||||
}
|
||||
|
||||
lockedReqs.Version = version.Version
|
||||
|
||||
updatedLockFileContent, err = yaml.Marshal(lockedReqs)
|
||||
|
||||
if err != nil {
|
||||
@@ -404,6 +410,25 @@ func (m *chartDependencyManager) Resolve(unresolved *UnresolvedDependencies) (*R
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// Make sure go run main.go works and compatible with old lock files.
|
||||
if version.Version != "" && lockedReqs.Version != "" {
|
||||
lockedVersion, err := goversion.NewVersion(lockedReqs.Version)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
currentVersion, err := goversion.NewVersion(version.Version)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if currentVersion.LessThan(lockedVersion) {
|
||||
return nil, false, fmt.Errorf("the lockfile was created by Helmfile %s, which is newer than current %s; Please upgrade to Helmfile %s or greater", lockedVersion.Original(), currentVersion.Original(), lockedVersion.Original())
|
||||
}
|
||||
}
|
||||
|
||||
resolved := &ResolvedDependencies{deps: map[string][]ResolvedChartDependency{}}
|
||||
for _, d := range lockedReqs.ResolvedDependencies {
|
||||
if err := resolved.add(d); err != nil {
|
||||
|
||||
+76
-36
@@ -4,17 +4,23 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/remote"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/maputil"
|
||||
"github.com/variantdev/vals"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultHelmBinary = "helm"
|
||||
)
|
||||
|
||||
type StateLoadError struct {
|
||||
msg string
|
||||
Cause error
|
||||
@@ -33,27 +39,39 @@ func (e *UndefinedEnvError) Error() string {
|
||||
}
|
||||
|
||||
type StateCreator struct {
|
||||
logger *zap.SugaredLogger
|
||||
readFile func(string) ([]byte, error)
|
||||
fileExists func(string) (bool, error)
|
||||
abs func(string) (string, error)
|
||||
glob func(string) ([]string, error)
|
||||
helm helmexec.Interface
|
||||
logger *zap.SugaredLogger
|
||||
readFile func(string) ([]byte, error)
|
||||
fileExists func(string) (bool, error)
|
||||
abs func(string) (string, error)
|
||||
glob func(string) ([]string, error)
|
||||
DeleteFile func(string) error
|
||||
valsRuntime vals.Evaluator
|
||||
|
||||
Strict bool
|
||||
|
||||
LoadFile func(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*HelmState, error)
|
||||
|
||||
getHelm func(*HelmState) helmexec.Interface
|
||||
|
||||
overrideHelmBinary string
|
||||
|
||||
remote *remote.Remote
|
||||
}
|
||||
|
||||
func NewCreator(logger *zap.SugaredLogger, readFile func(string) ([]byte, error), fileExists func(string) (bool, error), abs func(string) (string, error), glob func(string) ([]string, error), helm helmexec.Interface) *StateCreator {
|
||||
func NewCreator(logger *zap.SugaredLogger, readFile func(string) ([]byte, error), fileExists func(string) (bool, error), abs func(string) (string, error), glob func(string) ([]string, error), valsRuntime vals.Evaluator, getHelm func(*HelmState) helmexec.Interface, overrideHelmBinary string, remote *remote.Remote) *StateCreator {
|
||||
return &StateCreator{
|
||||
logger: logger,
|
||||
readFile: readFile,
|
||||
fileExists: fileExists,
|
||||
abs: abs,
|
||||
glob: glob,
|
||||
Strict: true,
|
||||
helm: helm,
|
||||
logger: logger,
|
||||
readFile: readFile,
|
||||
fileExists: fileExists,
|
||||
abs: abs,
|
||||
glob: glob,
|
||||
Strict: true,
|
||||
valsRuntime: valsRuntime,
|
||||
getHelm: getHelm,
|
||||
|
||||
overrideHelmBinary: overrideHelmBinary,
|
||||
|
||||
remote: remote,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +81,6 @@ func (c *StateCreator) Parse(content []byte, baseDir, file string) (*HelmState,
|
||||
|
||||
state.FilePath = file
|
||||
state.basePath = baseDir
|
||||
state.helm = c.helm
|
||||
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(content))
|
||||
if !c.Strict {
|
||||
@@ -101,36 +118,49 @@ func (c *StateCreator) Parse(content []byte, baseDir, file string) (*HelmState,
|
||||
state.HelmDefaults.KubeContext = state.DeprecatedContext
|
||||
}
|
||||
|
||||
if c.overrideHelmBinary != "" && c.overrideHelmBinary != DefaultHelmBinary {
|
||||
state.DefaultHelmBinary = c.overrideHelmBinary
|
||||
} else if state.DefaultHelmBinary == "" {
|
||||
// Let `helmfile --helm-binary ""` not break this helmfile run
|
||||
state.DefaultHelmBinary = DefaultHelmBinary
|
||||
}
|
||||
|
||||
state.logger = c.logger
|
||||
|
||||
state.readFile = c.readFile
|
||||
state.removeFile = os.Remove
|
||||
state.fileExists = c.fileExists
|
||||
state.glob = c.glob
|
||||
state.valsRuntime = c.valsRuntime
|
||||
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
// LoadEnvValues loads environment values files relative to the `baseDir`
|
||||
func (c *StateCreator) LoadEnvValues(target *HelmState, env string, ctxEnv *environment.Environment) (*HelmState, error) {
|
||||
func (c *StateCreator) LoadEnvValues(target *HelmState, env string, ctxEnv *environment.Environment, failOnMissingEnv bool) (*HelmState, error) {
|
||||
state := *target
|
||||
|
||||
e, err := state.loadEnvValues(env, ctxEnv, c.readFile, c.glob)
|
||||
e, err := c.loadEnvValues(&state, env, failOnMissingEnv, ctxEnv, c.readFile, c.glob)
|
||||
if err != nil {
|
||||
return nil, &StateLoadError{fmt.Sprintf("failed to read %s", state.FilePath), err}
|
||||
}
|
||||
|
||||
e.Defaults, err = state.loadValuesEntries(nil, state.DefaultValues)
|
||||
newDefaults, err := state.loadValuesEntries(nil, state.DefaultValues, c.remote)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := mergo.Merge(&e.Defaults, newDefaults, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
state.Env = *e
|
||||
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
// Parses YAML into HelmState, while loading environment values files relative to the `baseDir`
|
||||
// evaluateBases=true means that this is NOT a base helmfile
|
||||
func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envName string, evaluateBases bool, envValues *environment.Environment) (*HelmState, error) {
|
||||
state, err := c.Parse(content, baseDir, file)
|
||||
if err != nil {
|
||||
@@ -141,20 +171,26 @@ func (c *StateCreator) ParseAndLoad(content []byte, baseDir, file string, envNam
|
||||
if len(state.Bases) > 0 {
|
||||
return nil, errors.New("nested `base` helmfile is unsupported. please submit a feature request if you need this!")
|
||||
}
|
||||
} else {
|
||||
state, err = c.loadBases(envValues, state, baseDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
state, err = c.loadBases(envValues, state, baseDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
state, err = c.LoadEnvValues(state, envName, envValues)
|
||||
state, err = c.LoadEnvValues(state, envName, envValues, evaluateBases)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
state.FilePath = file
|
||||
|
||||
vals, err := state.Env.GetMergedValues()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rendering values: %w", err)
|
||||
}
|
||||
state.RenderedValues = vals
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
@@ -178,12 +214,12 @@ func (c *StateCreator) loadBases(envValues *environment.Environment, st *HelmSta
|
||||
return layers[0], nil
|
||||
}
|
||||
|
||||
func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment, readFile func(string) ([]byte, error), glob func(string) ([]string, error)) (*environment.Environment, error) {
|
||||
func (c *StateCreator) loadEnvValues(st *HelmState, name string, failOnMissingEnv bool, ctxEnv *environment.Environment, readFile func(string) ([]byte, error), glob func(string) ([]string, error)) (*environment.Environment, error) {
|
||||
envVals := map[string]interface{}{}
|
||||
envSpec, ok := st.Environments[name]
|
||||
if ok {
|
||||
var err error
|
||||
envVals, err = st.loadValuesEntries(envSpec.MissingFileHandler, envSpec.Values)
|
||||
envVals, err = st.loadValuesEntries(envSpec.MissingFileHandler, envSpec.Values, c.remote)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -202,11 +238,11 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment,
|
||||
|
||||
envSecretFiles = append(envSecretFiles, resolved...)
|
||||
}
|
||||
if err = st.scatterGatherEnvSecretFiles(envSecretFiles, envVals, readFile); err != nil {
|
||||
if err = c.scatterGatherEnvSecretFiles(st, envSecretFiles, envVals, readFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
} else if ctxEnv == nil && name != DefaultEnv {
|
||||
} else if ctxEnv == nil && name != DefaultEnv && failOnMissingEnv {
|
||||
return nil, &UndefinedEnvError{msg: fmt.Sprintf("environment \"%s\" is not defined", name)}
|
||||
}
|
||||
|
||||
@@ -215,7 +251,7 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment,
|
||||
if ctxEnv != nil {
|
||||
intEnv := *ctxEnv
|
||||
|
||||
if err := mergo.Merge(&intEnv, newEnv, mergo.WithOverride); err != nil {
|
||||
if err := mergo.Merge(&intEnv, newEnv, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
|
||||
return nil, fmt.Errorf("error while merging environment values for \"%s\": %v", name, err)
|
||||
}
|
||||
|
||||
@@ -225,7 +261,7 @@ func (st *HelmState) loadEnvValues(name string, ctxEnv *environment.Environment,
|
||||
return newEnv, nil
|
||||
}
|
||||
|
||||
func (st *HelmState) scatterGatherEnvSecretFiles(envSecretFiles []string, envVals map[string]interface{}, readFile func(string) ([]byte, error)) error {
|
||||
func (c *StateCreator) scatterGatherEnvSecretFiles(st *HelmState, envSecretFiles []string, envVals map[string]interface{}, readFile func(string) ([]byte, error)) error {
|
||||
var errs []error
|
||||
|
||||
inputs := envSecretFiles
|
||||
@@ -239,7 +275,6 @@ func (st *HelmState) scatterGatherEnvSecretFiles(envSecretFiles []string, envVal
|
||||
|
||||
secrets := make(chan string, inputsSize)
|
||||
results := make(chan secretResult, inputsSize)
|
||||
helm := st.helm
|
||||
|
||||
st.scatterGather(0, inputsSize,
|
||||
func() {
|
||||
@@ -252,11 +287,16 @@ func (st *HelmState) scatterGatherEnvSecretFiles(envSecretFiles []string, envVal
|
||||
for path := range secrets {
|
||||
release := &ReleaseSpec{}
|
||||
flags := st.appendConnectionFlags([]string{}, release)
|
||||
decFile, err := helm.DecryptSecret(st.createHelmContext(release, 0), path, flags...)
|
||||
decFile, err := c.getHelm(st).DecryptSecret(st.createHelmContext(release, 0), path, flags...)
|
||||
if err != nil {
|
||||
results <- secretResult{nil, err, path}
|
||||
continue
|
||||
}
|
||||
defer func() {
|
||||
if err := c.DeleteFile(decFile); err != nil {
|
||||
c.logger.Warnf("removing decrypted file %s: %w", decFile, err)
|
||||
}
|
||||
}()
|
||||
bytes, err := readFile(decFile)
|
||||
if err != nil {
|
||||
results <- secretResult{nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err), path}
|
||||
@@ -284,7 +324,7 @@ func (st *HelmState) scatterGatherEnvSecretFiles(envSecretFiles []string, envVal
|
||||
if result.err != nil {
|
||||
errs = append(errs, result.err)
|
||||
} else {
|
||||
if err := mergo.Merge(&envVals, &result.result, mergo.WithOverride); err != nil {
|
||||
if err := mergo.Merge(&envVals, &result.result, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to load environment secrets file \"%s\": %v", result.path, err))
|
||||
}
|
||||
}
|
||||
@@ -302,11 +342,11 @@ func (st *HelmState) scatterGatherEnvSecretFiles(envSecretFiles []string, envVal
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *HelmState) loadValuesEntries(missingFileHandler *string, entries []interface{}) (map[string]interface{}, error) {
|
||||
func (st *HelmState) loadValuesEntries(missingFileHandler *string, entries []interface{}, remote *remote.Remote) (map[string]interface{}, error) {
|
||||
envVals := map[string]interface{}{}
|
||||
|
||||
valuesEntries := append([]interface{}{}, entries...)
|
||||
ld := NewEnvironmentValuesLoader(st.storage(), st.readFile, st.logger)
|
||||
ld := NewEnvironmentValuesLoader(st.storage(), st.readFile, st.logger, remote)
|
||||
var err error
|
||||
envVals, err = ld.LoadEnvironmentValues(missingFileHandler, valuesEntries)
|
||||
if err != nil {
|
||||
|
||||
+143
-9
@@ -1,7 +1,9 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"github.com/roboll/helmfile/pkg/remote"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
@@ -9,8 +11,8 @@ import (
|
||||
"github.com/roboll/helmfile/pkg/testhelper"
|
||||
"go.uber.org/zap"
|
||||
|
||||
. "gotest.tools/assert"
|
||||
"gotest.tools/assert/cmp"
|
||||
. "gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func createFromYaml(content []byte, file string, env string, logger *zap.SugaredLogger) (*HelmState, error) {
|
||||
@@ -18,9 +20,11 @@ func createFromYaml(content []byte, file string, env string, logger *zap.Sugared
|
||||
logger: logger,
|
||||
readFile: ioutil.ReadFile,
|
||||
abs: filepath.Abs,
|
||||
Strict: true,
|
||||
|
||||
DeleteFile: os.Remove,
|
||||
Strict: true,
|
||||
}
|
||||
return c.ParseAndLoad(content, filepath.Dir(file), file, env, false, nil)
|
||||
return c.ParseAndLoad(content, filepath.Dir(file), file, env, true, nil)
|
||||
}
|
||||
|
||||
func TestReadFromYaml(t *testing.T) {
|
||||
@@ -46,7 +50,7 @@ func TestReadFromYaml(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFromYaml_InexistentEnv(t *testing.T) {
|
||||
func TestReadFromYaml_NonexistentEnv(t *testing.T) {
|
||||
yamlFile := "example/path/to/yaml/file"
|
||||
yamlContent := []byte(`releases:
|
||||
- name: myrelease
|
||||
@@ -59,6 +63,35 @@ func TestReadFromYaml_InexistentEnv(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type stateTestEnv struct {
|
||||
Files map[string]string
|
||||
WorkDir string
|
||||
}
|
||||
|
||||
func (testEnv stateTestEnv) MustLoadState(t *testing.T, file, envName string) *HelmState {
|
||||
t.Helper()
|
||||
|
||||
testFs := testhelper.NewTestFs(testEnv.Files)
|
||||
|
||||
if testFs.Cwd == "" {
|
||||
testFs.Cwd = "/"
|
||||
}
|
||||
|
||||
yamlContent, ok := testEnv.Files[file]
|
||||
if !ok {
|
||||
t.Fatalf("no file named %q registered", file)
|
||||
}
|
||||
|
||||
r := remote.NewRemote(logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
|
||||
state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob, nil, nil, "", r).
|
||||
ParseAndLoad([]byte(yamlContent), filepath.Dir(file), file, envName, true, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
func TestReadFromYaml_NonDefaultEnv(t *testing.T) {
|
||||
yamlFile := "/example/path/to/helmfile.yaml"
|
||||
yamlContent := []byte(`environments:
|
||||
@@ -96,9 +129,15 @@ bar: {{ readFile "bar.txt" }}
|
||||
}
|
||||
|
||||
valuesFile := "/example/path/to/values.yaml.gotmpl"
|
||||
valuesContent := []byte(`env: {{ .Environment.Name }}`)
|
||||
valuesContent := []byte(`env: {{ .Environment.Name }}
|
||||
releaseName: {{ .Release.Name }}
|
||||
releaseNamespace: {{ .Release.Namespace }}
|
||||
`)
|
||||
|
||||
expectedValues := `env: production`
|
||||
expectedValues := `env: production
|
||||
releaseName: myrelease
|
||||
releaseNamespace: mynamespace
|
||||
`
|
||||
|
||||
testFs := testhelper.NewTestFs(map[string]string{
|
||||
fooYamlFile: string(fooYamlContent),
|
||||
@@ -108,7 +147,9 @@ bar: {{ readFile "bar.txt" }}
|
||||
})
|
||||
testFs.Cwd = "/example/path/to"
|
||||
|
||||
state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob, nil).ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", false, nil)
|
||||
r := remote.NewRemote(logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
|
||||
state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob, nil, nil, "", r).
|
||||
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -118,7 +159,98 @@ bar: {{ readFile "bar.txt" }}
|
||||
t.Errorf("unexpected environment values: expected=%v, actual=%v", expected, actual)
|
||||
}
|
||||
|
||||
actualValuesData, err := state.RenderValuesFileToBytes(valuesFile)
|
||||
release := state.Releases[0]
|
||||
|
||||
state.ApplyOverrides(&release)
|
||||
|
||||
actualValuesData, err := state.RenderReleaseValuesFileToBytes(&release, valuesFile)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
actualValues := string(actualValuesData)
|
||||
|
||||
if !reflect.DeepEqual(expectedValues, actualValues) {
|
||||
t.Errorf("unexpected values: expected=%v, actual=%v", expectedValues, actualValues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFromYaml_OverrideNamespace(t *testing.T) {
|
||||
yamlFile := "/example/path/to/helmfile.yaml"
|
||||
yamlContent := []byte(`environments:
|
||||
production:
|
||||
values:
|
||||
- foo.yaml
|
||||
- bar.yaml.gotmpl
|
||||
|
||||
# A.k.a helmfile apply --namespace myns
|
||||
namespace: myns
|
||||
|
||||
releases:
|
||||
- name: myrelease
|
||||
namespace: mynamespace
|
||||
chart: mychart
|
||||
values:
|
||||
- values.yaml.gotmpl
|
||||
`)
|
||||
|
||||
fooYamlFile := "/example/path/to/foo.yaml"
|
||||
fooYamlContent := []byte(`foo: foo
|
||||
# As this file doesn't have an file extension ".gotmpl", this template expression should not be evaluated
|
||||
baz: "{{ readFile \"baz.txt\" }}"`)
|
||||
|
||||
barYamlFile := "/example/path/to/bar.yaml.gotmpl"
|
||||
barYamlContent := []byte(`foo: FOO
|
||||
bar: {{ readFile "bar.txt" }}
|
||||
`)
|
||||
|
||||
barTextFile := "/example/path/to/bar.txt"
|
||||
barTextContent := []byte("BAR")
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"foo": "FOO",
|
||||
"bar": "BAR",
|
||||
// As the file doesn't have an file extension ".gotmpl", this template expression should not be evaluated
|
||||
"baz": "{{ readFile \"baz.txt\" }}",
|
||||
}
|
||||
|
||||
valuesFile := "/example/path/to/values.yaml.gotmpl"
|
||||
valuesContent := []byte(`env: {{ .Environment.Name }}
|
||||
releaseName: {{ .Release.Name }}
|
||||
releaseNamespace: {{ .Release.Namespace }}
|
||||
overrideNamespace: {{ .Namespace }}
|
||||
`)
|
||||
|
||||
expectedValues := `env: production
|
||||
releaseName: myrelease
|
||||
releaseNamespace: myns
|
||||
overrideNamespace: myns
|
||||
`
|
||||
|
||||
testFs := testhelper.NewTestFs(map[string]string{
|
||||
fooYamlFile: string(fooYamlContent),
|
||||
barYamlFile: string(barYamlContent),
|
||||
barTextFile: string(barTextContent),
|
||||
valuesFile: string(valuesContent),
|
||||
})
|
||||
testFs.Cwd = "/example/path/to"
|
||||
|
||||
r := remote.NewRemote(logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
|
||||
state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob, nil, nil, "", r).
|
||||
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
actual := state.Env.Values
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Errorf("unexpected environment values: expected=%v, actual=%v", expected, actual)
|
||||
}
|
||||
|
||||
release := state.Releases[0]
|
||||
|
||||
state.ApplyOverrides(&release)
|
||||
|
||||
actualValuesData, err := state.RenderReleaseValuesFileToBytes(&release, valuesFile)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -244,6 +376,8 @@ func TestReadFromYaml_FilterNegatives(t *testing.T) {
|
||||
[]bool{false, true, false}},
|
||||
{LabelFilter{negativeLabels: [][]string{[]string{"stage", "pre"}, []string{"stage", "post"}}},
|
||||
[]bool{false, false, true}},
|
||||
{LabelFilter{negativeLabels: [][]string{[]string{"foo", "bar"}}},
|
||||
[]bool{false, true, true}},
|
||||
}
|
||||
state, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
|
||||
if err != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/maputil"
|
||||
"github.com/roboll/helmfile/pkg/remote"
|
||||
"github.com/roboll/helmfile/pkg/tmpl"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v2"
|
||||
@@ -17,13 +18,16 @@ type EnvironmentValuesLoader struct {
|
||||
readFile func(string) ([]byte, error)
|
||||
|
||||
logger *zap.SugaredLogger
|
||||
|
||||
remote *remote.Remote
|
||||
}
|
||||
|
||||
func NewEnvironmentValuesLoader(storage *Storage, readFile func(string) ([]byte, error), logger *zap.SugaredLogger) *EnvironmentValuesLoader {
|
||||
func NewEnvironmentValuesLoader(storage *Storage, readFile func(string) ([]byte, error), logger *zap.SugaredLogger, remote *remote.Remote) *EnvironmentValuesLoader {
|
||||
return &EnvironmentValuesLoader{
|
||||
storage: storage,
|
||||
readFile: readFile,
|
||||
logger: logger,
|
||||
remote: remote,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +40,11 @@ func (ld *EnvironmentValuesLoader) LoadEnvironmentValues(missingFileHandler *str
|
||||
switch strOrMap := entry.(type) {
|
||||
case string:
|
||||
urlOrPath := strOrMap
|
||||
localPath, err := ld.remote.Locate(urlOrPath)
|
||||
if err == nil {
|
||||
urlOrPath = localPath
|
||||
}
|
||||
|
||||
files, skipped, err := ld.storage.resolveFile(missingFileHandler, "environment values", urlOrPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -73,7 +82,7 @@ func (ld *EnvironmentValuesLoader) LoadEnvironmentValues(missingFileHandler *str
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mergo.Merge(&result, &vals, mergo.WithOverride); err != nil {
|
||||
if err := mergo.Merge(&result, &vals, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
|
||||
return nil, fmt.Errorf("failed to merge %v: %v", m, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/roboll/helmfile/pkg/remote"
|
||||
"go.uber.org/zap"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func newLoader() *EnvironmentValuesLoader {
|
||||
log, err := zap.NewDevelopment(zap.AddStacktrace(zap.DebugLevel))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sugar := log.Sugar()
|
||||
|
||||
storage := &Storage{
|
||||
FilePath: "./helmfile.yaml",
|
||||
basePath: ".",
|
||||
glob: filepath.Glob,
|
||||
logger: sugar,
|
||||
}
|
||||
|
||||
readFile := func(s string) ([]byte, error) { return []byte{}, nil }
|
||||
dirExists := func(d string) bool { return false }
|
||||
fileExists := func(f string) bool { return false }
|
||||
return NewEnvironmentValuesLoader(storage, ioutil.ReadFile, sugar, remote.NewRemote(sugar, "/tmp", readFile, dirExists, fileExists))
|
||||
}
|
||||
|
||||
// See https://github.com/roboll/helmfile/pull/1169
|
||||
func TestEnvValsLoad_SingleValuesFile(t *testing.T) {
|
||||
l := newLoader()
|
||||
|
||||
actual, err := l.LoadEnvironmentValues(nil, []interface{}{"testdata/values.5.yaml"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"affinity": map[string]interface{}{},
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
// See https://github.com/roboll/helmfile/issues/1150
|
||||
func TestEnvValsLoad_OverwriteNilValue_Issue1150(t *testing.T) {
|
||||
l := newLoader()
|
||||
|
||||
actual, err := l.LoadEnvironmentValues(nil, []interface{}{"testdata/values.1.yaml", "testdata/values.2.yaml"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"components": map[string]interface{}{
|
||||
"etcd-operator": map[string]interface{}{
|
||||
"version": "0.10.3",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
// See https://github.com/roboll/helmfile/issues/1154
|
||||
func TestEnvValsLoad_OverwriteWithNilValue_Issue1154(t *testing.T) {
|
||||
l := newLoader()
|
||||
|
||||
actual, err := l.LoadEnvironmentValues(nil, []interface{}{"testdata/values.3.yaml", "testdata/values.4.yaml"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"components": map[string]interface{}{
|
||||
"etcd-operator": map[string]interface{}{
|
||||
"version": "0.10.3",
|
||||
},
|
||||
"prometheus": nil,
|
||||
},
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
|
||||
// See https://github.com/roboll/helmfile/issues/1168
|
||||
func TestEnvValsLoad_OverwriteEmptyValue_Issue1168(t *testing.T) {
|
||||
l := newLoader()
|
||||
|
||||
actual, err := l.LoadEnvironmentValues(nil, []interface{}{"testdata/issues/1168/addons.yaml", "testdata/issues/1168/addons2.yaml"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"addons": map[string]interface{}{
|
||||
"mychart": map[string]interface{}{
|
||||
"skip": false,
|
||||
"name": "mychart",
|
||||
"namespace": "kube-system",
|
||||
"chart": "stable/mychart",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(expected, actual); diff != "" {
|
||||
t.Errorf(diff)
|
||||
}
|
||||
}
|
||||
+133
-21
@@ -1,5 +1,15 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/remote"
|
||||
"github.com/variantdev/chartify"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Dependency struct {
|
||||
Chart string `yaml:"chart"`
|
||||
Version string `yaml:"version"`
|
||||
@@ -7,49 +17,151 @@ type Dependency struct {
|
||||
}
|
||||
|
||||
func (st *HelmState) appendHelmXFlags(flags []string, release *ReleaseSpec) ([]string, error) {
|
||||
for _, d := range release.Dependencies {
|
||||
var dep string
|
||||
if d.Alias != "" {
|
||||
dep += d.Alias + "="
|
||||
}
|
||||
dep += d.Chart
|
||||
if d.Version != "" {
|
||||
dep += ":" + d.Version
|
||||
}
|
||||
flags = append(flags, "--dependency", dep)
|
||||
}
|
||||
|
||||
for _, adopt := range release.Adopt {
|
||||
flags = append(flags, "--adopt", adopt)
|
||||
}
|
||||
|
||||
return flags, nil
|
||||
}
|
||||
|
||||
func fileExistsAt(path string) bool {
|
||||
fileInfo, err := os.Stat(path)
|
||||
return err == nil && fileInfo.Mode().IsRegular()
|
||||
}
|
||||
|
||||
func directoryExistsAt(path string) bool {
|
||||
fileInfo, err := os.Stat(path)
|
||||
return err == nil && fileInfo.Mode().IsDir()
|
||||
}
|
||||
|
||||
type Chartify struct {
|
||||
Opts *chartify.ChartifyOpts
|
||||
Clean func()
|
||||
}
|
||||
|
||||
func (st *HelmState) goGetterChart(chart, dir string, force bool) (string, error) {
|
||||
if dir != "" && chart == "" {
|
||||
chart = dir
|
||||
}
|
||||
|
||||
_, err := remote.Parse(chart)
|
||||
if err != nil {
|
||||
if force {
|
||||
return "", fmt.Errorf("Parsing url from dir failed due to error %q.\nContinuing the process assuming this is a regular Helm chart or a local dir.", err.Error())
|
||||
}
|
||||
} else {
|
||||
r := remote.NewRemote(st.logger, st.basePath, st.readFile, directoryExistsAt, fileExistsAt)
|
||||
|
||||
fetchedDir, err := r.Fetch(chart)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetching %q: %v", chart, err)
|
||||
}
|
||||
|
||||
chart = fetchedDir
|
||||
}
|
||||
|
||||
return chart, nil
|
||||
}
|
||||
|
||||
func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSpec, chart string, workerIndex int) (*Chartify, func(), error) {
|
||||
chartify := &Chartify{
|
||||
Opts: &chartify.ChartifyOpts{
|
||||
WorkaroundOutputDirIssue: true,
|
||||
EnableKustomizeAlphaPlugins: true,
|
||||
ChartVersion: release.Version,
|
||||
Namespace: release.Namespace,
|
||||
},
|
||||
}
|
||||
|
||||
var filesNeedCleaning []string
|
||||
|
||||
clean := func() {
|
||||
st.removeFiles(filesNeedCleaning)
|
||||
}
|
||||
|
||||
var shouldRun bool
|
||||
|
||||
dir := filepath.Join(st.basePath, chart)
|
||||
if stat, _ := os.Stat(dir); stat != nil && stat.IsDir() {
|
||||
if exists, err := st.fileExists(filepath.Join(dir, "Chart.yaml")); err == nil && !exists {
|
||||
shouldRun = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, d := range release.Dependencies {
|
||||
var dep string
|
||||
|
||||
if d.Alias != "" {
|
||||
dep += d.Alias + "="
|
||||
} else {
|
||||
a := strings.Split(d.Chart, "/")
|
||||
|
||||
chart := a[len(a)-1]
|
||||
|
||||
dep += chart + "="
|
||||
}
|
||||
|
||||
dep += d.Chart
|
||||
|
||||
if d.Version != "" {
|
||||
dep += ":" + d.Version
|
||||
}
|
||||
|
||||
chartify.Opts.AdhocChartDependencies = append(chartify.Opts.AdhocChartDependencies, dep)
|
||||
|
||||
shouldRun = true
|
||||
}
|
||||
|
||||
jsonPatches := release.JSONPatches
|
||||
if len(jsonPatches) > 0 {
|
||||
generatedFiles, err := st.generateTemporaryValuesFiles(jsonPatches, release.MissingFileHandler)
|
||||
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, jsonPatches, release.MissingFileHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, clean, err
|
||||
}
|
||||
|
||||
filesNeedCleaning = append(filesNeedCleaning, generatedFiles...)
|
||||
|
||||
for _, f := range generatedFiles {
|
||||
flags = append(flags, "--json-patch", f)
|
||||
chartify.Opts.JsonPatches = append(chartify.Opts.JsonPatches, f)
|
||||
}
|
||||
|
||||
release.generatedValues = append(release.generatedValues, generatedFiles...)
|
||||
shouldRun = true
|
||||
}
|
||||
|
||||
strategicMergePatches := release.StrategicMergePatches
|
||||
if len(strategicMergePatches) > 0 {
|
||||
generatedFiles, err := st.generateTemporaryValuesFiles(strategicMergePatches, release.MissingFileHandler)
|
||||
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, strategicMergePatches, release.MissingFileHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, clean, err
|
||||
}
|
||||
|
||||
for _, f := range generatedFiles {
|
||||
flags = append(flags, "--strategic-merge-patch", f)
|
||||
chartify.Opts.StrategicMergePatches = append(chartify.Opts.StrategicMergePatches, f)
|
||||
}
|
||||
|
||||
release.generatedValues = append(release.generatedValues, generatedFiles...)
|
||||
filesNeedCleaning = append(filesNeedCleaning, generatedFiles...)
|
||||
|
||||
shouldRun = true
|
||||
}
|
||||
|
||||
return flags, nil
|
||||
if release.ForceNamespace != "" {
|
||||
chartify.Opts.OverrideNamespace = release.ForceNamespace
|
||||
|
||||
shouldRun = true
|
||||
}
|
||||
|
||||
if shouldRun {
|
||||
generatedFiles, err := st.generateValuesFiles(helm, release, workerIndex)
|
||||
if err != nil {
|
||||
return nil, clean, err
|
||||
}
|
||||
|
||||
filesNeedCleaning = append(filesNeedCleaning, generatedFiles...)
|
||||
|
||||
chartify.Opts.ValuesFiles = generatedFiles
|
||||
|
||||
return chartify, clean, nil
|
||||
}
|
||||
|
||||
return nil, clean, nil
|
||||
}
|
||||
|
||||
+16
-9
@@ -93,7 +93,7 @@ func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*R
|
||||
result.Labels[key] = s.String()
|
||||
}
|
||||
|
||||
if result.ValuesTemplate != nil && len(result.ValuesTemplate) > 0 {
|
||||
if len(result.ValuesTemplate) > 0 {
|
||||
for i, t := range result.ValuesTemplate {
|
||||
switch ts := t.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
@@ -117,7 +117,9 @@ func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*R
|
||||
}
|
||||
}
|
||||
|
||||
result.Values = result.ValuesTemplate
|
||||
var newvals []interface{}
|
||||
newvals = append(newvals, result.ValuesTemplate...)
|
||||
result.Values = append(newvals, result.Values...)
|
||||
}
|
||||
|
||||
for i, t := range result.Values {
|
||||
@@ -131,15 +133,18 @@ func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*R
|
||||
}
|
||||
}
|
||||
|
||||
for i, ts := range result.Secrets {
|
||||
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".secrets[%d] = \"%s\": %v", r.Name, i, ts, err)
|
||||
for i, t := range result.Secrets {
|
||||
switch ts := t.(type) {
|
||||
case string:
|
||||
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".secrets[%d] = \"%s\": %v", r.Name, i, ts, err)
|
||||
}
|
||||
result.Secrets[i] = s.String()
|
||||
}
|
||||
result.Secrets[i] = s.String()
|
||||
}
|
||||
|
||||
if result.SetValuesTemplate != nil && len(result.SetValuesTemplate) > 0 {
|
||||
if len(result.SetValuesTemplate) > 0 {
|
||||
for i, val := range result.SetValuesTemplate {
|
||||
{
|
||||
// name
|
||||
@@ -178,7 +183,9 @@ func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*R
|
||||
}
|
||||
}
|
||||
|
||||
result.SetValues = result.SetValuesTemplate
|
||||
var newvals []SetValue
|
||||
newvals = append(newvals, result.SetValuesTemplate...)
|
||||
result.SetValues = append(newvals, result.SetValues...)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
@@ -13,9 +13,19 @@ type ReleaseError struct {
|
||||
}
|
||||
|
||||
func (e *ReleaseError) Error() string {
|
||||
return fmt.Sprintf("failed processing release %s: %v", e.Name, e.err.Error())
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func newReleaseError(release *ReleaseSpec, err error) *ReleaseError {
|
||||
return &ReleaseError{release, err, ReleaseErrorCodeFailure}
|
||||
func NewReleaseError(release *ReleaseSpec, err error, code int) *ReleaseError {
|
||||
return &ReleaseError{
|
||||
ReleaseSpec: release,
|
||||
err: err,
|
||||
Code: code,
|
||||
}
|
||||
}
|
||||
|
||||
func newReleaseFailedError(release *ReleaseSpec, err error) *ReleaseError {
|
||||
wrappedErr := fmt.Errorf("failed processing release %s: %v", release.Name, err.Error())
|
||||
|
||||
return NewReleaseError(release, wrappedErr, ReleaseErrorCodeFailure)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func (l LabelFilter) Match(r ReleaseSpec) bool {
|
||||
k := element[0]
|
||||
v := element[1]
|
||||
if rVal, ok := r.Labels[k]; !ok {
|
||||
return true
|
||||
|
||||
} else if rVal == v {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSelectReleasesWithOverrides(t *testing.T) {
|
||||
type testcase struct {
|
||||
subject string
|
||||
selector []string
|
||||
want []string
|
||||
}
|
||||
|
||||
testcases := []testcase{
|
||||
{
|
||||
subject: "multiple OR selectors (nillable label first)",
|
||||
selector: []string{"type=bar", "name=nolabel2", "name=nolabel1"},
|
||||
want: []string{"nolabel1", "nolabel2", "foo"},
|
||||
},
|
||||
{
|
||||
subject: "multiple OR selectors (non-nillable label first)",
|
||||
selector: []string{"name=foo", "type!=bar"},
|
||||
want: []string{"nolabel1", "nolabel2", "foo"},
|
||||
},
|
||||
{
|
||||
subject: "multiple AND conditions (nillable label first)",
|
||||
selector: []string{"type!=bar,name!=nolabel2"},
|
||||
want: []string{"nolabel1"},
|
||||
},
|
||||
{
|
||||
subject: "multiple AND conditions (non-nillable label first)",
|
||||
selector: []string{"name!=nolabel2,type!=bar"},
|
||||
want: []string{"nolabel1"},
|
||||
},
|
||||
{
|
||||
subject: "inequality on nillable label",
|
||||
selector: []string{"type!=bar"},
|
||||
want: []string{"nolabel1", "nolabel2"},
|
||||
},
|
||||
{
|
||||
subject: "equality on nillable label",
|
||||
selector: []string{"type=bar"},
|
||||
want: []string{"foo"},
|
||||
},
|
||||
{
|
||||
subject: "inequality on non-nillable label",
|
||||
selector: []string{"name!=nolabel1"},
|
||||
want: []string{"nolabel2", "foo"},
|
||||
},
|
||||
{
|
||||
subject: "equality on non-nillable label",
|
||||
selector: []string{"name=nolabel1"},
|
||||
want: []string{"nolabel1"},
|
||||
},
|
||||
}
|
||||
|
||||
example := []byte(`releases:
|
||||
- name: nolabel1
|
||||
namespace: kube-system
|
||||
chart: stable/nolabel
|
||||
- name: nolabel2
|
||||
namespace: default
|
||||
chart: stable/nolabel
|
||||
- name: foo
|
||||
namespace: kube-system
|
||||
chart: stable/foo
|
||||
labels:
|
||||
type: bar
|
||||
`)
|
||||
|
||||
state := stateTestEnv{
|
||||
Files: map[string]string{
|
||||
"/helmfile.yaml": string(example),
|
||||
},
|
||||
WorkDir: "/",
|
||||
}.MustLoadState(t, "/helmfile.yaml", "default")
|
||||
|
||||
for _, tc := range testcases {
|
||||
state.Selectors = tc.selector
|
||||
|
||||
rs, err := state.GetSelectedReleasesWithOverrides()
|
||||
if err != nil {
|
||||
t.Fatalf("%s %s: %v", tc.selector, tc.subject, err)
|
||||
}
|
||||
|
||||
var got []string
|
||||
|
||||
for _, r := range rs {
|
||||
got = append(got, r.Name)
|
||||
}
|
||||
|
||||
if d := cmp.Diff(tc.want, got); d != "" {
|
||||
t.Errorf("%s %s: %s", tc.selector, tc.subject, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1397
-351
File diff suppressed because it is too large
Load Diff
@@ -4,43 +4,29 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/pkg/maputil"
|
||||
"github.com/roboll/helmfile/pkg/tmpl"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func (st *HelmState) Values() (map[string]interface{}, error) {
|
||||
vals := map[string]interface{}{}
|
||||
|
||||
if err := mergo.Merge(&vals, st.Env.Defaults, mergo.WithOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mergo.Merge(&vals, st.Env.Values, mergo.WithOverride); err != nil {
|
||||
return nil, err
|
||||
func (st *HelmState) Values() map[string]interface{} {
|
||||
if st.RenderedValues == nil {
|
||||
panic("[bug] RenderedValues is nil")
|
||||
}
|
||||
|
||||
vals, err := maputil.CastKeysToStrings(vals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vals, nil
|
||||
return st.RenderedValues
|
||||
}
|
||||
|
||||
func (st *HelmState) mustLoadVals() map[string]interface{} {
|
||||
vals, err := st.Values()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return vals
|
||||
}
|
||||
|
||||
func (st *HelmState) valuesFileTemplateData() EnvironmentTemplateData {
|
||||
return EnvironmentTemplateData{
|
||||
func (st *HelmState) createReleaseTemplateData(release *ReleaseSpec, vals map[string]interface{}) releaseTemplateData {
|
||||
return releaseTemplateData{
|
||||
Environment: st.Env,
|
||||
Namespace: st.Namespace,
|
||||
Values: st.mustLoadVals(),
|
||||
Namespace: st.OverrideNamespace,
|
||||
Values: vals,
|
||||
Release: releaseTemplateDataRelease{
|
||||
Name: release.Name,
|
||||
Chart: release.Chart,
|
||||
Namespace: release.Namespace,
|
||||
Labels: release.Labels,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,19 +82,18 @@ func updateBoolTemplatedValues(r *ReleaseSpec) error {
|
||||
func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
|
||||
r := *st
|
||||
|
||||
vals, err := st.Values()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vals := st.Values()
|
||||
|
||||
for i, rt := range st.Releases {
|
||||
if rt.Labels == nil {
|
||||
rt.Labels = map[string]string{}
|
||||
}
|
||||
for k, v := range st.CommonLabels {
|
||||
rt.Labels[k] = v
|
||||
}
|
||||
successFlag := false
|
||||
for it, prev := 0, &rt; it < 6; it++ {
|
||||
tmplData := releaseTemplateData{
|
||||
Environment: st.Env,
|
||||
Release: *prev,
|
||||
Values: vals,
|
||||
}
|
||||
tmplData := st.createReleaseTemplateData(prev, vals)
|
||||
renderer := tmpl.NewFileRenderer(st.readFile, st.basePath, tmplData)
|
||||
r, err := rt.ExecuteTemplateExpressions(renderer)
|
||||
if err != nil {
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestHelmState_executeTemplates(t *testing.T) {
|
||||
Name: "test-app",
|
||||
Namespace: "test-namespace-{{ .Release.Name }}",
|
||||
ValuesTemplate: []interface{}{"config/{{ .Environment.Name }}/{{ .Release.Name }}/values.yaml"},
|
||||
Secrets: []string{"config/{{ .Environment.Name }}/{{ .Release.Name }}/secrets.yaml"},
|
||||
Secrets: []interface{}{"config/{{ .Environment.Name }}/{{ .Release.Name }}/secrets.yaml"},
|
||||
Labels: map[string]string{"id": "{{ .Release.Name }}"},
|
||||
},
|
||||
want: ReleaseSpec{
|
||||
@@ -45,7 +45,7 @@ func TestHelmState_executeTemplates(t *testing.T) {
|
||||
Name: "test-app",
|
||||
Namespace: "test-namespace-test-app",
|
||||
Values: []interface{}{"config/test_env/test-app/values.yaml"},
|
||||
Secrets: []string{"config/test_env/test-app/secrets.yaml"},
|
||||
Secrets: []interface{}{"config/test_env/test-app/secrets.yaml"},
|
||||
Labels: map[string]string{"id": "test-app"},
|
||||
},
|
||||
},
|
||||
@@ -135,15 +135,18 @@ func TestHelmState_executeTemplates(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
state := &HelmState{
|
||||
basePath: ".",
|
||||
HelmDefaults: HelmSpec{
|
||||
KubeContext: "test_context",
|
||||
},
|
||||
Env: environment.Environment{Name: "test_env"},
|
||||
Namespace: "test-namespace_",
|
||||
Repositories: nil,
|
||||
Releases: []ReleaseSpec{
|
||||
tt.input,
|
||||
ReleaseSetSpec: ReleaseSetSpec{
|
||||
HelmDefaults: HelmSpec{
|
||||
KubeContext: "test_context",
|
||||
},
|
||||
Env: environment.Environment{Name: "test_env"},
|
||||
OverrideNamespace: "test-namespace_",
|
||||
Repositories: nil,
|
||||
Releases: []ReleaseSpec{
|
||||
tt.input,
|
||||
},
|
||||
},
|
||||
RenderedValues: map[string]interface{}{},
|
||||
}
|
||||
|
||||
r, err := state.ExecuteTemplates()
|
||||
@@ -235,15 +238,18 @@ func TestHelmState_recursiveRefsTemplates(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
state := &HelmState{
|
||||
basePath: ".",
|
||||
HelmDefaults: HelmSpec{
|
||||
KubeContext: "test_context",
|
||||
},
|
||||
Env: environment.Environment{Name: "test_env"},
|
||||
Namespace: "test-namespace_",
|
||||
Repositories: nil,
|
||||
Releases: []ReleaseSpec{
|
||||
tt.input,
|
||||
ReleaseSetSpec: ReleaseSetSpec{
|
||||
HelmDefaults: HelmSpec{
|
||||
KubeContext: "test_context",
|
||||
},
|
||||
Env: environment.Environment{Name: "test_env"},
|
||||
OverrideNamespace: "test-namespace_",
|
||||
Repositories: nil,
|
||||
Releases: []ReleaseSpec{
|
||||
tt.input,
|
||||
},
|
||||
},
|
||||
RenderedValues: map[string]interface{}{},
|
||||
}
|
||||
|
||||
r, err := state.ExecuteTemplates()
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGoGetter(t *testing.T) {
|
||||
logger := helmexec.NewLogger(os.Stderr, "warn")
|
||||
|
||||
testcases := []struct {
|
||||
chart, dir string
|
||||
force bool
|
||||
|
||||
out, err string
|
||||
}{
|
||||
{
|
||||
chart: "raw/incubator",
|
||||
dir: "",
|
||||
force: false,
|
||||
out: "raw/incubator",
|
||||
err: "",
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testcases {
|
||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
||||
d, err := ioutil.TempDir("", "testgogetter")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(d)
|
||||
|
||||
st := &HelmState{
|
||||
logger: logger,
|
||||
readFile: ioutil.ReadFile,
|
||||
basePath: d,
|
||||
}
|
||||
|
||||
out, err := st.goGetterChart(tc.chart, tc.dir, false)
|
||||
|
||||
if diff := cmp.Diff(tc.out, out); diff != "" {
|
||||
t.Fatalf("Unexpected out:\n%s", diff)
|
||||
}
|
||||
|
||||
var errMsg string
|
||||
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tc.err, errMsg); diff != "" {
|
||||
t.Fatalf("Unexpected err:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+112
-10
@@ -2,9 +2,11 @@ package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/variantdev/dag/pkg/dag"
|
||||
)
|
||||
|
||||
type result struct {
|
||||
@@ -36,9 +38,7 @@ func (st *HelmState) scatterGather(concurrency int, items int, produceInputs fun
|
||||
|
||||
for w := 1; w <= concurrency; w++ {
|
||||
go func(id int) {
|
||||
st.logger.Debugf("worker %d/%d started", id, concurrency)
|
||||
receiveInputsAndProduceIntermediates(id)
|
||||
st.logger.Debugf("worker %d/%d finished", id, concurrency)
|
||||
waitGroup.Done()
|
||||
}(w)
|
||||
}
|
||||
@@ -51,9 +51,14 @@ func (st *HelmState) scatterGather(concurrency int, items int, produceInputs fun
|
||||
|
||||
func (st *HelmState) scatterGatherReleases(helm helmexec.Interface, concurrency int,
|
||||
do func(ReleaseSpec, int) error) []error {
|
||||
|
||||
return st.iterateOnReleases(helm, concurrency, st.Releases, do)
|
||||
}
|
||||
|
||||
func (st *HelmState) iterateOnReleases(helm helmexec.Interface, concurrency int, inputs []ReleaseSpec,
|
||||
do func(ReleaseSpec, int) error) []error {
|
||||
var errs []error
|
||||
|
||||
inputs := st.Releases
|
||||
inputsSize := len(inputs)
|
||||
|
||||
releases := make(chan ReleaseSpec)
|
||||
@@ -71,21 +76,16 @@ func (st *HelmState) scatterGatherReleases(helm helmexec.Interface, concurrency
|
||||
func(id int) {
|
||||
for release := range releases {
|
||||
err := do(release, id)
|
||||
st.logger.Debugf("sending result for release: %s\n", release.Name)
|
||||
st.logger.Debugf("release %q processed", release.Name)
|
||||
results <- result{release: release, err: err}
|
||||
st.logger.Debugf("sent result for release: %s\n", release.Name)
|
||||
}
|
||||
},
|
||||
func() {
|
||||
for i := range inputs {
|
||||
st.logger.Debugf("receiving result %d", i)
|
||||
for range inputs {
|
||||
r := <-results
|
||||
if r.err != nil {
|
||||
errs = append(errs, fmt.Errorf("release \"%s\" failed: %v", r.release.Name, r.err))
|
||||
} else {
|
||||
st.logger.Debugf("received result for release \"%s\"", r.release.Name)
|
||||
}
|
||||
st.logger.Debugf("received result for %d", i)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -96,3 +96,105 @@ func (st *HelmState) scatterGatherReleases(helm helmexec.Interface, concurrency
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type PlanOpts struct {
|
||||
Reverse bool
|
||||
}
|
||||
|
||||
func (st *HelmState) PlanReleases(reverse bool) ([][]Release, error) {
|
||||
marked, err := st.SelectReleasesWithOverrides()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groups, err := SortedReleaseGroups(marked, reverse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func SortedReleaseGroups(releases []Release, reverse bool) ([][]Release, error) {
|
||||
groups, err := GroupReleasesByDependency(releases)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if reverse {
|
||||
sort.Slice(groups, func(i, j int) bool {
|
||||
return j < i
|
||||
})
|
||||
}
|
||||
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func GroupReleasesByDependency(releases []Release) ([][]Release, error) {
|
||||
idToReleases := map[string][]Release{}
|
||||
idToIndex := map[string]int{}
|
||||
|
||||
d := dag.New()
|
||||
for i, r := range releases {
|
||||
|
||||
id := ReleaseToID(&r.ReleaseSpec)
|
||||
|
||||
idToReleases[id] = append(idToReleases[id], r)
|
||||
idToIndex[id] = i
|
||||
|
||||
// Only compute dependencies from non-filtered releases
|
||||
if !r.Filtered {
|
||||
d.Add(id, dag.Dependencies(r.Needs))
|
||||
}
|
||||
}
|
||||
|
||||
for _, r := range releases {
|
||||
if !r.Filtered {
|
||||
for _, n := range r.Needs {
|
||||
if _, ok := idToReleases[n]; !ok {
|
||||
id := ReleaseToID(&r.ReleaseSpec)
|
||||
return nil, fmt.Errorf("%q depends on nonexistent release %q", id, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plan, err := d.Plan()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result [][]Release
|
||||
|
||||
for groupIndex := 0; groupIndex < len(plan); groupIndex++ {
|
||||
dagNodesInGroup := plan[groupIndex]
|
||||
|
||||
var idsInGroup []string
|
||||
var releasesInGroup []Release
|
||||
|
||||
for _, node := range dagNodesInGroup {
|
||||
idsInGroup = append(idsInGroup, node.Id)
|
||||
}
|
||||
|
||||
// Make the helmfile behavior deterministic for reproducibility and ease of testing
|
||||
// We try to keep the order of definitions to keep backward-compatibility
|
||||
// See https://github.com/roboll/helmfile/issues/988
|
||||
sort.Slice(idsInGroup, func(i, j int) bool {
|
||||
ii := idToIndex[idsInGroup[i]]
|
||||
ij := idToIndex[idsInGroup[j]]
|
||||
return ii < ij
|
||||
})
|
||||
|
||||
for _, id := range idsInGroup {
|
||||
releases, ok := idToReleases[id]
|
||||
if !ok {
|
||||
panic(fmt.Errorf("bug: unexpectedly failed to get releases for id %q", id))
|
||||
}
|
||||
releasesInGroup = append(releasesInGroup, releases...)
|
||||
}
|
||||
|
||||
result = append(result, releasesInGroup)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
+542
-338
File diff suppressed because it is too large
Load Diff
+7
@@ -0,0 +1,7 @@
|
||||
addons:
|
||||
mychart:
|
||||
skip: true
|
||||
name: mychart
|
||||
namespace: kube-system
|
||||
chart: stable/mychart
|
||||
version: 1.0.0
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
addons:
|
||||
mychart:
|
||||
skip: false
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
components:
|
||||
etcd-operator:
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
components:
|
||||
etcd-operator:
|
||||
version: 0.10.3
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
components:
|
||||
etcd-operator:
|
||||
version: 0.10.0
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
components:
|
||||
etcd-operator:
|
||||
version: 0.10.3
|
||||
prometheus:
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
affinity: {}
|
||||
+23
-3
@@ -19,12 +19,32 @@ type EnvironmentTemplateData struct {
|
||||
Values map[string]interface{}
|
||||
}
|
||||
|
||||
// releaseTemplateData provides variables accessible while executing golang text/template expressions in releases of a helmfile YAML file
|
||||
// releaseTemplateData provides variables accessible while executing golang text/template expressions in release templates
|
||||
// and release values templates within a Helmfile YAML file.
|
||||
type releaseTemplateData struct {
|
||||
// Environment is accessible as `.Environment` from any template expression executed by the renderer
|
||||
Environment environment.Environment
|
||||
// Release is accessible as `.Release` from any template expression executed by the renderer
|
||||
Release ReleaseSpec
|
||||
// Release is accessible as `.Release` from any template expression executed by the renderer.
|
||||
// It contains a subset of ReleaseSpec that is known to be useful to dynamically render values.
|
||||
Release releaseTemplateDataRelease
|
||||
// Values is accessible as `.Values` and it contains default state values overrode by environment values and override values.
|
||||
Values map[string]interface{}
|
||||
// Namespace is HelmState.OverrideNamespace.
|
||||
// You should better use Release.Namespace as it might work as you'd expect even if OverrideNamespace is not set.
|
||||
// See releaseTemplateDataRelease.Namespace for more information.
|
||||
Namespace string
|
||||
}
|
||||
|
||||
type releaseTemplateDataRelease struct {
|
||||
// Name is basically ReleaseSpec.Name exposed to the template
|
||||
Name string
|
||||
|
||||
// Namespace is HelmState.OverrideNamespace, or if it's empty, ReleaseSpec.Namespace.
|
||||
Namespace string
|
||||
|
||||
// Labels is ReleaseSpec.Labels
|
||||
Labels map[string]string
|
||||
|
||||
// Chart is ReleaseSpec.Chart
|
||||
Chart string
|
||||
}
|
||||
|
||||
+9
-3
@@ -21,7 +21,8 @@ func isLocalChart(chart string) bool {
|
||||
return chart == "" ||
|
||||
chart[0] == '/' ||
|
||||
strings.Index(chart, "/") == -1 ||
|
||||
len(strings.Split(chart, "/")) != 2
|
||||
(len(strings.Split(chart, "/")) != 2 &&
|
||||
len(strings.Split(chart, "/")) != 3)
|
||||
}
|
||||
|
||||
func resolveRemoteChart(repoAndChart string) (string, string, bool) {
|
||||
@@ -29,8 +30,13 @@ func resolveRemoteChart(repoAndChart string) (string, string, bool) {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
parts := strings.Split(repoAndChart, "/")
|
||||
if len(parts) != 2 {
|
||||
uriLike := strings.Index(repoAndChart, "://") > -1
|
||||
if uriLike {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
parts := strings.SplitN(repoAndChart, "/", 2)
|
||||
if len(parts) < 2 {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
|
||||
+10
-4
@@ -15,13 +15,17 @@ func TestIsLocalChart(t *testing.T) {
|
||||
input: "stable/mysql",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: "center/stable/mysql",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: "./charts/myapp",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
input: "charts/mysubsystem/myapp",
|
||||
expected: true,
|
||||
input: "center/stable/myapp",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: "./charts/mysubsystem/myapp",
|
||||
@@ -77,8 +81,10 @@ func TestResolveRemortChart(t *testing.T) {
|
||||
remote: false,
|
||||
},
|
||||
{
|
||||
input: "charts/mysubsystem/myapp",
|
||||
remote: false,
|
||||
input: "center/stable/myapp",
|
||||
repo: "center",
|
||||
chart: "stable/myapp",
|
||||
remote: true,
|
||||
},
|
||||
{
|
||||
input: "./charts/mysubsystem/myapp",
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package testhelper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/aryann/difflib"
|
||||
)
|
||||
|
||||
func Diff(want, got string, context int) (string, bool) {
|
||||
records := difflib.Diff(
|
||||
strings.Split(want, "\n"),
|
||||
strings.Split(got, "\n"),
|
||||
)
|
||||
|
||||
w := &bytes.Buffer{}
|
||||
|
||||
changed := checkAndPrintRecords(w, records, context)
|
||||
|
||||
return w.String(), changed
|
||||
}
|
||||
|
||||
func checkAndPrintRecords(w io.Writer, records []difflib.DiffRecord, context int) bool {
|
||||
var changed bool
|
||||
if context >= 0 {
|
||||
distances := calculateDistances(records)
|
||||
omitting := false
|
||||
for i, diff := range records {
|
||||
if diff.Delta != difflib.Common {
|
||||
changed = true
|
||||
}
|
||||
if distances[i] > context {
|
||||
if !omitting {
|
||||
fmt.Fprintln(w, "...")
|
||||
omitting = true
|
||||
}
|
||||
} else {
|
||||
omitting = false
|
||||
fmt.Fprintln(w, formatRecord(diff))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, diff := range records {
|
||||
if diff.Delta != difflib.Common {
|
||||
changed = true
|
||||
}
|
||||
fmt.Fprintln(w, formatRecord(diff))
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
func formatRecord(diff difflib.DiffRecord) string {
|
||||
var prefix string
|
||||
switch diff.Delta {
|
||||
case difflib.RightOnly:
|
||||
prefix = "+ "
|
||||
case difflib.LeftOnly:
|
||||
prefix = "- "
|
||||
case difflib.Common:
|
||||
prefix = " "
|
||||
}
|
||||
|
||||
return prefix + diff.Payload
|
||||
}
|
||||
|
||||
// Shamelessly and thankfully copied from https://github.com/databus23/helm-diff/blob/99b8474af7726ca6f57b37b0b8b8f3cd36c991e8/diff/diff.go#L116
|
||||
func calculateDistances(diffs []difflib.DiffRecord) map[int]int {
|
||||
distances := map[int]int{}
|
||||
|
||||
// Iterate forwards through diffs, set 'distance' based on closest 'change' before this line
|
||||
change := -1
|
||||
for i, diff := range diffs {
|
||||
if diff.Delta != difflib.Common {
|
||||
change = i
|
||||
}
|
||||
distance := math.MaxInt32
|
||||
if change != -1 {
|
||||
distance = i - change
|
||||
}
|
||||
distances[i] = distance
|
||||
}
|
||||
|
||||
// Iterate backwards through diffs, reduce 'distance' based on closest 'change' after this line
|
||||
change = -1
|
||||
for i := len(diffs) - 1; i >= 0; i-- {
|
||||
diff := diffs[i]
|
||||
if diff.Delta != difflib.Common {
|
||||
change = i
|
||||
}
|
||||
if change != -1 {
|
||||
distance := change - i
|
||||
if distance < distances[i] {
|
||||
distances[i] = distance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return distances
|
||||
}
|
||||
@@ -70,7 +70,7 @@ func (f *TestFs) ReadFile(filename string) ([]byte, error) {
|
||||
str, ok = f.files[filepath.Join(f.Cwd, filename)]
|
||||
}
|
||||
if !ok {
|
||||
return []byte(nil), fmt.Errorf("no registered file found: %s", filename)
|
||||
return []byte(nil), os.ErrNotExist
|
||||
}
|
||||
|
||||
f.fileReaderCalls += 1
|
||||
|
||||
+52
-39
@@ -2,6 +2,8 @@ package tmpl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io"
|
||||
"os"
|
||||
@@ -16,15 +18,18 @@ type Values = map[string]interface{}
|
||||
|
||||
func (c *Context) createFuncMap() template.FuncMap {
|
||||
funcMap := template.FuncMap{
|
||||
"exec": c.Exec,
|
||||
"readFile": c.ReadFile,
|
||||
"toYaml": ToYaml,
|
||||
"fromYaml": FromYaml,
|
||||
"setValueAtPath": SetValueAtPath,
|
||||
"requiredEnv": RequiredEnv,
|
||||
"get": get,
|
||||
"getOrNil": getOrNil,
|
||||
"tpl": c.Tpl,
|
||||
"exec": c.Exec,
|
||||
"readFile": c.ReadFile,
|
||||
"toYaml": ToYaml,
|
||||
"fromYaml": FromYaml,
|
||||
"setValueAtPath": SetValueAtPath,
|
||||
"requiredEnv": RequiredEnv,
|
||||
"get": get,
|
||||
"getOrNil": getOrNil,
|
||||
"tpl": c.Tpl,
|
||||
"required": Required,
|
||||
"fetchSecretValue": fetchSecretValue,
|
||||
"expandSecretRefs": fetchSecretValues,
|
||||
}
|
||||
if c.preRender {
|
||||
// disable potential side-effect template calls
|
||||
@@ -58,60 +63,56 @@ func (c *Context) Exec(command string, args []interface{}, inputs ...string) (st
|
||||
cmd := exec.Command(command, strArgs...)
|
||||
cmd.Dir = c.basePath
|
||||
|
||||
writeErrs := make(chan error)
|
||||
cmdErrs := make(chan error)
|
||||
cmdOuts := make(chan []byte)
|
||||
g := errgroup.Group{}
|
||||
|
||||
if len(input) > 0 {
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
go func(input string, stdin io.WriteCloser) {
|
||||
|
||||
g.Go(func() error {
|
||||
defer stdin.Close()
|
||||
defer close(writeErrs)
|
||||
|
||||
size := len(input)
|
||||
|
||||
var n int
|
||||
var err error
|
||||
i := 0
|
||||
|
||||
for {
|
||||
n, err = io.WriteString(stdin, input[i:])
|
||||
n, err := io.WriteString(stdin, input[i:])
|
||||
if err != nil {
|
||||
writeErrs <- fmt.Errorf("failed while writing %d bytes to stdin of \"%s\": %v", len(input), command, err)
|
||||
break
|
||||
return fmt.Errorf("failed while writing %d bytes to stdin of \"%s\": %v", len(input), command, err)
|
||||
}
|
||||
|
||||
i += n
|
||||
if n == size {
|
||||
break
|
||||
|
||||
if i == size {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}(input, stdin)
|
||||
})
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer close(cmdOuts)
|
||||
defer close(cmdErrs)
|
||||
var bytes []byte
|
||||
|
||||
bytes, err := cmd.Output()
|
||||
g.Go(func() error {
|
||||
// We use CombinedOutput to produce helpful error messages
|
||||
// See https://github.com/roboll/helmfile/issues/1158
|
||||
bs, err := helmexec.Output(cmd)
|
||||
if err != nil {
|
||||
cmdErrs <- fmt.Errorf("exec cmd=%s args=[%s] failed: %v", command, strings.Join(strArgs, ", "), err)
|
||||
} else {
|
||||
cmdOuts <- bytes
|
||||
return err
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case bytes := <-cmdOuts:
|
||||
return string(bytes), nil
|
||||
case err := <-cmdErrs:
|
||||
return "", err
|
||||
case err := <-writeErrs:
|
||||
return "", err
|
||||
}
|
||||
bytes = bs
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
func (c *Context) ReadFile(filename string) (string, error) {
|
||||
@@ -206,3 +207,15 @@ func RequiredEnv(name string) (string, error) {
|
||||
|
||||
return "", fmt.Errorf("required env var `%s` is not set", name)
|
||||
}
|
||||
|
||||
func Required(warn string, val interface{}) (interface{}, error) {
|
||||
if val == nil {
|
||||
return nil, fmt.Errorf(warn)
|
||||
} else if _, ok := val.(string); ok {
|
||||
if val == "" {
|
||||
return nil, fmt.Errorf(warn)
|
||||
}
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
@@ -133,3 +133,47 @@ func TestTpl(t *testing.T) {
|
||||
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequired(t *testing.T) {
|
||||
type args struct {
|
||||
warn string
|
||||
val interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want interface{}
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "required val is nil",
|
||||
args: args{warn: "This value is required", val: nil},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "required val is empty string",
|
||||
args: args{warn: "This value is required", val: ""},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "required val is existed",
|
||||
args: args{warn: "This value is required", val: "foo"},
|
||||
want: "foo",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := Required(tt.args.warn, tt.args.val)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Required() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Required() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,31 @@ package tmpl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/Masterminds/sprig"
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
func (c *Context) stringTemplate() *template.Template {
|
||||
func (c *Context) newTemplate() *template.Template {
|
||||
aliased := template.FuncMap{}
|
||||
|
||||
aliases := map[string]string{
|
||||
"get": "sprigGet",
|
||||
}
|
||||
|
||||
funcMap := sprig.TxtFuncMap()
|
||||
|
||||
for orig, alias := range aliases {
|
||||
aliased[alias] = funcMap[orig]
|
||||
}
|
||||
|
||||
for name, f := range c.createFuncMap() {
|
||||
funcMap[name] = f
|
||||
}
|
||||
|
||||
for name, f := range aliased {
|
||||
funcMap[name] = f
|
||||
}
|
||||
|
||||
tmpl := template.New("stringTemplate").Funcs(funcMap)
|
||||
if c.preRender {
|
||||
tmpl = tmpl.Option("missingkey=zero")
|
||||
@@ -21,7 +37,7 @@ func (c *Context) stringTemplate() *template.Template {
|
||||
}
|
||||
|
||||
func (c *Context) RenderTemplateToBuffer(s string, data ...interface{}) (*bytes.Buffer, error) {
|
||||
var t, parseErr = c.stringTemplate().Parse(s)
|
||||
var t, parseErr = c.newTemplate().Parse(s)
|
||||
if parseErr != nil {
|
||||
return nil, parseErr
|
||||
}
|
||||
|
||||
@@ -250,3 +250,81 @@ func Test_renderTemplateToString(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderTemplate_Required(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
s string
|
||||
data map[string]interface{}
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: ".foo is existed",
|
||||
s: `{{ required ".foo.bar is required" .foo }}`,
|
||||
data: map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
want: "bar",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: ".foo.bar is existed",
|
||||
s: `{{ required "foo.bar is required" .foo.bar }}`,
|
||||
data: map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"bar": "FOO_BAR",
|
||||
},
|
||||
},
|
||||
want: "FOO_BAR",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: ".foo.bar is existed but value is nil",
|
||||
s: `{{ required "foo.bar is required" .foo.bar }}`,
|
||||
data: map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"bar": nil,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: ".foo.bar is existed but value is empty string",
|
||||
s: `{{ required "foo.bar is required" .foo.bar }}`,
|
||||
data: map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"bar": "",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: ".foo is nil",
|
||||
s: `{{ required "foo is required" .foo }}`,
|
||||
data: map[string]interface{}{
|
||||
"foo": nil,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: ".foo is a empty string",
|
||||
s: `{{ required "foo is required" .foo }}`,
|
||||
data: map[string]interface{}{
|
||||
"foo": "",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got, err := renderTemplateToString(tt.s, tt.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("renderTemplateToString() for %s error = %v, wantErr %v", tt.name, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("renderTemplateToString() for %s = %v, want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package tmpl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/roboll/helmfile/pkg/plugins"
|
||||
"github.com/variantdev/vals"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//to generate mock run mockgen -source=expand_secret_ref.go -destination=expand_secrets_mock.go -package=tmpl
|
||||
type valClient interface {
|
||||
Eval(template map[string]interface{}) (map[string]interface{}, error)
|
||||
}
|
||||
|
||||
var once sync.Once
|
||||
var secretsClient valClient
|
||||
|
||||
func fetchSecretValue(path string) (string, error) {
|
||||
tmpMap := make(map[string]interface{})
|
||||
tmpMap["key"] = path
|
||||
resultMap, err := fetchSecretValues(tmpMap)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
rendered, ok := resultMap["key"]
|
||||
if !ok {
|
||||
return "", errors.New(fmt.Sprintf("unexpected error occurred, %v doesn't have 'key' key", resultMap))
|
||||
}
|
||||
|
||||
result, ok := rendered.(string)
|
||||
if !ok {
|
||||
return "", errors.New(fmt.Sprintf("expected %v to be string", rendered))
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func fetchSecretValues(values map[string]interface{}) (map[string]interface{}, error) {
|
||||
var err error
|
||||
// below lines are for tests
|
||||
once.Do(func() {
|
||||
var valRuntime *vals.Runtime
|
||||
if secretsClient == nil {
|
||||
valRuntime, err = plugins.ValsInstance()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
secretsClient = valRuntime
|
||||
}
|
||||
})
|
||||
if secretsClient == nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resultMap, err := secretsClient.Eval(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resultMap, nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package tmpl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/golang/mock/gomock"
|
||||
"gotest.tools/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_fetchSecretValue(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
c := NewMockvalClient(controller)
|
||||
secretsClient = c
|
||||
|
||||
secretPath := "ref+vault://key/#path"
|
||||
expectArg := make(map[string]interface{})
|
||||
expectArg["key"] = secretPath
|
||||
|
||||
valsResult := make(map[string]interface{})
|
||||
valsResult["key"] = "key_value"
|
||||
c.EXPECT().Eval(expectArg).Return(valsResult, nil)
|
||||
result, err := fetchSecretValue(secretPath)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, result, "key_value")
|
||||
}
|
||||
|
||||
func Test_fetchSecretValue_error(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
c := NewMockvalClient(controller)
|
||||
secretsClient = c
|
||||
|
||||
secretPath := "ref+vault://key/#path"
|
||||
expectArg := make(map[string]interface{})
|
||||
expectArg["key"] = secretPath
|
||||
|
||||
expectedErr := errors.New("some error")
|
||||
c.EXPECT().Eval(expectArg).Return(nil, expectedErr)
|
||||
result, err := fetchSecretValue(secretPath)
|
||||
assert.Equal(t, err, expectedErr)
|
||||
assert.Equal(t, result, "")
|
||||
}
|
||||
|
||||
func Test_fetchSecretValue_no_key(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
c := NewMockvalClient(controller)
|
||||
secretsClient = c
|
||||
|
||||
secretPath := "ref+vault://key/#path"
|
||||
expectArg := make(map[string]interface{})
|
||||
expectArg["key"] = secretPath
|
||||
|
||||
valsResult := make(map[string]interface{})
|
||||
c.EXPECT().Eval(expectArg).Return(valsResult, nil)
|
||||
result, err := fetchSecretValue(secretPath)
|
||||
assert.Error(t, err, "unexpected error occurred, map[] doesn't have 'key' key")
|
||||
assert.Equal(t, result, "")
|
||||
}
|
||||
|
||||
func Test_fetchSecretValue_invalid_type(t *testing.T) {
|
||||
controller := gomock.NewController(t)
|
||||
defer controller.Finish()
|
||||
c := NewMockvalClient(controller)
|
||||
secretsClient = c
|
||||
|
||||
secretPath := "ref+vault://key/#path"
|
||||
expectArg := make(map[string]interface{})
|
||||
expectArg["key"] = secretPath
|
||||
|
||||
valsResult := make(map[string]interface{})
|
||||
valsResult["key"] = 10
|
||||
c.EXPECT().Eval(expectArg).Return(valsResult, nil)
|
||||
result, err := fetchSecretValue(secretPath)
|
||||
assert.Error(t, err, "expected 10 to be string")
|
||||
assert.Equal(t, result, "")
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: expand_secret_ref.go
|
||||
|
||||
package tmpl
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockvalClient is a mock of valClient interface
|
||||
type MockvalClient struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockvalClientMockRecorder
|
||||
}
|
||||
|
||||
// MockvalClientMockRecorder is the mock recorder for MockvalClient
|
||||
type MockvalClientMockRecorder struct {
|
||||
mock *MockvalClient
|
||||
}
|
||||
|
||||
// NewMockvalClient creates a new mock instance
|
||||
func NewMockvalClient(ctrl *gomock.Controller) *MockvalClient {
|
||||
mock := &MockvalClient{ctrl: ctrl}
|
||||
mock.recorder = &MockvalClientMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (_m *MockvalClient) EXPECT() *MockvalClientMockRecorder {
|
||||
return _m.recorder
|
||||
}
|
||||
|
||||
// Eval mocks base method
|
||||
func (_m *MockvalClient) Eval(template map[string]interface{}) (map[string]interface{}, error) {
|
||||
ret := _m.ctrl.Call(_m, "Eval", template)
|
||||
ret0, _ := ret[0].(map[string]interface{})
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Eval indicates an expected call of Eval
|
||||
func (_mr *MockvalClientMockRecorder) Eval(arg0 interface{}) *gomock.Call {
|
||||
return _mr.mock.ctrl.RecordCallWithMethodType(_mr.mock, "Eval", reflect.TypeOf((*MockvalClient)(nil).Eval), arg0)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package tmpl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMergeOverwrite(t *testing.T) {
|
||||
ctx := &Context{}
|
||||
buf, err := ctx.RenderTemplateToBuffer(`
|
||||
{{- $v1 := dict "bool" true "int" 2 "str" "v1" "str2" "v1" -}}
|
||||
{{- $v2 := dict "bool" false "int" 0 "str" "v2" "str2" "" -}}
|
||||
{{- $mo1 := mergeOverwrite (dict) $v1 $v2 }}
|
||||
{{- $mo1 -}}
|
||||
`)
|
||||
expected := "map[bool:false int:0 str:v2 str2:]"
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
actual := buf.String()
|
||||
if actual != expected {
|
||||
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
repositories:
|
||||
- name: incubator
|
||||
url: https://kubernetes-charts-incubator.storage.googleapis.com
|
||||
|
||||
releases:
|
||||
- name: kustomapp
|
||||
chart: ./kustomapp
|
||||
values:
|
||||
- namePrefix: kustomapp-
|
||||
- name: raw1
|
||||
chart: incubator/raw
|
||||
version: 0.1.0
|
||||
values:
|
||||
- resources:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: raw1
|
||||
namespace: default
|
||||
data:
|
||||
foo: FOO
|
||||
strategicMergePatches:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: raw1
|
||||
namespace: default
|
||||
data:
|
||||
bar: BAR
|
||||
- name: raw2
|
||||
chart: incubator/raw
|
||||
values:
|
||||
- resources:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: raw2
|
||||
namespace: default
|
||||
data:
|
||||
foo: FOO
|
||||
jsonPatches:
|
||||
- target:
|
||||
version: v1
|
||||
kind: ConfigMap
|
||||
name: raw2
|
||||
namespace: default
|
||||
patch:
|
||||
- op: replace
|
||||
path: /data/baz
|
||||
value: "BAZ"
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: kustomapp
|
||||
namespace: default
|
||||
data:
|
||||
kustomappfoo: FOO
|
||||
@@ -0,0 +1,2 @@
|
||||
resources:
|
||||
- all.yaml
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: kustomapp2
|
||||
namespace: default
|
||||
data:
|
||||
kustomappfoo: FOO
|
||||
@@ -0,0 +1,2 @@
|
||||
resources:
|
||||
- all.yaml
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
description: Chart for testing helmx features
|
||||
name: helmx
|
||||
version: 0.1.0
|
||||
@@ -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 -}}
|
||||
@@ -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 }}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user