mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-22 16:37:08 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b910591e1d | ||
|
|
d807510dd7 | ||
|
|
4e485219d7 | ||
|
|
b0e042988c | ||
|
|
f877de7778 | ||
|
|
655d5934a2 | ||
|
|
f6bf885fb7 | ||
|
|
2857382006 | ||
|
|
972667f115 | ||
|
|
3c88145329 | ||
|
|
709913b5ea | ||
|
|
88884b68dc | ||
|
|
5a15b65b94 | ||
|
|
3899680672 | ||
|
|
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 | ||
|
|
921f69bae7 | ||
|
|
10a9a16f3d | ||
|
|
ba2e522617 | ||
|
|
2e98e907b0 | ||
|
|
b762ab0b78 | ||
|
|
216c228c0b | ||
|
|
ef63a05513 | ||
|
|
06b0c99a0b | ||
|
|
fd0133e10a | ||
|
|
9d851cda3b | ||
|
|
f79db2ec8d | ||
|
|
94a6fcfb9f | ||
|
|
fb2041555e | ||
|
|
cbf5b8b1e7 | ||
|
|
267e0fa1fe | ||
|
|
5488198d6d | ||
|
|
4bbd09ccb2 | ||
|
|
4e4f1bee59 | ||
|
|
6dfc9be0d1 | ||
|
|
1823bb1e5b | ||
|
|
4bc99337b2 | ||
|
|
01ae59fedd | ||
|
|
2a6bd24e3c | ||
|
|
11d0abba6e | ||
|
|
dd58badf81 | ||
|
|
e24c15324b | ||
|
|
ba0891b7b7 | ||
|
|
b58e082a8f | ||
|
|
bf9dcc0982 | ||
|
|
cd5d906afb | ||
|
|
a584aeab2e | ||
|
|
765bfe6cfd | ||
|
|
622cba9f19 | ||
|
|
63a337ee57 | ||
|
|
6baad71b1f | ||
|
|
bce2f4728b | ||
|
|
e5038fb04f | ||
|
|
fe5102e777 | ||
|
|
eda961edc2 | ||
|
|
b2a6231dcf | ||
|
|
4166b418c9 | ||
|
|
408bf7b8bf | ||
|
|
2f9f52033c | ||
|
|
63b5040ec4 | ||
|
|
4cc40cf0f1 | ||
|
|
1df9b0e9e9 | ||
|
|
b82f77ee50 | ||
|
|
e6bb8282db | ||
|
|
b6a04d012c | ||
|
|
9eef44ffe6 | ||
|
|
cfe309ec21 | ||
|
|
65818cd345 | ||
|
|
ddb5be1b9d | ||
|
|
f61334d9bb | ||
|
|
3bf0f7d54e | ||
|
|
7dec948950 | ||
|
|
e0d0a1cf7f | ||
|
|
31768b5bac | ||
|
|
1bde201eae | ||
|
|
1831d3375f | ||
|
|
2e38f42a40 | ||
|
|
1da3488599 | ||
|
|
78b03e0d92 | ||
|
|
11eda66eaa | ||
|
|
65d404b276 | ||
|
|
72425aebfc | ||
|
|
34fbfb4fd0 | ||
|
|
56c27c2bd9 | ||
|
|
34c793d87e | ||
|
|
b9862bdae6 | ||
|
|
e3e7905f07 | ||
|
|
2e57beb448 | ||
|
|
3986f6497e | ||
|
|
820abbc06d | ||
|
|
3710f6233e | ||
|
|
2d2b3e486f | ||
|
|
65ee6a2124 | ||
|
|
1d3f5f8a33 | ||
|
|
e2d6dc4afa | ||
|
|
f6057a1cca | ||
|
|
c68fc5bc50 | ||
|
|
68b95f14d4 | ||
|
|
9318d8a040 | ||
|
|
44686298f2 | ||
|
|
206372b7aa | ||
|
|
e1c04971c1 | ||
|
|
b3f4586ed8 | ||
|
|
591086dac9 | ||
|
|
1226ea6d1a | ||
|
|
681c866ce1 | ||
|
|
dd70c857a5 | ||
|
|
a896f801ab | ||
|
|
4a5996d083 | ||
|
|
f6264bfa9d | ||
|
|
3b3d3092dd | ||
|
|
1012256f16 | ||
|
|
90390492a3 | ||
|
|
4c9c42d3c5 | ||
|
|
bd60548f10 | ||
|
|
0534117b62 | ||
|
|
0104c91fce | ||
|
|
a769d1a82a | ||
|
|
b0179218b9 | ||
|
|
c9a43ad9cb |
+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
|
||||
+4
-1
@@ -1,3 +1,6 @@
|
||||
dist/
|
||||
.idea/
|
||||
helmfile
|
||||
helmfile
|
||||
helmfile.lock
|
||||
test/integration/tmp
|
||||
vendor/
|
||||
|
||||
+29
-9
@@ -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,22 +7,42 @@ RUN make static-linux
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
FROM alpine:3.8
|
||||
FROM alpine:3.11
|
||||
|
||||
RUN apk add --no-cache ca-certificates git bash curl
|
||||
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
|
||||
|
||||
RUN mkdir -p "$(helm home)/plugins"
|
||||
# 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", "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/futuresimple/helm-secrets && \
|
||||
helm plugin install https://github.com/hypnoglow/helm-s3.git && \
|
||||
helm plugin install https://github.com/aslafy-z/helm-git.git && \
|
||||
helm plugin install https://github.com/rimusz/helm-tiller
|
||||
|
||||
COPY --from=builder /workspace/helmfile/dist/helmfile_linux_amd64 /usr/local/bin/helmfile
|
||||
|
||||
|
||||
@@ -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 '!freebsd !netbsd' -arch '!arm' -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" -ldflags '-X main.Version=${TAG}' ${TARGETS}
|
||||
env CGO_ENABLED=0 gox -os '!openbsd !freebsd !netbsd' -arch '!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 go build -o "dist/helmfile_linux_amd64" -ldflags '-X main.Version=${TAG}' ${TARGETS}
|
||||
.PHONY: linux
|
||||
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
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
# helmfile [](https://circleci.com/gh/roboll/helmfile)
|
||||
# Helmfile [](https://circleci.com/gh/roboll/helmfile)
|
||||
|
||||
Deploy Kubernetes Helm Charts
|
||||
|
||||
[](https://quay.io/repository/roboll/helmfile)
|
||||
[](https://slack.sweetops.com)
|
||||
|
||||
## status
|
||||
## Status
|
||||
|
||||
Even though Helmfile is used in production environments [across multiple organizations](USERS.md), it is still in its early stage of development, hence versioned 0.x.
|
||||
|
||||
Helmfile complies to Semantic Versioning 2.0.0 in which v0.x means that there could be backward-incompatible changes for every release.
|
||||
|
||||
Note that we will try our best to document any backward incompatibility.
|
||||
Note that we will try our best to document any backward incompatibility. And in reality, helmfile had no breaking change for a year or so.
|
||||
|
||||
## about
|
||||
## About
|
||||
|
||||
Helmfile is a declarative spec for deploying helm charts. It lets you...
|
||||
|
||||
@@ -23,38 +23,77 @@ Helmfile is a declarative spec for deploying helm charts. It lets you...
|
||||
|
||||
To avoid upgrades for each iteration of `helm`, the `helmfile` executable delegates to `helm` - as a result, `helm` must be installed.
|
||||
|
||||
## configuration syntax
|
||||
## Highlights
|
||||
|
||||
**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.31.0](https://github.com/roboll/helmfile/tree/v0.31.0).
|
||||
**Declarative**: Write, version-control, apply the desired state file for visibility and reproducibility.
|
||||
|
||||
The default helmfile is `helmfile.yaml`:
|
||||
**Modules**: Modularize common patterns of your infrastructure, distribute it via Git, S3, etc. to be reused across the entire company (See [#648](https://github.com/roboll/helmfile/pull/648))
|
||||
|
||||
**Versatility**: Manage your cluster consisting of charts, [kustomizations](https://github.com/kubernetes-sigs/kustomize), and directories of Kubernetes resources, turning everything to Helm releases (See [#673](https://github.com/roboll/helmfile/pull/673))
|
||||
|
||||
**Patch**: JSON/Strategic-Merge Patch Kubernetes resources before `helm-install`ing, without forking upstream charts (See [#673](https://github.com/roboll/helmfile/pull/673))
|
||||
|
||||
## 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.92.1](https://github.com/roboll/helmfile/tree/v0.92.1).
|
||||
|
||||
The default name for a helmfile is `helmfile.yaml`:
|
||||
|
||||
```yaml
|
||||
# Chart repositories used from within this state file
|
||||
#
|
||||
# Use `helm-s3` and `helm-git` and whatever Helm Downloader plugins
|
||||
# to use repositories other than the official repository or one backend by chartmuseum.
|
||||
repositories:
|
||||
- name: roboll
|
||||
url: http://roboll.io/charts
|
||||
certFile: optional_client_cert
|
||||
keyFile: optional_client_key
|
||||
username: optional_username
|
||||
password: optional_password
|
||||
# To use official "stable" charts a.k.a https://github.com/helm/charts/tree/master/stable
|
||||
- name: stable
|
||||
url: https://charts.helm.sh/stable
|
||||
# To use official "incubator" charts a.k.a https://github.com/helm/charts/tree/master/incubator
|
||||
- name: incubator
|
||||
url: https://charts.helm.sh/incubator
|
||||
# helm-git powered repository: You can treat any Git repository as a charts repository
|
||||
- name: polaris
|
||||
url: git+https://github.com/reactiveops/polaris@deploy/helm?ref=master
|
||||
# Advanced configuration: You can setup basic or tls auth
|
||||
- name: roboll
|
||||
url: http://roboll.io/charts
|
||||
certFile: optional_client_cert
|
||||
keyFile: optional_client_key
|
||||
username: optional_username
|
||||
password: optional_password
|
||||
# Advanced configuration: You can use a ca bundle to use an https repo
|
||||
# with a self-signed certificate
|
||||
- name: insecure
|
||||
url: https://charts.my-insecure-domain.com
|
||||
caFile: optional_ca_crt
|
||||
|
||||
# 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
|
||||
# Path to alternative helm binary (--helm-binary)
|
||||
helmBinary: path/to/helm3
|
||||
|
||||
# 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"
|
||||
@@ -62,21 +101,48 @@ 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.
|
||||
#
|
||||
# Helmfile runs various helm commands to converge the current state in the live cluster to the desired state defined here.
|
||||
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
|
||||
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.
|
||||
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:
|
||||
# value files passed via --values
|
||||
# Value files passed via --values
|
||||
- vault.yaml
|
||||
# inline values, passed via a temporary values file and --values
|
||||
# Inline values, passed via a temporary values file and --values, so that it doesn't suffer from type issues like --set
|
||||
- address: https://vault.example.com
|
||||
# Go template available in inline values and values files.
|
||||
- image:
|
||||
# The end result is more or less YAML. So do `quote` to prevent number-like strings from accidentally parsed into numbers!
|
||||
# See https://github.com/roboll/helmfile/issues/608
|
||||
tag: {{ requiredEnv "IMAGE_TAG" | quote }}
|
||||
# Otherwise:
|
||||
# tag: "{{ requiredEnv "IMAGE_TAG" }}"
|
||||
# tag: !!string {{ requiredEnv "IMAGE_TAG" }}
|
||||
db:
|
||||
username: {{ requiredEnv "DB_USERNAME" }}
|
||||
# value taken from environment variable. Quotes are necessary. Will throw an error if the environment variable is not set. $DB_PASSWORD needs to be set in the calling environment ex: export DB_PASSWORD='password1'
|
||||
@@ -85,6 +151,8 @@ releases:
|
||||
# Interpolate environment variable with a fixed string
|
||||
domain: {{ requiredEnv "PLATFORM_ID" }}.my-domain.com
|
||||
scheme: {{ env "SCHEME" | default "https" }}
|
||||
# Use `values` whenever possible!
|
||||
# `set` translates to helm's `--set key=val`, that is known to suffer from type issues like https://github.com/roboll/helmfile/issues/608
|
||||
set:
|
||||
# single value loaded from a local file, translates to --set-file foo.config=path/to/file
|
||||
- name: foo.config
|
||||
@@ -100,23 +168,23 @@ releases:
|
||||
# will attempt to decrypt it using helm-secrets plugin
|
||||
secrets:
|
||||
- vault_secret.yaml
|
||||
# wait for k8s resources via --wait. Defaults to `false`
|
||||
# Override helmDefaults options for verify, wait, timeout, recreatePods and force.
|
||||
verify: true
|
||||
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"
|
||||
@@ -124,6 +192,16 @@ releases:
|
||||
tlsCert: "path/to/cert.pem"
|
||||
# path to TLS key file (default "$HELM_HOME/key.pem")
|
||||
tlsKey: "path/to/key.pem"
|
||||
# --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
|
||||
@@ -134,12 +212,120 @@ releases:
|
||||
- ./values/{{ requiredEnv "PLATFORM_ENV" }}/config.yaml # Values file taken from path with environment variable. $PLATFORM_ENV must be set in the calling environment.
|
||||
wait: true
|
||||
|
||||
#
|
||||
# Advanced Configuration: Nested States
|
||||
#
|
||||
helmfiles:
|
||||
- # Path to the helmfile state file being processed BEFORE releases in this state file
|
||||
path: path/to/subhelmfile.yaml
|
||||
# Label selector used for filtering releases in the nested state.
|
||||
# For example, `name=prometheus` in this context is equivalent to processing the nested state like
|
||||
# helmfile -f path/to/subhelmfile.yaml -l name=prometheus sync
|
||||
selectors:
|
||||
- name=prometheus
|
||||
# Override state values
|
||||
values:
|
||||
# Values files merged into the nested state's values
|
||||
- additional.values.yaml
|
||||
# One important aspect of using values here is that they first need to be defined in the values section
|
||||
# of the origin helmfile, so in this example key1 needs to be in the values or environments.NAME.values of path/to/subhelmfile.yaml
|
||||
# Inline state values merged into the nested state's values
|
||||
- key1: val1
|
||||
- # All the nested state files under `helmfiles:` is processed in the order of definition.
|
||||
# So it can be used for preparation for your main `releases`. An example would be creating CRDs required by `releases` in the parent state file.
|
||||
path: path/to/mycrd.helmfile.yaml
|
||||
- # Terraform-module-like URL for importing a remote directory and use a file in it as a nested-state file
|
||||
# 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
|
||||
#
|
||||
|
||||
# The list of environments managed by helmfile.
|
||||
#
|
||||
# The default is `environments: {"default": {}}` which implies:
|
||||
#
|
||||
# - `{{ .Environment.Name }}` evaluates to "default"
|
||||
# - `{{ .Values }}` being empty
|
||||
environments:
|
||||
# The "default" environment is available and used when `helmfile` is run without `--environment NAME`.
|
||||
default:
|
||||
# Everything from the values.yaml is available via `{{ .Values.KEY }}`.
|
||||
# Suppose `{"foo": {"bar": 1}}` contained in the values.yaml below,
|
||||
# `{{ .Values.foo.bar }}` is evaluated to `1`.
|
||||
values:
|
||||
- environments/default/values.yaml
|
||||
# Each entry in values can be either a file path or inline values.
|
||||
# The below is an example of inline values, which is merged to the `.Values`
|
||||
- myChartVer: 1.0.0-dev
|
||||
# Any environment other than `default` is used only when `helmfile` is run with `--environment NAME`.
|
||||
# That is, the "production" env below is used when and only when it is run like `helmfile --environment production sync`.
|
||||
production:
|
||||
values:
|
||||
- environment/production/values.yaml
|
||||
- myChartVer: 1.0.0
|
||||
# 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`.
|
||||
#
|
||||
# Possible values are "Error", "Warn", "Info", "Debug". The default is "Error".
|
||||
#
|
||||
# Use "Warn", "Info", or "Debug" if you want helmfile to not fail when a values file is missing, while just leaving
|
||||
# a message about the missing file at the log-level.
|
||||
missingFileHandler: Error
|
||||
|
||||
#
|
||||
# Advanced Configuration: Layering
|
||||
#
|
||||
# Helmfile merges all the "base" state files and this state file before processing.
|
||||
#
|
||||
# Assuming this state file is named `helmfile.yaml`, all the files are merged in the order of:
|
||||
# environments.yaml <- defaults.yaml <- templates.yaml <- helmfile.yaml
|
||||
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.
|
||||
@@ -172,15 +358,20 @@ 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/) or
|
||||
- openSUSE: install via `zypper in helmfile` assuming you are on Tumbleweed; if you are on Leap you must add the [kubic](https://download.opensuse.org/repositories/devel:/kubic/) repo for your distribution version once before that command, e.g. `zypper ar https://download.opensuse.org/repositories/devel:/kubic/openSUSE_Leap_\$releasever kubic`, or
|
||||
- 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!
|
||||
|
||||
@@ -206,23 +397,24 @@ 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.52.0
|
||||
v0.92.1
|
||||
|
||||
COMMANDS:
|
||||
deps update charts based on the contents of requirements.yaml
|
||||
repos sync repositories from state file (helm repo add && helm repo update)
|
||||
charts DEPRECATED: sync releases from state file (helm upgrade --install)
|
||||
diff diff releases from state file against env (helm diff)
|
||||
@@ -234,33 +426,57 @@ 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.
|
||||
A release must match all labels in a group in order to be used. Multiple groups can be specified at once.
|
||||
--selector tier=frontend,tier!=proxy --selector tier=backend. Will match all frontend, non-proxy releases AND all backend releases.
|
||||
The name of a release can be used as a label. --selector name=myrelease
|
||||
--allow-no-matching-release Do not exit with an error code if the provided selector has no matching releases.
|
||||
--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
|
||||
|
||||
The `helmfile sync` sub-command sync your cluster state as described in your `helmfile`. The default helmfile is `helmfile.yaml`, but any yaml file can be passed by specifying a `--file path/to/your/yaml/file` flag.
|
||||
The `helmfile sync` sub-command sync your cluster state as described in your `helmfile`. The default helmfile is `helmfile.yaml`, but any YAML file can be passed by specifying a `--file path/to/your/yaml/file` flag.
|
||||
|
||||
Under the covers, Helmfile executes `helm upgrade --install` for each `release` declared in the manifest, by optionally decrypting [secrets](#secrets) to be consumed as helm chart values. It also updates specified chart repositories and updates the
|
||||
dependencies of any referenced local charts.
|
||||
|
||||
For Helm 2.9+ you can use a username and password to authenticate to a remote repository.
|
||||
|
||||
### deps
|
||||
|
||||
The `helmfile deps` sub-command locks your helmfile state and local charts dependencies.
|
||||
|
||||
It basically runs `helm dependency update` on your helmfile state file and all the referenced local charts, so that you get a "lock" file per each helmfile state or local chart.
|
||||
|
||||
All the other `helmfile` sub-commands like `sync` use chart versions recorded in the lock files, so that e.g. untested chart versions won't suddenly get deployed to the production environment.
|
||||
|
||||
For example, the lock file for a helmfile state file named `helmfile.1.yaml` will be `helmfile.1.lock`. The lock file for a local chart would be `requirements.lock`, which is the same as `helm`.
|
||||
|
||||
It is recommended to version-control all the lock files, so that they can be used in the production deployment pipeline for extra reproducibility.
|
||||
|
||||
To bring in chart updates systematically, it would also be a good idea to run `helmfile deps` regularly, test it, and then update the lock files in the version-control system.
|
||||
|
||||
### diff
|
||||
|
||||
The `helmfile diff` sub-command executes the [helm-diff](https://github.com/databus23/helm-diff) plugin across all of
|
||||
@@ -311,6 +527,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:
|
||||
@@ -319,9 +536,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.
|
||||
@@ -330,10 +548,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:
|
||||
@@ -347,13 +600,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:
|
||||
@@ -375,7 +629,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:
|
||||
@@ -415,10 +669,10 @@ releases:
|
||||
namespace: {{ requiredEnv "NAME" }}
|
||||
chart: roboll/vault-secret-manager
|
||||
values:
|
||||
- values.yaml.tmpl
|
||||
- values.yaml.gotmpl
|
||||
```
|
||||
|
||||
`values.yaml.tmpl`:
|
||||
`values.yaml.gotmpl`:
|
||||
|
||||
```yaml
|
||||
db:
|
||||
@@ -448,11 +702,9 @@ environments:
|
||||
production:
|
||||
|
||||
releases:
|
||||
|
||||
{{ if eq .Environment.Name "production" }}
|
||||
- name: newrelic-agent
|
||||
installed: {{ eq .Environment.Name "production" | toYaml }}
|
||||
# snip
|
||||
{{ end }}
|
||||
- name: myapp
|
||||
# snip
|
||||
```
|
||||
@@ -488,7 +740,7 @@ releaseName: prod
|
||||
`values.yaml.gotmpl`
|
||||
|
||||
```yaml
|
||||
domain: {{ .Environment.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`,
|
||||
@@ -515,17 +767,45 @@ environments:
|
||||
- other.yaml.gotmpl # template directives with potential side-effects like `exec` and `readFile` will be honoured
|
||||
|
||||
releases:
|
||||
- name: myapp-{{ .Environment.Values.releaseName }} # release name will be one of `dev` or `prod` depending on selected environment
|
||||
- name: myapp-{{ .Values.releaseName }} # release name will be one of `dev` or `prod` depending on selected environment
|
||||
values:
|
||||
- values.yaml.gotmpl
|
||||
|
||||
{{ if eq (.Environment.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 on Environment.Values vs Values
|
||||
|
||||
The `{{ .Values.foo }}` syntax is the recommended way of using environment values.
|
||||
|
||||
Prior to this [pull request](https://github.com/roboll/helmfile/pull/647), environment values were made available through the `{{ .Environment.Values.foo }}` syntax.
|
||||
This is still working but is **deprecated** and the new `{{ .Values.foo }}` syntax should be used instead.
|
||||
|
||||
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`.
|
||||
@@ -560,7 +840,7 @@ releases:
|
||||
Then the environment secret `foo.bar` can be referenced by the below template expression in your `values.yaml.gotmpl`:
|
||||
|
||||
```yaml
|
||||
{{ .Environment.Values.foo.bar }}
|
||||
{{ .Values.foo.bar }}
|
||||
```
|
||||
|
||||
## Tillerless
|
||||
@@ -570,8 +850,51 @@ 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.
|
||||
|
||||
Since every commands is run with `helm tiller run ...`, you have to disable concurrency. Otherwise you'll get
|
||||
mysterious errors about the tiller daemon.
|
||||
## 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
|
||||
|
||||
@@ -644,19 +967,22 @@ Just run `helmfile sync` inside `myteam/`, and you are done.
|
||||
All the files are sorted alphabetically per group = array item inside `helmfiles:`, so that you have granular control over ordering, too.
|
||||
|
||||
#### selectors
|
||||
|
||||
When composing helmfiles you can use selectors from the command line as well as explicit selectors inside the parent helmfile to filter the releases to be used.
|
||||
|
||||
```yaml
|
||||
helmfiles:
|
||||
- apps/*/helmfile.yaml
|
||||
- path: apps/a-helmfile.yaml
|
||||
selectors: # list of selectors
|
||||
- name=prometheus
|
||||
- tier=frontend
|
||||
selectors: # list of selectors
|
||||
- name=prometheus
|
||||
- tier=frontend
|
||||
- path: apps/b-helmfile.yaml # no selector, so all releases are used
|
||||
selectors: []
|
||||
selectors: []
|
||||
- path: apps/c-helmfile.yaml # parent selector to be used or cli selector for the initial helmfile
|
||||
selectorsInherited: true
|
||||
selectorsInherited: true
|
||||
```
|
||||
|
||||
* When a selector is specified, only this selector applies and the parents or CLI selectors are ignored.
|
||||
* When not selector is specified there are 2 modes for the selector inheritance because we would like to change the current inheritance behavior (see [issue #344](https://github.com/roboll/helmfile/issues/344) ).
|
||||
* Legacy mode, sub-helmfiles without selectors inherit selectors from their parent helmfile. The initial helmfiles inherit from the command line selectors.
|
||||
@@ -671,14 +997,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 }}
|
||||
```
|
||||
@@ -692,31 +1018,46 @@ A Helmfile hook is a per-release extension point that is composed of:
|
||||
- `events`
|
||||
- `command`
|
||||
- `args`
|
||||
- `showlogs`
|
||||
|
||||
Helmfile triggers various `events` while it is running.
|
||||
Once `events` are triggered, associated `hooks` are executed, by running the `command` with `args`.
|
||||
Once `events` are triggered, associated `hooks` are executed, by running the `command` with `args`. The standard output of the `command` will be displayed if `showlogs` is set and it's value is `true`.
|
||||
|
||||
Currently supported `events` are:
|
||||
|
||||
- `prepare`
|
||||
- `cleanup`
|
||||
- `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`.
|
||||
|
||||
`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
|
||||
# *snip*
|
||||
hooks:
|
||||
- events: ["prepare", "cleanup"]
|
||||
showlogs: true
|
||||
command: "echo"
|
||||
args: ["{{`{{.Environment.Name}}`}}", "{{`{{.Release.Name}}`}}", "{{`{{.HelmfileCommand}}`}}\
|
||||
"]
|
||||
@@ -743,6 +1084,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
|
||||
@@ -751,7 +1106,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:
|
||||
|
||||
@@ -805,9 +1160,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
|
||||
@@ -815,7 +1170,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.
|
||||
|
||||
@@ -829,18 +1184,95 @@ 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)
|
||||
|
||||
If you want to enable all experimental features set the env var to `HELMFILE_EXPERIMENTAL=true`
|
||||
|
||||
## `bash` and `zsh` completion
|
||||
|
||||
Copy `autocomplete/helmfile_bash_autocomplete` or `autocomplete/helmfile_zsh_autocomplete` (depending on your shell of choice) to directory where you keep other shell completion scripts to make sure it is sourced.
|
||||
|
||||
## Examples
|
||||
|
||||
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:
|
||||
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
# Helmfile Users
|
||||
|
||||
The following table is compiled from public information or from survey responses. If you are using Helmfile in your
|
||||
company or organization, we would like to invite you to [participate in our user survey](https://docs.google.com/forms/d/e/1FAIpQLSfrF5kR9nXsr5yWqNZGgN-KquM1mBTuYNn5lRmEVXEvhq0VIQ/viewform)
|
||||
to help us extend this list.
|
||||
Who's actually using Helmfile? The following table is compiled from public information, survey response and PRs. It's
|
||||
likely this is only a small selection of actual users, but it's a start.
|
||||
|
||||
If you are using Helmfile in your company or organization, we would like to invite you to create a PR to add your
|
||||
information to this file.
|
||||
|
||||
<!-- TABLE_START -->
|
||||
| Organization | Workload | More Info | Location |
|
||||
|---|---|---|---|
|
||||
| [reddit.com](https://www.reddit.com/) | production | [GitHub issue](https://github.com/roboll/helmfile/issues/96#issue-312012249) | San Francisco, CA, US |
|
||||
| [ricardo.ch](https://www.ricardo.ch/) | proof-of-concept | | Zug, Switzerland |
|
||||
| Organization | Workload | More Info | Location | Added |
|
||||
|---|---|---|---|---|
|
||||
| [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) | 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 -->
|
||||
|
||||
## Contact
|
||||
|
||||
Please fill out [the Google form](https://docs.google.com/forms/d/e/1FAIpQLSfrF5kR9nXsr5yWqNZGgN-KquM1mBTuYNn5lRmEVXEvhq0VIQ/viewform) to add your organization instead of opening a PR.
|
||||
|
||||
You can also [reach the maintainer on Twitter](https://twitter.com/DerCed).
|
||||
@@ -0,0 +1,18 @@
|
||||
#! /bin/bash
|
||||
|
||||
_helmfile_bash_autocomplete() {
|
||||
if [[ "${COMP_WORDS[0]}" != "source" ]]; then
|
||||
local cur opts base
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == "-"* ]]; then
|
||||
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
|
||||
else
|
||||
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
|
||||
fi
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
complete -o bashdefault -o default -o nospace -F _helmfile_bash_autocomplete helmfile
|
||||
@@ -0,0 +1,23 @@
|
||||
#compdef helmfile
|
||||
|
||||
_helmfile_zsh_autocomplete() {
|
||||
|
||||
local -a opts
|
||||
local cur
|
||||
cur=${words[-1]}
|
||||
if [[ "$cur" == "-"* ]]; then
|
||||
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} ${cur} --generate-bash-completion)}")
|
||||
else
|
||||
opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}")
|
||||
fi
|
||||
|
||||
if [[ "${opts[1]}" != "" ]]; then
|
||||
_describe 'values' opts
|
||||
else
|
||||
_files
|
||||
fi
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
compdef _helmfile_zsh_autocomplete helmfile
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/roboll/helmfile/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/app"
|
||||
"github.com/roboll/helmfile/state"
|
||||
"github.com/urfave/cli"
|
||||
"go.uber.org/zap"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func VisitAllDesiredStates(c *cli.Context, converge func(*state.HelmState, helmexec.Interface, app.Context) (bool, []error)) error {
|
||||
a, fileOrDir, err := InitAppEntry(c, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := app.NewContext()
|
||||
|
||||
convergeWithHelmBinary := func(st *state.HelmState, helm helmexec.Interface) (bool, []error) {
|
||||
if c.GlobalString("helm-binary") != "" {
|
||||
helm.SetHelmBinary(c.GlobalString("helm-binary"))
|
||||
}
|
||||
return converge(st, helm, ctx)
|
||||
}
|
||||
|
||||
err = a.VisitDesiredStates(fileOrDir, a.Selectors, convergeWithHelmBinary)
|
||||
|
||||
return toCliError(err)
|
||||
}
|
||||
|
||||
func InitAppEntry(c *cli.Context, reverse bool) (*app.App, string, error) {
|
||||
if c.NArg() > 0 {
|
||||
cli.ShowAppHelp(c)
|
||||
return nil, "", fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", "))
|
||||
}
|
||||
|
||||
fileOrDir := c.GlobalString("file")
|
||||
kubeContext := c.GlobalString("kube-context")
|
||||
namespace := c.GlobalString("namespace")
|
||||
selectors := c.GlobalStringSlice("selector")
|
||||
logger := c.App.Metadata["logger"].(*zap.SugaredLogger)
|
||||
|
||||
env := c.GlobalString("environment")
|
||||
if env == "" {
|
||||
env = state.DefaultEnv
|
||||
}
|
||||
|
||||
app := app.Init(&app.App{
|
||||
KubeContext: kubeContext,
|
||||
Logger: logger,
|
||||
Reverse: reverse,
|
||||
Env: env,
|
||||
Namespace: namespace,
|
||||
Selectors: selectors,
|
||||
})
|
||||
|
||||
return app, fileOrDir, nil
|
||||
}
|
||||
|
||||
func FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c *cli.Context, reverse bool, converge func(*state.HelmState, helmexec.Interface, app.Context) []error) error {
|
||||
a, fileOrDir, err := InitAppEntry(c, reverse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := app.NewContext()
|
||||
|
||||
convergeWithHelmBinary := func(st *state.HelmState, helm helmexec.Interface) []error {
|
||||
if c.GlobalString("helm-binary") != "" {
|
||||
helm.SetHelmBinary(c.GlobalString("helm-binary"))
|
||||
}
|
||||
return converge(st, helm, ctx)
|
||||
}
|
||||
|
||||
err = a.VisitDesiredStatesWithReleasesFiltered(fileOrDir, convergeWithHelmBinary)
|
||||
|
||||
return toCliError(err)
|
||||
}
|
||||
|
||||
func toCliError(err error) error {
|
||||
if err != nil {
|
||||
switch e := err.(type) {
|
||||
case *app.NoMatchingHelmfileError:
|
||||
return cli.NewExitError(e.Error(), 2)
|
||||
case *app.Error:
|
||||
return cli.NewExitError(e.Error(), e.Code())
|
||||
default:
|
||||
panic(fmt.Errorf("BUG: please file an github issue for this unhandled error: %T: %v", e, e))
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/roboll/helmfile/args"
|
||||
"github.com/roboll/helmfile/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/app"
|
||||
"github.com/roboll/helmfile/state"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func Deps(a *app.App) cli.Command {
|
||||
return cli.Command{
|
||||
Name: "deps",
|
||||
Usage: "update charts based on the contents of requirements.yaml",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "args",
|
||||
Value: "",
|
||||
Usage: "pass args to helm exec",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return VisitAllDesiredStates(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) (bool, []error) {
|
||||
args := args.GetArgs(c.String("args"), state)
|
||||
if len(args) > 0 {
|
||||
helm.SetExtraArgs(args...)
|
||||
}
|
||||
|
||||
errs := state.UpdateDeps(helm)
|
||||
|
||||
ok := len(errs) == 0
|
||||
|
||||
return ok, errs
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
## Advanced Features
|
||||
|
||||
- [Import Configuration Parameters into Helmfile](#import-configuration-parameters-into-helmfile)
|
||||
- [Deploy Kustomization with Helmfile](#deploy-kustomizations-with-helmfile)
|
||||
- [Adhoc Kustomization of Helm Charts](#adhoc-kustomization-of-helm-charts)
|
||||
|
||||
### 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 Kustomization of Helm charts
|
||||
|
||||
With Helmfile's integration with Helmfile, not only deploying Kustomization as a Helm chart, you can kustomize charts before installation.
|
||||
|
||||
Currently, Helmfile allows you to set the following fields for kustomizing the chart:
|
||||
|
||||
- [`releases[].strategicMergePatches`](#strategicmergepatches)
|
||||
- `releases[].jsonPatches`
|
||||
- [`releases[].transformers`](#transformers)
|
||||
|
||||
#### `strategicMergePatches`
|
||||
|
||||
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.
|
||||
|
||||
#### `transformers`
|
||||
|
||||
You can set `transformers` to apply [Kustomize's transformers](https://github.com/kubernetes-sigs/kustomize/blob/master/examples/configureBuiltinPlugin.md#configuring-the-builtin-plugins-instead).
|
||||
|
||||
Each item can be a path to a YAML or go template file, or an embedded transformer declaration as a YAML hash.
|
||||
|
||||
It's often used to add common labels and annotations to your resources.
|
||||
|
||||
In the below example. we add common annotations and labels every resource rendered from the `aws-load-balancer-controller` chart:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: "aws-load-balancer-controller"
|
||||
namespace: "kube-system"
|
||||
forceNamespace: "kube-system"
|
||||
chart: "center/aws/aws-load-balancer-controller"
|
||||
transformers:
|
||||
- apiVersion: builtin
|
||||
kind: AnnotationsTransformer
|
||||
metadata:
|
||||
name: notImportantHere
|
||||
annotations:
|
||||
area: 51
|
||||
greeting: take me to your leader
|
||||
fieldSpecs:
|
||||
- path: metadata/annotations
|
||||
create: true
|
||||
- apiVersion: builtin
|
||||
kind: LabelTransformer
|
||||
metadata:
|
||||
name: notImportantHere
|
||||
labels:
|
||||
foo: bar
|
||||
fieldSpecs:
|
||||
- path: metadata/labels
|
||||
create: true
|
||||
```
|
||||
|
||||
As explained earlier, `transformers` can be not only a list of embedded transformers, but also YAML or go template files, or a mix of those three kinds.
|
||||
|
||||
```yaml
|
||||
transformers:
|
||||
# Embedded transformer
|
||||
- apiVersion: builtin
|
||||
kind: AnnotationsTransformer
|
||||
metadata:
|
||||
name: notImportantHere
|
||||
annotations:
|
||||
area: 51
|
||||
greeting: take me to your leader
|
||||
fieldSpecs:
|
||||
- path: metadata/annotations
|
||||
create: true
|
||||
# YAML file
|
||||
- path/to/transformer.yaml
|
||||
# Go template
|
||||
# The same set of template parameters as release values files templates is available.
|
||||
- path/to/transformer.yaml.gotmpl
|
||||
```
|
||||
|
||||
Please see https://github.com/kubernetes-sigs/kustomize/blob/master/examples/configureBuiltinPlugin.md#configuring-the-builtin-plugins-instead for more information on how to declare transformers.
|
||||
@@ -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
|
||||
|
||||
```
|
||||
|
||||
+214
-22
@@ -11,13 +11,13 @@ One example of how helmfile achieves it is that, `helmfile` fails when you tried
|
||||
That is, the following example let `helmfile` fail when you have no `eventApi.replicas` defined in environment values.
|
||||
|
||||
```
|
||||
{{ .Environment.Values.eventApi.replicas | default 1 }}
|
||||
{{ .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:
|
||||
|
||||
```
|
||||
{{ .Environment.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:
|
||||
|
||||
```
|
||||
{{ .Environment.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"
|
||||
```
|
||||
@@ -89,9 +89,51 @@ releases:
|
||||
<<: *default
|
||||
```
|
||||
|
||||
Release Templating supports the following parts of release definition:
|
||||
- basic fields: `name`, `namespace`, `chart`, `version`
|
||||
- boolean fields: `installed`, `wait`, `tillerless`, `verify` by the means of additional text
|
||||
fields designed for templating only: `installedTemplate`, `waitTemplate`, `tillerlessTemplate`, `verifyTemplate`
|
||||
```yaml
|
||||
# ...
|
||||
installedTemplate: '{{`{{ eq .Release.Namespace "kube-system" }}`}}'
|
||||
waitTemplate: '{{`{{ eq .Release.Labels.tag "safe" | not }}`}}'
|
||||
# ...
|
||||
```
|
||||
- `set` block values:
|
||||
```yaml
|
||||
# ...
|
||||
setTemplate:
|
||||
- name: '{{`{{ .Release.Name }}`}}'
|
||||
values: '{{`{{ .Release.Namespace }}`}}'
|
||||
# ...
|
||||
```
|
||||
- `values` and `secrets` file paths:
|
||||
```yaml
|
||||
# ...
|
||||
valuesTemplate:
|
||||
- config/{{`{{ .Release.Name }}`}}/values.yaml
|
||||
secrets:
|
||||
- config/{{`{{ .Release.Name }}`}}/secrets.yaml
|
||||
# ...
|
||||
```
|
||||
- inline `values` map:
|
||||
```yaml
|
||||
# ...
|
||||
valuesTemplate:
|
||||
- image:
|
||||
tag: `{{ .Release.Labels.tag }}`
|
||||
# ...
|
||||
```
|
||||
|
||||
See the [issue 428](https://github.com/roboll/helmfile/issues/428) for more context on how this is supposed to work.
|
||||
|
||||
## Layering
|
||||
## Layering Release Values
|
||||
|
||||
Please note, that it is not possible to layer `values` sections. If `values` is defined in the release and in the release template, only the `values` defined in the release will be considered. The same applies to `secrets` and `set`.
|
||||
|
||||
## Layering State Files
|
||||
|
||||
> See **Layering State Template Files** if you're layering templates.
|
||||
|
||||
You may occasionally end up with many helmfiles that shares common parts like which repositories to use, and whichi release to be bundled by default.
|
||||
|
||||
@@ -101,23 +143,16 @@ Let's assume that your `helmfile.yaml` looks like:
|
||||
|
||||
```
|
||||
bases:
|
||||
- commons.yaml
|
||||
- environments.yaml
|
||||
|
||||
releases:
|
||||
- name: metricbeat
|
||||
chart: stable/metricbeat
|
||||
- name: myapp
|
||||
chart: mychart
|
||||
```
|
||||
|
||||
Whereas `commons.yaml` contained a monitoring agent:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: metricbaet
|
||||
chart: stable/metricbeat
|
||||
```
|
||||
|
||||
And `environments.yaml` contained well-known environments:
|
||||
Whereas `environments.yaml` contained well-known environments:
|
||||
|
||||
```yaml
|
||||
environments:
|
||||
@@ -128,10 +163,6 @@ environments:
|
||||
At run time, `bases` in your `helmfile.yaml` are evaluated to produce:
|
||||
|
||||
```yaml
|
||||
# commons.yaml
|
||||
releases:
|
||||
- name: metricbaet
|
||||
chart: stable/metricbeat
|
||||
---
|
||||
# environments.yaml
|
||||
environments:
|
||||
@@ -142,6 +173,8 @@ environments:
|
||||
releases:
|
||||
- name: myapp
|
||||
chart: mychart
|
||||
- name: metricbeat
|
||||
chart: stable/metricbeat
|
||||
```
|
||||
|
||||
Finally the resulting YAML documents are merged in the order of occurrence,
|
||||
@@ -153,7 +186,7 @@ environments:
|
||||
production:
|
||||
|
||||
releases:
|
||||
- name: metricbaet
|
||||
- name: metricbeat
|
||||
chart: stable/metricbeat
|
||||
- name: myapp
|
||||
chart: mychart
|
||||
@@ -164,3 +197,162 @@ Great!
|
||||
Now, repeat the above steps for each your `helmfile.yaml`, so that all your helmfiles becomes DRY.
|
||||
|
||||
Please also see [the discussion in the issue 388](https://github.com/roboll/helmfile/issues/388#issuecomment-491710348) for more advanced layering examples.
|
||||
|
||||
## Merging Arrays in Layers
|
||||
|
||||
Helmfile doesn't merge arrays across layers. That is, the below example doesn't work as you might have expected:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: metricbeat
|
||||
chart: stable/metricbeat
|
||||
---
|
||||
releases:
|
||||
- name: myapp
|
||||
chart: mychart
|
||||
```
|
||||
|
||||
Helmfile overrides the `releases` array with the latest layer so the resulting state file will be:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
# metricbeat release disappeared! but that's how helmfile works
|
||||
- name: myapp
|
||||
chart: mychart
|
||||
```
|
||||
|
||||
A work-around is to treat the state file as a go template and use `readFile` template function to import the common part of your state file as a plain text:
|
||||
|
||||
`common.yaml`:
|
||||
|
||||
```yaml
|
||||
templates:
|
||||
metricbeat: &metricbeat
|
||||
name: metricbeat
|
||||
chart: stable/metricbeat
|
||||
```
|
||||
|
||||
`helmfile.yaml`:
|
||||
|
||||
```yaml
|
||||
{{ readFile "common.yaml" }}
|
||||
|
||||
releases:
|
||||
- <<: *metricbeat
|
||||
- name: myapp
|
||||
chart: mychart
|
||||
```
|
||||
|
||||
## Layering State Template Files
|
||||
|
||||
Do you need to make your state file even more DRY?
|
||||
|
||||
Turned out layering state files wasn't enough for you?
|
||||
|
||||
Helmfile supports an advanced feature that allows you to compose state "template" files to generate the final state to be processed.
|
||||
|
||||
In the following example `helmfile.yaml.gotmpl`, each `---` separated part of the file is a go template.
|
||||
|
||||
`helmfile.yaml.gotmpl`:
|
||||
|
||||
```yaml
|
||||
# Part 1: Reused Enviroment Values
|
||||
bases:
|
||||
- myenv.yaml
|
||||
---
|
||||
# Part 2: Reused Defaults
|
||||
bases:
|
||||
- mydefaults.yaml
|
||||
---
|
||||
# Part 3: Dynamic Releases
|
||||
releases:
|
||||
- name: test1
|
||||
chart: mychart-{{ .Values.myname }}
|
||||
values:
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository: "nginx"
|
||||
tag: "latest"
|
||||
```
|
||||
|
||||
Suppose the `myenv.yaml` and `test.env.yaml` loaded in the first part looks like:
|
||||
|
||||
`myenv.yaml`:
|
||||
|
||||
```yaml
|
||||
environments:
|
||||
test:
|
||||
values:
|
||||
- test.env.yaml
|
||||
```
|
||||
|
||||
`test.env.yaml`:
|
||||
|
||||
```yaml
|
||||
kubeContext: test
|
||||
wait: false
|
||||
cvOnly: false
|
||||
myname: "dog"
|
||||
```
|
||||
|
||||
Where the gotmpl file loaded in the second part looks like:
|
||||
|
||||
`mydefaults.yaml.gotmpl`:
|
||||
|
||||
```yaml
|
||||
helmDefaults:
|
||||
tillerNamespace: kube-system
|
||||
kubeContext: {{ .Values.kubeContext }}
|
||||
verify: false
|
||||
{{ if .Values.wait }}
|
||||
wait: true
|
||||
{{ else }}
|
||||
wait: false
|
||||
{{ end }}
|
||||
timeout: 600
|
||||
recreatePods: false
|
||||
force: true
|
||||
```
|
||||
|
||||
Each go template is rendered in the context where `.Values` is inherited from the previous part.
|
||||
|
||||
So in `mydefaults.yaml.gotmpl`, both `.Values.kubeContext` and `.Values.wait` are valid as they do exist in the environment values inherited from the previous part(=the first part) of your `helmfile.yaml.gotmpl`, and therefore the template is rendered to:
|
||||
|
||||
```yaml
|
||||
helmDefaults:
|
||||
tillerNamespace: kube-system
|
||||
kubeContext: test
|
||||
verify: false
|
||||
wait: false
|
||||
timeout: 600
|
||||
recreatePods: false
|
||||
force: true
|
||||
```
|
||||
|
||||
Similarly, the third part of the top-level `helmfile.yaml.gotmpl`, `.Values.myname` is valid as it is included in the environment values inherited from the previous parts:
|
||||
|
||||
```yaml
|
||||
# Part 3: Dynamic Releases
|
||||
releases:
|
||||
- name: test1
|
||||
chart: mychart-{{ .Values.myname }}
|
||||
values:
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository: "nginx"
|
||||
tag: "latest"
|
||||
````
|
||||
|
||||
hence rendered to:
|
||||
|
||||
```yaml
|
||||
# Part 3: Dynamic Releases
|
||||
releases:
|
||||
- name: test1
|
||||
chart: mychart-dog
|
||||
values:
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository: "nginx"
|
||||
tag: "latest"
|
||||
```
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package environment
|
||||
|
||||
type Environment struct {
|
||||
Name string
|
||||
Values map[string]interface{}
|
||||
}
|
||||
|
||||
var EmptyEnvironment Environment
|
||||
@@ -1,23 +1,37 @@
|
||||
module github.com/roboll/helmfile
|
||||
|
||||
go 1.12
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver v1.4.1 // indirect
|
||||
github.com/Masterminds/sprig v2.15.0+incompatible
|
||||
github.com/aokoli/goutils v1.0.1 // indirect
|
||||
github.com/google/go-cmp v0.3.0 // indirect
|
||||
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c // indirect
|
||||
github.com/huandu/xstrings v1.0.0 // indirect
|
||||
github.com/imdario/mergo v0.3.6
|
||||
github.com/mattn/go-runewidth v0.0.4 // indirect
|
||||
github.com/pkg/errors v0.8.1 // indirect
|
||||
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/frankban/quicktest v1.11.2 // indirect
|
||||
github.com/go-test/deep v1.0.7
|
||||
github.com/golang/mock v1.4.4
|
||||
github.com/google/go-cmp v0.5.2
|
||||
github.com/gosuri/uitable v0.0.4
|
||||
github.com/hashicorp/go-getter v1.5.0
|
||||
github.com/hashicorp/go-retryablehttp v0.6.3 // indirect
|
||||
github.com/hashicorp/go-version v1.2.1
|
||||
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 v1.1.0
|
||||
github.com/spf13/cobra v1.1.1
|
||||
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
|
||||
go.uber.org/multierr v1.1.0 // indirect
|
||||
go.uber.org/zap v1.8.0
|
||||
golang.org/x/crypto v0.0.0-20180403160946-b2aa35443fbc // indirect
|
||||
gopkg.in/yaml.v2 v2.2.1
|
||||
github.com/urfave/cli v1.22.5
|
||||
github.com/variantdev/chartify v0.5.0
|
||||
github.com/variantdev/dag v0.0.0-20191028002400-bb0b3c785363
|
||||
github.com/variantdev/vals v0.11.0
|
||||
go.uber.org/multierr v1.6.0
|
||||
go.uber.org/zap v1.16.0
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
||||
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
|
||||
k8s.io/apimachinery v0.19.3
|
||||
)
|
||||
|
||||
@@ -1,265 +0,0 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
const (
|
||||
command = "helm"
|
||||
)
|
||||
|
||||
type execer struct {
|
||||
helmBinary string
|
||||
runner Runner
|
||||
logger *zap.SugaredLogger
|
||||
kubeContext string
|
||||
extra []string
|
||||
decryptionMutex sync.Mutex
|
||||
}
|
||||
|
||||
func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger {
|
||||
var cfg zapcore.EncoderConfig
|
||||
cfg.MessageKey = "message"
|
||||
out := zapcore.AddSync(writer)
|
||||
var level zapcore.Level
|
||||
err := level.Set(logLevel)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
core := zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(cfg),
|
||||
out,
|
||||
level,
|
||||
)
|
||||
return zap.New(core).Sugar()
|
||||
}
|
||||
|
||||
// New for running helm commands
|
||||
func New(logger *zap.SugaredLogger, kubeContext string) *execer {
|
||||
return &execer{
|
||||
helmBinary: command,
|
||||
logger: logger,
|
||||
kubeContext: kubeContext,
|
||||
runner: &ShellRunner{
|
||||
logger: logger,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (helm *execer) SetExtraArgs(args ...string) {
|
||||
helm.extra = args
|
||||
}
|
||||
|
||||
func (helm *execer) SetHelmBinary(bin string) {
|
||||
helm.helmBinary = bin
|
||||
}
|
||||
|
||||
func (helm *execer) AddRepo(name, repository, certfile, keyfile, username, password string) error {
|
||||
var args []string
|
||||
args = append(args, "repo", "add", name, repository)
|
||||
if certfile != "" && keyfile != "" {
|
||||
args = append(args, "--cert-file", certfile, "--key-file", keyfile)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (helm *execer) UpdateRepo() error {
|
||||
helm.logger.Info("Updating repo")
|
||||
out, err := helm.exec([]string{"repo", "update"}, map[string]string{})
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) UpdateDeps(chart string) error {
|
||||
helm.logger.Infof("Updating dependency %v", chart)
|
||||
out, err := helm.exec([]string{"dependency", "update", chart}, map[string]string{})
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) BuildDeps(chart string) error {
|
||||
helm.logger.Infof("Building dependency %v", chart)
|
||||
out, err := helm.exec([]string{"dependency", "build", chart}, map[string]string{})
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) SyncRelease(context HelmContext, name, chart string, flags ...string) error {
|
||||
helm.logger.Infof("Upgrading %v", chart)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "upgrade", "--install", "--reset-values", name, chart), flags...), env)
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) ReleaseStatus(context HelmContext, name string, flags ...string) error {
|
||||
helm.logger.Infof("Getting status %v", name)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "status", name), flags...), env)
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "list", filter), flags...), env)
|
||||
helm.write(out)
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
func (helm *execer) DecryptSecret(context HelmContext, name string, flags ...string) (string, error) {
|
||||
// Prevents https://github.com/roboll/helmfile/issues/258
|
||||
helm.decryptionMutex.Lock()
|
||||
defer helm.decryptionMutex.Unlock()
|
||||
|
||||
helm.logger.Infof("Decrypting secret %v", name)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "secrets", "dec", name), flags...), env)
|
||||
helm.write(out)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tmpFile, err := ioutil.TempFile("", "secret")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
|
||||
// os.Rename seems to results in "cross-device link` errors in some cases
|
||||
// Instead of moving, copy it to the destination temp file as a work-around
|
||||
// See https://github.com/roboll/helmfile/issues/251#issuecomment-417166296f
|
||||
decFilename := name + ".dec"
|
||||
decFile, err := os.Open(decFilename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer decFile.Close()
|
||||
|
||||
_, err = io.Copy(tmpFile, decFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := decFile.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := os.Remove(decFilename); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tmpFile.Name(), err
|
||||
}
|
||||
|
||||
func (helm *execer) TemplateRelease(chart string, flags ...string) error {
|
||||
out, err := helm.exec(append([]string{"template", chart}, flags...), map[string]string{})
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) DiffRelease(context HelmContext, name, chart string, flags ...string) error {
|
||||
helm.logger.Infof("Comparing %v %v", name, chart)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "diff", "upgrade", "--allow-unreleased", name, chart), flags...), env)
|
||||
// Do our best to write STDOUT only when diff existed
|
||||
// Unfortunately, this works only when you run helmfile with `--detailed-exitcode`
|
||||
detailedExitcodeEnabled := false
|
||||
for _, f := range flags {
|
||||
if strings.Contains(f, "detailed-exitcode") {
|
||||
detailedExitcodeEnabled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if detailedExitcodeEnabled {
|
||||
switch e := err.(type) {
|
||||
case ExitError:
|
||||
if e.ExitStatus() == 2 {
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
helm.write(out)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) Lint(chart string, flags ...string) error {
|
||||
helm.logger.Infof("Linting %v", chart)
|
||||
out, err := helm.exec(append([]string{"lint", chart}, flags...), map[string]string{})
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) Fetch(chart string, flags ...string) error {
|
||||
helm.logger.Infof("Fetching %v", chart)
|
||||
out, err := helm.exec(append([]string{"fetch", chart}, flags...), map[string]string{})
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) DeleteRelease(context HelmContext, name string, flags ...string) error {
|
||||
helm.logger.Infof("Deleting %v", name)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "delete", name), flags...), env)
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) TestRelease(context HelmContext, name string, flags ...string) error {
|
||||
helm.logger.Infof("Testing %v", name)
|
||||
preArgs := context.GetTillerlessArgs(helm.helmBinary)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "test", name), flags...), env)
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) exec(args []string, env map[string]string) ([]byte, error) {
|
||||
cmdargs := args
|
||||
if len(helm.extra) > 0 {
|
||||
cmdargs = append(cmdargs, helm.extra...)
|
||||
}
|
||||
if helm.kubeContext != "" {
|
||||
cmdargs = append(cmdargs, "--kube-context", helm.kubeContext)
|
||||
}
|
||||
cmd := fmt.Sprintf("exec: %s %s", helm.helmBinary, strings.Join(cmdargs, " "))
|
||||
helm.logger.Debug(cmd)
|
||||
bytes, err := helm.runner.Execute(helm.helmBinary, cmdargs, env)
|
||||
helm.logger.Debugf("%s: %s", cmd, bytes)
|
||||
return bytes, err
|
||||
}
|
||||
|
||||
func (helm *execer) info(out []byte) {
|
||||
if len(out) > 0 {
|
||||
helm.logger.Infof("%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func (helm *execer) write(out []byte) {
|
||||
if len(out) > 0 {
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func newExitError(helmCmdPath string, exitStatus int, errorMessage string) ExitError {
|
||||
return ExitError{
|
||||
msg: fmt.Sprintf("%s exited with status %d:\n%s", filepath.Base(helmCmdPath), exitStatus, indent(strings.TrimSpace(errorMessage))),
|
||||
exitStatus: exitStatus,
|
||||
}
|
||||
}
|
||||
|
||||
func indent(text string) string {
|
||||
lines := strings.Split(text, "\n")
|
||||
for i := range lines {
|
||||
lines[i] = " " + lines[i]
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// ExitError is created whenever your shell command exits with a non-zero exit status
|
||||
type ExitError struct {
|
||||
msg string
|
||||
exitStatus int
|
||||
}
|
||||
|
||||
func (e ExitError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
func (e ExitError) ExitStatus() int {
|
||||
return e.exitStatus
|
||||
}
|
||||
@@ -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}"
|
||||
+1331
-97
File diff suppressed because it is too large
Load Diff
+3793
-230
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package app
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
// Copyright (c) 2017 Roland Singer [roland.singer@desertbit.com]
|
||||
//
|
||||
// Shamelessly borrowed from @r0l1's awesome work that is available at https://gist.github.com/r0l1/3dcbb0c8f6cfe9c66ab8008f55f8f28b
|
||||
func askForConfirmation(s string) bool {
|
||||
func AskForConfirmation(s string) bool {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
for {
|
||||
@@ -0,0 +1,175 @@
|
||||
package app
|
||||
|
||||
import "go.uber.org/zap"
|
||||
|
||||
type ConfigProvider interface {
|
||||
Args() string
|
||||
HelmBinary() string
|
||||
|
||||
FileOrDir() string
|
||||
KubeContext() string
|
||||
Namespace() string
|
||||
Selectors() []string
|
||||
StateValuesSet() map[string]interface{}
|
||||
StateValuesFiles() []string
|
||||
Env() string
|
||||
|
||||
loggingConfig
|
||||
}
|
||||
|
||||
type DeprecatedChartsConfigProvider interface {
|
||||
Values() []string
|
||||
|
||||
concurrencyConfig
|
||||
loggingConfig
|
||||
}
|
||||
|
||||
type DepsConfigProvider interface {
|
||||
Args() string
|
||||
SkipRepos() bool
|
||||
}
|
||||
|
||||
type ReposConfigProvider interface {
|
||||
Args() string
|
||||
}
|
||||
|
||||
type ApplyConfigProvider interface {
|
||||
Args() string
|
||||
|
||||
Values() []string
|
||||
Set() []string
|
||||
SkipDeps() bool
|
||||
|
||||
IncludeTests() bool
|
||||
|
||||
SuppressSecrets() bool
|
||||
SuppressDiff() bool
|
||||
|
||||
DetailedExitcode() bool
|
||||
|
||||
NoColor() bool
|
||||
Context() int
|
||||
|
||||
RetainValuesFiles() bool
|
||||
SkipCleanup() bool
|
||||
|
||||
concurrencyConfig
|
||||
interactive
|
||||
loggingConfig
|
||||
}
|
||||
|
||||
type SyncConfigProvider interface {
|
||||
Args() string
|
||||
|
||||
Values() []string
|
||||
Set() []string
|
||||
SkipDeps() bool
|
||||
|
||||
concurrencyConfig
|
||||
loggingConfig
|
||||
}
|
||||
|
||||
type DiffConfigProvider interface {
|
||||
Args() string
|
||||
|
||||
Values() []string
|
||||
Set() []string
|
||||
SkipDeps() bool
|
||||
|
||||
IncludeTests() bool
|
||||
|
||||
SuppressSecrets() bool
|
||||
SuppressDiff() bool
|
||||
|
||||
DetailedExitcode() bool
|
||||
NoColor() bool
|
||||
Context() int
|
||||
|
||||
concurrencyConfig
|
||||
}
|
||||
|
||||
type DeleteConfigProvider interface {
|
||||
Args() string
|
||||
|
||||
Purge() bool
|
||||
|
||||
interactive
|
||||
loggingConfig
|
||||
concurrencyConfig
|
||||
}
|
||||
|
||||
type DestroyConfigProvider interface {
|
||||
Args() string
|
||||
|
||||
interactive
|
||||
loggingConfig
|
||||
concurrencyConfig
|
||||
}
|
||||
|
||||
type TestConfigProvider interface {
|
||||
Args() string
|
||||
|
||||
Timeout() int
|
||||
Cleanup() bool
|
||||
Logs() bool
|
||||
|
||||
concurrencyConfig
|
||||
}
|
||||
|
||||
type LintConfigProvider interface {
|
||||
Args() string
|
||||
|
||||
Values() []string
|
||||
Set() []string
|
||||
SkipDeps() bool
|
||||
|
||||
concurrencyConfig
|
||||
}
|
||||
|
||||
type TemplateConfigProvider interface {
|
||||
Args() string
|
||||
|
||||
Values() []string
|
||||
Set() []string
|
||||
OutputDirTemplate() string
|
||||
Validate() bool
|
||||
SkipDeps() bool
|
||||
SkipCleanup() bool
|
||||
OutputDir() string
|
||||
IncludeCRDs() bool
|
||||
|
||||
concurrencyConfig
|
||||
}
|
||||
|
||||
type WriteValuesConfigProvider interface {
|
||||
Values() []string
|
||||
Set() []string
|
||||
OutputFileTemplate() string
|
||||
SkipDeps() bool
|
||||
}
|
||||
|
||||
type StatusesConfigProvider interface {
|
||||
Args() string
|
||||
|
||||
concurrencyConfig
|
||||
}
|
||||
|
||||
type StateConfigProvider interface {
|
||||
EmbedValues() bool
|
||||
}
|
||||
|
||||
type concurrencyConfig interface {
|
||||
Concurrency() int
|
||||
}
|
||||
|
||||
type loggingConfig interface {
|
||||
Logger() *zap.SugaredLogger
|
||||
}
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
+10
-18
@@ -1,33 +1,25 @@
|
||||
package app
|
||||
|
||||
import "github.com/roboll/helmfile/state"
|
||||
import (
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
)
|
||||
|
||||
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,35 +4,95 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/environment"
|
||||
"github.com/roboll/helmfile/state"
|
||||
"go.uber.org/zap"
|
||||
"log"
|
||||
"os"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultHelmBinary = state.DefaultHelmBinary
|
||||
)
|
||||
|
||||
type desiredStateLoader struct {
|
||||
KubeContext string
|
||||
Reverse bool
|
||||
overrideKubeContext string
|
||||
overrideHelmBinary string
|
||||
|
||||
env string
|
||||
namespace string
|
||||
|
||||
readFile func(string) ([]byte, error)
|
||||
abs func(string) (string, error)
|
||||
glob func(string) ([]string, error)
|
||||
readFile func(string) ([]byte, error)
|
||||
deleteFile func(string) error
|
||||
fileExists func(string) (bool, error)
|
||||
abs func(string) (string, error)
|
||||
glob func(string) ([]string, error)
|
||||
directoryExistsAt func(string) bool
|
||||
getHelm func(*state.HelmState) helmexec.Interface
|
||||
|
||||
logger *zap.SugaredLogger
|
||||
remote *remote.Remote
|
||||
logger *zap.SugaredLogger
|
||||
valsRuntime vals.Evaluator
|
||||
}
|
||||
|
||||
func (ld *desiredStateLoader) Load(f string) (*state.HelmState, error) {
|
||||
return ld.loadFile(filepath.Dir(f), filepath.Base(f), true)
|
||||
func (ld *desiredStateLoader) Load(f string, opts LoadOpts) (*state.HelmState, error) {
|
||||
var overrodeEnv *environment.Environment
|
||||
|
||||
args := opts.Environment.OverrideValues
|
||||
|
||||
if len(args) > 0 {
|
||||
if opts.CalleePath == "" {
|
||||
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, ld.remote)
|
||||
handler := state.MissingFileHandlerError
|
||||
vals, err := envld.LoadEnvironmentValues(&handler, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
overrodeEnv = &environment.Environment{
|
||||
Name: ld.env,
|
||||
Values: vals,
|
||||
}
|
||||
}
|
||||
|
||||
st, err := ld.loadFileWithOverrides(nil, overrodeEnv, filepath.Dir(f), filepath.Base(f), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if opts.Reverse {
|
||||
st.Reverse()
|
||||
}
|
||||
|
||||
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.overrideKubeContext
|
||||
}
|
||||
|
||||
if ld.namespace != "" {
|
||||
if st.OverrideNamespace != "" {
|
||||
return nil, errors.New("err: Cannot use option --namespace and set attribute namespace.")
|
||||
}
|
||||
st.OverrideNamespace = ld.namespace
|
||||
}
|
||||
|
||||
return st, nil
|
||||
}
|
||||
|
||||
func (ld *desiredStateLoader) loadFile(baseDir, file string, evaluateBases bool) (*state.HelmState, error) {
|
||||
func (ld *desiredStateLoader) loadFile(inheritedEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*state.HelmState, error) {
|
||||
return ld.loadFileWithOverrides(inheritedEnv, nil, baseDir, file, evaluateBases)
|
||||
}
|
||||
|
||||
func (ld *desiredStateLoader) loadFileWithOverrides(inheritedEnv, overrodeEnv *environment.Environment, baseDir, file string, evaluateBases bool) (*state.HelmState, error) {
|
||||
var f string
|
||||
if filepath.IsAbs(file) {
|
||||
f = file
|
||||
@@ -51,6 +111,8 @@ func (ld *desiredStateLoader) loadFile(baseDir, file string, evaluateBases bool)
|
||||
|
||||
if !experimentalModeEnabled() || ext == ".gotmpl" {
|
||||
self, err = ld.renderAndLoad(
|
||||
inheritedEnv,
|
||||
overrodeEnv,
|
||||
baseDir,
|
||||
f,
|
||||
fileBytes,
|
||||
@@ -62,101 +124,60 @@ func (ld *desiredStateLoader) loadFile(baseDir, file string, evaluateBases bool)
|
||||
baseDir,
|
||||
file,
|
||||
evaluateBases,
|
||||
inheritedEnv,
|
||||
overrodeEnv,
|
||||
)
|
||||
}
|
||||
|
||||
return self, err
|
||||
}
|
||||
|
||||
func (a *desiredStateLoader) load(yaml []byte, baseDir, file string, evaluateBases bool) (*state.HelmState, error) {
|
||||
c := state.NewCreator(a.logger, a.readFile, a.abs)
|
||||
st, err := c.ParseAndLoadEnv(yaml, baseDir, file, a.env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
helmfiles := []state.SubHelmfileSpec{}
|
||||
for _, hf := range st.Helmfiles {
|
||||
globPattern := hf.Path
|
||||
var absPathPattern string
|
||||
if filepath.IsAbs(globPattern) {
|
||||
absPathPattern = globPattern
|
||||
} else {
|
||||
absPathPattern = st.JoinBase(globPattern)
|
||||
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)
|
||||
}
|
||||
matches, err := a.glob(absPathPattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed processing %s: %v", globPattern, err)
|
||||
}
|
||||
sort.Strings(matches)
|
||||
for _, match := range matches {
|
||||
newHelmfile := hf
|
||||
newHelmfile.Path = match
|
||||
helmfiles = append(helmfiles, newHelmfile)
|
||||
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.directoryExistsAt, a.valsRuntime, a.getHelm, a.overrideHelmBinary, a.remote)
|
||||
c.DeleteFile = a.deleteFile
|
||||
c.LoadFile = a.loadFile
|
||||
return c
|
||||
}
|
||||
|
||||
func (a *desiredStateLoader) load(yaml []byte, baseDir, file string, evaluateBases bool, env, overrodeEnv *environment.Environment) (*state.HelmState, error) {
|
||||
merged, err := env.Merge(overrodeEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
st, err := a.underlying().ParseAndLoad(yaml, baseDir, file, a.env, evaluateBases, merged)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
helmfiles, err := st.ExpandedHelmfiles()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
st.Helmfiles = helmfiles
|
||||
|
||||
if a.Reverse {
|
||||
rev := func(i, j int) bool {
|
||||
return j < i
|
||||
}
|
||||
sort.Slice(st.Releases, rev)
|
||||
sort.Slice(st.Helmfiles, rev)
|
||||
}
|
||||
|
||||
if a.KubeContext != "" {
|
||||
if st.HelmDefaults.KubeContext != "" {
|
||||
log.Printf("err: Cannot use option --kube-context and set attribute helmDefaults.kubeContext.")
|
||||
os.Exit(1)
|
||||
}
|
||||
st.HelmDefaults.KubeContext = a.KubeContext
|
||||
}
|
||||
if a.namespace != "" {
|
||||
if st.Namespace != "" {
|
||||
log.Printf("err: Cannot use option --namespace and set attribute namespace.")
|
||||
os.Exit(1)
|
||||
}
|
||||
st.Namespace = a.namespace
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !evaluateBases {
|
||||
if len(st.Bases) > 0 {
|
||||
return nil, errors.New("nested `base` helmfile is unsupported. please submit a feature request if you need this!")
|
||||
}
|
||||
|
||||
return st, nil
|
||||
}
|
||||
|
||||
layers := []*state.HelmState{}
|
||||
for _, b := range st.Bases {
|
||||
base, err := a.loadFile(baseDir, b, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
layers = append(layers, base)
|
||||
}
|
||||
layers = append(layers, st)
|
||||
|
||||
for i := 1; i < len(layers); i++ {
|
||||
if err := mergo.Merge(layers[0], layers[i], mergo.WithAppendSlice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return layers[0], nil
|
||||
return st, nil
|
||||
}
|
||||
|
||||
func (ld *desiredStateLoader) renderAndLoad(baseDir, filename string, content []byte, evaluateBases bool) (*state.HelmState, error) {
|
||||
parts := bytes.Split(content, []byte("\n---\n"))
|
||||
func (ld *desiredStateLoader) renderAndLoad(env, overrodeEnv *environment.Environment, baseDir, filename string, content []byte, evaluateBases bool) (*state.HelmState, error) {
|
||||
// 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
|
||||
var env *environment.Environment
|
||||
|
||||
for i, part := range parts {
|
||||
var yamlBuf *bytes.Buffer
|
||||
@@ -164,13 +185,13 @@ func (ld *desiredStateLoader) renderAndLoad(baseDir, filename string, content []
|
||||
|
||||
id := fmt.Sprintf("%s.part.%d", filename, i)
|
||||
|
||||
if env == nil {
|
||||
if env == nil && overrodeEnv == nil {
|
||||
yamlBuf, err = ld.renderTemplatesToYaml(baseDir, id, part)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error during %s parsing: %v", id, err)
|
||||
}
|
||||
} else {
|
||||
yamlBuf, err = ld.renderTemplatesToYaml(baseDir, id, part, *env)
|
||||
yamlBuf, err = ld.renderTemplatesToYamlWithEnv(baseDir, id, part, env, overrodeEnv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error during %s parsing: %v", id, err)
|
||||
}
|
||||
@@ -181,17 +202,27 @@ func (ld *desiredStateLoader) renderAndLoad(baseDir, filename string, content []
|
||||
baseDir,
|
||||
filename,
|
||||
evaluateBases,
|
||||
env,
|
||||
overrodeEnv,
|
||||
)
|
||||
if err != nil {
|
||||
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.WithAppendSlice); 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
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
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 {
|
||||
bytes, err := yaml.Marshal(o)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
new := LoadOpts{}
|
||||
if err := yaml.Unmarshal(bytes, &new); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return new
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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 {
|
||||
state *state.HelmState
|
||||
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}
|
||||
}
|
||||
|
||||
func (r *Run) askForConfirmation(msg string) bool {
|
||||
if r.Ask != nil {
|
||||
return r.Ask(msg)
|
||||
}
|
||||
return AskForConfirmation(msg)
|
||||
}
|
||||
|
||||
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 !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 {
|
||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||
|
||||
return r.ctx.SyncReposOnce(r.state, r.helm)
|
||||
}
|
||||
|
||||
func (r *Run) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) []error {
|
||||
st := r.state
|
||||
helm := r.helm
|
||||
|
||||
affectedReleases := state.AffectedReleases{}
|
||||
errs := st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency())
|
||||
affectedReleases.DisplayAffectedReleases(c.Logger())
|
||||
return errs
|
||||
}
|
||||
|
||||
func (r *Run) Status(c StatusesConfigProvider) []error {
|
||||
workers := c.Concurrency()
|
||||
|
||||
r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...)
|
||||
|
||||
return r.state.ReleaseStatuses(r.helm, workers)
|
||||
}
|
||||
|
||||
func (a *App) diff(r *Run, c DiffConfigProvider) (*string, bool, bool, []error) {
|
||||
st := r.state
|
||||
|
||||
allReleases := st.GetReleasesWithOverrides()
|
||||
|
||||
toDiff, err := a.getSelectedReleases(r)
|
||||
if err != nil {
|
||||
return nil, false, false, []error{err}
|
||||
}
|
||||
|
||||
if len(toDiff) == 0 {
|
||||
return nil, false, false, nil
|
||||
}
|
||||
|
||||
// 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)...)
|
||||
|
||||
opts := &state.DiffOpts{
|
||||
Context: c.Context(),
|
||||
NoColor: c.NoColor(),
|
||||
Set: c.Set(),
|
||||
}
|
||||
|
||||
// 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 (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 st.TestReleases(r.helm, cleanup, timeout, concurrency, state.Logs(c.Logs()))
|
||||
}
|
||||
|
||||
func (r *Run) Lint(c LintConfigProvider) []error {
|
||||
st := r.state
|
||||
helm := r.helm
|
||||
|
||||
values := c.Values()
|
||||
args := argparser.GetArgs(c.Args(), st)
|
||||
workers := c.Concurrency()
|
||||
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
|
||||
}
|
||||
@@ -3,10 +3,10 @@ package app
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/environment"
|
||||
"github.com/roboll/helmfile/state"
|
||||
"github.com/roboll/helmfile/tmpl"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
"github.com/roboll/helmfile/pkg/tmpl"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -19,71 +19,118 @@ func prependLineNumbers(text string) string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (r *desiredStateLoader) renderEnvironment(firstPassEnv environment.Environment, baseDir, filename string, content []byte) environment.Environment {
|
||||
tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace}
|
||||
func (r *desiredStateLoader) renderPrestate(firstPassEnv *environment.Environment, baseDir, filename string, content []byte) (*environment.Environment, *state.HelmState) {
|
||||
tmplData := state.EnvironmentTemplateData{
|
||||
Environment: *firstPassEnv,
|
||||
Namespace: r.namespace,
|
||||
Values: map[string]interface{}{},
|
||||
}
|
||||
firstPassRenderer := tmpl.NewFirstPassRenderer(baseDir, tmplData)
|
||||
|
||||
// parse as much as we can, tolerate errors, this is a preparse
|
||||
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
|
||||
return firstPassEnv, nil
|
||||
}
|
||||
}
|
||||
c := state.NewCreator(r.logger, r.readFile, r.abs)
|
||||
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>", "")
|
||||
|
||||
if len(yamlData) != len(sanitized) {
|
||||
msg := "replaced <no value>s to workaround https://github.com/golang/go/issues/24963 to address https://github.com/roboll/helmfile/issues/553:\n%s"
|
||||
r.logger.Debugf(msg, cmp.Diff(yamlData, sanitized))
|
||||
}
|
||||
|
||||
c := r.underlying()
|
||||
c.Strict = false
|
||||
// create preliminary state, as we may have an environment. Tolerate errors.
|
||||
prestate, err := c.ParseAndLoadEnv(yamlBuf.Bytes(), baseDir, filename, r.env)
|
||||
prestate, err := c.ParseAndLoad([]byte(sanitized), baseDir, filename, r.env, false, firstPassEnv)
|
||||
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()))
|
||||
}
|
||||
|
||||
if prestate != nil {
|
||||
intEnv := environment.Environment{Name: firstPassEnv.Name}
|
||||
if err := mergo.Merge(&intEnv, &firstPassEnv, mergo.WithAppendSlice); err != nil {
|
||||
r.logger.Debugf("error in first-pass rendering: result of \"%s\": %v", filename, err)
|
||||
return firstPassEnv
|
||||
}
|
||||
if err := mergo.Merge(&intEnv, &prestate.Env, mergo.WithAppendSlice); err != nil {
|
||||
r.logger.Debugf("error in first-pass rendering: result of \"%s\": %v", filename, err)
|
||||
return firstPassEnv
|
||||
}
|
||||
firstPassEnv = intEnv
|
||||
}
|
||||
return firstPassEnv
|
||||
}
|
||||
|
||||
func (r *desiredStateLoader) renderTemplatesToYaml(baseDir, filename string, content []byte, context ...environment.Environment) (*bytes.Buffer, error) {
|
||||
var env environment.Environment
|
||||
|
||||
if len(context) > 0 {
|
||||
env = context[0]
|
||||
} else {
|
||||
env = environment.Environment{Name: r.env, Values: map[string]interface{}(nil)}
|
||||
firstPassEnv = &prestate.Env
|
||||
}
|
||||
|
||||
return r.twoPassRenderTemplateToYaml(env, baseDir, filename, content)
|
||||
return firstPassEnv, prestate
|
||||
}
|
||||
|
||||
func (r *desiredStateLoader) twoPassRenderTemplateToYaml(initEnv environment.Environment, baseDir, filename string, content []byte) (*bytes.Buffer, error) {
|
||||
type RenderOpts struct {
|
||||
}
|
||||
|
||||
func (r *desiredStateLoader) renderTemplatesToYaml(baseDir, filename string, content []byte) (*bytes.Buffer, error) {
|
||||
env := &environment.Environment{Name: r.env, Values: map[string]interface{}(nil)}
|
||||
|
||||
return r.renderTemplatesToYamlWithEnv(baseDir, filename, content, env, nil)
|
||||
}
|
||||
|
||||
func (r *desiredStateLoader) renderTemplatesToYamlWithEnv(baseDir, filename string, content []byte, inherited, overrode *environment.Environment) (*bytes.Buffer, error) {
|
||||
return r.twoPassRenderTemplateToYaml(inherited, overrode, baseDir, filename, content)
|
||||
}
|
||||
|
||||
func (r *desiredStateLoader) twoPassRenderTemplateToYaml(inherited, overrode *environment.Environment, baseDir, filename string, content []byte) (*bytes.Buffer, error) {
|
||||
// try a first pass render. This will always succeed, but can produce a limited env
|
||||
if r.logger != nil {
|
||||
r.logger.Debugf("first-pass rendering input of \"%s\": %v", filename, initEnv)
|
||||
r.logger.Debugf("first-pass rendering starting for \"%s\": inherited=%v, overrode=%v", filename, inherited, overrode)
|
||||
}
|
||||
|
||||
firstPassEnv := r.renderEnvironment(initEnv, baseDir, filename, content)
|
||||
initEnv, err := inherited.Merge(overrode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.logger != nil {
|
||||
r.logger.Debugf("first-pass rendering result of \"%s\": %v", filename, firstPassEnv)
|
||||
r.logger.Debugf("first-pass uses: %v", initEnv)
|
||||
}
|
||||
|
||||
tmplData := state.EnvironmentTemplateData{Environment: firstPassEnv, Namespace: r.namespace}
|
||||
renderedEnv, prestate := r.renderPrestate(initEnv, baseDir, filename, content)
|
||||
|
||||
if r.logger != nil {
|
||||
r.logger.Debugf("first-pass produced: %v", renderedEnv)
|
||||
}
|
||||
|
||||
finalEnv, err := inherited.Merge(renderedEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
finalEnv, err = finalEnv.Merge(overrode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.logger != nil {
|
||||
r.logger.Debugf("first-pass rendering result of \"%s\": %v", filename, *finalEnv)
|
||||
}
|
||||
|
||||
vals, err := finalEnv.GetMergedValues()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if prestate != nil {
|
||||
prestate.Env = *finalEnv
|
||||
r.logger.Debugf("vals:\n%v\ndefaultVals:%v", vals, prestate.DefaultValues)
|
||||
}
|
||||
|
||||
tmplData := state.EnvironmentTemplateData{
|
||||
Environment: *finalEnv,
|
||||
Namespace: r.namespace,
|
||||
Values: vals,
|
||||
}
|
||||
secondPassRenderer := tmpl.NewFileRenderer(r.readFile, baseDir, tmplData)
|
||||
yamlBuf, err := secondPassRenderer.RenderTemplateContentToBuffer(content)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/roboll/helmfile/pkg/remote"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/roboll/helmfile/helmexec"
|
||||
"github.com/roboll/helmfile/state"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
"github.com/roboll/helmfile/pkg/testhelper"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func makeLoader(readFile func(string) ([]byte, error), env string) *desiredStateLoader {
|
||||
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{
|
||||
readFile: readFile,
|
||||
env: env,
|
||||
namespace: "namespace",
|
||||
logger: helmexec.NewLogger(os.Stdout, "debug"),
|
||||
abs: filepath.Abs,
|
||||
}
|
||||
env: env,
|
||||
namespace: "namespace",
|
||||
logger: helmexec.NewLogger(os.Stdout, "debug"),
|
||||
readFile: testfs.ReadFile,
|
||||
fileExists: testfs.FileExists,
|
||||
abs: testfs.Abs,
|
||||
glob: testfs.Glob,
|
||||
remote: r,
|
||||
}, testfs
|
||||
}
|
||||
|
||||
func TestReadFromYaml_MakeEnvironmentHasNoSideEffects(t *testing.T) {
|
||||
@@ -36,30 +43,21 @@ releases:
|
||||
chart: mychart1
|
||||
`)
|
||||
|
||||
fileReaderCalls := 0
|
||||
// make a reader that returns a simulated context
|
||||
fileReader := func(filename string) ([]byte, error) {
|
||||
expectedFilename := filepath.Clean("default/values.yaml")
|
||||
if !strings.HasSuffix(filename, expectedFilename) {
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%s, actual=%s", expectedFilename, filename)
|
||||
}
|
||||
fileReaderCalls++
|
||||
if fileReaderCalls == 2 {
|
||||
return []byte("SecondPass"), nil
|
||||
}
|
||||
return []byte(""), nil
|
||||
files := map[string]string{
|
||||
"/path/to/default/values.yaml": ``,
|
||||
"/path/to/other/default/values.yaml": `SecondPass`,
|
||||
}
|
||||
|
||||
r := makeLoader(fileReader, "staging")
|
||||
r, testfs := makeLoader(files, "staging")
|
||||
yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var state state.HelmState
|
||||
err = yaml.Unmarshal(yamlBuf.Bytes(), &state)
|
||||
|
||||
if fileReaderCalls > 2 {
|
||||
if testfs.FileReaderCalls() > 2 {
|
||||
t.Error("reader should be called only twice")
|
||||
}
|
||||
|
||||
@@ -70,10 +68,10 @@ releases:
|
||||
|
||||
func TestReadFromYaml_RenderTemplate(t *testing.T) {
|
||||
|
||||
defaultValuesYaml := []byte(`
|
||||
defaultValuesYaml := `
|
||||
releaseName: "hello"
|
||||
conditionalReleaseTag: "yes"
|
||||
`)
|
||||
`
|
||||
|
||||
yamlContent := []byte(`
|
||||
environments:
|
||||
@@ -92,16 +90,11 @@ releases:
|
||||
|
||||
`)
|
||||
|
||||
// make a reader that returns a simulated context
|
||||
fileReader := func(filename string) ([]byte, error) {
|
||||
expectedFilename := filepath.Clean("default/values.yaml")
|
||||
if !strings.HasSuffix(filename, expectedFilename) {
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%s, actual=%s", expectedFilename, filename)
|
||||
}
|
||||
return defaultValuesYaml, nil
|
||||
files := map[string]string{
|
||||
"/path/to/default/values.yaml": defaultValuesYaml,
|
||||
}
|
||||
|
||||
r := makeLoader(fileReader, "staging")
|
||||
r, _ := makeLoader(files, "staging")
|
||||
// test the double rendering
|
||||
yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent)
|
||||
if err != nil {
|
||||
@@ -129,7 +122,7 @@ releases:
|
||||
}
|
||||
|
||||
func TestReadFromYaml_RenderTemplateWithValuesReferenceError(t *testing.T) {
|
||||
defaultValuesYaml := []byte("")
|
||||
defaultValuesYaml := ``
|
||||
|
||||
yamlContent := []byte(`
|
||||
environments:
|
||||
@@ -145,12 +138,11 @@ releases:
|
||||
{{ end }}
|
||||
`)
|
||||
|
||||
// make a reader that returns a simulated context
|
||||
fileReader := func(filename string) ([]byte, error) {
|
||||
return defaultValuesYaml, nil
|
||||
files := map[string]string{
|
||||
"/path/to/default/values.yaml": defaultValuesYaml,
|
||||
}
|
||||
|
||||
r := makeLoader(fileReader, "staging")
|
||||
r, _ := makeLoader(files, "staging")
|
||||
// test the double rendering
|
||||
_, err := r.renderTemplatesToYaml("", "", yamlContent)
|
||||
|
||||
@@ -164,9 +156,9 @@ releases:
|
||||
// This does not apply to .gotmpl files, which is a nice side-effect.
|
||||
func TestReadFromYaml_RenderTemplateWithGotmpl(t *testing.T) {
|
||||
|
||||
defaultValuesYamlGotmpl := []byte(`
|
||||
defaultValuesYamlGotmpl := `
|
||||
releaseName: {{ readFile "nonIgnoredFile" }}
|
||||
`)
|
||||
`
|
||||
|
||||
yamlContent := []byte(`
|
||||
environments:
|
||||
@@ -182,14 +174,12 @@ releases:
|
||||
{{ end }}
|
||||
`)
|
||||
|
||||
fileReader := func(filename string) ([]byte, error) {
|
||||
if strings.HasSuffix(filename, "nonIgnoredFile") {
|
||||
return []byte("release-a"), nil
|
||||
}
|
||||
return defaultValuesYamlGotmpl, nil
|
||||
files := map[string]string{
|
||||
"/path/to/nonIgnoredFile": `release-a`,
|
||||
"/path/to/values.yaml.gotmpl": defaultValuesYamlGotmpl,
|
||||
}
|
||||
|
||||
r := makeLoader(fileReader, "staging")
|
||||
r, _ := makeLoader(files, "staging")
|
||||
rendered, _ := r.renderTemplatesToYaml("", "", yamlContent)
|
||||
|
||||
var state state.HelmState
|
||||
@@ -205,18 +195,14 @@ releases:
|
||||
}
|
||||
|
||||
func TestReadFromYaml_RenderTemplateWithNamespace(t *testing.T) {
|
||||
defaultValuesYaml := []byte(``)
|
||||
yamlContent := []byte(`releases:
|
||||
- name: {{ .Namespace }}-myrelease
|
||||
chart: mychart
|
||||
`)
|
||||
|
||||
// make a reader that returns a simulated context
|
||||
fileReader := func(filename string) ([]byte, error) {
|
||||
return defaultValuesYaml, nil
|
||||
}
|
||||
files := map[string]string{}
|
||||
|
||||
r := makeLoader(fileReader, "staging")
|
||||
r, _ := makeLoader(files, "staging")
|
||||
yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
@@ -243,11 +229,8 @@ releases:
|
||||
{{ end }}
|
||||
chart: mychart
|
||||
`)
|
||||
fileReader := func(filename string) ([]byte, error) {
|
||||
return yamlContent, nil
|
||||
}
|
||||
|
||||
r := makeLoader(fileReader, "staging")
|
||||
r, _ := makeLoader(map[string]string{}, "staging")
|
||||
_, err := r.renderTemplatesToYaml("", "", yamlContent)
|
||||
if err == nil {
|
||||
t.Fatalf("wanted error, none returned")
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package version
|
||||
|
||||
var Version string
|
||||
@@ -1,10 +1,9 @@
|
||||
package args
|
||||
package argparser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
"strings"
|
||||
|
||||
"github.com/roboll/helmfile/state"
|
||||
)
|
||||
|
||||
type keyVal struct {
|
||||
@@ -83,10 +82,6 @@ func GetArgs(args string, state *state.HelmState) []string {
|
||||
}
|
||||
}
|
||||
|
||||
if state.HelmDefaults.KubeContext != "" {
|
||||
argsMap.SetArg("--kube-context", state.HelmDefaults.KubeContext, false)
|
||||
}
|
||||
|
||||
var argArr []string
|
||||
|
||||
for _, flag := range argsMap.flags {
|
||||
@@ -1,20 +1,23 @@
|
||||
package args
|
||||
package argparser
|
||||
|
||||
import (
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/roboll/helmfile/state"
|
||||
)
|
||||
|
||||
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 --kube-context=test"
|
||||
expectedOutput := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false --tiller-namespace ns --recreate-pods --force"
|
||||
|
||||
if compareArgs(expectedOutput, receivedArgs) == false {
|
||||
t.Errorf("expected %s, got %s", expectedOutput, strings.Join(receivedArgs, " "))
|
||||
@@ -25,10 +28,14 @@ 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 --kube-context=test"
|
||||
expectedOutput := "--timeout=3600 --set app1.bootstrap=true --set app2.bootstrap=false,app3.bootstrap=true --tiller-namespace ns --recreate-pods --force"
|
||||
|
||||
if compareArgs(expectedOutput, receivedArgs) == false {
|
||||
t.Errorf("expected %s, got %s", expectedOutput, strings.Join(receivedArgs, " "))
|
||||
@@ -0,0 +1,85 @@
|
||||
package environment
|
||||
|
||||
import (
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/pkg/maputil"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type Environment struct {
|
||||
Name string
|
||||
Values map[string]interface{}
|
||||
Defaults map[string]interface{}
|
||||
}
|
||||
|
||||
var EmptyEnvironment Environment
|
||||
|
||||
func (e Environment) DeepCopy() Environment {
|
||||
valuesBytes, err := yaml.Marshal(e.Values)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var values map[string]interface{}
|
||||
if err := yaml.Unmarshal(valuesBytes, &values); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
values, err = maputil.CastKeysToStrings(values)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defaultsBytes, err := yaml.Marshal(e.Defaults)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var defaults map[string]interface{}
|
||||
if err := yaml.Unmarshal(defaultsBytes, &defaults); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defaults, err = maputil.CastKeysToStrings(defaults)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return Environment{
|
||||
Name: e.Name,
|
||||
Values: values,
|
||||
Defaults: defaults,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Environment) Merge(other *Environment) (*Environment, error) {
|
||||
if e == nil {
|
||||
if other != nil {
|
||||
copy := other.DeepCopy()
|
||||
return ©, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
copy := e.DeepCopy()
|
||||
if other != 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)
|
||||
}
|
||||
}
|
||||
@@ -2,22 +2,26 @@ package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/roboll/helmfile/environment"
|
||||
"github.com/roboll/helmfile/helmexec"
|
||||
"github.com/roboll/helmfile/tmpl"
|
||||
"go.uber.org/zap"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/tmpl"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Hook struct {
|
||||
Name string `yaml:"name"`
|
||||
Events []string `yaml:"events"`
|
||||
Command string `yaml:"command"`
|
||||
Args []string `yaml:"args"`
|
||||
Name string `yaml:"name"`
|
||||
Events []string `yaml:"events"`
|
||||
Command string `yaml:"command"`
|
||||
Args []string `yaml:"args"`
|
||||
ShowLogs bool `yaml:"showlogs"`
|
||||
}
|
||||
|
||||
type event struct {
|
||||
Name string
|
||||
Name string
|
||||
Error error
|
||||
}
|
||||
|
||||
type Bus struct {
|
||||
@@ -34,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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,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 {
|
||||
@@ -90,6 +96,10 @@ func (bus *Bus) Trigger(evt string, context map[string]interface{}) (bool, error
|
||||
|
||||
bytes, err := bus.Runner.Execute(command, args, map[string]string{})
|
||||
bus.Logger.Debugf("hook[%s]: %s\n", name, string(bytes))
|
||||
if hook.ShowLogs {
|
||||
prefix := fmt.Sprintf("\nhook[%s] logs | ", evt)
|
||||
bus.Logger.Infow(prefix + strings.ReplaceAll(string(bytes), "\n", prefix))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("hook[%s]: command `%s` failed: %v", name, command, err)
|
||||
@@ -2,10 +2,13 @@ package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/roboll/helmfile/environment"
|
||||
"github.com/roboll/helmfile/helmexec"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest/observer"
|
||||
)
|
||||
|
||||
var logger = helmexec.NewLogger(os.Stdout, "warn")
|
||||
@@ -35,14 +38,21 @@ func TestTrigger(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
"okhook1",
|
||||
&Hook{"okhook1", []string{"foo"}, "ok", []string{}},
|
||||
&Hook{"okhook1", []string{"foo"}, "ok", []string{}, true},
|
||||
"foo",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"okhooké",
|
||||
&Hook{"okhook2", []string{"foo"}, "ok", []string{}, false},
|
||||
"foo",
|
||||
true,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"missinghook1",
|
||||
&Hook{"okhook1", []string{"foo"}, "ok", []string{}},
|
||||
&Hook{"okhook1", []string{"foo"}, "ok", []string{}, false},
|
||||
"bar",
|
||||
false,
|
||||
"",
|
||||
@@ -56,14 +66,14 @@ func TestTrigger(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"nghook1",
|
||||
&Hook{"nghook1", []string{"foo"}, "ng", []string{}},
|
||||
&Hook{"nghook1", []string{"foo"}, "ng", []string{}, false},
|
||||
"foo",
|
||||
false,
|
||||
"hook[nghook1]: command `ng` failed: cmd failed due to invalid cmd: ng",
|
||||
},
|
||||
{
|
||||
"nghook2",
|
||||
&Hook{"nghook2", []string{"foo"}, "ok", []string{"ng"}},
|
||||
&Hook{"nghook2", []string{"foo"}, "ok", []string{"ng"}, false},
|
||||
"foo",
|
||||
false,
|
||||
"hook[nghook2]: command `ok` failed: cmd failed due to invalid arg: ng",
|
||||
@@ -77,13 +87,15 @@ func TestTrigger(t *testing.T) {
|
||||
if c.hook != nil {
|
||||
hooks = append(hooks, *c.hook)
|
||||
}
|
||||
observer, observedLogs := observer.New(zap.InfoLevel)
|
||||
zeLogger := zap.New(observer).Sugar()
|
||||
bus := &Bus{
|
||||
Hooks: hooks,
|
||||
StateFilePath: "path/to/helmfile.yaml",
|
||||
BasePath: "path/to",
|
||||
Namespace: "myns",
|
||||
Env: environment.Environment{Name: "prod"},
|
||||
Logger: logger,
|
||||
Logger: zeLogger,
|
||||
ReadFile: readFile,
|
||||
}
|
||||
|
||||
@@ -92,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)
|
||||
@@ -109,5 +121,8 @@ func TestTrigger(t *testing.T) {
|
||||
t.Errorf("unexpected error for case \"%s\": %v", c.name, err)
|
||||
}
|
||||
}
|
||||
if observedLogs.Len() != 0 && !hooks[0].ShowLogs {
|
||||
t.Errorf("unexpected error for case \"%s\": Logs should not be created : %v", c.name, observedLogs.All())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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{}
|
||||
@@ -0,0 +1,437 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
type decryptedSecret struct {
|
||||
mutex sync.RWMutex
|
||||
bytes []byte
|
||||
}
|
||||
|
||||
type execer struct {
|
||||
helmBinary string
|
||||
version semver.Version
|
||||
runner Runner
|
||||
logger *zap.SugaredLogger
|
||||
kubeContext string
|
||||
extra []string
|
||||
decryptedSecretMutex sync.Mutex
|
||||
decryptedSecrets map[string]*decryptedSecret
|
||||
writeTempFile func([]byte) (string, error)
|
||||
}
|
||||
|
||||
func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger {
|
||||
var cfg zapcore.EncoderConfig
|
||||
cfg.MessageKey = "message"
|
||||
out := zapcore.AddSync(writer)
|
||||
var level zapcore.Level
|
||||
err := level.Set(logLevel)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
core := zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(cfg),
|
||||
out,
|
||||
level,
|
||||
)
|
||||
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(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: helmBinary,
|
||||
version: version,
|
||||
logger: logger,
|
||||
kubeContext: kubeContext,
|
||||
runner: runner,
|
||||
decryptedSecrets: make(map[string]*decryptedSecret),
|
||||
}
|
||||
}
|
||||
|
||||
func (helm *execer) SetExtraArgs(args ...string) {
|
||||
helm.extra = args
|
||||
}
|
||||
|
||||
func (helm *execer) SetHelmBinary(bin string) {
|
||||
helm.helmBinary = bin
|
||||
}
|
||||
|
||||
func (helm *execer) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string) error {
|
||||
var args []string
|
||||
var out []byte
|
||||
var err error
|
||||
if name == "" && repository != "" {
|
||||
helm.logger.Infof("empty field name\n")
|
||||
return fmt.Errorf("empty field name")
|
||||
}
|
||||
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
|
||||
}
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) UpdateRepo() error {
|
||||
helm.logger.Info("Updating repo")
|
||||
out, err := helm.exec([]string{"repo", "update"}, map[string]string{})
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) BuildDeps(name, chart string) error {
|
||||
helm.logger.Infof("Building dependency release=%v, chart=%v", name, chart)
|
||||
out, err := helm.exec([]string{"dependency", "build", chart}, map[string]string{})
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) UpdateDeps(chart string) error {
|
||||
helm.logger.Infof("Updating dependency %v", chart)
|
||||
out, err := helm.exec([]string{"dependency", "update", chart}, map[string]string{})
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
func (helm *execer) ReleaseStatus(context HelmContext, name string, flags ...string) error {
|
||||
helm.logger.Infof("Getting status %v", name)
|
||||
preArgs := context.GetTillerlessArgs(helm)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "status", name), flags...), env)
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) List(context HelmContext, filter string, flags ...string) (string, error) {
|
||||
helm.logger.Infof("Listing releases matching %v", filter)
|
||||
preArgs := context.GetTillerlessArgs(helm)
|
||||
env := context.getTillerlessEnv()
|
||||
var args []string
|
||||
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
|
||||
}
|
||||
|
||||
func (helm *execer) DecryptSecret(context HelmContext, name string, flags ...string) (string, error) {
|
||||
absPath, err := filepath.Abs(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
helm.logger.Debugf("Preparing to decrypt secret %v", absPath)
|
||||
helm.decryptedSecretMutex.Lock()
|
||||
|
||||
secret, ok := helm.decryptedSecrets[absPath]
|
||||
|
||||
// Cache miss
|
||||
if !ok {
|
||||
|
||||
secret = &decryptedSecret{}
|
||||
helm.decryptedSecrets[absPath] = secret
|
||||
|
||||
secret.mutex.Lock()
|
||||
defer secret.mutex.Unlock()
|
||||
helm.decryptedSecretMutex.Unlock()
|
||||
|
||||
helm.logger.Infof("Decrypting secret %v", absPath)
|
||||
preArgs := context.GetTillerlessArgs(helm)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "secrets", "dec", absPath), flags...), env)
|
||||
helm.info(out)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// HELM_SECRETS_DEC_SUFFIX is used by the helm-secrets plugin to define the output file
|
||||
decSuffix := os.Getenv("HELM_SECRETS_DEC_SUFFIX")
|
||||
if len(decSuffix) == 0 {
|
||||
decSuffix = ".yaml.dec"
|
||||
}
|
||||
decFilename := strings.Replace(absPath, ".yaml", decSuffix, 1)
|
||||
|
||||
secretBytes, err := ioutil.ReadFile(decFilename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
secret.bytes = secretBytes
|
||||
|
||||
if err := os.Remove(decFilename); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
} else {
|
||||
// Cache hit
|
||||
helm.logger.Debugf("Found secret in cache %v", absPath)
|
||||
|
||||
secret.mutex.RLock()
|
||||
helm.decryptedSecretMutex.Unlock()
|
||||
defer secret.mutex.RUnlock()
|
||||
}
|
||||
|
||||
tempFile := helm.writeTempFile
|
||||
|
||||
if tempFile == nil {
|
||||
tempFile = func(content []byte) (string, error) {
|
||||
tmpFile, err := ioutil.TempFile("", "secret")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = tmpFile.Write(content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tmpFile.Name(), nil
|
||||
}
|
||||
}
|
||||
|
||||
tmpFileName, err := tempFile(secret.bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
helm.logger.Debugf("Decrypted %s into %s", absPath, tmpFileName)
|
||||
|
||||
return tmpFileName, err
|
||||
}
|
||||
|
||||
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() {
|
||||
args = []string{"template", name, chart}
|
||||
} else {
|
||||
args = []string{"template", chart, "--name", name}
|
||||
}
|
||||
|
||||
out, err := helm.exec(append(args, flags...), map[string]string{})
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
// Unfortunately, this works only when you run helmfile with `--detailed-exitcode`
|
||||
detailedExitcodeEnabled := false
|
||||
for _, f := range flags {
|
||||
if strings.Contains(f, "detailed-exitcode") {
|
||||
detailedExitcodeEnabled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if detailedExitcodeEnabled {
|
||||
switch e := err.(type) {
|
||||
case ExitError:
|
||||
if e.ExitStatus() == 2 {
|
||||
if !(suppressDiff) {
|
||||
helm.write(out)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if !(suppressDiff) {
|
||||
helm.write(out)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) Lint(name, chart string, flags ...string) error {
|
||||
helm.logger.Infof("Linting release=%v, chart=%v", name, chart)
|
||||
out, err := helm.exec(append([]string{"lint", chart}, flags...), map[string]string{})
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) Fetch(chart string, flags ...string) error {
|
||||
helm.logger.Infof("Fetching %v", chart)
|
||||
out, err := helm.exec(append([]string{"fetch", chart}, flags...), map[string]string{})
|
||||
helm.info(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) DeleteRelease(context HelmContext, name string, flags ...string) error {
|
||||
helm.logger.Infof("Deleting %v", name)
|
||||
preArgs := context.GetTillerlessArgs(helm)
|
||||
env := context.getTillerlessEnv()
|
||||
out, err := helm.exec(append(append(preArgs, "delete", name), flags...), env)
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) TestRelease(context HelmContext, name string, flags ...string) error {
|
||||
helm.logger.Infof("Testing %v", name)
|
||||
preArgs := context.GetTillerlessArgs(helm)
|
||||
env := context.getTillerlessEnv()
|
||||
args := []string{"test", name}
|
||||
out, err := helm.exec(append(append(preArgs, args...), flags...), env)
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) exec(args []string, env map[string]string) ([]byte, error) {
|
||||
cmdargs := args
|
||||
if len(helm.extra) > 0 {
|
||||
cmdargs = append(cmdargs, helm.extra...)
|
||||
}
|
||||
if 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
|
||||
}
|
||||
|
||||
func (helm *execer) info(out []byte) {
|
||||
if len(out) > 0 {
|
||||
helm.logger.Infof("%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func (helm *execer) write(out []byte) {
|
||||
if len(out) > 0 {
|
||||
fmt.Printf("%s\n", out)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -2,11 +2,15 @@ package helmexec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -18,12 +22,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)
|
||||
execer.runner = &mockRunner{}
|
||||
execer := New("helm", logger, kubeContext, &mockRunner{})
|
||||
return execer
|
||||
}
|
||||
|
||||
@@ -31,8 +34,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")
|
||||
helm := MockExecer(NewLogger(buffer, "debug"), "dev")
|
||||
if helm.kubeContext != "dev" {
|
||||
t.Error("helmexec.New() - kubeContext")
|
||||
}
|
||||
@@ -45,7 +47,7 @@ func TestNewHelmExec(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_SetExtraArgs(t *testing.T) {
|
||||
helm := New(NewLogger(os.Stdout, "info"), "dev")
|
||||
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")
|
||||
@@ -61,7 +63,7 @@ func Test_SetExtraArgs(t *testing.T) {
|
||||
}
|
||||
|
||||
func Test_SetHelmBinary(t *testing.T) {
|
||||
helm := New(NewLogger(os.Stdout, "info"), "dev")
|
||||
helm := MockExecer(NewLogger(os.Stdout, "info"), "dev")
|
||||
if helm.helmBinary != "helm" {
|
||||
t.Error("helmexec.command - default command is not helm")
|
||||
}
|
||||
@@ -71,34 +73,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/", "", "", "", "")
|
||||
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/ --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/ --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/", "", "", "example_user", "example_password")
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "", "", "")
|
||||
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/
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
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 --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)
|
||||
@@ -111,8 +165,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)
|
||||
@@ -124,9 +177,8 @@ func Test_SyncRelease(t *testing.T) {
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.SyncRelease(HelmContext{}, "release", "chart", "--timeout 10", "--wait")
|
||||
expected := `Upgrading 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:
|
||||
expected := `Upgrading release=release, chart=chart
|
||||
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)
|
||||
@@ -134,9 +186,8 @@ exec: helm upgrade --install --reset-values release chart --timeout 10 --wait --
|
||||
|
||||
buffer.Reset()
|
||||
helm.SyncRelease(HelmContext{}, "release", "chart")
|
||||
expected = `Upgrading chart
|
||||
exec: helm upgrade --install --reset-values release chart --kube-context dev
|
||||
exec: helm upgrade --install --reset-values release chart --kube-context dev:
|
||||
expected = `Upgrading release=release, chart=chart
|
||||
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)
|
||||
@@ -149,9 +200,8 @@ func Test_SyncReleaseTillerless(t *testing.T) {
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.SyncRelease(HelmContext{Tillerless: true, TillerNamespace: "foo"}, "release", "chart",
|
||||
"--timeout 10", "--wait")
|
||||
expected := `Upgrading 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:
|
||||
expected := `Upgrading release=release, chart=chart
|
||||
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)
|
||||
@@ -164,8 +214,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)
|
||||
@@ -175,8 +224,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)
|
||||
@@ -187,10 +235,9 @@ func Test_BuildDeps(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.BuildDeps("./chart/foo")
|
||||
expected := `Building dependency ./chart/foo
|
||||
exec: helm dependency build ./chart/foo --kube-context dev
|
||||
exec: helm dependency build ./chart/foo --kube-context dev:
|
||||
helm.BuildDeps("foo", "./chart/foo")
|
||||
expected := `Building dependency release=foo, chart=./chart/foo
|
||||
exec: helm --kube-context dev dependency build ./chart/foo
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.BuildDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
@@ -198,10 +245,9 @@ exec: helm dependency build ./chart/foo --kube-context dev:
|
||||
|
||||
buffer.Reset()
|
||||
helm.SetExtraArgs("--verify")
|
||||
helm.BuildDeps("./chart/foo")
|
||||
expected = `Building dependency ./chart/foo
|
||||
exec: helm dependency build ./chart/foo --verify --kube-context dev
|
||||
exec: helm dependency build ./chart/foo --verify --kube-context dev:
|
||||
helm.BuildDeps("foo", "./chart/foo")
|
||||
expected = `Building dependency release=foo, chart=./chart/foo
|
||||
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)
|
||||
@@ -212,13 +258,29 @@ func Test_DecryptSecret(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
|
||||
tmpFilePath := "path/to/temp/file"
|
||||
helm.writeTempFile = func(content []byte) (string, error) {
|
||||
return tmpFilePath, nil
|
||||
}
|
||||
|
||||
helm.DecryptSecret(HelmContext{}, "secretName")
|
||||
expected := `Decrypting secret secretName
|
||||
exec: helm secrets dec secretName --kube-context dev
|
||||
exec: helm secrets dec secretName --kube-context dev:
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DecryptSecret()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
cwd, err := filepath.Abs(".")
|
||||
if err != nil {
|
||||
t.Errorf("Error: %v", err)
|
||||
}
|
||||
// Run again for caching
|
||||
helm.DecryptSecret(HelmContext{}, "secretName")
|
||||
|
||||
expected := fmt.Sprintf(`Preparing to decrypt secret %v/secretName
|
||||
Decrypting secret %s/secretName
|
||||
exec: helm --kube-context dev secrets dec %s/secretName
|
||||
Preparing to decrypt secret %s/secretName
|
||||
Found secret in cache %s/secretName
|
||||
Decrypted %s/secretName into path/to/temp/file
|
||||
`, cwd, cwd, cwd, cwd, cwd, cwd)
|
||||
if d := cmp.Diff(expected, buffer.String()); d != "" {
|
||||
t.Errorf("helmexec.DecryptSecret(): want (-), got (+):\n%s", d)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,20 +288,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")
|
||||
expected := `Comparing release chart
|
||||
exec: helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev
|
||||
exec: helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev:
|
||||
helm.DiffRelease(HelmContext{}, "release", "chart", false, "--timeout 10", "--wait")
|
||||
expected := `Comparing release=release, chart=chart
|
||||
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")
|
||||
expected = `Comparing release chart
|
||||
exec: helm diff upgrade --allow-unreleased release chart --kube-context dev
|
||||
exec: helm diff upgrade --allow-unreleased release chart --kube-context dev:
|
||||
helm.DiffRelease(HelmContext{}, "release", "chart", false)
|
||||
expected = `Comparing release=release, chart=chart
|
||||
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)
|
||||
@@ -250,10 +310,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")
|
||||
expected := `Comparing release chart
|
||||
exec: helm tiller run -- helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev
|
||||
exec: helm tiller run -- helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev:
|
||||
helm.DiffRelease(HelmContext{Tillerless: true}, "release", "chart", false, "--timeout 10", "--wait")
|
||||
expected := `Comparing release=release, chart=chart
|
||||
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)
|
||||
@@ -266,8 +325,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)
|
||||
@@ -279,8 +337,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)
|
||||
@@ -293,8 +350,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)
|
||||
@@ -306,8 +362,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)
|
||||
@@ -320,8 +375,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)
|
||||
@@ -335,7 +389,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)
|
||||
@@ -350,8 +403,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)
|
||||
@@ -359,8 +411,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)
|
||||
@@ -369,8 +420,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)
|
||||
@@ -381,7 +431,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)
|
||||
@@ -392,10 +441,9 @@ func Test_Lint(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.Lint("path/to/chart", "--values", "file.yml")
|
||||
expected := `Linting 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:
|
||||
helm.Lint("release", "path/to/chart", "--values", "file.yml")
|
||||
expected := `Linting release=release, chart=path/to/chart
|
||||
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)
|
||||
@@ -408,8 +456,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)
|
||||
@@ -419,7 +466,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/
|
||||
`,
|
||||
@@ -432,7 +478,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)
|
||||
}
|
||||
@@ -472,3 +518,70 @@ func Test_mergeEnv(t *testing.T) {
|
||||
t.Errorf("mergeEnv()\nactual = %v\nexpect = %v", actual, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Template(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.TemplateRelease("release", "path/to/chart", "--values", "file.yml")
|
||||
expected := `Templating release=release, chart=path/to/chart
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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{
|
||||
Message: fmt.Sprintf("command %q exited with non-zero status:\n\n%s", path, out),
|
||||
Code: exitStatus,
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
|
||||
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 b.String()
|
||||
}
|
||||
|
||||
// ExitError is created whenever your shell command exits with a non-zero exit status
|
||||
type ExitError struct {
|
||||
Message string
|
||||
Code int
|
||||
}
|
||||
|
||||
func (e ExitError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func (e ExitError) ExitStatus() int {
|
||||
return e.Code
|
||||
}
|
||||
@@ -1,22 +1,37 @@
|
||||
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, certfile, keyfile, username, password string) error
|
||||
AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string) error
|
||||
UpdateRepo() error
|
||||
BuildDeps(chart string) 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
|
||||
TemplateRelease(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(chart string, flags ...string) error
|
||||
Lint(name, chart string, flags ...string) error
|
||||
ReleaseStatus(context HelmContext, name string, flags ...string) error
|
||||
DeleteRelease(context HelmContext, name string, flags ...string) error
|
||||
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
|
||||
}
|
||||
@@ -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 (
|
||||
@@ -25,7 +28,7 @@ type Runner interface {
|
||||
type ShellRunner struct {
|
||||
Dir string
|
||||
|
||||
logger *zap.SugaredLogger
|
||||
Logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
// Execute a shell command
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package maputil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CastKeysToStrings(s interface{}) (map[string]interface{}, error) {
|
||||
new := map[string]interface{}{}
|
||||
switch src := s.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
for k, v := range src {
|
||||
var str_k string
|
||||
switch typed_k := k.(type) {
|
||||
case string:
|
||||
str_k = typed_k
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected type of key in map: expected string, got %T: value=%v, map=%v", typed_k, typed_k, src)
|
||||
}
|
||||
|
||||
casted_v, err := recursivelyStringifyMapKey(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
new[str_k] = casted_v
|
||||
}
|
||||
case map[string]interface{}:
|
||||
for k, v := range src {
|
||||
casted_v, err := recursivelyStringifyMapKey(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
new[k] = casted_v
|
||||
}
|
||||
}
|
||||
return new, nil
|
||||
}
|
||||
|
||||
func recursivelyStringifyMapKey(v interface{}) (interface{}, error) {
|
||||
var casted_v interface{}
|
||||
switch typed_v := v.(type) {
|
||||
case map[interface{}]interface{}, map[string]interface{}:
|
||||
tmp, err := CastKeysToStrings(typed_v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
casted_v = tmp
|
||||
case []interface{}:
|
||||
a := []interface{}{}
|
||||
for i := range typed_v {
|
||||
res, err := recursivelyStringifyMapKey(typed_v[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a = append(a, res)
|
||||
}
|
||||
casted_v = a
|
||||
default:
|
||||
casted_v = typed_v
|
||||
}
|
||||
return casted_v, nil
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
|
||||
for len(key) > 1 {
|
||||
m, key = getCursor(key[0]).getMap(m), key[1:]
|
||||
}
|
||||
|
||||
getCursor(key[0]).set(m, value)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package maputil
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMapUtil_StrKeys(t *testing.T) {
|
||||
m := map[string]interface{}{
|
||||
"a": []interface{}{
|
||||
map[string]interface{}{
|
||||
"b": []interface{}{
|
||||
map[string]interface{}{
|
||||
"c": "C",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r, err := CastKeysToStrings(m)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
a := r["a"].([]interface{})
|
||||
a0 := a[0].(map[string]interface{})
|
||||
b := a0["b"].([]interface{})
|
||||
b0 := b[0].(map[string]interface{})
|
||||
c := b0["c"]
|
||||
|
||||
if c != "C" {
|
||||
t.Errorf("unexpected c: expected=C, got=%s", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapUtil_IFKeys(t *testing.T) {
|
||||
m := map[interface{}]interface{}{
|
||||
"a": []interface{}{
|
||||
map[interface{}]interface{}{
|
||||
"b": []interface{}{
|
||||
map[interface{}]interface{}{
|
||||
"c": "C",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r, err := CastKeysToStrings(m)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
a := r["a"].([]interface{})
|
||||
a0 := a[0].(map[string]interface{})
|
||||
b := a0["b"].([]interface{})
|
||||
b0 := b[0].(map[string]interface{})
|
||||
c := b0["c"]
|
||||
|
||||
if c != "C" {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package remote
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/hashicorp/go-getter"
|
||||
"github.com/hashicorp/go-getter/helper/url"
|
||||
"go.uber.org/multierr"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v2"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const DefaultCacheDir = ".helmfile/cache"
|
||||
|
||||
type Remote struct {
|
||||
Logger *zap.SugaredLogger
|
||||
|
||||
// Home is the home directory for helmfile. Usually this points to $HOME of the user running helmfile.
|
||||
// Helmfile saves fetched remote files into .helmfile/cache under home
|
||||
Home string
|
||||
|
||||
// Getter is the underlying implementation of getter used for fetching remote files
|
||||
Getter Getter
|
||||
|
||||
// ReadFile is the implementation of the file reader that reads a local file from the specified path.
|
||||
// Inject any implementation of your choice, like an im-memory impl for testing, ioutil.ReadFile for the real-world use.
|
||||
ReadFile func(string) ([]byte, error)
|
||||
DirExists func(string) bool
|
||||
FileExists func(string) bool
|
||||
}
|
||||
|
||||
func (r *Remote) Unmarshal(src string, dst interface{}) error {
|
||||
bytes, err := r.GetBytes(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
strs := strings.Split(src, "/")
|
||||
file := strs[len(strs)-1]
|
||||
ext := filepath.Ext(file)
|
||||
|
||||
{
|
||||
r.Logger.Debugf("unmarshalling %s", string(bytes))
|
||||
|
||||
var err error
|
||||
switch ext {
|
||||
case "json":
|
||||
err = json.Unmarshal(bytes, dst)
|
||||
default:
|
||||
err = yaml.Unmarshal(bytes, dst)
|
||||
}
|
||||
|
||||
r.Logger.Debugf("unmarshalled to %v", dst)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Remote) GetBytes(goGetterSrc string) ([]byte, error) {
|
||||
f, err := r.Fetch(goGetterSrc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bytes, err := r.ReadFile(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read file: %v", err)
|
||||
}
|
||||
|
||||
return bytes, nil
|
||||
}
|
||||
|
||||
// Locate takes an URL to a remote file or a path to a local file.
|
||||
// If the argument was an URL, it fetches the remote directory contained within the URL,
|
||||
// and returns the path to the file in the fetched directory
|
||||
func (r *Remote) Locate(urlOrPath string) (string, error) {
|
||||
if r.FileExists(urlOrPath) || r.DirExists(urlOrPath) {
|
||||
return urlOrPath, nil
|
||||
}
|
||||
fetched, err := r.Fetch(urlOrPath)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case InvalidURLError:
|
||||
return urlOrPath, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return fetched, nil
|
||||
}
|
||||
|
||||
type InvalidURLError struct {
|
||||
err string
|
||||
}
|
||||
|
||||
func (e InvalidURLError) Error() string {
|
||||
return e.err
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
Getter, Scheme, User, Host, Dir, File, RawQuery string
|
||||
}
|
||||
|
||||
func IsRemote(goGetterSrc string) bool {
|
||||
if _, err := Parse(goGetterSrc); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func Parse(goGetterSrc string) (*Source, error) {
|
||||
items := strings.Split(goGetterSrc, "::")
|
||||
var getter string
|
||||
switch len(items) {
|
||||
case 2:
|
||||
getter = items[0]
|
||||
goGetterSrc = items[1]
|
||||
}
|
||||
|
||||
u, err := url.Parse(goGetterSrc)
|
||||
if err != nil {
|
||||
return nil, InvalidURLError{err: fmt.Sprintf("parse url: %v", err)}
|
||||
}
|
||||
|
||||
if u.Scheme == "" {
|
||||
return nil, InvalidURLError{err: fmt.Sprintf("parse url: missing scheme - probably this is a local file path? %s", goGetterSrc)}
|
||||
}
|
||||
|
||||
pathComponents := strings.Split(u.Path, "@")
|
||||
if len(pathComponents) != 2 {
|
||||
return nil, fmt.Errorf("invalid src format: it must be `[<getter>::]<scheme>://<host>/<path/to/dir>@<path/to/file>?key1=val1&key2=val2: got %s", goGetterSrc)
|
||||
}
|
||||
|
||||
return &Source{
|
||||
Getter: getter,
|
||||
User: u.User.String(),
|
||||
Scheme: u.Scheme,
|
||||
Host: u.Host,
|
||||
Dir: pathComponents[0],
|
||||
File: pathComponents[1],
|
||||
RawQuery: u.RawQuery,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Remote) Fetch(goGetterSrc string) (string, error) {
|
||||
u, err := Parse(goGetterSrc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
srcDir := fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Dir)
|
||||
file := u.File
|
||||
|
||||
r.Logger.Debugf("getter: %s", u.Getter)
|
||||
r.Logger.Debugf("scheme: %s", u.Scheme)
|
||||
r.Logger.Debugf("user: %s", u.User)
|
||||
r.Logger.Debugf("host: %s", u.Host)
|
||||
r.Logger.Debugf("dir: %s", u.Dir)
|
||||
r.Logger.Debugf("file: %s", u.File)
|
||||
|
||||
// This should be shared across variant commands, so that they can share cache for the shared imports
|
||||
cacheBaseDir := DefaultCacheDir
|
||||
|
||||
query := u.RawQuery
|
||||
|
||||
var cacheKey string
|
||||
replacer := strings.NewReplacer(":", "", "//", "_", "/", "_", ".", "_")
|
||||
dirKey := replacer.Replace(srcDir)
|
||||
if len(query) > 0 {
|
||||
paramsKey := strings.Replace(query, "&", "_", -1)
|
||||
cacheKey = fmt.Sprintf("%s.%s", dirKey, paramsKey)
|
||||
} else {
|
||||
cacheKey = dirKey
|
||||
}
|
||||
|
||||
cached := false
|
||||
|
||||
// e.g. .helmfile/cache/https_github_com_cloudposse_helmfiles_git.ref=0.xx.0
|
||||
getterDst := filepath.Join(cacheBaseDir, cacheKey)
|
||||
|
||||
// e.g. $PWD/.helmfile/cache/https_github_com_cloudposse_helmfiles_git.ref=0.xx.0
|
||||
cacheDirPath := filepath.Join(r.Home, getterDst)
|
||||
|
||||
r.Logger.Debugf("home: %s", r.Home)
|
||||
r.Logger.Debugf("getter dest: %s", getterDst)
|
||||
r.Logger.Debugf("cached dir: %s", cacheDirPath)
|
||||
|
||||
{
|
||||
if r.FileExists(cacheDirPath) {
|
||||
return "", fmt.Errorf("%s is not directory. please remove it so that variant could use it for dependency caching", getterDst)
|
||||
}
|
||||
|
||||
if r.DirExists(cacheDirPath) {
|
||||
cached = true
|
||||
}
|
||||
}
|
||||
|
||||
if !cached {
|
||||
var getterSrc string
|
||||
if u.User != "" {
|
||||
getterSrc = fmt.Sprintf("%s://%s@%s%s", u.Scheme, u.User, u.Host, u.Dir)
|
||||
} else {
|
||||
getterSrc = fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Dir)
|
||||
}
|
||||
|
||||
if len(query) > 0 {
|
||||
getterSrc = strings.Join([]string{getterSrc, query}, "?")
|
||||
}
|
||||
|
||||
if u.Getter != "" {
|
||||
getterSrc = u.Getter + "::" + getterSrc
|
||||
}
|
||||
|
||||
r.Logger.Debugf("downloading %s to %s", getterSrc, getterDst)
|
||||
|
||||
if err := r.Getter.Get(r.Home, getterSrc, cacheDirPath); err != nil {
|
||||
rmerr := os.RemoveAll(cacheDirPath)
|
||||
if rmerr != nil {
|
||||
return "", multierr.Append(err, rmerr)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return filepath.Join(cacheDirPath, file), nil
|
||||
}
|
||||
|
||||
type Getter interface {
|
||||
Get(wd, src, dst string) error
|
||||
}
|
||||
|
||||
type GoGetter struct {
|
||||
Logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
func (g *GoGetter) Get(wd, src, dst string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
get := &getter.Client{
|
||||
Ctx: ctx,
|
||||
Src: src,
|
||||
Dst: dst,
|
||||
Pwd: wd,
|
||||
Mode: getter.ClientModeDir,
|
||||
Options: []getter.ClientOption{},
|
||||
}
|
||||
|
||||
g.Logger.Debugf("client: %+v", *get)
|
||||
|
||||
if err := get.Get(); err != nil {
|
||||
return fmt.Errorf("get: %v", err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package remote
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/testhelper"
|
||||
)
|
||||
|
||||
func TestRemote_HttpsGitHub(t *testing.T) {
|
||||
cleanfs := map[string]string{
|
||||
"/path/to/home": "",
|
||||
}
|
||||
cachefs := map[string]string{
|
||||
"/path/to/home/.helmfile/cache/https_github_com_cloudposse_helmfiles_git.ref=0.40.0/releases/kiam.yaml": "foo: bar",
|
||||
}
|
||||
|
||||
type testcase struct {
|
||||
files map[string]string
|
||||
expectCacheHit bool
|
||||
}
|
||||
|
||||
testcases := []testcase{
|
||||
{files: cleanfs, expectCacheHit: false},
|
||||
{files: cachefs, expectCacheHit: true},
|
||||
}
|
||||
|
||||
for i := range testcases {
|
||||
testcase := testcases[i]
|
||||
|
||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
||||
testfs := testhelper.NewTestFs(testcase.files)
|
||||
|
||||
hit := true
|
||||
|
||||
get := func(wd, src, dst string) error {
|
||||
if wd != "/path/to/home" {
|
||||
return fmt.Errorf("unexpected wd: %s", wd)
|
||||
}
|
||||
if src != "git::https://github.com/cloudposse/helmfiles.git?ref=0.40.0" {
|
||||
return fmt.Errorf("unexpected src: %s", src)
|
||||
}
|
||||
|
||||
hit = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
getter := &testGetter{
|
||||
get: get,
|
||||
}
|
||||
remote := &Remote{
|
||||
Logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
Home: "/path/to/home",
|
||||
Getter: getter,
|
||||
ReadFile: testfs.ReadFile,
|
||||
FileExists: testfs.FileExistsAt,
|
||||
DirExists: testfs.DirectoryExistsAt,
|
||||
}
|
||||
|
||||
// FYI, go-getter in the `dir` mode accepts URL like the below. So helmfile expects URLs similar to it:
|
||||
// go-getter -mode dir git::https://github.com/cloudposse/helmfiles.git?ref=0.40.0 gettertest1/b
|
||||
|
||||
// We use `@` to separate dir and the file path. This is a good idea borrowed from helm-git:
|
||||
// https://github.com/aslafy-z/helm-git
|
||||
|
||||
url := "git::https://github.com/cloudposse/helmfiles.git@releases/kiam.yaml?ref=0.40.0"
|
||||
file, err := remote.Locate(url)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if file != "/path/to/home/.helmfile/cache/https_github_com_cloudposse_helmfiles_git.ref=0.40.0/releases/kiam.yaml" {
|
||||
t.Errorf("unexpected file located: %s", file)
|
||||
}
|
||||
|
||||
if testcase.expectCacheHit && !hit {
|
||||
t.Errorf("unexpected result: unexpected cache miss")
|
||||
}
|
||||
if !testcase.expectCacheHit && hit {
|
||||
t.Errorf("unexpected result: unexpected cache hit")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemote_SShGitHub(t *testing.T) {
|
||||
cleanfs := map[string]string{
|
||||
"/path/to/home": "",
|
||||
}
|
||||
cachefs := map[string]string{
|
||||
"/path/to/home/.helmfile/cache/ssh_github_com_cloudposse_helmfiles_git.ref=0.40.0/releases/kiam.yaml": "foo: bar",
|
||||
}
|
||||
|
||||
type testcase struct {
|
||||
files map[string]string
|
||||
expectCacheHit bool
|
||||
}
|
||||
|
||||
testcases := []testcase{
|
||||
{files: cleanfs, expectCacheHit: false},
|
||||
{files: cachefs, expectCacheHit: true},
|
||||
}
|
||||
|
||||
for i := range testcases {
|
||||
testcase := testcases[i]
|
||||
|
||||
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
|
||||
testfs := testhelper.NewTestFs(testcase.files)
|
||||
|
||||
hit := true
|
||||
|
||||
get := func(wd, src, dst string) error {
|
||||
if wd != "/path/to/home" {
|
||||
return fmt.Errorf("unexpected wd: %s", wd)
|
||||
}
|
||||
if src != "git::ssh://git@github.com/cloudposse/helmfiles.git?ref=0.40.0" {
|
||||
return fmt.Errorf("unexpected src: %s", src)
|
||||
}
|
||||
|
||||
hit = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
getter := &testGetter{
|
||||
get: get,
|
||||
}
|
||||
remote := &Remote{
|
||||
Logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
Home: "/path/to/home",
|
||||
Getter: getter,
|
||||
ReadFile: testfs.ReadFile,
|
||||
FileExists: testfs.FileExistsAt,
|
||||
DirExists: testfs.DirectoryExistsAt,
|
||||
}
|
||||
|
||||
url := "git::ssh://git@github.com/cloudposse/helmfiles.git@releases/kiam.yaml?ref=0.40.0"
|
||||
file, err := remote.Locate(url)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if file != "/path/to/home/.helmfile/cache/ssh_github_com_cloudposse_helmfiles_git.ref=0.40.0/releases/kiam.yaml" {
|
||||
t.Errorf("unexpected file located: %s", file)
|
||||
}
|
||||
|
||||
if testcase.expectCacheHit && !hit {
|
||||
t.Errorf("unexpected result: unexpected cache miss")
|
||||
}
|
||||
if !testcase.expectCacheHit && hit {
|
||||
t.Errorf("unexpected result: unexpected cache hit")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (t *testGetter) Get(wd, src, dst string) error {
|
||||
return t.get(wd, src, dst)
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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 {
|
||||
Name string `yaml:"name"`
|
||||
}
|
||||
|
||||
type unresolvedChartDependency struct {
|
||||
// ChartName identifies the dependant chart. In Helmfile, ChartName for `chart: stable/envoy` would be just `envoy`.
|
||||
// It can't be collided with other charts referenced in the same helmfile spec.
|
||||
// That is, collocating `chart: incubator/foo` and `chart: stable/foo` isn't allowed. Name them differently for a work-around.
|
||||
ChartName string `yaml:"name"`
|
||||
// Repository contains the URL for the helm chart repository that hosts the chart identified by ChartName
|
||||
Repository string `yaml:"repository"`
|
||||
// VersionConstraint is the version constraint of the dependent chart. "*" means the latest version.
|
||||
VersionConstraint string `yaml:"version"`
|
||||
}
|
||||
|
||||
type ResolvedChartDependency struct {
|
||||
// ChartName identifies the dependant chart. In Helmfile, ChartName for `chart: stable/envoy` would be just `envoy`.
|
||||
// It can't be collided with other charts referenced in the same helmfile spec.
|
||||
// That is, collocating `chart: incubator/foo` and `chart: stable/foo` isn't allowed. Name them differently for a work-around.
|
||||
ChartName string `yaml:"name"`
|
||||
// Repository contains the URL for the helm chart repository that hosts the chart identified by ChartName
|
||||
Repository string `yaml:"repository"`
|
||||
// Version is the version number of the dependent chart.
|
||||
// In the context of helmfile this can be omitted. When omitted, it is considered `*` which results helm/helmfile fetching the latest version.
|
||||
Version string `yaml:"version"`
|
||||
}
|
||||
|
||||
type UnresolvedDependencies struct {
|
||||
deps map[string][]unresolvedChartDependency
|
||||
}
|
||||
|
||||
type ChartRequirements struct {
|
||||
UnresolvedDependencies []unresolvedChartDependency `yaml:"dependencies"`
|
||||
}
|
||||
|
||||
type ChartLockedRequirements struct {
|
||||
Version string `yaml:"version"`
|
||||
ResolvedDependencies []ResolvedChartDependency `yaml:"dependencies"`
|
||||
Digest string `yaml:"digest"`
|
||||
Generated string `yaml:"generated"`
|
||||
}
|
||||
|
||||
func (d *UnresolvedDependencies) Add(chart, url, versionConstraint string) error {
|
||||
dep := unresolvedChartDependency{
|
||||
ChartName: chart,
|
||||
Repository: url,
|
||||
VersionConstraint: versionConstraint,
|
||||
}
|
||||
return d.add(dep)
|
||||
}
|
||||
|
||||
func (d *UnresolvedDependencies) add(dep unresolvedChartDependency) error {
|
||||
deps := d.deps[dep.ChartName]
|
||||
if deps == nil {
|
||||
deps = []unresolvedChartDependency{dep}
|
||||
} else {
|
||||
deps = append(deps, dep)
|
||||
}
|
||||
d.deps[dep.ChartName] = deps
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *UnresolvedDependencies) ToChartRequirements() *ChartRequirements {
|
||||
deps := []unresolvedChartDependency{}
|
||||
|
||||
for _, ds := range d.deps {
|
||||
for _, d := range ds {
|
||||
if d.VersionConstraint == "" {
|
||||
d.VersionConstraint = "*"
|
||||
}
|
||||
deps = append(deps, d)
|
||||
}
|
||||
}
|
||||
|
||||
return &ChartRequirements{UnresolvedDependencies: deps}
|
||||
}
|
||||
|
||||
type ResolvedDependencies struct {
|
||||
deps map[string][]ResolvedChartDependency
|
||||
}
|
||||
|
||||
func (d *ResolvedDependencies) add(dep ResolvedChartDependency) error {
|
||||
deps := d.deps[dep.ChartName]
|
||||
if deps == nil {
|
||||
deps = []ResolvedChartDependency{dep}
|
||||
} else {
|
||||
deps = append(deps, dep)
|
||||
}
|
||||
d.deps[dep.ChartName] = deps
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *ResolvedDependencies) Get(chart, versionConstraint string) (string, error) {
|
||||
if versionConstraint == "" {
|
||||
versionConstraint = "*"
|
||||
}
|
||||
|
||||
deps, exists := d.deps[chart]
|
||||
if exists {
|
||||
for _, dep := range deps {
|
||||
constraint, err := semver.NewConstraint(versionConstraint)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
version, err := semver.NewVersion(dep.Version)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if constraint.Check(version) {
|
||||
return dep.Version, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no resolved dependency found for \"%s\"", chart)
|
||||
}
|
||||
|
||||
func (st *HelmState) mergeLockedDependencies() (*HelmState, error) {
|
||||
filename, unresolved, err := getUnresolvedDependenciess(st)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(unresolved.deps) == 0 {
|
||||
return st, nil
|
||||
}
|
||||
|
||||
depMan := NewChartDependencyManager(filename, st.logger)
|
||||
|
||||
if st.readFile != nil {
|
||||
depMan.readFile = st.readFile
|
||||
}
|
||||
|
||||
return resolveDependencies(st, depMan, unresolved)
|
||||
}
|
||||
|
||||
func resolveDependencies(st *HelmState, depMan *chartDependencyManager, unresolved *UnresolvedDependencies) (*HelmState, error) {
|
||||
resolved, lockfileExists, err := depMan.Resolve(unresolved)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to resolve %d deps: %v", len(unresolved.deps), err)
|
||||
}
|
||||
if !lockfileExists {
|
||||
return st, nil
|
||||
}
|
||||
|
||||
repoToURL := map[string]string{}
|
||||
|
||||
for _, r := range st.Repositories {
|
||||
repoToURL[r.Name] = r.URL
|
||||
}
|
||||
|
||||
updated := *st
|
||||
for i, r := range updated.Releases {
|
||||
repo, chart, ok := resolveRemoteChart(r.Chart)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
_, ok = repoToURL[repo]
|
||||
// Skip this chart from dependency management, as there's no matching `repository` in the helmfile state,
|
||||
// which may imply that this is a local chart within a directory, like `charts/myapp`
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
ver, err := resolved.Get(chart, r.Version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updated.Releases[i].Version = ver
|
||||
}
|
||||
|
||||
return &updated, nil
|
||||
}
|
||||
|
||||
func (st *HelmState) updateDependenciesInTempDir(shell helmexec.DependencyUpdater, tempDir func(string, string) (string, error)) (*HelmState, error) {
|
||||
filename, unresolved, err := getUnresolvedDependenciess(st)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(unresolved.deps) == 0 {
|
||||
st.logger.Warnf("There are no repositories defined in your helmfile.yaml.\nThis means helmfile cannot update your dependencies or create a lock file.\nSee https://github.com/roboll/helmfile/issues/878 for more information.")
|
||||
return st, nil
|
||||
}
|
||||
|
||||
d, err := tempDir("", "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(d)
|
||||
|
||||
return updateDependencies(st, shell, unresolved, filename, d)
|
||||
}
|
||||
|
||||
func getUnresolvedDependenciess(st *HelmState) (string, *UnresolvedDependencies, error) {
|
||||
repoToURL := map[string]string{}
|
||||
|
||||
for _, r := range st.Repositories {
|
||||
repoToURL[r.Name] = r.URL
|
||||
}
|
||||
|
||||
unresolved := &UnresolvedDependencies{deps: map[string][]unresolvedChartDependency{}}
|
||||
//if err := unresolved.Add("stable/envoy", "https://kubernetes-charts.storage.googleapis.com", ""); err != nil {
|
||||
// panic(err)
|
||||
//}
|
||||
|
||||
for _, r := range st.Releases {
|
||||
repo, chart, ok := resolveRemoteChart(r.Chart)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
url, ok := repoToURL[repo]
|
||||
// Skip this chart from dependency management, as there's no matching `repository` in the helmfile state,
|
||||
// which may imply that this is a local chart within a directory, like `charts/myapp`
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := unresolved.Add(chart, url, r.Version); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
|
||||
filename := filepath.Base(st.FilePath)
|
||||
filename = strings.TrimSuffix(filename, ".gotmpl")
|
||||
filename = strings.TrimSuffix(filename, ".yaml")
|
||||
filename = strings.TrimSuffix(filename, ".yml")
|
||||
|
||||
return filename, unresolved, nil
|
||||
}
|
||||
|
||||
func updateDependencies(st *HelmState, shell helmexec.DependencyUpdater, unresolved *UnresolvedDependencies, filename, wd string) (*HelmState, error) {
|
||||
depMan := NewChartDependencyManager(filename, st.logger)
|
||||
|
||||
_, err := depMan.Update(shell, wd, unresolved)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to update %d deps: %v", len(unresolved.deps), err)
|
||||
}
|
||||
|
||||
return resolveDependencies(st, depMan, unresolved)
|
||||
}
|
||||
|
||||
type chartDependencyManager struct {
|
||||
Name string
|
||||
|
||||
logger *zap.SugaredLogger
|
||||
|
||||
readFile func(string) ([]byte, error)
|
||||
writeFile func(string, []byte, os.FileMode) error
|
||||
}
|
||||
|
||||
func NewChartDependencyManager(name string, logger *zap.SugaredLogger) *chartDependencyManager {
|
||||
return &chartDependencyManager{
|
||||
Name: name,
|
||||
readFile: ioutil.ReadFile,
|
||||
writeFile: ioutil.WriteFile,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *chartDependencyManager) lockFileName() string {
|
||||
return fmt.Sprintf("%s.lock", m.Name)
|
||||
}
|
||||
|
||||
func (m *chartDependencyManager) Update(shell helmexec.DependencyUpdater, wd string, unresolved *UnresolvedDependencies) (*ResolvedDependencies, error) {
|
||||
if shell.IsHelm3() {
|
||||
return m.updateHelm3(shell, wd, unresolved)
|
||||
}
|
||||
return m.updateHelm2(shell, wd, unresolved)
|
||||
}
|
||||
|
||||
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\napiVersion: v2\n", m.Name)
|
||||
|
||||
// Generate `requirements.yaml` of the temporary local chart from the helmfile state
|
||||
reqsContent, err := yaml.Marshal(unresolved.ToChartRequirements())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.writeBytes(filepath.Join(wd, "Chart.yaml"), []byte(chartMetaContent+string(reqsContent))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.doUpdate("Chart.lock", unresolved, shell, wd)
|
||||
}
|
||||
|
||||
func (m *chartDependencyManager) updateHelm2(shell helmexec.DependencyUpdater, wd string, unresolved *UnresolvedDependencies) (*ResolvedDependencies, error) {
|
||||
// Generate `Chart.yaml` of the temporary local chart
|
||||
if err := m.writeBytes(filepath.Join(wd, "Chart.yaml"), []byte(fmt.Sprintf("name: %s\nversion: 1.0.0\n", m.Name))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate `requirements.yaml` of the temporary local chart from the helmfile state
|
||||
reqsContent, err := yaml.Marshal(unresolved.ToChartRequirements())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := m.writeBytes(filepath.Join(wd, "requirements.yaml"), reqsContent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m.doUpdate("requirements.lock", unresolved, shell, wd)
|
||||
}
|
||||
|
||||
func (m *chartDependencyManager) doUpdate(chartLockFile string, unresolved *UnresolvedDependencies, shell helmexec.DependencyUpdater, wd string) (*ResolvedDependencies, error) {
|
||||
|
||||
// Generate `requirements.lock` of the temporary local chart by coping `<basename>.lock`
|
||||
lockFile := m.lockFileName()
|
||||
|
||||
originalLockFileContent, err := m.readBytes(lockFile)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if shell.IsHelm3() && originalLockFileContent != nil {
|
||||
if err := m.writeBytes(filepath.Join(wd, chartLockFile), originalLockFileContent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Update the lock file by running `helm dependency update`
|
||||
if err := shell.UpdateDeps(wd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
updatedLockFileContent, err := m.readBytes(filepath.Join(wd, chartLockFile))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sort requirements alphabetically by name.
|
||||
lockedReqs := &ChartLockedRequirements{}
|
||||
if err := yaml.Unmarshal(updatedLockFileContent, lockedReqs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Slice(lockedReqs.ResolvedDependencies, func(i, j int) bool {
|
||||
return lockedReqs.ResolvedDependencies[i].ChartName < lockedReqs.ResolvedDependencies[j].ChartName
|
||||
})
|
||||
|
||||
// Don't update lock file if no dependency updated.
|
||||
if !shell.IsHelm3() && originalLockFileContent != nil {
|
||||
originalLockedReqs := &ChartLockedRequirements{}
|
||||
if err := yaml.Unmarshal(originalLockFileContent, originalLockedReqs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
changes, err := diff.Diff(originalLockedReqs.ResolvedDependencies, lockedReqs.ResolvedDependencies)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(changes) == 0 {
|
||||
lockedReqs.Generated = originalLockedReqs.Generated
|
||||
}
|
||||
}
|
||||
|
||||
lockedReqs.Version = version.Version
|
||||
|
||||
updatedLockFileContent, err = yaml.Marshal(lockedReqs)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Commit the lock file if and only if everything looks ok
|
||||
if err := m.writeBytes(lockFile, updatedLockFileContent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resolved, _, err := m.Resolve(unresolved)
|
||||
return resolved, err
|
||||
}
|
||||
|
||||
func (m *chartDependencyManager) Resolve(unresolved *UnresolvedDependencies) (*ResolvedDependencies, bool, error) {
|
||||
updatedLockFileContent, err := m.readBytes(m.lockFileName())
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, false, nil
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// Load resolved dependencies into memory
|
||||
lockedReqs := &ChartLockedRequirements{}
|
||||
if err := yaml.Unmarshal(updatedLockFileContent, lockedReqs); err != nil {
|
||||
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 {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
return resolved, true, nil
|
||||
}
|
||||
|
||||
func (m *chartDependencyManager) readBytes(filename string) ([]byte, error) {
|
||||
bytes, err := m.readFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.logger.Debugf("readBytes: read from %s:\n%s", filename, bytes)
|
||||
return bytes, nil
|
||||
}
|
||||
|
||||
func (m *chartDependencyManager) writeBytes(filename string, data []byte) error {
|
||||
err := m.writeFile(filename, data, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.logger.Debugf("writeBytes: wrote to %s:\n%s", filename, data)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/remote"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"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
|
||||
}
|
||||
|
||||
func (e *StateLoadError) Error() string {
|
||||
return fmt.Sprintf("%s: %v", e.msg, e.Cause)
|
||||
}
|
||||
|
||||
type UndefinedEnvError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e *UndefinedEnvError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
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)
|
||||
DeleteFile func(string) error
|
||||
directoryExistsAt func(string) bool
|
||||
|
||||
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), directoryExistsAt func(string) bool, 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,
|
||||
directoryExistsAt: directoryExistsAt,
|
||||
|
||||
Strict: true,
|
||||
valsRuntime: valsRuntime,
|
||||
getHelm: getHelm,
|
||||
|
||||
overrideHelmBinary: overrideHelmBinary,
|
||||
|
||||
remote: remote,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses YAML into HelmState
|
||||
func (c *StateCreator) Parse(content []byte, baseDir, file string) (*HelmState, error) {
|
||||
var state HelmState
|
||||
|
||||
state.FilePath = file
|
||||
state.basePath = baseDir
|
||||
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(content))
|
||||
if !c.Strict {
|
||||
decoder.SetStrict(false)
|
||||
} else {
|
||||
decoder.SetStrict(true)
|
||||
}
|
||||
i := 0
|
||||
for {
|
||||
i++
|
||||
|
||||
var intermediate HelmState
|
||||
|
||||
err := decoder.Decode(&intermediate)
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, &StateLoadError{fmt.Sprintf("failed to read %s: reading document at index %d", file, i), err}
|
||||
}
|
||||
|
||||
if err := mergo.Merge(&state, &intermediate, mergo.WithAppendSlice); err != nil {
|
||||
return nil, &StateLoadError{fmt.Sprintf("failed to read %s: merging document at index %d", file, i), err}
|
||||
}
|
||||
}
|
||||
|
||||
if len(state.DeprecatedReleases) > 0 {
|
||||
if len(state.Releases) > 0 {
|
||||
return nil, fmt.Errorf("failed to parse %s: you can't specify both `charts` and `releases` sections", file)
|
||||
}
|
||||
state.Releases = state.DeprecatedReleases
|
||||
state.DeprecatedReleases = []ReleaseSpec{}
|
||||
}
|
||||
|
||||
if state.DeprecatedContext != "" && state.HelmDefaults.KubeContext == "" {
|
||||
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.directoryExistsAt = c.directoryExistsAt
|
||||
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, failOnMissingEnv bool) (*HelmState, error) {
|
||||
state := *target
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !evaluateBases {
|
||||
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.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
|
||||
}
|
||||
|
||||
func (c *StateCreator) loadBases(envValues *environment.Environment, st *HelmState, baseDir string) (*HelmState, error) {
|
||||
layers := []*HelmState{}
|
||||
for _, b := range st.Bases {
|
||||
base, err := c.LoadFile(envValues, baseDir, b, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
layers = append(layers, base)
|
||||
}
|
||||
layers = append(layers, st)
|
||||
|
||||
for i := 1; i < len(layers); i++ {
|
||||
if err := mergo.Merge(layers[0], layers[i], mergo.WithAppendSlice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return layers[0], nil
|
||||
}
|
||||
|
||||
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, c.remote)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(envSpec.Secrets) > 0 {
|
||||
|
||||
var envSecretFiles []string
|
||||
for _, urlOrPath := range envSpec.Secrets {
|
||||
resolved, skipped, err := st.storage().resolveFile(envSpec.MissingFileHandler, "environment values", urlOrPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if skipped {
|
||||
continue
|
||||
}
|
||||
|
||||
envSecretFiles = append(envSecretFiles, resolved...)
|
||||
}
|
||||
if err = c.scatterGatherEnvSecretFiles(st, envSecretFiles, envVals, readFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
} else if ctxEnv == nil && name != DefaultEnv && failOnMissingEnv {
|
||||
return nil, &UndefinedEnvError{msg: fmt.Sprintf("environment \"%s\" is not defined", name)}
|
||||
}
|
||||
|
||||
newEnv := &environment.Environment{Name: name, Values: envVals}
|
||||
|
||||
if ctxEnv != nil {
|
||||
intEnv := *ctxEnv
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
newEnv = &intEnv
|
||||
}
|
||||
|
||||
return newEnv, nil
|
||||
}
|
||||
|
||||
func (c *StateCreator) scatterGatherEnvSecretFiles(st *HelmState, envSecretFiles []string, envVals map[string]interface{}, readFile func(string) ([]byte, error)) error {
|
||||
var errs []error
|
||||
|
||||
inputs := envSecretFiles
|
||||
inputsSize := len(inputs)
|
||||
|
||||
type secretResult struct {
|
||||
result map[string]interface{}
|
||||
err error
|
||||
path string
|
||||
}
|
||||
|
||||
secrets := make(chan string, inputsSize)
|
||||
results := make(chan secretResult, inputsSize)
|
||||
|
||||
st.scatterGather(0, inputsSize,
|
||||
func() {
|
||||
for _, secretFile := range envSecretFiles {
|
||||
secrets <- secretFile
|
||||
}
|
||||
close(secrets)
|
||||
},
|
||||
func(id int) {
|
||||
for path := range secrets {
|
||||
release := &ReleaseSpec{}
|
||||
flags := st.appendConnectionFlags([]string{}, release)
|
||||
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}
|
||||
continue
|
||||
}
|
||||
m := map[string]interface{}{}
|
||||
if err := yaml.Unmarshal(bytes, &m); err != nil {
|
||||
results <- secretResult{nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err), path}
|
||||
continue
|
||||
}
|
||||
// All the nested map key should be string. Otherwise we get strange errors due to that
|
||||
// mergo or reflect is unable to merge map[interface{}]interface{} with map[string]interface{} or vice versa.
|
||||
// See https://github.com/roboll/helmfile/issues/677
|
||||
vals, err := maputil.CastKeysToStrings(m)
|
||||
if err != nil {
|
||||
results <- secretResult{nil, fmt.Errorf("failed to load environment secrets file \"%s\": %v", path, err), path}
|
||||
continue
|
||||
}
|
||||
results <- secretResult{vals, nil, path}
|
||||
}
|
||||
},
|
||||
func() {
|
||||
for i := 0; i < inputsSize; i++ {
|
||||
result := <-results
|
||||
if result.err != nil {
|
||||
errs = append(errs, result.err)
|
||||
} else {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
close(results)
|
||||
},
|
||||
)
|
||||
|
||||
if len(errs) > 1 {
|
||||
for _, err := range errs {
|
||||
st.logger.Error(err)
|
||||
}
|
||||
return fmt.Errorf("Failed loading environment secrets with %d errors", len(errs))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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, remote)
|
||||
var err error
|
||||
envVals, err = ld.LoadEnvironmentValues(missingFileHandler, valuesEntries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return envVals, nil
|
||||
}
|
||||
@@ -1,25 +1,31 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go.uber.org/zap"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
. "gotest.tools/assert"
|
||||
"gotest.tools/assert/cmp"
|
||||
"github.com/roboll/helmfile/pkg/remote"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/testhelper"
|
||||
"go.uber.org/zap"
|
||||
|
||||
. "gotest.tools/v3/assert"
|
||||
"gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func createFromYaml(content []byte, file string, env string, logger *zap.SugaredLogger) (*HelmState, error) {
|
||||
c := &creator{
|
||||
logger,
|
||||
ioutil.ReadFile,
|
||||
filepath.Abs,
|
||||
true,
|
||||
c := &StateCreator{
|
||||
logger: logger,
|
||||
readFile: ioutil.ReadFile,
|
||||
abs: filepath.Abs,
|
||||
|
||||
DeleteFile: os.Remove,
|
||||
Strict: true,
|
||||
}
|
||||
return c.ParseAndLoadEnv(content, filepath.Dir(file), file, env)
|
||||
return c.ParseAndLoad(content, filepath.Dir(file), file, env, true, nil)
|
||||
}
|
||||
|
||||
func TestReadFromYaml(t *testing.T) {
|
||||
@@ -45,7 +51,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
|
||||
@@ -58,6 +64,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, testFs.DirectoryExistsAt, 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:
|
||||
@@ -95,27 +130,29 @@ 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
|
||||
`
|
||||
|
||||
readFile := func(filename string) ([]byte, error) {
|
||||
switch filename {
|
||||
case fooYamlFile:
|
||||
return fooYamlContent, nil
|
||||
case barYamlFile:
|
||||
return barYamlContent, nil
|
||||
case barTextFile:
|
||||
return barTextContent, nil
|
||||
case valuesFile:
|
||||
return valuesContent, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected filename: %s", filename)
|
||||
}
|
||||
testFs := testhelper.NewTestFs(map[string]string{
|
||||
fooYamlFile: string(fooYamlContent),
|
||||
barYamlFile: string(barYamlContent),
|
||||
barTextFile: string(barTextContent),
|
||||
valuesFile: string(valuesContent),
|
||||
})
|
||||
testFs.Cwd = "/example/path/to"
|
||||
|
||||
state, err := NewCreator(logger, readFile, filepath.Abs).ParseAndLoadEnv(yamlContent, filepath.Dir(yamlFile), yamlFile, "production")
|
||||
r := remote.NewRemote(logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
|
||||
state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob, testFs.DirectoryExistsAt, nil, nil, "", r).
|
||||
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, nil)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
actual := state.Env.Values
|
||||
@@ -123,7 +160,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, testFs.DirectoryExistsAt, 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)
|
||||
}
|
||||
@@ -249,6 +377,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 {
|
||||
@@ -0,0 +1,15 @@
|
||||
package state
|
||||
|
||||
type EnvironmentSpec struct {
|
||||
Values []interface{} `yaml:"values,omitempty"`
|
||||
Secrets []string `yaml:"secrets,omitempty"`
|
||||
|
||||
// MissingFileHandler instructs helmfile to fail when unable to find a environment values file listed
|
||||
// under `environments.NAME.values`.
|
||||
//
|
||||
// Possible values are "Error", "Warn", "Info", "Debug". The default is "Error".
|
||||
//
|
||||
// Use "Warn", "Info", or "Debug" if you want helmfile to not fail when a values file is missing, while just leaving
|
||||
// a message about the missing file at the log-level.
|
||||
MissingFileHandler *string `yaml:"missingFileHandler,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type EnvironmentValuesLoader struct {
|
||||
storage *Storage
|
||||
|
||||
readFile func(string) ([]byte, error)
|
||||
|
||||
logger *zap.SugaredLogger
|
||||
|
||||
remote *remote.Remote
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func (ld *EnvironmentValuesLoader) LoadEnvironmentValues(missingFileHandler *string, valuesEntries []interface{}) (map[string]interface{}, error) {
|
||||
result := map[string]interface{}{}
|
||||
|
||||
for _, entry := range valuesEntries {
|
||||
maps := []interface{}{}
|
||||
|
||||
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
|
||||
}
|
||||
if skipped {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
tmplData := EnvironmentTemplateData{environment.EmptyEnvironment, "", map[string]interface{}{}}
|
||||
r := tmpl.NewFileRenderer(ld.readFile, filepath.Dir(f), tmplData)
|
||||
bytes, err := r.RenderToBytes(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v", f, err)
|
||||
}
|
||||
m := map[string]interface{}{}
|
||||
if err := yaml.Unmarshal(bytes, &m); err != nil {
|
||||
return nil, fmt.Errorf("failed to load environment values file \"%s\": %v\n\nOffending YAML:\n%s", f, err, bytes)
|
||||
}
|
||||
maps = append(maps, m)
|
||||
if ld.logger != nil {
|
||||
ld.logger.Debugf("envvals_loader: loaded %s:%v", strOrMap, m)
|
||||
}
|
||||
}
|
||||
case map[interface{}]interface{}:
|
||||
maps = append(maps, strOrMap)
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected type of value: value=%v, type=%T", strOrMap, strOrMap)
|
||||
}
|
||||
for _, m := range maps {
|
||||
// All the nested map key should be string. Otherwise we get strange errors due to that
|
||||
// mergo or reflect is unable to merge map[interface{}]interface{} with map[string]interface{} or vice versa.
|
||||
// See https://github.com/roboll/helmfile/issues/677
|
||||
vals, err := maputil.CastKeysToStrings(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mergo.Merge(&result, &vals, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
|
||||
return nil, fmt.Errorf("failed to merge %v: %v", m, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
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"`
|
||||
Alias string `yaml:"alias"`
|
||||
}
|
||||
|
||||
func (st *HelmState) appendHelmXFlags(flags []string, release *ReleaseSpec) ([]string, error) {
|
||||
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.generateTemporaryReleaseValuesFiles(release, jsonPatches, release.MissingFileHandler)
|
||||
if err != nil {
|
||||
return nil, clean, err
|
||||
}
|
||||
|
||||
filesNeedCleaning = append(filesNeedCleaning, generatedFiles...)
|
||||
|
||||
for _, f := range generatedFiles {
|
||||
chartify.Opts.JsonPatches = append(chartify.Opts.JsonPatches, f)
|
||||
}
|
||||
|
||||
shouldRun = true
|
||||
}
|
||||
|
||||
strategicMergePatches := release.StrategicMergePatches
|
||||
if len(strategicMergePatches) > 0 {
|
||||
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, strategicMergePatches, release.MissingFileHandler)
|
||||
if err != nil {
|
||||
return nil, clean, err
|
||||
}
|
||||
|
||||
for _, f := range generatedFiles {
|
||||
chartify.Opts.StrategicMergePatches = append(chartify.Opts.StrategicMergePatches, f)
|
||||
}
|
||||
|
||||
filesNeedCleaning = append(filesNeedCleaning, generatedFiles...)
|
||||
|
||||
shouldRun = true
|
||||
}
|
||||
|
||||
transformers := release.Transformers
|
||||
if len(transformers) > 0 {
|
||||
generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, transformers, release.MissingFileHandler)
|
||||
if err != nil {
|
||||
return nil, clean, err
|
||||
}
|
||||
|
||||
for _, f := range generatedFiles {
|
||||
chartify.Opts.Transformers = append(chartify.Opts.Transformers, f)
|
||||
}
|
||||
|
||||
filesNeedCleaning = append(filesNeedCleaning, generatedFiles...)
|
||||
|
||||
shouldRun = true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/tmpl"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func (r ReleaseSpec) ExecuteTemplateExpressions(renderer *tmpl.FileRenderer) (*ReleaseSpec, error) {
|
||||
var result *ReleaseSpec
|
||||
var err error
|
||||
|
||||
result, err = r.Clone()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\": %v", r.Name, err)
|
||||
}
|
||||
|
||||
{
|
||||
ts := result.Name
|
||||
result.Name, err = renderer.RenderTemplateContentToString([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".name = \"%s\": %v", r.Name, ts, err)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
ts := result.Chart
|
||||
result.Chart, err = renderer.RenderTemplateContentToString([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".chart = \"%s\": %v", r.Name, ts, err)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
ts := result.Namespace
|
||||
result.Namespace, err = renderer.RenderTemplateContentToString([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".namespace = \"%s\": %v", r.Name, ts, err)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
ts := result.Version
|
||||
result.Version, err = renderer.RenderTemplateContentToString([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
|
||||
}
|
||||
}
|
||||
|
||||
if result.WaitTemplate != nil {
|
||||
ts := *result.WaitTemplate
|
||||
resultTmpl, err := renderer.RenderTemplateContentToString([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
|
||||
}
|
||||
result.WaitTemplate = &resultTmpl
|
||||
}
|
||||
|
||||
if result.InstalledTemplate != nil {
|
||||
ts := *result.InstalledTemplate
|
||||
resultTmpl, err := renderer.RenderTemplateContentToString([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
|
||||
}
|
||||
result.InstalledTemplate = &resultTmpl
|
||||
}
|
||||
|
||||
if result.TillerlessTemplate != nil {
|
||||
ts := *result.TillerlessTemplate
|
||||
resultTmpl, err := renderer.RenderTemplateContentToString([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
|
||||
}
|
||||
result.TillerlessTemplate = &resultTmpl
|
||||
}
|
||||
|
||||
if result.VerifyTemplate != nil {
|
||||
ts := *result.VerifyTemplate
|
||||
resultTmpl, err := renderer.RenderTemplateContentToString([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".version = \"%s\": %v", r.Name, ts, err)
|
||||
}
|
||||
result.VerifyTemplate = &resultTmpl
|
||||
}
|
||||
|
||||
for key, val := range result.Labels {
|
||||
ts := val
|
||||
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".labels[%s] = \"%s\": %v", r.Name, key, ts, err)
|
||||
}
|
||||
result.Labels[key] = s.String()
|
||||
}
|
||||
|
||||
if len(result.ValuesTemplate) > 0 {
|
||||
for i, t := range result.ValuesTemplate {
|
||||
switch ts := t.(type) {
|
||||
case map[interface{}]interface{}:
|
||||
serialized, err := yaml.Marshal(ts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".values[%d] = \"%v\": %v", r.Name, i, ts, err)
|
||||
}
|
||||
|
||||
s, err := renderer.RenderTemplateContentToBuffer([]byte(serialized))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".values[%d] = \"%v\": %v", r.Name, i, serialized, err)
|
||||
}
|
||||
|
||||
var deserialized map[interface{}]interface{}
|
||||
|
||||
if err := yaml.Unmarshal(s.Bytes(), &deserialized); err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".values[%d] = \"%v\": %v", r.Name, i, ts, err)
|
||||
}
|
||||
|
||||
result.ValuesTemplate[i] = deserialized
|
||||
}
|
||||
}
|
||||
|
||||
var newvals []interface{}
|
||||
newvals = append(newvals, result.ValuesTemplate...)
|
||||
result.Values = append(newvals, result.Values...)
|
||||
}
|
||||
|
||||
for i, t := range result.Values {
|
||||
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\".values[%d] = \"%s\": %v", r.Name, i, ts, err)
|
||||
}
|
||||
result.Values[i] = s.String()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
if len(result.SetValuesTemplate) > 0 {
|
||||
for i, val := range result.SetValuesTemplate {
|
||||
{
|
||||
// name
|
||||
ts := val.Name
|
||||
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".set[%d].name = \"%s\": %v", r.Name, i, ts, err)
|
||||
}
|
||||
result.SetValuesTemplate[i].Name = s.String()
|
||||
}
|
||||
{
|
||||
// value
|
||||
ts := val.Value
|
||||
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".set[%d].value = \"%s\": %v", r.Name, i, ts, err)
|
||||
}
|
||||
result.SetValuesTemplate[i].Value = s.String()
|
||||
}
|
||||
{
|
||||
// file
|
||||
ts := val.File
|
||||
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".set[%d].file = \"%s\": %v", r.Name, i, ts, err)
|
||||
}
|
||||
result.SetValuesTemplate[i].File = s.String()
|
||||
}
|
||||
for j, ts := range val.Values {
|
||||
// values
|
||||
s, err := renderer.RenderTemplateContentToBuffer([]byte(ts))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing template expressions in release \"%s\".set[%d].values[%d] = \"%s\": %v", r.Name, i, j, ts, err)
|
||||
}
|
||||
result.SetValuesTemplate[i].Values[j] = s.String()
|
||||
}
|
||||
}
|
||||
|
||||
var newvals []SetValue
|
||||
newvals = append(newvals, result.SetValuesTemplate...)
|
||||
result.SetValues = append(newvals, result.SetValues...)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r ReleaseSpec) Clone() (*ReleaseSpec, error) {
|
||||
serialized, err := yaml.Marshal(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed cloning release \"%s\": %v", r.Name, err)
|
||||
}
|
||||
|
||||
var deserialized ReleaseSpec
|
||||
|
||||
if err := yaml.Unmarshal(serialized, &deserialized); err != nil {
|
||||
return nil, fmt.Errorf("failed cloning release \"%s\": %v", r.Name, err)
|
||||
}
|
||||
|
||||
return &deserialized, nil
|
||||
}
|
||||
|
||||
func (r ReleaseSpec) Desired() bool {
|
||||
return r.Installed == nil || *r.Installed
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const ReleaseErrorCodeFailure = 1
|
||||
|
||||
type ReleaseError struct {
|
||||
*ReleaseSpec
|
||||
err error
|
||||
Code int
|
||||
}
|
||||
|
||||
func (e *ReleaseError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+2832
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,119 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/tmpl"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func (st *HelmState) Values() map[string]interface{} {
|
||||
if st.RenderedValues == nil {
|
||||
panic("[bug] RenderedValues is nil")
|
||||
}
|
||||
|
||||
return st.RenderedValues
|
||||
}
|
||||
|
||||
func (st *HelmState) createReleaseTemplateData(release *ReleaseSpec, vals map[string]interface{}) releaseTemplateData {
|
||||
return releaseTemplateData{
|
||||
Environment: st.Env,
|
||||
Namespace: st.OverrideNamespace,
|
||||
Values: vals,
|
||||
Release: releaseTemplateDataRelease{
|
||||
Name: release.Name,
|
||||
Chart: release.Chart,
|
||||
Namespace: release.Namespace,
|
||||
Labels: release.Labels,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getBoolRefFromStringTemplate(templateRef string) (*bool, error) {
|
||||
var result bool
|
||||
if err := yaml.Unmarshal([]byte(templateRef), &result); err != nil {
|
||||
return nil, fmt.Errorf("failed deserialising string %s: %v", templateRef, err)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func updateBoolTemplatedValues(r *ReleaseSpec) error {
|
||||
|
||||
if r.InstalledTemplate != nil {
|
||||
if installed, err := getBoolRefFromStringTemplate(*r.InstalledTemplate); err != nil {
|
||||
return fmt.Errorf("installedTemplate: %v", err)
|
||||
} else {
|
||||
r.InstalledTemplate = nil
|
||||
r.Installed = installed
|
||||
}
|
||||
}
|
||||
|
||||
if r.WaitTemplate != nil {
|
||||
if wait, err := getBoolRefFromStringTemplate(*r.WaitTemplate); err != nil {
|
||||
return fmt.Errorf("waitTemplate: %v", err)
|
||||
} else {
|
||||
r.WaitTemplate = nil
|
||||
r.Wait = wait
|
||||
}
|
||||
}
|
||||
|
||||
if r.TillerlessTemplate != nil {
|
||||
if tillerless, err := getBoolRefFromStringTemplate(*r.TillerlessTemplate); err != nil {
|
||||
return fmt.Errorf("tillerlessTemplate: %v", err)
|
||||
} else {
|
||||
r.TillerlessTemplate = nil
|
||||
r.Tillerless = tillerless
|
||||
}
|
||||
}
|
||||
|
||||
if r.VerifyTemplate != nil {
|
||||
if verify, err := getBoolRefFromStringTemplate(*r.VerifyTemplate); err != nil {
|
||||
return fmt.Errorf("verifyTemplate: %v", err)
|
||||
} else {
|
||||
r.VerifyTemplate = nil
|
||||
r.Verify = verify
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
|
||||
r := *st
|
||||
|
||||
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 := st.createReleaseTemplateData(prev, vals)
|
||||
renderer := tmpl.NewFileRenderer(st.readFile, st.basePath, tmplData)
|
||||
r, err := rt.ExecuteTemplateExpressions(renderer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed executing templates in release \"%s\".\"%s\": %v", st.FilePath, rt.Name, err)
|
||||
}
|
||||
if reflect.DeepEqual(prev, r) {
|
||||
successFlag = true
|
||||
if err := updateBoolTemplatedValues(r); err != nil {
|
||||
return nil, fmt.Errorf("failed executing templates in release \"%s\".\"%s\": %v", st.FilePath, rt.Name, err)
|
||||
}
|
||||
st.Releases[i] = *r
|
||||
break
|
||||
}
|
||||
prev = r
|
||||
}
|
||||
if !successFlag {
|
||||
return nil, fmt.Errorf("failed executing templates in release \"%s\".\"%s\": %s", st.FilePath, rt.Name,
|
||||
"recursive references can't be resolved")
|
||||
}
|
||||
}
|
||||
|
||||
return &r, nil
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
)
|
||||
|
||||
func boolPtrToString(ptr *bool) string {
|
||||
if ptr == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprintf("&%t", *ptr)
|
||||
}
|
||||
|
||||
func ptr(v interface{}) interface{} {
|
||||
r := v
|
||||
return reflect.ValueOf(r).Addr().Interface()
|
||||
}
|
||||
|
||||
func TestHelmState_executeTemplates(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input ReleaseSpec
|
||||
want ReleaseSpec
|
||||
}{
|
||||
{
|
||||
name: "Has template expressions in chart, values, secrets, version, labels",
|
||||
input: ReleaseSpec{
|
||||
Chart: "test-charts/{{ .Release.Name }}",
|
||||
Version: "{{ .Release.Name }}-0.1",
|
||||
Name: "test-app",
|
||||
Namespace: "test-namespace-{{ .Release.Name }}",
|
||||
ValuesTemplate: []interface{}{"config/{{ .Environment.Name }}/{{ .Release.Name }}/values.yaml"},
|
||||
Secrets: []interface{}{"config/{{ .Environment.Name }}/{{ .Release.Name }}/secrets.yaml"},
|
||||
Labels: map[string]string{"id": "{{ .Release.Name }}"},
|
||||
},
|
||||
want: ReleaseSpec{
|
||||
Chart: "test-charts/test-app",
|
||||
Version: "test-app-0.1",
|
||||
Name: "test-app",
|
||||
Namespace: "test-namespace-test-app",
|
||||
Values: []interface{}{"config/test_env/test-app/values.yaml"},
|
||||
Secrets: []interface{}{"config/test_env/test-app/secrets.yaml"},
|
||||
Labels: map[string]string{"id": "test-app"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Has template expressions in name with recursive refs",
|
||||
input: ReleaseSpec{
|
||||
Chart: "test-chart",
|
||||
Name: "{{ .Release.Labels.id }}-{{ .Release.Namespace }}",
|
||||
Namespace: "dev",
|
||||
Labels: map[string]string{"id": "{{ .Release.Chart }}"},
|
||||
},
|
||||
want: ReleaseSpec{
|
||||
Chart: "test-chart",
|
||||
Name: "test-chart-dev",
|
||||
Namespace: "dev",
|
||||
Labels: map[string]string{"id": "test-chart"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Has template expressions in boolean values",
|
||||
input: ReleaseSpec{
|
||||
Chart: "test-chart",
|
||||
Name: "app-dev",
|
||||
Namespace: "dev",
|
||||
Labels: map[string]string{"id": "app"},
|
||||
InstalledTemplate: func(i string) *string { return &i }(`{{ eq .Release.Labels.id "app" | ternary "yes" "no" }}`),
|
||||
VerifyTemplate: func(i string) *string { return &i }(`{{ true }}`),
|
||||
Verify: func(i bool) *bool { return &i }(false),
|
||||
WaitTemplate: func(i string) *string { return &i }(`{{ false }}`),
|
||||
TillerlessTemplate: func(i string) *string { return &i }(`yes`),
|
||||
},
|
||||
want: ReleaseSpec{
|
||||
Chart: "test-chart",
|
||||
Name: "app-dev",
|
||||
Namespace: "dev",
|
||||
Labels: map[string]string{"id": "app"},
|
||||
Installed: func(i bool) *bool { return &i }(true),
|
||||
Verify: func(i bool) *bool { return &i }(true),
|
||||
Wait: func(i bool) *bool { return &i }(false),
|
||||
Tillerless: func(i bool) *bool { return &i }(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Has template in set-values",
|
||||
input: ReleaseSpec{
|
||||
Chart: "test-charts/chart",
|
||||
Name: "test-app",
|
||||
Namespace: "dev",
|
||||
SetValuesTemplate: []SetValue{
|
||||
SetValue{Name: "val1", Value: "{{ .Release.Name }}-val1"},
|
||||
SetValue{Name: "val2", File: "{{ .Release.Name }}.yml"},
|
||||
SetValue{Name: "val3", Values: []string{"{{ .Release.Name }}-val2", "{{ .Release.Name }}-val3"}},
|
||||
},
|
||||
},
|
||||
want: ReleaseSpec{
|
||||
Chart: "test-charts/chart",
|
||||
Name: "test-app",
|
||||
Namespace: "dev",
|
||||
SetValues: []SetValue{
|
||||
SetValue{Name: "val1", Value: "test-app-val1"},
|
||||
SetValue{Name: "val2", File: "test-app.yml"},
|
||||
SetValue{Name: "val3", Values: []string{"test-app-val2", "test-app-val3"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Has template in values (map)",
|
||||
input: ReleaseSpec{
|
||||
Chart: "test-charts/chart",
|
||||
Verify: nil,
|
||||
Name: "app",
|
||||
Namespace: "dev",
|
||||
ValuesTemplate: []interface{}{map[string]string{"key": "{{ .Release.Name }}-val0"}},
|
||||
},
|
||||
want: ReleaseSpec{
|
||||
Chart: "test-charts/chart",
|
||||
Verify: nil,
|
||||
Name: "app",
|
||||
Namespace: "dev",
|
||||
Values: []interface{}{map[interface{}]interface{}{"key": "app-val0"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
state := &HelmState{
|
||||
basePath: ".",
|
||||
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()
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
actual := r.Releases[0]
|
||||
|
||||
if !reflect.DeepEqual(actual.Name, tt.want.Name) {
|
||||
t.Errorf("expected Name %+v, got %+v", tt.want.Name, actual.Name)
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Chart, tt.want.Chart) {
|
||||
t.Errorf("expected Chart %+v, got %+v", tt.want.Chart, actual.Chart)
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Namespace, tt.want.Namespace) {
|
||||
t.Errorf("expected Namespace %+v, got %+v", tt.want.Namespace, actual.Namespace)
|
||||
}
|
||||
if diff := deep.Equal(actual.Values, tt.want.Values); diff != nil && len(actual.Values) > 0 {
|
||||
t.Errorf("Values differs \n%+v", strings.Join(diff, "\n"))
|
||||
}
|
||||
if diff := deep.Equal(actual.Secrets, tt.want.Secrets); diff != nil && len(actual.Secrets) > 0 {
|
||||
t.Errorf("Secrets differs \n%+v", strings.Join(diff, "\n"))
|
||||
}
|
||||
if diff := deep.Equal(actual.SetValues, tt.want.SetValues); diff != nil && len(actual.SetValues) > 0 {
|
||||
t.Errorf("SetValues differs \n%+v", strings.Join(diff, "\n"))
|
||||
}
|
||||
if diff := deep.Equal(actual.Labels, tt.want.Labels); diff != nil && len(actual.Labels) > 0 {
|
||||
t.Errorf("Labels differs \n%+v", strings.Join(diff, "\n"))
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Version, tt.want.Version) {
|
||||
t.Errorf("expected Version %+v, got %+v", tt.want.Version, actual.Version)
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Installed, tt.want.Installed) {
|
||||
t.Errorf("expected actual.Installed %+v, got %+v",
|
||||
boolPtrToString(tt.want.Installed), boolPtrToString(actual.Installed),
|
||||
)
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Tillerless, tt.want.Tillerless) {
|
||||
t.Errorf("expected actual.Tillerless %+v, got %+v",
|
||||
boolPtrToString(tt.want.Tillerless), boolPtrToString(actual.Tillerless),
|
||||
)
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Verify, tt.want.Verify) {
|
||||
t.Errorf("expected actual.Verify %+v, got %+v",
|
||||
boolPtrToString(tt.want.Verify), boolPtrToString(actual.Verify),
|
||||
)
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Wait, tt.want.Wait) {
|
||||
t.Errorf("expected actual.Wait %+v, got %+v",
|
||||
boolPtrToString(tt.want.Wait), boolPtrToString(actual.Wait),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelmState_recursiveRefsTemplates(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input ReleaseSpec
|
||||
}{
|
||||
{
|
||||
name: "Has reqursive references",
|
||||
input: ReleaseSpec{
|
||||
Chart: "test-charts/{{ .Release.Name }}",
|
||||
Verify: nil,
|
||||
Name: "{{ .Release.Labels.id }}",
|
||||
Namespace: "dev",
|
||||
Labels: map[string]string{"id": "app-{{ .Release.Name }}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Has unresolvable boolean templates",
|
||||
input: ReleaseSpec{
|
||||
Name: "app-dev",
|
||||
Chart: "test-charts/app",
|
||||
Verify: nil,
|
||||
Namespace: "dev",
|
||||
WaitTemplate: func(i string) *string { return &i }("hi"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
state := &HelmState{
|
||||
basePath: ".",
|
||||
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()
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, got valid response: %v", r)
|
||||
t.FailNow()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/variantdev/dag/pkg/dag"
|
||||
)
|
||||
|
||||
type result struct {
|
||||
release ReleaseSpec
|
||||
err error
|
||||
}
|
||||
|
||||
func (st *HelmState) scatterGather(concurrency int, items int, produceInputs func(), receiveInputsAndProduceIntermediates func(int), aggregateIntermediates func()) {
|
||||
|
||||
if concurrency < 1 || concurrency > items {
|
||||
concurrency = items
|
||||
}
|
||||
|
||||
for _, r := range st.Releases {
|
||||
if r.Tillerless != nil {
|
||||
if *r.Tillerless {
|
||||
concurrency = 1
|
||||
}
|
||||
} else if st.HelmDefaults.Tillerless {
|
||||
concurrency = 1
|
||||
}
|
||||
}
|
||||
|
||||
// WaitGroup is required to wait until goroutine per job in job queue cleanly stops.
|
||||
var waitGroup sync.WaitGroup
|
||||
waitGroup.Add(concurrency)
|
||||
|
||||
go produceInputs()
|
||||
|
||||
for w := 1; w <= concurrency; w++ {
|
||||
go func(id int) {
|
||||
receiveInputsAndProduceIntermediates(id)
|
||||
waitGroup.Done()
|
||||
}(w)
|
||||
}
|
||||
|
||||
aggregateIntermediates()
|
||||
|
||||
// Wait until all the goroutines to gracefully finish
|
||||
waitGroup.Wait()
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
inputsSize := len(inputs)
|
||||
|
||||
releases := make(chan ReleaseSpec)
|
||||
results := make(chan result)
|
||||
|
||||
st.scatterGather(
|
||||
concurrency,
|
||||
inputsSize,
|
||||
func() {
|
||||
for _, release := range inputs {
|
||||
releases <- release
|
||||
}
|
||||
close(releases)
|
||||
},
|
||||
func(id int) {
|
||||
for release := range releases {
|
||||
err := do(release, id)
|
||||
st.logger.Debugf("release %q processed", release.Name)
|
||||
results <- result{release: release, err: err}
|
||||
}
|
||||
},
|
||||
func() {
|
||||
for range inputs {
|
||||
r := <-results
|
||||
if r.err != nil {
|
||||
errs = append(errs, fmt.Errorf("release \"%s\" failed: %v", r.release.Name, r.err))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,99 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go.uber.org/zap"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
logger *zap.SugaredLogger
|
||||
|
||||
FilePath string
|
||||
|
||||
basePath string
|
||||
glob func(string) ([]string, error)
|
||||
}
|
||||
|
||||
func NewStorage(forFile string, logger *zap.SugaredLogger, glob func(string) ([]string, error)) *Storage {
|
||||
return &Storage{
|
||||
FilePath: forFile,
|
||||
basePath: filepath.Dir(forFile),
|
||||
logger: logger,
|
||||
glob: glob,
|
||||
}
|
||||
}
|
||||
|
||||
func (st *Storage) resolveFile(missingFileHandler *string, tpe, path string) ([]string, bool, error) {
|
||||
title := fmt.Sprintf("%s file", tpe)
|
||||
|
||||
files, err := st.ExpandPaths(path)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
var handlerId string
|
||||
|
||||
if missingFileHandler != nil {
|
||||
handlerId = *missingFileHandler
|
||||
} else {
|
||||
handlerId = MissingFileHandlerError
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
switch handlerId {
|
||||
case MissingFileHandlerError:
|
||||
return nil, false, fmt.Errorf("%s matching \"%s\" does not exist in \"%s\"", title, path, st.basePath)
|
||||
case MissingFileHandlerWarn:
|
||||
st.logger.Warnf("skipping missing %s matching \"%s\"", title, path)
|
||||
return nil, true, nil
|
||||
case MissingFileHandlerInfo:
|
||||
st.logger.Infof("skipping missing %s matching \"%s\"", title, path)
|
||||
return nil, true, nil
|
||||
case MissingFileHandlerDebug:
|
||||
st.logger.Debugf("skipping missing %s matching \"%s\"", title, path)
|
||||
return nil, true, nil
|
||||
default:
|
||||
available := []string{
|
||||
MissingFileHandlerError,
|
||||
MissingFileHandlerWarn,
|
||||
MissingFileHandlerInfo,
|
||||
MissingFileHandlerDebug,
|
||||
}
|
||||
return nil, false, fmt.Errorf("invalid missing file handler \"%s\" while processing \"%s\" in \"%s\": it must be one of %s", handlerId, path, st.FilePath, available)
|
||||
}
|
||||
}
|
||||
|
||||
return files, false, nil
|
||||
}
|
||||
|
||||
func (st *Storage) ExpandPaths(globPattern string) ([]string, error) {
|
||||
result := []string{}
|
||||
absPathPattern := st.normalizePath(globPattern)
|
||||
matches, err := st.glob(absPathPattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed processing %s: %v", globPattern, err)
|
||||
}
|
||||
|
||||
sort.Strings(matches)
|
||||
|
||||
result = append(result, matches...)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// normalizes relative path to absolute one
|
||||
func (st *Storage) normalizePath(path string) string {
|
||||
u, _ := url.Parse(path)
|
||||
if u.Scheme != "" || filepath.IsAbs(path) {
|
||||
return path
|
||||
} else {
|
||||
return st.JoinBase(path)
|
||||
}
|
||||
}
|
||||
|
||||
// JoinBase returns an absolute path in the form basePath/relative
|
||||
func (st *Storage) JoinBase(relPath string) string {
|
||||
return filepath.Join(st.basePath, relPath)
|
||||
}
|
||||
+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: {}
|
||||
@@ -0,0 +1,50 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
)
|
||||
|
||||
// TemplateSpec defines the structure of a reusable and composable template for helm releases.
|
||||
type TemplateSpec struct {
|
||||
ReleaseSpec `yaml:",inline"`
|
||||
}
|
||||
|
||||
// EnvironmentTemplateData provides variables accessible while executing golang text/template expressions in helmfile and values YAML files
|
||||
type EnvironmentTemplateData struct {
|
||||
// Environment is accessible as `.Environment` from any template executed by the renderer
|
||||
Environment environment.Environment
|
||||
// Namespace is accessible as `.Namespace` from any non-values template executed by the renderer
|
||||
Namespace string
|
||||
// Values is accessible as `.Values` and it contains default state values overrode by environment values and override values.
|
||||
Values map[string]interface{}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func isLocalChart(chart string) bool {
|
||||
regex, _ := regexp.Compile("^[.]?./")
|
||||
matched := regex.MatchString(chart)
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
|
||||
uriLike := strings.Index(chart, "://") > -1
|
||||
if uriLike {
|
||||
return false
|
||||
}
|
||||
|
||||
return chart == "" ||
|
||||
chart[0] == '/' ||
|
||||
strings.Index(chart, "/") == -1 ||
|
||||
(len(strings.Split(chart, "/")) != 2 &&
|
||||
len(strings.Split(chart, "/")) != 3)
|
||||
}
|
||||
|
||||
func resolveRemoteChart(repoAndChart string) (string, string, bool) {
|
||||
if isLocalChart(repoAndChart) {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
uriLike := strings.Index(repoAndChart, "://") > -1
|
||||
if uriLike {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
parts := strings.SplitN(repoAndChart, "/", 2)
|
||||
if len(parts) < 2 {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
repo := parts[0]
|
||||
chart := parts[1]
|
||||
|
||||
return repo, chart, true
|
||||
}
|
||||
|
||||
// normalizeChart allows for the distinction between a file path reference and repository references.
|
||||
// - Any single (or double character) followed by a `/` will be considered a local file reference and
|
||||
// be constructed relative to the `base path`.
|
||||
// - Everything else is assumed to be an absolute path or an actual <repository>/<chart> reference.
|
||||
func normalizeChart(basePath, chart string) string {
|
||||
if !isLocalChart(chart) || chart[0] == '/' {
|
||||
return chart
|
||||
}
|
||||
return filepath.Join(basePath, chart)
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package state
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIsLocalChart(t *testing.T) {
|
||||
testcases := []struct {
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
input: "mychart",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
input: "stable/mysql",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: "center/stable/mysql",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: "./charts/myapp",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
input: "center/stable/myapp",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: "./charts/mysubsystem/myapp",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
input: "/charts/mysubsystem/myapp",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
// Regression test case for:
|
||||
// * https://github.com/roboll/helmfile/issues/675
|
||||
// * https://github.com/roboll/helmfile/issues/687
|
||||
input: "https://github.com/arangodb/kube-arangodb/releases/download/0.3.11/kube-arangodb-crd.tgz",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: "https://example.com/bar.tgz",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range testcases {
|
||||
testcase := testcases[i]
|
||||
|
||||
actual := isLocalChart(testcase.input)
|
||||
|
||||
if testcase.expected != actual {
|
||||
t.Errorf("unexpected result: isLocalChart(\"%s\"): expected=%v, got=%v", testcase.input, testcase.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveRemortChart(t *testing.T) {
|
||||
testcases := []struct {
|
||||
input string
|
||||
repo string
|
||||
chart string
|
||||
remote bool
|
||||
}{
|
||||
{
|
||||
input: "mychart",
|
||||
remote: false,
|
||||
},
|
||||
{
|
||||
input: "stable/mysql",
|
||||
repo: "stable",
|
||||
chart: "mysql",
|
||||
remote: true,
|
||||
},
|
||||
{
|
||||
input: "./charts/myapp",
|
||||
remote: false,
|
||||
},
|
||||
{
|
||||
input: "center/stable/myapp",
|
||||
repo: "center",
|
||||
chart: "stable/myapp",
|
||||
remote: true,
|
||||
},
|
||||
{
|
||||
input: "./charts/mysubsystem/myapp",
|
||||
remote: false,
|
||||
},
|
||||
{
|
||||
input: "/charts/mysubsystem/myapp",
|
||||
remote: false,
|
||||
},
|
||||
{
|
||||
// Regression test case for:
|
||||
// * https://github.com/roboll/helmfile/issues/675
|
||||
// * https://github.com/roboll/helmfile/issues/687
|
||||
input: "https://github.com/arangodb/kube-arangodb/releases/download/0.3.11/kube-arangodb-crd.tgz",
|
||||
remote: false,
|
||||
},
|
||||
{
|
||||
input: "https://example.com/bar.tgz",
|
||||
remote: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i := range testcases {
|
||||
testcase := testcases[i]
|
||||
|
||||
repo, chart, actual := resolveRemoteChart(testcase.input)
|
||||
|
||||
if testcase.remote != actual {
|
||||
t.Fatalf("unexpected result: reolveRemoteChart(\"%s\"): expected=%v, got=%v", testcase.input, testcase.remote, actual)
|
||||
}
|
||||
|
||||
if testcase.repo != repo {
|
||||
t.Errorf("unexpected repo: %s: expected=%v, got=%v", testcase.input, testcase.repo, repo)
|
||||
}
|
||||
|
||||
if testcase.chart != chart {
|
||||
t.Errorf("unexpected chart: %s: expected=%v, got=%v", testcase.input, testcase.chart, chart)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeChart(t *testing.T) {
|
||||
testcases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
input: "mychart",
|
||||
expected: "/path/to/mychart",
|
||||
},
|
||||
{
|
||||
input: "/charts/mychart",
|
||||
expected: "/charts/mychart",
|
||||
},
|
||||
}
|
||||
|
||||
for i := range testcases {
|
||||
testcase := testcases[i]
|
||||
|
||||
actual := normalizeChart("/path/to", testcase.input)
|
||||
|
||||
if testcase.expected != actual {
|
||||
t.Fatalf("unexpected result: normalizeChart(\"/path/to\", \"%s\"): expected=%v, got=%v", testcase.input, testcase.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package testhelper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type TestFs struct {
|
||||
Cwd string
|
||||
dirs map[string]bool
|
||||
files map[string]string
|
||||
|
||||
GlobFixtures map[string][]string
|
||||
|
||||
fileReaderCalls int
|
||||
successfulReads []string
|
||||
}
|
||||
|
||||
func NewTestFs(files map[string]string) *TestFs {
|
||||
dirs := map[string]bool{}
|
||||
for abs, _ := range files {
|
||||
for d := filepath.Dir(abs); !dirs[d]; d = filepath.Dir(d) {
|
||||
dirs[d] = true
|
||||
fmt.Fprintf(os.Stderr, "testfs: recognized dir: %s\n", d)
|
||||
}
|
||||
}
|
||||
return &TestFs{
|
||||
Cwd: "/path/to",
|
||||
dirs: dirs,
|
||||
files: files,
|
||||
|
||||
successfulReads: []string{},
|
||||
|
||||
GlobFixtures: map[string][]string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *TestFs) FileExistsAt(path string) bool {
|
||||
var ok bool
|
||||
if strings.HasPrefix(path, "/") {
|
||||
_, ok = f.files[path]
|
||||
} else {
|
||||
_, ok = f.files[filepath.Join(f.Cwd, path)]
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func (f *TestFs) FileExists(path string) (bool, error) {
|
||||
return f.FileExistsAt(path), nil
|
||||
}
|
||||
|
||||
func (f *TestFs) DirectoryExistsAt(path string) bool {
|
||||
var ok bool
|
||||
if strings.HasPrefix(path, "/") {
|
||||
_, ok = f.dirs[path]
|
||||
} else {
|
||||
_, ok = f.dirs[filepath.Join(f.Cwd, path)]
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func (f *TestFs) ReadFile(filename string) ([]byte, error) {
|
||||
var str string
|
||||
var ok bool
|
||||
if strings.HasPrefix(filename, "/") {
|
||||
str, ok = f.files[filename]
|
||||
} else {
|
||||
str, ok = f.files[filepath.Join(f.Cwd, filename)]
|
||||
}
|
||||
if !ok {
|
||||
return []byte(nil), os.ErrNotExist
|
||||
}
|
||||
|
||||
f.fileReaderCalls += 1
|
||||
|
||||
f.successfulReads = append(f.successfulReads, filename)
|
||||
|
||||
return []byte(str), nil
|
||||
}
|
||||
|
||||
func (f *TestFs) SuccessfulReads() []string {
|
||||
return f.successfulReads
|
||||
}
|
||||
|
||||
func (f *TestFs) FileReaderCalls() int {
|
||||
return f.fileReaderCalls
|
||||
}
|
||||
|
||||
func (f *TestFs) Glob(relPattern string) ([]string, error) {
|
||||
var pattern string
|
||||
if strings.HasPrefix(relPattern, "/") {
|
||||
pattern = relPattern
|
||||
} else {
|
||||
pattern = filepath.Join(f.Cwd, relPattern)
|
||||
}
|
||||
|
||||
fixtures, ok := f.GlobFixtures[pattern]
|
||||
if ok {
|
||||
return fixtures, nil
|
||||
}
|
||||
|
||||
matches := []string{}
|
||||
for name, _ := range f.files {
|
||||
matched, err := filepath.Match(pattern, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if matched {
|
||||
matches = append(matches, name)
|
||||
}
|
||||
}
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
func (f *TestFs) Abs(path string) (string, error) {
|
||||
var p string
|
||||
if strings.HasPrefix(path, "/") {
|
||||
p = path
|
||||
} else {
|
||||
p = filepath.Join(f.Cwd, path)
|
||||
}
|
||||
return filepath.Clean(p), nil
|
||||
}
|
||||
|
||||
func (f *TestFs) Getwd() (string, error) {
|
||||
return f.Cwd, nil
|
||||
}
|
||||
|
||||
func (f *TestFs) Chdir(dir string) error {
|
||||
if _, ok := f.dirs[dir]; ok {
|
||||
f.Cwd = dir
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unexpected chdir \"%s\"", dir)
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -149,7 +150,7 @@ func FromYaml(str string) (Values, error) {
|
||||
m := Values{}
|
||||
|
||||
if err := yaml.Unmarshal([]byte(str), &m); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%s, offending yaml: %s", err, str)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
@@ -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,26 +2,42 @@ 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.Option("missingkey=zero")
|
||||
tmpl = tmpl.Option("missingkey=zero")
|
||||
} else {
|
||||
tmpl.Option("missingkey=error")
|
||||
tmpl = tmpl.Option("missingkey=error")
|
||||
}
|
||||
return tmpl
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -230,7 +230,8 @@ func Test_renderTemplateToString(t *testing.T) {
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for k, v := range tt.args.envs {
|
||||
err := os.Setenv(k, v)
|
||||
@@ -249,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)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user