mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-22 17:59:24 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
9eec72b318 | ||
|
|
081107bb82 | ||
|
|
af69d1ba97 | ||
|
|
53ad91607a | ||
|
|
255d92064e | ||
|
|
aef366660b | ||
|
|
1db205de48 | ||
|
|
4f83e69bf6 | ||
|
|
bae842f234 | ||
|
|
55c275b3aa | ||
|
|
0fc8ac395b | ||
|
|
272d55e31a | ||
|
|
d683ea1d7b | ||
|
|
0ea5960ef2 | ||
|
|
9a820d7bf2 | ||
|
|
4581e004b8 | ||
|
|
d2353b5b70 | ||
|
|
fb7e0a360c | ||
|
|
59b254ee45 | ||
|
|
950bd23d86 | ||
|
|
8f030d5eab | ||
|
|
a31077a1c0 | ||
|
|
32588ae319 | ||
|
|
f74963f4b6 | ||
|
|
d93ec77ea3 | ||
|
|
04a9ea1d2a | ||
|
|
f53c66749e | ||
|
|
8a13999de3 | ||
|
|
1acd07fa7e | ||
|
|
72c43a2f46 | ||
|
|
08b48a8cca | ||
|
|
0e00cdf2a5 | ||
|
|
f522ba320b | ||
|
|
8f1a15c9cd | ||
|
|
fa95e0dd92 | ||
|
|
eb36e93c5a | ||
|
|
3edc7d6f33 | ||
|
|
57df25c3ff | ||
|
|
14a392666e | ||
|
|
283dac1531 | ||
|
|
f5e565ea3e | ||
|
|
5f8b2e5c7f | ||
|
|
870d33f418 | ||
|
|
d3c5417177 | ||
|
|
0639714136 | ||
|
|
056d150856 | ||
|
|
9bcec38b3b | ||
|
|
3e40e90a2a | ||
|
|
acbb416028 | ||
|
|
c4eb95f3ec | ||
|
|
f2996e2452 | ||
|
|
79ae2df71f | ||
|
|
8f7ab740bd | ||
|
|
841580f8b3 | ||
|
|
c6c54fdbed | ||
|
|
35fd5ff117 | ||
|
|
f2bdcb9113 | ||
|
|
9cfd6aeae7 | ||
|
|
27da8a6b7a | ||
|
|
c6236a15bb | ||
|
|
b894012c80 | ||
|
|
fb256b0161 | ||
|
|
abeccc5e8e | ||
|
|
f84ed93754 | ||
|
|
bf42d2519d | ||
|
|
99ce8570c7 | ||
|
|
571f351a8f | ||
|
|
b9a097ed14 | ||
|
|
344c20d790 | ||
|
|
5f52f96709 | ||
|
|
171eacfb3c | ||
|
|
7d976e5271 | ||
|
|
f813ac2642 | ||
|
|
23178b398c | ||
|
|
fea62032e9 | ||
|
|
1f7a8ddbe3 | ||
|
|
413ab4f55b | ||
|
|
39bc28c23f | ||
|
|
4b22d4fc78 | ||
|
|
2a35b6864c | ||
|
|
4b23213ce4 | ||
|
|
971cbcf1db | ||
|
|
d898dfbd0d | ||
|
|
b41b44c313 | ||
|
|
b3e27db8b3 | ||
|
|
9cab7b00f6 | ||
|
|
bfc86de92d | ||
|
|
5ad916003f | ||
|
|
b0f9062e08 | ||
|
|
770c3daa5f | ||
|
|
6f0dc6e069 | ||
|
|
f23a43c80c | ||
|
|
942f9a20b0 | ||
|
|
429c42d429 | ||
|
|
7d1787d0b6 | ||
|
|
98be623701 | ||
|
|
76122738c8 | ||
|
|
973d47bfc0 | ||
|
|
2eb13c25c5 | ||
|
|
b94265122f | ||
|
|
f2b610afdf | ||
|
|
6cba77d4f2 | ||
|
|
b9de22b256 | ||
|
|
4da13b44ee | ||
|
|
5d1a92d3a5 |
+148
-19
@@ -4,59 +4,125 @@ jobs:
|
||||
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.10.1
|
||||
working_directory: /go/src/github.com/roboll/helmfile
|
||||
- image: circleci/golang:1.14.2
|
||||
environment:
|
||||
GOPATH: /go
|
||||
working_directory: /home/circleci/workspace/helmfile
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-mod-cache-v1-{{ checksum "./go.sum" }}
|
||||
- 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:
|
||||
- /go/pkg/mod
|
||||
- persist_to_workspace:
|
||||
root: /go/src/github.com/roboll/helmfile
|
||||
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.10.1
|
||||
working_directory: /go/src/github.com/roboll/helmfile
|
||||
- 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-
|
||||
- run: make check
|
||||
- run: make pristine
|
||||
- run: make test
|
||||
|
||||
# thanks to https://raw.githubusercontent.com/weaveworks/launcher/master/.circleci/config.yml
|
||||
integration_tests:
|
||||
machine: true
|
||||
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: 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.9.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:
|
||||
CHANGE_MINIKUBE_NONE_USER: true
|
||||
K8S_VERSION: v1.9.0
|
||||
MINIKUBE_VERSION: v0.25.2
|
||||
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 minikube config set WantReportErrorPrompt false
|
||||
sudo -E minikube start --vm-driver=none --kubernetes-version=${K8S_VERSION}
|
||||
sudo -E minikube update-context
|
||||
- run:
|
||||
@@ -68,35 +134,98 @@ 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.10.1
|
||||
working_directory: /go/src/github.com/roboll/helmfile
|
||||
- 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: |
|
||||
docker login -u="$DOCKER_USER" -p="$DOCKER_PASS" quay.io
|
||||
make tools
|
||||
BUILD_URL="$CIRCLE_BUILD_URL" make push release
|
||||
go mod vendor
|
||||
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:
|
||||
|
||||
+4
-1
@@ -1,3 +1,6 @@
|
||||
dist/
|
||||
.idea/
|
||||
helmfile
|
||||
helmfile
|
||||
helmfile.lock
|
||||
test/integration/tmp
|
||||
vendor/
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# DCO + License
|
||||
|
||||
By contributing to `helmfile`, you accept and agree to the following DCO and license terms and
|
||||
conditions for your present and future Contributions submitted to the `helmfile` project.
|
||||
|
||||
[DCO](https://developercertificate.org/)
|
||||
[License](https://github.com/roboll/helmfile/blob/master/LICENSE)
|
||||
|
||||
# Developing helmfile
|
||||
|
||||
Locate your `GOPATH`, usually `~/go`, and run:
|
||||
|
||||
```console
|
||||
$ go get github.com/roboll/helmfile
|
||||
|
||||
$ cd $GOPATH/src/github.com/roboll/helmfile
|
||||
|
||||
$ git checkout -b your-shiny-new-feature origin/master
|
||||
|
||||
...
|
||||
|
||||
$ git commit -m 'feat: do whatever for whatever purpose
|
||||
|
||||
This adds ... by:
|
||||
|
||||
- Adding ...
|
||||
- Changing ...
|
||||
- Removing...
|
||||
|
||||
Resolves #ISSUE_NUMBER
|
||||
'
|
||||
|
||||
$ hub fork
|
||||
$ git push YOUR_GITHUB_USER your-shiny-new-feature
|
||||
$ hub pull-request
|
||||
```
|
||||
|
||||
Note that the above tutorial uses [hub](https://github.com/github/hub) just for ease of explanation.
|
||||
Please use whatever tool or way to author your pull request!
|
||||
+28
-12
@@ -1,29 +1,45 @@
|
||||
FROM golang:1.10.1-alpine3.7 as builder
|
||||
FROM golang:1.14.2-alpine3.11 as builder
|
||||
|
||||
RUN apk add --no-cache make git
|
||||
WORKDIR /go/src/github.com/roboll/helmfile/
|
||||
COPY . /go/src/github.com/roboll/helmfile/
|
||||
WORKDIR /workspace/helmfile
|
||||
COPY . /workspace/helmfile
|
||||
RUN make static-linux
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
FROM alpine:3.7
|
||||
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.10.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="0fa2ed4983b1e4a3f90f776d08b88b0c73fd83f305b5b634175cb15e61342ffe"
|
||||
ARG HELM_SHA256="756ab375314329b66b452c0f9d569f74b0760141670217c07b79890ad314c214"
|
||||
RUN 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"
|
||||
RUN helm plugin install https://github.com/databus23/helm-diff && \
|
||||
helm plugin install https://github.com/futuresimple/helm-secrets
|
||||
# using the install documentation found at https://kubernetes.io/docs/tasks/tools/install-kubectl/
|
||||
# for now but in a future version of alpine (in the testing version at the time of writing)
|
||||
# we should be able to install using apk add.
|
||||
ENV KUBECTL_VERSION="v1.14.5"
|
||||
ENV KUBECTL_SHA256="26681319de56820a8467c9407e9203d5b15fb010ffc75ac5b99c9945ad0bd28c"
|
||||
RUN curl --retry 3 --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
|
||||
|
||||
COPY --from=builder /go/src/github.com/roboll/helmfile/dist/helmfile_linux_amd64 /usr/local/bin/helmfile
|
||||
RUN ["helm", "init", "--client-only"]
|
||||
RUN helm plugin install https://github.com/databus23/helm-diff && \
|
||||
helm plugin install https://github.com/futuresimple/helm-secrets && \
|
||||
helm plugin install https://github.com/hypnoglow/helm-s3.git && \
|
||||
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
|
||||
|
||||
CMD ["/usr/local/bin/helmfile"]
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
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.3
|
||||
ARG HELM_LOCATION="https://get.helm.sh"
|
||||
ARG HELM_FILENAME="helm-${HELM_VERSION}-linux-amd64.tar.gz"
|
||||
ARG HELM_SHA256="246d58b6b353e63ae8627415a7340089015e3eb542ff7b5ce124b0b1409369cc"
|
||||
RUN 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.
|
||||
ENV KUBECTL_VERSION="v1.14.5"
|
||||
ENV KUBECTL_SHA256="26681319de56820a8467c9407e9203d5b15fb010ffc75ac5b99c9945ad0bd28c"
|
||||
RUN curl --retry 3 --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"]
|
||||
Generated
-90
@@ -1,90 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/Masterminds/semver"
|
||||
packages = ["."]
|
||||
revision = "8d82589cda2d7b5b9167396841975c4535c9bec6"
|
||||
version = "v1.4.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/Masterminds/sprig"
|
||||
packages = ["."]
|
||||
revision = "6b2a58267f6a8b1dc8e2eb5519b984008fa85e8c"
|
||||
version = "v2.15.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/aokoli/goutils"
|
||||
packages = ["."]
|
||||
revision = "3391d3790d23d03408670993e957e8f408993c34"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/google/uuid"
|
||||
packages = ["."]
|
||||
revision = "064e2069ce9c359c118179501254f67d7d37ba24"
|
||||
version = "0.2"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/huandu/xstrings"
|
||||
packages = ["."]
|
||||
revision = "2bf18b218c51864a87384c06996e40ff9dcff8e1"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/imdario/mergo"
|
||||
packages = ["."]
|
||||
revision = "9d5f1277e9a8ed20c3684bda8fde67c05628518c"
|
||||
version = "v0.3.4"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/urfave/cli"
|
||||
packages = ["."]
|
||||
revision = "6011f165dc288c72abd8acd7722f837c5c64198d"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/atomic"
|
||||
packages = ["."]
|
||||
revision = "1ea20fb1cbb1cc08cbd0d913a96dead89aa18289"
|
||||
version = "v1.3.2"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/multierr"
|
||||
packages = ["."]
|
||||
revision = "3c4937480c32f4c13a875a1829af76c98ca3d40a"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
name = "go.uber.org/zap"
|
||||
packages = [
|
||||
".",
|
||||
"buffer",
|
||||
"internal/bufferpool",
|
||||
"internal/color",
|
||||
"internal/exit",
|
||||
"zapcore"
|
||||
]
|
||||
revision = "eeedf312bc6c57391d84767a4cd413f02a917974"
|
||||
version = "v1.8.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = [
|
||||
"pbkdf2",
|
||||
"scrypt"
|
||||
]
|
||||
revision = "b2aa35443fbc700ab74c586ae79b81c171851023"
|
||||
|
||||
[[projects]]
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
|
||||
version = "v2.2.1"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "b1f000751afc0a44973307c69b6a4b8e8c1b807fd9881a13f370c30fcbcab7a2"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
[[constraint]]
|
||||
name = "github.com/Masterminds/sprig"
|
||||
version = "2.15.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
|
||||
[prune]
|
||||
go-tests = true
|
||||
unused-packages = true
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/imdario/mergo"
|
||||
version = "0.3.4"
|
||||
@@ -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,19 +30,24 @@ 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 '!arm !mips !mipsle !mips64 !mips64le !s390x' -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" -ldflags '-X github.com/roboll/helmfile/pkg/app/version.Version=${TAG}' ${TARGETS}
|
||||
.PHONY: cross
|
||||
|
||||
static-linux:
|
||||
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 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 github.com/roboll/helmfile/pkg/app/version.Version=${TAG}' ${TARGETS}
|
||||
.PHONY: install
|
||||
|
||||
clean:
|
||||
rm dist/helmfile_*
|
||||
.PHONY: clean
|
||||
|
||||
pristine: generate fmt
|
||||
git ls-files --exclude-standard --modified --deleted --others | diff /dev/null -
|
||||
git diff | cat
|
||||
git ls-files --exclude-standard --modified --deleted --others -x vendor | grep -v '^go.' | diff /dev/null -
|
||||
.PHONY: pristine
|
||||
|
||||
release: pristine cross
|
||||
@@ -54,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
|
||||
@@ -61,3 +77,19 @@ tools:
|
||||
TAG = $(shell git describe --tags --abbrev=0 HEAD)
|
||||
LAST = $(shell git describe --tags --abbrev=0 HEAD^)
|
||||
BODY = "`git log ${LAST}..HEAD --oneline --decorate` `printf '\n\#\#\# [Build Info](${BUILD_URL})'`"
|
||||
|
||||
release/minor:
|
||||
git checkout master
|
||||
git pull --rebase origin master
|
||||
bash -c 'if git branch | grep autorelease; then git branch -D autorelease; else echo no branch to be cleaned; fi'
|
||||
git checkout -b autorelease origin/master
|
||||
bash -c 'SEMTAG_REMOTE=origin hack/semtag final -s minor'
|
||||
git checkout master
|
||||
|
||||
release/patch:
|
||||
git checkout master
|
||||
git pull --rebase origin master
|
||||
bash -c 'if git branch | grep autorelease; then git branch -D autorelease; else echo no branch to be cleaned; fi'
|
||||
git checkout -b autorelease origin/master
|
||||
bash -c 'SEMTAG_REMOTE=origin hack/semtag final -s patch'
|
||||
git checkout master
|
||||
|
||||
@@ -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).
|
||||
-413
@@ -1,413 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/roboll/helmfile/helmexec"
|
||||
"github.com/roboll/helmfile/state"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// See https://github.com/roboll/helmfile/issues/193
|
||||
func TestFindAndIterateOverDesiredStates(t *testing.T) {
|
||||
absPaths := map[string]string{
|
||||
".": "/path/to",
|
||||
"/path/to/helmfile.d": "/path/to/helmfile.d",
|
||||
}
|
||||
dirs := map[string]bool{
|
||||
"helmfile.d": true,
|
||||
}
|
||||
files := map[string]string{
|
||||
"helmfile.yaml": `
|
||||
helmfiles:
|
||||
- helmfile.d/a*.yaml
|
||||
- helmfile.d/b*.yaml
|
||||
`,
|
||||
"/path/to/helmfile.d/a1.yaml": `
|
||||
releases:
|
||||
- name: zipkin
|
||||
chart: stable/zipkin
|
||||
`,
|
||||
"/path/to/helmfile.d/a2.yaml": `
|
||||
releases:
|
||||
- name: prometheus
|
||||
chart: stable/prometheus
|
||||
`,
|
||||
"/path/to/helmfile.d/b.yaml": `
|
||||
releases:
|
||||
- name: grafana
|
||||
chart: stable/grafana
|
||||
`,
|
||||
}
|
||||
globMatches := map[string][]string{
|
||||
"/path/to/helmfile.d/a*.yaml": []string{"/path/to/helmfile.d/a1.yaml", "/path/to/helmfile.d/a2.yaml"},
|
||||
"/path/to/helmfile.d/b*.yaml": []string{"/path/to/helmfile.d/b.yaml"},
|
||||
}
|
||||
fileExistsAt := func(path string) bool {
|
||||
_, ok := files[path]
|
||||
return ok
|
||||
}
|
||||
directoryExistsAt := func(path string) bool {
|
||||
_, ok := dirs[path]
|
||||
return ok
|
||||
}
|
||||
readFile := func(filename string) ([]byte, error) {
|
||||
str, ok := files[filename]
|
||||
if !ok {
|
||||
return []byte(nil), fmt.Errorf("no file found: %s", filename)
|
||||
}
|
||||
return []byte(str), nil
|
||||
}
|
||||
glob := func(pattern string) ([]string, error) {
|
||||
matches, ok := globMatches[pattern]
|
||||
if !ok {
|
||||
return []string(nil), fmt.Errorf("no file matched: %s", pattern)
|
||||
}
|
||||
return matches, nil
|
||||
}
|
||||
abs := func(path string) (string, error) {
|
||||
a, ok := absPaths[path]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("abs: unexpected path: %s", path)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
app := &app{
|
||||
readFile: readFile,
|
||||
glob: glob,
|
||||
abs: abs,
|
||||
fileExistsAt: fileExistsAt,
|
||||
directoryExistsAt: directoryExistsAt,
|
||||
kubeContext: "default",
|
||||
logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
}
|
||||
noop := func(st *state.HelmState, helm helmexec.Interface) []error {
|
||||
return []error{}
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
expectErr bool
|
||||
}{
|
||||
{name: "prometheus", expectErr: false},
|
||||
{name: "zipkin", expectErr: false},
|
||||
{name: "grafana", expectErr: false},
|
||||
{name: "elasticsearch", expectErr: true},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
err := app.FindAndIterateOverDesiredStates(
|
||||
"helmfile.yaml", noop, "", []string{fmt.Sprintf("name=%s", testcase.name)}, "default",
|
||||
)
|
||||
if testcase.expectErr && err == nil {
|
||||
t.Errorf("error expected but not happened for name=%s", testcase.name)
|
||||
} else if !testcase.expectErr && err != nil {
|
||||
t.Errorf("unexpected error for name=%s: %v", testcase.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See https://github.com/roboll/helmfile/issues/320
|
||||
func TestFindAndIterateOverDesiredStates_UndefinedEnv(t *testing.T) {
|
||||
absPaths := map[string]string{
|
||||
".": "/path/to",
|
||||
"/path/to/helmfile.d": "/path/to/helmfile.d",
|
||||
}
|
||||
dirs := map[string]bool{
|
||||
"helmfile.d": true,
|
||||
}
|
||||
files := map[string]string{
|
||||
"helmfile.yaml": `
|
||||
environments:
|
||||
prod:
|
||||
|
||||
helmfiles:
|
||||
- helmfile.d/a*.yaml
|
||||
`,
|
||||
"/path/to/helmfile.d/a1.yaml": `
|
||||
environments:
|
||||
prod:
|
||||
|
||||
releases:
|
||||
- name: zipkin
|
||||
chart: stable/zipkin
|
||||
`,
|
||||
}
|
||||
globMatches := map[string][]string{
|
||||
"/path/to/helmfile.d/a*.yaml": []string{"/path/to/helmfile.d/a1.yaml"},
|
||||
}
|
||||
fileExistsAt := func(path string) bool {
|
||||
_, ok := files[path]
|
||||
return ok
|
||||
}
|
||||
directoryExistsAt := func(path string) bool {
|
||||
_, ok := dirs[path]
|
||||
return ok
|
||||
}
|
||||
readFile := func(filename string) ([]byte, error) {
|
||||
str, ok := files[filename]
|
||||
if !ok {
|
||||
return []byte(nil), fmt.Errorf("no file found: %s", filename)
|
||||
}
|
||||
return []byte(str), nil
|
||||
}
|
||||
glob := func(pattern string) ([]string, error) {
|
||||
matches, ok := globMatches[pattern]
|
||||
if !ok {
|
||||
return []string(nil), fmt.Errorf("no file matched: %s", pattern)
|
||||
}
|
||||
return matches, nil
|
||||
}
|
||||
abs := func(path string) (string, error) {
|
||||
a, ok := absPaths[path]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("abs: unexpected path: %s", path)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
app := &app{
|
||||
readFile: readFile,
|
||||
glob: glob,
|
||||
abs: abs,
|
||||
fileExistsAt: fileExistsAt,
|
||||
directoryExistsAt: directoryExistsAt,
|
||||
kubeContext: "default",
|
||||
logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
}
|
||||
noop := func(st *state.HelmState, helm helmexec.Interface) []error {
|
||||
return []error{}
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
expectErr bool
|
||||
}{
|
||||
{name: "undefined_env", expectErr: true},
|
||||
{name: "default", expectErr: false},
|
||||
{name: "prod", expectErr: false},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
err := app.FindAndIterateOverDesiredStates(
|
||||
"helmfile.yaml", noop, "", []string{}, testcase.name,
|
||||
)
|
||||
if testcase.expectErr && err == nil {
|
||||
t.Errorf("error expected but not happened for environment=%s", testcase.name)
|
||||
} else if !testcase.expectErr && err != nil {
|
||||
t.Errorf("unexpected error for environment=%s: %v", testcase.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See https://github.com/roboll/helmfile/issues/322
|
||||
func TestFindAndIterateOverDesiredStates_Selectors(t *testing.T) {
|
||||
absPaths := map[string]string{
|
||||
".": "/path/to",
|
||||
"/path/to/helmfile.d": "/path/to/helmfile.d",
|
||||
}
|
||||
dirs := map[string]bool{
|
||||
"helmfile.d": true,
|
||||
}
|
||||
files := map[string]string{
|
||||
"helmfile.yaml": `
|
||||
helmfiles:
|
||||
- helmfile.d/a*.yaml
|
||||
- helmfile.d/b*.yaml
|
||||
`,
|
||||
"/path/to/helmfile.d/a1.yaml": `
|
||||
releases:
|
||||
- name: zipkin
|
||||
chart: stable/zipkin
|
||||
`,
|
||||
"/path/to/helmfile.d/a2.yaml": `
|
||||
releases:
|
||||
- name: prometheus
|
||||
chart: stable/prometheus
|
||||
`,
|
||||
"/path/to/helmfile.d/b.yaml": `
|
||||
releases:
|
||||
- name: grafana
|
||||
chart: stable/grafana
|
||||
`,
|
||||
}
|
||||
globMatches := map[string][]string{
|
||||
"/path/to/helmfile.d/a*.yaml": []string{"/path/to/helmfile.d/a1.yaml", "/path/to/helmfile.d/a2.yaml"},
|
||||
"/path/to/helmfile.d/b*.yaml": []string{"/path/to/helmfile.d/b.yaml"},
|
||||
}
|
||||
fileExistsAt := func(path string) bool {
|
||||
_, ok := files[path]
|
||||
return ok
|
||||
}
|
||||
directoryExistsAt := func(path string) bool {
|
||||
_, ok := dirs[path]
|
||||
return ok
|
||||
}
|
||||
readFile := func(filename string) ([]byte, error) {
|
||||
str, ok := files[filename]
|
||||
if !ok {
|
||||
return []byte(nil), fmt.Errorf("no file found: %s", filename)
|
||||
}
|
||||
return []byte(str), nil
|
||||
}
|
||||
glob := func(pattern string) ([]string, error) {
|
||||
matches, ok := globMatches[pattern]
|
||||
if !ok {
|
||||
return []string(nil), fmt.Errorf("no file matched: %s", pattern)
|
||||
}
|
||||
return matches, nil
|
||||
}
|
||||
abs := func(path string) (string, error) {
|
||||
a, ok := absPaths[path]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("abs: unexpected path: %s", path)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
app := &app{
|
||||
readFile: readFile,
|
||||
glob: glob,
|
||||
abs: abs,
|
||||
fileExistsAt: fileExistsAt,
|
||||
directoryExistsAt: directoryExistsAt,
|
||||
kubeContext: "default",
|
||||
logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
}
|
||||
noop := func(st *state.HelmState, helm helmexec.Interface) []error {
|
||||
return []error{}
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
label string
|
||||
expectErr bool
|
||||
errMsg string
|
||||
}{
|
||||
{label: "name=prometheus", expectErr: false},
|
||||
{label: "name=", expectErr: true, errMsg: "Malformed label: name=. Expected label in form k=v or k!=v"},
|
||||
{label: "name!=", expectErr: true, errMsg: "Malformed label: name!=. Expected label in form k=v or k!=v"},
|
||||
{label: "name", expectErr: true, errMsg: "Malformed label: name. Expected label in form k=v or k!=v"},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
err := app.FindAndIterateOverDesiredStates(
|
||||
"helmfile.yaml", noop, "", []string{testcase.label}, "default",
|
||||
)
|
||||
if testcase.expectErr {
|
||||
if err == nil {
|
||||
t.Errorf("error expected but not happened for name=%s", testcase.label)
|
||||
} else if err.Error() != testcase.errMsg {
|
||||
t.Errorf("unexpected error message: expected=\"%s\", actual=\"%s\"", testcase.errMsg, err.Error())
|
||||
}
|
||||
} else if !testcase.expectErr && err != nil {
|
||||
t.Errorf("unexpected error for name=%s: %v", testcase.label, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See https://github.com/roboll/helmfile/issues/312
|
||||
func TestFindAndIterateOverDesiredStates_ReverseOrder(t *testing.T) {
|
||||
absPaths := map[string]string{
|
||||
".": "/path/to",
|
||||
"/path/to/helmfile.d": "/path/to/helmfile.d",
|
||||
}
|
||||
dirs := map[string]bool{
|
||||
"helmfile.d": true,
|
||||
}
|
||||
files := map[string]string{
|
||||
"helmfile.yaml": `
|
||||
helmfiles:
|
||||
- helmfile.d/a*.yaml
|
||||
- helmfile.d/b*.yaml
|
||||
`,
|
||||
"/path/to/helmfile.d/a1.yaml": `
|
||||
releases:
|
||||
- name: zipkin
|
||||
chart: stable/zipkin
|
||||
`,
|
||||
"/path/to/helmfile.d/a2.yaml": `
|
||||
releases:
|
||||
- name: prometheus
|
||||
chart: stable/prometheus
|
||||
- name: elasticsearch
|
||||
chart: stable/elasticsearch
|
||||
`,
|
||||
"/path/to/helmfile.d/b.yaml": `
|
||||
releases:
|
||||
- name: grafana
|
||||
chart: stable/grafana
|
||||
`,
|
||||
}
|
||||
globMatches := map[string][]string{
|
||||
"/path/to/helmfile.d/a*.yaml": []string{"/path/to/helmfile.d/a1.yaml", "/path/to/helmfile.d/a2.yaml"},
|
||||
"/path/to/helmfile.d/b*.yaml": []string{"/path/to/helmfile.d/b.yaml"},
|
||||
}
|
||||
fileExistsAt := func(path string) bool {
|
||||
_, ok := files[path]
|
||||
return ok
|
||||
}
|
||||
directoryExistsAt := func(path string) bool {
|
||||
_, ok := dirs[path]
|
||||
return ok
|
||||
}
|
||||
readFile := func(filename string) ([]byte, error) {
|
||||
str, ok := files[filename]
|
||||
if !ok {
|
||||
return []byte(nil), fmt.Errorf("no file found: %s", filename)
|
||||
}
|
||||
return []byte(str), nil
|
||||
}
|
||||
glob := func(pattern string) ([]string, error) {
|
||||
matches, ok := globMatches[pattern]
|
||||
if !ok {
|
||||
return []string(nil), fmt.Errorf("no file matched: %s", pattern)
|
||||
}
|
||||
return matches, nil
|
||||
}
|
||||
abs := func(path string) (string, error) {
|
||||
a, ok := absPaths[path]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("abs: unexpected path: %s", path)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
expected := []string{"grafana", "elasticsearch", "prometheus", "zipkin"}
|
||||
|
||||
testcases := []struct {
|
||||
reverse bool
|
||||
expected []string
|
||||
}{
|
||||
{reverse: false, expected: []string{"zipkin", "prometheus", "elasticsearch", "grafana"}},
|
||||
{reverse: true, expected: []string{"grafana", "elasticsearch", "prometheus", "zipkin"}},
|
||||
}
|
||||
for _, testcase := range testcases {
|
||||
actual := []string{}
|
||||
|
||||
collectReleases := func(st *state.HelmState, helm helmexec.Interface) []error {
|
||||
for _, r := range st.Releases {
|
||||
actual = append(actual, r.Name)
|
||||
}
|
||||
return []error{}
|
||||
}
|
||||
|
||||
app := &app{
|
||||
readFile: readFile,
|
||||
glob: glob,
|
||||
abs: abs,
|
||||
fileExistsAt: fileExistsAt,
|
||||
directoryExistsAt: directoryExistsAt,
|
||||
kubeContext: "default",
|
||||
logger: helmexec.NewLogger(os.Stderr, "debug"),
|
||||
reverse: testcase.reverse,
|
||||
}
|
||||
err := app.FindAndIterateOverDesiredStates(
|
||||
"helmfile.yaml", collectReleases, "", []string{}, "default",
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(testcase.expected, actual) {
|
||||
t.Errorf("releases did not match: expected=%v actual=%v", expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
## Advanced Features
|
||||
|
||||
- [Import Configuration Parameters into Helmfile](#import-configuration-parameters-into-helmfile)
|
||||
|
||||
### Import Configuration Parameters into Helmfile
|
||||
|
||||
Helmfile integrates [vals]() to import configuration parameters from following backends:
|
||||
|
||||
- AWS SSM Parameter Store
|
||||
- AWS SecretsManager
|
||||
- Vault
|
||||
- SOPS
|
||||
|
||||
See [Vals "Suported Backends"](https://github.com/variantdev/vals#suported-backends) for the full list of available backends.
|
||||
|
||||
This feature was implemented in https://github.com/roboll/helmfile/pull/906.
|
||||
If you're curious how it's designed and how it works, please consult the pull request.
|
||||
|
||||
### Deploy Kustomizations with Helmfile
|
||||
|
||||
You can deploy [kustomize](https://github.com/kubernetes-sigs/kustomize) "kustomization"s with Helmfile.
|
||||
|
||||
Most of Kustomize operations that is usually done with `kustomize edit` can be done declaratively via Helm values.yaml files.
|
||||
|
||||
Under the hood, Helmfile transforms the kustomization into a local chart in a temporary directory so that it can be `helm upgrade --install`ed.
|
||||
|
||||
The transformation is done by generating (1)a temporary kustomization from various options and (2)temporary chart from the temporary kustomization.
|
||||
|
||||
An example pseudo code for the transformation logic can be written as:
|
||||
|
||||
```console
|
||||
$ TMPCHART=/tmp/sometmpdir
|
||||
$ mkdir -p ${TMPCHART}/templates
|
||||
$ somehow_generate_chart_yaml ${TMPCHART}/Chart.yaml
|
||||
|
||||
$ TMPKUSTOMIZATION=/tmp/sometmpdir2
|
||||
$ somehow_generate_temp_kustomization_yaml ${TMPKUSTOMIZATION}/kustomization.yaml
|
||||
$ kustomize build ${TMPKUSTOMIZATION}/kustomization.yaml > ${TMPCHART}/templates/all.yaml
|
||||
```
|
||||
|
||||
Let's say you have a `helmfile.yaml` that looks like the below:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: myapp
|
||||
chart: mykustomization
|
||||
values:
|
||||
- values.yaml
|
||||
```
|
||||
|
||||
Helmfile firstly generates a temporary `kustomization.yaml` that looks like:
|
||||
|
||||
```yaml
|
||||
bases:
|
||||
- $(ABS_PATH_TO_HELMFILE_YAML}/mykustomization
|
||||
```
|
||||
|
||||
Followed by the below steps:
|
||||
|
||||
- Running `kustomize edit set image $IMAGE` for every `$IMAGE` generated from your values.yaml
|
||||
- Running `kustomize edit set nameprefix $NAMEPREFIX` with the nameprefix specified in your values.yaml
|
||||
- Running `kustomize edit set namesuffix $NAMESUFFIX` with the namesuffix specified in your values.yaml
|
||||
- Running `kustomize edit set namespace $NS` with the namespace specified in your values.yaml
|
||||
|
||||
A `values.yaml` file for kustomization would look like the below:
|
||||
|
||||
```yaml
|
||||
images:
|
||||
# kustomize edit set image mysql=eu.gcr.io/my-project/mysql@canary
|
||||
- name: mysql
|
||||
newName: eu.gcr.io/my-project/mysql
|
||||
newTag: canary
|
||||
# kustomize edit set image myapp=my-registry/my-app@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3
|
||||
- name: myapp
|
||||
digest: sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d3
|
||||
newName: my-registry/my-app
|
||||
|
||||
# kustomize edit set nameprefix foo-
|
||||
namePrefix: foo-
|
||||
|
||||
# kustomize edit set namesuffix -bar
|
||||
nameSuffix: -bar
|
||||
|
||||
# kustomize edit set namespace myapp
|
||||
namespace: myapp
|
||||
```
|
||||
|
||||
At this point, Helmfile can generate a complete kustomization from the base kustomization you specified in `releases[].chart` of your helmfile.yaml and `values.yaml`,
|
||||
which can be included in the temporary chart.
|
||||
|
||||
After all, Helmfile just installs the temporary chart like standard charts, which allows you to manage everything with Helmfile regardless of each app is declared using a Helm chart or a kustomization.
|
||||
|
||||
Please also see [test/advanced/helmfile.yaml](https://github.com/roboll/helmfile/tree/master/test/advanced/helmfile.yaml) for an example of kustomization support and more.
|
||||
|
||||
## Adhoc Customization of Helm charts
|
||||
|
||||
You can add/update any Kubernetes resource field rendered from a Helm chart by specifying `releases[].strategicMergePatches`:
|
||||
|
||||
```
|
||||
repositories:
|
||||
- name: incubator
|
||||
url: https://kubernetes-charts-incubator.storage.googleapis.com
|
||||
|
||||
releases:
|
||||
- name: raw1
|
||||
chart: incubator/raw
|
||||
values:
|
||||
- resources:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: raw1
|
||||
namespace: default
|
||||
data:
|
||||
foo: FOO
|
||||
strategicMergePatches:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: raw1
|
||||
namespace: default
|
||||
data:
|
||||
bar: BAR
|
||||
```
|
||||
|
||||
Running `helmfile template` on the above example results in a ConfigMap called `raw` whose `data` is:
|
||||
|
||||
```yaml
|
||||
foo: FOO
|
||||
bar: BAR
|
||||
```
|
||||
|
||||
Please note that the second `data` field `bar` is coming from the strategic-merge patch defined in the above helmfile.yaml.
|
||||
|
||||
There's also `releases[].jsonPatches` that works similarly to `strategicMergePatches` but has additional capability to remove fields.
|
||||
|
||||
Please also see [test/advanced/helmfile.yaml](https://github.com/roboll/helmfile/tree/master/test/advanced/helmfile.yaml) for an example of patching support and more.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Secrets
|
||||
|
||||
helmfile can handle secrets using [helm-secrets](https://github.com/zendesk/helm-secrets) plugin or using remote secrets storage
|
||||
(everything that package [vals](https://github.com/variantdev/vals) can handle vault, AWS SSM etc)
|
||||
This section will describe the second use case.
|
||||
|
||||
# Remote secrets
|
||||
|
||||
This paragraph will describe how to use remote secrets storage (vault, SSM etc) in helmfile
|
||||
|
||||
## Fetching single key
|
||||
|
||||
To fetch single key from remote secret storage you can use `fetchSecretValue` template function example below
|
||||
|
||||
```yaml
|
||||
# helmfile.yaml
|
||||
|
||||
repositories:
|
||||
- name: stable
|
||||
url: https://kubernetes-charts.storage.googleapis.com
|
||||
|
||||
environments:
|
||||
default:
|
||||
values:
|
||||
- service:
|
||||
password: ref+vault://svc/#pass
|
||||
login: ref+vault://svc/#login
|
||||
releases:
|
||||
- name: service
|
||||
namespace: default
|
||||
labels:
|
||||
cluster: services
|
||||
secrets: vault
|
||||
chart: stable/svc
|
||||
version: 0.1.0
|
||||
values:
|
||||
- service:
|
||||
login: {{ .Values.service.login | fetchSecretValue }} # this will resolve ref+vault://svc/#pass and fetch secret from vault
|
||||
password: {{ .Values.service.password | fetchSecretValue | quote }}
|
||||
# - values/service.yaml.gotmpl # alternatively
|
||||
```
|
||||
## Fetching multiple keys
|
||||
Alternatively you can use `expandSecretRefs` to fetch a map of secrets
|
||||
```yaml
|
||||
# values/service.yaml.gotmpl
|
||||
service:
|
||||
{{ .Values.service | expandSecretRefs | toYaml | nindent 2 }}
|
||||
```
|
||||
|
||||
This will produce
|
||||
```yaml
|
||||
# values/service.yaml
|
||||
service:
|
||||
login: svc-login # fetched from vault
|
||||
password: pass
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# Shared Configuration Across Teams
|
||||
|
||||
Assume you have a two or more teams, each works for a different internal or external service, like:
|
||||
|
||||
- Product 1
|
||||
- Product 2
|
||||
- Observability
|
||||
|
||||
The simplest `helmfile.yaml` that declares the whole cluster that is composed of the three services would look like the below:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: product1-api
|
||||
chart: product1-charts/api
|
||||
# snip
|
||||
- name: product1-web
|
||||
chart: product1-charts/web
|
||||
# snip
|
||||
- name: product2-api
|
||||
chart: saas-charts/api
|
||||
# snip
|
||||
- name: product2-web
|
||||
chart: product2-charts/web
|
||||
# snip
|
||||
- name: observability-prometheus-operator
|
||||
chart: stable/prometheus-operator
|
||||
# snip
|
||||
- name: observability-process-exporter
|
||||
chart: stable/prometheus-operator
|
||||
# snip
|
||||
```
|
||||
|
||||
This works, but what if you wanted to a separate cluster per service to achieve smaller blast radius?
|
||||
|
||||
Let's start by creating a `helmfile.yaml` for each service.
|
||||
|
||||
`product1/helmfile.yaml`:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: product1-api
|
||||
chart: product1-charts/api
|
||||
# snip
|
||||
- name: product1-web
|
||||
chart: product1-charts/web
|
||||
# snip
|
||||
- name: observability-prometheus-operator
|
||||
chart: stable/prometheus-operator
|
||||
# snip
|
||||
- name: observability-process-exporter
|
||||
chart: stable/prometheus-operator
|
||||
# snip
|
||||
```
|
||||
|
||||
`product2/helmfile.yaml`:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: product2-api
|
||||
chart: product2-charts/api
|
||||
# snip
|
||||
- name: product2-web
|
||||
chart: product2-charts/web
|
||||
# snip
|
||||
- name: observability-prometheus-operator
|
||||
chart: stable/prometheus-operator
|
||||
# snip
|
||||
- name: observability-process-exporter
|
||||
chart: stable/prometheus-operator
|
||||
# snip
|
||||
```
|
||||
|
||||
You will (of course!) notice this isn't DRY.
|
||||
|
||||
To remove the duplication of observability stack between the two helmfiles, create a "sub-helmfile" for the observability stack.
|
||||
|
||||
`observability/helmfile.yaml`:
|
||||
|
||||
```yaml
|
||||
- name: observability-prometheus-operator
|
||||
chart: stable/prometheus-operator
|
||||
# snip
|
||||
- name: observability-process-exporter
|
||||
chart: stable/prometheus-operator
|
||||
# snip
|
||||
```
|
||||
|
||||
As you might have imagined, the observability helmfile can be reused from the two product helmfiles by declaring `helmfiles`.
|
||||
|
||||
`product1/helmfile.yaml`:
|
||||
|
||||
```yaml
|
||||
helmfiles:
|
||||
- ../observability/helmfile.yaml
|
||||
|
||||
releases:
|
||||
- name: product1-api
|
||||
chart: product1-charts/api
|
||||
# snip
|
||||
- name: product1-web
|
||||
chart: product1-charts/web
|
||||
# snip
|
||||
```
|
||||
|
||||
`product2/helmfile.yaml`:
|
||||
|
||||
```yaml
|
||||
helmfiles:
|
||||
- ../observability/helmfile.yaml
|
||||
|
||||
releases:
|
||||
- name: product2-api
|
||||
chart: product2-charts/api
|
||||
# snip
|
||||
- name: product2-web
|
||||
chart: product2-charts/web
|
||||
# snip
|
||||
```
|
||||
|
||||
## Using sub-helmfile as a template
|
||||
|
||||
You can go even further by generalizing the product related releases as a pair of `api` and `web`:
|
||||
|
||||
`shared/helmfile.yaml`:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: product{{ env "PRODUCT_ID" }}-api
|
||||
chart: product{{ env "PRODUCT_ID" }}-charts/api
|
||||
# snip
|
||||
- name: product{{ env "PRODUCT_ID" }}-web
|
||||
chart: product{{ env "PRODUCT_ID" }}-charts/web
|
||||
# snip
|
||||
```
|
||||
|
||||
Then you only need one single product helmfile
|
||||
|
||||
|
||||
`product/helmfile.yaml`:
|
||||
|
||||
```yaml
|
||||
helmfiles:
|
||||
- ../observability/helmfile.yaml
|
||||
- ../shared/helmfile.yaml
|
||||
```
|
||||
|
||||
Now that we use the environment variable `PRODUCT_ID` to as the parameters of release names, you need to set it before running `helmfile`, so that it produces the differently named releases per product:
|
||||
|
||||
```console
|
||||
$ PRODUCT_ID=1 helmfile -f product/helmfile.yaml apply
|
||||
$ PRODUCT_ID=2 helmfile -f product/helmfile.yaml apply
|
||||
```
|
||||
+307
-21
@@ -2,33 +2,157 @@
|
||||
|
||||
This guide covers the Helmfile’s considered patterns for writing advanced helmfiles. It focuses on how helmfile should be structured and executed.
|
||||
|
||||
## Layering
|
||||
## Missing keys and Default values
|
||||
|
||||
helmfile tries its best to inform users for noticing potential mistakes.
|
||||
|
||||
One example of how helmfile achieves it is that, `helmfile` fails when you tried to access missing keys in environment values.
|
||||
|
||||
That is, the following example let `helmfile` fail when you have no `eventApi.replicas` defined in environment values.
|
||||
|
||||
```
|
||||
{{ .Values.eventApi.replicas | default 1 }}
|
||||
```
|
||||
|
||||
In case it isn't a mistake and you do want to allow missing keys, use the `get` template function:
|
||||
|
||||
```
|
||||
{{ .Values | get "eventApi.replicas" nil }}
|
||||
```
|
||||
|
||||
This result in printing `<no value` in your template, that may or may not result in a failure.
|
||||
|
||||
If you want a kind of default values that is used when a missing key was referenced, use `default` like:
|
||||
|
||||
```
|
||||
{{ .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 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`:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
# *snip*
|
||||
- name: heapster
|
||||
namespace: kube-system
|
||||
chart: stable/heapster
|
||||
version: 0.3.2
|
||||
values:
|
||||
- "./config/heapster/values.yaml"
|
||||
- "./config/heapster/{{ .Environment.Name }}.yaml"
|
||||
secrets:
|
||||
- "./config/heapster/secrets.yaml"
|
||||
- "./config/heapster/{{ .Environment.Name }}-secrets.yaml"
|
||||
|
||||
- name: kubernetes-dashboard
|
||||
namespace: kube-system
|
||||
chart: stable/kubernetes-dashboard
|
||||
version: 0.10.0
|
||||
values:
|
||||
- "./config/kubernetes-dashboard/values.yaml"
|
||||
- "./config/kubernetes-dashboard/{{ .Environment.Name }}.yaml"
|
||||
secrets:
|
||||
- "./config/kubernetes-dashboard/secrets.yaml"
|
||||
- "./config/kubernetes-dashboard/{{ .Environment.Name }}-secrets.yaml"
|
||||
```
|
||||
|
||||
This is where Helmfile's advanced feature called Release Template comes handy.
|
||||
|
||||
It allows you to abstract away the repetitions in releases into a template, which is then included and executed by using YAML anchor/alias:
|
||||
|
||||
```yaml
|
||||
templates:
|
||||
default: &default
|
||||
chart: stable/{{`{{ .Release.Name }}`}}
|
||||
namespace: kube-system
|
||||
# This prevents helmfile exiting when it encounters a missing file
|
||||
# Valid values are "Error", "Warn", "Info", "Debug". The default is "Error"
|
||||
# Use "Debug" to make missing files errors invisible at the default log level(--log-level=INFO)
|
||||
missingFileHandler: Warn
|
||||
values:
|
||||
- config/{{`{{ .Release.Name }}`}}/values.yaml
|
||||
- config/{{`{{ .Release.Name }}`}}/{{`{{ .Environment.Name }}`}}.yaml
|
||||
secrets:
|
||||
- config/{{`{{ .Release.Name }}`}}/secrets.yaml
|
||||
- config/{{`{{ .Release.Name }}`}}/{{`{{ .Environment.Name }}`}}-secrets.yaml
|
||||
|
||||
releases:
|
||||
- name: heapster
|
||||
<<: *default
|
||||
- name: kubernetes-dashboard
|
||||
<<: *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 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.
|
||||
|
||||
Use Layering to extract te common parts into a dedicated *library helmfile*s, so that each helmfile becomes DRY.
|
||||
Use Layering to extract the common parts into a dedicated *library helmfile*s, so that each helmfile becomes DRY.
|
||||
|
||||
Let's assume that your `helmfile.yaml` looks like:
|
||||
|
||||
```
|
||||
{ readFile "commons.yaml" }}
|
||||
---
|
||||
{{ readFile "environments.yaml" }}
|
||||
---
|
||||
bases:
|
||||
- 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:
|
||||
@@ -36,23 +160,24 @@ environments:
|
||||
production:
|
||||
```
|
||||
|
||||
At run time, template expressions in your `helmfile.yaml` are executed:
|
||||
At run time, `bases` in your `helmfile.yaml` are evaluated to produce:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: metricbaet
|
||||
chart: stable/metricbeat
|
||||
---
|
||||
# environments.yaml
|
||||
environments:
|
||||
development:
|
||||
production:
|
||||
---
|
||||
# helmfile.yaml
|
||||
releases:
|
||||
- name: myapp
|
||||
chart: mychart
|
||||
- name: metricbeat
|
||||
chart: stable/metricbeat
|
||||
```
|
||||
|
||||
Resulting YAML documents are merged in the order of occurrence,
|
||||
Finally the resulting YAML documents are merged in the order of occurrence,
|
||||
so that your `helmfile.yaml` becomes:
|
||||
|
||||
```yaml
|
||||
@@ -61,7 +186,7 @@ environments:
|
||||
production:
|
||||
|
||||
releases:
|
||||
- name: metricbaet
|
||||
- name: metricbeat
|
||||
chart: stable/metricbeat
|
||||
- name: myapp
|
||||
chart: mychart
|
||||
@@ -70,3 +195,164 @@ releases:
|
||||
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
|
||||
@@ -0,0 +1,36 @@
|
||||
module github.com/roboll/helmfile
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go v35.0.0+incompatible // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.0
|
||||
github.com/Masterminds/sprig/v3 v3.1.0
|
||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a
|
||||
github.com/go-test/deep v1.0.3
|
||||
github.com/golang/mock v1.4.3
|
||||
github.com/google/go-cmp v0.4.0
|
||||
github.com/gosuri/uitable v0.0.3
|
||||
github.com/hashicorp/go-getter v1.3.0
|
||||
github.com/hashicorp/go-retryablehttp v0.6.3 // indirect
|
||||
github.com/hashicorp/go-version v1.2.0
|
||||
github.com/howeyc/gopass v0.0.0-20190910152052-7cb4b85ec19c // indirect
|
||||
github.com/imdario/mergo v0.3.11
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/pierrec/lz4 v2.3.0+incompatible // indirect
|
||||
github.com/r3labs/diff v0.0.0-20190801153147-a71de73c46ad
|
||||
github.com/spf13/cobra v0.0.3
|
||||
github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939
|
||||
github.com/urfave/cli v1.20.0
|
||||
github.com/variantdev/chartify v0.4.4
|
||||
github.com/variantdev/dag v0.0.0-20191028002400-bb0b3c785363
|
||||
github.com/variantdev/vals v0.10.3
|
||||
go.uber.org/multierr v1.1.0
|
||||
go.uber.org/zap v1.9.1
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
|
||||
gopkg.in/square/go-jose.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
gotest.tools v2.2.0+incompatible
|
||||
gotest.tools/v3 v3.0.3-0.20200410202438-4e4a41b7851a
|
||||
k8s.io/apimachinery v0.0.0-20190409092423-760d1845f48b
|
||||
)
|
||||
Executable
+623
@@ -0,0 +1,623 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
PROG=semtag
|
||||
PROG_VERSION="v0.1.0"
|
||||
|
||||
SEMVER_REGEX="^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$"
|
||||
IDENTIFIER_REGEX="^\-([0-9A-Za-z-]+)\.([0-9A-Za-z-]+)*$"
|
||||
|
||||
# Global variables
|
||||
FIRST_VERSION="v0.0.0"
|
||||
finalversion=$FIRST_VERSION
|
||||
lastversion=$FIRST_VERSION
|
||||
hasversiontag="false"
|
||||
scope="patch"
|
||||
displayonly="false"
|
||||
forcetag="false"
|
||||
forcedversion=
|
||||
versionname=
|
||||
identifier=
|
||||
|
||||
HELP="\
|
||||
Usage:
|
||||
$PROG
|
||||
$PROG getlast
|
||||
$PROG getfinal
|
||||
$PROG (final|alpha|beta|candidate) [-s <scope> (major|minor|patch|auto) | -o]
|
||||
$PROG --help
|
||||
$PROG --version
|
||||
Options:
|
||||
-s The scope that must be increased, can be major, minor or patch.
|
||||
The resulting version will match X.Y.Z(-PRERELEASE)(+BUILD)
|
||||
where X, Y and Z are positive integers, PRERELEASE is an optionnal
|
||||
string composed of alphanumeric characters describing if the build is
|
||||
a release candidate, alpha or beta version, with a number.
|
||||
BUILD is also an optional string composed of alphanumeric
|
||||
characters and hyphens.
|
||||
Setting the scope as 'auto', the script will chose the scope between
|
||||
'minor' and 'patch', depending on the amount of lines added (<10% will
|
||||
choose patch).
|
||||
-v Specifies manually the version to be tagged, must be a valid semantic version
|
||||
in the format X.Y.Z where X, Y and Z are positive integers.
|
||||
-o Output the version only, shows the bumped version, but doesn't tag.
|
||||
-f Forces to tag, even if there are unstaged or uncommited changes.
|
||||
Commands:
|
||||
--help Print this help message.
|
||||
--version Prints the program's version.
|
||||
get Returns both current final version and last tagged version.
|
||||
getlast Returns the latest tagged version.
|
||||
getfinal Returns the latest tagged final version.
|
||||
getcurrent Returns the current version, based on the latest one, if there are uncommited or
|
||||
unstaged changes, they will be reflected in the version, adding the number of
|
||||
pending commits, current branch and commit hash.
|
||||
final Tags the current build as a final version, this only can be done on the master branch.
|
||||
candidate Tags the current build as a release candidate, the tag will contain all
|
||||
the commits from the last final version.
|
||||
alpha Tags the current build as an alpha version, the tag will contain all
|
||||
the commits from the last final version.
|
||||
beta Tags the current build as a beta version, the tag will contain all
|
||||
the commits from the last final version."
|
||||
|
||||
# Commands and options
|
||||
ACTION="getlast"
|
||||
ACTION="$1"
|
||||
shift
|
||||
|
||||
# We get the parameters
|
||||
while getopts "v:s:of" opt; do
|
||||
case $opt in
|
||||
v)
|
||||
forcedversion="$OPTARG"
|
||||
;;
|
||||
s)
|
||||
scope="$OPTARG"
|
||||
;;
|
||||
o)
|
||||
displayonly="true"
|
||||
;;
|
||||
f)
|
||||
forcetag="true"
|
||||
;;
|
||||
\?)
|
||||
echo "Invalid option: -$OPTARG" >&2
|
||||
exit 1
|
||||
;;
|
||||
:)
|
||||
echo "Option -$OPTARG requires an argument." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Gets a string with the version and returns an array of maximum size of 5 with all the parts of the sematinc version
|
||||
# $1 The string containing the version in semantic format
|
||||
# $2 The variable to store the result array:
|
||||
# position 0: major number
|
||||
# position 1: minor number
|
||||
# position 2: patch number
|
||||
# position 3: identifier (or prerelease identifier)
|
||||
# position 4: build info
|
||||
function explode_version {
|
||||
local __version=$1
|
||||
local __result=$2
|
||||
if [[ $__version =~ $SEMVER_REGEX ]] ; then
|
||||
local __major=${BASH_REMATCH[1]}
|
||||
local __minor=${BASH_REMATCH[2]}
|
||||
local __patch=${BASH_REMATCH[3]}
|
||||
local __prere=${BASH_REMATCH[4]}
|
||||
local __build=${BASH_REMATCH[5]}
|
||||
eval "$__result=(\"$__major\" \"$__minor\" \"$__patch\" \"$__prere\" \"$__build\")"
|
||||
else
|
||||
eval "$__result="
|
||||
fi
|
||||
}
|
||||
|
||||
# Compare two versions and returns -1, 0 or 1
|
||||
# $1 The first version to compare
|
||||
# $2 The second version to compare
|
||||
# $3 The variable where to store the result
|
||||
function compare_versions {
|
||||
local __first
|
||||
local __second
|
||||
explode_version $1 __first
|
||||
explode_version $2 __second
|
||||
local lv=$3
|
||||
|
||||
# Compares MAJOR, MINOR and PATCH
|
||||
for i in 0 1 2; do
|
||||
local __numberfirst=${__first[$i]}
|
||||
local __numbersecond=${__second[$i]}
|
||||
case $(($__numberfirst - $__numbersecond)) in
|
||||
0)
|
||||
;;
|
||||
-[0-9]*)
|
||||
eval "$lv=-1"
|
||||
return 0
|
||||
;;
|
||||
[0-9]*)
|
||||
eval "$lv=1"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Identifiers should compare with the ASCII order.
|
||||
local __identifierfirst=${__first[3]}
|
||||
local __identifiersecond=${__second[3]}
|
||||
if [[ -n "$__identifierfirst" ]] && [[ -n "$__identifiersecond" ]]; then
|
||||
if [[ "$__identifierfirst" > "$__identifiersecond" ]]; then
|
||||
eval "$lv=1"
|
||||
return 0
|
||||
elif [[ "$__identifierfirst" < "$__identifiersecond" ]]; then
|
||||
eval "$lv=-1"
|
||||
return 0
|
||||
fi
|
||||
elif [[ -z "$__identifierfirst" ]] && [[ -n "$__identifiersecond" ]]; then
|
||||
eval "$lv=1"
|
||||
return 0
|
||||
elif [[ -n "$__identifierfirst" ]] && [[ -z "$__identifiersecond" ]]; then
|
||||
eval "$lv=-1"
|
||||
return 0
|
||||
fi
|
||||
|
||||
eval "$lv=0"
|
||||
}
|
||||
|
||||
# Returns the last version of two
|
||||
# $1 The first version to compare
|
||||
# $2 The second version to compare
|
||||
# $3 The variable where to store the last one
|
||||
function get_latest_of_two {
|
||||
local __first=$1
|
||||
local __second=$2
|
||||
local __result
|
||||
local __latest=$3
|
||||
compare_versions $__first $__second __result
|
||||
case $__result in
|
||||
0)
|
||||
eval "$__latest=$__second"
|
||||
;;
|
||||
-1)
|
||||
eval "$__latest=$__second"
|
||||
;;
|
||||
1)
|
||||
eval "$__latest=$__first"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Assigns a 2 size array with the identifier, having the identifier at pos 0, and the number in pos 1
|
||||
# $1 The identifier in the format -id.#
|
||||
# $2 The vferiable where to store the 2 size array
|
||||
function explode_identifier {
|
||||
local __identifier=$1
|
||||
local __result=$2
|
||||
if [[ $__identifier =~ $IDENTIFIER_REGEX ]] ; then
|
||||
local __id=${BASH_REMATCH[1]}
|
||||
local __number=${BASH_REMATCH[2]}
|
||||
if [[ -z "$__number" ]]; then
|
||||
__number=1
|
||||
fi
|
||||
eval "$__result=(\"$__id\" \"$__number\")"
|
||||
else
|
||||
eval "$__result="
|
||||
fi
|
||||
}
|
||||
|
||||
# Gets a list of tags and assigns the base and latest versions
|
||||
# Receives an array with the tags containing the versions
|
||||
# Assigns to the global variables finalversion and lastversion the final version and the latest version
|
||||
function get_latest {
|
||||
local __taglist=("$@")
|
||||
local __tagsnumber=${#__taglist[@]}
|
||||
local __current
|
||||
case $__tagsnumber in
|
||||
0)
|
||||
finalversion=$FIRST_VERSION
|
||||
lastversion=$FIRST_VERSION
|
||||
;;
|
||||
1)
|
||||
__current=${__taglist[0]}
|
||||
explode_version $__current ver
|
||||
if [ -n "$ver" ]; then
|
||||
if [ -n "${ver[3]}" ]; then
|
||||
finalversion=$FIRST_VERSION
|
||||
else
|
||||
finalversion=$__current
|
||||
fi
|
||||
lastversion=$__current
|
||||
else
|
||||
finalversion=$FIRST_VERSION
|
||||
lastversion=$FIRST_VERSION
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
local __lastpos=$(($__tagsnumber-1))
|
||||
for i in $(seq 0 $__lastpos)
|
||||
do
|
||||
__current=${__taglist[i]}
|
||||
explode_version ${__taglist[i]} ver
|
||||
if [ -n "$ver" ]; then
|
||||
if [ -z "${ver[3]}" ]; then
|
||||
get_latest_of_two $finalversion $__current finalversion
|
||||
get_latest_of_two $lastversion $finalversion lastversion
|
||||
else
|
||||
get_latest_of_two $lastversion $__current lastversion
|
||||
fi
|
||||
fi
|
||||
done
|
||||
;;
|
||||
esac
|
||||
|
||||
if git rev-parse -q --verify "refs/tags/$lastversion" >/dev/null; then
|
||||
hasversiontag="true"
|
||||
else
|
||||
hasversiontag="false"
|
||||
fi
|
||||
}
|
||||
|
||||
# Gets the next version given the provided scope
|
||||
# $1 The version that is going to be bumped
|
||||
# $2 The scope to bump
|
||||
# $3 The variable where to stoer the result
|
||||
function get_next_version {
|
||||
local __exploded
|
||||
local __fromversion=$1
|
||||
local __scope=$2
|
||||
local __result=$3
|
||||
explode_version $__fromversion __exploded
|
||||
case $__scope in
|
||||
major)
|
||||
__exploded[0]=$((${__exploded[0]}+1))
|
||||
__exploded[1]=0
|
||||
__exploded[2]=0
|
||||
;;
|
||||
minor)
|
||||
__exploded[1]=$((${__exploded[1]}+1))
|
||||
__exploded[2]=0
|
||||
;;
|
||||
patch)
|
||||
__exploded[2]=$((${__exploded[2]}+1))
|
||||
;;
|
||||
esac
|
||||
|
||||
eval "$__result=v${__exploded[0]}.${__exploded[1]}.${__exploded[2]}"
|
||||
}
|
||||
|
||||
function bump_version {
|
||||
## First we try to get the next version based on the existing last one
|
||||
if [ "$scope" == "auto" ]; then
|
||||
get_scope_auto scope
|
||||
fi
|
||||
|
||||
local __candidatefromlast=$FIRST_VERSION
|
||||
local __explodedlast
|
||||
explode_version $lastversion __explodedlast
|
||||
if [[ -n "${__explodedlast[3]}" ]]; then
|
||||
# Last version is not final
|
||||
local __idlast
|
||||
explode_identifier ${__explodedlast[3]} __idlast
|
||||
|
||||
# We get the last, given the desired id based on the scope
|
||||
__candidatefromlast="v${__explodedlast[0]}.${__explodedlast[1]}.${__explodedlast[2]}"
|
||||
if [[ -n "$identifier" ]]; then
|
||||
local __nextid="$identifier.1"
|
||||
if [ "$identifier" == "${__idlast[0]}" ]; then
|
||||
# We target the same identifier as the last so we increase one
|
||||
__nextid="$identifier.$(( ${__idlast[1]}+1 ))"
|
||||
__candidatefromlast="$__candidatefromlast-$__nextid"
|
||||
else
|
||||
# Different identifiers, we make sure we are assigning a higher identifier, if not, we increase the version
|
||||
__candidatefromlast="$__candidatefromlast-$__nextid"
|
||||
local __comparedwithlast
|
||||
compare_versions $__candidatefromlast $lastversion __comparedwithlast
|
||||
if [ "$__comparedwithlast" == -1 ]; then
|
||||
get_next_version $__candidatefromlast $scope __candidatefromlast
|
||||
__candidatefromlast="$__candidatefromlast-$__nextid"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Then we try to get the version based on the latest final one
|
||||
local __candidatefromfinal=$FIRST_VERSION
|
||||
get_next_version $finalversion $scope __candidatefromfinal
|
||||
if [[ -n "$identifier" ]]; then
|
||||
__candidatefromfinal="$__candidatefromfinal-$identifier.1"
|
||||
fi
|
||||
|
||||
# Finally we compare both candidates
|
||||
local __resultversion
|
||||
local __result
|
||||
compare_versions $__candidatefromlast $__candidatefromfinal __result
|
||||
case $__result in
|
||||
0)
|
||||
__resultversion=$__candidatefromlast
|
||||
;;
|
||||
-1)
|
||||
__resultversion="$__candidatefromfinal"
|
||||
;;
|
||||
1)
|
||||
__resultversion=$__candidatefromlast
|
||||
;;
|
||||
esac
|
||||
|
||||
eval "$1=$__resultversion"
|
||||
}
|
||||
|
||||
function increase_version {
|
||||
local __version=
|
||||
|
||||
if [ -z $forcedversion ]; then
|
||||
bump_version __version
|
||||
else
|
||||
if [[ $forcedversion =~ $SEMVER_REGEX ]] ; then
|
||||
compare_versions $forcedversion $lastversion __result
|
||||
if [ $__result -le 0 ]; then
|
||||
echo "Version can't be lower than last version: $lastversion"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Non valid version to bump"
|
||||
exit 1
|
||||
fi
|
||||
__version=$forcedversion
|
||||
fi
|
||||
|
||||
if [ "$displayonly" == "true" ]; then
|
||||
echo "$__version"
|
||||
else
|
||||
if [ "$forcetag" == "false" ]; then
|
||||
check_git_dirty_status
|
||||
fi
|
||||
local __commitlist
|
||||
if [ "$finalversion" == "$FIRST_VERSION" ] || [ "$hasversiontag" != "true" ]; then
|
||||
__commitlist="$(git log --pretty=oneline | cat)"
|
||||
else
|
||||
__commitlist="$(git log --pretty=oneline $finalversion... | cat)"
|
||||
fi
|
||||
|
||||
# If we are forcing a bump, we add bump to the commit list
|
||||
if [[ -z $__commitlist && "$forcetag" == "true" ]]; then
|
||||
__commitlist="bump"
|
||||
fi
|
||||
|
||||
if [[ -z $__commitlist ]]; then
|
||||
echo "No commits since the last final version, not bumping version"
|
||||
else
|
||||
if [[ -z $versionname ]]; then
|
||||
versionname=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
fi
|
||||
local __message="$versionname
|
||||
$__commitlist"
|
||||
|
||||
# We check we have info on the user
|
||||
local __username=$(git config user.name)
|
||||
if [ -z "$__username" ]; then
|
||||
__username=$(id -u -n)
|
||||
git config user.name $__username
|
||||
fi
|
||||
local __useremail=$(git config user.email)
|
||||
if [ -z "$__useremail" ]; then
|
||||
__useremail=$(hostname)
|
||||
git config user.email "$__username@$__useremail"
|
||||
fi
|
||||
|
||||
git tag -a $__version -m "$__message"
|
||||
|
||||
# If we have a remote, we push there
|
||||
local __remote=${SEMTAG_REMOTE:-$(git remote)}
|
||||
if [[ -n $__remote ]]; then
|
||||
git push $__remote $__version > /dev/null
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "$__version"
|
||||
else
|
||||
echo "Error pushing the tag $__version to $__remote"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "$__version"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function check_git_dirty_status {
|
||||
local __repostatus=
|
||||
get_work_tree_status __repostatus
|
||||
|
||||
if [ "$__repostatus" == "uncommitted" ]; then
|
||||
echo "ERROR: You have uncommitted changes"
|
||||
git status --porcelain
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$__repostatus" == "unstaged" ]; then
|
||||
echo "ERROR: You have unstaged changes"
|
||||
git status --porcelain
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Get the total amount of lines of code in the repo
|
||||
function get_total_lines {
|
||||
local __empty_id="$(git hash-object -t tree /dev/null)"
|
||||
local __changes="$(git diff --numstat $__empty_id | cat)"
|
||||
local __added_deleted=$1
|
||||
get_changed_lines "$__changes" $__added_deleted
|
||||
}
|
||||
|
||||
# Get the total amount of lines of code since the provided tag
|
||||
function get_sincetag_lines {
|
||||
local __sincetag=$1
|
||||
local __changes="$(git diff --numstat $__sincetag | cat)"
|
||||
local __added_deleted=$2
|
||||
get_changed_lines "$__changes" $__added_deleted
|
||||
}
|
||||
|
||||
function get_changed_lines {
|
||||
local __changes_numstat=$1
|
||||
local __result=$2
|
||||
IFS=$'\n' read -rd '' -a __changes_array <<<"$__changes_numstat"
|
||||
local __diff_regex="^([0-9]+)[[:space:]]+([0-9]+)[[:space:]]+.+$"
|
||||
|
||||
local __total_added=0
|
||||
local __total_deleted=0
|
||||
for i in "${__changes_array[@]}"
|
||||
do
|
||||
if [[ $i =~ $__diff_regex ]] ; then
|
||||
local __added=${BASH_REMATCH[1]}
|
||||
local __deleted=${BASH_REMATCH[2]}
|
||||
__total_added=$(( $__total_added+$__added ))
|
||||
__total_deleted=$(( $__total_deleted+$__deleted ))
|
||||
fi
|
||||
done
|
||||
eval "$2=( $__total_added $__total_deleted )"
|
||||
}
|
||||
|
||||
function get_scope_auto {
|
||||
local __verbose=$2
|
||||
local __total=0
|
||||
local __since=0
|
||||
local __scope=
|
||||
|
||||
get_total_lines __total
|
||||
get_sincetag_lines $finalversion __since
|
||||
|
||||
local __percentage=0
|
||||
if [ "$__total" != "0" ]; then
|
||||
local __percentage=$(( 100*$__since/$__total ))
|
||||
if [ $__percentage -gt "10" ]; then
|
||||
__scope="minor"
|
||||
else
|
||||
__scope="patch"
|
||||
fi
|
||||
fi
|
||||
|
||||
eval "$1=$__scope"
|
||||
if [[ -n "$__verbose" ]]; then
|
||||
echo "[Auto Scope] Percentage of lines changed: $__percentage"
|
||||
echo "[Auto Scope] : $__scope"
|
||||
fi
|
||||
}
|
||||
|
||||
function get_work_tree_status {
|
||||
# Update the index
|
||||
git update-index -q --ignore-submodules --refresh > /dev/null
|
||||
eval "$1="
|
||||
|
||||
if ! git diff-files --quiet --ignore-submodules -- > /dev/null
|
||||
then
|
||||
eval "$1=unstaged"
|
||||
fi
|
||||
|
||||
if ! git diff-index --cached --quiet HEAD --ignore-submodules -- > /dev/null
|
||||
then
|
||||
eval "$1=uncommitted"
|
||||
fi
|
||||
}
|
||||
|
||||
function get_current {
|
||||
if [ "$hasversiontag" == "true" ]; then
|
||||
local __commitcount="$(git rev-list $lastversion.. --count)"
|
||||
else
|
||||
local __commitcount="$(git rev-list --count HEAD)"
|
||||
fi
|
||||
local __status=
|
||||
get_work_tree_status __status
|
||||
|
||||
if [ "$__commitcount" == "0" ] && [ -z "$__status" ]; then
|
||||
eval "$1=$lastversion"
|
||||
else
|
||||
local __buildinfo="$(git rev-parse --short HEAD)"
|
||||
local __currentbranch="$(git rev-parse --abbrev-ref HEAD)"
|
||||
if [ "$__currentbranch" != "master" ]; then
|
||||
__buildinfo="$__currentbranch.$__buildinfo"
|
||||
fi
|
||||
|
||||
local __suffix=
|
||||
if [ "$__commitcount" != "0" ]; then
|
||||
if [ -n "$__suffix" ]; then
|
||||
__suffix="$__suffix."
|
||||
fi
|
||||
__suffix="$__suffix$__commitcount"
|
||||
fi
|
||||
if [ -n "$__status" ]; then
|
||||
if [ -n "$__suffix" ]; then
|
||||
__suffix="$__suffix."
|
||||
fi
|
||||
__suffix="$__suffix$__status"
|
||||
fi
|
||||
|
||||
__suffix="$__suffix+$__buildinfo"
|
||||
if [ "$lastversion" == "$finalversion" ]; then
|
||||
scope="patch"
|
||||
identifier=
|
||||
local __bumped=
|
||||
bump_version __bumped
|
||||
eval "$1=$__bumped-dev.$__suffix"
|
||||
else
|
||||
eval "$1=$lastversion.$__suffix"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function init {
|
||||
git fetch > /dev/null
|
||||
TAGS="$(git tag)"
|
||||
IFS=$'\n' read -rd '' -a TAG_ARRAY <<<"$TAGS"
|
||||
|
||||
get_latest ${TAG_ARRAY[@]}
|
||||
currentbranch="$(git rev-parse --abbrev-ref HEAD)"
|
||||
}
|
||||
|
||||
case $ACTION in
|
||||
--help)
|
||||
echo -e "$HELP"
|
||||
;;
|
||||
--version)
|
||||
echo -e "${PROG}: $PROG_VERSION"
|
||||
;;
|
||||
final)
|
||||
init
|
||||
diff=$(git diff master | cat)
|
||||
if [ "$forcetag" == "false" ]; then
|
||||
if [ -n "$diff" ]; then
|
||||
echo "ERROR: Branch must be updated with master for final versions"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
increase_version
|
||||
;;
|
||||
alpha|beta)
|
||||
init
|
||||
identifier="$ACTION"
|
||||
increase_version
|
||||
;;
|
||||
candidate)
|
||||
init
|
||||
identifier="rc"
|
||||
increase_version
|
||||
;;
|
||||
getlast)
|
||||
init
|
||||
echo "$lastversion"
|
||||
;;
|
||||
getfinal)
|
||||
init
|
||||
echo "$finalversion"
|
||||
;;
|
||||
getcurrent)
|
||||
init
|
||||
get_current current
|
||||
echo "$current"
|
||||
;;
|
||||
get)
|
||||
init
|
||||
echo "Current final version: $finalversion"
|
||||
echo "Last tagged version: $lastversion"
|
||||
;;
|
||||
*)
|
||||
echo "'$ACTION' is not a valid command, see --help for available commands."
|
||||
;;
|
||||
esac
|
||||
@@ -1,204 +0,0 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"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{},
|
||||
}
|
||||
}
|
||||
|
||||
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...)
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) UpdateRepo() error {
|
||||
helm.logger.Info("Updating repo")
|
||||
out, err := helm.exec("repo", "update")
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) UpdateDeps(chart string) error {
|
||||
helm.logger.Infof("Updating dependency %v", chart)
|
||||
out, err := helm.exec("dependency", "update", chart)
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) SyncRelease(name, chart string, flags ...string) error {
|
||||
helm.logger.Infof("Upgrading %v", chart)
|
||||
out, err := helm.exec(append([]string{"upgrade", "--install", "--reset-values", name, chart}, flags...)...)
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) ReleaseStatus(name string) error {
|
||||
helm.logger.Infof("Getting status %v", name)
|
||||
out, err := helm.exec(append([]string{"status", name})...)
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) DecryptSecret(name 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)
|
||||
out, err := helm.exec(append([]string{"secrets", "dec", name})...)
|
||||
helm.write(out)
|
||||
|
||||
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...)...)
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) DiffRelease(name, chart string, flags ...string) error {
|
||||
helm.logger.Infof("Comparing %v %v", name, chart)
|
||||
out, err := helm.exec(append([]string{"diff", "upgrade", "--allow-unreleased", name, chart}, flags...)...)
|
||||
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...)...)
|
||||
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...)...)
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) DeleteRelease(name string, flags ...string) error {
|
||||
helm.logger.Infof("Deleting %v", name)
|
||||
out, err := helm.exec(append([]string{"delete", name}, flags...)...)
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) TestRelease(name string, flags ...string) error {
|
||||
helm.logger.Infof("Testing %v", name)
|
||||
out, err := helm.exec(append([]string{"test", name}, flags...)...)
|
||||
helm.write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) exec(args ...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)
|
||||
}
|
||||
helm.logger.Debugf("exec: %s %s", helm.helmBinary, strings.Join(cmdargs, " "))
|
||||
return helm.runner.Execute(helm.helmBinary, cmdargs)
|
||||
}
|
||||
|
||||
func (helm *execer) write(out []byte) {
|
||||
if len(out) > 0 {
|
||||
helm.logger.Infof("%s", out)
|
||||
}
|
||||
}
|
||||
@@ -1,355 +0,0 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Mocking the command-line runner
|
||||
|
||||
type mockRunner struct {
|
||||
output []byte
|
||||
err error
|
||||
}
|
||||
|
||||
func (mock *mockRunner) Execute(cmd string, args []string) ([]byte, error) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer {
|
||||
execer := New(logger, kubeContext)
|
||||
execer.runner = &mockRunner{}
|
||||
return execer
|
||||
}
|
||||
|
||||
// Test methods
|
||||
|
||||
func TestNewHelmExec(t *testing.T) {
|
||||
buffer := bytes.NewBufferString("something")
|
||||
logger := NewLogger(buffer, "debug")
|
||||
helm := New(logger, "dev")
|
||||
if helm.kubeContext != "dev" {
|
||||
t.Error("helmexec.New() - kubeContext")
|
||||
}
|
||||
if buffer.String() != "something" {
|
||||
t.Error("helmexec.New() - changed buffer")
|
||||
}
|
||||
if len(helm.extra) != 0 {
|
||||
t.Error("helmexec.New() - extra args not empty")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SetExtraArgs(t *testing.T) {
|
||||
helm := New(NewLogger(os.Stdout, "info"), "dev")
|
||||
helm.SetExtraArgs()
|
||||
if len(helm.extra) != 0 {
|
||||
t.Error("helmexec.SetExtraArgs() - passing no arguments should not change extra field")
|
||||
}
|
||||
helm.SetExtraArgs("foo")
|
||||
if !reflect.DeepEqual(helm.extra, []string{"foo"}) {
|
||||
t.Error("helmexec.SetExtraArgs() - one extra argument missing")
|
||||
}
|
||||
helm.SetExtraArgs("alpha", "beta")
|
||||
if !reflect.DeepEqual(helm.extra, []string{"alpha", "beta"}) {
|
||||
t.Error("helmexec.SetExtraArgs() - two extra arguments missing (overwriting the previous value)")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SetHelmBinary(t *testing.T) {
|
||||
helm := New(NewLogger(os.Stdout, "info"), "dev")
|
||||
if helm.helmBinary != "helm" {
|
||||
t.Error("helmexec.command - default command is not helm")
|
||||
}
|
||||
helm.SetHelmBinary("foo")
|
||||
if helm.helmBinary != "foo" {
|
||||
t.Errorf("helmexec.SetHelmBinary() - actual = %s expect = foo", helm.helmBinary)
|
||||
}
|
||||
}
|
||||
|
||||
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", "", "")
|
||||
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
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "", "")
|
||||
expected = `Adding repo myRepo https://repo.example.com/
|
||||
exec: helm repo add myRepo https://repo.example.com/ --kube-context dev
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "", "", "example_user", "example_password")
|
||||
expected = `Adding repo myRepo https://repo.example.com/
|
||||
exec: helm repo add myRepo https://repo.example.com/ --username example_user --password example_password --kube-context dev
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_UpdateRepo(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.UpdateRepo()
|
||||
expected := `Updating repo
|
||||
exec: helm repo update --kube-context dev
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.UpdateRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SyncRelease(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.SyncRelease("release", "chart", "--timeout 10", "--wait")
|
||||
expected := `Upgrading chart
|
||||
exec: helm upgrade --install --reset-values release chart --timeout 10 --wait --kube-context dev
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.SyncRelease("release", "chart")
|
||||
expected = `Upgrading chart
|
||||
exec: helm upgrade --install --reset-values release chart --kube-context dev
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.SyncRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_UpdateDeps(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.UpdateDeps("./chart/foo")
|
||||
expected := `Updating dependency ./chart/foo
|
||||
exec: helm dependency update ./chart/foo --kube-context dev
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.UpdateDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.SetExtraArgs("--verify")
|
||||
helm.UpdateDeps("./chart/foo")
|
||||
expected = `Updating dependency ./chart/foo
|
||||
exec: helm dependency update ./chart/foo --verify --kube-context dev
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DecryptSecret(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.DecryptSecret("secretName")
|
||||
expected := `Decrypting secret secretName
|
||||
exec: helm secrets dec secretName --kube-context dev
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DecryptSecret()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DiffRelease(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.DiffRelease("release", "chart", "--timeout 10", "--wait")
|
||||
expected := `Comparing release chart
|
||||
exec: helm diff upgrade --allow-unreleased release chart --timeout 10 --wait --kube-context dev
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.DiffRelease("release", "chart")
|
||||
expected = `Comparing release chart
|
||||
exec: helm diff upgrade --allow-unreleased release chart --kube-context dev
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DiffRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DeleteRelease(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.DeleteRelease("release")
|
||||
expected := `Deleting release
|
||||
exec: helm delete release --kube-context dev
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
func Test_DeleteRelease_Flags(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.DeleteRelease("release", "--purge")
|
||||
expected := `Deleting release
|
||||
exec: helm delete release --purge --kube-context dev
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_TestRelease(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.TestRelease("release")
|
||||
expected := `Testing release
|
||||
exec: helm test release --kube-context dev
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
func Test_TestRelease_Flags(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.TestRelease("release", "--cleanup", "--timeout", "60")
|
||||
expected := `Testing release
|
||||
exec: helm test release --cleanup --timeout 60 --kube-context dev
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ReleaseStatus(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.ReleaseStatus("myRelease")
|
||||
expected := `Getting status myRelease
|
||||
exec: helm status myRelease --kube-context dev
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.ReleaseStatus()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_exec(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "")
|
||||
helm.exec("version")
|
||||
expected := "exec: helm version\n"
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
helm = MockExecer(logger, "dev")
|
||||
ret, _ := helm.exec("diff")
|
||||
if len(ret) != 0 {
|
||||
t.Error("helmexec.exec() - expected empty return value")
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm = MockExecer(logger, "dev")
|
||||
helm.exec("diff", "release", "chart", "--timeout 10", "--wait")
|
||||
expected = "exec: helm diff release chart --timeout 10 --wait --kube-context dev\n"
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.exec("version")
|
||||
expected = "exec: helm version --kube-context dev\n"
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.SetExtraArgs("foo")
|
||||
helm.exec("version")
|
||||
expected = "exec: helm version foo --kube-context dev\n"
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm = MockExecer(logger, "")
|
||||
helm.SetHelmBinary("overwritten")
|
||||
helm.exec("version")
|
||||
expected = "exec: overwritten version\n"
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Fetch(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
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
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
`,
|
||||
"info": `Adding repo myRepo https://repo.example.com/
|
||||
`,
|
||||
"warn": ``,
|
||||
}
|
||||
|
||||
func Test_LogLevels(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
for logLevel, expected := range logLevelTests {
|
||||
buffer.Reset()
|
||||
logger := NewLogger(&buffer, logLevel)
|
||||
helm := MockExecer(logger, "")
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package helmexec
|
||||
|
||||
// Interface for executing helm commands
|
||||
type Interface interface {
|
||||
SetExtraArgs(args ...string)
|
||||
SetHelmBinary(bin string)
|
||||
|
||||
AddRepo(name, repository, certfile, keyfile, username, password string) error
|
||||
UpdateRepo() error
|
||||
UpdateDeps(chart string) error
|
||||
SyncRelease(name, chart string, flags ...string) error
|
||||
DiffRelease(name, chart string, flags ...string) error
|
||||
TemplateRelease(chart string, flags ...string) error
|
||||
Fetch(chart string, flags ...string) error
|
||||
Lint(chart string, flags ...string) error
|
||||
ReleaseStatus(name string) error
|
||||
DeleteRelease(name string, flags ...string) error
|
||||
TestRelease(name string, flags ...string) error
|
||||
|
||||
DecryptSecret(name string) (string, error)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
const (
|
||||
tmpPrefix = "helmfile-"
|
||||
tmpSuffix = "-exec"
|
||||
)
|
||||
|
||||
// Runner interface for shell commands
|
||||
type Runner interface {
|
||||
Execute(cmd string, args []string) ([]byte, error)
|
||||
}
|
||||
|
||||
// ShellRunner implemention for shell commands
|
||||
type ShellRunner struct{}
|
||||
|
||||
// Execute a shell command
|
||||
func (shell ShellRunner) Execute(cmd string, args []string) ([]byte, error) {
|
||||
preparedCmd := exec.Command(cmd, args...)
|
||||
return preparedCmd.CombinedOutput()
|
||||
}
|
||||
@@ -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}"
|
||||
+1629
File diff suppressed because it is too large
Load Diff
+4238
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,171 @@
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
OutputDir() string
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultHelmfile = "helmfile.yaml"
|
||||
DeprecatedHelmfile = "charts.yaml"
|
||||
DefaultHelmfileDirectory = "helmfile.d"
|
||||
ExperimentalEnvVar = "HELMFILE_EXPERIMENTAL" // environment variable for experimental features, expecting "true" lower case
|
||||
ExperimentalSelectorExplicit = "explicit-selector-inheritance" // value to remove default selector inheritance to sub-helmfiles and use the explicit one
|
||||
)
|
||||
|
||||
func experimentalModeEnabled() bool {
|
||||
return os.Getenv(ExperimentalEnvVar) == "true"
|
||||
}
|
||||
|
||||
func isExplicitSelectorInheritanceEnabled() bool {
|
||||
return experimentalModeEnabled() || strings.Contains(os.Getenv(ExperimentalEnvVar), ExperimentalSelectorExplicit)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/env"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsExplicitSelectorInheritanceEnabled(t *testing.T) {
|
||||
//env var ExperimentalEnvVar is set
|
||||
assert.Assert(t, is.Equal(os.Getenv(ExperimentalEnvVar), ""))
|
||||
assert.Check(t, !isExplicitSelectorInheritanceEnabled())
|
||||
|
||||
//check for env var ExperimentalEnvVar set to true
|
||||
defer env.Patch(t, ExperimentalEnvVar, "true")()
|
||||
assert.Check(t, isExplicitSelectorInheritanceEnabled())
|
||||
|
||||
//check for env var ExperimentalEnvVar set to anything
|
||||
defer env.Patch(t, ExperimentalEnvVar, "foo")()
|
||||
assert.Check(t, !isExplicitSelectorInheritanceEnabled())
|
||||
|
||||
//check for env var ExperimentalEnvVar set to ExperimentalSelectorExplicit
|
||||
defer env.Patch(t, ExperimentalEnvVar, ExperimentalSelectorExplicit)()
|
||||
assert.Check(t, isExplicitSelectorInheritanceEnabled())
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
updatedRepos map[string]bool
|
||||
}
|
||||
|
||||
func NewContext() Context {
|
||||
return Context{
|
||||
updatedRepos: map[string]bool{},
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx Context) SyncReposOnce(st *state.HelmState, helm state.RepoUpdater) error {
|
||||
updated, err := st.SyncRepos(helm, ctx.updatedRepos)
|
||||
|
||||
for _, r := range updated {
|
||||
ctx.updatedRepos[r] = true
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/remote"
|
||||
"github.com/roboll/helmfile/pkg/state"
|
||||
"github.com/variantdev/vals"
|
||||
"go.uber.org/zap"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultHelmBinary = state.DefaultHelmBinary
|
||||
)
|
||||
|
||||
type desiredStateLoader struct {
|
||||
overrideKubeContext string
|
||||
overrideHelmBinary string
|
||||
|
||||
env string
|
||||
namespace string
|
||||
|
||||
readFile func(string) ([]byte, error)
|
||||
deleteFile func(string) error
|
||||
fileExists func(string) (bool, error)
|
||||
abs func(string) (string, error)
|
||||
glob func(string) ([]string, error)
|
||||
getHelm func(*state.HelmState) helmexec.Interface
|
||||
|
||||
remote *remote.Remote
|
||||
logger *zap.SugaredLogger
|
||||
valsRuntime vals.Evaluator
|
||||
}
|
||||
|
||||
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(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
|
||||
} else {
|
||||
f = filepath.Join(baseDir, file)
|
||||
}
|
||||
|
||||
fileBytes, err := ld.readFile(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ext := filepath.Ext(f)
|
||||
|
||||
var self *state.HelmState
|
||||
|
||||
if !experimentalModeEnabled() || ext == ".gotmpl" {
|
||||
self, err = ld.renderAndLoad(
|
||||
inheritedEnv,
|
||||
overrodeEnv,
|
||||
baseDir,
|
||||
f,
|
||||
fileBytes,
|
||||
evaluateBases,
|
||||
)
|
||||
} else {
|
||||
self, err = ld.load(
|
||||
fileBytes,
|
||||
baseDir,
|
||||
file,
|
||||
evaluateBases,
|
||||
inheritedEnv,
|
||||
overrodeEnv,
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, h := range self.Helmfiles {
|
||||
if h.Path == f {
|
||||
return nil, fmt.Errorf("%s contains a recursion into the same sub-helmfile at helmfiles[%d]", f, i)
|
||||
}
|
||||
if h.Path == "." {
|
||||
return nil, fmt.Errorf("%s contains a recursion into the the directory containing this helmfile at helmfiles[%d]", f, i)
|
||||
}
|
||||
}
|
||||
|
||||
return self, nil
|
||||
}
|
||||
|
||||
func (a *desiredStateLoader) underlying() *state.StateCreator {
|
||||
c := state.NewCreator(a.logger, a.readFile, a.fileExists, a.abs, a.glob, a.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
|
||||
|
||||
return st, nil
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
for i, part := range parts {
|
||||
var yamlBuf *bytes.Buffer
|
||||
var err error
|
||||
|
||||
id := fmt.Sprintf("%s.part.%d", filename, i)
|
||||
|
||||
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.renderTemplatesToYamlWithEnv(baseDir, id, part, env, overrodeEnv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error during %s parsing: %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
currentState, err := ld.load(
|
||||
yamlBuf.Bytes(),
|
||||
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.ReleaseSetSpec, ¤tState.ReleaseSetSpec, mergo.WithOverride); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
finalState.RenderedValues = currentState.RenderedValues
|
||||
}
|
||||
|
||||
env = &finalState.Env
|
||||
|
||||
ld.logger.Debugf("merged environment: %v", env)
|
||||
}
|
||||
|
||||
return finalState, nil
|
||||
}
|
||||
@@ -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,19 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type NoMatchingHelmfileError struct {
|
||||
selectors []string
|
||||
env string
|
||||
}
|
||||
|
||||
func (e *NoMatchingHelmfileError) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"err: no releases found that matches specified selector(%s) and environment(%s), in any helmfile",
|
||||
strings.Join(e.selectors, ", "),
|
||||
e.env,
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"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"
|
||||
)
|
||||
|
||||
func prependLineNumbers(text string) string {
|
||||
buf := bytes.NewBufferString("")
|
||||
lines := strings.Split(text, "\n")
|
||||
for i, line := range lines {
|
||||
buf.WriteString(fmt.Sprintf("%2d: %s\n", i, line))
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
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
|
||||
return firstPassEnv, nil
|
||||
}
|
||||
}
|
||||
yamlData := yamlBuf.String()
|
||||
if r.logger != nil {
|
||||
r.logger.Debugf("first-pass rendering output of \"%s\":\n%s", filename, prependLineNumbers(yamlData))
|
||||
}
|
||||
|
||||
// Work-around for https://github.com/golang/go/issues/24963
|
||||
sanitized := strings.ReplaceAll(yamlData, "<no value>", "")
|
||||
|
||||
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.ParseAndLoad([]byte(sanitized), baseDir, filename, r.env, false, firstPassEnv)
|
||||
if err != nil && r.logger != nil {
|
||||
switch err.(type) {
|
||||
case *state.StateLoadError:
|
||||
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 {
|
||||
firstPassEnv = &prestate.Env
|
||||
}
|
||||
|
||||
return firstPassEnv, prestate
|
||||
}
|
||||
|
||||
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 starting for \"%s\": inherited=%v, overrode=%v", filename, inherited, overrode)
|
||||
}
|
||||
|
||||
initEnv, err := inherited.Merge(overrode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.logger != nil {
|
||||
r.logger.Debugf("first-pass uses: %v", initEnv)
|
||||
}
|
||||
|
||||
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 {
|
||||
if r.logger != nil {
|
||||
r.logger.Debugf("second-pass rendering failed, input of \"%s\":\n%s", filename, prependLineNumbers(string(content)))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if r.logger != nil {
|
||||
r.logger.Debugf("second-pass rendering result of \"%s\":\n%s", filename, prependLineNumbers(yamlBuf.String()))
|
||||
}
|
||||
return yamlBuf, nil
|
||||
}
|
||||
@@ -1,55 +1,32 @@
|
||||
package main
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"github.com/roboll/helmfile/pkg/remote"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
// See https://github.com/roboll/helmfile/issues/193
|
||||
func TestReadFromYaml_DuplicateReleaseName(t *testing.T) {
|
||||
yamlFile := "example/path/to/yaml/file"
|
||||
yamlContent := []byte(`releases:
|
||||
- name: myrelease1
|
||||
chart: mychart1
|
||||
labels:
|
||||
stage: pre
|
||||
foo: bar
|
||||
- name: myrelease1
|
||||
chart: mychart2
|
||||
labels:
|
||||
stage: post
|
||||
`)
|
||||
app := &app{
|
||||
readFile: ioutil.ReadFile,
|
||||
glob: filepath.Glob,
|
||||
abs: filepath.Abs,
|
||||
kubeContext: "default",
|
||||
logger: logger,
|
||||
}
|
||||
_, _, err := app.loadDesiredStateFromYaml(yamlContent, yamlFile, "default", []string{}, "default")
|
||||
if err == nil {
|
||||
t.Error("error expected but not happened")
|
||||
}
|
||||
if err.Error() != "duplicate release \"myrelease1\" found: there were 2 releases named \"myrelease1\" matching specified selector" {
|
||||
t.Errorf("unexpected error happened: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func makeRenderer(readFile func(string) ([]byte, error), env string) *twoPassRenderer {
|
||||
return &twoPassRenderer{
|
||||
reader: readFile,
|
||||
env: env,
|
||||
namespace: "namespace",
|
||||
filename: "",
|
||||
logger: logger,
|
||||
abs: filepath.Abs,
|
||||
}
|
||||
func makeLoader(files map[string]string, env string) (*desiredStateLoader, *testhelper.TestFs) {
|
||||
testfs := testhelper.NewTestFs(files)
|
||||
logger := helmexec.NewLogger(os.Stdout, "debug")
|
||||
r := remote.NewRemote(logger, testfs.Cwd, testfs.ReadFile, testfs.DirectoryExistsAt, testfs.FileExistsAt)
|
||||
return &desiredStateLoader{
|
||||
env: env,
|
||||
namespace: "namespace",
|
||||
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) {
|
||||
@@ -66,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 := makeRenderer(fileReader, "staging")
|
||||
yamlBuf, err := r.renderTemplate(yamlContent)
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -100,10 +68,10 @@ releases:
|
||||
|
||||
func TestReadFromYaml_RenderTemplate(t *testing.T) {
|
||||
|
||||
defaultValuesYaml := []byte(`
|
||||
defaultValuesYaml := `
|
||||
releaseName: "hello"
|
||||
conditionalReleaseTag: "yes"
|
||||
`)
|
||||
`
|
||||
|
||||
yamlContent := []byte(`
|
||||
environments:
|
||||
@@ -122,18 +90,13 @@ 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 := makeRenderer(fileReader, "staging")
|
||||
r, _ := makeLoader(files, "staging")
|
||||
// test the double rendering
|
||||
yamlBuf, err := r.renderTemplate(yamlContent)
|
||||
yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -159,7 +122,7 @@ releases:
|
||||
}
|
||||
|
||||
func TestReadFromYaml_RenderTemplateWithValuesReferenceError(t *testing.T) {
|
||||
defaultValuesYaml := []byte("")
|
||||
defaultValuesYaml := ``
|
||||
|
||||
yamlContent := []byte(`
|
||||
environments:
|
||||
@@ -175,14 +138,13 @@ 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 := makeRenderer(fileReader, "staging")
|
||||
r, _ := makeLoader(files, "staging")
|
||||
// test the double rendering
|
||||
_, err := r.renderTemplate(yamlContent)
|
||||
_, err := r.renderTemplatesToYaml("", "", yamlContent)
|
||||
|
||||
if !strings.Contains(err.Error(), "stringTemplate:8") {
|
||||
t.Fatalf("error should contain a stringTemplate error (reference to unknow key) %v", err)
|
||||
@@ -194,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:
|
||||
@@ -212,15 +174,13 @@ 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 := makeRenderer(fileReader, "staging")
|
||||
rendered, _ := r.renderTemplate(yamlContent)
|
||||
r, _ := makeLoader(files, "staging")
|
||||
rendered, _ := r.renderTemplatesToYaml("", "", yamlContent)
|
||||
|
||||
var state state.HelmState
|
||||
yaml.Unmarshal(rendered.Bytes(), &state)
|
||||
@@ -235,19 +195,15 @@ 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 := makeRenderer(fileReader, "staging")
|
||||
yamlBuf, err := r.renderTemplate(yamlContent)
|
||||
r, _ := makeLoader(files, "staging")
|
||||
yamlBuf, err := r.renderTemplatesToYaml("", "", yamlContent)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
@@ -259,3 +215,24 @@ func TestReadFromYaml_RenderTemplateWithNamespace(t *testing.T) {
|
||||
t.Errorf("release name should be namespace-myrelease")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFromYaml_HelmfileShouldBeResilentToTemplateErrors(t *testing.T) {
|
||||
yamlContent := []byte(`environments:
|
||||
staging:
|
||||
production:
|
||||
|
||||
releases:
|
||||
{{ if (eq .Environment.Name "production" }} # notice syntax error: unclosed left paren
|
||||
- name: prod-myrelease
|
||||
{{ else }}
|
||||
- name: myapp
|
||||
{{ end }}
|
||||
chart: mychart
|
||||
`)
|
||||
|
||||
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,13 +82,6 @@ func GetArgs(args string, state *state.HelmState) []string {
|
||||
}
|
||||
}
|
||||
|
||||
if state.HelmDefaults.TillerNamespace != "" {
|
||||
argsMap.SetArg("--tiller-namespace", state.HelmDefaults.TillerNamespace, false)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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"`
|
||||
ShowLogs bool `yaml:"showlogs"`
|
||||
}
|
||||
|
||||
type event struct {
|
||||
Name string
|
||||
Error error
|
||||
}
|
||||
|
||||
type Bus struct {
|
||||
Runner helmexec.Runner
|
||||
Hooks []Hook
|
||||
|
||||
BasePath string
|
||||
StateFilePath string
|
||||
Namespace string
|
||||
|
||||
Env environment.Environment
|
||||
|
||||
ReadFile func(string) ([]byte, error)
|
||||
Logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
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,
|
||||
Logger: bus.Logger,
|
||||
}
|
||||
}
|
||||
|
||||
executed := false
|
||||
|
||||
for _, hook := range bus.Hooks {
|
||||
contained := false
|
||||
for _, e := range hook.Events {
|
||||
contained = contained || e == evt
|
||||
}
|
||||
if !contained {
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
name := hook.Name
|
||||
if name == "" {
|
||||
name = hook.Command
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "%s: basePath=%s\n", bus.StateFilePath, bus.BasePath)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Environment": bus.Env,
|
||||
"Namespace": bus.Namespace,
|
||||
"Event": event{
|
||||
Name: evt,
|
||||
Error: evtErr,
|
||||
},
|
||||
}
|
||||
for k, v := range context {
|
||||
data[k] = v
|
||||
}
|
||||
render := tmpl.NewTextRenderer(bus.ReadFile, bus.BasePath, data)
|
||||
|
||||
bus.Logger.Debugf("hook[%s]: triggered by event \"%s\"\n", name, evt)
|
||||
|
||||
command, err := render.RenderTemplateText(hook.Command)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("hook[%s]: %v", name, err)
|
||||
}
|
||||
|
||||
args := make([]string, len(hook.Args))
|
||||
for i, raw := range hook.Args {
|
||||
args[i], err = render.RenderTemplateText(raw)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("hook[%s]: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
executed = true
|
||||
}
|
||||
|
||||
return executed, nil
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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")
|
||||
|
||||
type runner struct {
|
||||
}
|
||||
|
||||
func (r *runner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) {
|
||||
if cmd == "ng" {
|
||||
return nil, fmt.Errorf("cmd failed due to invalid cmd: %s", cmd)
|
||||
}
|
||||
for _, a := range args {
|
||||
if a == "ng" {
|
||||
return nil, fmt.Errorf("cmd failed due to invalid arg: %s", a)
|
||||
}
|
||||
}
|
||||
return []byte(""), nil
|
||||
}
|
||||
|
||||
func TestTrigger(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
hook *Hook
|
||||
triggeredEvt string
|
||||
expectedResult bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
"okhook1",
|
||||
&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{}, false},
|
||||
"bar",
|
||||
false,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"nohook1",
|
||||
nil,
|
||||
"bar",
|
||||
false,
|
||||
"",
|
||||
},
|
||||
{
|
||||
"nghook1",
|
||||
&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"}, false},
|
||||
"foo",
|
||||
false,
|
||||
"hook[nghook2]: command `ok` failed: cmd failed due to invalid arg: ng",
|
||||
},
|
||||
}
|
||||
readFile := func(filename string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("unexpected call to readFile: %s", filename)
|
||||
}
|
||||
for _, c := range cases {
|
||||
hooks := []Hook{}
|
||||
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: zeLogger,
|
||||
ReadFile: readFile,
|
||||
}
|
||||
|
||||
bus.Runner = &runner{}
|
||||
data := map[string]interface{}{
|
||||
"Release": "myrel",
|
||||
"HelmfileCommand": "mycmd",
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
if c.expectedErr != "" {
|
||||
if err == nil {
|
||||
t.Error("error expected, but not occurred")
|
||||
} else if err.Error() != c.expectedErr {
|
||||
t.Errorf("unexpected error for case \"%s\": expected=%s, actual=%v", c.name, c.expectedErr, err)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type HelmContext struct {
|
||||
Tillerless bool
|
||||
TillerNamespace string
|
||||
HistoryMax int
|
||||
WorkerIndex int
|
||||
}
|
||||
|
||||
func (context *HelmContext) GetTillerlessArgs(helm *execer) []string {
|
||||
if context.Tillerless && !helm.IsHelm3() {
|
||||
if context.TillerNamespace != "" {
|
||||
return []string{"tiller", "run", context.TillerNamespace, "--", helm.helmBinary}
|
||||
} else {
|
||||
return []string{"tiller", "run", "--", helm.helmBinary}
|
||||
}
|
||||
} else {
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
func (context *HelmContext) getTillerlessEnv() map[string]string {
|
||||
if context.Tillerless {
|
||||
result := map[string]string{
|
||||
"HELM_TILLER_SILENT": "true",
|
||||
// Changing the TILLER port doesn't really work: https://github.com/helm/helm/issues/3159
|
||||
// So this is not used for the moment.
|
||||
// "HELM_TILLER_PORT": strconv.Itoa(44134 + context.WorkerIndex),
|
||||
}
|
||||
if config := os.Getenv("KUBECONFIG"); config != "" {
|
||||
absConfig, err := filepath.Abs(config)
|
||||
if err == nil {
|
||||
result["KUBECONFIG"] = absConfig
|
||||
}
|
||||
}
|
||||
return result
|
||||
} else {
|
||||
return map[string]string{}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
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
|
||||
}
|
||||
|
||||
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, < 3.3.4"); 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()
|
||||
}
|
||||
|
||||
tmpFile, err := ioutil.TempFile("", "secret")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = tmpFile.Write(secret.bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return tmpFile.Name(), 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)
|
||||
}
|
||||
@@ -0,0 +1,579 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Mocking the command-line runner
|
||||
|
||||
type mockRunner struct {
|
||||
output []byte
|
||||
err error
|
||||
}
|
||||
|
||||
func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) {
|
||||
return mock.output, mock.err
|
||||
}
|
||||
|
||||
func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer {
|
||||
execer := New("helm", logger, kubeContext, &mockRunner{})
|
||||
return execer
|
||||
}
|
||||
|
||||
// Test methods
|
||||
|
||||
func TestNewHelmExec(t *testing.T) {
|
||||
buffer := bytes.NewBufferString("something")
|
||||
helm := MockExecer(NewLogger(buffer, "debug"), "dev")
|
||||
if helm.kubeContext != "dev" {
|
||||
t.Error("helmexec.New() - kubeContext")
|
||||
}
|
||||
if buffer.String() != "something" {
|
||||
t.Error("helmexec.New() - changed buffer")
|
||||
}
|
||||
if len(helm.extra) != 0 {
|
||||
t.Error("helmexec.New() - extra args not empty")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SetExtraArgs(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
helm.SetExtraArgs("foo")
|
||||
if !reflect.DeepEqual(helm.extra, []string{"foo"}) {
|
||||
t.Error("helmexec.SetExtraArgs() - one extra argument missing")
|
||||
}
|
||||
helm.SetExtraArgs("alpha", "beta")
|
||||
if !reflect.DeepEqual(helm.extra, []string{"alpha", "beta"}) {
|
||||
t.Error("helmexec.SetExtraArgs() - two extra arguments missing (overwriting the previous value)")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SetHelmBinary(t *testing.T) {
|
||||
helm := MockExecer(NewLogger(os.Stdout, "info"), "dev")
|
||||
if helm.helmBinary != "helm" {
|
||||
t.Error("helmexec.command - default command is not helm")
|
||||
}
|
||||
helm.SetHelmBinary("foo")
|
||||
if helm.helmBinary != "foo" {
|
||||
t.Errorf("helmexec.SetHelmBinary() - actual = %s expect = foo", helm.helmBinary)
|
||||
}
|
||||
}
|
||||
|
||||
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", "", "", "")
|
||||
expected := `Adding repo myRepo https://repo.example.com/
|
||||
exec: helm --kube-context dev repo add myRepo https://repo.example.com/ --cert-file cert.pem --key-file key.pem
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.AddRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.AddRepo("myRepo", "https://repo.example.com/", "ca.crt", "", "", "", "", "")
|
||||
expected = `Adding repo myRepo https://repo.example.com/
|
||||
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/", "", "", "", "", "", "")
|
||||
expected = `Adding repo myRepo https://repo.example.com/
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_UpdateRepo(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.UpdateRepo()
|
||||
expected := `Updating repo
|
||||
exec: helm --kube-context dev repo update
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.UpdateRepo()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SyncRelease(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.SyncRelease(HelmContext{}, "release", "chart", "--timeout 10", "--wait")
|
||||
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)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.SyncRelease(HelmContext{}, "release", "chart")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SyncReleaseTillerless(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.SyncRelease(HelmContext{Tillerless: true, TillerNamespace: "foo"}, "release", "chart",
|
||||
"--timeout 10", "--wait")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_UpdateDeps(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.UpdateDeps("./chart/foo")
|
||||
expected := `Updating dependency ./chart/foo
|
||||
exec: helm --kube-context dev dependency update ./chart/foo
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.UpdateDeps()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.SetExtraArgs("--verify")
|
||||
helm.UpdateDeps("./chart/foo")
|
||||
expected = `Updating dependency ./chart/foo
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_BuildDeps(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "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)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.SetExtraArgs("--verify")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DecryptSecret(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.DecryptSecret(HelmContext{}, "secretName")
|
||||
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
|
||||
`, cwd, cwd, cwd, cwd, cwd)
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DecryptSecret()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DiffRelease(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "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", 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)
|
||||
}
|
||||
}
|
||||
|
||||
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", 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)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DeleteRelease(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.DeleteRelease(HelmContext{}, "release")
|
||||
expected := `Deleting release
|
||||
exec: helm --kube-context dev delete release
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
func Test_DeleteRelease_Flags(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.DeleteRelease(HelmContext{}, "release", "--purge")
|
||||
expected := `Deleting release
|
||||
exec: helm --kube-context dev delete release --purge
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.DeleteRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_TestRelease(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.TestRelease(HelmContext{}, "release")
|
||||
expected := `Testing release
|
||||
exec: helm --kube-context dev test release
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.TestRelease()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
func Test_TestRelease_Flags(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.TestRelease(HelmContext{}, "release", "--cleanup", "--timeout", "60")
|
||||
expected := `Testing release
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ReleaseStatus(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.ReleaseStatus(HelmContext{}, "myRelease")
|
||||
expected := `Getting status myRelease
|
||||
exec: helm --kube-context dev status myRelease
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.ReleaseStatus()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_exec(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "")
|
||||
env := map[string]string{}
|
||||
helm.exec([]string{"version"}, env)
|
||||
expected := `exec: helm version
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
helm = MockExecer(logger, "dev")
|
||||
ret, _ := helm.exec([]string{"diff"}, env)
|
||||
if len(ret) != 0 {
|
||||
t.Error("helmexec.exec() - expected empty return value")
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm = MockExecer(logger, "dev")
|
||||
helm.exec([]string{"diff", "release", "chart", "--timeout 10", "--wait"}, env)
|
||||
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)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.exec([]string{"version"}, env)
|
||||
expected = `exec: helm --kube-context dev version
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm.SetExtraArgs("foo")
|
||||
helm.exec([]string{"version"}, env)
|
||||
expected = `exec: helm --kube-context dev version foo
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
|
||||
buffer.Reset()
|
||||
helm = MockExecer(logger, "")
|
||||
helm.SetHelmBinary("overwritten")
|
||||
helm.exec([]string{"version"}, env)
|
||||
expected = `exec: overwritten version
|
||||
`
|
||||
if buffer.String() != expected {
|
||||
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Lint(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "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)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Fetch(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
logger := NewLogger(&buffer, "debug")
|
||||
helm := MockExecer(logger, "dev")
|
||||
helm.Fetch("chart", "--version", "1.2.3", "--untar", "--untardir", "/tmp/dir")
|
||||
expected := `Fetching chart
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
`,
|
||||
"info": `Adding repo myRepo https://repo.example.com/
|
||||
`,
|
||||
"warn": ``,
|
||||
}
|
||||
|
||||
func Test_LogLevels(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
for logLevel, expected := range logLevelTests {
|
||||
buffer.Reset()
|
||||
logger := NewLogger(&buffer, logLevel)
|
||||
helm := MockExecer(logger, "")
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getTillerlessEnv(t *testing.T) {
|
||||
context := HelmContext{Tillerless: true, TillerNamespace: "foo", WorkerIndex: 1}
|
||||
|
||||
os.Unsetenv("KUBECONFIG")
|
||||
actual := context.getTillerlessEnv()
|
||||
if val, found := actual["HELM_TILLER_SILENT"]; !found || val != "true" {
|
||||
t.Errorf("getTillerlessEnv() HELM_TILLER_SILENT\nactual = %s\nexpect = true", val)
|
||||
}
|
||||
// This feature is disabled until it is fixed in helm
|
||||
/*if val, found := actual["HELM_TILLER_PORT"]; !found || val != "44135" {
|
||||
t.Errorf("getTillerlessEnv() HELM_TILLER_PORT\nactual = %s\nexpect = 44135", val)
|
||||
}*/
|
||||
if val, found := actual["KUBECONFIG"]; found {
|
||||
t.Errorf("getTillerlessEnv() KUBECONFIG\nactual = %s\nexpect = nil", val)
|
||||
}
|
||||
|
||||
os.Setenv("KUBECONFIG", "toto")
|
||||
actual = context.getTillerlessEnv()
|
||||
cwd, _ := os.Getwd()
|
||||
expected := path.Join(cwd, "toto")
|
||||
if val, found := actual["KUBECONFIG"]; !found || val != expected {
|
||||
t.Errorf("getTillerlessEnv() KUBECONFIG\nactual = %s\nexpect = %s", val, expected)
|
||||
}
|
||||
os.Unsetenv("KUBECONFIG")
|
||||
}
|
||||
|
||||
func Test_mergeEnv(t *testing.T) {
|
||||
actual := env2map(mergeEnv([]string{"A=1", "B=c=d", "E=2"}, map[string]string{"B": "3", "F": "4"}))
|
||||
expected := map[string]string{"A": "1", "B": "3", "E": "2", "F": "4"}
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +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, cafile, certfile, keyfile, username, password string, managed string) error
|
||||
UpdateRepo() error
|
||||
BuildDeps(name, chart string) error
|
||||
UpdateDeps(chart string) error
|
||||
SyncRelease(context HelmContext, name, chart string, flags ...string) error
|
||||
DiffRelease(context HelmContext, name, chart string, suppressDiff bool, flags ...string) error
|
||||
TemplateRelease(name, chart string, flags ...string) error
|
||||
Fetch(chart string, flags ...string) error
|
||||
Lint(name, chart string, flags ...string) error
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package helmexec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
tmpPrefix = "helmfile-"
|
||||
tmpSuffix = "-exec"
|
||||
)
|
||||
|
||||
// Runner interface for shell commands
|
||||
type Runner interface {
|
||||
Execute(cmd string, args []string, env map[string]string) ([]byte, error)
|
||||
}
|
||||
|
||||
// ShellRunner implemention for shell commands
|
||||
type ShellRunner struct {
|
||||
Dir string
|
||||
|
||||
Logger *zap.SugaredLogger
|
||||
}
|
||||
|
||||
// Execute a shell command
|
||||
func (shell ShellRunner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) {
|
||||
preparedCmd := exec.Command(cmd, args...)
|
||||
preparedCmd.Dir = shell.Dir
|
||||
preparedCmd.Env = mergeEnv(os.Environ(), env)
|
||||
return Output(preparedCmd, &logWriterGenerator{
|
||||
log: shell.Logger,
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
var combined bytes.Buffer
|
||||
|
||||
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:
|
||||
//
|
||||
// err: release "envoy2" in "helmfile.yaml" failed: exit status 1: Error: could not find a ready tiller pod
|
||||
// <redundant new line!>
|
||||
// err: release "envoy" in "helmfile.yaml" failed: exit status 1: Error: could not find a ready tiller pod
|
||||
switch ee := err.(type) {
|
||||
case *exec.ExitError:
|
||||
// Propagate any non-zero exit status from the external command, rather than throwing it away,
|
||||
// so that helmfile could return its own exit code accordingly
|
||||
waitStatus := ee.Sys().(syscall.WaitStatus)
|
||||
exitStatus := waitStatus.ExitStatus()
|
||||
err = newExitError(c.Path, c.Args, exitStatus, ee, stderr.String(), combined.String())
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected error: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
return stdout.Bytes(), err
|
||||
}
|
||||
|
||||
func mergeEnv(orig []string, new map[string]string) []string {
|
||||
wanted := env2map(orig)
|
||||
for k, v := range new {
|
||||
wanted[k] = v
|
||||
}
|
||||
return map2env(wanted)
|
||||
}
|
||||
|
||||
func map2env(wanted map[string]string) []string {
|
||||
result := []string{}
|
||||
for k, v := range wanted {
|
||||
result = append(result, k+"="+v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func env2map(env []string) map[string]string {
|
||||
wanted := map[string]string{}
|
||||
for _, cur := range env {
|
||||
pair := strings.SplitN(cur, "=", 2)
|
||||
|
||||
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,235 @@
|
||||
package remote
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/testhelper"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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,357 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/roboll/helmfile/pkg/helmexec"
|
||||
"github.com/roboll/helmfile/pkg/remote"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"github.com/roboll/helmfile/pkg/environment"
|
||||
"github.com/roboll/helmfile/pkg/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
|
||||
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), valsRuntime vals.Evaluator, getHelm func(*HelmState) helmexec.Interface, overrideHelmBinary string, remote *remote.Remote) *StateCreator {
|
||||
return &StateCreator{
|
||||
logger: logger,
|
||||
readFile: readFile,
|
||||
fileExists: fileExists,
|
||||
abs: abs,
|
||||
glob: glob,
|
||||
Strict: true,
|
||||
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.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
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"github.com/roboll/helmfile/pkg/remote"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"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 := &StateCreator{
|
||||
logger: logger,
|
||||
readFile: ioutil.ReadFile,
|
||||
abs: filepath.Abs,
|
||||
|
||||
DeleteFile: os.Remove,
|
||||
Strict: true,
|
||||
}
|
||||
return c.ParseAndLoad(content, filepath.Dir(file), file, env, true, nil)
|
||||
}
|
||||
|
||||
func TestReadFromYaml(t *testing.T) {
|
||||
yamlFile := "example/path/to/yaml/file"
|
||||
yamlContent := []byte(`releases:
|
||||
- name: myrelease
|
||||
namespace: mynamespace
|
||||
chart: mychart
|
||||
`)
|
||||
state, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
|
||||
if err != nil {
|
||||
t.Errorf("unxpected error: %v", err)
|
||||
}
|
||||
|
||||
if state.Releases[0].Name != "myrelease" {
|
||||
t.Errorf("unexpected release name: expected=myrelease actual=%s", state.Releases[0].Name)
|
||||
}
|
||||
if state.Releases[0].Namespace != "mynamespace" {
|
||||
t.Errorf("unexpected chart namespace: expected=mynamespace actual=%s", state.Releases[0].Chart)
|
||||
}
|
||||
if state.Releases[0].Chart != "mychart" {
|
||||
t.Errorf("unexpected chart name: expected=mychart actual=%s", state.Releases[0].Chart)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFromYaml_NonexistentEnv(t *testing.T) {
|
||||
yamlFile := "example/path/to/yaml/file"
|
||||
yamlContent := []byte(`releases:
|
||||
- name: myrelease
|
||||
namespace: mynamespace
|
||||
chart: mychart
|
||||
`)
|
||||
_, err := createFromYaml(yamlContent, yamlFile, "production", logger)
|
||||
if err == nil {
|
||||
t.Error("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
type stateTestEnv struct {
|
||||
Files map[string]string
|
||||
WorkDir string
|
||||
}
|
||||
|
||||
func (testEnv stateTestEnv) MustLoadState(t *testing.T, file, envName string) *HelmState {
|
||||
t.Helper()
|
||||
|
||||
testFs := testhelper.NewTestFs(testEnv.Files)
|
||||
|
||||
if testFs.Cwd == "" {
|
||||
testFs.Cwd = "/"
|
||||
}
|
||||
|
||||
yamlContent, ok := testEnv.Files[file]
|
||||
if !ok {
|
||||
t.Fatalf("no file named %q registered", file)
|
||||
}
|
||||
|
||||
r := remote.NewRemote(logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
|
||||
state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob, nil, nil, "", r).
|
||||
ParseAndLoad([]byte(yamlContent), filepath.Dir(file), file, envName, true, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
func TestReadFromYaml_NonDefaultEnv(t *testing.T) {
|
||||
yamlFile := "/example/path/to/helmfile.yaml"
|
||||
yamlContent := []byte(`environments:
|
||||
production:
|
||||
values:
|
||||
- foo.yaml
|
||||
- bar.yaml.gotmpl
|
||||
|
||||
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 }}
|
||||
`)
|
||||
|
||||
expectedValues := `env: production
|
||||
releaseName: myrelease
|
||||
releaseNamespace: mynamespace
|
||||
`
|
||||
|
||||
testFs := testhelper.NewTestFs(map[string]string{
|
||||
fooYamlFile: string(fooYamlContent),
|
||||
barYamlFile: string(barYamlContent),
|
||||
barTextFile: string(barTextContent),
|
||||
valuesFile: string(valuesContent),
|
||||
})
|
||||
testFs.Cwd = "/example/path/to"
|
||||
|
||||
r := remote.NewRemote(logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
|
||||
state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob, nil, nil, "", r).
|
||||
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
actual := state.Env.Values
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Errorf("unexpected environment values: expected=%v, actual=%v", expected, actual)
|
||||
}
|
||||
|
||||
release := state.Releases[0]
|
||||
|
||||
state.ApplyOverrides(&release)
|
||||
|
||||
actualValuesData, err := state.RenderReleaseValuesFileToBytes(&release, valuesFile)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
actualValues := string(actualValuesData)
|
||||
|
||||
if !reflect.DeepEqual(expectedValues, actualValues) {
|
||||
t.Errorf("unexpected values: expected=%v, actual=%v", expectedValues, actualValues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFromYaml_OverrideNamespace(t *testing.T) {
|
||||
yamlFile := "/example/path/to/helmfile.yaml"
|
||||
yamlContent := []byte(`environments:
|
||||
production:
|
||||
values:
|
||||
- foo.yaml
|
||||
- bar.yaml.gotmpl
|
||||
|
||||
# A.k.a helmfile apply --namespace myns
|
||||
namespace: myns
|
||||
|
||||
releases:
|
||||
- name: myrelease
|
||||
namespace: mynamespace
|
||||
chart: mychart
|
||||
values:
|
||||
- values.yaml.gotmpl
|
||||
`)
|
||||
|
||||
fooYamlFile := "/example/path/to/foo.yaml"
|
||||
fooYamlContent := []byte(`foo: foo
|
||||
# As this file doesn't have an file extension ".gotmpl", this template expression should not be evaluated
|
||||
baz: "{{ readFile \"baz.txt\" }}"`)
|
||||
|
||||
barYamlFile := "/example/path/to/bar.yaml.gotmpl"
|
||||
barYamlContent := []byte(`foo: FOO
|
||||
bar: {{ readFile "bar.txt" }}
|
||||
`)
|
||||
|
||||
barTextFile := "/example/path/to/bar.txt"
|
||||
barTextContent := []byte("BAR")
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"foo": "FOO",
|
||||
"bar": "BAR",
|
||||
// As the file doesn't have an file extension ".gotmpl", this template expression should not be evaluated
|
||||
"baz": "{{ readFile \"baz.txt\" }}",
|
||||
}
|
||||
|
||||
valuesFile := "/example/path/to/values.yaml.gotmpl"
|
||||
valuesContent := []byte(`env: {{ .Environment.Name }}
|
||||
releaseName: {{ .Release.Name }}
|
||||
releaseNamespace: {{ .Release.Namespace }}
|
||||
overrideNamespace: {{ .Namespace }}
|
||||
`)
|
||||
|
||||
expectedValues := `env: production
|
||||
releaseName: myrelease
|
||||
releaseNamespace: myns
|
||||
overrideNamespace: myns
|
||||
`
|
||||
|
||||
testFs := testhelper.NewTestFs(map[string]string{
|
||||
fooYamlFile: string(fooYamlContent),
|
||||
barYamlFile: string(barYamlContent),
|
||||
barTextFile: string(barTextContent),
|
||||
valuesFile: string(valuesContent),
|
||||
})
|
||||
testFs.Cwd = "/example/path/to"
|
||||
|
||||
r := remote.NewRemote(logger, testFs.Cwd, testFs.ReadFile, testFs.DirectoryExistsAt, testFs.FileExistsAt)
|
||||
state, err := NewCreator(logger, testFs.ReadFile, testFs.FileExists, testFs.Abs, testFs.Glob, nil, nil, "", r).
|
||||
ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
actual := state.Env.Values
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Errorf("unexpected environment values: expected=%v, actual=%v", expected, actual)
|
||||
}
|
||||
|
||||
release := state.Releases[0]
|
||||
|
||||
state.ApplyOverrides(&release)
|
||||
|
||||
actualValuesData, err := state.RenderReleaseValuesFileToBytes(&release, valuesFile)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
actualValues := string(actualValuesData)
|
||||
|
||||
if !reflect.DeepEqual(expectedValues, actualValues) {
|
||||
t.Errorf("unexpected values: expected=%v, actual=%v", expectedValues, actualValues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFromYaml_StrictUnmarshalling(t *testing.T) {
|
||||
yamlFile := "example/path/to/yaml/file"
|
||||
yamlContent := []byte(`releases:
|
||||
- name: myrelease
|
||||
namespace: mynamespace
|
||||
releases: mychart
|
||||
`)
|
||||
_, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
|
||||
if err == nil {
|
||||
t.Error("expected an error for wrong key 'releases' which is not in struct")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFromYaml_DeprecatedReleaseReferences(t *testing.T) {
|
||||
yamlFile := "example/path/to/yaml/file"
|
||||
yamlContent := []byte(`charts:
|
||||
- name: myrelease
|
||||
chart: mychart
|
||||
`)
|
||||
state, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
|
||||
if err != nil {
|
||||
t.Errorf("unxpected error: %v", err)
|
||||
}
|
||||
|
||||
if state.Releases[0].Name != "myrelease" {
|
||||
t.Errorf("unexpected release name: expected=myrelease actual=%s", state.Releases[0].Name)
|
||||
}
|
||||
if state.Releases[0].Chart != "mychart" {
|
||||
t.Errorf("unexpected chart name: expected=mychart actual=%s", state.Releases[0].Chart)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFromYaml_ConflictingReleasesConfig(t *testing.T) {
|
||||
yamlFile := "example/path/to/yaml/file"
|
||||
yamlContent := []byte(`charts:
|
||||
- name: myrelease1
|
||||
chart: mychart1
|
||||
releases:
|
||||
- name: myrelease2
|
||||
chart: mychart2
|
||||
`)
|
||||
_, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
|
||||
if err == nil {
|
||||
t.Error("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFromYaml_FilterReleasesOnLabels(t *testing.T) {
|
||||
yamlFile := "example/path/to/yaml/file"
|
||||
yamlContent := []byte(`releases:
|
||||
- name: myrelease1
|
||||
chart: mychart1
|
||||
labels:
|
||||
tier: frontend
|
||||
foo: bar
|
||||
- name: myrelease2
|
||||
chart: mychart2
|
||||
labels:
|
||||
tier: frontend
|
||||
- name: myrelease3
|
||||
chart: mychart3
|
||||
labels:
|
||||
tier: backend
|
||||
`)
|
||||
cases := []struct {
|
||||
filter LabelFilter
|
||||
results []bool
|
||||
}{
|
||||
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}}},
|
||||
[]bool{true, true, false}},
|
||||
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}, []string{"foo", "bar"}}},
|
||||
[]bool{true, false, false}},
|
||||
{LabelFilter{negativeLabels: [][]string{[]string{"tier", "frontend"}}},
|
||||
[]bool{false, false, true}},
|
||||
{LabelFilter{positiveLabels: [][]string{[]string{"tier", "frontend"}}, negativeLabels: [][]string{[]string{"foo", "bar"}}},
|
||||
[]bool{false, true, false}},
|
||||
}
|
||||
state, err := createFromYaml(yamlContent, yamlFile, DefaultEnv, logger)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
for idx, c := range cases {
|
||||
for idx2, expected := range c.results {
|
||||
if f := c.filter.Match(state.Releases[idx2]); f != expected {
|
||||
t.Errorf("[case: %d][outcome: %d] Unexpected outcome wanted %t, got %t", idx, idx2, expected, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFromYaml_FilterNegatives(t *testing.T) {
|
||||
yamlFile := "example/path/to/yaml/file"
|
||||
yamlContent := []byte(`releases:
|
||||
- name: myrelease1
|
||||
chart: mychart1
|
||||
labels:
|
||||
stage: pre
|
||||
foo: bar
|
||||
- name: myrelease2
|
||||
chart: mychart2
|
||||
labels:
|
||||
stage: post
|
||||
- name: myrelease3
|
||||
chart: mychart3
|
||||
`)
|
||||
cases := []struct {
|
||||
filter LabelFilter
|
||||
results []bool
|
||||
}{
|
||||
{LabelFilter{positiveLabels: [][]string{[]string{"stage", "pre"}}},
|
||||
[]bool{true, false, false}},
|
||||
{LabelFilter{positiveLabels: [][]string{[]string{"stage", "post"}}},
|
||||
[]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 {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
for idx, c := range cases {
|
||||
for idx2, expected := range c.results {
|
||||
if f := c.filter.Match(state.Releases[idx2]); f != expected {
|
||||
t.Errorf("[case: %d][outcome: %d] Unexpected outcome wanted %t, got %t", idx, idx2, expected, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFromYaml_Helmfiles_Selectors(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
content []byte
|
||||
wantErr bool
|
||||
helmfiles []SubHelmfileSpec
|
||||
}{
|
||||
{
|
||||
path: "working/selector",
|
||||
content: []byte(`helmfiles:
|
||||
- simple/helmfile.yaml
|
||||
- path: path/prefix/selector.yaml
|
||||
selectors:
|
||||
- name=zorba
|
||||
- foo=bar
|
||||
- path: path/prefix/empty/selector.yaml
|
||||
selectors: []
|
||||
- path: path/prefix/inherits/selector.yaml
|
||||
selectorsInherited: true
|
||||
`),
|
||||
wantErr: false,
|
||||
helmfiles: []SubHelmfileSpec{{Path: "simple/helmfile.yaml", Selectors: nil, SelectorsInherited: false},
|
||||
{Path: "path/prefix/selector.yaml", Selectors: []string{"name=zorba", "foo=bar"}, SelectorsInherited: false},
|
||||
{Path: "path/prefix/empty/selector.yaml", Selectors: []string{}, SelectorsInherited: false},
|
||||
{Path: "path/prefix/inherits/selector.yaml", Selectors: nil, SelectorsInherited: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "failing2/selector",
|
||||
content: []byte(`helmfiles:
|
||||
- path: failing2/helmfile.yaml
|
||||
wrongkey:
|
||||
`),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
path: "failing3/selector",
|
||||
content: []byte(`helmfiles:
|
||||
- path: failing3/helmfile.yaml
|
||||
selectors: foo
|
||||
`),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
path: "failing4/selector",
|
||||
content: []byte(`helmfiles:
|
||||
- path: failing4/helmfile.yaml
|
||||
selectors:
|
||||
`),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
path: "failing4/selector",
|
||||
content: []byte(`helmfiles:
|
||||
- path: failing4/helmfile.yaml
|
||||
selectors:
|
||||
- colon: not-authorized
|
||||
`),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
path: "failing6/selector",
|
||||
content: []byte(`helmfiles:
|
||||
- selectors:
|
||||
- whatever
|
||||
`),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
path: "failing7/selector",
|
||||
content: []byte(`helmfiles:
|
||||
- path: foo/bar
|
||||
selectors:
|
||||
- foo=bar
|
||||
selectorsInherited: true
|
||||
`),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
st, err := createFromYaml(test.content, test.path, DefaultEnv, logger)
|
||||
if err != nil {
|
||||
if test.wantErr {
|
||||
continue
|
||||
} else {
|
||||
t.Error("unexpected error:", err)
|
||||
}
|
||||
}
|
||||
Assert(t, cmp.DeepEqual(st.Helmfiles, test.helmfiles), "for path %v", test.path)
|
||||
}
|
||||
}
|
||||
@@ -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,167 @@
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+2743
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.Contains(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.Contains(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 filename[0] == '/' {
|
||||
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 relPattern[0] == '/' {
|
||||
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 path[0] == '/' {
|
||||
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,12 +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,
|
||||
"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
|
||||
@@ -55,64 +63,65 @@ 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) {
|
||||
path := filepath.Join(c.basePath, filename)
|
||||
var path string
|
||||
if filepath.IsAbs(filename) {
|
||||
path = filename
|
||||
} else {
|
||||
path = filepath.Join(c.basePath, filename)
|
||||
}
|
||||
|
||||
bytes, err := c.readFile(path)
|
||||
if err != nil {
|
||||
@@ -121,6 +130,14 @@ func (c *Context) ReadFile(filename string) (string, error) {
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
func (c *Context) Tpl(text string, data interface{}) (string, error) {
|
||||
buf, err := c.RenderTemplateToBuffer(text, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func ToYaml(v interface{}) (string, error) {
|
||||
data, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
@@ -133,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
|
||||
}
|
||||
@@ -190,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
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package tmpl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
@@ -11,7 +12,27 @@ func TestReadFile(t *testing.T) {
|
||||
bar: BAR
|
||||
`
|
||||
expectedFilename := "values.yaml"
|
||||
ctx := &Context{readFile: func(filename string) ([]byte, error) {
|
||||
ctx := &Context{basePath: ".", readFile: func(filename string) ([]byte, error) {
|
||||
if filename != expectedFilename {
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
|
||||
}
|
||||
return []byte(expected), nil
|
||||
}}
|
||||
actual, err := ctx.ReadFile(expectedFilename)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFile_PassAbsPath(t *testing.T) {
|
||||
expected := `foo:
|
||||
bar: BAR
|
||||
`
|
||||
expectedFilename, _ := filepath.Abs("values.yaml")
|
||||
ctx := &Context{basePath: ".", readFile: func(filename string) ([]byte, error) {
|
||||
if filename != expectedFilename {
|
||||
return nil, fmt.Errorf("unexpected filename: expected=%v, actual=%s", expectedFilename, filename)
|
||||
}
|
||||
@@ -97,3 +118,62 @@ func TestSetValueAtPath_TwoComponents(t *testing.T) {
|
||||
t.Errorf("unexpected result: expected=%v, actual=%v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTpl(t *testing.T) {
|
||||
text := `foo: {{ .foo }}
|
||||
`
|
||||
expected := `foo: FOO
|
||||
`
|
||||
ctx := &Context{basePath: "."}
|
||||
actual, err := ctx.Tpl(text, map[string]interface{}{"foo": "FOO"})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user