Compare commits
543 Commits
Author | SHA1 | Date |
---|---|---|
|
c0557127de | |
|
2e7d7be21b | |
|
599f5b83b4 | |
|
d68a2bc207 | |
|
ee52ee1f10 | |
|
708116dca3 | |
|
c602f9ba6b | |
|
fe39f2314c | |
|
56411056ed | |
|
d2f5598bd2 | |
|
5618fc875f | |
|
0845592ef1 | |
|
966471af19 | |
|
3536da5d3d | |
|
eded6d09e8 | |
|
4e21b54765 | |
|
1dfb4a3ec8 | |
|
b69b78a585 | |
|
87fb0fe348 | |
|
3afed4a627 | |
|
d2e6f21b29 | |
|
a8a6504ded | |
|
6e51e97210 | |
|
f8184ed615 | |
|
e2f31a4b6b | |
|
58fad4c416 | |
|
7a102d31a6 | |
|
c6d69a8828 | |
|
841b0d17d9 | |
|
55fd92efa4 | |
|
b3610336f2 | |
|
34d5b1b4aa | |
|
d9360c0b91 | |
|
3a96f687c1 | |
|
b224570ad0 | |
|
1f49401f92 | |
|
d9e9d6be4f | |
|
989407ed13 | |
|
cb66ba89fd | |
|
8620dbb266 | |
|
5d81c7b409 | |
|
d1976ed252 | |
|
7b0e38653d | |
|
337610ede1 | |
|
9e37965ba9 | |
|
03d056595e | |
|
d64230ae7e | |
|
abe42943b2 | |
|
cd3c258319 | |
|
689a4db96c | |
|
2881dbdb35 | |
|
43c73a54b2 | |
|
791d6a2a6f | |
|
ce3902a7a6 | |
|
4b5a5ebe47 | |
|
88aa8b9557 | |
|
1478cddfb2 | |
|
92c9496023 | |
|
5d0ff0ba67 | |
|
487809a0fe | |
|
6a3bc1a525 | |
|
b3e099a211 | |
|
cef58c7e58 | |
|
ae8296e17e | |
|
2117b20c64 | |
|
971d169b2b | |
|
8601f66f16 | |
|
db94a329fe | |
|
16bd07ec8e | |
|
4cdfcd2fcd | |
|
90b168fa55 | |
|
fdaf7d00ac | |
|
5fab8be11b | |
|
37a25a1dd6 | |
|
bc053fb0d6 | |
|
99eb18e5a8 | |
|
ff799ec0b6 | |
|
557884198f | |
|
4cd838bc6d | |
|
cd37e5c198 | |
|
92a47d7345 | |
|
50994f3235 | |
|
d40417a478 | |
|
7635134cd8 | |
|
6bc093f5b2 | |
|
80fa089da7 | |
|
3b4fde8230 | |
|
6f15673a25 | |
|
465171f59d | |
|
6fd6744a81 | |
|
1b2ed84d2a | |
|
750d1a8d1e | |
|
75ae1d409f | |
|
5bb2ef99a0 | |
|
7879374150 | |
|
d837a0ffc3 | |
|
ede04da45e | |
|
dd0447ea8d | |
|
e8458e6e38 | |
|
52091ac0e0 | |
|
d1a5282610 | |
|
c2efca45f5 | |
|
36395ff716 | |
|
7e7efc28ba | |
|
6ee62c5990 | |
|
f6ac372438 | |
|
33fa1de0d2 | |
|
ea44f4d095 | |
|
9bdd567f7c | |
|
7810dc21a1 | |
|
69baabd2b8 | |
|
33b256c069 | |
|
eab616291a | |
|
98befc788f | |
|
4bf779853b | |
|
13a46d4eb7 | |
|
e32c9da466 | |
|
463e0570b0 | |
|
fc01f1a894 | |
|
7b8de08512 | |
|
7bb35601b9 | |
|
aa9c3e835c | |
|
c5f6ec47bd | |
|
36098a7c32 | |
|
c4176f3707 | |
|
a369b1a435 | |
|
edf9d98658 | |
|
a6bca47291 | |
|
0c8186ecd0 | |
|
2e58c7b13f | |
|
48f5f8929f | |
|
4940e4fb5e | |
|
cbe049f558 | |
|
411ac7ebd9 | |
|
d71fbcf925 | |
|
53e97162e1 | |
|
3df9d72424 | |
|
10ac87dd05 | |
|
6b54476f5c | |
|
ace11a0318 | |
|
616070b3c4 | |
|
c9c55b8b94 | |
|
3d13325375 | |
|
37972ac887 | |
|
40f0fe2ba8 | |
|
a7dd292eb0 | |
|
37b029b61a | |
|
5be51bb835 | |
|
34bd2d98d1 | |
|
6d20e4b63c | |
|
5393f4294a | |
|
355a7d93ff | |
|
17f2e6397e | |
|
7ad83496cc | |
|
f06cbc9ba9 | |
|
7760da7b7d | |
|
0abf57b90d | |
|
69770e775a | |
|
bacb79de94 | |
|
97b3f998fd | |
|
bf2fec0439 | |
|
f1046aecd5 | |
|
ee08fb30c2 | |
|
9899306882 | |
|
3a418fbd8c | |
|
2a0aa6f22c | |
|
4a8cafc034 | |
|
117756c596 | |
|
adac29beb9 | |
|
f9a6b98f30 | |
|
a74faed581 | |
|
11a0e90fbd | |
|
b069fc57e4 | |
|
ba53a52f96 | |
|
6d3a06d4f6 | |
|
83f30c3255 | |
|
0def153271 | |
|
4c52a3d7ec | |
|
2a98b2400e | |
|
400f76cae6 | |
|
ed3156f0d5 | |
|
89491b3319 | |
|
c231477d77 | |
|
698b15215d | |
|
a4cc7138a9 | |
|
f0e8f5438a | |
|
01e3ac0047 | |
|
d71caf00a0 | |
|
6e447df9e5 | |
|
bf0846cded | |
|
f20db7aaed | |
|
f28b19f428 | |
|
19693758dc | |
|
04d281602f | |
|
2eb61811f8 | |
|
37c7bd4a0f | |
|
dd2de6ed04 | |
|
2d71501327 | |
|
517109328e | |
|
c0bb8bb10f | |
|
e43a363414 | |
|
dea5e9bb3b | |
|
9a6ba96281 | |
|
7b04905d6f | |
|
0d58fe6426 | |
|
f62453bfec | |
|
d69c1e107c | |
|
dddf76e4a3 | |
|
45be0dbf64 | |
|
f80a5e04e0 | |
|
5432c70c8b | |
|
6461ef97cc | |
|
0a45c215bb | |
|
5a34ef4ad1 | |
|
e1e2fe3643 | |
|
3119422504 | |
|
086b42f5b6 | |
|
b5d7d5e6c8 | |
|
54400b259e | |
|
121d84cbb7 | |
|
6f831daacc | |
|
7d4584fe60 | |
|
bcbfebc202 | |
|
a3ad18c94c | |
|
ac3d10bf58 | |
|
c1ecbfa789 | |
|
939a4162bb | |
|
9a1c834360 | |
|
1011fa7ca1 | |
|
9602e396e0 | |
|
2fa4ca6f1c | |
|
dac19e1757 | |
|
9c7513c15b | |
|
780711c0b1 | |
|
f3842eca56 | |
|
d6677866d9 | |
|
577d1c81c6 | |
|
b00cd14cab | |
|
5bc3cc5ad3 | |
|
aa9dbb38d2 | |
|
ddb1fbcd3d | |
|
20f474448d | |
|
1aedbd44ec | |
|
1e009b6e42 | |
|
3afac53eab | |
|
48014da258 | |
|
d52b25fdb1 | |
|
56d06f6a1d | |
|
0d4c952d61 | |
|
819bbb4c23 | |
|
c07ccfa8d4 | |
|
3393a603cd | |
|
e4b5b34459 | |
|
59dd7ea55a | |
|
ab1742653a | |
|
b5b8e5885a | |
|
9b9de98ca6 | |
|
2bc83e3c79 | |
|
1141095fc8 | |
|
57a3abd2a6 | |
|
fad4557e72 | |
|
f6ec2061da | |
|
e2911d9227 | |
|
badcce96a1 | |
|
3a1f2be7a0 | |
|
73b1f9b119 | |
|
6e56fa9f37 | |
|
ad4da74bba | |
|
157754da9d | |
|
d19b104ab4 | |
|
3309f3d8e0 | |
|
4d7566b19b | |
|
38061ec035 | |
|
391b796b9a | |
|
33b410ad3b | |
|
a45d751d11 | |
|
8d4f8900d4 | |
|
3040697295 | |
|
96475f134a | |
|
272ff8fb0f | |
|
608b4c49ba | |
|
8ac935c7d4 | |
|
656a53eccd | |
|
4cda5066c8 | |
|
62af617eba | |
|
6238a5f194 | |
|
dbdf4b45cd | |
|
e0f0e03b19 | |
|
9d9dfaa7e5 | |
|
9028c291f4 | |
|
6596d2179e | |
|
2d1ea1d92b | |
|
dd33684c0b | |
|
cacb1838c2 | |
|
cbab621785 | |
|
c640444ba4 | |
|
0103a3ce27 | |
|
8aa035a2b7 | |
|
5a648a9756 | |
|
61e21505ee | |
|
26c5520226 | |
|
70e7072799 | |
|
4cd3452e0d | |
|
c52a7ddb89 | |
|
b92df0e29a | |
|
faa4f54bca | |
|
402d92954e | |
|
137fa58d22 | |
|
585113b733 | |
|
7528b2df34 | |
|
24bfe84ed4 | |
|
4701a362d0 | |
|
81adbd2d71 | |
|
51558b7dfb | |
|
b52ee3c4ed | |
|
20e8b5f3f9 | |
|
5383cae133 | |
|
fd7047bd3d | |
|
efdee6a2d1 | |
|
e82b1e5b63 | |
|
92777f00ac | |
|
c05265ea72 | |
|
eea968fc96 | |
|
cd396e6a10 | |
|
427b07d638 | |
|
a4434d222d | |
|
6eb3f33f1e | |
|
7e5c753da2 | |
|
16468a4dda | |
|
e59563fadf | |
|
a0fbc60853 | |
|
678e9105d6 | |
|
a1c685dfc8 | |
|
91a80d2a4c | |
|
01ade07075 | |
|
ea8dc7f3b6 | |
|
a770621611 | |
|
d6c0e11010 | |
|
d2fd01829d | |
|
e6f39466fb | |
|
4b32d544ce | |
|
08075c332f | |
|
a6fd98dbd1 | |
|
e3de03d33e | |
|
cb32d9612b | |
|
a50697d7c6 | |
|
800a1bc8f1 | |
|
957b1843c7 | |
|
24698f8b70 | |
|
d8a52817fb | |
|
fe7083544e | |
|
4d83fbc6b9 | |
|
051b4ea2f5 | |
|
7a29a5f548 | |
|
6241741343 | |
|
e4b1ca063c | |
|
7c206d5e6f | |
|
ff367cda32 | |
|
eadf32c095 | |
|
d151111c38 | |
|
125700bb2c | |
|
772ccd7f84 | |
|
2ecd5ad9ed | |
|
a96d5d41eb | |
|
cfc23def0b | |
|
e9284fd307 | |
|
99b806588a | |
|
f3677c82c1 | |
|
4faebf9d27 | |
|
40adcfd6fd | |
|
58256572bf | |
|
474b04ed6f | |
|
7041720158 | |
|
1d3831b28a | |
|
f8fbe3c900 | |
|
026fb5505c | |
|
05358df33b | |
|
7afa8dc336 | |
|
cd6204043c | |
|
cc5abfcdcb | |
|
25c37776d8 | |
|
35dc7a4b64 | |
|
5eecad2157 | |
|
b89b411e39 | |
|
af7cf3eff1 | |
|
788f36e2fd | |
|
54b3afbbfe | |
|
41e42d6930 | |
|
c1afac4553 | |
|
28fd8981d0 | |
|
0847a28e84 | |
|
abd1e9bf27 | |
|
a66e139f6d | |
|
f6353e0050 | |
|
74e2524e21 | |
|
8a915bf062 | |
|
3dd40b652b | |
|
c7d7deb928 | |
|
12461a0410 | |
|
be27a6e56d | |
|
fb7e0ab3ad | |
|
9c8b51e5af | |
|
eae780ebf3 | |
|
540f5bb691 | |
|
e5ba5c3232 | |
|
ddabfdf926 | |
|
372a45b1ef | |
|
5b6bb21f95 | |
|
68d38c09bf | |
|
348503fb55 | |
|
61ff8b8ed6 | |
|
f2cbb8399d | |
|
891ddf2018 | |
|
ea35baa1a4 | |
|
0b4923c602 | |
|
ebeac324f9 | |
|
7630ff03fb | |
|
18873399fe | |
|
b9a76ea202 | |
|
120000b1fe | |
|
9d6273c808 | |
|
dfb72a37dc | |
|
778649c9f8 | |
|
649302696f | |
|
44eeeb20b1 | |
|
3cf7b566fb | |
|
514fe62b0d | |
|
8b4c919b11 | |
|
d96d243327 | |
|
044bcc6494 | |
|
0a552fe393 | |
|
d648a5a89d | |
|
2f0027664e | |
|
0d08af35da | |
|
4f8becbe2f | |
|
19823f8769 | |
|
585c6a6843 | |
|
ab68043314 | |
|
46068dfe68 | |
|
e0513d159b | |
|
63d9f92c06 | |
|
ec6859dddc | |
|
55fe203cd9 | |
|
dc6bad0421 | |
|
1aa97c66c7 | |
|
a855e4dab9 | |
|
c15ef4d04b | |
|
93babfbef5 | |
|
f0a9ee39bb | |
|
b085fb7165 | |
|
f4c2d8fbb7 | |
|
8be5164acb | |
|
12445907af | |
|
a6fe392ef0 | |
|
ce9e8532f8 | |
|
b999d6f833 | |
|
44a06f0ada | |
|
4b3112cd47 | |
|
f7bb5c22d1 | |
|
32ecae62c1 | |
|
354efe2a4d | |
|
c0285789bc | |
|
f47ab097df | |
|
888eb0f6d7 | |
|
6d1232dfd1 | |
|
2594758ddb | |
|
fa74ed965f | |
|
5cef223549 | |
|
882dff3113 | |
|
b77746161f | |
|
f74c4e8afb | |
|
7a5ab073de | |
|
a6b259335a | |
|
721ed02e4c | |
|
1dc1c4a69e | |
|
a2e30f0d76 | |
|
777c666572 | |
|
63ef1275d9 | |
|
6067942e36 | |
|
1053681add | |
|
18addeeed8 | |
|
94ca421560 | |
|
509855a65c | |
|
e934e2be5d | |
|
b37d1d2db1 | |
|
ef514618e0 | |
|
f03e9e9840 | |
|
9648ab7a02 | |
|
3a8a060fe4 | |
|
3014e818e4 | |
|
de038829eb | |
|
e067cacfb8 | |
|
6bfea6285d | |
|
24e654a641 | |
|
26f133bff4 | |
|
cedaf7c235 | |
|
847dc1cde5 | |
|
1752f8a684 | |
|
a5867ac1e8 | |
|
267ac866c8 | |
|
58c4a94a8b | |
|
c6f3ea4f14 | |
|
4bcc5d5223 | |
|
fd83d579b8 | |
|
c0cd5cd70c | |
|
804582c7b4 | |
|
a773e89d61 | |
|
6debdb1c04 | |
|
e867cb9bea | |
|
9ec005c926 | |
|
0edfc35155 | |
|
a8beaa2802 | |
|
a6d9c52811 | |
|
6a28def64a | |
|
5a99a11348 | |
|
c5e5dda487 | |
|
30f1870a69 | |
|
ae13027fb6 | |
|
0fb9007d1d | |
|
c592958634 | |
|
964b58ff38 | |
|
11e27bf311 | |
|
58390ca56e | |
|
1d003e80c3 | |
|
6e90b7a9af | |
|
f771c0fe5d | |
|
997a5f361f | |
|
41f1569faa | |
|
9ec35bef60 | |
|
05f6022a04 | |
|
3d6bf56916 | |
|
c08ae217b7 | |
|
fc6eccdac4 | |
|
bfcd8e23a3 | |
|
0faacb9a73 | |
|
9fd5ca27fe | |
|
9e53edc6a7 | |
|
887dcd15cc | |
|
9581dfdc9f | |
|
d89c4c2507 | |
|
950a99f2f0 | |
|
5261c74ac1 | |
|
cd233e8160 |
|
@ -0,0 +1,54 @@
|
|||
name: BuildImage
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch: { }
|
||||
|
||||
jobs:
|
||||
build-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
oamdev/terraform-controller
|
||||
ghcr.io/kubevela/oamdev/terraform-controller
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
- name: Login docker.io
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a # v2.1.0
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
|
@ -0,0 +1,48 @@
|
|||
name: HelmChart
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
publish-charts:
|
||||
env:
|
||||
HELM_CHART: chart/
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v1
|
||||
with:
|
||||
version: v3.4.0
|
||||
- name: Get the version
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
|
||||
- name: Tag helm chart image
|
||||
run: |
|
||||
image_tag=${{ steps.get_version.outputs.VERSION }}
|
||||
chart_version=${{ steps.get_version.outputs.VERSION }}
|
||||
sed -i "s/tag: latest/tag: ${image_tag}/g" $HELM_CHART/values.yaml
|
||||
chart_semver=${chart_version#"v"}
|
||||
sed -i "s/0.1.0/$chart_semver/g" $HELM_CHART/Chart.yaml
|
||||
- uses: jnwng/github-app-installation-token-action@v2
|
||||
id: get_app_token
|
||||
with:
|
||||
appId: 340472
|
||||
installationId: 38064967
|
||||
privateKey: ${{ secrets.GH_KUBEVELA_APP_PRIVATE_KEY }}
|
||||
- name: Sync Chart Repo
|
||||
run: |
|
||||
git config --global user.email "135009839+kubevela[bot]@users.noreply.github.com"
|
||||
git config --global user.name "kubevela[bot]"
|
||||
git clone https://x-access-token:${{ steps.get_app_token.outputs.token }}@github.com/kubevela/charts.git kubevela-charts
|
||||
helm package $HELM_CHART --destination ./kubevela-charts/docs/
|
||||
helm repo index --url https://kubevela.github.io/charts ./kubevela-charts/docs/
|
||||
cd kubevela-charts/
|
||||
git add docs/
|
||||
chart_version=${GITHUB_REF#refs/tags/}
|
||||
git commit -m "update terraform-controller chart ${chart_version}"
|
||||
git push https://x-access-token:${{ steps.get_app_token.outputs.token }}@github.com/kubevela/charts.git
|
|
@ -0,0 +1,73 @@
|
|||
name: E2E Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- v*
|
||||
workflow_dispatch: {}
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
GO_VERSION: '1.23.8'
|
||||
KIND_VERSION: 'v0.12.0'
|
||||
|
||||
jobs:
|
||||
e2e-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
go get -v -t -d ./...
|
||||
|
||||
- name: Setup Kind
|
||||
uses: engineerd/setup-kind@v0.5.0
|
||||
with:
|
||||
version: ${{ env.KIND_VERSION }}
|
||||
skipClusterCreation: true
|
||||
|
||||
- name: Setup Kind Cluster
|
||||
run: |
|
||||
kind delete cluster
|
||||
kind create cluster --image kindest/node:v1.20.7@sha256:688fba5ce6b825be62a7c7fe1415b35da2bdfbb5a69227c499ea4cc0008661ca
|
||||
kubectl version
|
||||
kubectl cluster-info
|
||||
|
||||
- name: Load Image to kind cluster
|
||||
run: make kind-load
|
||||
|
||||
- name: Install chart
|
||||
run: |
|
||||
kubectl cluster-info
|
||||
echo "current-context:" $(kubectl config current-context)
|
||||
helm upgrade --install --create-namespace --namespace terraform terraform-controller ./chart --set image.tag=e2e --set image.pullPolicy=IfNotPresent --set backend.namespace=terraform --wait
|
||||
helm test -n terraform terraform-controller --timeout 5m
|
||||
kubectl get pod -n terraform -l "app=terraform-controller"
|
||||
|
||||
- name: E2E tests
|
||||
run: |
|
||||
make configuration
|
||||
env:
|
||||
TERRAFORM_BACKEND_NAMESPACE: terraform
|
||||
- name: dump controller logs
|
||||
if: ${{ always() }}
|
||||
run: kubectl logs deploy/terraform-controller -n terraform
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./e2e-coverage1.xml
|
||||
flags: e2e
|
|
@ -0,0 +1,52 @@
|
|||
name: Format
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- release-*
|
||||
workflow_dispatch: {}
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- release-*
|
||||
|
||||
env:
|
||||
# Common versions
|
||||
GO_VERSION: '1.23.8'
|
||||
KIND_VERSION: 'v0.7.0'
|
||||
|
||||
jobs:
|
||||
detect-noop:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
noop: ${{ steps.noop.outputs.should_skip }}
|
||||
steps:
|
||||
- name: Detect No-op Changes
|
||||
id: noop
|
||||
uses: fkirc/skip-duplicate-actions@v3.3.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
paths_ignore: '["**.md", "**.png", "**.jpg"]'
|
||||
do_not_skip: '["workflow_dispatch", "schedule", "push"]'
|
||||
concurrent_skipping: false
|
||||
|
||||
make-reviewable:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: detect-noop
|
||||
if: needs.detect-noop.outputs.noop != 'true'
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
id: go
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Make reviewable
|
||||
run: make reviewable
|
|
@ -1,95 +0,0 @@
|
|||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- release-*
|
||||
workflow_dispatch: {}
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- release-*
|
||||
|
||||
env:
|
||||
# Common versions
|
||||
GO_VERSION: '1.14'
|
||||
GOLANGCI_VERSION: 'v1.39'
|
||||
DOCKER_BUILDX_VERSION: 'v0.4.2'
|
||||
KIND_VERSION: 'v0.7.0'
|
||||
|
||||
jobs:
|
||||
detect-noop:
|
||||
runs-on: ubuntu-20.04
|
||||
outputs:
|
||||
noop: ${{ steps.noop.outputs.should_skip }}
|
||||
steps:
|
||||
- name: Detect No-op Changes
|
||||
id: noop
|
||||
uses: fkirc/skip-duplicate-actions@v3.3.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
paths_ignore: '["**.md", "**.png", "**.jpg"]'
|
||||
do_not_skip: '["workflow_dispatch", "schedule", "push"]'
|
||||
concurrent_skipping: false
|
||||
|
||||
unit-tests:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: detect-noop
|
||||
if: needs.detect-noop.outputs.noop != 'true'
|
||||
|
||||
steps:
|
||||
- name: Set up Go 1.14
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Cache Go Dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: .work/pkg
|
||||
key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: ${{ runner.os }}-pkg-
|
||||
|
||||
- name: Install ginkgo
|
||||
run: |
|
||||
sudo apt-get install -y golang-ginkgo-dev
|
||||
|
||||
- name: Setup Kind Cluster
|
||||
uses: engineerd/setup-kind@v0.5.0
|
||||
with:
|
||||
version: ${{ env.KIND_VERSION }}
|
||||
|
||||
- name: install Kubebuilder
|
||||
uses: wonderflow/kubebuilder-action@v1.1
|
||||
|
||||
- name: Run Make test
|
||||
run: make test
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: ./coverage.txt
|
||||
flags: unittests
|
||||
name: codecov-umbrella
|
||||
|
||||
make-reviewable:
|
||||
runs-on: ubuntu-20.04
|
||||
needs: detect-noop
|
||||
if: needs.detect-noop.outputs.noop != 'true'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Make reviewable
|
||||
run: make reviewable
|
|
@ -1,4 +1,4 @@
|
|||
name: license
|
||||
name: License
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch: { }
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: build
|
||||
strategy:
|
||||
matrix:
|
||||
TARGETS: [ linux/amd64, darwin/amd64, windows/amd64, linux/arm64, darwin/arm64 ]
|
||||
env:
|
||||
BACKUP_RESTORE_TOOL_VERSION_KEY: github.com/kubevela/terraform-controller/version.BackupRestoreToolVersion
|
||||
BACKUP_RESTORE_TOOL_VERSION: cat hack/tool/backup_restore/VERSION
|
||||
GO_BUILD_ENV: GO111MODULE=on
|
||||
DIST_DIRS: find * -type d -exec
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.23
|
||||
- name: Get release
|
||||
id: get_release
|
||||
uses: bruceadams/get-release@v1.2.2
|
||||
- name: Get matrix
|
||||
id: get_matrix
|
||||
run: |
|
||||
TARGETS=${{matrix.TARGETS}}
|
||||
echo ::set-output name=OS::${TARGETS%/*}
|
||||
echo ::set-output name=ARCH::${TARGETS#*/}
|
||||
- name: Get ldflags
|
||||
id: get_ldflags
|
||||
run: |
|
||||
LDFLAGS="-s -w -X ${{ env.BACKUP_RESTORE_TOOL_VERSION_KEY }}=${{ env.BACKUP_RESTORE_TOOL_VERSION }}"
|
||||
echo "LDFLAGS=${LDFLAGS}" >> $GITHUB_ENV
|
||||
- name: Build
|
||||
run: |
|
||||
cd ./hack/tool/backup_restore && \
|
||||
${{ env.GO_BUILD_ENV }} GOOS=${{ steps.get_matrix.outputs.OS }} GOARCH=${{ steps.get_matrix.outputs.ARCH }} \
|
||||
go build -ldflags "${{ env.LDFLAGS }}" \
|
||||
-o _bin/backup_restore/${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}/backup_restore \
|
||||
-v .
|
||||
- name: Compress
|
||||
run: |
|
||||
cd _bin/backup_restore && \
|
||||
${{ env.DIST_DIRS }} cp ../../LICENSE {} \; && \
|
||||
${{ env.DIST_DIRS }} cp ../../README.md {} \; && \
|
||||
${{ env.DIST_DIRS }} tar -zcf backup-restore-{}.tar.gz {} \; && \
|
||||
${{ env.DIST_DIRS }} zip -r backup-restore-{}.zip {} \; && \
|
||||
cd .. && \
|
||||
sha256sum backup_restore/backup-restore-* >> sha256-${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}.txt \
|
||||
- name: Upload backup-restore tar.gz
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
with:
|
||||
upload_url: ${{ steps.get_release.outputs.upload_url }}
|
||||
asset_path: ./_bin/backup_restore/backup-restore-${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}.tar.gz
|
||||
asset_name: backup-restore-${{ env.BACKUP_RESTORE_TOOL_VERSION }}-${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}.tar.gz
|
||||
asset_content_type: binary/octet-stream
|
||||
- name: Upload backup-restore zip
|
||||
uses: actions/upload-release-asset@v1.0.2
|
||||
with:
|
||||
upload_url: ${{ steps.get_release.outputs.upload_url }}
|
||||
asset_path: ./_bin/backup_restore/backup-restore-${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}.zip
|
||||
asset_name: backup-restore-${{ env.BACKUP_RESTORE_TOOL_VERSION }}-${{ steps.get_matrix.outputs.OS }}-${{ steps.get_matrix.outputs.ARCH }}.zip
|
||||
asset_content_type: binary/octet-stream
|
|
@ -0,0 +1,69 @@
|
|||
name: Unit Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- release-*
|
||||
tags:
|
||||
- v*
|
||||
workflow_dispatch: {}
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- release-*
|
||||
|
||||
env:
|
||||
# Common versions
|
||||
GO_VERSION: '1.23.8'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- uses: actions/checkout@v2
|
||||
- name: golangci-lint
|
||||
run: |
|
||||
make lint
|
||||
|
||||
unit-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Cache Go Dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .work/pkg
|
||||
key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: ${{ runner.os }}-pkg-
|
||||
|
||||
- name: Install Kubebuilder
|
||||
run: |
|
||||
wget https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.2/kubebuilder_2.3.2_linux_amd64.tar.gz
|
||||
tar -zxf kubebuilder_2.3.2_linux_amd64.tar.gz
|
||||
sudo mv kubebuilder_2.3.2_linux_amd64 /usr/local/kubebuilder
|
||||
|
||||
- name: Run Make test
|
||||
run: make test
|
||||
|
||||
- name: Upload coverage report
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./ut-coverage1.xml
|
||||
flags: unit
|
|
@ -1,6 +1,4 @@
|
|||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
@ -10,27 +8,28 @@
|
|||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
e2e-coverage1.xml
|
||||
ut-coverage1.xml
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
.vela/
|
||||
|
||||
.DS_Store
|
||||
|
||||
examples/tf-native/alibaba/json/terraform.tfstate
|
||||
examples/tf-native/alibaba/json/terraform.tfstate.backup
|
||||
examples/tf-native/alibaba/json/.terraform
|
||||
examples/tf-native/alibaba/json/.terraform.lock.hcl
|
||||
# Terraform
|
||||
terraform.tfstate
|
||||
terraform.tfstate.backup
|
||||
.terraform
|
||||
.terraform.lock.hcl
|
||||
terraform-controller-*
|
||||
examples/tf-native/alibaba/cs/kubeconfig
|
||||
|
||||
examples/tf-native/alibaba/hcl/terraform.tfstate
|
||||
examples/tf-native/alibaba/hcl/terraform.tfstate.backup
|
||||
examples/tf-native/alibaba/hcl/.terraform
|
||||
examples/tf-native/alibaba/hcl/.terraform.lock.hcl
|
||||
bin/manager
|
||||
|
||||
examples/tf-native/aws/hcl/terraform.tfstate
|
||||
examples/tf-native/aws/hcl/terraform.tfstate.backup
|
||||
examples/tf-native/aws/hcl/.terraform
|
||||
examples/tf-native/aws/hcl/.terraform.lock.hcl
|
||||
# Secret for git server
|
||||
examples/git-credentials/git-ssh-auth-secret.yaml
|
|
@ -0,0 +1,215 @@
|
|||
run:
|
||||
timeout: 10m
|
||||
|
||||
skip-files:
|
||||
|
||||
|
||||
output:
|
||||
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
|
||||
formats: colored-line-number
|
||||
|
||||
linters-settings:
|
||||
errcheck:
|
||||
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
|
||||
# default is false: such cases aren't reported by default.
|
||||
check-type-assertions: false
|
||||
|
||||
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
|
||||
# default is false: such cases aren't reported by default.
|
||||
check-blank: false
|
||||
|
||||
# [deprecated] comma-separated list of pairs of the form pkg:regex
|
||||
# the regex is used to ignore names within pkg. (default "fmt:.*").
|
||||
# see https://github.com/kisielk/errcheck#the-deprecated-method for details
|
||||
exclude-functions: fmt:.*,io/ioutil:^Read.*
|
||||
|
||||
exhaustive:
|
||||
# indicates that switch statements are to be considered exhaustive if a
|
||||
# 'default' case is present, even if all enum members aren't listed in the
|
||||
# switch
|
||||
default-signifies-exhaustive: true
|
||||
|
||||
govet:
|
||||
# report about shadowed variables
|
||||
check-shadowing: false
|
||||
|
||||
revive:
|
||||
# minimal confidence for issues, default is 0.8
|
||||
min-confidence: 0.8
|
||||
|
||||
gofmt:
|
||||
# simplify code: gofmt with `-s` option, true by default
|
||||
simplify: true
|
||||
|
||||
gocyclo:
|
||||
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
||||
min-complexity: 32
|
||||
|
||||
maligned:
|
||||
# print struct with more effective memory layout or not, false by default
|
||||
suggest-new: true
|
||||
|
||||
dupl:
|
||||
# tokens count to trigger issue, 150 by default
|
||||
threshold: 100
|
||||
|
||||
goconst:
|
||||
# minimal length of string constant, 3 by default
|
||||
min-len: 3
|
||||
# minimal occurrences count to trigger, 3 by default
|
||||
min-occurrences: 5
|
||||
|
||||
lll:
|
||||
# tab width in spaces. Default to 1.
|
||||
tab-width: 1
|
||||
|
||||
unused:
|
||||
# treat code as a program (not a library) and report unused exported identifiers; default is false.
|
||||
# XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
|
||||
# if it's called for subdir of a project it can't find funcs usages. All text editor integrations
|
||||
# with golangci-lint call it on a directory with the changed file.
|
||||
check-exported: false
|
||||
|
||||
unparam:
|
||||
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
|
||||
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
|
||||
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
|
||||
# with golangci-lint call it on a directory with the changed file.
|
||||
check-exported: false
|
||||
|
||||
nakedret:
|
||||
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
|
||||
max-func-lines: 30
|
||||
|
||||
gocritic:
|
||||
# Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint` run to see all tags and checks.
|
||||
# Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags".
|
||||
enabled-tags:
|
||||
- performance
|
||||
|
||||
settings: # settings passed to gocritic
|
||||
captLocal: # must be valid enabled check name
|
||||
paramsOnly: true
|
||||
rangeValCopy:
|
||||
sizeThreshold: 32
|
||||
|
||||
makezero:
|
||||
# Allow only slices initialized with a length of zero. Default is false.
|
||||
always: false
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- govet
|
||||
- gocyclo
|
||||
- gocritic
|
||||
- goconst
|
||||
- goimports
|
||||
- gofmt # We enable this as well as goimports for its simplify mode.
|
||||
- revive
|
||||
- unconvert
|
||||
- misspell
|
||||
- nakedret
|
||||
- staticcheck
|
||||
- gosimple
|
||||
- unused
|
||||
|
||||
presets:
|
||||
- bugs
|
||||
- unused
|
||||
fast: false
|
||||
|
||||
|
||||
issues:
|
||||
exclude-files:
|
||||
# Exclude files that are generated by controller-gen.
|
||||
- "zz_generated\\..+\\.go$"
|
||||
# Exclude test files.
|
||||
- ".*_test.go$"
|
||||
|
||||
# Excluding configuration per-path and per-linter
|
||||
exclude-rules:
|
||||
# Exclude some linters from running on tests files.
|
||||
- path: _test(ing)?\.go
|
||||
linters:
|
||||
- gocyclo
|
||||
- errcheck
|
||||
- dupl
|
||||
- gosec
|
||||
- exportloopref
|
||||
- unparam
|
||||
|
||||
# Ease some gocritic warnings on test files.
|
||||
- path: _test\.go
|
||||
text: "(unnamedResult|exitAfterDefer)"
|
||||
linters:
|
||||
- gocritic
|
||||
|
||||
# These are performance optimisations rather than style issues per se.
|
||||
# They warn when function arguments or range values copy a lot of memory
|
||||
# rather than using a pointer.
|
||||
- text: "(hugeParam|rangeValCopy):"
|
||||
linters:
|
||||
- gocritic
|
||||
|
||||
# This "TestMain should call os.Exit to set exit code" warning is not clever
|
||||
# enough to notice that we call a helper method that calls os.Exit.
|
||||
- text: "SA3000:"
|
||||
linters:
|
||||
- staticcheck
|
||||
|
||||
- text: "k8s.io/api/core/v1"
|
||||
linters:
|
||||
- goimports
|
||||
|
||||
# This is a "potential hardcoded credentials" warning. It's triggered by
|
||||
# any variable with 'secret' in the same, and thus hits a lot of false
|
||||
# positives in Kubernetes land where a Secret is an object type.
|
||||
- text: "G101:"
|
||||
linters:
|
||||
- gosec
|
||||
- gas
|
||||
|
||||
# This is an 'errors unhandled' warning that duplicates errcheck.
|
||||
- text: "G104:"
|
||||
linters:
|
||||
- gosec
|
||||
- gas
|
||||
|
||||
# The Azure AddToUserAgent method appends to the existing user agent string.
|
||||
# It returns an error if you pass it an empty string lettinga you know the
|
||||
# user agent did not change, making it more of a warning.
|
||||
- text: \.AddToUserAgent
|
||||
linters:
|
||||
- errcheck
|
||||
|
||||
- text: "don't use an underscore"
|
||||
linters:
|
||||
- revive
|
||||
|
||||
- text: "package-comments:"
|
||||
linters:
|
||||
- revive
|
||||
|
||||
- text: "exported:"
|
||||
linters:
|
||||
- revive
|
||||
|
||||
# Independently from option `exclude` we use default exclude patterns,
|
||||
# it can be disabled by this option. To list all
|
||||
# excluded by default patterns execute `golangci-lint run --help`.
|
||||
# Default value for this option is true.
|
||||
exclude-use-default: false
|
||||
|
||||
# Show only new issues: if there are unstaged changes or untracked files,
|
||||
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
|
||||
# It's a super-useful option for integration of golangci-lint into existing
|
||||
# large codebase. It's not practical to fix all existing issues at the moment
|
||||
# of integration: much better don't allow issues in new code.
|
||||
# Default is false.
|
||||
new: false
|
||||
|
||||
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
|
||||
max-per-linter: 0
|
||||
|
||||
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
||||
max-same-issues: 0
|
|
@ -0,0 +1,161 @@
|
|||
# Contributing to Terraform Controller
|
||||
|
||||
Thanks for contributing to Terraform Controller!
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Go
|
||||
|
||||
Go version `>=1.17` is required.
|
||||
|
||||
- Helm Cli (optional)
|
||||
|
||||
Refer to [Helm official Doc](https://helm.sh/docs/intro/install/) to install `helm` Cli.
|
||||
|
||||
## How to start up the project
|
||||
|
||||
- Apply CRDs to a Kubernetes cluster
|
||||
|
||||
```shell
|
||||
$ make install
|
||||
go: creating new go.mod: module tmp
|
||||
...
|
||||
go: downloading sigs.k8s.io/controller-tools v0.6.0
|
||||
go: downloading k8s.io/apiextensions-apiserver v0.21.1
|
||||
go: downloading k8s.io/apimachinery v0.21.1
|
||||
go: downloading k8s.io/api v0.21.1
|
||||
go: downloading k8s.io/utils v0.0.0-20201110183641-67b214c5f920
|
||||
go: downloading k8s.io/klog/v2 v2.8.0
|
||||
go: downloading sigs.k8s.io/structured-merge-diff/v4 v4.1.0
|
||||
/Users/zhouzhengxi/go/bin/controller-gen "crd:trivialVersions=true" webhook paths="./..." output:crd:artifacts:config=chart/crds
|
||||
kubectl apply -f chart/crds
|
||||
customresourcedefinition.apiextensions.k8s.io/configurations.terraform.core.oam.dev created
|
||||
customresourcedefinition.apiextensions.k8s.io/providers.terraform.core.oam.dev created
|
||||
```
|
||||
|
||||
- Run Terraform Controller
|
||||
|
||||
```shell
|
||||
$ make run
|
||||
go: creating new go.mod: module tmp
|
||||
...
|
||||
go: downloading sigs.k8s.io/yaml v1.2.0
|
||||
/Users/zhouzhengxi/go/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
|
||||
go fmt ./...
|
||||
go vet ./...
|
||||
/Users/zhouzhengxi/go/bin/controller-gen "crd:trivialVersions=true" webhook paths="./..." output:crd:artifacts:config=chart/crds
|
||||
go run ./main.go
|
||||
I0227 12:15:32.890376 38818 request.go:668] Waited for 1.001811845s due to client-side throttling, not priority and fairness, request: GET:https://47.242.126.78:6443/apis/apigatewayv2.aws.crossplane.io/v1alpha1?timeout=32s
|
||||
```
|
||||
|
||||
## An development example for Cloud Resources Management
|
||||
|
||||
Let's take Alibaba Cloud as an example.
|
||||
|
||||
### Apply Provider credentials
|
||||
|
||||
```shell
|
||||
$ export ALICLOUD_ACCESS_KEY=xxx; export ALICLOUD_SECRET_KEY=yyy
|
||||
```
|
||||
|
||||
If you'd like to use Alicloud Security Token Service, also export `ALICLOUD_SECURITY_TOKEN`.
|
||||
```shell
|
||||
$ export ALICLOUD_SECURITY_TOKEN=zzz
|
||||
```
|
||||
|
||||
```
|
||||
$ make alibaba
|
||||
```
|
||||
|
||||
### Apply Terraform Configuration
|
||||
|
||||
Apply [OSS Terraform Configuration](./examples/alibaba/oss/configuration_hcl_bucket.yaml) to provision an Alibaba OSS bucket.
|
||||
|
||||
```shell
|
||||
$ kubectl get configuration.terraform.core.oam.dev
|
||||
NAME AGE
|
||||
alibaba-oss 1h
|
||||
|
||||
$ kubectl get configuration.terraform.core.oam.dev alibaba-oss -o yaml
|
||||
apiVersion: terraform.core.oam.dev/v1beta1
|
||||
kind: Configuration
|
||||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/last-applied-configuration: |
|
||||
{"apiVersion":"terraform.core.oam.dev/v1beta1","kind":"Configuration","metadata":{"annotations":{},"name":"alibaba-oss","namespace":"default"},"spec":{"JSON":"{\n \"resource\": {\n \"alicloud_oss_bucket\": {\n \"bucket-acl\": {\n \"bucket\": \"${var.bucket}\",\n \"acl\": \"${var.acl}\"\n }\n }\n },\n \"output\": {\n \"BUCKET_NAME\": {\n \"value\": \"${alicloud_oss_bucket.bucket-acl.bucket}.${alicloud_oss_bucket.bucket-acl.extranet_endpoint}\"\n }\n },\n \"variable\": {\n \"bucket\": {\n \"default\": \"poc\"\n },\n \"acl\": {\n \"default\": \"private\"\n }\n }\n}\n","variable":{"acl":"private","bucket":"vela-website"},"writeConnectionSecretToRef":{"name":"oss-conn","namespace":"default"}}}
|
||||
creationTimestamp: "2021-04-02T08:17:08Z"
|
||||
generation: 2
|
||||
spec:
|
||||
...
|
||||
variable:
|
||||
acl: private
|
||||
bucket: vela-website
|
||||
writeConnectionSecretToRef:
|
||||
name: oss-conn
|
||||
namespace: default
|
||||
status:
|
||||
outputs:
|
||||
BUCKET_NAME:
|
||||
type: string
|
||||
value: vela-website.oss-cn-beijing.aliyuncs.com
|
||||
state: provisioned
|
||||
```
|
||||
|
||||
Use [ossutil cli](https://www.alibabacloud.com/help/en/doc-detail/207217.htm) to check whether OSS bucket is provisioned.
|
||||
|
||||
```shell
|
||||
$ ossutil ls oss://
|
||||
CreationTime Region StorageClass BucketName
|
||||
2021-04-10 00:42:09 +0800 CST oss-cn-beijing Standard oss://vela-website
|
||||
Bucket Number is: 1
|
||||
|
||||
0.146789(s) elapsed
|
||||
```
|
||||
|
||||
- Check the generated connection secret
|
||||
|
||||
```shell
|
||||
$ kubectl get secret oss-conn
|
||||
NAME TYPE DATA AGE
|
||||
oss-conn Opaque 1 2m41s
|
||||
```
|
||||
|
||||
### Update Configuration
|
||||
|
||||
Change the OSS ACL to `public-read`.
|
||||
|
||||
```yaml
|
||||
apiVersion: terraform.core.oam.dev/v1beta1
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: alibaba-oss
|
||||
spec:
|
||||
...
|
||||
|
||||
variable:
|
||||
...
|
||||
acl: "public-read"
|
||||
|
||||
```
|
||||
|
||||
### Delete Configuration
|
||||
|
||||
Delete the configuration will destroy the OSS cloud resource.
|
||||
|
||||
```shell
|
||||
$ kubectl delete configuration.terraform.core.oam.dev alibaba-oss
|
||||
configuration.terraform.core.oam.dev "alibaba-oss" deleted
|
||||
|
||||
$ ossutil ls oss://
|
||||
Bucket Number is: 0
|
||||
|
||||
0.030917(s) elapsed
|
||||
```
|
||||
|
||||
## Generate CRDs
|
||||
|
||||
```shell
|
||||
$ make manifests
|
||||
go: creating new go.mod: module tmp
|
||||
/Users/zhouzhengxi/go/bin/controller-gen "crd:trivialVersions=true" webhook paths="./..." output:crd:artifacts:config=chart/crds
|
||||
```
|
|
@ -0,0 +1,65 @@
|
|||
# Design
|
||||
|
||||

|
||||
|
||||
## Components
|
||||
|
||||
### Provider
|
||||
|
||||
The `Provider` object is used to accept credentials from a Cloud provider, like Alibaba Cloud or AWS. For example, `ALICLOUD_ACCESS_KEY`,
|
||||
`ALICLOUD_SECRET_KEY` from `Provider` will be used by `terraform init`.
|
||||
|
||||
This component is inspired by [Crossplane runtime](https://crossplane.io/), which can support various cloud providers.
|
||||
|
||||
### Configuration
|
||||
|
||||
The `Configuration` object is used to accept Terraform HCL/JSON configuration provisioning, updating and deletion. It covers
|
||||
the whole lifecycle of a cloud resource.
|
||||
|
||||
- Configuration init component
|
||||
|
||||
This init component will retrieve HCL/JSON configuration from the object and store it to ConfigMap `aliyun-${ConfigurationName}-tf-input`.
|
||||
|
||||
During creation stage, it will mount the ConfigMap to a volume and copy the Terraform configuration file to the working directory.
|
||||
|
||||
During update stage, it will mount Terraform state file ConfigMap `aliyun-${ConfigurationName}-tf-state`, which will be generated
|
||||
after a cloud resource is successfully provisioned, to the volume and copy it to the working directory.
|
||||
|
||||
This component is taken upon by container `pause`.
|
||||
|
||||
- Terraform configuration executor component
|
||||
|
||||
This executor component will perform `terraform init` and `terraform apply`. After a cloud resource is successfully provisioned,
|
||||
Terraform state file will be generated.
|
||||
|
||||
This executor is job, which has the ability to retry and auto-recovery from failures.
|
||||
|
||||
It's taken upon by container oam-dev/docker-terraform:0.14.10, which is built from [oamdev/docker-terraform](https://github.com/oam-dev/docker-terraform.git).
|
||||
|
||||
|
||||
- Terraform state file retriever
|
||||
|
||||
This component is relatively simple, which will monitor the generation of Terraform state file. Upon the state file is
|
||||
generated, it will store the file content to ConfigMap `aliyun-${ConfigurationName}-tf-state`, which will be used during
|
||||
`Configuration` update and deletion stage.
|
||||
|
||||
This component is taken upon by the container zzxwill/terraform-tfstate-retriever:v0.2, which built from [terraform-tfstate-retriever](https://github.com/zzxwill/terraform-tfstate-retriever).
|
||||
|
||||
## Technical alternatives
|
||||
|
||||
### Why taking Crossplane ProviderConfiguration as cloud credentials Provider?
|
||||
|
||||
As Terraform controller is intended to support various Cloud providers, like AWS, Azure, Alibaba Cloud, GCP, and VMWare.
|
||||
Crossplane new `ProviderConfiguration` is known as it mature model for these cloud providers. By utilizing the model, this
|
||||
controller can support various cloud providers at the very first day.
|
||||
|
||||
### Why choosing ConfigMap as the storage system over cloud shared disks or Object storage system?
|
||||
|
||||
By using ConfigMap to store terraform configuration files and generated state file will be a generic way for nearly all
|
||||
Kubernetes clusters.
|
||||
|
||||
By using cloud shared volumes/Object Storage System(like Alibaba OSS, and AWS S3), it's straight forward as terraform
|
||||
HCL/JSON configuration and generated state are files. But we have to adapt to various cloud providers with various storage
|
||||
solution like cloud disk or OSS to Alibaba Cloud, s3 to AWS.
|
||||
|
||||
Here is a drawback for the choice: we have to grant the Pod in the Job to create ConfigMaps.
|
11
Dockerfile
11
Dockerfile
|
@ -1,5 +1,5 @@
|
|||
# Build the manager binary
|
||||
FROM golang:1.13 as builder
|
||||
FROM golang:1.23-alpine as builder
|
||||
|
||||
WORKDIR /workspace
|
||||
# Copy the Go Modules manifests
|
||||
|
@ -19,9 +19,14 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager
|
|||
|
||||
# Use distroless as minimal base image to package the manager binary
|
||||
# Refer to https://github.com/GoogleContainerTools/distroless for more details
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
FROM alpine
|
||||
WORKDIR /
|
||||
COPY --from=builder /workspace/manager .
|
||||
USER nonroot:nonroot
|
||||
#USER nonroot:nonroot
|
||||
|
||||
# COPY terraform binary
|
||||
COPY bin/terraform /usr/bin/terraform
|
||||
#RUN chmod +x /usr/bin/terraform
|
||||
RUN apk add git
|
||||
|
||||
ENTRYPOINT ["/manager"]
|
||||
|
|
201
Makefile
201
Makefile
|
@ -1,8 +1,15 @@
|
|||
|
||||
# Image URL to use all building/pushing image targets
|
||||
IMG ?= controller:latest
|
||||
IMG ?= oamdev/terraform-controller:0.2.8
|
||||
|
||||
# Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
|
||||
CRD_OPTIONS ?= "crd:trivialVersions=true"
|
||||
CRD_OPTIONS ?= "crd"
|
||||
|
||||
TIME_SHORT = `date +%H:%M:%S`
|
||||
TIME = $(TIME_SHORT)
|
||||
GREEN := $(shell printf "\033[32m")
|
||||
CNone := $(shell printf "\033[0m")
|
||||
OK = echo ${TIME} ${GREEN}[ OK ]${CNone}
|
||||
|
||||
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
|
||||
ifeq (,$(shell go env GOBIN))
|
||||
|
@ -15,7 +22,7 @@ all: manager
|
|||
|
||||
# Run tests
|
||||
test: generate fmt vet manifests
|
||||
go test ./... -coverprofile cover.out
|
||||
go test -coverprofile=ut-coverage1.xml ./controllers/...
|
||||
|
||||
# Build manager binary
|
||||
manager: generate fmt vet
|
||||
|
@ -27,11 +34,11 @@ run: generate fmt vet manifests
|
|||
|
||||
# Install CRDs into a cluster
|
||||
install: manifests
|
||||
kustomize build config/crd | kubectl apply -f -
|
||||
kubectl apply -f chart/crds
|
||||
|
||||
# Uninstall CRDs from a cluster
|
||||
uninstall: manifests
|
||||
kustomize build config/crd | kubectl delete -f -
|
||||
kustomize build chart/crds | kubectl delete -f -
|
||||
|
||||
# Deploy controller in the configured Kubernetes cluster in ~/.kube/config
|
||||
deploy: manifests
|
||||
|
@ -40,7 +47,7 @@ deploy: manifests
|
|||
|
||||
# Generate manifests e.g. CRD, RBAC etc.
|
||||
manifests: controller-gen
|
||||
$(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
|
||||
$(CONTROLLER_GEN) $(CRD_OPTIONS) webhook paths="./..." output:crd:artifacts:config=chart/crds
|
||||
|
||||
# Run go fmt against code
|
||||
fmt: goimports
|
||||
|
@ -62,6 +69,10 @@ docker-build: test
|
|||
docker-push:
|
||||
docker push ${IMG}
|
||||
|
||||
# Make helm chart
|
||||
chart: docker-build docker-push
|
||||
helm package chart --destination .
|
||||
|
||||
# find or download controller-gen
|
||||
# download controller-gen if necessary
|
||||
controller-gen:
|
||||
|
@ -71,7 +82,7 @@ ifeq (, $(shell which controller-gen))
|
|||
CONTROLLER_GEN_TMP_DIR=$$(mktemp -d) ;\
|
||||
cd $$CONTROLLER_GEN_TMP_DIR ;\
|
||||
go mod init tmp ;\
|
||||
go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.2.5 ;\
|
||||
go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.5 ;\
|
||||
rm -rf $$CONTROLLER_GEN_TMP_DIR ;\
|
||||
}
|
||||
CONTROLLER_GEN=$(GOBIN)/controller-gen
|
||||
|
@ -79,7 +90,7 @@ else
|
|||
CONTROLLER_GEN=$(shell which controller-gen)
|
||||
endif
|
||||
|
||||
GOLANGCILINT_VERSION ?= v1.31.0
|
||||
GOLANGCILINT_VERSION ?= v1.60.1
|
||||
HOSTOS := $(shell uname -s | tr '[:upper:]' '[:lower:]')
|
||||
HOSTARCH := $(shell uname -m)
|
||||
ifeq ($(HOSTARCH),x86_64)
|
||||
|
@ -87,16 +98,20 @@ HOSTARCH := amd64
|
|||
endif
|
||||
|
||||
golangci:
|
||||
ifeq (, $(shell which golangci-lint))
|
||||
ifneq ($(shell which golangci-lint),)
|
||||
@$(OK) golangci-lint is already installed
|
||||
GOLANGCILINT=$(shell which golangci-lint)
|
||||
else ifeq (, $(shell which $(GOBIN)/golangci-lint))
|
||||
@{ \
|
||||
set -e ;\
|
||||
echo 'installing golangci-lint-$(GOLANGCILINT_VERSION)' ;\
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOBIN) $(GOLANGCILINT_VERSION) ;\
|
||||
echo 'Install succeed' ;\
|
||||
echo 'Successfully installed' ;\
|
||||
}
|
||||
GOLANGCILINT=$(GOBIN)/golangci-lint
|
||||
else
|
||||
GOLANGCILINT=$(shell which golangci-lint)
|
||||
@$(OK) golangci-lint is already installed
|
||||
GOLANGCILINT=$(GOBIN)/golangci-lint
|
||||
endif
|
||||
|
||||
lint: golangci
|
||||
|
@ -116,9 +131,169 @@ goimports:
|
|||
ifeq (, $(shell which goimports))
|
||||
@{ \
|
||||
set -e ;\
|
||||
GO111MODULE=off go get -u golang.org/x/tools/cmd/goimports ;\
|
||||
go install golang.org/x/tools/cmd/goimports@latest ;\
|
||||
}
|
||||
GOIMPORTS=$(GOBIN)/goimports
|
||||
else
|
||||
GOIMPORTS=$(shell which goimports)
|
||||
endif
|
||||
endif
|
||||
|
||||
install-chart:
|
||||
helm lint ./chart
|
||||
helm upgrade --install --create-namespace --namespace terraform terraform-controller ./chart
|
||||
helm test -n terraform terraform-controller --timeout 5m
|
||||
kubectl get pod -n terraform -l "app=terraform-controller"
|
||||
|
||||
# load docker image to the kind cluster
|
||||
kind-load:
|
||||
docker build -t oamdev/terraform-controller:e2e .
|
||||
kind load docker-image oamdev/terraform-controller:e2e
|
||||
|
||||
|
||||
alibaba-credentials:
|
||||
ifeq (, $(ALICLOUD_ACCESS_KEY))
|
||||
@echo "Environment variable ALICLOUD_ACCESS_KEY is not set"
|
||||
exit 1
|
||||
endif
|
||||
|
||||
ifeq (, $(ALICLOUD_SECRET_KEY))
|
||||
@echo "Environment variable ALICLOUD_SECRET_KEY is not set"
|
||||
exit 1
|
||||
endif
|
||||
|
||||
echo "accessKeyID: ${ALICLOUD_ACCESS_KEY}\naccessKeySecret: ${ALICLOUD_SECRET_KEY}\nsecurityToken: ${ALICLOUD_SECURITY_TOKEN}" > alibaba-credentials.conf
|
||||
kubectl create secret generic alibaba-account-creds -n vela-system --from-file=credentials=alibaba-credentials.conf
|
||||
rm -f alibaba-credentials.conf
|
||||
kubectl get secret -n vela-system alibaba-account-creds
|
||||
|
||||
alibaba-provider:
|
||||
kubectl apply -f examples/alibaba/provider.yaml
|
||||
|
||||
alibaba: alibaba-credentials alibaba-provider
|
||||
|
||||
|
||||
aws-credentials:
|
||||
ifeq (, $(AWS_ACCESS_KEY_ID))
|
||||
@echo "Environment variable AWS_ACCESS_KEY_ID is not set"
|
||||
exit 1
|
||||
endif
|
||||
|
||||
ifeq (, $(AWS_SECRET_ACCESS_KEY))
|
||||
@echo "Environment variable AWS_SECRET_ACCESS_KEY is not set"
|
||||
exit 1
|
||||
endif
|
||||
|
||||
# refer to https://registry.terraform.io/providers/hashicorp/aws/latest/docs
|
||||
echo "awsAccessKeyID: ${AWS_ACCESS_KEY_ID}\nawsSecretAccessKey: ${AWS_SECRET_ACCESS_KEY}\nawsSessionToken: ${AWS_SESSION_TOKEN}" > aws-credentials.conf
|
||||
kubectl create secret generic aws-account-creds -n vela-system --from-file=credentials=aws-credentials.conf
|
||||
rm -f aws-credentials.conf
|
||||
|
||||
aws-provider:
|
||||
kubectl apply -f examples/aws/provider.yaml
|
||||
|
||||
aws: aws-credentials aws-provider
|
||||
|
||||
|
||||
azure-credentials:
|
||||
ifeq (, $(ARM_CLIENT_ID))
|
||||
@echo "Environment variable ARM_CLIENT_ID is not set"
|
||||
exit 1
|
||||
endif
|
||||
|
||||
ifeq (, $(ARM_CLIENT_SECRET))
|
||||
@echo "Environment variable ARM_CLIENT_SECRET is not set"
|
||||
exit 1
|
||||
endif
|
||||
|
||||
ifeq (, $(ARM_SUBSCRIPTION_ID))
|
||||
@echo "Environment variable ARM_SUBSCRIPTION_ID is not set"
|
||||
exit 1
|
||||
endif
|
||||
|
||||
ifeq (, $(ARM_TENANT_ID))
|
||||
@echo "Environment variable ARM_TENANT_ID is not set"
|
||||
exit 1
|
||||
endif
|
||||
|
||||
echo "armClientID: ${ARM_CLIENT_ID}\narmClientSecret: ${ARM_CLIENT_SECRET}\narmSubscriptionID: ${ARM_SUBSCRIPTION_ID}\narmTenantID: ${ARM_TENANT_ID}" > azure-credentials.conf
|
||||
kubectl create secret generic azure-account-creds -n vela-system --from-file=credentials=azure-credentials.conf
|
||||
rm -f azure-credentials.conf
|
||||
|
||||
azure-provider:
|
||||
kubectl apply -f examples/azure/provider.yaml
|
||||
|
||||
azure: azure-credentials azure-provider
|
||||
|
||||
|
||||
ucloud-credentials:
|
||||
ifeq (, $(UCLOUD_PRIVATE_KEY))
|
||||
@echo "Environment variable UCLOUD_PRIVATE_KEY is not set"
|
||||
exit 1
|
||||
endif
|
||||
|
||||
ifeq (, $(UCLOUD_PUBLIC_KEY))
|
||||
@echo "Environment variable UCLOUD_PUBLIC_KEY is not set"
|
||||
exit 1
|
||||
endif
|
||||
|
||||
ifeq (, $(UCLOUD_PROJECT_ID))
|
||||
@echo "Environment variable UCLOUD_PROJECT_ID is not set"
|
||||
exit 1
|
||||
endif
|
||||
|
||||
ifeq (, $(UCLOUD_REGION))
|
||||
@echo "Environment variable UCLOUD_REGION is not set"
|
||||
exit 1
|
||||
endif
|
||||
echo "publicKey: ${UCLOUD_PUBLIC_KEY}\nprivateKey: ${UCLOUD_PRIVATE_KEY}\nregion: ${UCLOUD_REGION}\nprojectID: ${UCLOUD_PROJECT_ID}" > ucloud-credentials.conf
|
||||
kubectl create secret generic ucloud-account-creds -n vela-system --from-file=credentials=ucloud-credentials.conf
|
||||
rm -f ucloud-credentials.conf
|
||||
|
||||
ucloud-provider:
|
||||
kubectl apply -f examples/ucloud/provider.yaml
|
||||
|
||||
ucloud: ucloud-credentials ucloud-provider
|
||||
|
||||
|
||||
custom-credentials:
|
||||
echo "Token: mytoken" > custom-credentials.conf
|
||||
kubectl create secret generic custom-account-creds -n vela-system --from-file=credentials=custom-credentials.conf
|
||||
rm -f custom-credentials.conf
|
||||
|
||||
custom-provider:
|
||||
kubectl apply -f examples/custom/provider.yaml
|
||||
|
||||
custom: custom-credentials custom-provider
|
||||
|
||||
|
||||
configuration:
|
||||
go test -coverprofile=e2e-coverage1.xml -v $(shell go list ./e2e/...|grep -v controllernamespace) -count=1
|
||||
go test -v ./e2e/controllernamespace/...
|
||||
|
||||
e2e-setup: install-chart alibaba
|
||||
|
||||
e2e: e2e-setup configuration
|
||||
|
||||
e2e-gitee:
|
||||
go test -coverprofile=e2e-gitee-coverage1.xml -v ./gitee/...
|
||||
|
||||
tencent-credentials:
|
||||
ifeq (, $(TENCENTCLOUD_SECRET_ID))
|
||||
@echo "Environment variable TENCENTCLOUD_SECRET_ID is not set"
|
||||
exit 1
|
||||
endif
|
||||
|
||||
ifeq (, $(TENCENTCLOUD_SECRET_KEY))
|
||||
@echo "Environment variable TENCENTCLOUD_SECRET_KEY is not set"
|
||||
exit 1
|
||||
endif
|
||||
|
||||
echo "secretID: ${TENCENTCLOUD_SECRET_ID}\nsecretKey: ${TENCENTCLOUD_SECRET_KEY}" > tencent-credentials.conf
|
||||
kubectl create secret generic tencent-account-creds -n vela-system --from-file=credentials=tencent-credentials.conf
|
||||
rm -f tencent-credentials.conf
|
||||
kubectl get secret -n vela-system tencent-account-creds
|
||||
|
||||
tencent-provider:
|
||||
kubectl apply -f examples/tencent/provider.yaml
|
||||
|
||||
tencent: tencent-credentials tencent-provider
|
||||
|
|
462
README.md
462
README.md
|
@ -1,6 +1,10 @@
|
|||
[](https://goreportcard.com/report/github.com/oam-dev/terraform-controller)
|
||||

|
||||
[](https://codecov.io/gh/oam-dev/terraform-controller)
|
||||
|
||||
# Terraform Controller
|
||||
|
||||
Terraform Controller is a Kubernetes Controller for Terraform, which can address the requirement of [Using Terraform HCL as IaC module in KubeVela](https://github.com/oam-dev/kubevela/issues/698)
|
||||
Terraform Controller is a Kubernetes Controller for Terraform.
|
||||
|
||||

|
||||
|
||||
|
@ -8,452 +12,32 @@ Terraform Controller is a Kubernetes Controller for Terraform, which can address
|
|||
|
||||
## Supported Cloud Providers
|
||||
|
||||
- Alibaba Cloud
|
||||
- AWS
|
||||
| Cloud Provider | Contributor |
|
||||
|----------------------------------------------------------------------------------------------------------------------|----------------------------------------------------|
|
||||
| [Alibaba Cloud](https://www.alibabacloud.com/) | KubeVela team |
|
||||
| [AWS](https://aws.amazon.com/) | KubeVela team |
|
||||
| [Azure](https://portal.azure.com/) | KubeVela team |
|
||||
| [Elastic Cloud](https://www.elastic.co/) | [@mattkirby](https://github.com/mattkirby) |
|
||||
| [GCP](https://cloud.google.com/) | [@emanuelr93](https://github.com/emanuelr93) |
|
||||
| [VMware vSphere](https://www.vmware.com/hk/products/vsphere.html) | [@just-do1](https://github.com/just-do1) |
|
||||
| [UCloud](https://www.ucloud.cn/) | [@wangwang](https://github.com/wangwang) |
|
||||
| [Custom](https://github.com/oam-dev/terraform-controller/blob/master/examples/custom/configuration_hcl_example.yaml) | [@evanli18](https://github.com/evanli18) |
|
||||
| [Tencent Cloud](https://cloud.tencent.com/) | [@captainroy-hy](https://github.com/captainroy-hy) |
|
||||
| [Baidu Cloud](https://cloud.baidu.com/) | KubeVela team |
|
||||
|
||||
## Supported Terraform Configuration
|
||||
|
||||
- HCL
|
||||
- JSON
|
||||
|
||||
# Design
|
||||
|
||||
## Components
|
||||
|
||||
### Provider
|
||||
|
||||
The `Provider` object is used to accept credentials from a Cloud provider, like Alibaba Cloud or AWS. For example, `ALICLOUD_ACCESS_KEY`,
|
||||
`ALICLOUD_SECRET_KEY` from `Provider` will be used by `terraform init`.
|
||||
|
||||
This component is inspired by [Crossplane runtime](https://crossplane.io/), which can support various cloud providers.
|
||||
|
||||
### Configuration
|
||||
|
||||
The `Configuration` object is used to accept Terraform HCL/JSON configuration provisioning, updating and deletion. It covers
|
||||
the whole lifecycle of a cloud resource.
|
||||
|
||||
- Configuration init component
|
||||
|
||||
This init component will retrieve HCL/JSON configuration from the object and store it to ConfigMap `aliyun-${ConfigurationName}-tf-input`.
|
||||
|
||||
During creation stage, it will mount the ConfigMap to a volume and copy the Terraform configuration file to the working directory.
|
||||
|
||||
During update stage, it will mount Terraform state file ConfigMap `aliyun-${ConfigurationName}-tf-state`, which will be generated
|
||||
after a cloud resource is successfully provisioned, to the volume and copy it to the working directory.
|
||||
|
||||
This component is taken upon by container `pause`.
|
||||
|
||||
- Terraform configuration executor component
|
||||
|
||||
This executor component will perform `terrform init` and `terraform apply`. After a cloud resource is successfully provisioned,
|
||||
Terraform state file will be generated.
|
||||
|
||||
This executor is job, which has the ability to retry and auto-recovery from failures.
|
||||
|
||||
It's taken upon by container zzxwill/docker-terraform:0.14.10, which is built from [zzxwill/broadinstitute-docker-terraform](https://github.com/zzxwill/broadinstitute-docker-terraform.git).
|
||||
|
||||
|
||||
- Terraform state file retriever
|
||||
|
||||
This component is relatively simple, which will monitor the generation of Terraform state file. Upon the state file is
|
||||
generated, it will store the file content to ConfigMap `aliyun-${ConfigurationName}-tf-state`, which will be used during
|
||||
`Configuration` update and deletion stage.
|
||||
|
||||
This component is taken upon by the container zzxwill/terraform-tfstate-retriever:v0.2, which built from [terraform-tfstate-retriever](https://github.com/zzxwill/terraform-tfstate-retriever).
|
||||
|
||||
## Technical alternatives
|
||||
|
||||
### Why taking Crossplane ProviderConfiguration as cloud credentials Provider?
|
||||
|
||||
As Terraform controller is intended to support various Cloud providers, like AWS, Azure, Alibaba Cloud, GCP, and VMWare.
|
||||
Crossplane new `ProviderConfiguration` is known as it mature model for these cloud providers. By utilizing the model, this
|
||||
controller can support various cloud providers at the very first day.
|
||||
|
||||
### Why choosing ConfigMap as the storage system over cloud shared disks or Object storage system?
|
||||
|
||||
By using ConfigMap to store terraform configuration files and generated state file will be a generic way for nearly all
|
||||
Kubernetes clusters.
|
||||
|
||||
By using cloud shared volumes/Object Storage System(like Alibaba OSS, and AWS S3), it's straight forward as terraform
|
||||
HCL/JSON configuration and generated state are files. But we have to adapt to various cloud providers with various storage
|
||||
solution like cloud disk or OSS to Alibaba Cloud, s3 to AWS.
|
||||
|
||||
Here is a drawback for the choice: we have to grant the Pod in the Job to create ConfigMaps.
|
||||
- JSON (Deprecated in v0.3.1, removed in v0.4.6)
|
||||
|
||||
# Get started
|
||||
|
||||
- Install the controller
|
||||
See our [Getting Started](./getting-started.md) guide please.
|
||||
|
||||
## Alibaba Cloud
|
||||
# Design
|
||||
|
||||
### Locally run Terraform Controller
|
||||
Please refer to [Design](./DESIGN.md).
|
||||
|
||||
Get the codebase from [release v0.1-alpha.1](https://github.com/zzxwill/terraform-controller/releases/tag/v0.1-alpha.1),
|
||||
and run it locally.
|
||||
# Contributing
|
||||
|
||||
### Apply Provider configuration
|
||||
|
||||
```shell
|
||||
$ export ALICLOUD_ACCESS_KEY=xxx; export ALICLOUD_SECRET_KEY=yyy
|
||||
|
||||
$ sh hack/prepare-alibaba-credentials.sh
|
||||
|
||||
$ kubectl get secret -n vela-system
|
||||
NAME TYPE DATA AGE
|
||||
alibaba-account-creds Opaque 1 11s
|
||||
|
||||
$ k apply -f examples/alibaba/provider.yaml
|
||||
provider.terraform.core.oam.dev/default created
|
||||
```
|
||||
|
||||
### Authenticate pods to create ConfigMaps
|
||||
|
||||
Terraform state file is essential to update or destroy cloud resources. After terraform execution completes, its state file
|
||||
needs to be stored to a ConfigMap.
|
||||
|
||||
```shell
|
||||
$ kubectl apply -f examples/rbac.yaml
|
||||
clusterrole.rbac.authorization.k8s.io/tf-clusterrole created
|
||||
clusterrolebinding.rbac.authorization.k8s.io/tf-binding created
|
||||
```
|
||||
|
||||
### Apply Terraform Configuration
|
||||
|
||||
Apply Terraform configuration [configuration_hcl_oss.yaml](./examples/alibaba/configuration_hcl_oss.yaml) (JSON configuration [configuration_oss.yaml](./examples/alibaba/configuration_json_oss.yaml) is also supported) to provision an Alibaba OSS bucket.
|
||||
|
||||
```yaml
|
||||
apiVersion: terraform.core.oam.dev/v1beta1
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: aliyun-oss
|
||||
spec:
|
||||
hcl: |
|
||||
resource "alicloud_oss_bucket" "bucket-acl" {
|
||||
bucket = var.bucket
|
||||
acl = var.acl
|
||||
}
|
||||
|
||||
output "BUCKET_NAME" {
|
||||
value = "${alicloud_oss_bucket.bucket-acl.bucket}.${alicloud_oss_bucket.bucket-acl.extranet_endpoint}"
|
||||
}
|
||||
|
||||
variable "bucket" {
|
||||
default = "vela-website"
|
||||
}
|
||||
|
||||
variable "acl" {
|
||||
default = "private"
|
||||
}
|
||||
|
||||
variable:
|
||||
bucket: "vela-website"
|
||||
acl: "private"
|
||||
|
||||
writeConnectionSecretToRef:
|
||||
name: oss-conn
|
||||
namespace: default
|
||||
|
||||
```
|
||||
|
||||
```shell
|
||||
$ kubectl get configuration.terraform.core.oam.dev
|
||||
NAME AGE
|
||||
aliyun-oss 1h
|
||||
|
||||
$ kubectl get configuration.terraform.core.oam.dev aliyun-oss -o yaml
|
||||
apiVersion: terraform.core.oam.dev/v1beta1
|
||||
kind: Configuration
|
||||
metadata:
|
||||
annotations:
|
||||
kubectl.kubernetes.io/last-applied-configuration: |
|
||||
{"apiVersion":"terraform.core.oam.dev/v1beta1","kind":"Configuration","metadata":{"annotations":{},"name":"aliyun-oss","namespace":"default"},"spec":{"JSON":"{\n \"resource\": {\n \"alicloud_oss_bucket\": {\n \"bucket-acl\": {\n \"bucket\": \"${var.bucket}\",\n \"acl\": \"${var.acl}\"\n }\n }\n },\n \"output\": {\n \"BUCKET_NAME\": {\n \"value\": \"${alicloud_oss_bucket.bucket-acl.bucket}.${alicloud_oss_bucket.bucket-acl.extranet_endpoint}\"\n }\n },\n \"variable\": {\n \"bucket\": {\n \"default\": \"poc\"\n },\n \"acl\": {\n \"default\": \"private\"\n }\n }\n}\n","variable":{"acl":"private","bucket":"vela-website"},"writeConnectionSecretToRef":{"name":"oss-conn","namespace":"default"}}}
|
||||
creationTimestamp: "2021-04-02T08:17:08Z"
|
||||
generation: 2
|
||||
spec:
|
||||
...
|
||||
variable:
|
||||
acl: private
|
||||
bucket: vela-website
|
||||
writeConnectionSecretToRef:
|
||||
name: oss-conn
|
||||
namespace: default
|
||||
status:
|
||||
outputs:
|
||||
BUCKET_NAME:
|
||||
type: string
|
||||
value: vela-website.oss-cn-beijing.aliyuncs.com
|
||||
state: provisioned
|
||||
```
|
||||
|
||||
### Looking into Configuration (optional)
|
||||
|
||||
#### Watch the job to complete
|
||||
|
||||
```shell
|
||||
$ kubectl get job
|
||||
NAME COMPLETIONS DURATION AGE
|
||||
aliyun-oss-apply 1/1 12s 94s
|
||||
|
||||
$ kubectl get pod
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
aliyun-oss-apply-5c8b6 0/2 Completed 0 111s
|
||||
|
||||
$ kubectl logs aliyun-oss-rllx4 terraform-executor
|
||||
|
||||
Initializing the backend...
|
||||
|
||||
Initializing provider plugins...
|
||||
- Finding latest version of hashicorp/alicloud...
|
||||
- Installing hashicorp/alicloud v1.119.1...
|
||||
- Installed hashicorp/alicloud v1.119.1 (signed by HashiCorp)
|
||||
|
||||
Terraform has created a lock file .terraform.lock.hcl to record the provider
|
||||
selections it made above. Include this file in your version control repository
|
||||
so that Terraform can guarantee to make the same selections by default when
|
||||
you run "terraform init" in the future.
|
||||
|
||||
|
||||
Warning: Additional provider information from registry
|
||||
|
||||
The remote registry returned warnings for
|
||||
registry.terraform.io/hashicorp/alicloud:
|
||||
- For users on Terraform 0.13 or greater, this provider has moved to
|
||||
aliyun/alicloud. Please update your source in required_providers.
|
||||
|
||||
Terraform has been successfully initialized!
|
||||
|
||||
You may now begin working with Terraform. Try running "terraform plan" to see
|
||||
any changes that are required for your infrastructure. All Terraform commands
|
||||
should now work.
|
||||
|
||||
If you ever set or change modules or backend configuration for Terraform,
|
||||
rerun this command to reinitialize your working directory. If you forget, other
|
||||
commands will detect it and remind you to do so if necessary.
|
||||
alicloud_oss_bucket.bucket-acl: Creating...
|
||||
alicloud_oss_bucket.bucket-acl: Creation complete after 3s [id=vela-website]
|
||||
|
||||
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
|
||||
|
||||
Outputs:
|
||||
|
||||
BUCKET_NAME = "vela-website.oss-cn-beijing.aliyuncs.com"
|
||||
```
|
||||
|
||||
OSS bucket is provisioned.
|
||||
|
||||
```shell
|
||||
$ ossutil ls oss://
|
||||
CreationTime Region StorageClass BucketName
|
||||
2021-04-10 00:42:09 +0800 CST oss-cn-beijing Standard oss://vela-website
|
||||
Bucket Number is: 1
|
||||
|
||||
0.146789(s) elapsed
|
||||
```
|
||||
|
||||
#### Check whether Terraform state file is stored
|
||||
|
||||
```shell
|
||||
$ kubectl get cm | grep aliyun-oss
|
||||
aliyun-oss-tf-input 1 16m
|
||||
aliyun-oss-tf-state 1 11m
|
||||
|
||||
$ kubectl get cm aliyun-oss-tf-state -o yaml
|
||||
apiVersion: v1
|
||||
data:
|
||||
terraform.tfstate: |
|
||||
{
|
||||
"version": 4,
|
||||
"terraform_version": "0.14.9",
|
||||
"serial": 2,
|
||||
"lineage": "61cbded2-6323-0f83-823d-9c40c000b91d",
|
||||
"outputs": {
|
||||
"BUCKET_NAME": {
|
||||
"value": "vela-website.oss-cn-beijing.aliyuncs.com",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "alicloud_oss_bucket",
|
||||
"name": "bucket-acl",
|
||||
"provider": "provider[\"registry.terraform.io/hashicorp/alicloud\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"acl": "private",
|
||||
"bucket": "vela-website",
|
||||
"cors_rule": [],
|
||||
"creation_date": "2021-04-02",
|
||||
"extranet_endpoint": "oss-cn-beijing.aliyuncs.com",
|
||||
"force_destroy": false,
|
||||
"id": "vela-website",
|
||||
"intranet_endpoint": "oss-cn-beijing-internal.aliyuncs.com",
|
||||
"lifecycle_rule": [],
|
||||
"location": "oss-cn-beijing",
|
||||
"logging": [],
|
||||
"logging_isenable": null,
|
||||
"owner": "1874279259696164",
|
||||
"policy": "",
|
||||
"redundancy_type": "LRS",
|
||||
"referer_config": [],
|
||||
"server_side_encryption_rule": [],
|
||||
"storage_class": "Standard",
|
||||
"tags": null,
|
||||
"versioning": [],
|
||||
"website": []
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "bnVsbA=="
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: "2021-04-02T03:37:31Z"
|
||||
managedFields:
|
||||
- apiVersion: v1
|
||||
fieldsType: FieldsV1
|
||||
fieldsV1:
|
||||
f:data:
|
||||
.: {}
|
||||
f:terraform.tfstate: {}
|
||||
manager: terraform-tfstate-retriever
|
||||
operation: Update
|
||||
time: "2021-04-02T03:37:31Z"
|
||||
name: aliyun-oss-tf-state
|
||||
namespace: default
|
||||
resourceVersion: "33145818"
|
||||
selfLink: /api/v1/namespaces/default/configmaps/aliyun-oss-tf-state
|
||||
uid: 762b1912-1f8f-428c-a4c7-2a7297375579
|
||||
```
|
||||
|
||||
#### Check the generated connection secret
|
||||
|
||||
```shell
|
||||
$ kubectl get secret oss-conn
|
||||
NAME TYPE DATA AGE
|
||||
oss-conn Opaque 1 2m41s
|
||||
```
|
||||
|
||||
### Update Configuration
|
||||
|
||||
Change the OSS ACL to `public-read`.
|
||||
|
||||
```yaml
|
||||
apiVersion: terraform.core.oam.dev/v1beta1
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: aliyun-oss
|
||||
spec:
|
||||
JSON: |
|
||||
..
|
||||
|
||||
variable:
|
||||
...
|
||||
acl: "public-read"
|
||||
|
||||
```
|
||||
|
||||
### Delete Configuration
|
||||
|
||||
Delete the configuration will destroy the OSS cloud resource.
|
||||
|
||||
```shell
|
||||
$ kubectl delete configuration.terraform.core.oam.dev aliyun-oss
|
||||
configuration.terraform.core.oam.dev "aliyun-oss" deleted
|
||||
|
||||
$ ossutil ls oss://
|
||||
Bucket Number is: 0
|
||||
|
||||
0.030917(s) elapsed
|
||||
```
|
||||
|
||||
## AWS
|
||||
|
||||
### Apply Provider configuration
|
||||
|
||||
```shell
|
||||
$ export AWS_ACCESS_KEY_ID=xxx;export AWS_SECRET_ACCESS_KEY=yyy
|
||||
|
||||
$ sh hack/prepare-aws-credentials.sh
|
||||
|
||||
$ kubectl get secret -n vela-system
|
||||
NAME TYPE DATA AGE
|
||||
aws-account-creds Opaque 1 52s
|
||||
|
||||
$ k apply -f examples/aws/provider.yaml
|
||||
provider.terraform.core.oam.dev/default created
|
||||
|
||||
$ kubectl apply -f examples/rbac.yaml
|
||||
clusterrole.rbac.authorization.k8s.io/tf-clusterrole created
|
||||
clusterrolebinding.rbac.authorization.k8s.io/tf-binding created
|
||||
```
|
||||
|
||||
### Apply Terraform Configuration
|
||||
|
||||
Apply Terraform configuration [configuration_hcl_s3.yaml](./examples/aws/configuration_hcl_s3.yaml) to provision a s3 bucket.
|
||||
|
||||
```yaml
|
||||
apiVersion: terraform.core.oam.dev/v1beta1
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: aws-s3
|
||||
spec:
|
||||
hcl: |
|
||||
resource "aws_s3_bucket" "bucket-acl" {
|
||||
bucket = var.bucket
|
||||
acl = var.acl
|
||||
}
|
||||
|
||||
output "BUCKET_NAME" {
|
||||
value = aws_s3_bucket.bucket-acl.bucket_domain_name
|
||||
}
|
||||
|
||||
variable "bucket" {
|
||||
default = "vela-website"
|
||||
}
|
||||
|
||||
variable "acl" {
|
||||
default = "private"
|
||||
}
|
||||
|
||||
variable:
|
||||
bucket: "vela-website"
|
||||
acl: "private"
|
||||
|
||||
writeConnectionSecretToRef:
|
||||
name: s3-conn
|
||||
namespace: default
|
||||
|
||||
```
|
||||
|
||||
```shell
|
||||
$ kubectl get configuration.terraform.core.oam.dev
|
||||
NAME AGE
|
||||
aws-s3 6m48s
|
||||
|
||||
$ kubectl describe configuration.terraform.core.oam.dev aws-s3
|
||||
apiVersion: terraform.core.oam.dev/v1beta1
|
||||
kind: Configuration
|
||||
...
|
||||
Write Connection Secret To Ref:
|
||||
Name: s3-conn
|
||||
Namespace: default
|
||||
Status:
|
||||
Outputs:
|
||||
BUCKET_NAME:
|
||||
Type: string
|
||||
Value: vela-website.s3.amazonaws.com
|
||||
State: provisioned
|
||||
|
||||
$ kubectl get secret s3-conn
|
||||
NAME TYPE DATA AGE
|
||||
s3-conn Opaque 1 7m37s
|
||||
|
||||
$ aws s3 ls
|
||||
2021-04-12 19:03:32 vela-website
|
||||
```
|
||||
This is the [contributing guide](./CONTRIBUTING.md). Looking forward to your contribution.
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
package types
|
||||
|
||||
const (
|
||||
DefaultNamespace = "default"
|
||||
|
||||
GitCredsKnownHosts = "known_hosts"
|
||||
// TerraformCredentials -
|
||||
TerraformCredentials = "credentials.tfrc.json"
|
||||
// TerraformRegistryConfig -
|
||||
TerraformRegistryConfig = ".terraformrc"
|
||||
)
|
||||
|
||||
const (
|
||||
// TerraformContainerName is the name of the container that executes terraform in the pod
|
||||
TerraformContainerName = "terraform-executor"
|
||||
TerraformInitContainerName = "terraform-init"
|
||||
)
|
||||
|
||||
const (
|
||||
// TFInputConfigMapName is the CM name for Terraform Input Configuration
|
||||
TFInputConfigMapName = "tf-%s"
|
||||
// TFVariableSecret is the Secret name for variables, including credentials from Provider
|
||||
TFVariableSecret = "variable-%s"
|
||||
)
|
||||
|
||||
// TerraformExecutionType is the type for Terraform execution
|
||||
type TerraformExecutionType string
|
||||
|
||||
const (
|
||||
// TerraformApply is the name to mark `terraform apply`
|
||||
TerraformApply TerraformExecutionType = "apply"
|
||||
// TerraformDestroy is the name to mark `terraform destroy`
|
||||
TerraformDestroy TerraformExecutionType = "destroy"
|
||||
)
|
||||
|
||||
const (
|
||||
// ClusterRoleName is the name of the ClusterRole for Terraform Job
|
||||
ClusterRoleName = "tf-executor-clusterrole"
|
||||
// ServiceAccountName is the name of the ServiceAccount for Terraform Job
|
||||
ServiceAccountName = "tf-executor-service-account"
|
||||
)
|
||||
|
||||
// Volume names and mount paths
|
||||
const (
|
||||
// WorkingVolumeMountPath is the mount path for working volume
|
||||
WorkingVolumeMountPath = "/data"
|
||||
|
||||
// InputTFConfigurationVolumeName is the volume name for input Terraform Configuration
|
||||
InputTFConfigurationVolumeName = "tf-input-configuration"
|
||||
// InputTFConfigurationVolumeMountPath is the volume mount path for input Terraform Configuration
|
||||
InputTFConfigurationVolumeMountPath = "/opt/tf-configuration"
|
||||
|
||||
// BackendVolumeName is the volume name for Terraform backend
|
||||
BackendVolumeName = "tf-backend"
|
||||
// BackendVolumeMountPath is the volume mount path for Terraform backend
|
||||
BackendVolumeMountPath = "/opt/tf-backend"
|
||||
|
||||
// GitAuthConfigVolumeName is the volume name for git auth configurtaion
|
||||
GitAuthConfigVolumeName = "git-auth-configuration"
|
||||
// GitAuthConfigVolumeMountPath is the volume mount path for git auth configurtaion
|
||||
GitAuthConfigVolumeMountPath = "/root/.ssh"
|
||||
|
||||
// TerraformCredentialsConfigVolumeName is the volume name for terraform auth configurtaion
|
||||
TerraformCredentialsConfigVolumeName = "terraform-credentials-configuration"
|
||||
// TerraformCredentialsConfigVolumeMountPath is the volume mount path for terraform auth configurtaion
|
||||
TerraformCredentialsConfigVolumeMountPath = "/root/.terraform.d"
|
||||
|
||||
// TerraformRCConfigVolumeName is the volume name of the terraform registry configuration
|
||||
TerraformRCConfigVolumeName = "terraform-rc-configuration"
|
||||
// TerraformRCConfigVolumeMountPath is the volume mount path for registry configuration
|
||||
TerraformRCConfigVolumeMountPath = "/root"
|
||||
|
||||
// TerraformCredentialsHelperConfigVolumeName is the volume name for terraform auth configurtaion
|
||||
TerraformCredentialsHelperConfigVolumeName = "terraform-credentials-helper-configuration"
|
||||
// TerraformCredentialsHelperConfigVolumeMountPath is the volume mount path for terraform auth configurtaion
|
||||
TerraformCredentialsHelperConfigVolumeMountPath = "/root/.terraform.d/plugins"
|
||||
)
|
|
@ -50,5 +50,15 @@ type SecretReference struct {
|
|||
Name string `json:"name"`
|
||||
|
||||
// Namespace of the secret.
|
||||
Namespace string `json:"namespace"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
}
|
||||
|
||||
// A Reference to a named object.
|
||||
type Reference struct {
|
||||
// Name of the referenced object.
|
||||
Name string `json:"name"`
|
||||
|
||||
// Namespace of the referenced object.
|
||||
// +kubebuilder:default:=default
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
}
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
Copyright 2019 The Crossplane Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package types
|
||||
|
||||
// A ConfigurationState represents the status of a resource
|
||||
type ConfigurationState string
|
||||
|
||||
// Reasons a resource is or is not ready.
|
||||
const (
|
||||
Authorizing ConfigurationState = "Authorizing"
|
||||
ProviderNotFound ConfigurationState = "ProviderNotFound"
|
||||
ProviderNotReady ConfigurationState = "ProviderNotReady"
|
||||
ConfigurationStaticCheckFailed ConfigurationState = "ConfigurationSpecNotValid"
|
||||
Available ConfigurationState = "Available"
|
||||
ConfigurationProvisioningAndChecking ConfigurationState = "ProvisioningAndChecking"
|
||||
ConfigurationDestroying ConfigurationState = "Destroying"
|
||||
ConfigurationApplyFailed ConfigurationState = "ApplyFailed"
|
||||
ConfigurationDestroyFailed ConfigurationState = "DestroyFailed"
|
||||
ConfigurationReloading ConfigurationState = "ConfigurationReloading"
|
||||
GeneratingOutputs ConfigurationState = "GeneratingTerraformOutputs"
|
||||
InvalidRegion ConfigurationState = "InvalidRegion"
|
||||
TerraformInitError ConfigurationState = "TerraformInitError"
|
||||
InvalidGitCredentialsSecretReference ConfigurationState = "InvalidGitCredentialsSecretReference"
|
||||
InvalidTerraformCredentialsSecretReference ConfigurationState = "InvalidTerraformCredentialsSecretReference"
|
||||
InvalidTerraformRCConfigMapReference ConfigurationState = "InvalidTerraformRCConfigMapReference"
|
||||
InvalidTerraformCredentialsHelperConfigMapReference ConfigurationState = "InvalidTerraformCredentialsHelperConfigMapReference"
|
||||
)
|
||||
|
||||
// Stage is the Terraform stage
|
||||
type Stage string
|
||||
|
||||
const (
|
||||
InitStage Stage = "InitStage"
|
||||
ApplyStage Stage = "Apply"
|
||||
)
|
||||
|
||||
const (
|
||||
// MessageDestroyJobNotCompleted is the message when Configuration deletion isn't completed
|
||||
MessageDestroyJobNotCompleted = "Configuration deletion isn't completed"
|
||||
// MessageApplyJobNotCompleted is the message when cloud resources are not created completed
|
||||
MessageApplyJobNotCompleted = "cloud resources are not created completed"
|
||||
// MessageCloudResourceProvisioningAndChecking is the message when cloud resource is being provisioned
|
||||
MessageCloudResourceProvisioningAndChecking = "Cloud resources are being provisioned and provisioning status is checking..."
|
||||
// ErrUpdateTerraformApplyJob means hitting an issue to update Terraform apply job
|
||||
ErrUpdateTerraformApplyJob = "Hit an issue to update Terraform apply job"
|
||||
// MessageCloudResourceDeployed means Cloud resources are deployed and ready to use
|
||||
MessageCloudResourceDeployed = "Cloud resources are deployed and ready to use"
|
||||
// MessageCloudResourceDestroying is the message when cloud resource is being destroyed
|
||||
MessageCloudResourceDestroying = "Cloud resources is being destroyed..."
|
||||
// ErrProviderNotFound means provider not found
|
||||
ErrProviderNotFound = "provider not found"
|
||||
// ErrProviderNotReady means provider object is not ready
|
||||
ErrProviderNotReady = "Provider is not ready"
|
||||
// ConfigurationReloadingAsHCLChanged means Configuration changed and needs reloading
|
||||
ConfigurationReloadingAsHCLChanged = "Configuration's HCL has changed, and starts reloading"
|
||||
// ConfigurationReloadingAsVariableChanged means Configuration changed and needs reloading
|
||||
ConfigurationReloadingAsVariableChanged = "Configuration's variable has changed, and starts reloading"
|
||||
// ErrGenerateOutputs means error to generate outputs
|
||||
ErrGenerateOutputs = "Hit an issue to generate outputs"
|
||||
)
|
||||
|
||||
// ProviderState is the type for Provider state
|
||||
type ProviderState string
|
||||
|
||||
const (
|
||||
// ProviderIsReady is the `ready` state
|
||||
ProviderIsReady ProviderState = "ready"
|
||||
// ProviderIsNotReady marks the state of a Provider is not ready
|
||||
ProviderIsNotReady ProviderState = "ProviderNotReady"
|
||||
)
|
|
@ -0,0 +1,42 @@
|
|||
package types
|
||||
|
||||
import "k8s.io/apimachinery/pkg/api/resource"
|
||||
|
||||
const (
|
||||
// TerraformHCLConfigurationName is the file name for Terraform hcl Configuration
|
||||
TerraformHCLConfigurationName = "main.tf"
|
||||
)
|
||||
|
||||
// ConfigurationType is the type for Terraform Configuration
|
||||
type ConfigurationType string
|
||||
|
||||
const (
|
||||
// ConfigurationHCL is the HCL type Configuration
|
||||
ConfigurationHCL ConfigurationType = "HCL"
|
||||
// ConfigurationRemote means HCL stores in a remote git repository
|
||||
ConfigurationRemote ConfigurationType = "Remote"
|
||||
)
|
||||
|
||||
type Git struct {
|
||||
URL string
|
||||
Path string
|
||||
Ref GitRef
|
||||
}
|
||||
|
||||
// GitRef specifies the git reference
|
||||
type GitRef struct {
|
||||
Branch string `json:"branch,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Commit string `json:"commit,omitempty"`
|
||||
}
|
||||
|
||||
type ResourceQuota struct {
|
||||
ResourcesLimitsCPU string
|
||||
ResourcesLimitsCPUQuantity resource.Quantity
|
||||
ResourcesLimitsMemory string
|
||||
ResourcesLimitsMemoryQuantity resource.Quantity
|
||||
ResourcesRequestsCPU string
|
||||
ResourcesRequestsCPUQuantity resource.Quantity
|
||||
ResourcesRequestsMemory string
|
||||
ResourcesRequestsMemoryQuantity resource.Quantity
|
||||
}
|
|
@ -17,45 +17,116 @@ limitations under the License.
|
|||
package v1beta1
|
||||
|
||||
import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
state "github.com/oam-dev/terraform-controller/api/types"
|
||||
types "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime"
|
||||
)
|
||||
|
||||
// ConfigurationSpec defines the desired state of Configuration
|
||||
type ConfigurationSpec struct {
|
||||
// JSON is the Terraform JSON syntax configuration
|
||||
// JSON is the Terraform JSON syntax configuration.
|
||||
// Deprecated: after v0.3.1, use HCL instead.
|
||||
JSON string `json:"JSON,omitempty"`
|
||||
// HCL is the Terraform HCL type configuration
|
||||
HCL string `json:"hcl,omitempty"`
|
||||
// +kubebuilder:pruning:PreserveUnknownFields
|
||||
Variable runtime.RawExtension `json:"variable"`
|
||||
|
||||
// Remote is a git repo which contains hcl files. Currently, only public git repos are supported.
|
||||
Remote string `json:"remote,omitempty"`
|
||||
|
||||
// +kubebuilder:pruning:PreserveUnknownFields
|
||||
Variable *runtime.RawExtension `json:"variable,omitempty"`
|
||||
|
||||
// Backend stores the state in a Kubernetes secret with locking done using a Lease resource.
|
||||
// TODO(zzxwill) If a backend exists in HCL/JSON, this can be optional. Currently, if Backend is not set by users, it
|
||||
// still will set by the controller, ignoring the settings in HCL/JSON backend
|
||||
Backend *Backend `json:"backend,omitempty"`
|
||||
|
||||
// Path is the sub-directory of remote git repository.
|
||||
Path string `json:"path,omitempty"`
|
||||
|
||||
BaseConfigurationSpec `json:",inline"`
|
||||
|
||||
// GitCredentialsSecretReference specifies the reference to the secret containing the git credentials
|
||||
GitCredentialsSecretReference *v1.SecretReference `json:"gitCredentialsSecretReference,omitempty"`
|
||||
|
||||
// TerraformCredentialsSecretReference specifies the reference to the secret containing the terraform credentials
|
||||
TerraformCredentialsSecretReference *v1.SecretReference `json:"terraformCredentialsSecretReference,omitempty"`
|
||||
|
||||
// TerraformRCConfigMapReference specifies the reference to a config map containing the terraform registry configuration
|
||||
TerraformRCConfigMapReference *v1.SecretReference `json:"terraformRCConfigMapReference,omitempty"`
|
||||
|
||||
// TerraformCredentialsHelperConfigMapReference specifies the reference to a configmap containing the terraform registry credentials helper
|
||||
TerraformCredentialsHelperConfigMapReference *v1.SecretReference `json:"terraformCredentialsHelperConfigMapReference,omitempty"`
|
||||
}
|
||||
|
||||
// BaseConfigurationSpec defines the common fields of a ConfigurationSpec
|
||||
type BaseConfigurationSpec struct {
|
||||
// WriteConnectionSecretToReference specifies the namespace and name of a
|
||||
// Secret to which any connection details for this managed resource should
|
||||
// be written. Connection details frequently include the endpoint, username,
|
||||
// and password required to connect to the managed resource.
|
||||
// +optional
|
||||
WriteConnectionSecretToReference *types.SecretReference `json:"writeConnectionSecretToRef,omitempty"`
|
||||
|
||||
// ProviderReference specifies the reference to Provider
|
||||
ProviderReference *types.Reference `json:"providerRef,omitempty"`
|
||||
|
||||
// DeleteResource will determine whether provisioned cloud resources will be deleted when CR is deleted
|
||||
// +kubebuilder:default:=true
|
||||
DeleteResource bool `json:"deleteResource,omitempty"`
|
||||
|
||||
// Region is cloud provider's region. It will override the region in the region field of ProviderReference
|
||||
Region string `json:"region,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigurationStatus defines the observed state of Configuration
|
||||
type ConfigurationStatus struct {
|
||||
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
|
||||
// Important: Run "make" to regenerate code after modifying this file
|
||||
State string `json:"state,omitempty"`
|
||||
Outputs map[string]Property `json:"outputs,omitempty"`
|
||||
// observedGeneration is the most recent generation observed for this Configuration. It corresponds to the
|
||||
// Configuration's generation, which is updated on mutation by the API Server.
|
||||
// +optional
|
||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||
|
||||
Apply ConfigurationApplyStatus `json:"apply,omitempty"`
|
||||
Destroy ConfigurationDestroyStatus `json:"destroy,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigurationApplyStatus is the status for Configuration apply
|
||||
type ConfigurationApplyStatus struct {
|
||||
State state.ConfigurationState `json:"state,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Outputs map[string]Property `json:"outputs,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigurationDestroyStatus is the status for Configuration destroy
|
||||
type ConfigurationDestroyStatus struct {
|
||||
State state.ConfigurationState `json:"state,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// Property is the property for an output
|
||||
type Property struct {
|
||||
Value string `json:"value,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Backend stores the state in a Kubernetes secret with locking done using a Lease resource.
|
||||
type Backend struct {
|
||||
// SecretSuffix used when creating secrets. Secrets will be named in the format: tfstate-{workspace}-{secretSuffix}
|
||||
SecretSuffix string `json:"secretSuffix,omitempty"`
|
||||
// InClusterConfig Used to authenticate to the cluster from inside a pod. Only `true` is allowed
|
||||
InClusterConfig bool `json:"inClusterConfig,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// Configuration is the Schema for the configurations API
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="STATE",type="string",JSONPath=".status.apply.state"
|
||||
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
|
||||
// +kubebuilder:resource:shortName={conf,terraform-conf}
|
||||
type Configuration struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
|
|
@ -17,9 +17,10 @@ limitations under the License.
|
|||
package v1beta1
|
||||
|
||||
import (
|
||||
"github.com/oam-dev/terraform-controller/api/types"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
types "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime"
|
||||
crossplanetypes "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime"
|
||||
)
|
||||
|
||||
// ProviderSpec defines the desired state of Provider.
|
||||
|
@ -28,7 +29,7 @@ type ProviderSpec struct {
|
|||
Provider string `json:"provider"`
|
||||
|
||||
// Region is cloud provider's region
|
||||
Region string `json:"region"`
|
||||
Region string `json:"region,omitempty"`
|
||||
|
||||
// Credentials required to authenticate to this provider.
|
||||
Credentials ProviderCredentials `json:"credentials"`
|
||||
|
@ -38,23 +39,26 @@ type ProviderSpec struct {
|
|||
type ProviderCredentials struct {
|
||||
// Source of the provider credentials.
|
||||
// +kubebuilder:validation:Enum=None;Secret;InjectedIdentity;Environment;Filesystem
|
||||
Source types.CredentialsSource `json:"source"`
|
||||
Source crossplanetypes.CredentialsSource `json:"source"`
|
||||
|
||||
// A SecretRef is a reference to a secret key that contains the credentials
|
||||
// that must be used to connect to the provider.
|
||||
// +optional
|
||||
SecretRef *types.SecretKeySelector `json:"secretRef,omitempty"`
|
||||
SecretRef *crossplanetypes.SecretKeySelector `json:"secretRef,omitempty"`
|
||||
}
|
||||
|
||||
// ProviderStatus defines the observed state of Provider.
|
||||
type ProviderStatus struct {
|
||||
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
|
||||
// Important: Run "make" to regenerate code after modifying this file
|
||||
State types.ProviderState `json:"state,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="STATE",type="string",JSONPath=".status.state"
|
||||
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
|
||||
|
||||
// Provider is the Schema for the providerconfigs API.
|
||||
// Provider is the Schema for the providers API.
|
||||
type Provider struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// +build !ignore_autogenerated
|
||||
//go:build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
|
@ -22,9 +22,50 @@ package v1beta1
|
|||
|
||||
import (
|
||||
crossplane_runtime "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime"
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Backend) DeepCopyInto(out *Backend) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Backend.
|
||||
func (in *Backend) DeepCopy() *Backend {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Backend)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BaseConfigurationSpec) DeepCopyInto(out *BaseConfigurationSpec) {
|
||||
*out = *in
|
||||
if in.WriteConnectionSecretToReference != nil {
|
||||
in, out := &in.WriteConnectionSecretToReference, &out.WriteConnectionSecretToReference
|
||||
*out = new(crossplane_runtime.SecretReference)
|
||||
**out = **in
|
||||
}
|
||||
if in.ProviderReference != nil {
|
||||
in, out := &in.ProviderReference, &out.ProviderReference
|
||||
*out = new(crossplane_runtime.Reference)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BaseConfigurationSpec.
|
||||
func (in *BaseConfigurationSpec) DeepCopy() *BaseConfigurationSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BaseConfigurationSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Configuration) DeepCopyInto(out *Configuration) {
|
||||
*out = *in
|
||||
|
@ -52,6 +93,43 @@ func (in *Configuration) DeepCopyObject() runtime.Object {
|
|||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ConfigurationApplyStatus) DeepCopyInto(out *ConfigurationApplyStatus) {
|
||||
*out = *in
|
||||
if in.Outputs != nil {
|
||||
in, out := &in.Outputs, &out.Outputs
|
||||
*out = make(map[string]Property, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationApplyStatus.
|
||||
func (in *ConfigurationApplyStatus) DeepCopy() *ConfigurationApplyStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ConfigurationApplyStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ConfigurationDestroyStatus) DeepCopyInto(out *ConfigurationDestroyStatus) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationDestroyStatus.
|
||||
func (in *ConfigurationDestroyStatus) DeepCopy() *ConfigurationDestroyStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ConfigurationDestroyStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ConfigurationList) DeepCopyInto(out *ConfigurationList) {
|
||||
*out = *in
|
||||
|
@ -87,10 +165,35 @@ func (in *ConfigurationList) DeepCopyObject() runtime.Object {
|
|||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ConfigurationSpec) DeepCopyInto(out *ConfigurationSpec) {
|
||||
*out = *in
|
||||
in.Variable.DeepCopyInto(&out.Variable)
|
||||
if in.WriteConnectionSecretToReference != nil {
|
||||
in, out := &in.WriteConnectionSecretToReference, &out.WriteConnectionSecretToReference
|
||||
*out = new(crossplane_runtime.SecretReference)
|
||||
if in.Variable != nil {
|
||||
in, out := &in.Variable, &out.Variable
|
||||
*out = new(runtime.RawExtension)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Backend != nil {
|
||||
in, out := &in.Backend, &out.Backend
|
||||
*out = new(Backend)
|
||||
**out = **in
|
||||
}
|
||||
in.BaseConfigurationSpec.DeepCopyInto(&out.BaseConfigurationSpec)
|
||||
if in.GitCredentialsSecretReference != nil {
|
||||
in, out := &in.GitCredentialsSecretReference, &out.GitCredentialsSecretReference
|
||||
*out = new(v1.SecretReference)
|
||||
**out = **in
|
||||
}
|
||||
if in.TerraformCredentialsSecretReference != nil {
|
||||
in, out := &in.TerraformCredentialsSecretReference, &out.TerraformCredentialsSecretReference
|
||||
*out = new(v1.SecretReference)
|
||||
**out = **in
|
||||
}
|
||||
if in.TerraformRCConfigMapReference != nil {
|
||||
in, out := &in.TerraformRCConfigMapReference, &out.TerraformRCConfigMapReference
|
||||
*out = new(v1.SecretReference)
|
||||
**out = **in
|
||||
}
|
||||
if in.TerraformCredentialsHelperConfigMapReference != nil {
|
||||
in, out := &in.TerraformCredentialsHelperConfigMapReference, &out.TerraformCredentialsHelperConfigMapReference
|
||||
*out = new(v1.SecretReference)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
@ -108,13 +211,8 @@ func (in *ConfigurationSpec) DeepCopy() *ConfigurationSpec {
|
|||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ConfigurationStatus) DeepCopyInto(out *ConfigurationStatus) {
|
||||
*out = *in
|
||||
if in.Outputs != nil {
|
||||
in, out := &in.Outputs, &out.Outputs
|
||||
*out = make(map[string]Property, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
in.Apply.DeepCopyInto(&out.Apply)
|
||||
out.Destroy = in.Destroy
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationStatus.
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1beta2
|
||||
|
||||
import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
apitypes "github.com/oam-dev/terraform-controller/api/types"
|
||||
types "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime"
|
||||
)
|
||||
|
||||
// ConfigurationSpec defines the desired state of Configuration
|
||||
type ConfigurationSpec struct {
|
||||
// HCL is the Terraform HCL type configuration
|
||||
HCL string `json:"hcl,omitempty"`
|
||||
|
||||
// Remote is a git repo which contains hcl files. Currently, only public git repos are supported.
|
||||
Remote string `json:"remote,omitempty"`
|
||||
|
||||
// GitRef is the git branch or tag or commit hash to checkout. Only used when Remote is specified.
|
||||
GitRef apitypes.GitRef `json:"gitRef,omitempty"`
|
||||
|
||||
// +kubebuilder:pruning:PreserveUnknownFields
|
||||
Variable *runtime.RawExtension `json:"variable,omitempty"`
|
||||
|
||||
// Backend describes the Terraform backend configuration.
|
||||
// This field is needed if the users use a git repo to provide the hcl files or
|
||||
// want to use their custom Terraform backend (instead of the default kubernetes backend type).
|
||||
// Notice: This field may cause two backend blocks in the final Terraform module and make the executor job failed.
|
||||
// So, please make sure that there are no backend configurations in your inline hcl code or the git repo.
|
||||
Backend *Backend `json:"backend,omitempty"`
|
||||
|
||||
// Path is the sub-directory of remote git repository.
|
||||
Path string `json:"path,omitempty"`
|
||||
|
||||
// WriteConnectionSecretToReference specifies the namespace and name of a
|
||||
// Secret to which any connection details for this managed resource should
|
||||
// be written. Connection details frequently include the endpoint, username,
|
||||
// and password required to connect to the managed resource.
|
||||
// +optional
|
||||
WriteConnectionSecretToReference *types.SecretReference `json:"writeConnectionSecretToRef,omitempty"`
|
||||
|
||||
// ProviderReference specifies the reference to Provider
|
||||
ProviderReference *types.Reference `json:"providerRef,omitempty"`
|
||||
// +kubebuilder:pruning:PreserveUnknownFields
|
||||
JobEnv *runtime.RawExtension `json:"JobEnv,omitempty"`
|
||||
// InlineCredentials specifies the credentials in spec.HCl field as below.
|
||||
// provider "aws" {
|
||||
// region = "us-west-2"
|
||||
// access_key = "my-access-key"
|
||||
// secret_key = "my-secret-key"
|
||||
// }
|
||||
// Or indicates a Terraform module or configuration don't need credentials at all, like provider `random`
|
||||
InlineCredentials bool `json:"inlineCredentials,omitempty"`
|
||||
|
||||
// DeleteResource will determine whether provisioned cloud resources will be deleted when CR is deleted
|
||||
// +kubebuilder:default:=true
|
||||
DeleteResource *bool `json:"deleteResource,omitempty"`
|
||||
|
||||
// Region is cloud provider's region. It will override the region in the region field of ProviderReference
|
||||
Region string `json:"customRegion,omitempty"`
|
||||
|
||||
// ForceDelete will force delete Configuration no matter which state it is or whether it has provisioned some resources
|
||||
// It will help delete Configuration in unexpected cases.
|
||||
ForceDelete *bool `json:"forceDelete,omitempty"`
|
||||
|
||||
// GitCredentialsSecretReference specifies the reference to the secret containing the git credentials
|
||||
GitCredentialsSecretReference *v1.SecretReference `json:"gitCredentialsSecretReference,omitempty"`
|
||||
|
||||
// TerraformCredentialsSecretReference specifies the reference to the secret containing the terraform credentials and terraform registry details
|
||||
TerraformCredentialsSecretReference *v1.SecretReference `json:"terraformCredentialsSecretReference,omitempty"`
|
||||
|
||||
// TerraformRCConfigMapReference specifies the reference to a config map containing the terraform registry configuration
|
||||
TerraformRCConfigMapReference *v1.SecretReference `json:"terraformRCConfigMapReference,omitempty"`
|
||||
|
||||
// TerraformCredentialsHelperConfigMapReference specifies the reference to a configmap containing the terraform registry credentials helper
|
||||
TerraformCredentialsHelperConfigMapReference *v1.SecretReference `json:"terraformCredentialsHelperConfigMapReference,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigurationStatus defines the observed state of Configuration
|
||||
type ConfigurationStatus struct {
|
||||
// observedGeneration is the most recent generation observed for this Configuration. It corresponds to the
|
||||
// Configuration's generation, which is updated on mutation by the API Server.
|
||||
// If ObservedGeneration equals Generation, and State is Available, the value of Outputs is latest
|
||||
// +optional
|
||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||
|
||||
Apply ConfigurationApplyStatus `json:"apply,omitempty"`
|
||||
Destroy ConfigurationDestroyStatus `json:"destroy,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigurationApplyStatus is the status for Configuration apply
|
||||
type ConfigurationApplyStatus struct {
|
||||
State apitypes.ConfigurationState `json:"state,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Outputs map[string]Property `json:"outputs,omitempty"`
|
||||
// Region is the region for the cloud resources created by this Configuration. If spec.region is not empty, it's the
|
||||
// value of it. Otherwise, it's the value of spec.providerReference.region.
|
||||
Region string `json:"region,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigurationDestroyStatus is the status for Configuration destroy
|
||||
type ConfigurationDestroyStatus struct {
|
||||
State apitypes.ConfigurationState `json:"state,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// Property is the property for an output
|
||||
type Property struct {
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// Backend describes the Terraform backend configuration
|
||||
type Backend struct {
|
||||
// SecretSuffix used when creating secrets. Secrets will be named in the format: tfstate-{workspace}-{secretSuffix}
|
||||
SecretSuffix string `json:"secretSuffix,omitempty"`
|
||||
// InClusterConfig Used to authenticate to the cluster from inside a pod. Only `true` is allowed
|
||||
InClusterConfig bool `json:"inClusterConfig,omitempty"`
|
||||
|
||||
// Inline allows users to use raw hcl code to specify their Terraform backend
|
||||
Inline string `json:"inline,omitempty"`
|
||||
|
||||
// BackendType indicates which backend type to use. This field is needed for custom backend configuration.
|
||||
// +kubebuilder:validation:Enum=kubernetes;s3
|
||||
BackendType string `json:"backendType,omitempty"`
|
||||
|
||||
// Kubernetes is needed for the Terraform `kubernetes` backend type.
|
||||
Kubernetes *KubernetesBackendConf `json:"kubernetes,omitempty"`
|
||||
|
||||
// S3 is needed for the Terraform `s3` backend type.
|
||||
S3 *S3BackendConf `json:"s3,omitempty"`
|
||||
}
|
||||
|
||||
// KubernetesBackendConf defines all options supported by the Terraform `kubernetes` backend type.
|
||||
// You can refer to https://www.terraform.io/language/settings/backends/kubernetes for the usage of each option.
|
||||
type KubernetesBackendConf struct {
|
||||
SecretSuffix string `json:"secret_suffix" hcl:"secret_suffix"`
|
||||
Namespace *string `json:"namespace,omitempty" hcl:"namespace"`
|
||||
}
|
||||
|
||||
// S3BackendConf defines all options supported by the Terraform `s3` backend type.
|
||||
// You can refer to https://www.terraform.io/language/settings/backends/s3 for the usage of each option.
|
||||
type S3BackendConf struct {
|
||||
// Region is optional, default to the AWS_DEFAULT_REGION in the credentials of the provider
|
||||
Region *string `json:"region,omitempty" hcl:"region"`
|
||||
Bucket string `json:"bucket" hcl:"bucket"`
|
||||
Key string `json:"key" hcl:"key"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// Configuration is the Schema for the configurations API
|
||||
// +kubebuilder:storageversion
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="APPLY",type="string",JSONPath=".status.apply.state"
|
||||
// +kubebuilder:printcolumn:name="DESTROY",type="string",JSONPath=".status.destroy.state"
|
||||
// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp"
|
||||
type Configuration struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ConfigurationSpec `json:"spec,omitempty"`
|
||||
Status ConfigurationStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// ConfigurationList contains a list of Configuration
|
||||
type ConfigurationList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Configuration `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&Configuration{}, &ConfigurationList{})
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package v1beta2 contains API Schema definitions for the terraform v1beta2 API group
|
||||
// +kubebuilder:object:generate=true
|
||||
// +groupName=terraform.core.oam.dev
|
||||
package v1beta2
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/scheme"
|
||||
)
|
||||
|
||||
var (
|
||||
// GroupVersion is group version used to register these objects
|
||||
GroupVersion = schema.GroupVersion{Group: "terraform.core.oam.dev", Version: "v1beta2"}
|
||||
|
||||
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
|
||||
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
|
||||
|
||||
// AddToScheme adds the types in this group-version to the given scheme.
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
|
@ -0,0 +1,291 @@
|
|||
//go:build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
package v1beta2
|
||||
|
||||
import (
|
||||
crossplane_runtime "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime"
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Backend) DeepCopyInto(out *Backend) {
|
||||
*out = *in
|
||||
if in.Kubernetes != nil {
|
||||
in, out := &in.Kubernetes, &out.Kubernetes
|
||||
*out = new(KubernetesBackendConf)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.S3 != nil {
|
||||
in, out := &in.S3, &out.S3
|
||||
*out = new(S3BackendConf)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Backend.
|
||||
func (in *Backend) DeepCopy() *Backend {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Backend)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Configuration) DeepCopyInto(out *Configuration) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Configuration.
|
||||
func (in *Configuration) DeepCopy() *Configuration {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Configuration)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Configuration) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ConfigurationApplyStatus) DeepCopyInto(out *ConfigurationApplyStatus) {
|
||||
*out = *in
|
||||
if in.Outputs != nil {
|
||||
in, out := &in.Outputs, &out.Outputs
|
||||
*out = make(map[string]Property, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationApplyStatus.
|
||||
func (in *ConfigurationApplyStatus) DeepCopy() *ConfigurationApplyStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ConfigurationApplyStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ConfigurationDestroyStatus) DeepCopyInto(out *ConfigurationDestroyStatus) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationDestroyStatus.
|
||||
func (in *ConfigurationDestroyStatus) DeepCopy() *ConfigurationDestroyStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ConfigurationDestroyStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ConfigurationList) DeepCopyInto(out *ConfigurationList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Configuration, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationList.
|
||||
func (in *ConfigurationList) DeepCopy() *ConfigurationList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ConfigurationList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ConfigurationList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ConfigurationSpec) DeepCopyInto(out *ConfigurationSpec) {
|
||||
*out = *in
|
||||
out.GitRef = in.GitRef
|
||||
if in.Variable != nil {
|
||||
in, out := &in.Variable, &out.Variable
|
||||
*out = new(runtime.RawExtension)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Backend != nil {
|
||||
in, out := &in.Backend, &out.Backend
|
||||
*out = new(Backend)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.WriteConnectionSecretToReference != nil {
|
||||
in, out := &in.WriteConnectionSecretToReference, &out.WriteConnectionSecretToReference
|
||||
*out = new(crossplane_runtime.SecretReference)
|
||||
**out = **in
|
||||
}
|
||||
if in.ProviderReference != nil {
|
||||
in, out := &in.ProviderReference, &out.ProviderReference
|
||||
*out = new(crossplane_runtime.Reference)
|
||||
**out = **in
|
||||
}
|
||||
if in.JobEnv != nil {
|
||||
in, out := &in.JobEnv, &out.JobEnv
|
||||
*out = new(runtime.RawExtension)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.DeleteResource != nil {
|
||||
in, out := &in.DeleteResource, &out.DeleteResource
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.ForceDelete != nil {
|
||||
in, out := &in.ForceDelete, &out.ForceDelete
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.GitCredentialsSecretReference != nil {
|
||||
in, out := &in.GitCredentialsSecretReference, &out.GitCredentialsSecretReference
|
||||
*out = new(v1.SecretReference)
|
||||
**out = **in
|
||||
}
|
||||
if in.TerraformCredentialsSecretReference != nil {
|
||||
in, out := &in.TerraformCredentialsSecretReference, &out.TerraformCredentialsSecretReference
|
||||
*out = new(v1.SecretReference)
|
||||
**out = **in
|
||||
}
|
||||
if in.TerraformRCConfigMapReference != nil {
|
||||
in, out := &in.TerraformRCConfigMapReference, &out.TerraformRCConfigMapReference
|
||||
*out = new(v1.SecretReference)
|
||||
**out = **in
|
||||
}
|
||||
if in.TerraformCredentialsHelperConfigMapReference != nil {
|
||||
in, out := &in.TerraformCredentialsHelperConfigMapReference, &out.TerraformCredentialsHelperConfigMapReference
|
||||
*out = new(v1.SecretReference)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationSpec.
|
||||
func (in *ConfigurationSpec) DeepCopy() *ConfigurationSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ConfigurationSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ConfigurationStatus) DeepCopyInto(out *ConfigurationStatus) {
|
||||
*out = *in
|
||||
in.Apply.DeepCopyInto(&out.Apply)
|
||||
out.Destroy = in.Destroy
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationStatus.
|
||||
func (in *ConfigurationStatus) DeepCopy() *ConfigurationStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ConfigurationStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *KubernetesBackendConf) DeepCopyInto(out *KubernetesBackendConf) {
|
||||
*out = *in
|
||||
if in.Namespace != nil {
|
||||
in, out := &in.Namespace, &out.Namespace
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesBackendConf.
|
||||
func (in *KubernetesBackendConf) DeepCopy() *KubernetesBackendConf {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(KubernetesBackendConf)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Property) DeepCopyInto(out *Property) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Property.
|
||||
func (in *Property) DeepCopy() *Property {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Property)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *S3BackendConf) DeepCopyInto(out *S3BackendConf) {
|
||||
*out = *in
|
||||
if in.Region != nil {
|
||||
in, out := &in.Region, &out.Region
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new S3BackendConf.
|
||||
func (in *S3BackendConf) DeepCopy() *S3BackendConf {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(S3BackendConf)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
*
|
Binary file not shown.
|
@ -0,0 +1,6 @@
|
|||
apiVersion: v1
|
||||
name: terraform-controller
|
||||
version: 0.1.0
|
||||
description: A Kubernetes Terraform controller
|
||||
home: https://github.com/oam-dev/terraform-controller
|
||||
appVersion: 0.1.0
|
|
@ -0,0 +1,498 @@
|
|||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.16.5
|
||||
name: configurations.terraform.core.oam.dev
|
||||
spec:
|
||||
group: terraform.core.oam.dev
|
||||
names:
|
||||
kind: Configuration
|
||||
listKind: ConfigurationList
|
||||
plural: configurations
|
||||
shortNames:
|
||||
- conf
|
||||
- terraform-conf
|
||||
singular: configuration
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .status.apply.state
|
||||
name: STATE
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: AGE
|
||||
type: date
|
||||
name: v1beta1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: Configuration is the Schema for the configurations API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: ConfigurationSpec defines the desired state of Configuration
|
||||
properties:
|
||||
JSON:
|
||||
description: |-
|
||||
JSON is the Terraform JSON syntax configuration.
|
||||
Deprecated: after v0.3.1, use HCL instead.
|
||||
type: string
|
||||
backend:
|
||||
description: |-
|
||||
Backend stores the state in a Kubernetes secret with locking done using a Lease resource.
|
||||
still will set by the controller, ignoring the settings in HCL/JSON backend
|
||||
properties:
|
||||
inClusterConfig:
|
||||
description: InClusterConfig Used to authenticate to the cluster
|
||||
from inside a pod. Only `true` is allowed
|
||||
type: boolean
|
||||
secretSuffix:
|
||||
description: 'SecretSuffix used when creating secrets. Secrets
|
||||
will be named in the format: tfstate-{workspace}-{secretSuffix}'
|
||||
type: string
|
||||
type: object
|
||||
deleteResource:
|
||||
default: true
|
||||
description: DeleteResource will determine whether provisioned cloud
|
||||
resources will be deleted when CR is deleted
|
||||
type: boolean
|
||||
gitCredentialsSecretReference:
|
||||
description: GitCredentialsSecretReference specifies the reference
|
||||
to the secret containing the git credentials
|
||||
properties:
|
||||
name:
|
||||
description: name is unique within a namespace to reference a
|
||||
secret resource.
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace defines the space within which the secret
|
||||
name must be unique.
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
hcl:
|
||||
description: HCL is the Terraform HCL type configuration
|
||||
type: string
|
||||
path:
|
||||
description: Path is the sub-directory of remote git repository.
|
||||
type: string
|
||||
providerRef:
|
||||
description: ProviderReference specifies the reference to Provider
|
||||
properties:
|
||||
name:
|
||||
description: Name of the referenced object.
|
||||
type: string
|
||||
namespace:
|
||||
default: default
|
||||
description: Namespace of the referenced object.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
region:
|
||||
description: Region is cloud provider's region. It will override the
|
||||
region in the region field of ProviderReference
|
||||
type: string
|
||||
remote:
|
||||
description: Remote is a git repo which contains hcl files. Currently,
|
||||
only public git repos are supported.
|
||||
type: string
|
||||
terraformCredentialsHelperConfigMapReference:
|
||||
description: TerraformCredentialsHelperConfigMapReference specifies
|
||||
the reference to a configmap containing the terraform registry credentials
|
||||
helper
|
||||
properties:
|
||||
name:
|
||||
description: name is unique within a namespace to reference a
|
||||
secret resource.
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace defines the space within which the secret
|
||||
name must be unique.
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
terraformCredentialsSecretReference:
|
||||
description: TerraformCredentialsSecretReference specifies the reference
|
||||
to the secret containing the terraform credentials
|
||||
properties:
|
||||
name:
|
||||
description: name is unique within a namespace to reference a
|
||||
secret resource.
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace defines the space within which the secret
|
||||
name must be unique.
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
terraformRCConfigMapReference:
|
||||
description: TerraformRCConfigMapReference specifies the reference
|
||||
to a config map containing the terraform registry configuration
|
||||
properties:
|
||||
name:
|
||||
description: name is unique within a namespace to reference a
|
||||
secret resource.
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace defines the space within which the secret
|
||||
name must be unique.
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
variable:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
writeConnectionSecretToRef:
|
||||
description: |-
|
||||
WriteConnectionSecretToReference specifies the namespace and name of a
|
||||
Secret to which any connection details for this managed resource should
|
||||
be written. Connection details frequently include the endpoint, username,
|
||||
and password required to connect to the managed resource.
|
||||
properties:
|
||||
name:
|
||||
description: Name of the secret.
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the secret.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: object
|
||||
status:
|
||||
description: ConfigurationStatus defines the observed state of Configuration
|
||||
properties:
|
||||
apply:
|
||||
description: ConfigurationApplyStatus is the status for Configuration
|
||||
apply
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
outputs:
|
||||
additionalProperties:
|
||||
description: Property is the property for an output
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
state:
|
||||
description: A ConfigurationState represents the status of a resource
|
||||
type: string
|
||||
type: object
|
||||
destroy:
|
||||
description: ConfigurationDestroyStatus is the status for Configuration
|
||||
destroy
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
state:
|
||||
description: A ConfigurationState represents the status of a resource
|
||||
type: string
|
||||
type: object
|
||||
observedGeneration:
|
||||
description: |-
|
||||
observedGeneration is the most recent generation observed for this Configuration. It corresponds to the
|
||||
Configuration's generation, which is updated on mutation by the API Server.
|
||||
format: int64
|
||||
type: integer
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: false
|
||||
subresources:
|
||||
status: {}
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .status.apply.state
|
||||
name: APPLY
|
||||
type: string
|
||||
- jsonPath: .status.destroy.state
|
||||
name: DESTROY
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: AGE
|
||||
type: date
|
||||
name: v1beta2
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: Configuration is the Schema for the configurations API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: ConfigurationSpec defines the desired state of Configuration
|
||||
properties:
|
||||
JobEnv:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
backend:
|
||||
description: |-
|
||||
Backend describes the Terraform backend configuration.
|
||||
This field is needed if the users use a git repo to provide the hcl files or
|
||||
want to use their custom Terraform backend (instead of the default kubernetes backend type).
|
||||
Notice: This field may cause two backend blocks in the final Terraform module and make the executor job failed.
|
||||
So, please make sure that there are no backend configurations in your inline hcl code or the git repo.
|
||||
properties:
|
||||
backendType:
|
||||
description: BackendType indicates which backend type to use.
|
||||
This field is needed for custom backend configuration.
|
||||
enum:
|
||||
- kubernetes
|
||||
- s3
|
||||
type: string
|
||||
inClusterConfig:
|
||||
description: InClusterConfig Used to authenticate to the cluster
|
||||
from inside a pod. Only `true` is allowed
|
||||
type: boolean
|
||||
inline:
|
||||
description: Inline allows users to use raw hcl code to specify
|
||||
their Terraform backend
|
||||
type: string
|
||||
kubernetes:
|
||||
description: Kubernetes is needed for the Terraform `kubernetes`
|
||||
backend type.
|
||||
properties:
|
||||
namespace:
|
||||
type: string
|
||||
secret_suffix:
|
||||
type: string
|
||||
required:
|
||||
- secret_suffix
|
||||
type: object
|
||||
s3:
|
||||
description: S3 is needed for the Terraform `s3` backend type.
|
||||
properties:
|
||||
bucket:
|
||||
type: string
|
||||
key:
|
||||
type: string
|
||||
region:
|
||||
description: Region is optional, default to the AWS_DEFAULT_REGION
|
||||
in the credentials of the provider
|
||||
type: string
|
||||
required:
|
||||
- bucket
|
||||
- key
|
||||
type: object
|
||||
secretSuffix:
|
||||
description: 'SecretSuffix used when creating secrets. Secrets
|
||||
will be named in the format: tfstate-{workspace}-{secretSuffix}'
|
||||
type: string
|
||||
type: object
|
||||
customRegion:
|
||||
description: Region is cloud provider's region. It will override the
|
||||
region in the region field of ProviderReference
|
||||
type: string
|
||||
deleteResource:
|
||||
default: true
|
||||
description: DeleteResource will determine whether provisioned cloud
|
||||
resources will be deleted when CR is deleted
|
||||
type: boolean
|
||||
forceDelete:
|
||||
description: |-
|
||||
ForceDelete will force delete Configuration no matter which state it is or whether it has provisioned some resources
|
||||
It will help delete Configuration in unexpected cases.
|
||||
type: boolean
|
||||
gitCredentialsSecretReference:
|
||||
description: GitCredentialsSecretReference specifies the reference
|
||||
to the secret containing the git credentials
|
||||
properties:
|
||||
name:
|
||||
description: name is unique within a namespace to reference a
|
||||
secret resource.
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace defines the space within which the secret
|
||||
name must be unique.
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
gitRef:
|
||||
description: GitRef is the git branch or tag or commit hash to checkout.
|
||||
Only used when Remote is specified.
|
||||
properties:
|
||||
branch:
|
||||
type: string
|
||||
commit:
|
||||
type: string
|
||||
tag:
|
||||
type: string
|
||||
type: object
|
||||
hcl:
|
||||
description: HCL is the Terraform HCL type configuration
|
||||
type: string
|
||||
inlineCredentials:
|
||||
description: "InlineCredentials specifies the credentials in spec.HCl
|
||||
field as below.\n\tprovider \"aws\" {\n\t\tregion = \"us-west-2\"\n\t\taccess_key
|
||||
= \"my-access-key\"\n\t\tsecret_key = \"my-secret-key\"\n\t}\nOr
|
||||
indicates a Terraform module or configuration don't need credentials
|
||||
at all, like provider `random`"
|
||||
type: boolean
|
||||
path:
|
||||
description: Path is the sub-directory of remote git repository.
|
||||
type: string
|
||||
providerRef:
|
||||
description: ProviderReference specifies the reference to Provider
|
||||
properties:
|
||||
name:
|
||||
description: Name of the referenced object.
|
||||
type: string
|
||||
namespace:
|
||||
default: default
|
||||
description: Namespace of the referenced object.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
remote:
|
||||
description: Remote is a git repo which contains hcl files. Currently,
|
||||
only public git repos are supported.
|
||||
type: string
|
||||
terraformCredentialsHelperConfigMapReference:
|
||||
description: TerraformCredentialsHelperConfigMapReference specifies
|
||||
the reference to a configmap containing the terraform registry credentials
|
||||
helper
|
||||
properties:
|
||||
name:
|
||||
description: name is unique within a namespace to reference a
|
||||
secret resource.
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace defines the space within which the secret
|
||||
name must be unique.
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
terraformCredentialsSecretReference:
|
||||
description: TerraformCredentialsSecretReference specifies the reference
|
||||
to the secret containing the terraform credentials and terraform
|
||||
registry details
|
||||
properties:
|
||||
name:
|
||||
description: name is unique within a namespace to reference a
|
||||
secret resource.
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace defines the space within which the secret
|
||||
name must be unique.
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
terraformRCConfigMapReference:
|
||||
description: TerraformRCConfigMapReference specifies the reference
|
||||
to a config map containing the terraform registry configuration
|
||||
properties:
|
||||
name:
|
||||
description: name is unique within a namespace to reference a
|
||||
secret resource.
|
||||
type: string
|
||||
namespace:
|
||||
description: namespace defines the space within which the secret
|
||||
name must be unique.
|
||||
type: string
|
||||
type: object
|
||||
x-kubernetes-map-type: atomic
|
||||
variable:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
writeConnectionSecretToRef:
|
||||
description: |-
|
||||
WriteConnectionSecretToReference specifies the namespace and name of a
|
||||
Secret to which any connection details for this managed resource should
|
||||
be written. Connection details frequently include the endpoint, username,
|
||||
and password required to connect to the managed resource.
|
||||
properties:
|
||||
name:
|
||||
description: Name of the secret.
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the secret.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
type: object
|
||||
status:
|
||||
description: ConfigurationStatus defines the observed state of Configuration
|
||||
properties:
|
||||
apply:
|
||||
description: ConfigurationApplyStatus is the status for Configuration
|
||||
apply
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
outputs:
|
||||
additionalProperties:
|
||||
description: Property is the property for an output
|
||||
properties:
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
region:
|
||||
description: |-
|
||||
Region is the region for the cloud resources created by this Configuration. If spec.region is not empty, it's the
|
||||
value of it. Otherwise, it's the value of spec.providerReference.region.
|
||||
type: string
|
||||
state:
|
||||
description: A ConfigurationState represents the status of a resource
|
||||
type: string
|
||||
type: object
|
||||
destroy:
|
||||
description: ConfigurationDestroyStatus is the status for Configuration
|
||||
destroy
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
state:
|
||||
description: A ConfigurationState represents the status of a resource
|
||||
type: string
|
||||
type: object
|
||||
observedGeneration:
|
||||
description: |-
|
||||
observedGeneration is the most recent generation observed for this Configuration. It corresponds to the
|
||||
Configuration's generation, which is updated on mutation by the API Server.
|
||||
If ObservedGeneration equals Generation, and State is Available, the value of Outputs is latest
|
||||
format: int64
|
||||
type: integer
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
|
@ -0,0 +1,105 @@
|
|||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.16.5
|
||||
name: providers.terraform.core.oam.dev
|
||||
spec:
|
||||
group: terraform.core.oam.dev
|
||||
names:
|
||||
kind: Provider
|
||||
listKind: ProviderList
|
||||
plural: providers
|
||||
singular: provider
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .status.state
|
||||
name: STATE
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: AGE
|
||||
type: date
|
||||
name: v1beta1
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
description: Provider is the Schema for the providers API.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: |-
|
||||
APIVersion defines the versioned schema of this representation of an object.
|
||||
Servers should convert recognized schemas to the latest internal value, and
|
||||
may reject unrecognized values.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||
type: string
|
||||
kind:
|
||||
description: |-
|
||||
Kind is a string value representing the REST resource this object represents.
|
||||
Servers may infer this from the endpoint the client submits requests to.
|
||||
Cannot be updated.
|
||||
In CamelCase.
|
||||
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: ProviderSpec defines the desired state of Provider.
|
||||
properties:
|
||||
credentials:
|
||||
description: Credentials required to authenticate to this provider.
|
||||
properties:
|
||||
secretRef:
|
||||
description: |-
|
||||
A SecretRef is a reference to a secret key that contains the credentials
|
||||
that must be used to connect to the provider.
|
||||
properties:
|
||||
key:
|
||||
description: The key to select.
|
||||
type: string
|
||||
name:
|
||||
description: Name of the secret.
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the secret.
|
||||
type: string
|
||||
required:
|
||||
- key
|
||||
- name
|
||||
type: object
|
||||
source:
|
||||
description: Source of the provider credentials.
|
||||
enum:
|
||||
- None
|
||||
- Secret
|
||||
- InjectedIdentity
|
||||
- Environment
|
||||
- Filesystem
|
||||
type: string
|
||||
required:
|
||||
- source
|
||||
type: object
|
||||
provider:
|
||||
description: Provider is the cloud service provider, like `alibaba`
|
||||
type: string
|
||||
region:
|
||||
description: Region is cloud provider's region
|
||||
type: string
|
||||
required:
|
||||
- credentials
|
||||
- provider
|
||||
type: object
|
||||
status:
|
||||
description: ProviderStatus defines the observed state of Provider.
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
state:
|
||||
description: ProviderState is the type for Provider state
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
|
@ -0,0 +1,67 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: terraform-controller
|
||||
namespace: {{ .Release.Namespace }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: terraform-controller
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: terraform-controller
|
||||
app.kubernetes.io/name: {{ .Release.Name }}
|
||||
app.kubernetes.io/part-of: kubevela
|
||||
app.kubernetes.io/managed-by: helm
|
||||
spec:
|
||||
containers:
|
||||
- name: terraform-controller
|
||||
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
args:
|
||||
{{- if .Values.controllerNamespace }}
|
||||
- --controller-namespace={{ .Values.controllerNamespace }}
|
||||
{{- end }}
|
||||
- --feature-gates=AllowDeleteProvisioningResource={{ .Values.featureGates.AllowDeleteProvisioningResource }}
|
||||
env:
|
||||
- name: CONTROLLER_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: TERRAFORM_IMAGE
|
||||
value: {{ .Values.terraformImage}}
|
||||
- name: TERRAFORM_BACKEND_NAMESPACE
|
||||
value: {{ .Values.backend.namespace }}
|
||||
- name: BUSYBOX_IMAGE
|
||||
value: {{ .Values.busyboxImage}}
|
||||
- name: GIT_IMAGE
|
||||
value: {{ .Values.gitImage}}
|
||||
- name: GITHUB_BLOCKED
|
||||
value: {{ .Values.githubBlocked }}
|
||||
{{ if .Values.jobBackoffLimit }}
|
||||
- name: JOB_BACKOFF_LIMIT
|
||||
value: {{ .Values.jobBackoffLimit }}
|
||||
{{ end }}
|
||||
{{ if .Values.jobNodeSelector }}
|
||||
- name: JOB_NODE_SELECTOR
|
||||
value: {{ .Values.jobNodeSelector }}
|
||||
{{ end }}
|
||||
{{ if .Values.resources.limits.cpu }}
|
||||
- name: RESOURCES_LIMITS_CPU
|
||||
value: {{ .Values.resources.limits.cpu }}
|
||||
{{ end }}
|
||||
{{ if .Values.resources.limits.memory }}
|
||||
- name: RESOURCES_LIMITS_MEMORY
|
||||
value: {{ .Values.resources.limits.memory }}
|
||||
{{ end }}
|
||||
{{ if .Values.resources.requests.cpu }}
|
||||
- name: RESOURCES_REQUESTS_CPU
|
||||
value: {{ .Values.resources.requests.cpu }}
|
||||
{{ end }}
|
||||
{{ if .Values.resources.requests.memory }}
|
||||
- name: RESOURCES_REQUESTS_MEMORY
|
||||
value: {{ .Values.resources.requests.memory }}
|
||||
{{ end }}
|
||||
serviceAccountName: tf-controller-service-account
|
|
@ -0,0 +1,96 @@
|
|||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: tf-controller-clusterrole
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- "configmaps"
|
||||
verbs:
|
||||
- "list"
|
||||
- "watch"
|
||||
- "get"
|
||||
- "create"
|
||||
- "update"
|
||||
- "watch"
|
||||
- "delete"
|
||||
|
||||
# Required to write terraform outputs
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- "secrets"
|
||||
- "serviceaccounts"
|
||||
verbs:
|
||||
- "get"
|
||||
- "list"
|
||||
- "create"
|
||||
- "update"
|
||||
- "delete"
|
||||
- "watch"
|
||||
- "delete"
|
||||
|
||||
- apiGroups:
|
||||
- "batch"
|
||||
resources:
|
||||
- "jobs"
|
||||
verbs:
|
||||
- "get"
|
||||
- "list"
|
||||
- "create"
|
||||
- "update"
|
||||
- "delete"
|
||||
- "watch"
|
||||
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- "pods/log"
|
||||
- "pods"
|
||||
verbs:
|
||||
- "get"
|
||||
- "list"
|
||||
- "create"
|
||||
- "update"
|
||||
- "delete"
|
||||
- "watch"
|
||||
- "delete"
|
||||
|
||||
- apiGroups:
|
||||
- "terraform.core.oam.dev"
|
||||
resources:
|
||||
- "configurations"
|
||||
- "providers"
|
||||
- "providers/status"
|
||||
- "configurations/status"
|
||||
verbs:
|
||||
- "get"
|
||||
- "list"
|
||||
- "create"
|
||||
- "update"
|
||||
- "delete"
|
||||
- "watch"
|
||||
|
||||
- apiGroups:
|
||||
- "rbac.authorization.k8s.io"
|
||||
resources:
|
||||
- "clusterroles"
|
||||
- "clusterrolebindings"
|
||||
verbs:
|
||||
- "get"
|
||||
- "list"
|
||||
- "create"
|
||||
- "update"
|
||||
- "delete"
|
||||
- "watch"
|
||||
|
||||
- apiGroups:
|
||||
- "coordination.k8s.io"
|
||||
resources:
|
||||
- "leases"
|
||||
verbs:
|
||||
- "get"
|
||||
- "create"
|
||||
- "update"
|
||||
- "delete"
|
|
@ -0,0 +1,12 @@
|
|||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: tf-controller-clusterrolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: tf-controller-clusterrole
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: tf-controller-service-account
|
||||
namespace: {{ .Release.Namespace }}
|
|
@ -0,0 +1,5 @@
|
|||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: tf-controller-service-account
|
||||
namespace: {{ .Release.Namespace }}
|
|
@ -0,0 +1,34 @@
|
|||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: oamdev/terraform-controller
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
|
||||
gitImage: alpine/git:latest
|
||||
busyboxImage: busybox:latest
|
||||
terraformImage: oamdev/docker-terraform:1.1.5
|
||||
controllerNamespace: ""
|
||||
|
||||
# "{\"nat\": \"true\"}"
|
||||
jobNodeSelector: ""
|
||||
jobBackoffLimit: ""
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "500Mi"
|
||||
requests:
|
||||
cpu: "250m"
|
||||
memory: "250Mi"
|
||||
|
||||
backend:
|
||||
namespace: vela-system
|
||||
|
||||
githubBlocked: "'false'"
|
||||
|
||||
featureGates:
|
||||
# Enable the feature of allowing to delete a configuration whose cloud resources is not fully provisioned, or error happens
|
||||
# This guarantees that the partial cloud resources will be deleted when the configuration is deleted
|
||||
# Default value is true
|
||||
AllowDeleteProvisioningResource: true
|
|
@ -0,0 +1,9 @@
|
|||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 0.1%
|
||||
patch:
|
||||
default:
|
||||
target: 80%
|
|
@ -1,95 +0,0 @@
|
|||
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.2.5
|
||||
creationTimestamp: null
|
||||
name: configurations.terraform.core.oam.dev
|
||||
spec:
|
||||
group: terraform.core.oam.dev
|
||||
names:
|
||||
kind: Configuration
|
||||
listKind: ConfigurationList
|
||||
plural: configurations
|
||||
singular: configuration
|
||||
scope: Namespaced
|
||||
validation:
|
||||
openAPIV3Schema:
|
||||
description: Configuration is the Schema for the configurations API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: ConfigurationSpec defines the desired state of Configuration
|
||||
properties:
|
||||
JSON:
|
||||
description: JSON is the Terraform JSON syntax configuration
|
||||
type: string
|
||||
hcl:
|
||||
description: HCL is the Terraform HCL type configuration
|
||||
type: string
|
||||
variable:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
writeConnectionSecretToRef:
|
||||
description: WriteConnectionSecretToReference specifies the namespace
|
||||
and name of a Secret to which any connection details for this managed
|
||||
resource should be written. Connection details frequently include
|
||||
the endpoint, username, and password required to connect to the managed
|
||||
resource.
|
||||
properties:
|
||||
name:
|
||||
description: Name of the secret.
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the secret.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- namespace
|
||||
type: object
|
||||
required:
|
||||
- variable
|
||||
type: object
|
||||
status:
|
||||
description: ConfigurationStatus defines the observed state of Configuration
|
||||
properties:
|
||||
outputs:
|
||||
additionalProperties:
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
state:
|
||||
description: 'INSERT ADDITIONAL STATUS FIELD - define observed state
|
||||
of cluster Important: Run "make" to regenerate code after modifying
|
||||
this file'
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
version: v1beta1
|
||||
versions:
|
||||
- name: v1beta1
|
||||
served: true
|
||||
storage: true
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
|
@ -1,95 +0,0 @@
|
|||
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.2.5
|
||||
creationTimestamp: null
|
||||
name: providers.terraform.core.oam.dev
|
||||
spec:
|
||||
group: terraform.core.oam.dev
|
||||
names:
|
||||
kind: Provider
|
||||
listKind: ProviderList
|
||||
plural: providers
|
||||
singular: provider
|
||||
scope: Namespaced
|
||||
validation:
|
||||
openAPIV3Schema:
|
||||
description: Provider is the Schema for the providerconfigs API.
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: ProviderSpec defines the desired state of Provider.
|
||||
properties:
|
||||
credentials:
|
||||
description: Credentials required to authenticate to this provider.
|
||||
properties:
|
||||
secretRef:
|
||||
description: A SecretRef is a reference to a secret key that contains
|
||||
the credentials that must be used to connect to the provider.
|
||||
properties:
|
||||
key:
|
||||
description: The key to select.
|
||||
type: string
|
||||
name:
|
||||
description: Name of the secret.
|
||||
type: string
|
||||
namespace:
|
||||
description: Namespace of the secret.
|
||||
type: string
|
||||
required:
|
||||
- key
|
||||
- name
|
||||
- namespace
|
||||
type: object
|
||||
source:
|
||||
description: Source of the provider credentials.
|
||||
enum:
|
||||
- None
|
||||
- Secret
|
||||
- InjectedIdentity
|
||||
- Environment
|
||||
- Filesystem
|
||||
type: string
|
||||
required:
|
||||
- source
|
||||
type: object
|
||||
provider:
|
||||
description: Provider is the cloud service provider, like `alibaba`
|
||||
type: string
|
||||
region:
|
||||
description: Region is cloud provider's region
|
||||
type: string
|
||||
required:
|
||||
- credentials
|
||||
- provider
|
||||
- region
|
||||
type: object
|
||||
status:
|
||||
description: ProviderStatus defines the observed state of Provider.
|
||||
type: object
|
||||
type: object
|
||||
version: v1beta1
|
||||
versions:
|
||||
- name: v1beta1
|
||||
served: true
|
||||
storage: true
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
|
@ -1,13 +1,13 @@
|
|||
# permissions for end users to edit providerconfigs.
|
||||
# permissions for end users to edit providers.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: providerconfig-editor-role
|
||||
name: provider-editor-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- terraform.core.oam.dev
|
||||
resources:
|
||||
- providerconfigs
|
||||
- providers
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
|
@ -19,6 +19,6 @@ rules:
|
|||
- apiGroups:
|
||||
- terraform.core.oam.dev
|
||||
resources:
|
||||
- providerconfigs/status
|
||||
- providers/status
|
||||
verbs:
|
||||
- get
|
|
@ -1,13 +1,13 @@
|
|||
# permissions for end users to view providerconfigs.
|
||||
# permissions for end users to view providers.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: providerconfig-viewer-role
|
||||
name: provider-viewer-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- terraform.core.oam.dev
|
||||
resources:
|
||||
- providerconfigs
|
||||
- providers
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
|
@ -15,6 +15,6 @@ rules:
|
|||
- apiGroups:
|
||||
- terraform.core.oam.dev
|
||||
resources:
|
||||
- providerconfigs/status
|
||||
- providers/status
|
||||
verbs:
|
||||
- get
|
|
@ -4,7 +4,7 @@ apiVersion: rbac.authorization.k8s.io/v1
|
|||
kind: ClusterRole
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: manager-role
|
||||
name: tf-api-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- terraform.core.oam.dev
|
||||
|
@ -29,7 +29,7 @@ rules:
|
|||
- apiGroups:
|
||||
- terraform.core.oam.dev
|
||||
resources:
|
||||
- providerconfigs
|
||||
- providers
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
|
@ -41,7 +41,7 @@ rules:
|
|||
- apiGroups:
|
||||
- terraform.core.oam.dev
|
||||
resources:
|
||||
- providerconfigs/status
|
||||
- providers/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: manager-rolebinding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: manager-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: default
|
||||
namespace: system
|
|
@ -1,7 +0,0 @@
|
|||
apiVersion: terraform.core.oam.dev/v1beta1
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: configuration-sample
|
||||
spec:
|
||||
# Add fields here
|
||||
foo: bar
|
|
@ -1,7 +0,0 @@
|
|||
apiVersion: terraform.core.oam.dev/v1beta1
|
||||
kind: ProviderConfig
|
||||
metadata:
|
||||
name: providerconfig-sample
|
||||
spec:
|
||||
# Add fields here
|
||||
foo: bar
|
|
@ -0,0 +1,15 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
)
|
||||
|
||||
// Init initializes the Go client set
|
||||
func Init() (*kubernetes.Clientset, error) {
|
||||
config, err := config.GetConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return kubernetes.NewForConfig(config)
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/agiledragon/gomonkey/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
)
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
type args struct {
|
||||
configFile string
|
||||
}
|
||||
|
||||
type want struct {
|
||||
errMsg string
|
||||
}
|
||||
|
||||
pwd, err := os.Getwd()
|
||||
assert.NoError(t, err)
|
||||
kubeConfig := filepath.Join(pwd, "config")
|
||||
assert.NoError(t, os.WriteFile(kubeConfig, []byte(""), 0400))
|
||||
defer os.Remove(kubeConfig)
|
||||
os.Setenv("KUBECONFIG", kubeConfig)
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "init",
|
||||
args: args{},
|
||||
want: want{
|
||||
errMsg: "invalid configuration",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if _, err := Init(); tc.want.errMsg != "" && !strings.Contains(err.Error(), tc.want.errMsg) {
|
||||
t.Errorf("Init() error = %v, wantErr %v", err, tc.want.errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitWithWrongConfig(t *testing.T) {
|
||||
type args struct {
|
||||
configFile string
|
||||
}
|
||||
|
||||
type want struct {
|
||||
errMsg string
|
||||
}
|
||||
|
||||
gomonkey.ApplyFunc(config.GetConfigWithContext, func(context string) (*rest.Config, error) {
|
||||
return &rest.Config{}, nil
|
||||
})
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "init",
|
||||
args: args{},
|
||||
want: want{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if _, err := Init(); tc.want.errMsg != "" && !strings.Contains(err.Error(), tc.want.errMsg) {
|
||||
t.Errorf("Init() error = %v, wantErr %v", err, tc.want.errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
Copyright 2022 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
"github.com/hashicorp/hcl/v2/hclparse"
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta2"
|
||||
"github.com/pkg/errors"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
const (
|
||||
backendTypeK8S = "kubernetes"
|
||||
backendTypeS3 = "s3"
|
||||
)
|
||||
|
||||
// Backend is an abstraction of what all backend types can do
|
||||
type Backend interface {
|
||||
// HCL can get the hcl code string
|
||||
HCL() string
|
||||
|
||||
// GetTFStateJSON is used to get the Terraform state json from backend
|
||||
GetTFStateJSON(ctx context.Context) ([]byte, error)
|
||||
|
||||
// CleanUp is used to clean up the backend when delete the configuration object
|
||||
// For example, if the configuration use kubernetes backend, CleanUp will delete the backend secret
|
||||
CleanUp(ctx context.Context) error
|
||||
}
|
||||
|
||||
type backendInitFunc func(k8sClient client.Client, backendConf interface{}, credentials map[string]string) (Backend, error)
|
||||
|
||||
var backendInitFuncMap = map[string]backendInitFunc{
|
||||
backendTypeK8S: newK8SBackend,
|
||||
backendTypeS3: newS3Backend,
|
||||
}
|
||||
|
||||
// ParseConfigurationBackend parses backend Conf from the v1beta2.Configuration
|
||||
func ParseConfigurationBackend(configuration *v1beta2.Configuration, k8sClient client.Client, credentials map[string]string, controllerNSSpecified bool) (Backend, error) {
|
||||
backend := configuration.Spec.Backend
|
||||
|
||||
var (
|
||||
backendType string
|
||||
backendConf interface{}
|
||||
err error
|
||||
)
|
||||
|
||||
switch {
|
||||
case backend == nil || (backend.Inline == "" && backend.BackendType == ""):
|
||||
// use the default k8s backend
|
||||
return handleDefaultBackend(configuration, k8sClient, controllerNSSpecified)
|
||||
|
||||
case backend.Inline != "" && backend.BackendType != "":
|
||||
return nil, errors.New("it's not allowed to set `spec.backend.inline` and `spec.backend.backendType` at the same time")
|
||||
|
||||
case backend.Inline != "":
|
||||
// In this case, use the inline custom backend
|
||||
backendType, backendConf, err = handleInlineBackendHCL(backend.Inline)
|
||||
|
||||
case backend.BackendType != "":
|
||||
// In this case, use the explicit custom backend
|
||||
// we don't change backend secret suffix to UID of configuration here.
|
||||
// If backend specified, it's user's responsibility to set the right secret suffix, to avoid conflict.
|
||||
backendType, backendConf, err = handleExplicitBackend(backend)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
initFunc := backendInitFuncMap[backendType]
|
||||
if initFunc == nil {
|
||||
return nil, fmt.Errorf("backend type (%s) is not supported", backendType)
|
||||
}
|
||||
return initFunc(k8sClient, backendConf, credentials)
|
||||
}
|
||||
|
||||
func handleDefaultBackend(configuration *v1beta2.Configuration, k8sClient client.Client, controllerNSSpecified bool) (Backend, error) {
|
||||
if configuration.Spec.Backend != nil {
|
||||
if configuration.Spec.Backend.SecretSuffix == "" {
|
||||
configuration.Spec.Backend.SecretSuffix = configuration.Name
|
||||
}
|
||||
configuration.Spec.Backend.InClusterConfig = true
|
||||
} else {
|
||||
configuration.Spec.Backend = &v1beta2.Backend{
|
||||
SecretSuffix: configuration.Name,
|
||||
InClusterConfig: true,
|
||||
}
|
||||
}
|
||||
return newDefaultK8SBackend(configuration, k8sClient, controllerNSSpecified), nil
|
||||
}
|
||||
|
||||
func handleInlineBackendHCL(hclCode string) (string, interface{}, error) {
|
||||
type TerraformConfig struct {
|
||||
Terraform struct {
|
||||
Backend struct {
|
||||
Name string `hcl:"name,label"`
|
||||
Attrs hcl.Body `hcl:",remain"`
|
||||
} `hcl:"backend,block"`
|
||||
} `hcl:"terraform,block"`
|
||||
}
|
||||
|
||||
hclFile, diags := hclparse.NewParser().ParseHCL([]byte(hclCode), "backend")
|
||||
if diags.HasErrors() {
|
||||
return "", nil, fmt.Errorf("there are syntax errors in the inline backend hcl code: %w", diags)
|
||||
}
|
||||
|
||||
// try to parse hclFile to TerraformConfig or TerraformConfig.Terraform
|
||||
config := &TerraformConfig{}
|
||||
diags = gohcl.DecodeBody(hclFile.Body, nil, config)
|
||||
if diags.HasErrors() || config.Terraform.Backend.Name == "" {
|
||||
diags = gohcl.DecodeBody(hclFile.Body, nil, &config.Terraform)
|
||||
if diags.HasErrors() || config.Terraform.Backend.Name == "" {
|
||||
return "", nil, fmt.Errorf("the inline backend hcl code is not valid Terraform backend configuration: %w", diags)
|
||||
}
|
||||
}
|
||||
|
||||
backendType := config.Terraform.Backend.Name
|
||||
|
||||
var backendConf interface{}
|
||||
switch strings.ToLower(backendType) {
|
||||
case backendTypeK8S:
|
||||
backendConf = &v1beta2.KubernetesBackendConf{}
|
||||
case backendTypeS3:
|
||||
backendConf = &v1beta2.S3BackendConf{}
|
||||
default:
|
||||
return "", nil, fmt.Errorf("backend type (%s) is not supported", backendType)
|
||||
}
|
||||
diags = gohcl.DecodeBody(config.Terraform.Backend.Attrs, nil, backendConf)
|
||||
if diags.HasErrors() {
|
||||
return "", nil, fmt.Errorf("the inline backend hcl code is not valid Terraform backend configuration: %w", diags)
|
||||
}
|
||||
|
||||
return backendType, backendConf, nil
|
||||
}
|
||||
|
||||
func handleExplicitBackend(backend *v1beta2.Backend) (string, interface{}, error) {
|
||||
// check if is valid custom backend
|
||||
backendType := backend.BackendType
|
||||
|
||||
// fetch backendConfValue using reflection
|
||||
backendStructValue := reflect.ValueOf(backend)
|
||||
if backendStructValue.Kind() == reflect.Ptr {
|
||||
backendStructValue = backendStructValue.Elem()
|
||||
}
|
||||
backendField := backendStructValue.FieldByNameFunc(func(name string) bool {
|
||||
return strings.EqualFold(name, backendType)
|
||||
})
|
||||
if backendField.Kind() != reflect.Ptr || backendField.IsNil() {
|
||||
return "", nil, fmt.Errorf("there is no configuration for backendType %s", backend.BackendType)
|
||||
}
|
||||
return backendType, backendField.Interface(), nil
|
||||
}
|
|
@ -0,0 +1,346 @@
|
|||
package backend
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta2"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
func TestParseConfigurationBackend(t *testing.T) {
|
||||
type args struct {
|
||||
configuration *v1beta2.Configuration
|
||||
credentials map[string]string
|
||||
controllerNSSpecified bool
|
||||
}
|
||||
type want struct {
|
||||
backend Backend
|
||||
errMsg string
|
||||
}
|
||||
|
||||
secret := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "a",
|
||||
Name: "secretref",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"access": []byte("access_key"),
|
||||
},
|
||||
}
|
||||
configMap := &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "a",
|
||||
Name: "configmapref",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"token": "token",
|
||||
},
|
||||
}
|
||||
k8sClient := fake.NewClientBuilder().WithObjects(secret, configMap).Build()
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "backend is not nil, configuration is hcl",
|
||||
args: args{
|
||||
configuration: &v1beta2.Configuration{
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
Backend: &v1beta2.Backend{},
|
||||
HCL: "image_id=123",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
backend: &K8SBackend{
|
||||
Client: k8sClient,
|
||||
HCLCode: `
|
||||
terraform {
|
||||
backend "kubernetes" {
|
||||
secret_suffix = ""
|
||||
in_cluster_config = true
|
||||
namespace = ""
|
||||
}
|
||||
}
|
||||
`,
|
||||
SecretSuffix: "",
|
||||
SecretNS: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "backend is nil, configuration is remote",
|
||||
args: args{
|
||||
configuration: &v1beta2.Configuration{
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
Remote: "https://github.com/a/b.git",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
backend: &K8SBackend{
|
||||
Client: k8sClient,
|
||||
HCLCode: `
|
||||
terraform {
|
||||
backend "kubernetes" {
|
||||
secret_suffix = ""
|
||||
in_cluster_config = true
|
||||
namespace = ""
|
||||
}
|
||||
}
|
||||
`,
|
||||
SecretSuffix: "",
|
||||
SecretNS: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "backend is not nil, use invalid(has syntax error) inline backend conf",
|
||||
args: args{
|
||||
configuration: &v1beta2.Configuration{
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
Backend: &v1beta2.Backend{
|
||||
Inline: `
|
||||
terraform {
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
errMsg: "there are syntax errors in the inline backend hcl code",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "backend is not nil, use invalid inline backend conf",
|
||||
args: args{
|
||||
configuration: &v1beta2.Configuration{
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
Backend: &v1beta2.Backend{
|
||||
Inline: `
|
||||
terraform {
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
errMsg: "the inline backend hcl code is not valid Terraform backend configuration",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "backend is not nil, use valid inline backend conf",
|
||||
args: args{
|
||||
configuration: &v1beta2.Configuration{
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
Backend: &v1beta2.Backend{
|
||||
Inline: `
|
||||
terraform {
|
||||
backend "kubernetes" {
|
||||
secret_suffix = ""
|
||||
namespace = "vela-system"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
errMsg: "",
|
||||
backend: &K8SBackend{
|
||||
Client: k8sClient,
|
||||
HCLCode: `
|
||||
terraform {
|
||||
backend "kubernetes" {
|
||||
secret_suffix = ""
|
||||
in_cluster_config = true
|
||||
namespace = "vela-system"
|
||||
}
|
||||
}
|
||||
`,
|
||||
SecretSuffix: "",
|
||||
SecretNS: "vela-system",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "backend is not nil, use valid inline backend conf, should wrap",
|
||||
args: args{
|
||||
configuration: &v1beta2.Configuration{
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
Backend: &v1beta2.Backend{
|
||||
Inline: `backend "kubernetes" {
|
||||
secret_suffix = "tt"
|
||||
namespace = "vela-system"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
errMsg: "",
|
||||
backend: &K8SBackend{
|
||||
Client: k8sClient,
|
||||
HCLCode: `
|
||||
terraform {
|
||||
backend "kubernetes" {
|
||||
secret_suffix = "tt"
|
||||
in_cluster_config = true
|
||||
namespace = "vela-system"
|
||||
}
|
||||
}
|
||||
`,
|
||||
SecretSuffix: "tt",
|
||||
SecretNS: "vela-system",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "backend is not nil, use explicit backend conf",
|
||||
args: args{
|
||||
configuration: &v1beta2.Configuration{
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
Backend: &v1beta2.Backend{
|
||||
BackendType: backendTypeK8S,
|
||||
Kubernetes: &v1beta2.KubernetesBackendConf{
|
||||
SecretSuffix: "suffix",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
errMsg: "",
|
||||
backend: &K8SBackend{
|
||||
Client: k8sClient,
|
||||
HCLCode: `
|
||||
terraform {
|
||||
backend "kubernetes" {
|
||||
secret_suffix = "suffix"
|
||||
in_cluster_config = true
|
||||
namespace = ""
|
||||
}
|
||||
}
|
||||
`,
|
||||
SecretSuffix: "suffix",
|
||||
SecretNS: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "backend is not nil, use explicit backend conf, no backendType",
|
||||
args: args{
|
||||
configuration: &v1beta2.Configuration{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "a"},
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
Backend: &v1beta2.Backend{
|
||||
Kubernetes: &v1beta2.KubernetesBackendConf{
|
||||
SecretSuffix: "suffix",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
errMsg: "",
|
||||
backend: &K8SBackend{
|
||||
Client: k8sClient,
|
||||
HCLCode: `
|
||||
terraform {
|
||||
backend "kubernetes" {
|
||||
secret_suffix = ""
|
||||
in_cluster_config = true
|
||||
namespace = "a"
|
||||
}
|
||||
}
|
||||
`,
|
||||
SecretSuffix: "",
|
||||
SecretNS: "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "backend is not nil, use explicit backend conf, invalid backendType",
|
||||
args: args{
|
||||
configuration: &v1beta2.Configuration{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "a"},
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
Backend: &v1beta2.Backend{
|
||||
BackendType: backendTypeK8S,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
errMsg: "there is no configuration for backendType kubernetes",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "backend is not nil, use both inline and explicit",
|
||||
args: args{
|
||||
configuration: &v1beta2.Configuration{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "a"},
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
Backend: &v1beta2.Backend{
|
||||
Inline: `kkk`,
|
||||
BackendType: backendTypeK8S,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
errMsg: "it's not allowed to set `spec.backend.inline` and `spec.backend.backendType` at the same time",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "backend is nil, specify controller namespace, generate backend with legacy secret suffix",
|
||||
args: args{
|
||||
configuration: &v1beta2.Configuration{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "name", Namespace: "ns", UID: "xxxx-xxxx"},
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
Backend: nil,
|
||||
},
|
||||
},
|
||||
controllerNSSpecified: true,
|
||||
},
|
||||
want: want{
|
||||
backend: &K8SBackend{
|
||||
LegacySecretSuffix: "name",
|
||||
SecretNS: "ns",
|
||||
SecretSuffix: "xxxx-xxxx",
|
||||
Client: k8sClient,
|
||||
HCLCode: `
|
||||
terraform {
|
||||
backend "kubernetes" {
|
||||
secret_suffix = "xxxx-xxxx"
|
||||
in_cluster_config = true
|
||||
namespace = "ns"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := ParseConfigurationBackend(tc.args.configuration, k8sClient, tc.args.credentials, tc.args.controllerNSSpecified)
|
||||
if tc.want.errMsg != "" && !strings.Contains(err.Error(), tc.want.errMsg) {
|
||||
t.Errorf("ValidConfigurationObject() error = %v, wantErr %v", err, tc.want.errMsg)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(tc.want.backend, got) {
|
||||
t.Errorf("\ngot %#v,\nwant %#v", got, tc.want.backend)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
Copyright 2022 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta2"
|
||||
"github.com/oam-dev/terraform-controller/controllers/util"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
terraformWorkspace = "default"
|
||||
// TerraformStateNameInSecret is the key name to store Terraform state
|
||||
TerraformStateNameInSecret = "tfstate"
|
||||
// TFBackendSecret is the Secret name for Kubernetes backend
|
||||
TFBackendSecret = "tfstate-%s-%s"
|
||||
)
|
||||
|
||||
// K8SBackend is used to interact with the Terraform kubernetes backend
|
||||
type K8SBackend struct {
|
||||
// Client is used to interact with the Kubernetes apiServer
|
||||
Client client.Client
|
||||
// HCLCode stores the backend hcl code string
|
||||
HCLCode string
|
||||
// SecretSuffix is the suffix of the name of the Terraform backend secret
|
||||
SecretSuffix string
|
||||
// SecretNS is the namespace of the Terraform backend secret
|
||||
SecretNS string
|
||||
// LegacySecretSuffix is the same as SecretSuffix, but only used when `--controller-namespace` is specified
|
||||
LegacySecretSuffix string
|
||||
}
|
||||
|
||||
func newDefaultK8SBackend(configuration *v1beta2.Configuration, client client.Client, controllerNSSpecified bool) *K8SBackend {
|
||||
ns := os.Getenv("TERRAFORM_BACKEND_NAMESPACE")
|
||||
if ns == "" {
|
||||
ns = configuration.GetNamespace()
|
||||
}
|
||||
|
||||
var (
|
||||
suffix = configuration.Spec.Backend.SecretSuffix
|
||||
legacySuffix string
|
||||
)
|
||||
if controllerNSSpecified {
|
||||
legacySuffix = suffix
|
||||
suffix = string(configuration.GetUID())
|
||||
}
|
||||
hcl := renderK8SBackendHCL(suffix, ns)
|
||||
return &K8SBackend{
|
||||
Client: client,
|
||||
HCLCode: hcl,
|
||||
SecretSuffix: suffix,
|
||||
SecretNS: ns,
|
||||
LegacySecretSuffix: legacySuffix,
|
||||
}
|
||||
}
|
||||
|
||||
func newK8SBackend(k8sClient client.Client, backendConf interface{}, _ map[string]string) (Backend, error) {
|
||||
conf, ok := backendConf.(*v1beta2.KubernetesBackendConf)
|
||||
if !ok || conf == nil {
|
||||
return nil, fmt.Errorf("invalid backendConf, want *v1beta2.KubernetesBackendConf, but got %#v", backendConf)
|
||||
}
|
||||
ns := ""
|
||||
if conf.Namespace != nil {
|
||||
ns = *conf.Namespace
|
||||
} else {
|
||||
ns = os.Getenv("TERRAFORM_BACKEND_NAMESPACE")
|
||||
}
|
||||
return &K8SBackend{
|
||||
Client: k8sClient,
|
||||
HCLCode: renderK8SBackendHCL(conf.SecretSuffix, ns),
|
||||
SecretSuffix: conf.SecretSuffix,
|
||||
SecretNS: ns,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func renderK8SBackendHCL(suffix, ns string) string {
|
||||
fmtStr := `
|
||||
terraform {
|
||||
backend "kubernetes" {
|
||||
secret_suffix = "%s"
|
||||
in_cluster_config = true
|
||||
namespace = "%s"
|
||||
}
|
||||
}
|
||||
`
|
||||
return fmt.Sprintf(fmtStr, suffix, ns)
|
||||
}
|
||||
|
||||
func (k *K8SBackend) secretName() string {
|
||||
return fmt.Sprintf(TFBackendSecret, terraformWorkspace, k.SecretSuffix)
|
||||
}
|
||||
|
||||
func (k *K8SBackend) legacySecretName() string {
|
||||
return fmt.Sprintf(TFBackendSecret, terraformWorkspace, k.LegacySecretSuffix)
|
||||
}
|
||||
|
||||
// GetTFStateJSON gets Terraform state json from the Terraform kubernetes backend
|
||||
func (k *K8SBackend) GetTFStateJSON(ctx context.Context) ([]byte, error) {
|
||||
var s = v1.Secret{}
|
||||
// Try to get legacy secret first, if it doesn't exist, try to get new secret
|
||||
err := k.Client.Get(ctx, client.ObjectKey{Name: k.legacySecretName(), Namespace: k.SecretNS}, &s)
|
||||
if err != nil {
|
||||
if err = k.Client.Get(ctx, client.ObjectKey{Name: k.secretName(), Namespace: k.SecretNS}, &s); err != nil {
|
||||
return nil, errors.Wrap(err, "terraform state file backend secret is not generated")
|
||||
}
|
||||
}
|
||||
tfStateData, ok := s.Data[TerraformStateNameInSecret]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to get %s from Terraform State secret %s", TerraformStateNameInSecret, s.Name)
|
||||
}
|
||||
|
||||
tfStateJSON, err := util.DecompressTerraformStateSecret(string(tfStateData))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to decompress state secret data")
|
||||
}
|
||||
return tfStateJSON, nil
|
||||
}
|
||||
|
||||
// CleanUp will delete the Terraform kubernetes backend secret when deleting the configuration object
|
||||
func (k *K8SBackend) CleanUp(ctx context.Context) error {
|
||||
klog.InfoS("Deleting the legacy secret which stores Kubernetes backend", "Name", k.legacySecretName())
|
||||
var kubernetesBackendSecret v1.Secret
|
||||
if err := k.Client.Get(ctx, client.ObjectKey{Name: k.legacySecretName(), Namespace: k.SecretNS}, &kubernetesBackendSecret); err == nil {
|
||||
if err := k.Client.Delete(ctx, &kubernetesBackendSecret); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
klog.InfoS("Deleting the secret which stores Kubernetes backend", "Name", k.secretName())
|
||||
if err := k.Client.Get(ctx, client.ObjectKey{Name: k.secretName(), Namespace: k.SecretNS}, &kubernetesBackendSecret); err == nil {
|
||||
if err := k.Client.Delete(ctx, &kubernetesBackendSecret); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HCL returns the backend hcl code string
|
||||
func (k *K8SBackend) HCL() string {
|
||||
if k.LegacySecretSuffix != "" {
|
||||
err := k.migrateLegacySecret()
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "Failed to migrate legacy secret")
|
||||
}
|
||||
}
|
||||
|
||||
if k.HCLCode == "" {
|
||||
k.HCLCode = renderK8SBackendHCL(k.SecretSuffix, k.SecretNS)
|
||||
}
|
||||
return k.HCLCode
|
||||
}
|
||||
|
||||
// migrateLegacySecret will migrate the legacy secret to the new secret if the legacy secret exists
|
||||
// This is needed when the --controller-namespace is specified and restart the controller
|
||||
func (k *K8SBackend) migrateLegacySecret() error {
|
||||
ctx := context.TODO()
|
||||
s := v1.Secret{}
|
||||
if err := k.Client.Get(ctx, client.ObjectKey{Name: k.legacySecretName(), Namespace: k.SecretNS}, &s); err == nil {
|
||||
klog.InfoS("Migrating legacy secret to new secret", "LegacyName", k.legacySecretName(), "NewName", k.secretName(), "Namespace", k.SecretNS)
|
||||
newSecret := v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: k.secretName(),
|
||||
Namespace: k.SecretNS,
|
||||
},
|
||||
Data: s.Data,
|
||||
}
|
||||
err = k.Client.Create(ctx, &newSecret)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Fail to create new secret, Name: %s, Namespace: %s", k.secretName(), k.SecretNS)
|
||||
} else if err = k.Client.Delete(ctx, &s); err != nil {
|
||||
// Only delete the legacy secret if the new secret is successfully created
|
||||
return errors.Wrapf(err, "Fail to delete legacy secret, Name: %s, Namespace: %s", k.legacySecretName(), k.SecretNS)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,274 @@
|
|||
package backend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
)
|
||||
|
||||
func TestK8SBackend_HCL(t *testing.T) {
|
||||
type fields struct {
|
||||
HCLCode string
|
||||
SecretSuffix string
|
||||
SecretNS string
|
||||
}
|
||||
|
||||
k8sClient := fake.NewClientBuilder().Build()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "HCLCode is empty",
|
||||
fields: fields{
|
||||
SecretSuffix: "tt",
|
||||
SecretNS: "ac",
|
||||
},
|
||||
want: `
|
||||
terraform {
|
||||
backend "kubernetes" {
|
||||
secret_suffix = "tt"
|
||||
in_cluster_config = true
|
||||
namespace = "ac"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "HCLCode is not empty",
|
||||
fields: fields{
|
||||
HCLCode: `
|
||||
terraform {
|
||||
backend "kubernetes" {
|
||||
secret_suffix = "tt"
|
||||
in_cluster_config = true
|
||||
namespace = "ac"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
want: `
|
||||
terraform {
|
||||
backend "kubernetes" {
|
||||
secret_suffix = "tt"
|
||||
in_cluster_config = true
|
||||
namespace = "ac"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
k := &K8SBackend{
|
||||
Client: k8sClient,
|
||||
HCLCode: tt.fields.HCLCode,
|
||||
SecretSuffix: tt.fields.SecretSuffix,
|
||||
SecretNS: tt.fields.SecretNS,
|
||||
}
|
||||
if got := k.HCL(); got != tt.want {
|
||||
t.Errorf("HCL() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestK8SBackend_GetTFStateJSON(t *testing.T) {
|
||||
const UID = "xxxx-xxxx"
|
||||
type fields struct {
|
||||
Client client.Client
|
||||
HCLCode string
|
||||
SecretSuffix string
|
||||
SecretNS string
|
||||
LegacySecretSuffix string
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
}
|
||||
tfStateData, _ := base64.StdEncoding.DecodeString("H4sIAAAAAAAA/4SQzarbMBCF934KoXUdPKNf+1VKCWNp5AocO8hyaSl592KlcBd3cZfnHPHpY/52QshfXI68b3IS+tuVK5dCaS+P+8ci4TbcULb94JJplZPAFte8MS18PQrKBO8Q+xk59SHa1AMA9M4YmoN3FGJ8M/azPs96yElcCkLIsG+V8sblnqOc3uXlRuvZ0GxSSuiCRUYbw2gGHRFGPxitEgJYQDQ0a68I2ChNo1cAZJ2bR20UtW8bsv55NuJRS94W2erXe5X5QQs3A/FZ4fhJaOwUgZTVMRjto1HGpSGSQuuD955hdDDPcR6NY1ZpQJ/YwagTRAvBpsi8LXn7Pa1U+ahfWHX/zWThYz9L4Otg3390r+5fAAAA//8hmcuNuQEAAA==")
|
||||
baseSecret := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tfstate-default-a",
|
||||
Namespace: "default",
|
||||
},
|
||||
Type: v1.SecretTypeOpaque,
|
||||
Data: map[string][]byte{
|
||||
TerraformStateNameInSecret: tfStateData,
|
||||
},
|
||||
}
|
||||
|
||||
k8sClient := fake.NewClientBuilder().WithObjects(baseSecret).Build()
|
||||
k8sClient2 := fake.NewClientBuilder().Build()
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
fields: fields{
|
||||
Client: k8sClient,
|
||||
HCLCode: "",
|
||||
SecretSuffix: "a",
|
||||
SecretNS: "default",
|
||||
},
|
||||
args: args{ctx: context.Background()},
|
||||
want: tfStateJson,
|
||||
},
|
||||
{
|
||||
name: "secret doesn't exist",
|
||||
fields: fields{
|
||||
Client: k8sClient2,
|
||||
HCLCode: "",
|
||||
SecretSuffix: "a",
|
||||
SecretNS: "default",
|
||||
},
|
||||
args: args{ctx: context.Background()},
|
||||
want: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "got a legacy secret",
|
||||
fields: fields{
|
||||
Client: k8sClient,
|
||||
HCLCode: "",
|
||||
LegacySecretSuffix: "a",
|
||||
SecretNS: "default",
|
||||
SecretSuffix: UID,
|
||||
},
|
||||
want: tfStateJson,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
k := &K8SBackend{
|
||||
Client: tt.fields.Client,
|
||||
HCLCode: tt.fields.HCLCode,
|
||||
SecretSuffix: tt.fields.SecretSuffix,
|
||||
SecretNS: tt.fields.SecretNS,
|
||||
LegacySecretSuffix: tt.fields.LegacySecretSuffix,
|
||||
}
|
||||
got, err := k.GetTFStateJSON(tt.args.ctx)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetTFStateJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GetTFStateJSON() got = %v, want %v", got, tt.want)
|
||||
t.Errorf("GetTFStateJSON() got = %s, want %s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestK8SBackend_CleanUp(t *testing.T) {
|
||||
type fields struct {
|
||||
Client client.Client
|
||||
HCLCode string
|
||||
SecretSuffix string
|
||||
SecretNS string
|
||||
}
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
}
|
||||
secret := v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tfstate-default-a",
|
||||
Namespace: "default",
|
||||
},
|
||||
Type: v1.SecretTypeOpaque,
|
||||
}
|
||||
k8sClient := fake.NewClientBuilder().WithObjects(&secret).Build()
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
fields: fields{
|
||||
Client: k8sClient,
|
||||
SecretSuffix: "a",
|
||||
SecretNS: "default",
|
||||
},
|
||||
args: args{ctx: context.Background()},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
k := &K8SBackend{
|
||||
Client: tt.fields.Client,
|
||||
HCLCode: tt.fields.HCLCode,
|
||||
SecretSuffix: tt.fields.SecretSuffix,
|
||||
SecretNS: tt.fields.SecretNS,
|
||||
}
|
||||
if err := k.CleanUp(tt.args.ctx); (err != nil) != tt.wantErr {
|
||||
t.Errorf("CleanUp() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMigrateLegacySecret(t *testing.T) {
|
||||
secretNS := "default"
|
||||
secret := v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tfstate-default-a",
|
||||
Namespace: secretNS,
|
||||
},
|
||||
Type: v1.SecretTypeOpaque,
|
||||
}
|
||||
k8sClient := fake.NewClientBuilder().WithObjects(&secret).Build()
|
||||
fakeUID := "xxxx-xxxx"
|
||||
k := &K8SBackend{
|
||||
Client: k8sClient,
|
||||
HCLCode: "",
|
||||
SecretSuffix: fakeUID,
|
||||
SecretNS: secretNS,
|
||||
LegacySecretSuffix: "a",
|
||||
}
|
||||
err := k.migrateLegacySecret()
|
||||
assert.NilError(t, err)
|
||||
NoLegacySecK8sClient := fake.NewClientBuilder().Build()
|
||||
k = &K8SBackend{
|
||||
Client: NoLegacySecK8sClient,
|
||||
HCLCode: "",
|
||||
SecretSuffix: fakeUID,
|
||||
SecretNS: secretNS,
|
||||
LegacySecretSuffix: "a",
|
||||
}
|
||||
err = k.migrateLegacySecret()
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
var tfStateJson = []byte(`{
|
||||
"version": 4,
|
||||
"terraform_version": "1.0.2",
|
||||
"serial": 2,
|
||||
"lineage": "c35c8722-b2ef-cd6f-1111-755abc87acdd",
|
||||
"outputs": {
|
||||
"container_id":{
|
||||
"value": "e5fff27c62e26dc9504d21980543f21161225ab483a1e534a98311a677b9453a",
|
||||
"type": "string"
|
||||
},
|
||||
"image_id": {
|
||||
"value": "sha256:d1a364dc548d5357f0da3268c888e1971bbdb957ee3f028fe7194f1d61c6fdeenginx:latest",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"resources": []
|
||||
}
|
||||
`)
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
awscredentials "github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3iface"
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta2"
|
||||
"github.com/oam-dev/terraform-controller/controllers/provider"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
// S3Backend is used to interact with the Terraform s3 backend
|
||||
type S3Backend struct {
|
||||
client s3iface.S3API
|
||||
Region string
|
||||
Key string
|
||||
Bucket string
|
||||
}
|
||||
|
||||
func newS3Backend(_ client.Client, backendConf interface{}, credentials map[string]string) (Backend, error) {
|
||||
conf, ok := backendConf.(*v1beta2.S3BackendConf)
|
||||
if !ok || conf == nil {
|
||||
return nil, fmt.Errorf("invalid backendConf, want *v1beta2.S3BackendConf, but got %#v", backendConf)
|
||||
}
|
||||
|
||||
var region string
|
||||
if conf.Region != nil && *conf.Region != "" {
|
||||
region = *conf.Region
|
||||
} else {
|
||||
region = credentials[provider.EnvAWSDefaultRegion]
|
||||
}
|
||||
if region == "" {
|
||||
return nil, errors.New("fail to get region when build s3 backend")
|
||||
}
|
||||
|
||||
s3Backend := &S3Backend{
|
||||
Region: region,
|
||||
Key: conf.Key,
|
||||
Bucket: conf.Bucket,
|
||||
}
|
||||
|
||||
accessKey := credentials[provider.EnvAWSAccessKeyID]
|
||||
secretKey := credentials[provider.EnvAWSSecretAccessKey]
|
||||
sessionToken := credentials[provider.EnvAWSSessionToken]
|
||||
if accessKey == "" || secretKey == "" {
|
||||
return nil, errors.New("fail to get credentials when build s3 backend")
|
||||
}
|
||||
|
||||
// build s3 client
|
||||
sessionOpts := session.Options{
|
||||
Config: aws.Config{
|
||||
Credentials: awscredentials.NewStaticCredentials(accessKey, secretKey, sessionToken),
|
||||
Region: aws.String(s3Backend.Region),
|
||||
},
|
||||
}
|
||||
sess, err := session.NewSessionWithOptions(sessionOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fial to build s3 backend: %w", err)
|
||||
}
|
||||
s3Backend.client = s3.New(sess)
|
||||
|
||||
// check if the bucket exists
|
||||
if err := s3Backend.checkBucketExists(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s3Backend, nil
|
||||
}
|
||||
|
||||
func (s *S3Backend) checkBucketExists() error {
|
||||
bucketListOutput, err := s.client.ListBuckets(&s3.ListBucketsInput{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("fail to list bucket when check if the bucket(%s) exists: %w", s.Bucket, err)
|
||||
}
|
||||
for _, bucket := range bucketListOutput.Buckets {
|
||||
if bucket.Name != nil && *bucket.Name == s.Bucket {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("fail to get bucket (%s), please make sure the bucket exists and the provider credentials have access to the bucket", s.Bucket)
|
||||
}
|
||||
|
||||
func (s *S3Backend) getObject() (*s3.GetObjectOutput, error) {
|
||||
input := &s3.GetObjectInput{
|
||||
Key: &s.Key,
|
||||
Bucket: &s.Bucket,
|
||||
}
|
||||
return s.client.GetObject(input)
|
||||
}
|
||||
|
||||
// GetTFStateJSON gets Terraform state json from the Terraform s3 backend
|
||||
func (s *S3Backend) GetTFStateJSON(_ context.Context) ([]byte, error) {
|
||||
output, err := s.getObject()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = output.Body.Close() }()
|
||||
writer := bytes.NewBuffer(nil)
|
||||
_, err = io.Copy(writer, output.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return writer.Bytes(), nil
|
||||
}
|
||||
|
||||
// CleanUp will delete the s3 object which contains the Terraform state
|
||||
func (s *S3Backend) CleanUp(_ context.Context) error {
|
||||
_, err := s.getObject()
|
||||
if err != nil {
|
||||
// nolint:errorlint
|
||||
if err, ok := err.(awserr.Error); ok && err.Code() == s3.ErrCodeNoSuchKey || err.Code() == s3.ErrCodeNoSuchBucket {
|
||||
// the object is not found, no need to delete
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
input := &s3.DeleteObjectInput{
|
||||
Bucket: &s.Bucket,
|
||||
Key: &s.Key,
|
||||
}
|
||||
_, err = s.client.DeleteObject(input)
|
||||
return err
|
||||
}
|
||||
|
||||
// HCL returns the backend hcl code string
|
||||
func (s S3Backend) HCL() string {
|
||||
fmtStr := `
|
||||
terraform {
|
||||
backend s3 {
|
||||
bucket = "%s"
|
||||
key = "%s"
|
||||
region = "%s"
|
||||
}
|
||||
}
|
||||
`
|
||||
return fmt.Sprintf(fmtStr, s.Bucket, s.Key, s.Region)
|
||||
}
|
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
Copyright 2022 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/aws/aws-sdk-go/service/s3/s3iface"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestS3Backend_HCL(t *testing.T) {
|
||||
type fields struct {
|
||||
Region string
|
||||
Key string
|
||||
Bucket string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "normal",
|
||||
fields: fields{
|
||||
Region: "a",
|
||||
Key: "b",
|
||||
Bucket: "c",
|
||||
},
|
||||
want: `
|
||||
terraform {
|
||||
backend s3 {
|
||||
bucket = "c"
|
||||
key = "b"
|
||||
region = "a"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := S3Backend{
|
||||
Region: tt.fields.Region,
|
||||
Key: tt.fields.Key,
|
||||
Bucket: tt.fields.Bucket,
|
||||
}
|
||||
if got := s.HCL(); got != tt.want {
|
||||
t.Errorf("HCL() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockNoSuchBucketError struct {
|
||||
}
|
||||
|
||||
func (err mockNoSuchBucketError) Error() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (err mockNoSuchBucketError) Message() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (err mockNoSuchBucketError) OrigErr() error {
|
||||
return errors.New(s3.ErrCodeNoSuchBucket)
|
||||
}
|
||||
|
||||
func (err mockNoSuchBucketError) Code() string {
|
||||
return s3.ErrCodeNoSuchBucket
|
||||
}
|
||||
|
||||
type mockNoSuchKeyError struct {
|
||||
}
|
||||
|
||||
func (err mockNoSuchKeyError) Error() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (err mockNoSuchKeyError) Message() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (err mockNoSuchKeyError) OrigErr() error {
|
||||
return errors.New(s3.ErrCodeNoSuchKey)
|
||||
}
|
||||
|
||||
func (err mockNoSuchKeyError) Code() string {
|
||||
return s3.ErrCodeNoSuchKey
|
||||
}
|
||||
|
||||
type mockS3Client struct {
|
||||
s3iface.S3API
|
||||
}
|
||||
|
||||
func (s *mockS3Client) GetObject(input *s3.GetObjectInput) (*s3.GetObjectOutput, error) {
|
||||
var resp string
|
||||
switch {
|
||||
case *(input.Bucket) == "a" && *(input.Key) == "a":
|
||||
resp = "test_a"
|
||||
|
||||
case *(input.Bucket) == "a" && *(input.Key) == "c":
|
||||
return nil, mockNoSuchKeyError{}
|
||||
|
||||
case *(input.Bucket) == "b":
|
||||
return nil, mockNoSuchBucketError{}
|
||||
}
|
||||
|
||||
if resp != "" {
|
||||
body := ioutil.NopCloser(bytes.NewBuffer([]byte(resp)))
|
||||
return &s3.GetObjectOutput{Body: body}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *mockS3Client) DeleteObject(input *s3.DeleteObjectInput) (*s3.DeleteObjectOutput, error) {
|
||||
switch {
|
||||
case *(input.Bucket) == "a" && *(input.Key) == "a":
|
||||
return &s3.DeleteObjectOutput{}, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestS3Backend_GetTFStateJSON(t *testing.T) {
|
||||
type fields struct {
|
||||
Key string
|
||||
Bucket string
|
||||
}
|
||||
type args struct {
|
||||
in0 context.Context
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "bucket exists, key exists",
|
||||
fields: fields{
|
||||
Key: "a",
|
||||
Bucket: "a",
|
||||
},
|
||||
want: []byte("test_a"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &S3Backend{
|
||||
client: &mockS3Client{},
|
||||
Key: tt.fields.Key,
|
||||
Bucket: tt.fields.Bucket,
|
||||
}
|
||||
got, err := s.GetTFStateJSON(tt.args.in0)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GetTFStateJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GetTFStateJSON() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestS3Backend_CleanUp(t *testing.T) {
|
||||
type fields struct {
|
||||
Key string
|
||||
Bucket string
|
||||
}
|
||||
type args struct {
|
||||
in0 context.Context
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "no such bucket",
|
||||
fields: fields{
|
||||
Key: "a",
|
||||
Bucket: "b",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no such key",
|
||||
fields: fields{
|
||||
Key: "c",
|
||||
Bucket: "a",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bucket exists, key exists",
|
||||
fields: fields{
|
||||
Key: "a",
|
||||
Bucket: "a",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &S3Backend{
|
||||
client: &mockS3Client{},
|
||||
Key: tt.fields.Key,
|
||||
Bucket: tt.fields.Bucket,
|
||||
}
|
||||
if err := s.CleanUp(tt.args.in0); (err != nil) != tt.wantErr {
|
||||
t.Errorf("CleanUp() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
package configuration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
apitypes "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/types"
|
||||
crossplane "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime"
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta1"
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta2"
|
||||
"github.com/oam-dev/terraform-controller/controllers/features"
|
||||
"github.com/oam-dev/terraform-controller/controllers/provider"
|
||||
)
|
||||
|
||||
const (
|
||||
// GithubPrefix is the constant of GitHub domain
|
||||
GithubPrefix = "https://github.com/"
|
||||
// GithubKubeVelaContribPrefix is the prefix of GitHub repository of kubevela-contrib
|
||||
GithubKubeVelaContribPrefix = "https://github.com/kubevela-contrib"
|
||||
// GiteeTerraformSourceOrg is the Gitee organization of Terraform source
|
||||
GiteeTerraformSourceOrg = "https://gitee.com/kubevela-terraform-source"
|
||||
// GiteePrefix is the constant of Gitee domain
|
||||
GiteePrefix = "https://gitee.com/"
|
||||
)
|
||||
|
||||
const errGitHubBlockedNotBoolean = "the value of githubBlocked is not a boolean"
|
||||
|
||||
// ValidConfigurationObject will validate a Configuration
|
||||
func ValidConfigurationObject(configuration *v1beta2.Configuration) (types.ConfigurationType, error) {
|
||||
hcl := configuration.Spec.HCL
|
||||
remote := configuration.Spec.Remote
|
||||
switch {
|
||||
case hcl == "" && remote == "":
|
||||
return "", errors.New("spec.HCL or spec.Remote should be set")
|
||||
case hcl != "" && remote != "":
|
||||
return "", errors.New("spec.HCL and spec.Remote cloud not be set at the same time")
|
||||
case hcl != "":
|
||||
return types.ConfigurationHCL, nil
|
||||
case remote != "":
|
||||
return types.ConfigurationRemote, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// SetRegion will set the region for Configuration
|
||||
func SetRegion(ctx context.Context, k8sClient client.Client, namespace, name string, providerObj *v1beta1.Provider) (string, error) {
|
||||
configuration, err := Get(ctx, k8sClient, apitypes.NamespacedName{Namespace: namespace, Name: name})
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to get configuration")
|
||||
}
|
||||
if configuration.Spec.Region != "" {
|
||||
return configuration.Spec.Region, nil
|
||||
}
|
||||
|
||||
configuration.Spec.Region = providerObj.Spec.Region
|
||||
return providerObj.Spec.Region, Update(ctx, k8sClient, &configuration)
|
||||
}
|
||||
|
||||
// Update will update the Configuration
|
||||
func Update(ctx context.Context, k8sClient client.Client, configuration *v1beta2.Configuration) error {
|
||||
return k8sClient.Update(ctx, configuration)
|
||||
}
|
||||
|
||||
// Get will get the Configuration
|
||||
func Get(ctx context.Context, k8sClient client.Client, namespacedName apitypes.NamespacedName) (v1beta2.Configuration, error) {
|
||||
configuration := &v1beta2.Configuration{}
|
||||
if err := k8sClient.Get(ctx, namespacedName, configuration); err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
klog.ErrorS(err, "unable to fetch Configuration", "NamespacedName", namespacedName)
|
||||
}
|
||||
return *configuration, err
|
||||
}
|
||||
return *configuration, nil
|
||||
}
|
||||
|
||||
// IsDeletable will check whether the Configuration can be deleted immediately
|
||||
// If deletable, it means
|
||||
// - feature gate AllowDeleteProvisioningResource is enabled
|
||||
// - no external cloud resources are provisioned
|
||||
// - it's in force-delete state
|
||||
func IsDeletable(ctx context.Context, k8sClient client.Client, configuration *v1beta2.Configuration) (bool, error) {
|
||||
if feature.DefaultFeatureGate.Enabled(features.AllowDeleteProvisioningResource) {
|
||||
return true, nil
|
||||
}
|
||||
if configuration.Spec.ForceDelete != nil && *configuration.Spec.ForceDelete {
|
||||
return true, nil
|
||||
}
|
||||
if !configuration.Spec.InlineCredentials {
|
||||
providerRef := GetProviderNamespacedName(*configuration)
|
||||
providerObj, err := provider.GetProviderFromConfiguration(ctx, k8sClient, providerRef.Namespace, providerRef.Name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// allow Configuration to delete when the Provider doesn't exist or is not ready, which means external cloud resources are
|
||||
// not provisioned at all
|
||||
if providerObj == nil || providerObj.Status.State == types.ProviderIsNotReady || configuration.Status.Apply.State == types.TerraformInitError {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
if configuration.Status.Apply.State == types.ConfigurationProvisioningAndChecking {
|
||||
warning := fmt.Sprintf("Destroy could not complete and needs to wait for Provision to complete first: %s", types.MessageCloudResourceProvisioningAndChecking)
|
||||
klog.Warning(warning)
|
||||
return false, errors.New(warning)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// ReplaceTerraformSource will replace the Terraform source from GitHub to Gitee
|
||||
func ReplaceTerraformSource(remote string, githubBlockedStr string) string {
|
||||
klog.InfoS("Whether GitHub is blocked", "githubBlocked", githubBlockedStr)
|
||||
githubBlocked, err := strconv.ParseBool(githubBlockedStr)
|
||||
if err != nil {
|
||||
klog.Warningf("%s: %v", errGitHubBlockedNotBoolean, err)
|
||||
return remote
|
||||
}
|
||||
klog.InfoS("Parsed GITHUB_BLOCKED env", "githubBlocked", githubBlocked)
|
||||
|
||||
if !githubBlocked {
|
||||
return remote
|
||||
}
|
||||
|
||||
if remote == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(remote, GithubPrefix) {
|
||||
var repo string
|
||||
if strings.HasPrefix(remote, GithubKubeVelaContribPrefix) {
|
||||
repo = strings.Replace(remote, GithubPrefix, GiteePrefix, 1)
|
||||
} else {
|
||||
tmp := strings.Split(strings.Replace(remote, GithubPrefix, "", 1), "/")
|
||||
if len(tmp) == 2 {
|
||||
repo = GiteeTerraformSourceOrg + "/" + tmp[1]
|
||||
}
|
||||
}
|
||||
klog.InfoS("New remote git", "Gitee", repo)
|
||||
return repo
|
||||
}
|
||||
return remote
|
||||
}
|
||||
|
||||
// GetProviderNamespacedName will get the provider namespaced name
|
||||
func GetProviderNamespacedName(configuration v1beta2.Configuration) *crossplane.Reference {
|
||||
if configuration.Spec.ProviderReference != nil {
|
||||
return configuration.Spec.ProviderReference
|
||||
}
|
||||
return &crossplane.Reference{
|
||||
Name: provider.DefaultName,
|
||||
Namespace: provider.DefaultNamespace,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,397 @@
|
|||
package configuration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/types"
|
||||
crossplane "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime"
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta1"
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta2"
|
||||
)
|
||||
|
||||
func TestValidConfigurationObject(t *testing.T) {
|
||||
type args struct {
|
||||
configuration *v1beta2.Configuration
|
||||
}
|
||||
type want struct {
|
||||
configurationType types.ConfigurationType
|
||||
errMsg string
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "hcl",
|
||||
args: args{
|
||||
configuration: &v1beta2.Configuration{
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
HCL: "abc",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
configurationType: types.ConfigurationHCL,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote",
|
||||
args: args{
|
||||
configuration: &v1beta2.Configuration{
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
Remote: "def",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
configurationType: types.ConfigurationRemote,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote and hcl are set",
|
||||
args: args{
|
||||
configuration: &v1beta2.Configuration{
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
HCL: "abc",
|
||||
Remote: "def",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
configurationType: "",
|
||||
errMsg: "spec.HCL and spec.Remote cloud not be set at the same time",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote and hcl are not set",
|
||||
args: args{
|
||||
configuration: &v1beta2.Configuration{
|
||||
Spec: v1beta2.ConfigurationSpec{},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
configurationType: "",
|
||||
errMsg: "spec.HCL or spec.Remote should be set",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := ValidConfigurationObject(tc.args.configuration)
|
||||
if tc.want.errMsg != "" && !strings.Contains(err.Error(), tc.want.errMsg) {
|
||||
t.Errorf("ValidConfigurationObject() error = %v, wantErr %v", err, tc.want.errMsg)
|
||||
return
|
||||
}
|
||||
if got != tc.want.configurationType {
|
||||
t.Errorf("ValidConfigurationObject() = %v, want %v", got, tc.want.configurationType)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestReplaceTerraformSource(t *testing.T) {
|
||||
testcases := []struct {
|
||||
remote string
|
||||
githubBlocked string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
remote: "",
|
||||
expected: "",
|
||||
githubBlocked: "xxx",
|
||||
},
|
||||
{
|
||||
remote: "https://github.com/kubevela-contrib/terraform-modules.git",
|
||||
expected: "https://github.com/kubevela-contrib/terraform-modules.git",
|
||||
githubBlocked: "false",
|
||||
},
|
||||
{
|
||||
remote: "https://github.com/kubevela-contrib/terraform-modules.git",
|
||||
expected: "https://gitee.com/kubevela-contrib/terraform-modules.git",
|
||||
githubBlocked: "true",
|
||||
},
|
||||
{
|
||||
remote: "https://github.com/abc/terraform-modules.git",
|
||||
expected: "https://gitee.com/kubevela-terraform-source/terraform-modules.git",
|
||||
githubBlocked: "true",
|
||||
},
|
||||
{
|
||||
remote: "abc",
|
||||
githubBlocked: "true",
|
||||
expected: "abc",
|
||||
},
|
||||
{
|
||||
remote: "",
|
||||
githubBlocked: "true",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.remote, func(t *testing.T) {
|
||||
actual := ReplaceTerraformSource(tc.remote, tc.githubBlocked)
|
||||
if actual != tc.expected {
|
||||
t.Errorf("expected %s, got %s", tc.expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDeletable(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := runtime.NewScheme()
|
||||
_ = v1beta1.AddToScheme(s)
|
||||
_ = v1beta2.AddToScheme(s)
|
||||
provider2 := &v1beta1.Provider{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1beta1.ProviderStatus{
|
||||
State: types.ProviderIsNotReady,
|
||||
},
|
||||
}
|
||||
provider3 := &v1beta1.Provider{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1beta1.ProviderStatus{
|
||||
State: types.ProviderIsReady,
|
||||
},
|
||||
}
|
||||
k8sClient1 := fake.NewClientBuilder().WithScheme(s).Build()
|
||||
k8sClient2 := fake.NewClientBuilder().WithScheme(s).WithObjects(provider2).Build()
|
||||
k8sClient3 := fake.NewClientBuilder().WithScheme(s).WithObjects(provider3).Build()
|
||||
k8sClient4 := fake.NewClientBuilder().Build()
|
||||
|
||||
configuration := &v1beta2.Configuration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "abc",
|
||||
},
|
||||
}
|
||||
configuration.Spec.ProviderReference = &crossplane.Reference{
|
||||
Name: "default",
|
||||
Namespace: "default",
|
||||
}
|
||||
configuration.Spec.InlineCredentials = false
|
||||
|
||||
defaultConfiguration := &v1beta2.Configuration{}
|
||||
defaultConfiguration.Spec.InlineCredentials = false
|
||||
|
||||
provisioningConfiguration := &v1beta2.Configuration{
|
||||
Status: v1beta2.ConfigurationStatus{
|
||||
Apply: v1beta2.ConfigurationApplyStatus{
|
||||
State: types.ConfigurationProvisioningAndChecking,
|
||||
},
|
||||
},
|
||||
}
|
||||
provisioningConfiguration.Spec.InlineCredentials = false
|
||||
|
||||
readyConfiguration := &v1beta2.Configuration{
|
||||
Status: v1beta2.ConfigurationStatus{
|
||||
Apply: v1beta2.ConfigurationApplyStatus{
|
||||
State: types.Available,
|
||||
},
|
||||
},
|
||||
}
|
||||
readyConfiguration.Spec.InlineCredentials = false
|
||||
|
||||
inlineConfiguration := &v1beta2.Configuration{}
|
||||
inlineConfiguration.Spec.InlineCredentials = true
|
||||
|
||||
type args struct {
|
||||
configuration *v1beta2.Configuration
|
||||
k8sClient client.Client
|
||||
}
|
||||
type want struct {
|
||||
deletable bool
|
||||
errMsg string
|
||||
}
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "provider is not found",
|
||||
args: args{
|
||||
k8sClient: k8sClient1,
|
||||
configuration: defaultConfiguration,
|
||||
},
|
||||
want: want{
|
||||
deletable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "provider is not ready, use default providerRef",
|
||||
args: args{
|
||||
k8sClient: k8sClient2,
|
||||
configuration: defaultConfiguration,
|
||||
},
|
||||
want: want{
|
||||
deletable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "provider is not ready, providerRef is set in configuration spec",
|
||||
args: args{
|
||||
k8sClient: k8sClient2,
|
||||
configuration: configuration,
|
||||
},
|
||||
want: want{
|
||||
deletable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "configuration is provisioning",
|
||||
args: args{
|
||||
k8sClient: k8sClient3,
|
||||
configuration: provisioningConfiguration,
|
||||
},
|
||||
want: want{
|
||||
errMsg: "Destroy could not complete and needs to wait for Provision to complete first",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "configuration is ready",
|
||||
args: args{
|
||||
k8sClient: k8sClient3,
|
||||
configuration: readyConfiguration,
|
||||
},
|
||||
want: want{},
|
||||
},
|
||||
{
|
||||
name: "failed to get provider",
|
||||
args: args{
|
||||
k8sClient: k8sClient4,
|
||||
configuration: defaultConfiguration,
|
||||
},
|
||||
want: want{
|
||||
errMsg: "failed to get Provider object",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no provider is needed",
|
||||
args: args{
|
||||
k8sClient: k8sClient4,
|
||||
configuration: inlineConfiguration,
|
||||
},
|
||||
want: want{
|
||||
deletable: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := IsDeletable(ctx, tc.args.k8sClient, tc.args.configuration)
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), tc.want.errMsg) {
|
||||
t.Errorf("IsDeletable() error = %v, wantErr %v", err, tc.want.errMsg)
|
||||
return
|
||||
}
|
||||
}
|
||||
if got != tc.want.deletable {
|
||||
t.Errorf("IsDeletable() = %v, want %v", got, tc.want.deletable)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetRegion(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := runtime.NewScheme()
|
||||
_ = v1beta2.AddToScheme(s)
|
||||
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
|
||||
configuration1 := v1beta2.Configuration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "abc",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1beta2.ConfigurationSpec{},
|
||||
}
|
||||
configuration1.Spec.Region = "xxx"
|
||||
assert.Nil(t, k8sClient.Create(ctx, &configuration1))
|
||||
|
||||
configuration2 := v1beta2.Configuration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "def",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1beta2.ConfigurationSpec{},
|
||||
}
|
||||
assert.Nil(t, k8sClient.Create(ctx, &configuration2))
|
||||
|
||||
provider := &v1beta1.Provider{
|
||||
Spec: v1beta1.ProviderSpec{
|
||||
Region: "yyy",
|
||||
},
|
||||
}
|
||||
|
||||
type args struct {
|
||||
namespace string
|
||||
name string
|
||||
}
|
||||
|
||||
type want struct {
|
||||
region string
|
||||
errMsg string
|
||||
}
|
||||
|
||||
testcases := map[string]struct {
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
"configuration is available, region is set": {
|
||||
args: args{
|
||||
namespace: "default",
|
||||
name: "abc",
|
||||
},
|
||||
want: want{
|
||||
region: "xxx",
|
||||
},
|
||||
},
|
||||
"configuration is available, region is not set": {
|
||||
args: args{
|
||||
namespace: "default",
|
||||
name: "def",
|
||||
},
|
||||
want: want{
|
||||
region: "yyy",
|
||||
},
|
||||
},
|
||||
"configuration isn't available": {
|
||||
args: args{
|
||||
namespace: "default",
|
||||
name: "ghi",
|
||||
},
|
||||
want: want{
|
||||
errMsg: "failed to get configuration",
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range testcases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
region, err := SetRegion(ctx, k8sClient, tc.args.namespace, tc.args.name, provider)
|
||||
if tc.want.errMsg != "" && !strings.Contains(err.Error(), tc.want.errMsg) {
|
||||
t.Errorf("SetRegion() error = %v, wantErr %v", err, tc.want.errMsg)
|
||||
}
|
||||
if region != tc.want.region {
|
||||
t.Errorf("SetRegion() want = %s, got %s", tc.want.region, region)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -14,10 +14,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package util
|
||||
package configuration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
@ -37,5 +39,27 @@ func RawExtension2Map(raw *runtime.RawExtension) (map[string]interface{}, error)
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, err
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Interface2String converts an interface{} type to string
|
||||
func Interface2String(v interface{}) (string, error) {
|
||||
var value string
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
value = v
|
||||
case int:
|
||||
value = strconv.Itoa(v)
|
||||
case float64:
|
||||
value = fmt.Sprint(v)
|
||||
case bool:
|
||||
value = strconv.FormatBool(v)
|
||||
default:
|
||||
valueJSON, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cloud not convert %v to string", v)
|
||||
}
|
||||
value = string(valueJSON)
|
||||
}
|
||||
return value, nil
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
package configuration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
func TestRawExtension2Map(t *testing.T) {
|
||||
type want struct {
|
||||
result interface{}
|
||||
err error
|
||||
}
|
||||
|
||||
type Spec struct {
|
||||
Variable *runtime.RawExtension `json:"variable,omitempty"`
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
variable string
|
||||
want want
|
||||
}{
|
||||
"StringType": {
|
||||
variable: `
|
||||
Variable:
|
||||
k: Will
|
||||
`,
|
||||
want: want{
|
||||
result: "Will",
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"ListType1": {
|
||||
variable: `
|
||||
Variable:
|
||||
k: ["Will", "Catherine"]
|
||||
`,
|
||||
want: want{
|
||||
result: []interface{}{"Will", "Catherine"},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"ListType2": {
|
||||
variable: `
|
||||
Variable:
|
||||
k:
|
||||
- "Will"
|
||||
- "Catherine"
|
||||
`,
|
||||
want: want{
|
||||
result: []interface{}{"Will", "Catherine"},
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"nil": {
|
||||
want: want{
|
||||
result: nil,
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
var spec Spec
|
||||
err := yaml.Unmarshal([]byte(tc.variable), &spec)
|
||||
assert.NilError(t, err)
|
||||
result, err := RawExtension2Map(spec.Variable)
|
||||
if tc.want.err != nil {
|
||||
assert.Error(t, err, tc.want.err.Error())
|
||||
} else {
|
||||
assert.Equal(t, tc.want.err, err)
|
||||
assert.DeepEqual(t, tc.want.result, result["k"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawExtension2Map2(t *testing.T) {
|
||||
type args struct {
|
||||
raw *runtime.RawExtension
|
||||
}
|
||||
type want struct {
|
||||
errMessage string
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
"bad raw": {
|
||||
args: args{
|
||||
raw: &runtime.RawExtension{
|
||||
Raw: []byte("xxx"),
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
errMessage: "cannot convert RawExtension with unrecognized content type to unstructured",
|
||||
},
|
||||
}}
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, err := RawExtension2Map(tc.args.raw)
|
||||
if tc.want.errMessage != "" {
|
||||
assert.Error(t, err, tc.want.errMessage)
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterface2String(t *testing.T) {
|
||||
type want struct {
|
||||
result string
|
||||
err error
|
||||
}
|
||||
cases := map[string]struct {
|
||||
variable interface{}
|
||||
want want
|
||||
}{
|
||||
"StringType": {
|
||||
variable: "Will",
|
||||
want: want{
|
||||
result: "Will",
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"IntType": {
|
||||
variable: 123,
|
||||
want: want{
|
||||
result: "123",
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"BoolType": {
|
||||
variable: true,
|
||||
want: want{
|
||||
result: "true",
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"NumberType": {
|
||||
variable: 1024.1,
|
||||
want: want{
|
||||
result: "1024.1",
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"ListType1": {
|
||||
variable: []interface{}{"Will", "Catherine"},
|
||||
want: want{
|
||||
result: `["Will","Catherine"]`,
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"ListType2": {
|
||||
variable: []interface{}{123, 456},
|
||||
want: want{
|
||||
result: `[123,456]`,
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"ObjectType": {
|
||||
variable: struct{ Name string }{"Terraform"},
|
||||
want: want{
|
||||
result: `{"Name":"Terraform"}`,
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
"ListObjectType": {
|
||||
variable: []struct{ Name string }{{"Terraform"}, {"OAM"}, {"Vela"}},
|
||||
want: want{
|
||||
result: `[{"Name":"Terraform"},{"Name":"OAM"},{"Name":"Vela"}]`,
|
||||
err: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
result, err := Interface2String(tc.variable)
|
||||
assert.Equal(t, tc.want.err, err)
|
||||
assert.DeepEqual(t, tc.want.result, result)
|
||||
})
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
Copyright 2023 The KubeVela Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package features
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/component-base/featuregate"
|
||||
)
|
||||
|
||||
const (
|
||||
AllowDeleteProvisioningResource featuregate.Feature = "AllowDeleteProvisioningResource"
|
||||
)
|
||||
|
||||
var defaultFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
|
||||
AllowDeleteProvisioningResource: {Default: false, PreRelease: featuregate.Alpha},
|
||||
}
|
||||
|
||||
func init() {
|
||||
runtime.Must(feature.DefaultMutableFeatureGate.Add(defaultFeatureGates))
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/types"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
)
|
||||
|
||||
func (a *Assembler) ApplyContainer(executionType types.TerraformExecutionType, resourceQuota types.ResourceQuota) v1.Container {
|
||||
|
||||
c := v1.Container{
|
||||
Name: types.TerraformContainerName,
|
||||
Image: a.TerraformImage,
|
||||
ImagePullPolicy: v1.PullIfNotPresent,
|
||||
Command: []string{
|
||||
"bash",
|
||||
"-c",
|
||||
fmt.Sprintf("terraform %s -lock=false -auto-approve", executionType),
|
||||
},
|
||||
VolumeMounts: []v1.VolumeMount{
|
||||
{
|
||||
Name: a.Name,
|
||||
MountPath: types.WorkingVolumeMountPath,
|
||||
},
|
||||
{
|
||||
Name: types.InputTFConfigurationVolumeName,
|
||||
MountPath: types.InputTFConfigurationVolumeMountPath,
|
||||
},
|
||||
},
|
||||
Env: a.Envs,
|
||||
}
|
||||
|
||||
if resourceQuota.ResourcesLimitsCPU != "" || resourceQuota.ResourcesLimitsMemory != "" ||
|
||||
resourceQuota.ResourcesRequestsCPU != "" || resourceQuota.ResourcesRequestsMemory != "" {
|
||||
resourceRequirements := v1.ResourceRequirements{}
|
||||
if resourceQuota.ResourcesLimitsCPU != "" || resourceQuota.ResourcesLimitsMemory != "" {
|
||||
resourceRequirements.Limits = map[v1.ResourceName]resource.Quantity{}
|
||||
if resourceQuota.ResourcesLimitsCPU != "" {
|
||||
resourceRequirements.Limits["cpu"] = resourceQuota.ResourcesLimitsCPUQuantity
|
||||
}
|
||||
if resourceQuota.ResourcesLimitsMemory != "" {
|
||||
resourceRequirements.Limits["memory"] = resourceQuota.ResourcesLimitsMemoryQuantity
|
||||
}
|
||||
}
|
||||
if resourceQuota.ResourcesRequestsCPU != "" || resourceQuota.ResourcesLimitsMemory != "" {
|
||||
resourceRequirements.Requests = map[v1.ResourceName]resource.Quantity{}
|
||||
if resourceQuota.ResourcesRequestsCPU != "" {
|
||||
resourceRequirements.Requests["cpu"] = resourceQuota.ResourcesRequestsCPUQuantity
|
||||
}
|
||||
if resourceQuota.ResourcesRequestsMemory != "" {
|
||||
resourceRequirements.Requests["memory"] = resourceQuota.ResourcesRequestsMemoryQuantity
|
||||
}
|
||||
}
|
||||
c.Resources = resourceRequirements
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"github.com/oam-dev/terraform-controller/api/types"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// Assembler helps to assemble the init containers
|
||||
type Assembler struct {
|
||||
Name string
|
||||
|
||||
GitCredential bool
|
||||
TerraformCredential bool
|
||||
TerraformRC bool
|
||||
TerraformCredentialsHelper bool
|
||||
|
||||
TerraformImage string
|
||||
BusyboxImage string
|
||||
GitImage string
|
||||
|
||||
Git types.Git
|
||||
Envs []v1.EnvVar
|
||||
}
|
||||
|
||||
func NewAssembler(name string) *Assembler {
|
||||
return &Assembler{Name: name}
|
||||
}
|
||||
|
||||
func (a *Assembler) GitCredReference(ptr *v1.SecretReference) *Assembler {
|
||||
if ptr != nil {
|
||||
a.GitCredential = true
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Assembler) TerraformCredReference(ptr *v1.SecretReference) *Assembler {
|
||||
if ptr != nil {
|
||||
a.TerraformCredential = true
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Assembler) TerraformRCReference(ptr *v1.SecretReference) *Assembler {
|
||||
if ptr != nil {
|
||||
a.TerraformRC = true
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Assembler) TerraformCredentialsHelperReference(ptr *v1.SecretReference) *Assembler {
|
||||
if ptr != nil {
|
||||
a.TerraformCredentialsHelper = true
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Assembler) SetTerraformImage(image string) *Assembler {
|
||||
a.TerraformImage = image
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Assembler) SetBusyboxImage(image string) *Assembler {
|
||||
a.BusyboxImage = image
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Assembler) SetGitImage(image string) *Assembler {
|
||||
a.GitImage = image
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Assembler) SetGit(git types.Git) *Assembler {
|
||||
a.Git = git
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Assembler) SetEnvs(envs []v1.EnvVar) *Assembler {
|
||||
a.Envs = envs
|
||||
return a
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/types"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
const GitContainerName = "git-configuration"
|
||||
|
||||
// GitContainer will clone the git repository, and copy the files to the working directory
|
||||
func (a *Assembler) GitContainer() v1.Container {
|
||||
mounts := []v1.VolumeMount{
|
||||
{
|
||||
Name: a.Name,
|
||||
MountPath: types.WorkingVolumeMountPath,
|
||||
},
|
||||
{
|
||||
Name: types.BackendVolumeName,
|
||||
MountPath: types.BackendVolumeMountPath,
|
||||
},
|
||||
}
|
||||
|
||||
if a.GitCredential {
|
||||
mounts = append(mounts,
|
||||
v1.VolumeMount{
|
||||
Name: types.GitAuthConfigVolumeName,
|
||||
MountPath: types.GitAuthConfigVolumeMountPath,
|
||||
})
|
||||
}
|
||||
|
||||
command := a.getCloneCommand()
|
||||
return v1.Container{
|
||||
Name: GitContainerName,
|
||||
Image: a.GitImage,
|
||||
ImagePullPolicy: v1.PullIfNotPresent,
|
||||
Command: command,
|
||||
VolumeMounts: mounts,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Assembler) getCloneCommand() []string {
|
||||
var cmd string
|
||||
hclPath := filepath.Join(types.BackendVolumeMountPath, a.Git.Path)
|
||||
copyCommand := fmt.Sprintf("cp -r %s/* %s", hclPath, types.WorkingVolumeMountPath)
|
||||
|
||||
checkoutCommand := ""
|
||||
checkoutObject := getCheckoutObj(a.Git.Ref)
|
||||
if checkoutObject != "" {
|
||||
checkoutCommand = fmt.Sprintf("git checkout %s", checkoutObject)
|
||||
}
|
||||
cloneCommand := fmt.Sprintf("git clone %s %s", a.Git.URL, types.BackendVolumeMountPath)
|
||||
|
||||
// Check for git credentials, mount the SSH known hosts and private key, add private key into the SSH authentication agent
|
||||
if a.GitCredential {
|
||||
sshCommand := fmt.Sprintf("eval `ssh-agent` && ssh-add %s/%s", types.GitAuthConfigVolumeMountPath, v1.SSHAuthPrivateKey)
|
||||
cloneCommand = fmt.Sprintf("%s && %s", sshCommand, cloneCommand)
|
||||
}
|
||||
|
||||
cmd = cloneCommand
|
||||
|
||||
if checkoutCommand != "" {
|
||||
cmd = fmt.Sprintf("%s && %s", cmd, checkoutCommand)
|
||||
}
|
||||
cmd = fmt.Sprintf("%s && %s", cmd, copyCommand)
|
||||
|
||||
command := []string{
|
||||
"sh",
|
||||
"-c",
|
||||
cmd,
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
func getCheckoutObj(ref types.GitRef) string {
|
||||
if ref.Commit != "" {
|
||||
return ref.Commit
|
||||
} else if ref.Tag != "" {
|
||||
return ref.Tag
|
||||
}
|
||||
return ref.Branch
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/types"
|
||||
)
|
||||
|
||||
func Test_getCheckoutObj(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ref types.GitRef
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "only branch",
|
||||
ref: types.GitRef{
|
||||
Branch: "feature",
|
||||
},
|
||||
want: "feature",
|
||||
},
|
||||
{
|
||||
name: "tag take precedence over branch",
|
||||
ref: types.GitRef{
|
||||
Branch: "feature",
|
||||
Tag: "v1.0.0",
|
||||
},
|
||||
want: "v1.0.0",
|
||||
},
|
||||
{
|
||||
name: "commit take precedence over tag",
|
||||
ref: types.GitRef{
|
||||
Branch: "feature",
|
||||
Tag: "v1.0.0",
|
||||
Commit: "123456",
|
||||
},
|
||||
want: "123456",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := getCheckoutObj(tt.ref); got != tt.want {
|
||||
t.Errorf("getCheckoutObj() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"github.com/oam-dev/terraform-controller/api/types"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// InitContainer will run terraform init
|
||||
func (a *Assembler) InitContainer() v1.Container {
|
||||
mounts := []v1.VolumeMount{
|
||||
{
|
||||
Name: a.Name,
|
||||
MountPath: types.WorkingVolumeMountPath,
|
||||
},
|
||||
}
|
||||
if a.TerraformCredential {
|
||||
mounts = append(mounts,
|
||||
v1.VolumeMount{
|
||||
Name: types.TerraformCredentialsConfigVolumeName,
|
||||
MountPath: types.TerraformCredentialsConfigVolumeMountPath,
|
||||
})
|
||||
}
|
||||
|
||||
if a.TerraformRC {
|
||||
mounts = append(mounts,
|
||||
v1.VolumeMount{
|
||||
Name: types.TerraformRCConfigVolumeName,
|
||||
MountPath: types.TerraformRCConfigVolumeMountPath,
|
||||
})
|
||||
}
|
||||
|
||||
if a.TerraformCredentialsHelper {
|
||||
mounts = append(mounts,
|
||||
v1.VolumeMount{
|
||||
Name: types.TerraformCredentialsHelperConfigVolumeName,
|
||||
MountPath: types.TerraformCredentialsHelperConfigVolumeMountPath,
|
||||
})
|
||||
}
|
||||
|
||||
c := v1.Container{
|
||||
Name: types.TerraformInitContainerName,
|
||||
Image: a.TerraformImage,
|
||||
ImagePullPolicy: v1.PullIfNotPresent,
|
||||
Command: []string{
|
||||
"sh",
|
||||
"-c",
|
||||
"terraform init",
|
||||
},
|
||||
VolumeMounts: mounts,
|
||||
Env: a.Envs,
|
||||
}
|
||||
return c
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/types"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
const InputContainerName = "prepare-input-terraform-configurations"
|
||||
|
||||
// InputContainer prepare input .tf files, copy them to the working directory
|
||||
func (a *Assembler) InputContainer() v1.Container {
|
||||
mounts := []v1.VolumeMount{
|
||||
|
||||
{
|
||||
Name: a.Name,
|
||||
MountPath: types.WorkingVolumeMountPath,
|
||||
},
|
||||
{
|
||||
Name: types.InputTFConfigurationVolumeName,
|
||||
MountPath: types.InputTFConfigurationVolumeMountPath,
|
||||
},
|
||||
}
|
||||
return v1.Container{
|
||||
Name: InputContainerName,
|
||||
Image: a.BusyboxImage,
|
||||
ImagePullPolicy: v1.PullIfNotPresent,
|
||||
Command: []string{
|
||||
"sh",
|
||||
"-c",
|
||||
fmt.Sprintf("cp %s/* %s", types.InputTFConfigurationVolumeMountPath, types.WorkingVolumeMountPath),
|
||||
},
|
||||
VolumeMounts: mounts,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package process
|
||||
|
||||
import (
|
||||
"github.com/oam-dev/terraform-controller/api/types"
|
||||
crossplane "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime"
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta2"
|
||||
tfcfg "github.com/oam-dev/terraform-controller/controllers/configuration"
|
||||
"github.com/oam-dev/terraform-controller/controllers/configuration/backend"
|
||||
"github.com/pkg/errors"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
// LegacySubResources if user specify ControllerNamespace when re-staring controller, there are some sub-resources like Secret
|
||||
// and ConfigMap that are in the namespace of the Configuration. We need to GC these sub-resources when Configuration is deleted.
|
||||
type LegacySubResources struct {
|
||||
// Namespace is the namespace of the Configuration, also the namespace of the sub-resources.
|
||||
Namespace string
|
||||
ApplyJobName string
|
||||
DestroyJobName string
|
||||
ConfigurationCMName string
|
||||
VariableSecretName string
|
||||
}
|
||||
|
||||
// TFConfigurationMeta is all the metadata of a Configuration
|
||||
type TFConfigurationMeta struct {
|
||||
Name string
|
||||
Namespace string
|
||||
ControllerNamespace string
|
||||
ConfigurationType types.ConfigurationType
|
||||
CompleteConfiguration string
|
||||
Git types.Git
|
||||
ConfigurationChanged bool
|
||||
EnvChanged bool
|
||||
ConfigurationCMName string
|
||||
ApplyJobName string
|
||||
DestroyJobName string
|
||||
Envs []v1.EnvVar
|
||||
ProviderReference *crossplane.Reference
|
||||
VariableSecretName string
|
||||
VariableSecretData map[string][]byte
|
||||
DeleteResource bool
|
||||
Region string
|
||||
Credentials map[string]string
|
||||
JobEnv map[string]interface{}
|
||||
GitCredentialsSecretReference *v1.SecretReference
|
||||
TerraformCredentialsSecretReference *v1.SecretReference
|
||||
TerraformRCConfigMapReference *v1.SecretReference
|
||||
TerraformCredentialsHelperConfigMapReference *v1.SecretReference
|
||||
|
||||
Backend backend.Backend
|
||||
// JobNodeSelector Expose the node selector of job to the controller level
|
||||
JobNodeSelector map[string]string
|
||||
|
||||
// TerraformImage is the Terraform image which can run `terraform init/plan/apply`
|
||||
TerraformImage string
|
||||
BusyboxImage string
|
||||
GitImage string
|
||||
|
||||
// BackoffLimit specifies the number of retries to mark the Job as failed
|
||||
BackoffLimit int32
|
||||
|
||||
// ResourceQuota series Variables are for Setting Compute Resources required by this container
|
||||
ResourceQuota types.ResourceQuota
|
||||
|
||||
LegacySubResources LegacySubResources
|
||||
ControllerNSSpecified bool
|
||||
|
||||
K8sClient client.Client
|
||||
}
|
||||
|
||||
// TFState is Terraform State
|
||||
type TFState struct {
|
||||
Outputs map[string]TfStateProperty `json:"outputs"`
|
||||
}
|
||||
|
||||
// TfStateProperty is the tf state property for an output
|
||||
type TfStateProperty struct {
|
||||
Value interface{} `json:"value,omitempty"`
|
||||
Type interface{} `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// ToProperty converts TfStateProperty type to Property
|
||||
func (tp *TfStateProperty) ToProperty() (v1beta2.Property, error) {
|
||||
var (
|
||||
property v1beta2.Property
|
||||
err error
|
||||
)
|
||||
sv, err := tfcfg.Interface2String(tp.Value)
|
||||
if err != nil {
|
||||
return property, errors.Wrapf(err, "failed to convert value %s of terraform state outputs to string", tp.Value)
|
||||
}
|
||||
property = v1beta2.Property{
|
||||
Value: sv,
|
||||
}
|
||||
return property, err
|
||||
}
|
|
@ -0,0 +1,757 @@
|
|||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/controllers/process/container"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/klog/v2"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/types"
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta1"
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta2"
|
||||
tfcfg "github.com/oam-dev/terraform-controller/controllers/configuration"
|
||||
"github.com/oam-dev/terraform-controller/controllers/configuration/backend"
|
||||
"github.com/oam-dev/terraform-controller/controllers/provider"
|
||||
"github.com/oam-dev/terraform-controller/controllers/util"
|
||||
)
|
||||
|
||||
type Option func(spec v1beta2.Configuration, meta *TFConfigurationMeta)
|
||||
|
||||
// ControllerNamespaceOption will set the controller namespace for TFConfigurationMeta
|
||||
func ControllerNamespaceOption(controllerNamespace string) Option {
|
||||
return func(configuration v1beta2.Configuration, meta *TFConfigurationMeta) {
|
||||
if controllerNamespace == "" {
|
||||
return
|
||||
}
|
||||
uid := string(configuration.GetUID())
|
||||
// @step: since we are using a single namespace to run these, we must ensure the names
|
||||
// are unique across the namespace
|
||||
meta.KeepLegacySubResourceMetas()
|
||||
meta.ApplyJobName = uid + "-" + string(types.TerraformApply)
|
||||
meta.DestroyJobName = uid + "-" + string(types.TerraformDestroy)
|
||||
meta.ConfigurationCMName = fmt.Sprintf(types.TFInputConfigMapName, uid)
|
||||
meta.VariableSecretName = fmt.Sprintf(types.TFVariableSecret, uid)
|
||||
meta.ControllerNamespace = controllerNamespace
|
||||
meta.ControllerNSSpecified = true
|
||||
}
|
||||
}
|
||||
|
||||
// New will create a new TFConfigurationMeta to process the configuration
|
||||
func New(req ctrl.Request, configuration v1beta2.Configuration, k8sClient client.Client, option ...Option) *TFConfigurationMeta {
|
||||
var meta = &TFConfigurationMeta{
|
||||
ControllerNamespace: req.Namespace,
|
||||
Namespace: req.Namespace,
|
||||
Name: req.Name,
|
||||
ConfigurationCMName: fmt.Sprintf(types.TFInputConfigMapName, req.Name),
|
||||
VariableSecretName: fmt.Sprintf(types.TFVariableSecret, req.Name),
|
||||
ApplyJobName: req.Name + "-" + string(types.TerraformApply),
|
||||
DestroyJobName: req.Name + "-" + string(types.TerraformDestroy),
|
||||
K8sClient: k8sClient,
|
||||
}
|
||||
|
||||
jobNodeSelectorStr := os.Getenv("JOB_NODE_SELECTOR")
|
||||
if jobNodeSelectorStr != "" {
|
||||
err := json.Unmarshal([]byte(jobNodeSelectorStr), &meta.JobNodeSelector)
|
||||
if err != nil {
|
||||
klog.Warningf("the value of JobNodeSelector is not a json string: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// githubBlocked mark whether GitHub is blocked in the cluster
|
||||
githubBlockedStr := os.Getenv("GITHUB_BLOCKED")
|
||||
if githubBlockedStr == "" {
|
||||
githubBlockedStr = "false"
|
||||
}
|
||||
|
||||
meta.Git.URL = tfcfg.ReplaceTerraformSource(configuration.Spec.Remote, githubBlockedStr)
|
||||
if configuration.Spec.Path == "" {
|
||||
meta.Git.Path = "."
|
||||
} else {
|
||||
meta.Git.Path = configuration.Spec.Path
|
||||
}
|
||||
if configuration.Spec.DeleteResource != nil {
|
||||
meta.DeleteResource = *configuration.Spec.DeleteResource
|
||||
} else {
|
||||
meta.DeleteResource = true
|
||||
}
|
||||
|
||||
if !configuration.Spec.InlineCredentials {
|
||||
meta.ProviderReference = tfcfg.GetProviderNamespacedName(configuration)
|
||||
}
|
||||
|
||||
if configuration.Spec.GitCredentialsSecretReference != nil {
|
||||
meta.GitCredentialsSecretReference = configuration.Spec.GitCredentialsSecretReference
|
||||
}
|
||||
|
||||
if configuration.Spec.TerraformCredentialsSecretReference != nil {
|
||||
meta.TerraformCredentialsSecretReference = configuration.Spec.TerraformCredentialsSecretReference
|
||||
}
|
||||
|
||||
if configuration.Spec.TerraformRCConfigMapReference != nil {
|
||||
meta.TerraformRCConfigMapReference = configuration.Spec.TerraformRCConfigMapReference
|
||||
}
|
||||
|
||||
if configuration.Spec.TerraformCredentialsHelperConfigMapReference != nil {
|
||||
meta.TerraformCredentialsHelperConfigMapReference = configuration.Spec.TerraformCredentialsHelperConfigMapReference
|
||||
}
|
||||
|
||||
for _, opt := range option {
|
||||
opt(configuration, meta)
|
||||
}
|
||||
return meta
|
||||
}
|
||||
|
||||
func (meta *TFConfigurationMeta) ValidateSecretAndConfigMap(ctx context.Context, k8sClient client.Client) error {
|
||||
|
||||
secretConfigMapToCheck := []struct {
|
||||
ref *v1.SecretReference
|
||||
notFoundState types.ConfigurationState
|
||||
isSecret bool
|
||||
neededKeys []string
|
||||
errKey string
|
||||
}{
|
||||
{
|
||||
ref: meta.GitCredentialsSecretReference,
|
||||
notFoundState: types.InvalidGitCredentialsSecretReference,
|
||||
isSecret: true,
|
||||
neededKeys: []string{types.GitCredsKnownHosts, v1.SSHAuthPrivateKey},
|
||||
errKey: "git credentials",
|
||||
},
|
||||
{
|
||||
ref: meta.TerraformCredentialsSecretReference,
|
||||
notFoundState: types.InvalidTerraformCredentialsSecretReference,
|
||||
isSecret: true,
|
||||
neededKeys: []string{types.TerraformCredentials},
|
||||
errKey: "terraform credentials",
|
||||
},
|
||||
{
|
||||
ref: meta.TerraformRCConfigMapReference,
|
||||
notFoundState: types.InvalidTerraformRCConfigMapReference,
|
||||
isSecret: false,
|
||||
neededKeys: []string{types.TerraformRegistryConfig},
|
||||
errKey: "terraformrc configuration",
|
||||
},
|
||||
{
|
||||
ref: meta.TerraformCredentialsHelperConfigMapReference,
|
||||
notFoundState: types.InvalidTerraformCredentialsHelperConfigMapReference,
|
||||
isSecret: false,
|
||||
neededKeys: []string{},
|
||||
errKey: "terraform credentials helper",
|
||||
},
|
||||
}
|
||||
for _, check := range secretConfigMapToCheck {
|
||||
if check.ref != nil {
|
||||
var object metav1.Object
|
||||
var err error
|
||||
object, err = GetSecretOrConfigMap(ctx, k8sClient, check.isSecret, check.ref, check.neededKeys, check.errKey)
|
||||
if object == nil {
|
||||
msg := string(check.notFoundState)
|
||||
if err != nil {
|
||||
msg = err.Error()
|
||||
}
|
||||
if updateStatusErr := meta.UpdateApplyStatus(ctx, k8sClient, check.notFoundState, msg); updateStatusErr != nil {
|
||||
return errors.Wrap(updateStatusErr, msg)
|
||||
}
|
||||
return errors.New(msg)
|
||||
}
|
||||
// fix: The configmap or secret that the pod restricts from mounting must be in the same namespace as the pod,
|
||||
// otherwise the volume mount will fail.
|
||||
if object.GetNamespace() != meta.ControllerNamespace {
|
||||
objectKind := "ConfigMap"
|
||||
if check.isSecret {
|
||||
objectKind = "Secret"
|
||||
}
|
||||
msg := fmt.Sprintf("Invalid %s '%s/%s', whose namespace '%s' is different from the Configuration, cannot mount the volume,"+
|
||||
" you can fix this issue by creating the Secret/ConfigMap in the '%s' namespace.",
|
||||
objectKind, object.GetNamespace(), object.GetName(), meta.ControllerNamespace, meta.ControllerNamespace)
|
||||
if updateStatusErr := meta.UpdateApplyStatus(ctx, k8sClient, check.notFoundState, msg); updateStatusErr != nil {
|
||||
return errors.Wrap(updateStatusErr, msg)
|
||||
}
|
||||
return errors.New(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (meta *TFConfigurationMeta) UpdateApplyStatus(ctx context.Context, k8sClient client.Client, state types.ConfigurationState, message string) error {
|
||||
var configuration v1beta2.Configuration
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: meta.Name, Namespace: meta.Namespace}, &configuration); err == nil {
|
||||
configuration.Status.Apply = v1beta2.ConfigurationApplyStatus{
|
||||
State: state,
|
||||
Message: message,
|
||||
Region: meta.Region,
|
||||
}
|
||||
configuration.Status.ObservedGeneration = configuration.Generation
|
||||
if state == types.Available {
|
||||
outputs, err := meta.getTFOutputs(ctx, k8sClient, configuration)
|
||||
if err != nil {
|
||||
klog.InfoS("Failed to get outputs", "error", err)
|
||||
configuration.Status.Apply = v1beta2.ConfigurationApplyStatus{
|
||||
State: types.GeneratingOutputs,
|
||||
Message: types.ErrGenerateOutputs + ": " + err.Error(),
|
||||
}
|
||||
} else {
|
||||
configuration.Status.Apply.Outputs = outputs
|
||||
}
|
||||
}
|
||||
|
||||
return k8sClient.Status().Update(ctx, &configuration)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (meta *TFConfigurationMeta) UpdateDestroyStatus(ctx context.Context, k8sClient client.Client, state types.ConfigurationState, message string) error {
|
||||
var configuration v1beta2.Configuration
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: meta.Name, Namespace: meta.Namespace}, &configuration); err == nil {
|
||||
configuration.Status.Destroy = v1beta2.ConfigurationDestroyStatus{
|
||||
State: state,
|
||||
Message: message,
|
||||
}
|
||||
return k8sClient.Status().Update(ctx, &configuration)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (meta *TFConfigurationMeta) AssembleAndTriggerJob(ctx context.Context, k8sClient client.Client, executionType types.TerraformExecutionType) error {
|
||||
// apply rbac
|
||||
if err := createTerraformExecutorServiceAccount(ctx, k8sClient, meta.ControllerNamespace, types.ServiceAccountName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := util.CreateTerraformExecutorClusterRoleBinding(ctx, k8sClient, meta.ControllerNamespace, fmt.Sprintf("%s-%s", meta.ControllerNamespace, types.ClusterRoleName), types.ServiceAccountName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
job := meta.assembleTerraformJob(executionType)
|
||||
|
||||
return k8sClient.Create(ctx, job)
|
||||
}
|
||||
|
||||
// UpdateTerraformJobIfNeeded will set deletion finalizer to the Terraform job if its envs are changed, which will result in
|
||||
// deleting the job. Finally, a new Terraform job will be generated
|
||||
func (meta *TFConfigurationMeta) UpdateTerraformJobIfNeeded(ctx context.Context, k8sClient client.Client, job batchv1.Job) error {
|
||||
// if either one changes, delete the job
|
||||
if meta.EnvChanged || meta.ConfigurationChanged {
|
||||
klog.InfoS("about to delete job", "Name", job.Name, "Namespace", job.Namespace)
|
||||
var j batchv1.Job
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: job.Name, Namespace: job.Namespace}, &j); err == nil {
|
||||
if deleteErr := k8sClient.Delete(ctx, &job, client.PropagationPolicy(metav1.DeletePropagationBackground)); deleteErr != nil {
|
||||
return deleteErr
|
||||
}
|
||||
}
|
||||
var s v1.Secret
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: meta.VariableSecretName, Namespace: meta.ControllerNamespace}, &s); err == nil {
|
||||
if deleteErr := k8sClient.Delete(ctx, &s); deleteErr != nil {
|
||||
return deleteErr
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (meta *TFConfigurationMeta) assembleTerraformJob(executionType types.TerraformExecutionType) *batchv1.Job {
|
||||
var (
|
||||
initContainers []v1.Container
|
||||
parallelism int32 = 1
|
||||
completions int32 = 1
|
||||
)
|
||||
|
||||
executorVolumes := meta.assembleExecutorVolumes()
|
||||
|
||||
assembler := container.NewAssembler(meta.Name).
|
||||
TerraformCredReference(meta.TerraformCredentialsSecretReference).
|
||||
TerraformRCReference(meta.TerraformRCConfigMapReference).
|
||||
TerraformCredentialsHelperReference(meta.TerraformCredentialsHelperConfigMapReference).
|
||||
GitCredReference(meta.GitCredentialsSecretReference).
|
||||
SetGit(meta.Git).
|
||||
SetBusyboxImage(meta.BusyboxImage).
|
||||
SetTerraformImage(meta.TerraformImage).
|
||||
SetGitImage(meta.GitImage).
|
||||
SetEnvs(meta.Envs)
|
||||
|
||||
initContainers = append(initContainers, assembler.InputContainer())
|
||||
if meta.Git.URL != "" {
|
||||
initContainers = append(initContainers, assembler.GitContainer())
|
||||
}
|
||||
initContainers = append(initContainers, assembler.InitContainer())
|
||||
|
||||
applyContainer := assembler.ApplyContainer(executionType, meta.ResourceQuota)
|
||||
|
||||
name := meta.ApplyJobName
|
||||
if executionType == types.TerraformDestroy {
|
||||
name = meta.DestroyJobName
|
||||
}
|
||||
|
||||
return &batchv1.Job{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Job",
|
||||
APIVersion: "batch/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: meta.ControllerNamespace,
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
Parallelism: ¶llelism,
|
||||
Completions: &completions,
|
||||
BackoffLimit: &meta.BackoffLimit,
|
||||
Template: v1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
// This annotation will prevent istio-proxy sidecar injection in the pods
|
||||
// as having the sidecar would have kept the Job in `Running` state and would
|
||||
// not transition to `Completed`
|
||||
"sidecar.istio.io/inject": "false",
|
||||
},
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
// InitContainer will copy Terraform configuration files to working directory and create Terraform
|
||||
// state file directory in advance
|
||||
InitContainers: initContainers,
|
||||
// Container terraform-executor will first copy predefined terraform.d to working directory, and
|
||||
// then run terraform init/apply.
|
||||
Containers: []v1.Container{applyContainer},
|
||||
ServiceAccountName: types.ServiceAccountName,
|
||||
Volumes: executorVolumes,
|
||||
RestartPolicy: v1.RestartPolicyOnFailure,
|
||||
NodeSelector: meta.JobNodeSelector,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (meta *TFConfigurationMeta) assembleExecutorVolumes() []v1.Volume {
|
||||
workingVolume := v1.Volume{Name: meta.Name}
|
||||
workingVolume.EmptyDir = &v1.EmptyDirVolumeSource{}
|
||||
inputTFConfigurationVolume := meta.createConfigurationVolume()
|
||||
tfBackendVolume := meta.createTFBackendVolume()
|
||||
executorVolumes := []v1.Volume{workingVolume, inputTFConfigurationVolume, tfBackendVolume}
|
||||
secretOrConfigMapReferences := []struct {
|
||||
ref *v1.SecretReference
|
||||
volumeName string
|
||||
isSecret bool
|
||||
}{
|
||||
{
|
||||
ref: meta.GitCredentialsSecretReference,
|
||||
volumeName: types.GitAuthConfigVolumeName,
|
||||
isSecret: true,
|
||||
},
|
||||
{
|
||||
ref: meta.TerraformCredentialsSecretReference,
|
||||
volumeName: types.TerraformCredentialsConfigVolumeName,
|
||||
isSecret: true,
|
||||
},
|
||||
{
|
||||
ref: meta.TerraformRCConfigMapReference,
|
||||
volumeName: types.TerraformRCConfigVolumeName,
|
||||
isSecret: false,
|
||||
},
|
||||
{
|
||||
ref: meta.TerraformCredentialsHelperConfigMapReference,
|
||||
volumeName: types.TerraformCredentialsHelperConfigVolumeName,
|
||||
isSecret: false,
|
||||
},
|
||||
}
|
||||
for _, ref := range secretOrConfigMapReferences {
|
||||
if ref.ref != nil {
|
||||
executorVolumes = append(executorVolumes, meta.createSecretOrConfigMapVolume(ref.isSecret, ref.ref.Name, ref.volumeName))
|
||||
}
|
||||
}
|
||||
return executorVolumes
|
||||
}
|
||||
|
||||
func (meta *TFConfigurationMeta) createConfigurationVolume() v1.Volume {
|
||||
inputCMVolumeSource := v1.ConfigMapVolumeSource{}
|
||||
inputCMVolumeSource.Name = meta.ConfigurationCMName
|
||||
inputTFConfigurationVolume := v1.Volume{Name: types.InputTFConfigurationVolumeName}
|
||||
inputTFConfigurationVolume.ConfigMap = &inputCMVolumeSource
|
||||
return inputTFConfigurationVolume
|
||||
|
||||
}
|
||||
|
||||
func (meta *TFConfigurationMeta) createTFBackendVolume() v1.Volume {
|
||||
gitVolume := v1.Volume{Name: types.BackendVolumeName}
|
||||
gitVolume.EmptyDir = &v1.EmptyDirVolumeSource{}
|
||||
return gitVolume
|
||||
}
|
||||
|
||||
func (meta *TFConfigurationMeta) createSecretOrConfigMapVolume(isSecret bool, secretOrConfigMapReferenceName string, volumeName string) v1.Volume {
|
||||
var defaultMode int32 = 0400
|
||||
volume := v1.Volume{Name: volumeName}
|
||||
if isSecret {
|
||||
volumeSource := v1.SecretVolumeSource{}
|
||||
volumeSource.SecretName = secretOrConfigMapReferenceName
|
||||
volumeSource.DefaultMode = &defaultMode
|
||||
volume.Secret = &volumeSource
|
||||
} else {
|
||||
volumeSource := v1.ConfigMapVolumeSource{}
|
||||
volumeSource.Name = secretOrConfigMapReferenceName
|
||||
volumeSource.DefaultMode = &defaultMode
|
||||
volume.ConfigMap = &volumeSource
|
||||
}
|
||||
return volume
|
||||
}
|
||||
|
||||
func (meta *TFConfigurationMeta) KeepLegacySubResourceMetas() {
|
||||
meta.LegacySubResources.Namespace = meta.Namespace
|
||||
meta.LegacySubResources.ApplyJobName = meta.ApplyJobName
|
||||
meta.LegacySubResources.DestroyJobName = meta.DestroyJobName
|
||||
meta.LegacySubResources.ConfigurationCMName = meta.ConfigurationCMName
|
||||
meta.LegacySubResources.VariableSecretName = meta.VariableSecretName
|
||||
}
|
||||
|
||||
func (meta *TFConfigurationMeta) GetApplyJob(ctx context.Context, k8sClient client.Client, job *batchv1.Job) error {
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: meta.LegacySubResources.ApplyJobName, Namespace: meta.LegacySubResources.Namespace}, job); err == nil {
|
||||
klog.InfoS("Found legacy apply job", "Configuration", fmt.Sprintf("%s/%s", meta.Name, meta.Namespace),
|
||||
"Job", fmt.Sprintf("%s/%s", meta.LegacySubResources.Namespace, meta.LegacySubResources.ApplyJobName))
|
||||
return nil
|
||||
}
|
||||
err := k8sClient.Get(ctx, client.ObjectKey{Name: meta.ApplyJobName, Namespace: meta.ControllerNamespace}, job)
|
||||
return err
|
||||
}
|
||||
|
||||
// RenderConfiguration will compose the Terraform configuration with hcl/json and backend
|
||||
func (meta *TFConfigurationMeta) RenderConfiguration(configuration *v1beta2.Configuration, configurationType types.ConfigurationType) (string, backend.Backend, error) {
|
||||
backendInterface, err := backend.ParseConfigurationBackend(configuration, meta.K8sClient, meta.Credentials, meta.ControllerNSSpecified)
|
||||
if err != nil {
|
||||
return "", nil, errors.Wrap(err, "failed to prepare Terraform backend configuration")
|
||||
}
|
||||
|
||||
switch configurationType {
|
||||
case types.ConfigurationHCL:
|
||||
completedConfiguration := configuration.Spec.HCL
|
||||
completedConfiguration += "\n" + backendInterface.HCL()
|
||||
return completedConfiguration, backendInterface, nil
|
||||
case types.ConfigurationRemote:
|
||||
return backendInterface.HCL(), backendInterface, nil
|
||||
default:
|
||||
return "", nil, errors.New("Unsupported Configuration Type")
|
||||
}
|
||||
}
|
||||
|
||||
func (meta *TFConfigurationMeta) IsTFStateGenerated(ctx context.Context) bool {
|
||||
// 1. exist backend
|
||||
if meta.Backend == nil {
|
||||
return false
|
||||
}
|
||||
// 2. and exist tfstate file
|
||||
_, err := meta.Backend.GetTFStateJSON(ctx)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
//nolint:funlen
|
||||
func (meta *TFConfigurationMeta) getTFOutputs(ctx context.Context, k8sClient client.Client, configuration v1beta2.Configuration) (map[string]v1beta2.Property, error) {
|
||||
var tfStateJSON []byte
|
||||
var err error
|
||||
if meta.Backend != nil {
|
||||
tfStateJSON, err = meta.Backend.GetTFStateJSON(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var tfState TFState
|
||||
if err := json.Unmarshal(tfStateJSON, &tfState); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outputs := make(map[string]v1beta2.Property)
|
||||
for k, v := range tfState.Outputs {
|
||||
property, err := v.ToProperty()
|
||||
if err != nil {
|
||||
return outputs, err
|
||||
}
|
||||
outputs[k] = property
|
||||
}
|
||||
writeConnectionSecretToReference := configuration.Spec.WriteConnectionSecretToReference
|
||||
if writeConnectionSecretToReference == nil || writeConnectionSecretToReference.Name == "" {
|
||||
return outputs, nil
|
||||
}
|
||||
|
||||
name := writeConnectionSecretToReference.Name
|
||||
ns := writeConnectionSecretToReference.Namespace
|
||||
if ns == "" {
|
||||
ns = types.DefaultNamespace
|
||||
}
|
||||
data := make(map[string][]byte)
|
||||
for k, v := range outputs {
|
||||
data[k] = []byte(v.Value)
|
||||
}
|
||||
var gotSecret v1.Secret
|
||||
configurationName := configuration.ObjectMeta.Name
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: name, Namespace: ns}, &gotSecret); err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
var secret = v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: ns,
|
||||
Labels: map[string]string{
|
||||
"terraform.core.oam.dev/created-by": "terraform-controller",
|
||||
"terraform.core.oam.dev/owned-by": configurationName,
|
||||
"terraform.core.oam.dev/owned-namespace": configuration.Namespace,
|
||||
},
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{Kind: "Secret"},
|
||||
Data: data,
|
||||
}
|
||||
err = k8sClient.Create(ctx, &secret)
|
||||
if kerrors.IsAlreadyExists(err) {
|
||||
return nil, fmt.Errorf("secret(%s) already exists", name)
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// check the owner of this secret
|
||||
labels := gotSecret.ObjectMeta.Labels
|
||||
ownerName := labels["terraform.core.oam.dev/owned-by"]
|
||||
ownerNamespace := labels["terraform.core.oam.dev/owned-namespace"]
|
||||
if (ownerName != "" && ownerName != configurationName) ||
|
||||
(ownerNamespace != "" && ownerNamespace != configuration.Namespace) {
|
||||
errMsg := fmt.Sprintf(
|
||||
"configuration(namespace: %s ; name: %s) cannot update secret(namespace: %s ; name: %s) whose owner is configuration(namespace: %s ; name: %s)",
|
||||
configuration.Namespace, configurationName,
|
||||
gotSecret.Namespace, name,
|
||||
ownerNamespace, ownerName,
|
||||
)
|
||||
klog.ErrorS(err, "fail to update backend secret")
|
||||
return nil, errors.New(errMsg)
|
||||
}
|
||||
gotSecret.Data = data
|
||||
if err := k8sClient.Update(ctx, &gotSecret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return outputs, nil
|
||||
}
|
||||
|
||||
func (meta *TFConfigurationMeta) PrepareTFVariables(configuration *v1beta2.Configuration) error {
|
||||
var (
|
||||
envs []v1.EnvVar
|
||||
data = map[string][]byte{}
|
||||
)
|
||||
|
||||
if configuration == nil {
|
||||
return errors.New("configuration is nil")
|
||||
}
|
||||
if !configuration.Spec.InlineCredentials && meta.ProviderReference == nil {
|
||||
return errors.New("The referenced provider could not be retrieved")
|
||||
}
|
||||
|
||||
tfVariable, err := getTerraformJSONVariable(configuration.Spec.Variable)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, fmt.Sprintf("failed to get Terraform JSON variables from Configuration Variables %v", configuration.Spec.Variable))
|
||||
}
|
||||
for k, v := range tfVariable {
|
||||
envValue, err := tfcfg.Interface2String(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data[k] = []byte(envValue)
|
||||
}
|
||||
|
||||
if !configuration.Spec.InlineCredentials && meta.Credentials == nil {
|
||||
return errors.New(provider.ErrCredentialNotRetrieved)
|
||||
}
|
||||
for k, v := range meta.Credentials {
|
||||
data[k] = []byte(v)
|
||||
}
|
||||
for k, v := range meta.JobEnv {
|
||||
envValue, err := tfcfg.Interface2String(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data[k] = []byte(envValue)
|
||||
}
|
||||
for k := range data {
|
||||
valueFrom := &v1.EnvVarSource{SecretKeyRef: &v1.SecretKeySelector{Key: k}}
|
||||
valueFrom.SecretKeyRef.Name = meta.VariableSecretName
|
||||
envs = append(envs, v1.EnvVar{Name: k, ValueFrom: valueFrom})
|
||||
}
|
||||
meta.Envs = envs
|
||||
meta.VariableSecretData = data
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCredentials will get credentials from secret of the Provider
|
||||
func (meta *TFConfigurationMeta) GetCredentials(ctx context.Context, k8sClient client.Client, providerObj *v1beta1.Provider) error {
|
||||
region, err := tfcfg.SetRegion(ctx, k8sClient, meta.Namespace, meta.Name, providerObj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
credentials, err := provider.GetProviderCredentials(ctx, k8sClient, providerObj, region)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if credentials == nil {
|
||||
return errors.New(provider.ErrCredentialNotRetrieved)
|
||||
}
|
||||
meta.Credentials = credentials
|
||||
meta.Region = region
|
||||
return nil
|
||||
}
|
||||
|
||||
func (meta *TFConfigurationMeta) createOrUpdateConfigMap(ctx context.Context, k8sClient client.Client, data map[string]string) error {
|
||||
var gotCM v1.ConfigMap
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: meta.ConfigurationCMName, Namespace: meta.ControllerNamespace}, &gotCM); err != nil {
|
||||
if !kerrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
cm := v1.ConfigMap{
|
||||
TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "ConfigMap"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: meta.ConfigurationCMName,
|
||||
Namespace: meta.ControllerNamespace,
|
||||
},
|
||||
Data: data,
|
||||
}
|
||||
|
||||
if err := k8sClient.Create(ctx, &cm); err != nil {
|
||||
return errors.Wrap(err, "failed to create TF configuration ConfigMap")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(gotCM.Data, data) {
|
||||
gotCM.Data = data
|
||||
|
||||
return errors.Wrap(k8sClient.Update(ctx, &gotCM), "failed to update TF configuration ConfigMap")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (meta *TFConfigurationMeta) prepareTFInputConfigurationData() map[string]string {
|
||||
var dataName string
|
||||
switch meta.ConfigurationType {
|
||||
case types.ConfigurationHCL:
|
||||
dataName = types.TerraformHCLConfigurationName
|
||||
case types.ConfigurationRemote:
|
||||
dataName = "terraform-backend.tf"
|
||||
}
|
||||
data := map[string]string{dataName: meta.CompleteConfiguration, "kubeconfig": ""}
|
||||
return data
|
||||
}
|
||||
|
||||
// StoreTFConfiguration will store Terraform configuration to ConfigMap
|
||||
func (meta *TFConfigurationMeta) StoreTFConfiguration(ctx context.Context, k8sClient client.Client) error {
|
||||
data := meta.prepareTFInputConfigurationData()
|
||||
return meta.createOrUpdateConfigMap(ctx, k8sClient, data)
|
||||
}
|
||||
|
||||
// CheckWhetherConfigurationChanges will check whether configuration is changed
|
||||
func (meta *TFConfigurationMeta) CheckWhetherConfigurationChanges(ctx context.Context, k8sClient client.Client, configurationType types.ConfigurationType) error {
|
||||
switch configurationType {
|
||||
case types.ConfigurationHCL:
|
||||
var cm v1.ConfigMap
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: meta.ConfigurationCMName, Namespace: meta.ControllerNamespace}, &cm); err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
meta.ConfigurationChanged = cm.Data[types.TerraformHCLConfigurationName] != meta.CompleteConfiguration
|
||||
if meta.ConfigurationChanged {
|
||||
klog.InfoS("Configuration HCL changed", "ConfigMap", cm.Data[types.TerraformHCLConfigurationName],
|
||||
"RenderedCompletedConfiguration", meta.CompleteConfiguration)
|
||||
}
|
||||
|
||||
return nil
|
||||
case types.ConfigurationRemote:
|
||||
meta.ConfigurationChanged = false
|
||||
return nil
|
||||
default:
|
||||
return errors.New("unsupported configuration type, only HCL or Remote is supported")
|
||||
}
|
||||
}
|
||||
|
||||
func GetSecretOrConfigMap(ctx context.Context, k8sClient client.Client, isSecret bool, ref *v1.SecretReference, neededKeys []string, errKey string) (metav1.Object, error) {
|
||||
secret := &v1.Secret{}
|
||||
configMap := &v1.ConfigMap{}
|
||||
var err error
|
||||
// key to determine if it is a secret or config map
|
||||
var typeKey string
|
||||
if isSecret {
|
||||
namespacedName := client.ObjectKey{Name: ref.Name, Namespace: ref.Namespace}
|
||||
err = k8sClient.Get(ctx, namespacedName, secret)
|
||||
typeKey = "secret"
|
||||
} else {
|
||||
namespacedName := client.ObjectKey{Name: ref.Name, Namespace: ref.Namespace}
|
||||
err = k8sClient.Get(ctx, namespacedName, configMap)
|
||||
typeKey = "configmap"
|
||||
}
|
||||
errMsg := fmt.Sprintf("Failed to get %s %s", errKey, typeKey)
|
||||
if err != nil {
|
||||
klog.ErrorS(err, errMsg, "Name", ref.Name, "Namespace", ref.Namespace)
|
||||
return nil, errors.Wrap(err, errMsg)
|
||||
}
|
||||
for _, key := range neededKeys {
|
||||
var keyErr bool
|
||||
if isSecret {
|
||||
if _, ok := secret.Data[key]; !ok {
|
||||
keyErr = true
|
||||
}
|
||||
} else {
|
||||
if _, ok := configMap.Data[key]; !ok {
|
||||
keyErr = true
|
||||
}
|
||||
}
|
||||
if keyErr {
|
||||
keyErr := errors.Errorf("'%s' not in %s %s", key, errKey, typeKey)
|
||||
return nil, keyErr
|
||||
}
|
||||
}
|
||||
if isSecret {
|
||||
return secret, nil
|
||||
}
|
||||
return configMap, nil
|
||||
}
|
||||
|
||||
func createTerraformExecutorServiceAccount(ctx context.Context, k8sClient client.Client, namespace, serviceAccountName string) error {
|
||||
var serviceAccount = v1.ServiceAccount{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ServiceAccount",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: serviceAccountName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: serviceAccountName, Namespace: namespace}, &v1.ServiceAccount{}); err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
if err := k8sClient.Create(ctx, &serviceAccount); err != nil {
|
||||
return errors.Wrap(err, "failed to create ServiceAccount for Terraform executor")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTerraformJSONVariable(tfVariables *runtime.RawExtension) (map[string]interface{}, error) {
|
||||
variables, err := tfcfg.RawExtension2Map(tfVariables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var environments = make(map[string]interface{})
|
||||
|
||||
for k, v := range variables {
|
||||
environments[fmt.Sprintf("TF_VAR_%s", k)] = v
|
||||
}
|
||||
return environments, nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,39 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
// EnvAWSAccessKeyID is the name of the AWS_ACCESS_KEY_ID env
|
||||
EnvAWSAccessKeyID = "AWS_ACCESS_KEY_ID"
|
||||
// EnvAWSSecretAccessKey is the name of the AWS_SECRET_ACCESS_KEY env
|
||||
EnvAWSSecretAccessKey = "AWS_SECRET_ACCESS_KEY"
|
||||
// EnvAWSDefaultRegion is the name of the AWS_DEFAULT_REGION env
|
||||
EnvAWSDefaultRegion = "AWS_DEFAULT_REGION"
|
||||
// EnvAWSSessionToken is the name of the AWS_SESSION_TOKEN env
|
||||
EnvAWSSessionToken = "AWS_SESSION_TOKEN"
|
||||
)
|
||||
|
||||
// AWSCredentials are credentials for AWS
|
||||
type AWSCredentials struct {
|
||||
AWSAccessKeyID string `yaml:"awsAccessKeyID"`
|
||||
AWSSecretAccessKey string `yaml:"awsSecretAccessKey"`
|
||||
AWSSessionToken string `yaml:"awsSessionToken"`
|
||||
}
|
||||
|
||||
func getAWSCredentials(secretData []byte, name, namespace, region string) (map[string]string, error) {
|
||||
var ak AWSCredentials
|
||||
if err := yaml.Unmarshal(secretData, &ak); err != nil {
|
||||
klog.ErrorS(err, errConvertCredentials, "Name", name, "Namespace", namespace)
|
||||
return nil, errors.Wrap(err, errConvertCredentials)
|
||||
}
|
||||
return map[string]string{
|
||||
EnvAWSAccessKeyID: ak.AWSAccessKeyID,
|
||||
EnvAWSSecretAccessKey: ak.AWSSecretAccessKey,
|
||||
EnvAWSSessionToken: ak.AWSSessionToken,
|
||||
EnvAWSDefaultRegion: region,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
envARMClientID = "ARM_CLIENT_ID"
|
||||
envARMClientSecret = "ARM_CLIENT_SECRET"
|
||||
envARMSubscriptionID = "ARM_SUBSCRIPTION_ID"
|
||||
envARMTenantID = "ARM_TENANT_ID"
|
||||
)
|
||||
|
||||
// AzureCredentials are credentials for Azure
|
||||
type AzureCredentials struct {
|
||||
ARMClientID string `yaml:"armClientID"`
|
||||
ARMClientSecret string `yaml:"armClientSecret"`
|
||||
ARMSubscriptionID string `yaml:"armSubscriptionID"`
|
||||
ARMTenantID string `yaml:"armTenantID"`
|
||||
}
|
||||
|
||||
func getAzureCredentials(secretData []byte, name, namespace string) (map[string]string, error) {
|
||||
var cred AzureCredentials
|
||||
if err := yaml.Unmarshal(secretData, &cred); err != nil {
|
||||
klog.ErrorS(err, errConvertCredentials, "Name", name, "Namespace", namespace)
|
||||
return nil, errors.Wrap(err, errConvertCredentials)
|
||||
}
|
||||
return map[string]string{
|
||||
envARMClientID: cred.ARMClientID,
|
||||
envARMClientSecret: cred.ARMClientSecret,
|
||||
envARMSubscriptionID: cred.ARMSubscriptionID,
|
||||
envARMTenantID: cred.ARMTenantID,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
envBaiduAccessKey = "BAIDUCLOUD_ACCESS_KEY"
|
||||
envBaiduSecretKey = "BAIDUCLOUD_SECRET_KEY"
|
||||
envBaiduRegion = "BAIDUCLOUD_REGION"
|
||||
)
|
||||
|
||||
// BaiduCloudCredentials are credentials for Baidu Cloud
|
||||
type BaiduCloudCredentials struct {
|
||||
KeyBaiduAccessKey string `yaml:"accessKey"`
|
||||
KeyBaiduSecretKey string `yaml:"secretKey"`
|
||||
}
|
||||
|
||||
func getBaiduCloudCredentials(secretData []byte, name, namespace, region string) (map[string]string, error) {
|
||||
var ak BaiduCloudCredentials
|
||||
if err := yaml.Unmarshal(secretData, &ak); err != nil {
|
||||
klog.ErrorS(err, errConvertCredentials, "Name", name, "Namespace", namespace)
|
||||
return nil, errors.Wrap(err, errConvertCredentials)
|
||||
}
|
||||
return map[string]string{
|
||||
envBaiduAccessKey: ak.KeyBaiduAccessKey,
|
||||
envBaiduSecretKey: ak.KeyBaiduSecretKey,
|
||||
envBaiduRegion: region,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aliyun/alibaba-cloud-sdk-go/services/sts"
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/pkg/errors"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta1"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultName is the name of Provider object
|
||||
DefaultName = "default"
|
||||
// DefaultNamespace is the namespace of Provider object
|
||||
DefaultNamespace = "default"
|
||||
)
|
||||
|
||||
// CloudProvider is a type for mark a Cloud Provider
|
||||
type CloudProvider string
|
||||
|
||||
const (
|
||||
alibaba CloudProvider = "alibaba"
|
||||
aws CloudProvider = "aws"
|
||||
gcp CloudProvider = "gcp"
|
||||
tencent CloudProvider = "tencent"
|
||||
azure CloudProvider = "azure"
|
||||
vsphere CloudProvider = "vsphere"
|
||||
ec CloudProvider = "ec"
|
||||
ucloud CloudProvider = "ucloud"
|
||||
custom CloudProvider = "custom"
|
||||
baidu CloudProvider = "baidu"
|
||||
huawei CloudProvider = "huawei"
|
||||
)
|
||||
|
||||
const (
|
||||
envAlicloudAcessKey = "ALICLOUD_ACCESS_KEY"
|
||||
envAlicloudSecretKey = "ALICLOUD_SECRET_KEY"
|
||||
envAlicloudRegion = "ALICLOUD_REGION"
|
||||
envAliCloudStsToken = "ALICLOUD_SECURITY_TOKEN"
|
||||
|
||||
errConvertCredentials = "failed to convert the credentials of Secret from Provider"
|
||||
errCredentialValid = "Credentials are not valid"
|
||||
ErrCredentialNotRetrieved = "Credentials are not retrieved from referenced Provider"
|
||||
)
|
||||
|
||||
// AlibabaCloudCredentials are credentials for Alibaba Cloud
|
||||
type AlibabaCloudCredentials struct {
|
||||
AccessKeyID string `yaml:"accessKeyID" json:"accessKeyID,omitempty"`
|
||||
AccessKeySecret string `yaml:"accessKeySecret" json:"accessKeySecret,omitempty"`
|
||||
SecurityToken string `yaml:"securityToken" json:"securityToken,omitempty"`
|
||||
}
|
||||
|
||||
// GetProviderCredentials gets provider credentials by cloud provider name
|
||||
func GetProviderCredentials(ctx context.Context, k8sClient client.Client, provider *v1beta1.Provider, region string) (map[string]string, error) {
|
||||
switch provider.Spec.Credentials.Source {
|
||||
case "Secret":
|
||||
var secret v1.Secret
|
||||
secretRef := provider.Spec.Credentials.SecretRef
|
||||
name := secretRef.Name
|
||||
namespace := secretRef.Namespace
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, &secret); err != nil {
|
||||
errMsg := "failed to get the Secret from Provider"
|
||||
klog.ErrorS(err, errMsg, "Name", name, "Namespace", namespace)
|
||||
return nil, errors.Wrap(err, errMsg)
|
||||
}
|
||||
secretData, ok := secret.Data[secretRef.Key]
|
||||
if !ok {
|
||||
return nil, errors.Errorf("in the provider %s, the key %s not found in the referenced secret %s", provider.Name, secretRef.Key, name)
|
||||
}
|
||||
switch provider.Spec.Provider {
|
||||
case string(alibaba):
|
||||
var ak AlibabaCloudCredentials
|
||||
if err := yaml.Unmarshal(secret.Data[secretRef.Key], &ak); err != nil {
|
||||
klog.ErrorS(err, errConvertCredentials, "Name", name, "Namespace", namespace)
|
||||
return nil, errors.Wrap(err, errConvertCredentials)
|
||||
}
|
||||
if err := checkAlibabaCloudCredentials(region, ak.AccessKeyID, ak.AccessKeySecret, ak.SecurityToken); err != nil {
|
||||
klog.ErrorS(err, errCredentialValid)
|
||||
return nil, errors.Wrap(err, errCredentialValid)
|
||||
}
|
||||
return map[string]string{
|
||||
envAlicloudAcessKey: ak.AccessKeyID,
|
||||
envAlicloudSecretKey: ak.AccessKeySecret,
|
||||
envAlicloudRegion: region,
|
||||
envAliCloudStsToken: ak.SecurityToken,
|
||||
}, nil
|
||||
case string(ucloud):
|
||||
return getUCloudCredentials(secretData, name, namespace, region)
|
||||
case string(aws):
|
||||
return getAWSCredentials(secretData, name, namespace, region)
|
||||
case string(gcp):
|
||||
return getGCPCredentials(secretData, name, namespace, region)
|
||||
case string(tencent):
|
||||
return getTencentCloudCredentials(secretData, name, namespace, region)
|
||||
case string(azure):
|
||||
return getAzureCredentials(secretData, name, namespace)
|
||||
case string(vsphere):
|
||||
return getVSphereCredentials(secretData, name, namespace)
|
||||
case string(ec):
|
||||
return getECCloudCredentials(secretData, name, namespace)
|
||||
case string(custom):
|
||||
return getCustomCredentials(secretData, name, namespace)
|
||||
case string(baidu):
|
||||
return getBaiduCloudCredentials(secretData, name, namespace, region)
|
||||
case string(huawei):
|
||||
return getHuaWeiCloudCredentials(secretData, name, namespace, region)
|
||||
default:
|
||||
errMsg := "unsupported provider"
|
||||
klog.InfoS(errMsg, "Provider", provider.Spec.Provider)
|
||||
return nil, errors.New(errMsg)
|
||||
}
|
||||
default:
|
||||
errMsg := "the credentials type is not supported."
|
||||
err := errors.New(errMsg)
|
||||
klog.ErrorS(err, "", "CredentialType", provider.Spec.Credentials.Source)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// GetProviderFromConfiguration gets provider object from Configuration
|
||||
// Returns:
|
||||
// 1) (nil, err): hit an issue to find the provider
|
||||
// 2) (nil, nil): provider not found
|
||||
// 3) (provider, nil): provider found
|
||||
func GetProviderFromConfiguration(ctx context.Context, k8sClient client.Client, namespace, name string) (*v1beta1.Provider, error) {
|
||||
var provider = &v1beta1.Provider{}
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, provider); err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
return nil, nil
|
||||
}
|
||||
errMsg := "failed to get Provider object"
|
||||
klog.ErrorS(err, errMsg, "Name", name)
|
||||
return nil, errors.Wrap(err, errMsg)
|
||||
}
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// checkAlibabaCloudProvider checks if the credentials from the provider are valid
|
||||
func checkAlibabaCloudCredentials(region string, accessKeyID, accessKeySecret, stsToken string) error {
|
||||
var (
|
||||
client *sts.Client
|
||||
err error
|
||||
)
|
||||
if stsToken != "" {
|
||||
client, err = sts.NewClientWithStsToken(region, accessKeyID, accessKeySecret, stsToken)
|
||||
} else {
|
||||
client, err = sts.NewClientWithAccessKey(region, accessKeyID, accessKeySecret)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request := sts.CreateGetCallerIdentityRequest()
|
||||
request.Scheme = "https"
|
||||
|
||||
_, err = client.GetCallerIdentity(request)
|
||||
if err != nil {
|
||||
errMsg := "Alibaba Cloud credentials are invalid"
|
||||
klog.ErrorS(err, errMsg)
|
||||
return errors.Wrap(err, errMsg)
|
||||
}
|
||||
return nil
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,19 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// CustomCredentials are credentials for custom (you self)
|
||||
type CustomCredentials map[string]string
|
||||
|
||||
func getCustomCredentials(secretData []byte, name, namespace string) (map[string]string, error) {
|
||||
var ck = make(CustomCredentials)
|
||||
if err := yaml.Unmarshal(secretData, &ck); err != nil {
|
||||
klog.ErrorS(err, errConvertCredentials, "Name", name, "Namespace", namespace)
|
||||
return nil, errors.Wrap(err, errConvertCredentials)
|
||||
}
|
||||
return ck, nil
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
envECApiKey = "EC_API_KEY"
|
||||
)
|
||||
|
||||
// ECCredentials are credentials for Elastic CLoud
|
||||
type ECCredentials struct {
|
||||
ECApiKey string `yaml:"ecApiKey"`
|
||||
}
|
||||
|
||||
func getECCloudCredentials(secretData []byte, name, namespace string) (map[string]string, error) {
|
||||
var ak ECCredentials
|
||||
if err := yaml.Unmarshal(secretData, &ak); err != nil {
|
||||
klog.ErrorS(err, errConvertCredentials, "Name", name, "Namespace", namespace)
|
||||
return nil, errors.Wrap(err, errConvertCredentials)
|
||||
}
|
||||
return map[string]string{
|
||||
envECApiKey: ak.ECApiKey,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
envGCPCredentialsJSON = "GOOGLE_CREDENTIALS"
|
||||
envGCPRegion = "GOOGLE_REGION"
|
||||
envGCPProject = "GOOGLE_PROJECT"
|
||||
)
|
||||
|
||||
// GCPCredentials are credentials for GCP
|
||||
type GCPCredentials struct {
|
||||
GCPCredentialsJSON string `yaml:"gcpCredentialsJSON"`
|
||||
GCPProject string `yaml:"gcpProject"`
|
||||
}
|
||||
|
||||
func getGCPCredentials(secretData []byte, name, namespace, region string) (map[string]string, error) {
|
||||
var ak GCPCredentials
|
||||
if err := yaml.Unmarshal(secretData, &ak); err != nil {
|
||||
klog.ErrorS(err, errConvertCredentials, "Name", name, "Namespace", namespace)
|
||||
return nil, errors.Wrap(err, errConvertCredentials)
|
||||
}
|
||||
return map[string]string{
|
||||
envGCPCredentialsJSON: ak.GCPCredentialsJSON,
|
||||
envGCPProject: ak.GCPProject,
|
||||
envGCPRegion: region,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
envHuaWeiCloudRegion = "HW_REGION_NAME"
|
||||
envHuaWeiCloudAccessKey = "HW_ACCESS_KEY"
|
||||
envHuaWeiCloudSecretKey = "HW_SECRET_KEY"
|
||||
)
|
||||
|
||||
// HuaWeiCloudCredentials are credentials for Huawei Cloud
|
||||
type HuaWeiCloudCredentials struct {
|
||||
AccessKey string `yaml:"accessKey"`
|
||||
SecretKey string `yaml:"secretKey"`
|
||||
}
|
||||
|
||||
func getHuaWeiCloudCredentials(secretData []byte, name, namespace, region string) (map[string]string, error) {
|
||||
var hwc HuaWeiCloudCredentials
|
||||
if err := yaml.Unmarshal(secretData, &hwc); err != nil {
|
||||
klog.ErrorS(err, errConvertCredentials, "Name", name, "Namespace", namespace)
|
||||
return nil, errors.Wrap(err, errConvertCredentials)
|
||||
}
|
||||
return map[string]string{
|
||||
envHuaWeiCloudAccessKey: hwc.AccessKey,
|
||||
envHuaWeiCloudSecretKey: hwc.SecretKey,
|
||||
envHuaWeiCloudRegion: region,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
envQCloudSecretID = "TENCENTCLOUD_SECRET_ID"
|
||||
envQCloudSecretKey = "TENCENTCLOUD_SECRET_KEY"
|
||||
envQCloudRegion = "TENCENTCLOUD_REGION"
|
||||
)
|
||||
|
||||
// TencentCloudCredentials are credentials for Tencent Cloud
|
||||
type TencentCloudCredentials struct {
|
||||
SecretID string `yaml:"secretID"`
|
||||
SecretKey string `yaml:"secretKey"`
|
||||
}
|
||||
|
||||
func getTencentCloudCredentials(secretData []byte, name, namespace, region string) (map[string]string, error) {
|
||||
var ak TencentCloudCredentials
|
||||
if err := yaml.Unmarshal(secretData, &ak); err != nil {
|
||||
klog.ErrorS(err, errConvertCredentials, "Name", name, "Namespace", namespace)
|
||||
return nil, errors.Wrap(err, errConvertCredentials)
|
||||
}
|
||||
return map[string]string{
|
||||
envQCloudSecretID: ak.SecretID,
|
||||
envQCloudSecretKey: ak.SecretKey,
|
||||
envQCloudRegion: region,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
envUCloudPrivateKey = "UCLOUD_PRIVATE_KEY"
|
||||
envUCloudProjectID = "UCLOUD_PROJECT_ID"
|
||||
envUCloudPublicKey = "UCLOUD_PUBLIC_KEY"
|
||||
envUCloudRegion = "UCLOUD_REGION"
|
||||
)
|
||||
|
||||
// UCloudCredentials are credentials for UCloud
|
||||
type UCloudCredentials struct {
|
||||
PublicKey string `yaml:"publicKey"`
|
||||
PrivateKey string `yaml:"privateKey"`
|
||||
Region string `yaml:"region"`
|
||||
ProjectID string `yaml:"projectID"`
|
||||
}
|
||||
|
||||
func getUCloudCredentials(secretData []byte, name, namespace, region string) (map[string]string, error) {
|
||||
var ak UCloudCredentials
|
||||
if err := yaml.Unmarshal(secretData, &ak); err != nil {
|
||||
klog.ErrorS(err, errConvertCredentials, "Name", name, "Namespace", namespace)
|
||||
return nil, errors.Wrap(err, errConvertCredentials)
|
||||
}
|
||||
return map[string]string{
|
||||
envUCloudPublicKey: ak.PublicKey,
|
||||
envUCloudPrivateKey: ak.PrivateKey,
|
||||
envUCloudRegion: region,
|
||||
envUCloudProjectID: ak.ProjectID,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetUCloudCredentials(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ns string
|
||||
secretData []byte
|
||||
region string
|
||||
wanted map[string]string
|
||||
}{
|
||||
{
|
||||
name: "ucloud1",
|
||||
ns: "qa",
|
||||
region: "cn-bj2",
|
||||
secretData: []byte("publicKey: xxxx1\nprivateKey: xxxx2\nregion: test1\nprojectID: test1"),
|
||||
wanted: map[string]string{
|
||||
envUCloudPrivateKey: "xxxx2",
|
||||
envUCloudProjectID: "test1",
|
||||
envUCloudPublicKey: "xxxx1",
|
||||
envUCloudRegion: "cn-bj2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ucloud1",
|
||||
ns: "qa",
|
||||
region: "",
|
||||
secretData: []byte("publicKey: xxxx1\nprivateKey: xxxx2\nregion: test1\nprojectID: test1"),
|
||||
wanted: map[string]string{
|
||||
envUCloudPrivateKey: "xxxx2",
|
||||
envUCloudProjectID: "test1",
|
||||
envUCloudPublicKey: "xxxx1",
|
||||
envUCloudRegion: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m, err := getUCloudCredentials(tt.secretData, tt.name, tt.ns, tt.region)
|
||||
assert.Nil(t, err)
|
||||
if !reflect.DeepEqual(tt.wanted, m) {
|
||||
t.Errorf("getUCloudCredentials got = %v, wanted %v", m, tt.wanted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
envVSphereUser = "VSPHERE_USER"
|
||||
envVSpherePassword = "VSPHERE_PASSWORD"
|
||||
envVSphereServer = "VSPHERE_SERVER"
|
||||
envVSphereAllowUnverifiedSSL = "VSPHERE_ALLOW_UNVERIFIED_SSL"
|
||||
)
|
||||
|
||||
// VSphereCredentials are credentials for VSphere
|
||||
type VSphereCredentials struct {
|
||||
VSphereUser string `yaml:"vSphereUser"`
|
||||
VSpherePassword string `yaml:"vSpherePassword"`
|
||||
VSphereServer string `yaml:"vSphereServer"`
|
||||
VSphereAllowUnverifiedSSL string `yaml:"vSphereAllowUnverifiedSSL,omitempty"`
|
||||
}
|
||||
|
||||
func getVSphereCredentials(secretData []byte, name, namespace string) (map[string]string, error) {
|
||||
var cred VSphereCredentials
|
||||
if err := yaml.Unmarshal(secretData, &cred); err != nil {
|
||||
klog.ErrorS(err, errConvertCredentials, "Name", name, "Namespace", namespace)
|
||||
return nil, errors.Wrap(err, errConvertCredentials)
|
||||
}
|
||||
return map[string]string{
|
||||
envVSphereUser: cred.VSphereUser,
|
||||
envVSpherePassword: cred.VSpherePassword,
|
||||
envVSphereServer: cred.VSphereServer,
|
||||
envVSphereAllowUnverifiedSSL: cred.VSphereAllowUnverifiedSSL,
|
||||
}, nil
|
||||
}
|
|
@ -18,14 +18,25 @@ package controllers
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
crossplanetypes "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime"
|
||||
"github.com/pkg/errors"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/klog/v2"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/types"
|
||||
terraformv1beta1 "github.com/oam-dev/terraform-controller/api/v1beta1"
|
||||
providercred "github.com/oam-dev/terraform-controller/controllers/provider"
|
||||
)
|
||||
|
||||
const (
|
||||
errGetCredentials = "failed to get credentials from the cloud provider"
|
||||
errSettingStatus = "failed to set status"
|
||||
)
|
||||
|
||||
// ProviderReconciler reconciles a Provider object
|
||||
|
@ -35,14 +46,15 @@ type ProviderReconciler struct {
|
|||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=terraform.core.oam.dev,resources=providerconfigs,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=terraform.core.oam.dev,resources=providerconfigs/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=terraform.core.oam.dev,resources=providers,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=terraform.core.oam.dev,resources=providers/status,verbs=get;update;patch
|
||||
|
||||
func (r *ProviderReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
var ctx = context.Background()
|
||||
_ = r.Log.WithValues("provider", req.NamespacedName)
|
||||
// Reconcile will reconcile periodically
|
||||
func (r *ProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
klog.InfoS("reconciling Terraform Provider...", "NamespacedName", req.NamespacedName)
|
||||
|
||||
var provider terraformv1beta1.Provider
|
||||
|
||||
if err := r.Get(ctx, req.NamespacedName, &provider); err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
err = nil
|
||||
|
@ -50,9 +62,39 @@ func (r *ProviderReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
|||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
err := func() error {
|
||||
switch provider.Spec.Credentials.Source {
|
||||
case crossplanetypes.CredentialsSourceInjectedIdentity:
|
||||
break
|
||||
case crossplanetypes.CredentialsSourceSecret:
|
||||
_, err := providercred.GetProviderCredentials(ctx, r.Client, &provider, provider.Spec.Region)
|
||||
return err
|
||||
default:
|
||||
return errors.Errorf("unsupported credentials source: %s", provider.Spec.Credentials.Source)
|
||||
}
|
||||
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
klog.ErrorS(err, errGetCredentials, "Provider", req.NamespacedName)
|
||||
|
||||
provider.Status.Message = fmt.Sprintf("%s: %s", errGetCredentials, err.Error())
|
||||
provider.Status = terraformv1beta1.ProviderStatus{State: types.ProviderIsNotReady}
|
||||
} else {
|
||||
provider.Status.Message = "Provider ready"
|
||||
provider.Status = terraformv1beta1.ProviderStatus{State: types.ProviderIsReady}
|
||||
}
|
||||
|
||||
if updateErr := r.Status().Update(ctx, &provider); updateErr != nil {
|
||||
klog.ErrorS(updateErr, errSettingStatus, "Provider", req.NamespacedName)
|
||||
|
||||
return ctrl.Result{}, errors.Wrap(updateErr, errSettingStatus)
|
||||
}
|
||||
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// SetupWithManager setups with a manager
|
||||
func (r *ProviderReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&terraformv1beta1.Provider{}).
|
||||
|
|
|
@ -0,0 +1,421 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/agiledragon/gomonkey/v2"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
crossplanetypes "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime"
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta1"
|
||||
"github.com/oam-dev/terraform-controller/controllers/provider"
|
||||
)
|
||||
|
||||
func TestReconcile(t *testing.T) {
|
||||
r1 := &ProviderReconciler{}
|
||||
ctx := context.Background()
|
||||
s := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(s)
|
||||
v1.AddToScheme(s)
|
||||
r1.Client = fake.NewClientBuilder().WithScheme(s).WithStatusSubresource(&v1beta1.Provider{}).Build()
|
||||
|
||||
r2 := &ProviderReconciler{}
|
||||
provider2 := &v1beta1.Provider{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "aws",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1beta1.ProviderSpec{
|
||||
Credentials: v1beta1.ProviderCredentials{
|
||||
Source: "Secret",
|
||||
SecretRef: &crossplanetypes.SecretKeySelector{
|
||||
SecretReference: crossplanetypes.SecretReference{
|
||||
Name: "abc",
|
||||
Namespace: "default",
|
||||
},
|
||||
Key: "credentials",
|
||||
},
|
||||
},
|
||||
Provider: "aws",
|
||||
},
|
||||
}
|
||||
|
||||
creds, _ := yaml.Marshal(&provider.AWSCredentials{
|
||||
AWSAccessKeyID: "a",
|
||||
AWSSecretAccessKey: "b",
|
||||
AWSSessionToken: "c",
|
||||
})
|
||||
secret2 := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "abc",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"credentials": creds,
|
||||
},
|
||||
Type: v1.SecretTypeOpaque,
|
||||
}
|
||||
|
||||
r2.Client = fake.NewClientBuilder().WithScheme(s).WithObjects(secret2, provider2).WithStatusSubresource(&v1beta1.Provider{}).Build()
|
||||
|
||||
r3 := &ProviderReconciler{}
|
||||
provider3 := &v1beta1.Provider{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "aws",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1beta1.ProviderSpec{
|
||||
Credentials: v1beta1.ProviderCredentials{
|
||||
Source: "Secret",
|
||||
SecretRef: &crossplanetypes.SecretKeySelector{
|
||||
SecretReference: crossplanetypes.SecretReference{
|
||||
Name: "abc",
|
||||
Namespace: "default",
|
||||
},
|
||||
Key: "credentials",
|
||||
},
|
||||
},
|
||||
Provider: "aws",
|
||||
},
|
||||
}
|
||||
|
||||
r3.Client = fake.NewClientBuilder().WithScheme(s).WithObjects(provider3).WithStatusSubresource(&v1beta1.Provider{}).Build()
|
||||
|
||||
r4 := &ProviderReconciler{}
|
||||
provider4 := &v1beta1.Provider{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "aws",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1beta1.ProviderSpec{
|
||||
Credentials: v1beta1.ProviderCredentials{Source: "InjectedIdentity"},
|
||||
Provider: "aws",
|
||||
},
|
||||
}
|
||||
r4.Client = fake.NewClientBuilder().WithScheme(s).WithObjects(provider4).WithStatusSubresource(&v1beta1.Provider{}).Build()
|
||||
|
||||
r5 := &ProviderReconciler{}
|
||||
provider5 := &v1beta1.Provider{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "aws",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1beta1.ProviderSpec{
|
||||
Credentials: v1beta1.ProviderCredentials{Source: "Invalid"},
|
||||
Provider: "aws",
|
||||
},
|
||||
}
|
||||
r5.Client = fake.NewClientBuilder().WithScheme(s).WithObjects(provider5).WithStatusSubresource(&v1beta1.Provider{}).Build()
|
||||
|
||||
type args struct {
|
||||
req reconcile.Request
|
||||
r *ProviderReconciler
|
||||
}
|
||||
|
||||
type want struct {
|
||||
errMsg string
|
||||
}
|
||||
|
||||
req := ctrl.Request{}
|
||||
req.NamespacedName = types.NamespacedName{
|
||||
Name: "aws",
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "Provider is not found",
|
||||
args: args{
|
||||
req: req,
|
||||
r: r1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Provider is found",
|
||||
args: args{
|
||||
req: req,
|
||||
r: r2,
|
||||
},
|
||||
want: want{},
|
||||
},
|
||||
{
|
||||
name: "Provider is found but the secret is not available",
|
||||
args: args{
|
||||
req: req,
|
||||
r: r3,
|
||||
},
|
||||
want: want{
|
||||
errMsg: `failed to get the Secret from Provider: secrets "abc" not found`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Provider is using source injected identity",
|
||||
args: args{
|
||||
req: req,
|
||||
r: r4,
|
||||
},
|
||||
want: want{},
|
||||
},
|
||||
{
|
||||
name: "Provider source is invalid",
|
||||
args: args{
|
||||
req: req,
|
||||
r: r5,
|
||||
},
|
||||
want: want{
|
||||
errMsg: `unsupported credentials source: Invalid`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := tc.args.r.Reconcile(ctx, tc.args.req)
|
||||
if tc.want.errMsg != "" && !strings.Contains(err.Error(), tc.want.errMsg) {
|
||||
t.Errorf("Reconcile() error = %v, wantErr %v", err, tc.want.errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileProviderIsReadyButFailedToUpdateStatus(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(s)
|
||||
v1.AddToScheme(s)
|
||||
|
||||
r2 := &ProviderReconciler{}
|
||||
provider2 := &v1beta1.Provider{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "aws",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1beta1.ProviderSpec{
|
||||
Credentials: v1beta1.ProviderCredentials{
|
||||
Source: "Secret",
|
||||
SecretRef: &crossplanetypes.SecretKeySelector{
|
||||
SecretReference: crossplanetypes.SecretReference{
|
||||
Name: "abc",
|
||||
Namespace: "default",
|
||||
},
|
||||
Key: "credentials",
|
||||
},
|
||||
},
|
||||
Provider: "aws",
|
||||
},
|
||||
}
|
||||
|
||||
creds, _ := yaml.Marshal(&provider.AWSCredentials{
|
||||
AWSAccessKeyID: "a",
|
||||
AWSSecretAccessKey: "b",
|
||||
AWSSessionToken: "c",
|
||||
})
|
||||
secret2 := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "abc",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"credentials": creds,
|
||||
},
|
||||
Type: v1.SecretTypeOpaque,
|
||||
}
|
||||
|
||||
r2.Client = fake.NewClientBuilder().WithScheme(s).WithObjects(secret2, provider2).WithStatusSubresource(&v1beta1.Provider{}).Build()
|
||||
|
||||
patches := ApplyFunc(apiutil.GVKForObject, func(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersionKind, error) {
|
||||
switch obj.(type) {
|
||||
case *v1beta1.Provider:
|
||||
p := obj.(*v1beta1.Provider)
|
||||
if p.Status.State != "" {
|
||||
return obj.GetObjectKind().GroupVersionKind(), errors.New("xxx")
|
||||
}
|
||||
}
|
||||
return apiutilGVKForObject(obj, scheme)
|
||||
})
|
||||
defer patches.Reset()
|
||||
|
||||
type args struct {
|
||||
req reconcile.Request
|
||||
r *ProviderReconciler
|
||||
}
|
||||
|
||||
type want struct {
|
||||
errMsg string
|
||||
}
|
||||
|
||||
req := ctrl.Request{}
|
||||
req.NamespacedName = types.NamespacedName{
|
||||
Name: "aws",
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "Provider is found",
|
||||
args: args{
|
||||
req: req,
|
||||
r: r2,
|
||||
},
|
||||
want: want{
|
||||
errMsg: "failed to set status",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if _, err := tc.args.r.Reconcile(ctx, tc.args.req); (tc.want.errMsg != "") &&
|
||||
!strings.Contains(err.Error(), tc.want.errMsg) {
|
||||
t.Errorf("Reconcile() error = %v, wantErr %v", err, tc.want.errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcile3(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(s)
|
||||
v1.AddToScheme(s)
|
||||
|
||||
r3 := &ProviderReconciler{}
|
||||
provider3 := &v1beta1.Provider{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "aws",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1beta1.ProviderSpec{
|
||||
Credentials: v1beta1.ProviderCredentials{
|
||||
Source: "Secret",
|
||||
SecretRef: &crossplanetypes.SecretKeySelector{
|
||||
SecretReference: crossplanetypes.SecretReference{
|
||||
Name: "abc",
|
||||
Namespace: "default",
|
||||
},
|
||||
Key: "credentials",
|
||||
},
|
||||
},
|
||||
Provider: errSettingStatus,
|
||||
},
|
||||
}
|
||||
|
||||
r3.Client = fake.NewClientBuilder().WithScheme(s).WithObjects(provider3).WithStatusSubresource(&v1beta1.Provider{}).Build()
|
||||
|
||||
patches := ApplyFunc(apiutil.GVKForObject, func(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersionKind, error) {
|
||||
switch obj.(type) {
|
||||
case *v1beta1.Provider:
|
||||
p := obj.(*v1beta1.Provider)
|
||||
if p.Status.State != "" {
|
||||
return obj.GetObjectKind().GroupVersionKind(), errors.New("xxx")
|
||||
}
|
||||
}
|
||||
return apiutilGVKForObject(obj, scheme)
|
||||
})
|
||||
defer patches.Reset()
|
||||
|
||||
type args struct {
|
||||
req reconcile.Request
|
||||
r *ProviderReconciler
|
||||
}
|
||||
|
||||
type want struct {
|
||||
errMsg string
|
||||
}
|
||||
|
||||
req := ctrl.Request{}
|
||||
req.NamespacedName = types.NamespacedName{
|
||||
Name: "aws",
|
||||
Namespace: "default",
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "Provider is found, but the secret is not available",
|
||||
args: args{
|
||||
req: req,
|
||||
r: r3,
|
||||
},
|
||||
want: want{
|
||||
errMsg: errSettingStatus,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if _, err := tc.args.r.Reconcile(ctx, tc.args.req); (tc.want.errMsg != "") &&
|
||||
!strings.Contains(err.Error(), tc.want.errMsg) {
|
||||
t.Errorf("Reconcile() error = %v, wantErr %v", err, tc.want.errMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func apiutilGVKForObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersionKind, error) {
|
||||
switch obj.(type) {
|
||||
case *v1beta1.Provider:
|
||||
p := obj.(*v1beta1.Provider)
|
||||
if p.Status.State != "" {
|
||||
return obj.GetObjectKind().GroupVersionKind(), errors.New("xxx")
|
||||
}
|
||||
}
|
||||
// a copy implementation of `apiutil.GVKForObject`
|
||||
_, isPartial := obj.(*metav1.PartialObjectMetadata) //nolint:ifshort
|
||||
_, isPartialList := obj.(*metav1.PartialObjectMetadataList)
|
||||
if isPartial || isPartialList {
|
||||
// we require that the GVK be populated in order to recognize the object
|
||||
gvk := obj.GetObjectKind().GroupVersionKind()
|
||||
if len(gvk.Kind) == 0 {
|
||||
return schema.GroupVersionKind{}, runtime.NewMissingKindErr("unstructured object has no kind")
|
||||
}
|
||||
if len(gvk.Version) == 0 {
|
||||
return schema.GroupVersionKind{}, runtime.NewMissingVersionErr("unstructured object has no version")
|
||||
}
|
||||
return gvk, nil
|
||||
}
|
||||
|
||||
gvks, isUnversioned, err := scheme.ObjectKinds(obj)
|
||||
if err != nil {
|
||||
return schema.GroupVersionKind{}, err
|
||||
}
|
||||
if isUnversioned {
|
||||
return schema.GroupVersionKind{}, fmt.Errorf("cannot create group-version-kind for unversioned type %T", obj)
|
||||
}
|
||||
|
||||
if len(gvks) < 1 {
|
||||
return schema.GroupVersionKind{}, fmt.Errorf("no group-version-kinds associated with type %T", obj)
|
||||
}
|
||||
if len(gvks) > 1 {
|
||||
// this should only trigger for things like metav1.XYZ --
|
||||
// normal versioned types should be fine
|
||||
return schema.GroupVersionKind{}, fmt.Errorf(
|
||||
"multiple group-version-kinds associated with type %T, refusing to guess at one", obj)
|
||||
}
|
||||
return gvks[0], nil
|
||||
}
|
|
@ -18,15 +18,17 @@ package controllers
|
|||
|
||||
import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
. "github.com/agiledragon/gomonkey/v2"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
|
||||
|
||||
// +kubebuilder:scaffold:imports
|
||||
|
||||
terraformv1beta1 "github.com/oam-dev/terraform-controller/api/v1beta1"
|
||||
|
@ -39,15 +41,23 @@ var cfg *rest.Config
|
|||
var k8sClient client.Client
|
||||
var testEnv *envtest.Environment
|
||||
|
||||
func init() {
|
||||
patches := NewPatches()
|
||||
patches.ApplyMethod(reflect.TypeOf(&envtest.Environment{}), "Start", func(_ *envtest.Environment) (*rest.Config, error) {
|
||||
return &rest.Config{}, nil
|
||||
})
|
||||
patches.ApplyMethod(reflect.TypeOf(&envtest.Environment{}), "Stop", func(_ *envtest.Environment) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIs(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
RunSpecsWithDefaultAndCustomReporters(t,
|
||||
"Controller Suite",
|
||||
[]Reporter{printer.NewlineReporter{}})
|
||||
RunSpecs(t, "Controller Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func(done Done) {
|
||||
var _ = BeforeSuite(func() {
|
||||
By("bootstrapping test environment")
|
||||
testEnv = &envtest.Environment{
|
||||
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
|
||||
|
@ -69,8 +79,6 @@ var _ = BeforeSuite(func(done Done) {
|
|||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(k8sClient).ToNot(BeNil())
|
||||
|
||||
close(done)
|
||||
}, 60)
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/types"
|
||||
)
|
||||
|
||||
func getPods(ctx context.Context, client kubernetes.Interface, namespace, jobName string) (*v1.PodList, error) {
|
||||
label := fmt.Sprintf("job-name=%s", jobName)
|
||||
pods, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{LabelSelector: label})
|
||||
if err != nil {
|
||||
klog.InfoS("pods are not found", "Label", label, "Error", err)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pods, nil
|
||||
}
|
||||
|
||||
func getPodLog(ctx context.Context, client kubernetes.Interface, namespace, jobName, containerName, initContainerName string) (types.Stage, string, error) {
|
||||
var (
|
||||
targetContainer = containerName
|
||||
stage = types.ApplyStage
|
||||
)
|
||||
pods, err := getPods(ctx, client, namespace, jobName)
|
||||
if err != nil || pods == nil || len(pods.Items) == 0 {
|
||||
klog.V(4).InfoS("pods are not found", "PodName", jobName, "Namepspace", namespace, "Error", err)
|
||||
return stage, "", nil
|
||||
}
|
||||
pod := pods.Items[0]
|
||||
|
||||
// Here are two cases for Pending phase: 1) init container `terraform init` is not finished yet, 2) pod is not ready yet.
|
||||
if pod.Status.Phase == v1.PodPending {
|
||||
for _, c := range pod.Status.InitContainerStatuses {
|
||||
if c.Name == initContainerName && !c.Ready {
|
||||
targetContainer = initContainerName
|
||||
stage = types.InitStage
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req := client.CoreV1().Pods(namespace).GetLogs(pod.Name, &v1.PodLogOptions{Container: targetContainer})
|
||||
logs, err := req.Stream(ctx)
|
||||
if err != nil {
|
||||
return stage, "", err
|
||||
}
|
||||
defer func(logs io.ReadCloser) {
|
||||
err := logs.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(logs)
|
||||
|
||||
log, err := flushStream(logs, pod.Name)
|
||||
if err != nil {
|
||||
return stage, "", err
|
||||
}
|
||||
|
||||
// To learn how it works, please refer to https://github.com/zzxwill/terraform-log-stripper.
|
||||
strippedLog := stripColor(log)
|
||||
return stage, strippedLog, nil
|
||||
}
|
||||
|
||||
func flushStream(rc io.ReadCloser, podName string) (string, error) {
|
||||
var buf = &bytes.Buffer{}
|
||||
_, err := io.Copy(buf, rc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
logContent := buf.String()
|
||||
klog.V(4).Info("pod logs", "Pod", podName, "Logs", logContent)
|
||||
return logContent, nil
|
||||
}
|
||||
|
||||
func stripColor(log string) string {
|
||||
var re = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||||
str := re.ReplaceAllString(log, "")
|
||||
return str
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/agiledragon/gomonkey/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
fakeclient "k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/kubernetes/typed/core/v1/fake"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/types"
|
||||
)
|
||||
|
||||
func TestGetPodLog(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
type args struct {
|
||||
client kubernetes.Interface
|
||||
namespace string
|
||||
name string
|
||||
containerName string
|
||||
initContainerName string
|
||||
}
|
||||
type want struct {
|
||||
state types.Stage
|
||||
log string
|
||||
errMsg string
|
||||
}
|
||||
|
||||
pod := &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "p1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"job-name": "j1",
|
||||
},
|
||||
},
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Pod",
|
||||
},
|
||||
Status: v1.PodStatus{
|
||||
Phase: v1.PodRunning,
|
||||
},
|
||||
}
|
||||
|
||||
k8sClientSet := fakeclient.NewSimpleClientset(pod)
|
||||
|
||||
patches := gomonkey.ApplyMethod(reflect.TypeOf(&fake.FakePods{}), "GetLogs",
|
||||
func(_ *fake.FakePods, _ string, _ *v1.PodLogOptions) *rest.Request {
|
||||
rate := flowcontrol.NewFakeNeverRateLimiter()
|
||||
restClient, _ := rest.NewRESTClient(
|
||||
&url.URL{
|
||||
Scheme: "http",
|
||||
Host: "",
|
||||
},
|
||||
"",
|
||||
rest.ClientContentConfig{},
|
||||
rate,
|
||||
http.DefaultClient)
|
||||
r := rest.NewRequest(restClient)
|
||||
r.Body([]byte("xxx"))
|
||||
return r
|
||||
})
|
||||
defer patches.Reset()
|
||||
|
||||
var testcases = []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "Pod is available, but no logs",
|
||||
args: args{
|
||||
client: k8sClientSet,
|
||||
namespace: "default",
|
||||
name: "j1",
|
||||
containerName: "terraform-executor",
|
||||
initContainerName: "terraform-init",
|
||||
},
|
||||
want: want{
|
||||
errMsg: "client rate limiter Wait returned an error: can not be accept",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
state, got, err := getPodLog(ctx, tc.args.client, tc.args.namespace, tc.args.name, tc.args.containerName, tc.args.initContainerName)
|
||||
if tc.want.errMsg != "" || err != nil {
|
||||
assert.EqualError(t, err, tc.want.errMsg)
|
||||
} else {
|
||||
assert.Equal(t, tc.want.log, got)
|
||||
assert.Equal(t, tc.want.state, state)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlushStream(t *testing.T) {
|
||||
type args struct {
|
||||
rc io.ReadCloser
|
||||
name string
|
||||
}
|
||||
type want struct {
|
||||
errMsg string
|
||||
}
|
||||
|
||||
var testcases = []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "Flush stream",
|
||||
args: args{
|
||||
rc: ioutil.NopCloser(strings.NewReader("xxx")),
|
||||
name: "p1",
|
||||
},
|
||||
want: want{},
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
logs, err := flushStream(tc.args.rc, tc.args.name)
|
||||
if tc.want.errMsg != "" {
|
||||
assert.Contains(t, err.Error(), tc.want.errMsg)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "xxx", logs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripColor(t *testing.T) {
|
||||
type args struct {
|
||||
log string
|
||||
}
|
||||
type want struct {
|
||||
newLog string
|
||||
}
|
||||
|
||||
var testcases = map[string]struct {
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
"without color": {
|
||||
args: args{
|
||||
log: "abc",
|
||||
},
|
||||
want: want{
|
||||
newLog: "abc",
|
||||
},
|
||||
},
|
||||
"with color": {
|
||||
args: args{
|
||||
log: `[1mFailed`,
|
||||
},
|
||||
want: want{
|
||||
newLog: "Failed",
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, tc := range testcases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := stripColor(tc.args.log)
|
||||
assert.Equal(t, tc.want.newLog, got)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/types"
|
||||
"github.com/oam-dev/terraform-controller/controllers/client"
|
||||
)
|
||||
|
||||
// GetTerraformStatus will get Terraform execution status
|
||||
func GetTerraformStatus(ctx context.Context, jobNamespace, jobName, containerName, initContainerName string) (types.ConfigurationState, error) {
|
||||
klog.InfoS("checking Terraform init and execution status", "Namespace", jobNamespace, "Job", jobName)
|
||||
clientSet, err := client.Init()
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "failed to init clientSet")
|
||||
return types.ConfigurationProvisioningAndChecking, err
|
||||
}
|
||||
|
||||
// check the stage of the pod
|
||||
stage, logs, err := getPodLog(ctx, clientSet, jobNamespace, jobName, containerName, initContainerName)
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "failed to get pod logs")
|
||||
return types.ConfigurationProvisioningAndChecking, err
|
||||
}
|
||||
|
||||
success, state, errMsg := analyzeTerraformLog(logs, stage)
|
||||
if success {
|
||||
return state, nil
|
||||
}
|
||||
|
||||
return state, errors.New(errMsg)
|
||||
}
|
||||
|
||||
// analyzeTerraformLog will analyze the logs of Terraform apply pod, returns true if check is ok.
|
||||
func analyzeTerraformLog(logs string, stage types.Stage) (bool, types.ConfigurationState, string) {
|
||||
lines := strings.Split(logs, "\n")
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "31mError:") {
|
||||
errMsg := strings.Join(lines[i:], "\n")
|
||||
if strings.Contains(errMsg, "Invalid Alibaba Cloud region") {
|
||||
return false, types.InvalidRegion, errMsg
|
||||
}
|
||||
switch stage {
|
||||
case types.InitStage:
|
||||
return false, types.TerraformInitError, errMsg
|
||||
case types.ApplyStage:
|
||||
return false, types.ConfigurationApplyFailed, errMsg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true, types.ConfigurationProvisioningAndChecking, ""
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/agiledragon/gomonkey/v2"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/types"
|
||||
)
|
||||
|
||||
func TestGetTerraformStatus(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
type args struct {
|
||||
namespace string
|
||||
name string
|
||||
containerName string
|
||||
}
|
||||
type want struct {
|
||||
state types.ConfigurationState
|
||||
errMsg string
|
||||
}
|
||||
|
||||
gomonkey.ApplyFunc(config.GetConfigWithContext, func(context string) (*rest.Config, error) {
|
||||
return &rest.Config{}, nil
|
||||
})
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "logs are not available",
|
||||
args: args{
|
||||
namespace: "default",
|
||||
name: "test",
|
||||
containerName: "terraform-executor",
|
||||
},
|
||||
want: want{
|
||||
state: types.ConfigurationProvisioningAndChecking,
|
||||
errMsg: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
state, err := GetTerraformStatus(ctx, tc.args.name, tc.args.namespace, tc.args.containerName, "")
|
||||
if tc.want.errMsg != "" {
|
||||
assert.EqualError(t, err, tc.want.errMsg)
|
||||
} else {
|
||||
assert.Equal(t, tc.want.state, state)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTerraformStatus2(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
type args struct {
|
||||
namespace string
|
||||
name string
|
||||
containerName string
|
||||
}
|
||||
type want struct {
|
||||
state types.ConfigurationState
|
||||
errMsg string
|
||||
}
|
||||
|
||||
gomonkey.ApplyFunc(config.GetConfigWithContext, func(context string) (*rest.Config, error) {
|
||||
return nil, errors.New("failed to init clientSet")
|
||||
})
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "failed to init clientSet",
|
||||
args: args{},
|
||||
want: want{
|
||||
state: types.ConfigurationProvisioningAndChecking,
|
||||
errMsg: "failed to init clientSet",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
state, err := GetTerraformStatus(ctx, tc.args.name, tc.args.namespace, tc.args.containerName, "")
|
||||
if tc.want.errMsg != "" {
|
||||
assert.Contains(t, err.Error(), tc.want.errMsg)
|
||||
} else {
|
||||
assert.Equal(t, tc.want.state, state)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeTerraformLog(t *testing.T) {
|
||||
type args struct {
|
||||
logs string
|
||||
}
|
||||
type want struct {
|
||||
success bool
|
||||
state types.ConfigurationState
|
||||
errMsg string
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "normal failed logs",
|
||||
args: args{
|
||||
logs: "31mError:",
|
||||
},
|
||||
want: want{
|
||||
success: false,
|
||||
state: types.ConfigurationApplyFailed,
|
||||
errMsg: "31mError:",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid region",
|
||||
args: args{
|
||||
logs: "31mError:\nInvalid Alibaba Cloud region",
|
||||
},
|
||||
want: want{
|
||||
success: false,
|
||||
state: types.InvalidRegion,
|
||||
errMsg: "Invalid Alibaba Cloud region",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
success, state, errMsg := analyzeTerraformLog(tc.args.logs, types.ApplyStage)
|
||||
if tc.want.errMsg != "" {
|
||||
assert.Contains(t, errMsg, tc.want.errMsg)
|
||||
} else {
|
||||
assert.Equal(t, tc.want.success, success)
|
||||
assert.Equal(t, tc.want.state, state)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
)
|
||||
|
||||
// DecompressTerraformStateSecret decompress the data of Terraform backend state secret
|
||||
// Modified based on Hashicorp code base https://github.com/hashicorp/terraform/blob/fabdf0bea1fa2bf6a9d56cc3ea0f28242bf5e812/backend/remote-state/kubernetes/client.go#L355
|
||||
// Licensed under Mozilla Public License 2.0
|
||||
func DecompressTerraformStateSecret(data string) ([]byte, error) {
|
||||
b := new(bytes.Buffer)
|
||||
gz, err := gzip.NewReader(bytes.NewReader([]byte(data)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := b.ReadFrom(gz); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := gz.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDecompressTerraformStateSecret(t *testing.T) {
|
||||
type args struct {
|
||||
data string
|
||||
needDecode bool
|
||||
}
|
||||
type want struct {
|
||||
raw string
|
||||
errMsg string
|
||||
}
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "decompress terraform state secret",
|
||||
args: args{
|
||||
data: "H4sIAAAAAAAA/0SMwa7CIBBF9/0KMutH80ArDb9ijKHDYEhqMQO4afrvBly4POfc3H0QAt7EOaYNrDj/NS7E7ELi5/1XQI3/o4beM3F0K1ihO65xI/egNsLThLPRWi6agkR/CVIppaSZJrfgbBx6//1ItbxqyWDFfnTBlFNlpKaut+EYPgEAAP//xUXpvZsAAAA=",
|
||||
needDecode: true,
|
||||
},
|
||||
want: want{
|
||||
raw: `{
|
||||
"version": 4,
|
||||
"terraform_version": "1.0.2",
|
||||
"serial": 2,
|
||||
"lineage": "c35c8722-b2ef-cd6f-1111-755abc87acdd",
|
||||
"outputs": {},
|
||||
"resources": []
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bad data",
|
||||
args: args{
|
||||
data: "abc",
|
||||
},
|
||||
want: want{
|
||||
errMsg: "EOF",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testcases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.args.needDecode {
|
||||
state, err := base64.StdEncoding.DecodeString(tt.args.data)
|
||||
assert.NoError(t, err)
|
||||
tt.args.data = string(state)
|
||||
}
|
||||
got, err := DecompressTerraformStateSecret(tt.args.data)
|
||||
if tt.want.errMsg != "" || err != nil {
|
||||
assert.Contains(t, err.Error(), tt.want.errMsg)
|
||||
} else {
|
||||
assert.Equal(t, tt.want.raw, string(got))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/pkg/errors"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta1"
|
||||
)
|
||||
|
||||
const (
|
||||
// ProviderName is the name of Provider object
|
||||
ProviderName = "default"
|
||||
// ProviderNamespace is the namespace of Provider object
|
||||
ProviderNamespace = "default"
|
||||
)
|
||||
|
||||
type CloudProvider string
|
||||
|
||||
const (
|
||||
Alibaba CloudProvider = "alibaba"
|
||||
AWS CloudProvider = "aws"
|
||||
)
|
||||
|
||||
const (
|
||||
EnvAlicloudAcessKey = "ALICLOUD_ACCESS_KEY"
|
||||
EnvAlicloudSecretKey = "ALICLOUD_SECRET_KEY"
|
||||
EnvAlicloudRegion = "ALICLOUD_REGION"
|
||||
|
||||
EnvAWSAccessKeyID = "AWS_ACCESS_KEY_ID"
|
||||
EnvAWSSecretAccessKey = "AWS_SECRET_ACCESS_KEY"
|
||||
EnvAWSDefaultRegion = "AWS_DEFAULT_REGION"
|
||||
)
|
||||
|
||||
type AlibabaCloudCredentials struct {
|
||||
AccessKeyID string `yaml:"accessKeyID"`
|
||||
AccessKeySecret string `yaml:"accessKeySecret"`
|
||||
}
|
||||
|
||||
type AWSCredentials struct {
|
||||
AWSAccessKeyID string `yaml:"awsAccessKeyID"`
|
||||
AWSSecretAccessKey string `yaml:"awsSecretAccessKey"`
|
||||
}
|
||||
|
||||
func GetProviderCredentials(ctx context.Context, k8sClient client.Client) (map[string]string, error) {
|
||||
var provider v1beta1.Provider
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: ProviderName, Namespace: ProviderNamespace}, &provider); err != nil {
|
||||
errMsg := "failed to get Provider object"
|
||||
klog.ErrorS(err, errMsg, "Name", ProviderName)
|
||||
return nil, errors.Wrap(err, errMsg)
|
||||
}
|
||||
|
||||
region := provider.Spec.Region
|
||||
switch provider.Spec.Credentials.Source {
|
||||
case "Secret":
|
||||
var secret v1.Secret
|
||||
secretRef := provider.Spec.Credentials.SecretRef
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: secretRef.Name, Namespace: secretRef.Namespace}, &secret); err != nil {
|
||||
errMsg := "failed to get the Secret from Provider"
|
||||
klog.ErrorS(err, errMsg, "Name", secretRef.Name, "Namespace", secretRef.Namespace)
|
||||
return nil, errors.Wrap(err, errMsg)
|
||||
}
|
||||
switch provider.Spec.Provider {
|
||||
case string(Alibaba):
|
||||
var ak AlibabaCloudCredentials
|
||||
if err := yaml.Unmarshal(secret.Data[secretRef.Key], &ak); err != nil {
|
||||
errMsg := "failed to convert the credentials of Secret from Provider"
|
||||
klog.ErrorS(err, errMsg, "Name", secretRef.Name, "Namespace", secretRef.Namespace)
|
||||
return nil, errors.Wrap(err, errMsg)
|
||||
}
|
||||
return map[string]string{
|
||||
EnvAlicloudAcessKey: ak.AccessKeyID,
|
||||
EnvAlicloudSecretKey: ak.AccessKeySecret,
|
||||
EnvAlicloudRegion: region,
|
||||
}, nil
|
||||
case string(AWS):
|
||||
var ak AWSCredentials
|
||||
if err := yaml.Unmarshal(secret.Data[secretRef.Key], &ak); err != nil {
|
||||
errMsg := "failed to convert the credentials of Secret from Provider"
|
||||
klog.ErrorS(err, errMsg, "Name", secretRef.Name, "Namespace", secretRef.Namespace)
|
||||
return nil, errors.Wrap(err, errMsg)
|
||||
}
|
||||
return map[string]string{
|
||||
EnvAWSAccessKeyID: ak.AWSAccessKeyID,
|
||||
EnvAWSSecretAccessKey: ak.AWSSecretAccessKey,
|
||||
EnvAWSDefaultRegion: region,
|
||||
}, nil
|
||||
|
||||
}
|
||||
default:
|
||||
errMsg := "the credentials type is not supported."
|
||||
err := errors.New(errMsg)
|
||||
klog.ErrorS(err, "", "CredentialType", provider.Spec.Credentials.Source)
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
func CreateTerraformExecutorClusterRole(ctx context.Context, k8sClient client.Client, clusterRoleName string) error {
|
||||
var clusterRole = rbacv1.ClusterRole{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "ClusterRole",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterRoleName,
|
||||
},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"secrets"},
|
||||
Verbs: []string{"get", "list", "create", "update", "delete"},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{"coordination.k8s.io"},
|
||||
Resources: []string{"leases"},
|
||||
Verbs: []string{"get", "create", "update", "delete"},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: clusterRoleName}, &rbacv1.ClusterRole{}); err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
if err := k8sClient.Create(ctx, &clusterRole); err != nil {
|
||||
return errors.Wrap(err, "failed to create ClusterRole for Terraform executor")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateTerraformExecutorClusterRoleBinding(ctx context.Context, k8sClient client.Client, namespace, clusterRoleName, serviceAccountName string) error {
|
||||
var crbName = fmt.Sprintf("%s-tf-executor-clusterrole-binding", namespace)
|
||||
var clusterRoleBinding = rbacv1.ClusterRoleBinding{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "ClusterRoleBinding",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: crbName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: clusterRoleName,
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: "ServiceAccount",
|
||||
Name: serviceAccountName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: crbName}, &rbacv1.ClusterRoleBinding{}); err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
if err := k8sClient.Create(ctx, &clusterRoleBinding); err != nil {
|
||||
return errors.Wrap(err, "failed to create ClusterRoleBinding for Terraform executor")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
)
|
||||
|
||||
var (
|
||||
env envtest.Environment
|
||||
k8sClient client.Client
|
||||
)
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
env = envtest.Environment{}
|
||||
cfg, err := env.Start()
|
||||
Expect(err).To(BeNil())
|
||||
Expect(cfg).ToNot(BeNil())
|
||||
k8sClient, err = client.New(cfg, client.Options{})
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
err := env.Stop()
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
|
||||
var _ = Describe("Utils", func() {
|
||||
roleName := "default-tf-executor-clusterrole"
|
||||
It("CreateTerraformExecutorClusterRole", func() {
|
||||
err := CreateTerraformExecutorClusterRole(context.TODO(), k8sClient, roleName)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
// Get and examine the role
|
||||
role := &rbacv1.ClusterRole{}
|
||||
err = k8sClient.Get(context.TODO(), client.ObjectKey{
|
||||
Name: roleName,
|
||||
}, role)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(len(role.Rules)).To(Equal(2))
|
||||
Expect(role.Rules[0].Resources).To(Equal([]string{"secrets"}))
|
||||
Expect(role.Rules[0].Verbs).To(Equal([]string{"get", "list", "create", "update", "delete"}))
|
||||
Expect(role.Rules[1].Resources).To(Equal([]string{"leases"}))
|
||||
Expect(role.Rules[1].Verbs).To(Equal([]string{"get", "create", "update", "delete"}))
|
||||
})
|
||||
|
||||
It("CreateTerraformExecutorClusterRoleBinding", func() {
|
||||
err := CreateTerraformExecutorClusterRoleBinding(context.TODO(), k8sClient, "default", roleName, "tf-executor-service-account")
|
||||
Expect(err).To(BeNil())
|
||||
})
|
||||
|
||||
})
|
|
@ -0,0 +1,53 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
. "github.com/agiledragon/gomonkey/v2"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
)
|
||||
|
||||
func init() {
|
||||
patches := ApplyMethod(reflect.TypeOf(&envtest.Environment{}), "Start", func(_ *envtest.Environment) (*rest.Config, error) {
|
||||
return &rest.Config{}, nil
|
||||
})
|
||||
patches.ApplyMethod(reflect.TypeOf(&envtest.Environment{}), "Stop", func(_ *envtest.Environment) error {
|
||||
return nil
|
||||
})
|
||||
patches.ApplyFunc(CreateTerraformExecutorClusterRole, func(ctx context.Context, c client.Client, name string) error {
|
||||
role := &rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: name},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
{Resources: []string{"secrets"}, Verbs: []string{"get", "list", "create", "update", "delete"}},
|
||||
{APIGroups: []string{"coordination.k8s.io"}, Resources: []string{"leases"}, Verbs: []string{"get", "create", "update", "delete"}},
|
||||
},
|
||||
}
|
||||
return c.Create(ctx, role)
|
||||
})
|
||||
patches.ApplyFunc(CreateTerraformExecutorClusterRoleBinding, func(ctx context.Context, c client.Client, namespace, clusterRoleName, serviceAccountName string) error {
|
||||
crb := &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("%s-tf-executor-clusterrole-binding", namespace), Namespace: namespace},
|
||||
RoleRef: rbacv1.RoleRef{APIGroup: "rbac.authorization.k8s.io", Kind: "ClusterRole", Name: clusterRoleName},
|
||||
Subjects: []rbacv1.Subject{{Kind: "ServiceAccount", Name: serviceAccountName, Namespace: namespace}},
|
||||
}
|
||||
return c.Create(ctx, crb)
|
||||
})
|
||||
patches.ApplyFunc(client.New, func(_ *rest.Config, _ client.Options) (client.Client, error) {
|
||||
return fake.NewClientBuilder().Build(), nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestUtils(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Utils Suite")
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta1"
|
||||
)
|
||||
|
||||
type ConfigurationType string
|
||||
|
||||
const (
|
||||
ConfigurationJSON ConfigurationType = "JSON"
|
||||
ConfigurationHCL ConfigurationType = "HCL"
|
||||
)
|
||||
|
||||
func ValidConfiguration(configuration v1beta1.Configuration) (ConfigurationType, string, error) {
|
||||
json := configuration.Spec.JSON
|
||||
hcl := configuration.Spec.HCL
|
||||
switch {
|
||||
case json == "" && hcl == "":
|
||||
return "", "", errors.New("spec.JSON or spec.HCL should be set")
|
||||
case json != "" && hcl != "":
|
||||
return "", "", errors.New("spec.JSON and spec.HCL cloud not be set at the same time")
|
||||
case json != "":
|
||||
return ConfigurationJSON, json, nil
|
||||
case hcl != "":
|
||||
return ConfigurationHCL, hcl, nil
|
||||
}
|
||||
return "", "", errors.New("unknown issue")
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 578 KiB After Width: | Height: | Size: 660 KiB |
|
@ -0,0 +1,127 @@
|
|||
package controllernamespace
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
types2 "github.com/oam-dev/terraform-controller/api/types"
|
||||
|
||||
crossplane "github.com/oam-dev/terraform-controller/api/types/crossplane-runtime"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/pkg/errors"
|
||||
appv1 "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
pkgClient "sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta2"
|
||||
)
|
||||
|
||||
var _ = Describe("Restart with controller-namespace", func() {
|
||||
const (
|
||||
defaultNamespace = "default"
|
||||
controllerNamespace = "terraform"
|
||||
chartNamespace = "terraform"
|
||||
)
|
||||
var (
|
||||
controllerDeployMeta = types.NamespacedName{Name: "terraform-controller", Namespace: chartNamespace}
|
||||
)
|
||||
ctx := context.Background()
|
||||
|
||||
// create k8s rest config
|
||||
restConf, err := config.GetConfig()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
k8sClient, err := pkgClient.New(restConf, pkgClient.Options{})
|
||||
s := k8sClient.Scheme()
|
||||
_ = v1beta2.AddToScheme(s)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
configuration := &v1beta2.Configuration{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "e2e-for-ctrl-ns",
|
||||
Namespace: defaultNamespace,
|
||||
},
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
HCL: `
|
||||
resource "random_id" "server" {
|
||||
byte_length = 8
|
||||
}
|
||||
|
||||
output "random_id" {
|
||||
value = random_id.server.hex
|
||||
}`,
|
||||
InlineCredentials: true,
|
||||
WriteConnectionSecretToReference: &crossplane.SecretReference{
|
||||
Name: "some-conn",
|
||||
Namespace: defaultNamespace,
|
||||
},
|
||||
},
|
||||
}
|
||||
AfterEach(func() {
|
||||
_ = k8sClient.Delete(ctx, configuration)
|
||||
})
|
||||
It("Restart with controller namespace", func() {
|
||||
By("apply configuration without --controller-namespace", func() {
|
||||
err = k8sClient.Create(ctx, configuration)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
var cfg = &v1beta2.Configuration{}
|
||||
Eventually(func() error {
|
||||
err = k8sClient.Get(ctx, types.NamespacedName{Name: configuration.Name, Namespace: configuration.Namespace}, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.Status.Apply.State != types2.Available {
|
||||
return errors.Errorf("configuration is not available, status now: %s", cfg.Status.Apply.State)
|
||||
}
|
||||
return nil
|
||||
}, time.Second*60, time.Second*5).Should(Succeed())
|
||||
})
|
||||
By("restart controller with --controller-namespace", func() {
|
||||
ctrlDeploy := appv1.Deployment{}
|
||||
err = k8sClient.Get(ctx, controllerDeployMeta, &ctrlDeploy)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
ctrlDeploy.Spec.Template.Spec.Containers[0].Args = append(ctrlDeploy.Spec.Template.Spec.Containers[0].Args, "--controller-namespace="+controllerNamespace)
|
||||
err := k8sClient.Update(ctx, &ctrlDeploy)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
Eventually(func() error {
|
||||
err := k8sClient.Get(ctx, controllerDeployMeta, &ctrlDeploy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ctrlDeploy.Status.UnavailableReplicas == 1 {
|
||||
return errors.New("controller is not updated")
|
||||
}
|
||||
return nil
|
||||
}, time.Second*60, time.Second*5).Should(Succeed())
|
||||
|
||||
})
|
||||
By("configuration should be still available", func() {
|
||||
// wait about half minute to check configuration's state isn't changed
|
||||
for i := 0; i < 30; i++ {
|
||||
err := k8sClient.Get(ctx, types.NamespacedName{
|
||||
Name: configuration.Name, Namespace: configuration.Namespace,
|
||||
}, configuration)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
})
|
||||
By("restore controller", func() {
|
||||
ctrlDeploy := appv1.Deployment{}
|
||||
err = k8sClient.Get(ctx, controllerDeployMeta, &ctrlDeploy)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
cmds := make([]string, 0)
|
||||
for _, cmd := range ctrlDeploy.Spec.Template.Spec.Containers[0].Args {
|
||||
if !strings.HasPrefix(cmd, "--controller-namespace") {
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
}
|
||||
ctrlDeploy.Spec.Template.Spec.Containers[0].Args = cmds
|
||||
err := k8sClient.Update(ctx, &ctrlDeploy)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,14 @@
|
|||
package controllernamespace_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestE2e(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
defer GinkgoRecover()
|
||||
RunSpecs(t, "E2e Suite")
|
||||
}
|
|
@ -0,0 +1,459 @@
|
|||
package normal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"gotest.tools/assert"
|
||||
coreV1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/controllers/client"
|
||||
)
|
||||
|
||||
var (
|
||||
testConfigurationsInlineCredentials = "examples/random/configuration_random.yaml"
|
||||
testConfigurationsInlineCredentialsCustomBackendKubernetes = "examples/random/configuration_random_custom_backend_kubernetes.yaml"
|
||||
testConfigurationsRegression = []string{
|
||||
"examples/alibaba/eip/configuration_eip.yaml",
|
||||
"examples/alibaba/eip/configuration_eip_remote_in_another_namespace.yaml",
|
||||
"examples/alibaba/eip/configuration_eip_remote_subdirectory.yaml",
|
||||
"examples/alibaba/oss/configuration_hcl_bucket.yaml",
|
||||
}
|
||||
testConfigurationsForceDelete = "examples/random/configuration_force_delete.yaml"
|
||||
testConfigurationsGitCredsSecretReference = "examples/random/configuration_git_ssh.yaml"
|
||||
testConfigurationDeleteProvisioningResources = "examples/random/configuration_delete_provisioning_resources.yaml"
|
||||
chartNamespace = "terraform"
|
||||
)
|
||||
|
||||
type ConfigurationAttr struct {
|
||||
Name string
|
||||
YamlPath string
|
||||
TFConfigMapName string
|
||||
BackendStateSecretName string
|
||||
BackendStateSecretNS string
|
||||
OutputsSecretName string
|
||||
VariableSecretName string
|
||||
}
|
||||
|
||||
type TestContext struct {
|
||||
context.Context
|
||||
Configuration *ConfigurationAttr
|
||||
BackendSecretNamespace string
|
||||
ClientSet *kubernetes.Clientset
|
||||
}
|
||||
|
||||
type DoFunc = func(ctx *TestContext)
|
||||
|
||||
type Injector struct {
|
||||
BeforeApplyConfiguration DoFunc
|
||||
CheckConfiguration DoFunc
|
||||
CleanUp DoFunc
|
||||
|
||||
// add more actions and check points if needed
|
||||
}
|
||||
|
||||
func invoke(do DoFunc, ctx *TestContext) {
|
||||
if do != nil {
|
||||
do(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func testBase(t *testing.T, configuration ConfigurationAttr, injector Injector, useCustomBackend bool) {
|
||||
klog.Infof("%s test begins……", configuration.Name)
|
||||
|
||||
waitConfigurationAvailable := func(ctx *TestContext) {
|
||||
for i := 0; i < 60; i++ {
|
||||
var fields []string
|
||||
output, err := exec.Command("bash", "-c", "kubectl get configuration").CombinedOutput()
|
||||
assert.NilError(t, err)
|
||||
t.Log("get configuration\n", string(output))
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for i, line := range lines {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
fields = strings.Fields(line)
|
||||
if len(fields) == 3 && fields[0] == configuration.Name && fields[1] == Available {
|
||||
return
|
||||
}
|
||||
}
|
||||
output, err = exec.Command("bash", "-c", "kubectl get pod").CombinedOutput()
|
||||
lines = strings.Split(string(output), "\n")
|
||||
t.Log("get pod\n", string(output))
|
||||
if i == 59 {
|
||||
t.Error("Configuration is not ready, getting controller's log")
|
||||
output, err = exec.Command("bash", "-c", "kubectl logs -n terraform deploy/terraform-controller").CombinedOutput()
|
||||
assert.NilError(t, err)
|
||||
t.Log(string(output))
|
||||
}
|
||||
time.Sleep(time.Second * 2)
|
||||
}
|
||||
}
|
||||
|
||||
backendSecretNamespace := configuration.BackendStateSecretNS
|
||||
if backendSecretNamespace == "" {
|
||||
backendSecretNamespace = os.Getenv("TERRAFORM_BACKEND_NAMESPACE")
|
||||
if backendSecretNamespace == "" {
|
||||
backendSecretNamespace = "vela-system"
|
||||
}
|
||||
}
|
||||
|
||||
clientSet, err := client.Init()
|
||||
assert.NilError(t, err)
|
||||
ctx := context.Background()
|
||||
|
||||
testCtx := &TestContext{
|
||||
Context: ctx,
|
||||
Configuration: &configuration,
|
||||
BackendSecretNamespace: backendSecretNamespace,
|
||||
ClientSet: clientSet,
|
||||
}
|
||||
|
||||
defer invoke(injector.CleanUp, testCtx)
|
||||
|
||||
klog.Info("1. Applying Configuration")
|
||||
invoke(injector.BeforeApplyConfiguration, testCtx)
|
||||
pwd, _ := os.Getwd()
|
||||
configuration.YamlPath = filepath.Join(pwd, "../..", configuration.YamlPath)
|
||||
cmd := fmt.Sprintf("kubectl apply -f %s", configuration.YamlPath)
|
||||
output, err := exec.Command("bash", "-c", cmd).CombinedOutput()
|
||||
assert.NilError(t, err, string(output))
|
||||
|
||||
klog.Info("2. Checking Configuration status")
|
||||
if injector.CheckConfiguration == nil {
|
||||
injector.CheckConfiguration = waitConfigurationAvailable
|
||||
}
|
||||
invoke(injector.CheckConfiguration, testCtx)
|
||||
|
||||
klog.Info("3. Checking the status of Configs and Secrets")
|
||||
|
||||
klog.Info("- Checking ConfigMap which stores .tf")
|
||||
_, err = clientSet.CoreV1().ConfigMaps("default").Get(ctx, configuration.TFConfigMapName, v1.GetOptions{})
|
||||
assert.NilError(t, err)
|
||||
|
||||
if !useCustomBackend {
|
||||
klog.Info("- Checking Secret which stores Backend")
|
||||
_, err = clientSet.CoreV1().Secrets(backendSecretNamespace).Get(ctx, configuration.BackendStateSecretName, v1.GetOptions{})
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
if configuration.OutputsSecretName != "" {
|
||||
klog.Info("- Checking Secret which stores outputs")
|
||||
_, err = clientSet.CoreV1().Secrets("default").Get(ctx, configuration.OutputsSecretName, v1.GetOptions{})
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
klog.Info("- Checking Secret which stores variables")
|
||||
_, err = clientSet.CoreV1().Secrets("default").Get(ctx, configuration.VariableSecretName, v1.GetOptions{})
|
||||
assert.NilError(t, err)
|
||||
|
||||
klog.Info("4. Deleting Configuration")
|
||||
cmd = fmt.Sprintf("kubectl delete -f %s", configuration.YamlPath)
|
||||
output, err = exec.Command("bash", "-c", cmd).CombinedOutput()
|
||||
assert.NilError(t, err, string(output))
|
||||
|
||||
klog.Info("5. Checking Configuration is deleted")
|
||||
for i := 0; i < 60; i++ {
|
||||
var (
|
||||
fields []string
|
||||
existed bool
|
||||
)
|
||||
output, err := exec.Command("bash", "-c", "kubectl get configuration").CombinedOutput()
|
||||
assert.NilError(t, err, string(output))
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
|
||||
for j, line := range lines {
|
||||
if j == 0 {
|
||||
continue
|
||||
}
|
||||
fields = strings.Fields(line)
|
||||
if len(fields) == 3 && fields[0] == configuration.Name {
|
||||
existed = true
|
||||
}
|
||||
}
|
||||
if existed {
|
||||
if i == 59 {
|
||||
t.Error("Configuration is not deleted")
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
klog.Info("6. Checking Secrets and ConfigMap which should all be deleted")
|
||||
|
||||
if configuration.OutputsSecretName != "" {
|
||||
_, err = clientSet.CoreV1().Secrets("default").Get(ctx, configuration.OutputsSecretName, v1.GetOptions{})
|
||||
assert.Equal(t, kerrors.IsNotFound(err), true)
|
||||
}
|
||||
|
||||
_, err = clientSet.CoreV1().Secrets("default").Get(ctx, configuration.VariableSecretName, v1.GetOptions{})
|
||||
assert.Equal(t, kerrors.IsNotFound(err), true)
|
||||
|
||||
if !useCustomBackend {
|
||||
_, err = clientSet.CoreV1().Secrets(backendSecretNamespace).Get(ctx, configuration.BackendStateSecretName, v1.GetOptions{})
|
||||
assert.Equal(t, kerrors.IsNotFound(err), true)
|
||||
}
|
||||
|
||||
_, err = clientSet.CoreV1().ConfigMaps("default").Get(ctx, configuration.TFConfigMapName, v1.GetOptions{})
|
||||
assert.Equal(t, kerrors.IsNotFound(err), true)
|
||||
|
||||
klog.Infof("%s test ends……", configuration.Name)
|
||||
}
|
||||
|
||||
func TestInlineCredentialsConfiguration(t *testing.T) {
|
||||
configuration := ConfigurationAttr{
|
||||
Name: "random-e2e",
|
||||
YamlPath: testConfigurationsInlineCredentials,
|
||||
TFConfigMapName: "tf-random-e2e",
|
||||
BackendStateSecretName: "tfstate-default-random-e2e",
|
||||
OutputsSecretName: "random-conn",
|
||||
VariableSecretName: "variable-random-e2e",
|
||||
}
|
||||
testBase(t, configuration, Injector{}, false)
|
||||
}
|
||||
|
||||
func TestInlineCredentialsConfigurationUseCustomBackendKubernetes(t *testing.T) {
|
||||
configuration := ConfigurationAttr{
|
||||
Name: "random-e2e-custom-backend-kubernetes",
|
||||
YamlPath: testConfigurationsInlineCredentialsCustomBackendKubernetes,
|
||||
BackendStateSecretName: "tfstate-default-custom-backend-kubernetes",
|
||||
BackendStateSecretNS: "a",
|
||||
TFConfigMapName: "tf-random-e2e-custom-backend-kubernetes",
|
||||
OutputsSecretName: "random-conn-custom-backend-kubernetes",
|
||||
VariableSecretName: "variable-random-e2e-custom-backend-kubernetes",
|
||||
}
|
||||
beforeApply := func(ctx *TestContext) {
|
||||
output, err := exec.Command("bash", "-c", "kubectl create ns a").CombinedOutput()
|
||||
if err != nil && !strings.Contains(string(output), "already exists") {
|
||||
assert.NilError(t, err, string(output))
|
||||
}
|
||||
}
|
||||
cleanUp := func(ctx *TestContext) {
|
||||
output, err := exec.Command("bash", "-c", "kubectl delete ns a").CombinedOutput()
|
||||
assert.NilError(t, err, string(output))
|
||||
}
|
||||
testBase(
|
||||
t,
|
||||
configuration,
|
||||
Injector{
|
||||
BeforeApplyConfiguration: beforeApply,
|
||||
CleanUp: cleanUp,
|
||||
},
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
func TestForceDeleteConfiguration(t *testing.T) {
|
||||
klog.Info("1. Applying Configuration whose hcl is not valid")
|
||||
pwd, _ := os.Getwd()
|
||||
configuration := filepath.Join(pwd, "..", testConfigurationsForceDelete)
|
||||
cmd := fmt.Sprintf("kubectl apply -f %s", configuration)
|
||||
err := exec.Command("bash", "-c", cmd).Start()
|
||||
assert.NilError(t, err)
|
||||
|
||||
klog.Info("2. Deleting Configuration")
|
||||
cmd = fmt.Sprintf("kubectl delete -f %s", configuration)
|
||||
err = exec.Command("bash", "-c", cmd).Start()
|
||||
assert.NilError(t, err)
|
||||
|
||||
klog.Info("5. Checking Configuration is deleted")
|
||||
for i := 0; i < 60; i++ {
|
||||
var (
|
||||
fields []string
|
||||
existed bool
|
||||
)
|
||||
output, err := exec.Command("bash", "-c", "kubectl get configuration").CombinedOutput()
|
||||
assert.NilError(t, err)
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
|
||||
for j, line := range lines {
|
||||
if j == 0 {
|
||||
continue
|
||||
}
|
||||
fields = strings.Fields(line)
|
||||
if len(fields) == 3 && fields[0] == "random-e2e-force-delete" {
|
||||
existed = true
|
||||
}
|
||||
}
|
||||
if existed {
|
||||
if i == 59 {
|
||||
t.Error("Configuration is not deleted")
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitCredentialsSecretReference(t *testing.T) {
|
||||
configuration := ConfigurationAttr{
|
||||
Name: "random-e2e-git-creds-secret-ref",
|
||||
YamlPath: testConfigurationsGitCredsSecretReference,
|
||||
TFConfigMapName: "tf-random-e2e-git-creds-secret-ref",
|
||||
BackendStateSecretName: "tfstate-default-random-e2e-git-creds-secret-ref",
|
||||
OutputsSecretName: "random-e2e-git-creds-secret-ref-conn",
|
||||
VariableSecretName: "variable-random-e2e-git-creds-secret-ref",
|
||||
}
|
||||
|
||||
clientSet, err := client.Init()
|
||||
assert.NilError(t, err)
|
||||
pwd, _ := os.Getwd()
|
||||
gitServer := filepath.Join(pwd, "../..", "examples/git-credentials")
|
||||
gitServerApplyCmd := fmt.Sprintf("kubectl apply -f %s", gitServer)
|
||||
gitServerDeleteCmd := fmt.Sprintf("kubectl delete -f %s", gitServer)
|
||||
gitSshAuthSecretYaml := filepath.Join(gitServer, "git-ssh-auth-secret.yaml")
|
||||
|
||||
beforeApply := func(ctx *TestContext) {
|
||||
output, err := exec.Command("bash", "-c", gitServerApplyCmd).CombinedOutput()
|
||||
assert.NilError(t, err, string(output))
|
||||
|
||||
klog.Info("- Checking git-server pod status")
|
||||
for i := 0; i < 120; i++ {
|
||||
serverReady := false
|
||||
pushReady := false
|
||||
pod, _ := clientSet.CoreV1().Pods("default").Get(ctx, "git-server", v1.GetOptions{})
|
||||
conditions := pod.Status.Conditions
|
||||
var index int
|
||||
for count, condition := range conditions {
|
||||
index = count
|
||||
if condition.Status == "True" && condition.Type == coreV1.PodReady {
|
||||
klog.Info("- pod=git-server ", condition.Type, "=", condition.Status)
|
||||
break
|
||||
}
|
||||
}
|
||||
if conditions[index].Status == "True" && conditions[index].Type == coreV1.PodReady {
|
||||
serverReady = true
|
||||
}
|
||||
job, _ := clientSet.BatchV1().Jobs("default").Get(ctx, "git-push", v1.GetOptions{})
|
||||
if job.Status.Succeeded == 1 {
|
||||
pushReady = true
|
||||
}
|
||||
if serverReady && pushReady {
|
||||
break
|
||||
}
|
||||
if i == 119 {
|
||||
t.Error("git-server pod is not running")
|
||||
}
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
|
||||
getKnownHostsCmd := "kubectl exec pod/git-server -- ssh-keyscan git-server"
|
||||
knownHosts, err := exec.Command("bash", "-c", getKnownHostsCmd).CombinedOutput()
|
||||
assert.NilError(t, err)
|
||||
|
||||
gitSshAuthSecretTmpl := filepath.Join(gitServer, "templates/git-ssh-auth-secret.tmpl")
|
||||
tmpl := template.Must(template.ParseFiles(gitSshAuthSecretTmpl))
|
||||
gitSshAuthSecretYamlFile, err := os.Create(gitSshAuthSecretYaml)
|
||||
assert.NilError(t, err)
|
||||
err = tmpl.Execute(gitSshAuthSecretYamlFile, base64.StdEncoding.EncodeToString(knownHosts))
|
||||
assert.NilError(t, err)
|
||||
|
||||
err = exec.Command("bash", "-c", gitServerApplyCmd).Run()
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
cleanUp := func(ctx *TestContext) {
|
||||
err = exec.Command("bash", "-c", gitServerDeleteCmd).Run()
|
||||
assert.NilError(t, err)
|
||||
os.Remove(gitSshAuthSecretYaml)
|
||||
}
|
||||
|
||||
testBase(
|
||||
t,
|
||||
configuration,
|
||||
Injector{
|
||||
BeforeApplyConfiguration: beforeApply,
|
||||
CleanUp: cleanUp,
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
func TestAllowDeleteProvisioningResoruce(t *testing.T) {
|
||||
configuration := ConfigurationAttr{
|
||||
Name: "random-e2e-delete-provisioning-resources",
|
||||
YamlPath: testConfigurationDeleteProvisioningResources,
|
||||
TFConfigMapName: "tf-random-e2e-delete-provisioning-resources",
|
||||
BackendStateSecretName: "tfstate-default-random-e2e-delete-provisioning-resources",
|
||||
// won't generate output at all
|
||||
OutputsSecretName: "",
|
||||
VariableSecretName: "variable-random-e2e-delete-provisioning-resources",
|
||||
}
|
||||
checkConfigurationIsProvisioning := func(ctx *TestContext) {
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
cfgProvisioning := false
|
||||
backendExist := false
|
||||
klog.Info("Check configuration is provisioning")
|
||||
var fields []string
|
||||
output, err := exec.Command("bash", "-c", "kubectl get configuration").CombinedOutput()
|
||||
assert.NilError(t, err)
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
for i, line := range lines {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
fields = strings.Fields(line)
|
||||
if len(fields) == 3 && fields[0] == configuration.Name && fields[1] == Provisioning {
|
||||
cfgProvisioning = true
|
||||
}
|
||||
}
|
||||
_, err = ctx.ClientSet.CoreV1().Secrets(ctx.BackendSecretNamespace).Get(ctx, configuration.BackendStateSecretName, v1.GetOptions{})
|
||||
if err == nil {
|
||||
backendExist = true
|
||||
}
|
||||
|
||||
if cfgProvisioning && backendExist {
|
||||
return
|
||||
}
|
||||
|
||||
if i == 119 {
|
||||
t.Error("Configuration is not ready")
|
||||
}
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
|
||||
}
|
||||
testBase(
|
||||
t,
|
||||
configuration,
|
||||
Injector{
|
||||
CheckConfiguration: checkConfigurationIsProvisioning,
|
||||
},
|
||||
false,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
//func TestBasicConfigurationRegression(t *testing.T) {
|
||||
// var retryTimes = 120
|
||||
//
|
||||
// klog.Info("0. Create namespace")
|
||||
// err := exec.Command("bash", "-c", "kubectl create ns abc").Start()
|
||||
// assert.NilError(t, err)
|
||||
//
|
||||
// Regression(t, testConfigurationsRegression, retryTimes)
|
||||
//}
|
|
@ -0,0 +1,110 @@
|
|||
package normal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gotest.tools/assert"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// Available is the available status of Configuration
|
||||
const Available = "Available"
|
||||
const Provisioning = "ProvisioningAndChecking"
|
||||
|
||||
// Regression test for the e2e.
|
||||
func Regression(t *testing.T, testcases []string, retryTimes int) {
|
||||
klog.Info("1. Applying Configuration")
|
||||
pwd, _ := os.Getwd()
|
||||
for _, p := range testcases {
|
||||
configuration := filepath.Join(pwd, "..", p)
|
||||
cmd := fmt.Sprintf("kubectl apply -f %s", configuration)
|
||||
err := exec.Command("bash", "-c", cmd).Start() // #nosec
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
klog.Info("2. Checking Configurations status")
|
||||
for i := 0; i < retryTimes; i++ {
|
||||
var fields []string
|
||||
output, err := exec.Command("bash", "-c", "kubectl get configuration -A").Output()
|
||||
assert.NilError(t, err)
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
// delete the last line which is empty
|
||||
lines = lines[:len(lines)-1]
|
||||
|
||||
if len(lines) < len(testcases)+1 {
|
||||
continue
|
||||
}
|
||||
|
||||
var available = true
|
||||
for i, line := range lines {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
fields = strings.Fields(line)
|
||||
if len(fields) == 4 {
|
||||
if fields[2] != Available {
|
||||
available = false
|
||||
t.Logf("Configuration %s is not available", fields[1])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if available {
|
||||
goto deletion
|
||||
}
|
||||
if i == retryTimes-1 {
|
||||
t.Error("Not all configurations are ready")
|
||||
}
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
|
||||
deletion:
|
||||
klog.Info("3. Deleting Configuration")
|
||||
for _, p := range testcases {
|
||||
configuration := filepath.Join(pwd, "..", p)
|
||||
cmd := fmt.Sprintf("kubectl delete -f %s", configuration)
|
||||
err := exec.Command("bash", "-c", cmd).Start() // #nosec
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
klog.Info("4. Checking Configuration is deleted")
|
||||
for i := 0; i < retryTimes; i++ {
|
||||
var (
|
||||
fields []string
|
||||
existed bool
|
||||
)
|
||||
output, err := exec.Command("bash", "-c", "kubectl get configuration -A").Output()
|
||||
assert.NilError(t, err)
|
||||
|
||||
lines := strings.Split(string(output), "\n")
|
||||
|
||||
for j, line := range lines {
|
||||
if j == 0 {
|
||||
continue
|
||||
}
|
||||
existed = true
|
||||
|
||||
fields = strings.Fields(line)
|
||||
if len(fields) == 4 {
|
||||
t.Logf("Retrying %d times. Configuration %s is deleting.", i+1, fields[1])
|
||||
}
|
||||
}
|
||||
if existed {
|
||||
if i == retryTimes-1 {
|
||||
t.Error("Configuration are not deleted")
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue