Compare commits
154 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 | |
|
bacb79de94 |
|
@ -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
|
|
@ -1,109 +1,48 @@
|
|||
name: Chart
|
||||
name: HelmChart
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch: {}
|
||||
|
||||
env:
|
||||
BUCKET: kubevelacharts
|
||||
ENDPOINT: oss-cn-hangzhou.aliyuncs.com
|
||||
ACCESS_KEY: ${{ secrets.OSS_ACCESS_KEY }}
|
||||
ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
|
||||
HELM_CHART: chart
|
||||
LOCAL_OSS_DIRECTORY: .oss/
|
||||
|
||||
jobs:
|
||||
chart-build:
|
||||
runs-on: ubuntu-20.04
|
||||
publish-charts:
|
||||
env:
|
||||
HELM_CHART: chart/
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Get the version
|
||||
id: get_version
|
||||
run: |
|
||||
tag=${GITHUB_REF#refs/tags/}
|
||||
VERSION=${tag#"v"}
|
||||
if [[ ${GITHUB_REF} == "refs/heads/master" ]]; then
|
||||
VERSION=latest
|
||||
fi
|
||||
echo ::set-output name=VERSION::${VERSION}
|
||||
- name: Get git revision
|
||||
id: vars
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::set-output name=git_revision::$(git rev-parse --short HEAD)"
|
||||
|
||||
- name: Login docker.io
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: docker.io
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
with:
|
||||
driver-opts: image=moby/buildkit:master
|
||||
|
||||
- uses: docker/build-push-action@v2
|
||||
name: Build & Pushing terraform controller for Dockerhub
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
labels: |-
|
||||
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
platforms: linux/amd64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
build-args: |
|
||||
GITVERSION=git-${{ steps.vars.outputs.git_revision }}
|
||||
VERSION=${{ steps.get_version.outputs.VERSION }}
|
||||
GOPROXY=https://proxy.golang.org
|
||||
tags: |-
|
||||
docker.io/oamdev/terraform-controller:${{ steps.get_version.outputs.VERSION }}
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@v1
|
||||
with:
|
||||
version: v3.4.0
|
||||
- uses: oprypin/find-latest-tag@v1
|
||||
with:
|
||||
repository: oam-dev/terraform-controller
|
||||
releases-only: true
|
||||
id: latest_tag
|
||||
- name: Get the version
|
||||
id: get_version
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
|
||||
- name: Tag helm chart image
|
||||
run: |
|
||||
latest_repo_tag=${{ steps.latest_tag.outputs.tag }}
|
||||
sub="."
|
||||
major="$(cut -d"$sub" -f1 <<<"$latest_repo_tag")"
|
||||
minor="$(cut -d"$sub" -f2 <<<"$latest_repo_tag")"
|
||||
patch="0"
|
||||
current_repo_tag="$major.$minor.$patch"
|
||||
tag=${GITHUB_REF#refs/tags/}
|
||||
image_tag=${tag#"v"}
|
||||
chart_version=$latest_repo_tag
|
||||
if [[ ${GITHUB_REF} == "refs/heads/master" ]]; then
|
||||
image_tag=latest
|
||||
chart_version=${current_repo_tag}-nightly-build
|
||||
fi
|
||||
# sed -i "s/0.2.8/${image_tag}/g" $HELM_CHART/Makefile
|
||||
chart_smever=${chart_version#"v"}
|
||||
sed -i "s/0.2.8/$chart_smever/g" $HELM_CHART/Chart.yaml
|
||||
sed -i "s/0.2.8/${image_tag}/g" $HELM_CHART/values.yaml
|
||||
|
||||
- name: Install ossutil
|
||||
run: wget http://gosspublic.alicdn.com/ossutil/1.7.0/ossutil64 && chmod +x ossutil64 && mv ossutil64 ossutil
|
||||
- name: Configure Alibaba Cloud OSSUTIL
|
||||
run: ./ossutil --config-file .ossutilconfig config -i ${ACCESS_KEY} -k ${ACCESS_KEY_SECRET} -e ${ENDPOINT} -c .ossutilconfig
|
||||
- name: sync cloud to local
|
||||
run: ./ossutil --config-file .ossutilconfig cp oss://$BUCKET/addons/index.yaml $LOCAL_OSS_DIRECTORY -f
|
||||
|
||||
- name: Package helm charts
|
||||
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: |
|
||||
helm package $HELM_CHART --destination $LOCAL_OSS_DIRECTORY
|
||||
helm repo index --url https://charts.kubevela.net/addons $LOCAL_OSS_DIRECTORY/ --merge $LOCAL_OSS_DIRECTORY/index.yaml
|
||||
|
||||
- name: sync local to cloud
|
||||
run: ./ossutil --config-file .ossutilconfig sync $LOCAL_OSS_DIRECTORY oss://$BUCKET/addons -f
|
||||
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
|
|
@ -4,19 +4,20 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- v*
|
||||
workflow_dispatch: {}
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
GO_VERSION: '1.17.6'
|
||||
GOLANGCI_VERSION: 'v1.38'
|
||||
KUBECONFIG: /home/github/.kube/config
|
||||
GO_VERSION: '1.23.8'
|
||||
KIND_VERSION: 'v0.12.0'
|
||||
|
||||
jobs:
|
||||
e2e-tests:
|
||||
runs-on: self-hosted
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
|
@ -31,28 +32,38 @@ jobs:
|
|||
run: |
|
||||
go get -v -t -d ./...
|
||||
|
||||
- name: Build and push images
|
||||
- name: Setup Kind
|
||||
uses: engineerd/setup-kind@v0.5.0
|
||||
with:
|
||||
version: ${{ env.KIND_VERSION }}
|
||||
skipClusterCreation: true
|
||||
|
||||
- name: Setup Kind Cluster
|
||||
run: |
|
||||
sed -i "s/0.2.8/latest/g" Makefile
|
||||
make docker-build
|
||||
make docker-push
|
||||
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 delete terraform-controller -n terraform
|
||||
|
||||
sed -i "s/0.2.8/latest/g" chart/values.yaml
|
||||
helm lint ./chart --debug
|
||||
helm upgrade --install --create-namespace --namespace terraform terraform-controller ./chart
|
||||
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
|
|
@ -13,12 +13,12 @@ on:
|
|||
|
||||
env:
|
||||
# Common versions
|
||||
GO_VERSION: '1.17.6'
|
||||
GO_VERSION: '1.23.8'
|
||||
KIND_VERSION: 'v0.7.0'
|
||||
|
||||
jobs:
|
||||
detect-noop:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
noop: ${{ steps.noop.outputs.should_skip }}
|
||||
steps:
|
||||
|
@ -32,7 +32,7 @@ jobs:
|
|||
concurrent_skipping: false
|
||||
|
||||
make-reviewable:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
needs: detect-noop
|
||||
if: needs.detect-noop.outputs.noop != 'true'
|
||||
|
||||
|
|
|
@ -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
|
|
@ -15,7 +15,7 @@ on:
|
|||
|
||||
env:
|
||||
# Common versions
|
||||
GO_VERSION: '1.17.6'
|
||||
GO_VERSION: '1.23.8'
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
@ -31,7 +31,7 @@ jobs:
|
|||
make lint
|
||||
|
||||
unit-tests:
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
|
@ -46,7 +46,7 @@ jobs:
|
|||
submodules: true
|
||||
|
||||
- name: Cache Go Dependencies
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .work/pkg
|
||||
key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }}
|
||||
|
|
|
@ -30,3 +30,6 @@ terraform-controller-*
|
|||
examples/tf-native/alibaba/cs/kubeconfig
|
||||
|
||||
bin/manager
|
||||
|
||||
# Secret for git server
|
||||
examples/git-credentials/git-ssh-auth-secret.yaml
|
|
@ -2,12 +2,11 @@ run:
|
|||
timeout: 10m
|
||||
|
||||
skip-files:
|
||||
- "zz_generated\\..+\\.go$"
|
||||
- ".*_test.go$"
|
||||
|
||||
|
||||
output:
|
||||
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
|
||||
format: colored-line-number
|
||||
formats: colored-line-number
|
||||
|
||||
linters-settings:
|
||||
errcheck:
|
||||
|
@ -22,7 +21,7 @@ linters-settings:
|
|||
# [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
|
||||
ignore: fmt:.*,io/ioutil:^Read.*
|
||||
exclude-functions: fmt:.*,io/ioutil:^Read.*
|
||||
|
||||
exhaustive:
|
||||
# indicates that switch statements are to be considered exhaustive if a
|
||||
|
@ -44,7 +43,7 @@ linters-settings:
|
|||
|
||||
gocyclo:
|
||||
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
||||
min-complexity: 30
|
||||
min-complexity: 32
|
||||
|
||||
maligned:
|
||||
# print struct with more effective memory layout or not, false by default
|
||||
|
@ -100,7 +99,6 @@ linters-settings:
|
|||
|
||||
linters:
|
||||
enable:
|
||||
- megacheck
|
||||
- govet
|
||||
- gocyclo
|
||||
- gocritic
|
||||
|
@ -111,6 +109,9 @@ linters:
|
|||
- unconvert
|
||||
- misspell
|
||||
- nakedret
|
||||
- staticcheck
|
||||
- gosimple
|
||||
- unused
|
||||
|
||||
presets:
|
||||
- bugs
|
||||
|
@ -119,6 +120,12 @@ linters:
|
|||
|
||||
|
||||
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.
|
||||
|
@ -179,6 +186,14 @@ issues:
|
|||
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`.
|
||||
|
|
|
@ -20,13 +20,17 @@ Refer to [Helm official Doc](https://helm.sh/docs/intro/install/) to install `he
|
|||
$ make install
|
||||
go: creating new go.mod: module tmp
|
||||
...
|
||||
go get: added sigs.k8s.io/controller-tools v0.6.0
|
||||
go get: added sigs.k8s.io/structured-merge-diff/v4 v4.1.0
|
||||
go get: added sigs.k8s.io/yaml v1.2.0
|
||||
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 configured
|
||||
customresourcedefinition.apiextensions.k8s.io/providers.terraform.core.oam.dev configured
|
||||
customresourcedefinition.apiextensions.k8s.io/configurations.terraform.core.oam.dev created
|
||||
customresourcedefinition.apiextensions.k8s.io/providers.terraform.core.oam.dev created
|
||||
```
|
||||
|
||||
- Run Terraform Controller
|
||||
|
@ -35,7 +39,7 @@ customresourcedefinition.apiextensions.k8s.io/providers.terraform.core.oam.dev c
|
|||
$ make run
|
||||
go: creating new go.mod: module tmp
|
||||
...
|
||||
go get: added sigs.k8s.io/yaml v1.2.0
|
||||
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 ./...
|
||||
|
@ -147,3 +151,11 @@ 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
|
||||
```
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Build the manager binary
|
||||
FROM golang:1.16 as builder
|
||||
FROM golang:1.23-alpine as builder
|
||||
|
||||
WORKDIR /workspace
|
||||
# Copy the Go Modules manifests
|
||||
|
@ -19,7 +19,7 @@ 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 golang:1.16
|
||||
FROM alpine
|
||||
WORKDIR /
|
||||
COPY --from=builder /workspace/manager .
|
||||
#USER nonroot:nonroot
|
||||
|
@ -27,6 +27,6 @@ COPY --from=builder /workspace/manager .
|
|||
# COPY terraform binary
|
||||
COPY bin/terraform /usr/bin/terraform
|
||||
#RUN chmod +x /usr/bin/terraform
|
||||
RUN apt-get install git
|
||||
RUN apk add git
|
||||
|
||||
ENTRYPOINT ["/manager"]
|
||||
|
|
17
Makefile
17
Makefile
|
@ -3,7 +3,7 @@
|
|||
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)
|
||||
|
@ -82,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.6.0 ;\
|
||||
go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.5 ;\
|
||||
rm -rf $$CONTROLLER_GEN_TMP_DIR ;\
|
||||
}
|
||||
CONTROLLER_GEN=$(GOBIN)/controller-gen
|
||||
|
@ -90,7 +90,7 @@ else
|
|||
CONTROLLER_GEN=$(shell which controller-gen)
|
||||
endif
|
||||
|
||||
GOLANGCILINT_VERSION ?= v1.38.0
|
||||
GOLANGCILINT_VERSION ?= v1.60.1
|
||||
HOSTOS := $(shell uname -s | tr '[:upper:]' '[:lower:]')
|
||||
HOSTARCH := $(shell uname -m)
|
||||
ifeq ($(HOSTARCH),x86_64)
|
||||
|
@ -131,7 +131,7 @@ 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
|
||||
|
@ -144,6 +144,12 @@ install-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"
|
||||
|
@ -261,7 +267,8 @@ custom: custom-credentials custom-provider
|
|||
|
||||
|
||||
configuration:
|
||||
go test -coverprofile=e2e-coverage1.xml -v ./e2e/... -count=1
|
||||
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
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ Terraform Controller is a Kubernetes Controller for Terraform.
|
|||
## Supported Terraform Configuration
|
||||
|
||||
- HCL
|
||||
- JSON (Deprecated in v0.3.1)
|
||||
- JSON (Deprecated in v0.3.1, removed in v0.4.6)
|
||||
|
||||
# Get started
|
||||
|
||||
|
|
|
@ -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"
|
||||
)
|
|
@ -21,18 +21,31 @@ 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"
|
||||
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 (
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package types
|
||||
|
||||
import "k8s.io/apimachinery/pkg/api/resource"
|
||||
|
||||
const (
|
||||
// TerraformJSONConfigurationName is the file name for Terraform json Configuration
|
||||
TerraformJSONConfigurationName = "main.tf.json"
|
||||
// TerraformHCLConfigurationName is the file name for Terraform hcl Configuration
|
||||
TerraformHCLConfigurationName = "main.tf"
|
||||
)
|
||||
|
@ -11,10 +11,32 @@ const (
|
|||
type ConfigurationType string
|
||||
|
||||
const (
|
||||
// ConfigurationJSON is the json type Configuration
|
||||
ConfigurationJSON ConfigurationType = "JSON"
|
||||
// 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,6 +17,7 @@ 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"
|
||||
|
||||
|
@ -47,6 +48,18 @@ type ConfigurationSpec struct {
|
|||
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
|
||||
|
@ -71,6 +84,11 @@ type BaseConfigurationSpec struct {
|
|||
|
||||
// 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.
|
||||
// +optional
|
||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||
|
||||
Apply ConfigurationApplyStatus `json:"apply,omitempty"`
|
||||
Destroy ConfigurationDestroyStatus `json:"destroy,omitempty"`
|
||||
}
|
||||
|
@ -108,6 +126,7 @@ type Backend struct {
|
|||
// +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"`
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
|
@ -23,6 +22,7 @@ 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"
|
||||
)
|
||||
|
||||
|
@ -176,6 +176,26 @@ func (in *ConfigurationSpec) DeepCopyInto(out *ConfigurationSpec) {
|
|||
**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
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationSpec.
|
||||
|
|
|
@ -17,40 +17,38 @@ 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"
|
||||
|
||||
state "github.com/oam-dev/terraform-controller/api/types"
|
||||
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 {
|
||||
// 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"`
|
||||
|
||||
// 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 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 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"`
|
||||
|
||||
BaseConfigurationSpec `json:",inline"`
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
@ -60,54 +58,118 @@ type BaseConfigurationSpec struct {
|
|||
|
||||
// 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"`
|
||||
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 state.ConfigurationState `json:"state,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Outputs map[string]Property `json:"outputs,omitempty"`
|
||||
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 state.ConfigurationState `json:"state,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
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"`
|
||||
Type string `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
// Backend stores the state in a Kubernetes secret with locking done using a Lease resource.
|
||||
// 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:storageversion
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="STATE",type="string",JSONPath=".status.apply.state"
|
||||
// +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"`
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
|
@ -23,12 +22,23 @@ 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.
|
||||
|
@ -41,31 +51,6 @@ func (in *Backend) DeepCopy() *Backend {
|
|||
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
|
||||
|
@ -165,6 +150,7 @@ 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
|
||||
out.GitRef = in.GitRef
|
||||
if in.Variable != nil {
|
||||
in, out := &in.Variable, &out.Variable
|
||||
*out = new(runtime.RawExtension)
|
||||
|
@ -173,9 +159,53 @@ func (in *ConfigurationSpec) DeepCopyInto(out *ConfigurationSpec) {
|
|||
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
|
||||
}
|
||||
in.BaseConfigurationSpec.DeepCopyInto(&out.BaseConfigurationSpec)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigurationSpec.
|
||||
|
@ -205,6 +235,26 @@ func (in *ConfigurationStatus) DeepCopy() *ConfigurationStatus {
|
|||
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
|
||||
|
@ -219,3 +269,23 @@ func (in *Property) DeepCopy() *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
|
||||
}
|
||||
|
|
BIN
bin/manager
BIN
bin/manager
Binary file not shown.
|
@ -1,6 +1,6 @@
|
|||
apiVersion: v1
|
||||
name: terraform-controller
|
||||
version: 0.2.8
|
||||
version: 0.1.0
|
||||
description: A Kubernetes Terraform controller
|
||||
home: https://github.com/oam-dev/terraform-controller
|
||||
appVersion: "0.3.0"
|
||||
appVersion: 0.1.0
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.6.0
|
||||
creationTimestamp: null
|
||||
controller-gen.kubebuilder.io/version: v0.16.5
|
||||
name: configurations.terraform.core.oam.dev
|
||||
spec:
|
||||
group: terraform.core.oam.dev
|
||||
|
@ -13,6 +11,9 @@ spec:
|
|||
kind: Configuration
|
||||
listKind: ConfigurationList
|
||||
plural: configurations
|
||||
shortNames:
|
||||
- conf
|
||||
- terraform-conf
|
||||
singular: configuration
|
||||
scope: Namespaced
|
||||
versions:
|
||||
|
@ -29,14 +30,19 @@ spec:
|
|||
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'
|
||||
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'
|
||||
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
|
||||
|
@ -44,15 +50,14 @@ 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.'
|
||||
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. 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
|
||||
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
|
||||
|
@ -68,6 +73,20 @@ spec:
|
|||
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
|
||||
|
@ -95,15 +114,58 @@ spec:
|
|||
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.
|
||||
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.
|
||||
|
@ -148,6 +210,12 @@ spec:
|
|||
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
|
||||
|
@ -156,7 +224,10 @@ spec:
|
|||
status: {}
|
||||
- additionalPrinterColumns:
|
||||
- jsonPath: .status.apply.state
|
||||
name: STATE
|
||||
name: APPLY
|
||||
type: string
|
||||
- jsonPath: .status.destroy.state
|
||||
name: DESTROY
|
||||
type: string
|
||||
- jsonPath: .metadata.creationTimestamp
|
||||
name: AGE
|
||||
|
@ -167,35 +238,77 @@ spec:
|
|||
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'
|
||||
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'
|
||||
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
|
||||
JobEnv:
|
||||
type: object
|
||||
x-kubernetes-preserve-unknown-fields: true
|
||||
backend:
|
||||
description: 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
|
||||
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}'
|
||||
|
@ -210,9 +323,46 @@ spec:
|
|||
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
|
||||
|
@ -233,15 +383,59 @@ spec:
|
|||
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.
|
||||
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.
|
||||
|
@ -266,12 +460,15 @@ spec:
|
|||
additionalProperties:
|
||||
description: Property is the property for an output
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
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
|
||||
|
@ -286,15 +483,16 @@ spec:
|
|||
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: {}
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.6.0
|
||||
creationTimestamp: null
|
||||
controller-gen.kubebuilder.io/version: v0.16.5
|
||||
name: providers.terraform.core.oam.dev
|
||||
spec:
|
||||
group: terraform.core.oam.dev
|
||||
|
@ -29,14 +27,19 @@ spec:
|
|||
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'
|
||||
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'
|
||||
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
|
||||
|
@ -47,8 +50,9 @@ spec:
|
|||
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.
|
||||
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.
|
||||
|
@ -99,9 +103,3 @@ spec:
|
|||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
|
|
|
@ -20,6 +20,11 @@ spec:
|
|||
- 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:
|
||||
|
@ -35,4 +40,28 @@ spec:
|
|||
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
|
||||
|
|
|
@ -2,14 +2,33 @@ replicaCount: 1
|
|||
|
||||
image:
|
||||
repository: oamdev/terraform-controller
|
||||
tag: 0.2.8
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
|
||||
gitImage: alpine/git:latest
|
||||
busyboxImage: busybox:latest
|
||||
terraformImage: oamdev/docker-terraform:1.1.2
|
||||
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,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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -9,12 +9,15 @@ import (
|
|||
"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"
|
||||
)
|
||||
|
||||
|
@ -32,17 +35,14 @@ const (
|
|||
const errGitHubBlockedNotBoolean = "the value of githubBlocked is not a boolean"
|
||||
|
||||
// ValidConfigurationObject will validate a Configuration
|
||||
func ValidConfigurationObject(configuration *v1beta1.Configuration) (types.ConfigurationType, error) {
|
||||
json := configuration.Spec.JSON
|
||||
func ValidConfigurationObject(configuration *v1beta2.Configuration) (types.ConfigurationType, error) {
|
||||
hcl := configuration.Spec.HCL
|
||||
remote := configuration.Spec.Remote
|
||||
switch {
|
||||
case json == "" && hcl == "" && remote == "":
|
||||
return "", errors.New("spec.JSON, spec.HCL or spec.Remote should be set")
|
||||
case json != "" && hcl != "", json != "" && remote != "", hcl != "" && remote != "":
|
||||
return "", errors.New("spec.JSON, spec.HCL and/or spec.Remote cloud not be set at the same time")
|
||||
case json != "":
|
||||
return types.ConfigurationJSON, nil
|
||||
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 != "":
|
||||
|
@ -51,38 +51,6 @@ func ValidConfigurationObject(configuration *v1beta1.Configuration) (types.Confi
|
|||
return "", nil
|
||||
}
|
||||
|
||||
// RenderConfiguration will compose the Terraform configuration with hcl/json and backend
|
||||
func RenderConfiguration(configuration *v1beta1.Configuration, terraformBackendNamespace string, configurationType types.ConfigurationType) (string, 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 = &v1beta1.Backend{
|
||||
SecretSuffix: configuration.Name,
|
||||
InClusterConfig: true,
|
||||
}
|
||||
}
|
||||
backendTF, err := RenderTemplate(configuration.Spec.Backend, terraformBackendNamespace)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to prepare Terraform backend configuration")
|
||||
}
|
||||
|
||||
switch configurationType {
|
||||
case types.ConfigurationJSON:
|
||||
return configuration.Spec.JSON, nil
|
||||
case types.ConfigurationHCL:
|
||||
completedConfiguration := configuration.Spec.HCL
|
||||
completedConfiguration += "\n" + backendTF
|
||||
return completedConfiguration, nil
|
||||
case types.ConfigurationRemote:
|
||||
return backendTF, nil
|
||||
default:
|
||||
return "", errors.New("Unsupported Configuration Type")
|
||||
}
|
||||
}
|
||||
|
||||
// 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})
|
||||
|
@ -98,13 +66,13 @@ func SetRegion(ctx context.Context, k8sClient client.Client, namespace, name str
|
|||
}
|
||||
|
||||
// Update will update the Configuration
|
||||
func Update(ctx context.Context, k8sClient client.Client, configuration *v1beta1.Configuration) error {
|
||||
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) (v1beta1.Configuration, error) {
|
||||
configuration := &v1beta1.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)
|
||||
|
@ -115,18 +83,29 @@ func Get(ctx context.Context, k8sClient client.Client, namespacedName apitypes.N
|
|||
}
|
||||
|
||||
// IsDeletable will check whether the Configuration can be deleted immediately
|
||||
// If deletable, it means no external cloud resources are provisioned
|
||||
func IsDeletable(ctx context.Context, k8sClient client.Client, configuration *v1beta1.Configuration) (bool, error) {
|
||||
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 {
|
||||
// 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)
|
||||
|
@ -142,7 +121,7 @@ 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(errGitHubBlockedNotBoolean, err)
|
||||
klog.Warningf("%s: %v", errGitHubBlockedNotBoolean, err)
|
||||
return remote
|
||||
}
|
||||
klog.InfoS("Parsed GITHUB_BLOCKED env", "githubBlocked", githubBlocked)
|
||||
|
@ -171,7 +150,7 @@ func ReplaceTerraformSource(remote string, githubBlockedStr string) string {
|
|||
}
|
||||
|
||||
// GetProviderNamespacedName will get the provider namespaced name
|
||||
func GetProviderNamespacedName(configuration v1beta1.Configuration) *crossplane.Reference {
|
||||
func GetProviderNamespacedName(configuration v1beta2.Configuration) *crossplane.Reference {
|
||||
if configuration.Spec.ProviderReference != nil {
|
||||
return configuration.Spec.ProviderReference
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@ package configuration
|
|||
|
||||
import (
|
||||
"context"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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"
|
||||
|
@ -14,11 +14,12 @@ 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/v1beta1"
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta2"
|
||||
)
|
||||
|
||||
func TestValidConfigurationObject(t *testing.T) {
|
||||
type args struct {
|
||||
configuration *v1beta1.Configuration
|
||||
configuration *v1beta2.Configuration
|
||||
}
|
||||
type want struct {
|
||||
configurationType types.ConfigurationType
|
||||
|
@ -33,8 +34,8 @@ func TestValidConfigurationObject(t *testing.T) {
|
|||
{
|
||||
name: "hcl",
|
||||
args: args{
|
||||
configuration: &v1beta1.Configuration{
|
||||
Spec: v1beta1.ConfigurationSpec{
|
||||
configuration: &v1beta2.Configuration{
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
HCL: "abc",
|
||||
},
|
||||
},
|
||||
|
@ -46,8 +47,8 @@ func TestValidConfigurationObject(t *testing.T) {
|
|||
{
|
||||
name: "remote",
|
||||
args: args{
|
||||
configuration: &v1beta1.Configuration{
|
||||
Spec: v1beta1.ConfigurationSpec{
|
||||
configuration: &v1beta2.Configuration{
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
Remote: "def",
|
||||
},
|
||||
},
|
||||
|
@ -56,24 +57,11 @@ func TestValidConfigurationObject(t *testing.T) {
|
|||
configurationType: types.ConfigurationRemote,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "json",
|
||||
args: args{
|
||||
configuration: &v1beta1.Configuration{
|
||||
Spec: v1beta1.ConfigurationSpec{
|
||||
JSON: "abc",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
configurationType: types.ConfigurationJSON,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote and hcl are set",
|
||||
args: args{
|
||||
configuration: &v1beta1.Configuration{
|
||||
Spec: v1beta1.ConfigurationSpec{
|
||||
configuration: &v1beta2.Configuration{
|
||||
Spec: v1beta2.ConfigurationSpec{
|
||||
HCL: "abc",
|
||||
Remote: "def",
|
||||
},
|
||||
|
@ -81,19 +69,19 @@ func TestValidConfigurationObject(t *testing.T) {
|
|||
},
|
||||
want: want{
|
||||
configurationType: "",
|
||||
errMsg: "spec.JSON, spec.HCL and/or spec.Remote cloud not be set at the same time",
|
||||
errMsg: "spec.HCL and spec.Remote cloud not be set at the same time",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remote and hcl are not set",
|
||||
args: args{
|
||||
configuration: &v1beta1.Configuration{
|
||||
Spec: v1beta1.ConfigurationSpec{},
|
||||
configuration: &v1beta2.Configuration{
|
||||
Spec: v1beta2.ConfigurationSpec{},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
configurationType: "",
|
||||
errMsg: "spec.JSON, spec.HCL or spec.Remote should be set",
|
||||
errMsg: "spec.HCL or spec.Remote should be set",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -113,99 +101,6 @@ func TestValidConfigurationObject(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
func TestRenderConfiguration(t *testing.T) {
|
||||
type args struct {
|
||||
configuration *v1beta1.Configuration
|
||||
ns string
|
||||
configurationType types.ConfigurationType
|
||||
}
|
||||
type want struct {
|
||||
cfg string
|
||||
errMsg string
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "backend is not nil, configuration is hcl",
|
||||
args: args{
|
||||
configuration: &v1beta1.Configuration{
|
||||
Spec: v1beta1.ConfigurationSpec{
|
||||
Backend: &v1beta1.Backend{},
|
||||
HCL: "abc",
|
||||
},
|
||||
},
|
||||
ns: "vela-system",
|
||||
configurationType: types.ConfigurationHCL,
|
||||
},
|
||||
want: want{
|
||||
cfg: `abc
|
||||
|
||||
terraform {
|
||||
backend "kubernetes" {
|
||||
secret_suffix = ""
|
||||
in_cluster_config = true
|
||||
namespace = "vela-system"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "backend is nil, configuration is remote",
|
||||
args: args{
|
||||
configuration: &v1beta1.Configuration{
|
||||
Spec: v1beta1.ConfigurationSpec{
|
||||
Remote: "https://github.com/a/b.git",
|
||||
},
|
||||
},
|
||||
ns: "vela-system",
|
||||
configurationType: types.ConfigurationRemote,
|
||||
},
|
||||
want: want{
|
||||
cfg: `
|
||||
terraform {
|
||||
backend "kubernetes" {
|
||||
secret_suffix = ""
|
||||
in_cluster_config = true
|
||||
namespace = "vela-system"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "backend is nil, configuration is not supported",
|
||||
args: args{
|
||||
configuration: &v1beta1.Configuration{
|
||||
Spec: v1beta1.ConfigurationSpec{},
|
||||
},
|
||||
ns: "vela-system",
|
||||
},
|
||||
want: want{
|
||||
errMsg: "Unsupported Configuration Type",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := RenderConfiguration(tc.args.configuration, tc.args.ns, tc.args.configurationType)
|
||||
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.cfg {
|
||||
t.Errorf("ValidConfigurationObject() = %v, want %v", got, tc.want.cfg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestReplaceTerraformSource(t *testing.T) {
|
||||
testcases := []struct {
|
||||
remote string
|
||||
|
@ -257,7 +152,8 @@ func TestReplaceTerraformSource(t *testing.T) {
|
|||
func TestIsDeletable(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(s)
|
||||
_ = v1beta1.AddToScheme(s)
|
||||
_ = v1beta2.AddToScheme(s)
|
||||
provider2 := &v1beta1.Provider{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
|
@ -281,7 +177,7 @@ func TestIsDeletable(t *testing.T) {
|
|||
k8sClient3 := fake.NewClientBuilder().WithScheme(s).WithObjects(provider3).Build()
|
||||
k8sClient4 := fake.NewClientBuilder().Build()
|
||||
|
||||
configuration := &v1beta1.Configuration{
|
||||
configuration := &v1beta2.Configuration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "abc",
|
||||
},
|
||||
|
@ -290,9 +186,34 @@ func TestIsDeletable(t *testing.T) {
|
|||
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 *v1beta1.Configuration
|
||||
configuration *v1beta2.Configuration
|
||||
k8sClient client.Client
|
||||
}
|
||||
type want struct {
|
||||
|
@ -308,7 +229,7 @@ func TestIsDeletable(t *testing.T) {
|
|||
name: "provider is not found",
|
||||
args: args{
|
||||
k8sClient: k8sClient1,
|
||||
configuration: &v1beta1.Configuration{},
|
||||
configuration: defaultConfiguration,
|
||||
},
|
||||
want: want{
|
||||
deletable: true,
|
||||
|
@ -318,7 +239,7 @@ func TestIsDeletable(t *testing.T) {
|
|||
name: "provider is not ready, use default providerRef",
|
||||
args: args{
|
||||
k8sClient: k8sClient2,
|
||||
configuration: &v1beta1.Configuration{},
|
||||
configuration: defaultConfiguration,
|
||||
},
|
||||
want: want{
|
||||
deletable: true,
|
||||
|
@ -337,14 +258,8 @@ func TestIsDeletable(t *testing.T) {
|
|||
{
|
||||
name: "configuration is provisioning",
|
||||
args: args{
|
||||
k8sClient: k8sClient3,
|
||||
configuration: &v1beta1.Configuration{
|
||||
Status: v1beta1.ConfigurationStatus{
|
||||
Apply: v1beta1.ConfigurationApplyStatus{
|
||||
State: types.ConfigurationProvisioningAndChecking,
|
||||
},
|
||||
},
|
||||
},
|
||||
k8sClient: k8sClient3,
|
||||
configuration: provisioningConfiguration,
|
||||
},
|
||||
want: want{
|
||||
errMsg: "Destroy could not complete and needs to wait for Provision to complete first",
|
||||
|
@ -353,14 +268,8 @@ func TestIsDeletable(t *testing.T) {
|
|||
{
|
||||
name: "configuration is ready",
|
||||
args: args{
|
||||
k8sClient: k8sClient3,
|
||||
configuration: &v1beta1.Configuration{
|
||||
Status: v1beta1.ConfigurationStatus{
|
||||
Apply: v1beta1.ConfigurationApplyStatus{
|
||||
State: types.Available,
|
||||
},
|
||||
},
|
||||
},
|
||||
k8sClient: k8sClient3,
|
||||
configuration: readyConfiguration,
|
||||
},
|
||||
want: want{},
|
||||
},
|
||||
|
@ -368,12 +277,22 @@ func TestIsDeletable(t *testing.T) {
|
|||
name: "failed to get provider",
|
||||
args: args{
|
||||
k8sClient: k8sClient4,
|
||||
configuration: &v1beta1.Configuration{},
|
||||
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) {
|
||||
|
@ -394,24 +313,24 @@ func TestIsDeletable(t *testing.T) {
|
|||
func TestSetRegion(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(s)
|
||||
_ = v1beta2.AddToScheme(s)
|
||||
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
|
||||
configuration1 := v1beta1.Configuration{
|
||||
configuration1 := v1beta2.Configuration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "abc",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1beta1.ConfigurationSpec{},
|
||||
Spec: v1beta2.ConfigurationSpec{},
|
||||
}
|
||||
configuration1.Spec.Region = "xxx"
|
||||
assert.Nil(t, k8sClient.Create(ctx, &configuration1))
|
||||
|
||||
configuration2 := v1beta1.Configuration{
|
||||
configuration2 := v1beta2.Configuration{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "def",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1beta1.ConfigurationSpec{},
|
||||
Spec: v1beta2.ConfigurationSpec{},
|
||||
}
|
||||
assert.Nil(t, k8sClient.Create(ctx, &configuration2))
|
||||
|
||||
|
|
|
@ -17,28 +17,13 @@ limitations under the License.
|
|||
package configuration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/api/v1beta1"
|
||||
)
|
||||
|
||||
var backendTF = `
|
||||
terraform {
|
||||
backend "kubernetes" {
|
||||
secret_suffix = "{{.SecretSuffix}}"
|
||||
in_cluster_config = {{.InClusterConfig}}
|
||||
namespace = "{{.Namespace}}"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// RawExtension2Map will convert rawExtension to map
|
||||
// This function is copied from oam-dev/kubevela
|
||||
func RawExtension2Map(raw *runtime.RawExtension) (map[string]interface{}, error) {
|
||||
|
@ -57,32 +42,6 @@ func RawExtension2Map(raw *runtime.RawExtension) (map[string]interface{}, error)
|
|||
return ret, nil
|
||||
}
|
||||
|
||||
type backendVars struct {
|
||||
SecretSuffix string
|
||||
InClusterConfig bool
|
||||
Namespace string
|
||||
}
|
||||
|
||||
// RenderTemplate renders Backend template
|
||||
func RenderTemplate(backend *v1beta1.Backend, namespace string) (string, error) {
|
||||
tmpl, err := template.New("backend").Funcs(template.FuncMap(sprig.FuncMap())).Parse(backendTF)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
templateVars := backendVars{
|
||||
SecretSuffix: backend.SecretSuffix,
|
||||
InClusterConfig: backend.InClusterConfig,
|
||||
Namespace: namespace,
|
||||
}
|
||||
var wr bytes.Buffer
|
||||
err = tmpl.Execute(&wr, templateVars)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return wr.String(), nil
|
||||
}
|
||||
|
||||
// Interface2String converts an interface{} type to string
|
||||
func Interface2String(v interface{}) (string, error) {
|
||||
var value string
|
||||
|
@ -96,11 +55,11 @@ func Interface2String(v interface{}) (string, error) {
|
|||
case bool:
|
||||
value = strconv.FormatBool(v)
|
||||
default:
|
||||
valuejson, err := json.Marshal(v)
|
||||
valueJSON, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cloud not convert %v to string", v)
|
||||
}
|
||||
value = string(valuejson)
|
||||
value = string(valueJSON)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ func TestRawExtension2Map2(t *testing.T) {
|
|||
},
|
||||
},
|
||||
want: want{
|
||||
errMessage: "invalid character 'x' looking for beginning of value",
|
||||
errMessage: "cannot convert RawExtension with unrecognized content type to unstructured",
|
||||
},
|
||||
}}
|
||||
for name, tc := range cases {
|
||||
|
|
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
|
@ -1,16 +1,20 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/go-yaml/yaml"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
envAWSAccessKeyID = "AWS_ACCESS_KEY_ID"
|
||||
envAWSSecretAccessKey = "AWS_SECRET_ACCESS_KEY"
|
||||
envAWSDefaultRegion = "AWS_DEFAULT_REGION"
|
||||
envAWSSessionToken = "AWS_SESSION_TOKEN"
|
||||
// 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
|
||||
|
@ -27,9 +31,9 @@ func getAWSCredentials(secretData []byte, name, namespace, region string) (map[s
|
|||
return nil, errors.Wrap(err, errConvertCredentials)
|
||||
}
|
||||
return map[string]string{
|
||||
envAWSAccessKeyID: ak.AWSAccessKeyID,
|
||||
envAWSSecretAccessKey: ak.AWSSecretAccessKey,
|
||||
envAWSSessionToken: ak.AWSSessionToken,
|
||||
envAWSDefaultRegion: region,
|
||||
EnvAWSAccessKeyID: ak.AWSAccessKeyID,
|
||||
EnvAWSSecretAccessKey: ak.AWSSecretAccessKey,
|
||||
EnvAWSSessionToken: ak.AWSSessionToken,
|
||||
EnvAWSDefaultRegion: region,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/go-yaml/yaml"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/go-yaml/yaml"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ const (
|
|||
ucloud CloudProvider = "ucloud"
|
||||
custom CloudProvider = "custom"
|
||||
baidu CloudProvider = "baidu"
|
||||
huawei CloudProvider = "huawei"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -43,15 +44,16 @@ const (
|
|||
envAlicloudRegion = "ALICLOUD_REGION"
|
||||
envAliCloudStsToken = "ALICLOUD_SECURITY_TOKEN"
|
||||
|
||||
errConvertCredentials = "failed to convert the credentials of Secret from Provider"
|
||||
errCredentialValid = "Credentials are not valid"
|
||||
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"`
|
||||
AccessKeySecret string `yaml:"accessKeySecret"`
|
||||
SecurityToken string `yaml:"securityToken"`
|
||||
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
|
||||
|
@ -89,7 +91,7 @@ func GetProviderCredentials(ctx context.Context, k8sClient client.Client, provid
|
|||
envAliCloudStsToken: ak.SecurityToken,
|
||||
}, nil
|
||||
case string(ucloud):
|
||||
return getUCloudCredentials(secretData, name, namespace)
|
||||
return getUCloudCredentials(secretData, name, namespace, region)
|
||||
case string(aws):
|
||||
return getAWSCredentials(secretData, name, namespace, region)
|
||||
case string(gcp):
|
||||
|
@ -106,6 +108,8 @@ func GetProviderCredentials(ctx context.Context, k8sClient client.Client, provid
|
|||
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)
|
||||
|
|
|
@ -7,11 +7,10 @@ import (
|
|||
"testing"
|
||||
|
||||
. "github.com/agiledragon/gomonkey/v2"
|
||||
"github.com/aliyun/alibaba-cloud-sdk-go/services/sts"
|
||||
"github.com/go-yaml/yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v2"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
@ -109,10 +108,8 @@ func TestGetProviderCredentials(t *testing.T) {
|
|||
}
|
||||
assert.Nil(t, k8sClient1.Create(ctx, secret))
|
||||
|
||||
patches := ApplyMethod(reflect.TypeOf(&sts.Client{}), "GetCallerIdentity", func(_ *sts.Client, request *sts.GetCallerIdentityRequest) (response *sts.GetCallerIdentityResponse, err error) {
|
||||
response = nil
|
||||
err = nil
|
||||
return
|
||||
patches := ApplyFunc(checkAlibabaCloudCredentials, func(region, accessKeyID, accessKeySecret, stsToken string) error {
|
||||
return nil
|
||||
})
|
||||
defer patches.Reset()
|
||||
|
||||
|
@ -380,7 +377,7 @@ func TestGetProviderCredentials(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetProviderCredentials(ctx, tt.args.k8sClient, &tt.args.provider, tt.args.region)
|
||||
if tt.errMsg != "" && err != nil && !strings.Contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("GetProviderCredentials() error = %v, wantErr %v", err, err.Error())
|
||||
t.Errorf("GetProviderCredentials() error = %q, wantErr %q", err, err.Error())
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
|
@ -777,7 +774,7 @@ func TestGetProviderCredentials4UCloud(t *testing.T) {
|
|||
creds, _ := yaml.Marshal(&UCloudCredentials{
|
||||
PublicKey: "a",
|
||||
PrivateKey: "b",
|
||||
Region: "bj",
|
||||
Region: "bj1",
|
||||
ProjectID: "c",
|
||||
})
|
||||
secret := &v1.Secret{
|
||||
|
@ -1048,10 +1045,10 @@ func TestGetProviderCredentials4AWS(t *testing.T) {
|
|||
region: "bj",
|
||||
},
|
||||
want: map[string]string{
|
||||
envAWSAccessKeyID: "a",
|
||||
envAWSSecretAccessKey: "b",
|
||||
envAWSSessionToken: "c",
|
||||
envAWSDefaultRegion: "bj",
|
||||
EnvAWSAccessKeyID: "a",
|
||||
EnvAWSSecretAccessKey: "b",
|
||||
EnvAWSSessionToken: "c",
|
||||
EnvAWSDefaultRegion: "bj",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -1254,3 +1251,104 @@ func TestGetProviderCredentials4Custom(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProviderCredentials4HuaweiCloud(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
k8sClient := fake.NewClientBuilder().Build()
|
||||
creds, _ := yaml.Marshal(&HuaWeiCloudCredentials{
|
||||
AccessKey: "a",
|
||||
SecretKey: "b",
|
||||
})
|
||||
secret := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"credentials": creds,
|
||||
},
|
||||
Type: v1.SecretTypeOpaque,
|
||||
}
|
||||
assert.Nil(t, k8sClient.Create(ctx, secret))
|
||||
|
||||
provider := v1beta1.Provider{
|
||||
Spec: v1beta1.ProviderSpec{
|
||||
Provider: string(huawei),
|
||||
Credentials: v1beta1.ProviderCredentials{
|
||||
Source: "Secret",
|
||||
SecretRef: &types.SecretKeySelector{
|
||||
SecretReference: types.SecretReference{
|
||||
Name: "default",
|
||||
Namespace: "default",
|
||||
},
|
||||
Key: "credentials",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
secret2 := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "wrong-data",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"credentials": []byte("xxx"),
|
||||
},
|
||||
Type: v1.SecretTypeOpaque,
|
||||
}
|
||||
k8sClient2 := fake.NewClientBuilder().Build()
|
||||
assert.Nil(t, k8sClient2.Create(ctx, secret2))
|
||||
|
||||
var badProvider v1beta1.Provider
|
||||
copier.CopyWithOption(&badProvider, &provider, copier.Option{DeepCopy: true})
|
||||
badProvider.Spec.Credentials.SecretRef.Name = "wrong-data"
|
||||
|
||||
type args struct {
|
||||
provider v1beta1.Provider
|
||||
region string
|
||||
k8sClient client.Client
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want map[string]string
|
||||
errMsg string
|
||||
}{
|
||||
{
|
||||
name: "provider",
|
||||
args: args{
|
||||
k8sClient: k8sClient,
|
||||
provider: provider,
|
||||
region: "cn-north-4",
|
||||
},
|
||||
want: map[string]string{
|
||||
envHuaWeiCloudAccessKey: "a",
|
||||
envHuaWeiCloudSecretKey: "b",
|
||||
envHuaWeiCloudRegion: "cn-north-4",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "provider with wrong data",
|
||||
args: args{
|
||||
k8sClient: k8sClient2,
|
||||
provider: badProvider,
|
||||
region: "xxx",
|
||||
},
|
||||
errMsg: errConvertCredentials,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GetProviderCredentials(ctx, tt.args.k8sClient, &tt.args.provider, tt.args.region)
|
||||
if tt.errMsg != "" && err != nil && !strings.Contains(err.Error(), tt.errMsg) {
|
||||
t.Errorf("GetProviderCredentials() error = %v, wantErr %v", err, err.Error())
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GetProviderCredentials() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/go-yaml/yaml"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/go-yaml/yaml"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/go-yaml/yaml"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/go-yaml/yaml"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/go-yaml/yaml"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
|
@ -21,7 +21,7 @@ type UCloudCredentials struct {
|
|||
ProjectID string `yaml:"projectID"`
|
||||
}
|
||||
|
||||
func getUCloudCredentials(secretData []byte, name, namespace string) (map[string]string, error) {
|
||||
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)
|
||||
|
@ -30,7 +30,7 @@ func getUCloudCredentials(secretData []byte, name, namespace string) (map[string
|
|||
return map[string]string{
|
||||
envUCloudPublicKey: ak.PublicKey,
|
||||
envUCloudPrivateKey: ak.PrivateKey,
|
||||
envUCloudRegion: ak.Region,
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"github.com/go-yaml/yaml"
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"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"
|
||||
|
@ -61,26 +62,36 @@ func (r *ProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
|
|||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if _, err := providercred.GetProviderCredentials(ctx, r.Client, &provider, provider.Spec.Region); err != nil {
|
||||
provider.Status.State = types.ProviderIsNotReady
|
||||
provider.Status.Message = fmt.Sprintf("%s: %s", errGetCredentials, err.Error())
|
||||
klog.ErrorS(err, errGetCredentials, "Provider", req.NamespacedName)
|
||||
if updateErr := r.Status().Update(ctx, &provider); updateErr != nil {
|
||||
klog.ErrorS(updateErr, errSettingStatus, "Provider", req.NamespacedName)
|
||||
return ctrl.Result{}, errors.Wrap(updateErr, errSettingStatus)
|
||||
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 ctrl.Result{}, errors.Wrap(err, errGetCredentials)
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
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{}, nil
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// SetupWithManager setups with a manager
|
||||
|
|
|
@ -3,19 +3,19 @@ package controllers
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/agiledragon/gomonkey/v2"
|
||||
"github.com/go-yaml/yaml"
|
||||
"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"
|
||||
|
||||
|
@ -30,7 +30,7 @@ func TestReconcile(t *testing.T) {
|
|||
s := runtime.NewScheme()
|
||||
v1beta1.AddToScheme(s)
|
||||
v1.AddToScheme(s)
|
||||
r1.Client = fake.NewClientBuilder().WithScheme(s).Build()
|
||||
r1.Client = fake.NewClientBuilder().WithScheme(s).WithStatusSubresource(&v1beta1.Provider{}).Build()
|
||||
|
||||
r2 := &ProviderReconciler{}
|
||||
provider2 := &v1beta1.Provider{
|
||||
|
@ -69,7 +69,7 @@ func TestReconcile(t *testing.T) {
|
|||
Type: v1.SecretTypeOpaque,
|
||||
}
|
||||
|
||||
r2.Client = fake.NewClientBuilder().WithScheme(s).WithObjects(secret2, provider2).Build()
|
||||
r2.Client = fake.NewClientBuilder().WithScheme(s).WithObjects(secret2, provider2).WithStatusSubresource(&v1beta1.Provider{}).Build()
|
||||
|
||||
r3 := &ProviderReconciler{}
|
||||
provider3 := &v1beta1.Provider{
|
||||
|
@ -92,7 +92,33 @@ func TestReconcile(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
r3.Client = fake.NewClientBuilder().WithScheme(s).WithObjects(provider3).Build()
|
||||
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
|
||||
|
@ -130,21 +156,39 @@ func TestReconcile(t *testing.T) {
|
|||
want: want{},
|
||||
},
|
||||
{
|
||||
name: "Provider is found, but the secret is not available",
|
||||
name: "Provider is found but the secret is not available",
|
||||
args: args{
|
||||
req: req,
|
||||
r: r3,
|
||||
},
|
||||
want: want{
|
||||
errMsg: errGetCredentials,
|
||||
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) {
|
||||
if _, err := tc.args.r.Reconcile(ctx, tc.args.req); (tc.want.errMsg != "") &&
|
||||
!strings.Contains(err.Error(), tc.want.errMsg) {
|
||||
_, 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)
|
||||
}
|
||||
})
|
||||
|
@ -194,7 +238,7 @@ func TestReconcileProviderIsReadyButFailedToUpdateStatus(t *testing.T) {
|
|||
Type: v1.SecretTypeOpaque,
|
||||
}
|
||||
|
||||
r2.Client = fake.NewClientBuilder().WithScheme(s).WithObjects(secret2, provider2).Build()
|
||||
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) {
|
||||
|
@ -277,7 +321,7 @@ func TestReconcile3(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
r3.Client = fake.NewClientBuilder().WithScheme(s).WithObjects(provider3).Build()
|
||||
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) {
|
||||
|
|
|
@ -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,12 +41,20 @@ 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() {
|
||||
|
|
|
@ -5,30 +5,55 @@ import (
|
|||
"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 getPodLog(ctx context.Context, client kubernetes.Interface, namespace, jobName, containerName string) (string, error) {
|
||||
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 || pods == nil || len(pods.Items) == 0 {
|
||||
if err != nil {
|
||||
klog.InfoS("pods are not found", "Label", label, "Error", err)
|
||||
return "", nil
|
||||
|
||||
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 {
|
||||
return "", nil
|
||||
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: containerName})
|
||||
req := client.CoreV1().Pods(namespace).GetLogs(pod.Name, &v1.PodLogOptions{Container: targetContainer})
|
||||
logs, err := req.Stream(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return stage, "", err
|
||||
}
|
||||
defer func(logs io.ReadCloser) {
|
||||
err := logs.Close()
|
||||
|
@ -37,7 +62,14 @@ func getPodLog(ctx context.Context, client kubernetes.Interface, namespace, jobN
|
|||
}
|
||||
}(logs)
|
||||
|
||||
return flushStream(logs, pod.Name)
|
||||
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) {
|
||||
|
@ -50,3 +82,9 @@ func flushStream(rc io.ReadCloser, podName string) (string, error) {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -19,17 +19,21 @@ import (
|
|||
"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
|
||||
client kubernetes.Interface
|
||||
namespace string
|
||||
name string
|
||||
containerName string
|
||||
initContainerName string
|
||||
}
|
||||
type want struct {
|
||||
state types.Stage
|
||||
log string
|
||||
errMsg string
|
||||
}
|
||||
|
@ -78,24 +82,25 @@ func TestGetPodLog(t *testing.T) {
|
|||
{
|
||||
name: "Pod is available, but no logs",
|
||||
args: args{
|
||||
client: k8sClientSet,
|
||||
namespace: "default",
|
||||
name: "j1",
|
||||
containerName: "terraform-executor",
|
||||
client: k8sClientSet,
|
||||
namespace: "default",
|
||||
name: "j1",
|
||||
containerName: "terraform-executor",
|
||||
initContainerName: "terraform-init",
|
||||
},
|
||||
want: want{
|
||||
errMsg: "can not be accept",
|
||||
errMsg: "client rate limiter Wait returned an error: can not be accept",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := getPodLog(ctx, tc.args.client, tc.args.namespace, tc.args.name, tc.args.containerName)
|
||||
if tc.want.errMsg != "" {
|
||||
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.NoError(t, err)
|
||||
assert.Equal(t, tc.want.log, got)
|
||||
assert.Equal(t, tc.want.state, state)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -136,3 +141,40 @@ func TestFlushStream(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,21 +12,22 @@ import (
|
|||
)
|
||||
|
||||
// GetTerraformStatus will get Terraform execution status
|
||||
func GetTerraformStatus(ctx context.Context, namespace, jobName, containerName string) (types.ConfigurationState, error) {
|
||||
klog.InfoS("checking Terraform execution status", "Namespace", namespace, "Job", jobName)
|
||||
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
|
||||
}
|
||||
|
||||
logs, err := getPodLog(ctx, clientSet, namespace, jobName, containerName)
|
||||
// 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)
|
||||
success, state, errMsg := analyzeTerraformLog(logs, stage)
|
||||
if success {
|
||||
return state, nil
|
||||
}
|
||||
|
@ -34,7 +35,8 @@ func GetTerraformStatus(ctx context.Context, namespace, jobName, containerName s
|
|||
return state, errors.New(errMsg)
|
||||
}
|
||||
|
||||
func analyzeTerraformLog(logs string) (bool, types.ConfigurationState, string) {
|
||||
// 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:") {
|
||||
|
@ -42,8 +44,14 @@ func analyzeTerraformLog(logs string) (bool, types.ConfigurationState, string) {
|
|||
if strings.Contains(errMsg, "Invalid Alibaba Cloud region") {
|
||||
return false, types.InvalidRegion, errMsg
|
||||
}
|
||||
return false, types.ConfigurationApplyFailed, errMsg
|
||||
switch stage {
|
||||
case types.InitStage:
|
||||
return false, types.TerraformInitError, errMsg
|
||||
case types.ApplyStage:
|
||||
return false, types.ConfigurationApplyFailed, errMsg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true, types.ConfigurationProvisioningAndChecking, ""
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ func TestGetTerraformStatus(t *testing.T) {
|
|||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
state, err := GetTerraformStatus(ctx, tc.args.namespace, tc.args.name, tc.args.containerName)
|
||||
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 {
|
||||
|
@ -92,7 +92,7 @@ func TestGetTerraformStatus2(t *testing.T) {
|
|||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
state, err := GetTerraformStatus(ctx, tc.args.namespace, tc.args.name, tc.args.containerName)
|
||||
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 {
|
||||
|
@ -143,7 +143,7 @@ func TestAnalyzeTerraformLog(t *testing.T) {
|
|||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
success, state, errMsg := analyzeTerraformLog(tc.args.logs)
|
||||
success, state, errMsg := analyzeTerraformLog(tc.args.logs, types.ApplyStage)
|
||||
if tc.want.errMsg != "" {
|
||||
assert.Contains(t, errMsg, tc.want.errMsg)
|
||||
} else {
|
||||
|
|
|
@ -9,7 +9,8 @@ import (
|
|||
|
||||
func TestDecompressTerraformStateSecret(t *testing.T) {
|
||||
type args struct {
|
||||
data string
|
||||
data string
|
||||
needDecode bool
|
||||
}
|
||||
type want struct {
|
||||
raw string
|
||||
|
@ -23,7 +24,8 @@ func TestDecompressTerraformStateSecret(t *testing.T) {
|
|||
{
|
||||
name: "decompress terraform state secret",
|
||||
args: args{
|
||||
data: "H4sIAAAAAAAA/0SMwa7CIBBF9/0KMutH80ArDb9ijKHDYEhqMQO4afrvBly4POfc3H0QAt7EOaYNrDj/NS7E7ELi5/1XQI3/o4beM3F0K1ihO65xI/egNsLThLPRWi6agkR/CVIppaSZJrfgbBx6//1ItbxqyWDFfnTBlFNlpKaut+EYPgEAAP//xUXpvZsAAAA=",
|
||||
data: "H4sIAAAAAAAA/0SMwa7CIBBF9/0KMutH80ArDb9ijKHDYEhqMQO4afrvBly4POfc3H0QAt7EOaYNrDj/NS7E7ELi5/1XQI3/o4beM3F0K1ihO65xI/egNsLThLPRWi6agkR/CVIppaSZJrfgbBx6//1ItbxqyWDFfnTBlFNlpKaut+EYPgEAAP//xUXpvZsAAAA=",
|
||||
needDecode: true,
|
||||
},
|
||||
want: want{
|
||||
raw: `{
|
||||
|
@ -37,14 +39,26 @@ func TestDecompressTerraformStateSecret(t *testing.T) {
|
|||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bad data",
|
||||
args: args{
|
||||
data: "abc",
|
||||
},
|
||||
want: want{
|
||||
errMsg: "EOF",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testcases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
state, err := base64.StdEncoding.DecodeString(tt.args.data)
|
||||
assert.NoError(t, err)
|
||||
got, err := DecompressTerraformStateSecret(string(state))
|
||||
if tt.want.errMsg != "" {
|
||||
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,18 +1,17 @@
|
|||
package controllers
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
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 {
|
||||
func CreateTerraformExecutorClusterRole(ctx context.Context, k8sClient client.Client, clusterRoleName string) error {
|
||||
var clusterRole = rbacv1.ClusterRole{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
|
@ -44,7 +43,7 @@ func createTerraformExecutorClusterRole(ctx context.Context, k8sClient client.Cl
|
|||
return nil
|
||||
}
|
||||
|
||||
func createTerraformExecutorClusterRoleBinding(ctx context.Context, k8sClient client.Client, namespace, clusterRoleName, serviceAccountName string) error {
|
||||
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{
|
||||
|
@ -77,24 +76,3 @@ func createTerraformExecutorClusterRoleBinding(ctx context.Context, k8sClient cl
|
|||
}
|
||||
return 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
|
||||
}
|
|
@ -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,148 +0,0 @@
|
|||
package e2e
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"gotest.tools/assert"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/controllers/client"
|
||||
)
|
||||
|
||||
const (
|
||||
backendSecretNamespace = "vela-system"
|
||||
)
|
||||
|
||||
var (
|
||||
testConfigurationsBasic = "examples/alibaba/eip/configuration_eip.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/rds/configuration_hcl_rds.yaml",
|
||||
"examples/alibaba/oss/configuration_hcl_bucket.yaml",
|
||||
}
|
||||
)
|
||||
|
||||
func TestBasicConfiguration(t *testing.T) {
|
||||
clientSet, err := client.Init()
|
||||
assert.NilError(t, err)
|
||||
ctx := context.Background()
|
||||
|
||||
klog.Info("1. Applying Configuration")
|
||||
pwd, _ := os.Getwd()
|
||||
configuration := filepath.Join(pwd, "..", testConfigurationsBasic)
|
||||
cmd := fmt.Sprintf("kubectl apply -f %s", configuration)
|
||||
err = exec.Command("bash", "-c", cmd).Start()
|
||||
assert.NilError(t, err)
|
||||
|
||||
klog.Info("2. Checking Configuration status")
|
||||
for i := 0; i < 60; i++ {
|
||||
var fields []string
|
||||
output, err := exec.Command("bash", "-c", "kubectl get configuration").Output()
|
||||
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] == "alibaba-eip" && fields[1] == Available {
|
||||
goto continueCheck
|
||||
}
|
||||
}
|
||||
if i == 59 {
|
||||
t.Error("Configuration is not ready")
|
||||
}
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
|
||||
continueCheck:
|
||||
klog.Info("3. Checking Configuration status")
|
||||
|
||||
klog.Info("- Checking ConfigMap which stores .tf")
|
||||
_, err = clientSet.CoreV1().ConfigMaps("default").Get(ctx, "tf-alibaba-eip", v1.GetOptions{})
|
||||
assert.NilError(t, err)
|
||||
|
||||
klog.Info("- Checking Secret which stores Backend")
|
||||
_, err = clientSet.CoreV1().Secrets(backendSecretNamespace).Get(ctx, "tfstate-default-alibaba-eip", v1.GetOptions{})
|
||||
assert.NilError(t, err)
|
||||
|
||||
klog.Info("- Checking Secret which stores outputs")
|
||||
_, err = clientSet.CoreV1().Secrets("default").Get(ctx, "eip-conn", v1.GetOptions{})
|
||||
assert.NilError(t, err)
|
||||
|
||||
klog.Info("- Checking Secret which stores variables")
|
||||
_, err = clientSet.CoreV1().Secrets("default").Get(ctx, "variable-alibaba-eip", v1.GetOptions{})
|
||||
assert.NilError(t, err)
|
||||
|
||||
klog.Info("4. 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").Output()
|
||||
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] == "alibaba-eip" {
|
||||
existed = true
|
||||
}
|
||||
}
|
||||
if existed {
|
||||
if i == 59 {
|
||||
t.Error("Configuration is not ready")
|
||||
}
|
||||
|
||||
time.Sleep(time.Second * 5)
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
klog.Info("6. Checking Secrets and ConfigMap which should all be deleted")
|
||||
_, err = clientSet.CoreV1().Secrets("default").Get(ctx, "variable-alibaba-eip", v1.GetOptions{})
|
||||
assert.Equal(t, kerrors.IsNotFound(err), true)
|
||||
|
||||
_, err = clientSet.CoreV1().Secrets("default").Get(ctx, "eip-conn", v1.GetOptions{})
|
||||
assert.Equal(t, kerrors.IsNotFound(err), true)
|
||||
|
||||
_, err = clientSet.CoreV1().Secrets(backendSecretNamespace).Get(ctx, "tfstate-default-alibaba-eip", v1.GetOptions{})
|
||||
assert.Equal(t, kerrors.IsNotFound(err), true)
|
||||
|
||||
_, err = clientSet.CoreV1().ConfigMaps("default").Get(ctx, "tf-alibaba-eip", v1.GetOptions{})
|
||||
assert.Equal(t, kerrors.IsNotFound(err), true)
|
||||
}
|
||||
|
||||
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,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)
|
||||
//}
|
|
@ -1,4 +1,4 @@
|
|||
package e2e
|
||||
package normal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -15,6 +15,7 @@ import (
|
|||
|
||||
// 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) {
|
||||
|
@ -103,8 +104,7 @@ deletion:
|
|||
|
||||
time.Sleep(time.Second * 5)
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
apiVersion: terraform.core.oam.dev/v1beta2
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: alibaba-eip-inline
|
||||
spec:
|
||||
hcl: |
|
||||
provider "alicloud" {
|
||||
access_key = var.access_key
|
||||
secret_key = var.secret_key
|
||||
region = var.region
|
||||
}
|
||||
|
||||
resource "alicloud_eip" "this" {
|
||||
bandwidth = var.bandwidth
|
||||
address_name = var.address_name
|
||||
}
|
||||
|
||||
variable "address_name" {
|
||||
description = "Name to be used on all resources as prefix. Default to 'TF-Module-EIP'."
|
||||
default = "TF-Module-EIP"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "bandwidth" {
|
||||
description = "Maximum bandwidth to the elastic public network, measured in Mbps (Mega bit per second)."
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "access_key" {
|
||||
description = "Access Key ID of the Alibaba Cloud account."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "secret_key" {
|
||||
description = "Access Key Secret of the Alibaba Cloud account."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "Region of the Alibaba Cloud account."
|
||||
type = string
|
||||
default = "cn-beijing"
|
||||
}
|
||||
|
||||
output "EIP_ADDRESS" {
|
||||
description = "The elastic ip address."
|
||||
value = alicloud_eip.this.ip_address
|
||||
}
|
||||
|
||||
output "NAME" {
|
||||
value = var.address_name
|
||||
}
|
||||
|
||||
variable:
|
||||
access_key: xxx
|
||||
secret_key: yyy
|
||||
|
||||
inlineCredentials: true
|
||||
|
||||
writeConnectionSecretToRef:
|
||||
name: eip-e2e-inline
|
||||
namespace: default
|
||||
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
apiVersion: terraform.core.oam.dev/v1beta1
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: alibaba-oss-bucket-json
|
||||
spec:
|
||||
JSON: |
|
||||
{
|
||||
"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": "poc"
|
||||
},
|
||||
"acl": {
|
||||
"default": "private"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable:
|
||||
bucket: "vela-website"
|
||||
acl: "private"
|
||||
|
||||
writeConnectionSecretToRef:
|
||||
name: oss-conn
|
||||
namespace: default
|
|
@ -0,0 +1,42 @@
|
|||
apiVersion: terraform.core.oam.dev/v1beta2
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: alibaba-oss-bucket-hcl-backend-example
|
||||
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" {
|
||||
description = "OSS bucket name"
|
||||
default = "loheagn-terraform-controller-2"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "acl" {
|
||||
description = "OSS bucket ACL, supported 'private', 'public-read', 'public-read-write'"
|
||||
default = "private"
|
||||
type = string
|
||||
}
|
||||
|
||||
backend:
|
||||
inline: |
|
||||
terraform {
|
||||
backend "kubernetes" {
|
||||
secret_suffix = "a"
|
||||
}
|
||||
}
|
||||
|
||||
variable:
|
||||
bucket: "terraform-controller-20220523"
|
||||
acl: "private"
|
||||
|
||||
writeConnectionSecretToRef:
|
||||
name: oss-conn
|
||||
namespace: default
|
|
@ -0,0 +1,40 @@
|
|||
apiVersion: terraform.core.oam.dev/v1beta2
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: alibaba-oss-bucket-hcl-backend-example
|
||||
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" {
|
||||
description = "OSS bucket name"
|
||||
default = "loheagn-terraform-controller-2"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "acl" {
|
||||
description = "OSS bucket ACL, supported 'private', 'public-read', 'public-read-write'"
|
||||
default = "private"
|
||||
type = string
|
||||
}
|
||||
|
||||
backend:
|
||||
backendType: kubernetes
|
||||
kubernetes:
|
||||
secret_suffix: 'ali-oss'
|
||||
namespace: 'terraform'
|
||||
|
||||
variable:
|
||||
bucket: "terraform-controller-20220523"
|
||||
acl: "private"
|
||||
|
||||
writeConnectionSecretToRef:
|
||||
name: oss-conn
|
||||
namespace: default
|
|
@ -0,0 +1,28 @@
|
|||
apiVersion: terraform.core.oam.dev/v1beta2
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: aws-s3
|
||||
spec:
|
||||
remote: https://github.com/kubevela-contrib/terraform-modules.git
|
||||
path: aws/s3
|
||||
|
||||
variable:
|
||||
bucket: "vela-website-aws-20220628"
|
||||
acl: "private"
|
||||
|
||||
backend:
|
||||
backendType: s3
|
||||
s3:
|
||||
region: us-east-1
|
||||
bucket: tf-controller-test
|
||||
key: example.state
|
||||
|
||||
deleteResource: true
|
||||
|
||||
providerRef:
|
||||
name: aws
|
||||
namespace: default
|
||||
|
||||
writeConnectionSecretToRef:
|
||||
name: s3-conn
|
||||
namespace: default
|
|
@ -0,0 +1,86 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: simple-terraform-module
|
||||
namespace: default
|
||||
data:
|
||||
main.tf: |
|
||||
resource "tls_private_key" "private_key" {
|
||||
algorithm = var.algorithm
|
||||
}
|
||||
outputs.tf: |
|
||||
output "openssh_private_key" {
|
||||
value = nonsensitive(tls_private_key.private_key.private_key_pem)
|
||||
}
|
||||
variables.tf: |
|
||||
variable "algorithm" {
|
||||
description = "Encryption algorithm for the private key"
|
||||
type = string
|
||||
}
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: git-push
|
||||
namespace: default
|
||||
labels:
|
||||
name: git-push
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
initContainers:
|
||||
- name: wait-for-git-server
|
||||
image: alpine/git
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- >-
|
||||
set -x &&
|
||||
until nc -vzw 2 git-server 22; do sleep 10; done
|
||||
volumeMounts:
|
||||
- mountPath: /usr/.ssh
|
||||
name: ssh-keys
|
||||
containers:
|
||||
- name: git-push
|
||||
image: alpine/git
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- >-
|
||||
set -x &&
|
||||
eval $(ssh-agent) &&
|
||||
ssh-add /usr/.ssh/id_rsa &&
|
||||
mkdir /root/.ssh &&
|
||||
ssh-keyscan git-server >> /root/.ssh/known_hosts &&
|
||||
mkdir /usr/simple-terraform-module &&
|
||||
cd /usr/simple-terraform-module &&
|
||||
cp /simple-terraform-module/*.tf . &&
|
||||
git config --global init.defaultBranch master &&
|
||||
git init &&
|
||||
git config user.name "john.doe" &&
|
||||
git config user.email "john@doe.com" &&
|
||||
git add . &&
|
||||
git commit -m "initial commit" &&
|
||||
git remote add origin git@git-server:simple-terraform-module.git &&
|
||||
git push --set-upstream origin master
|
||||
volumeMounts:
|
||||
- mountPath: /usr/.ssh
|
||||
name: ssh-keys
|
||||
- mountPath: /simple-terraform-module
|
||||
name: simple-terraform-module
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "0.1"
|
||||
volumes:
|
||||
- name: ssh-keys
|
||||
secret:
|
||||
secretName: ssh-keys
|
||||
items:
|
||||
- key: id_rsa
|
||||
path: id_rsa
|
||||
defaultMode: 0400
|
||||
- name: simple-terraform-module
|
||||
configMap:
|
||||
name: simple-terraform-module
|
|
@ -0,0 +1,85 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: init-git-server-script
|
||||
namespace: default
|
||||
data:
|
||||
init-git-server.sh: |
|
||||
#!/bin/sh
|
||||
set -x
|
||||
mkdir -p ~/.ssh
|
||||
chmod 0700 ~/.ssh
|
||||
touch ~/.ssh/authorized_keys
|
||||
chmod 0600 ~/.ssh/authorized_keys
|
||||
mkdir simple-terraform-module.git
|
||||
git config --global init.defaultBranch master &>/dev/null
|
||||
git init --bare simple-terraform-module.git
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: git-server
|
||||
namespace: default
|
||||
labels:
|
||||
name: git-server
|
||||
spec:
|
||||
containers:
|
||||
- name: git-server
|
||||
image: ubuntu:22.04
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- >-
|
||||
apt update &&
|
||||
apt install git openssh-server -y &&
|
||||
mkdir /var/run/sshd &&
|
||||
useradd -r -m -U -d /home/git -s /bin/bash git &&
|
||||
su - git -c /tmp/scripts/init-git-server.sh &&
|
||||
cat ~/.ssh/authorized_keys >> /home/git/.ssh/authorized_keys &&
|
||||
/usr/sbin/sshd -D
|
||||
lifecycle:
|
||||
preStop:
|
||||
exec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- service ssh stop
|
||||
volumeMounts:
|
||||
- mountPath: /root/.ssh
|
||||
name: ssh-keys
|
||||
- mountPath: /tmp/scripts
|
||||
name: init-git-server
|
||||
ports:
|
||||
- containerPort: 22
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: 22
|
||||
initialDelaySeconds: 30
|
||||
resources:
|
||||
limits:
|
||||
memory: "500Mi"
|
||||
cpu: "0.2"
|
||||
volumes:
|
||||
- name: ssh-keys
|
||||
secret:
|
||||
secretName: ssh-keys
|
||||
items:
|
||||
- key: id_rsa.pub
|
||||
path: authorized_keys
|
||||
- name: init-git-server
|
||||
configMap:
|
||||
name: init-git-server-script
|
||||
defaultMode: 0555
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: git-server
|
||||
namespace: default
|
||||
spec:
|
||||
ports:
|
||||
- port: 22
|
||||
protocol: TCP
|
||||
targetPort: 22
|
||||
selector:
|
||||
name: git-server
|
|
@ -0,0 +1,10 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ssh-keys
|
||||
namespace: default
|
||||
type: Opaque
|
||||
data:
|
||||
id_rsa: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBbCtDTXIvbXl4eDlrNnJoUTlvbXhEQk1ZemYvMTJwTisvb24zSjJTTTVQUjhndFdOCk1mL2taWmdGQWNHYk55T3o5emI4MFVqTWNKY2lCNXdzNFVBcTJEa2ZzU090dllQbW5Nand2dUJId3p3cmpBcEgKT1ZVN1ozbjQ2VUpya2crUkVoWElVbGRYcXQzellOSllWZXF3MHhTOTBpNFNyR1RLS3VrR1FGQVh1L3Q5YzBERApDNHFGZXk1UXlQYndxMWFhN01uVDlqRHZvdWQxMjhtUnZoSzNSQkxZQjE3SURIQ3o4bURQNllkYVJJLys5MHdTCkh3c0dTdk44K3FtbTdzazFkdkJ2RkhNTllnaDl2S09mbDNuQitOOWlNQ2pOV1BvMFdBZkl1VHVCU0ozMnVHZ3UKdkxSZFJocXVpWlAyenpraEhpSnUzdHlVWDRWWHp2TXMwdSsyY1FJREFRQUJBb0lCQVFDS2ZVZk1iM1NGL2lxWApuZHExOUhocytqejBHeUtrWFRyQUFDNU96WEZzbFVPMFNlYW1ZU0J6UTF2TmJpMks2aE9BcWJOL1kxS0ltRWQvCmlQbWpyRTlsT3pHYTVWM1lJaDUzZVFPT0NoVm1BY2Z6dXF1WHJCQ3ZHcG5PbWJKZFRiU0xPVEdoWStPYyt5YWkKY3l2NXJEZnhRa2lWRDA0WHhSQlVjSWd5dk5Ybm90UHcwbmVEVHhISDN5N3VWL0tLRE9YUjVWdmZ3RzJJelpzRQp0UUNtejRRUHNRYUpLOGdMYk1BTC8xTmcwV0oxUjI0UFhlMzlid3ZqRytnMC9MYmFrcFB4YTJ2Tjc1VmZScEJXCjdaYVd3L2ZGWStwYjdjdENCSGVFQ3E1T1I1L0lOWGJuVEFUMHJKTlM0eEdEeUR6am9hOGtGamR0bGU0bmdKU2YKaG1wbFdxa0JBb0dCQU1Md3pZRDBEMEhxTWswZ3VuWlNsTDBhK3ZzdFF5S1VwOURvTHNSRnFNeEgrakt3c1J4eApWaE1nVzQrTEpSenllV0lPOWJWajkrZUY1SUxsQXo3WithQVNYdjVVdHlVSzJyM1I4N3FCcjFTMDhjS3hlYi9hCjVualBHbkl6aEVTOHNXRExmS2N6MkhYMnF5MWlsL1NNdloyQ2JZMllLYVk4ekNrU1JDQ2FIS2ZGQW9HQkFNZHkKdWhmN29DK0Z6dUtoanUzQ3l0aU1EZGhybDVhWVBmdWxWTHNlamxYR1ZxWU1nZDRXNzg5QTdCbUJZMERzdmh6RwprRnZ0bWhzUEw3RGhEdUZ2bGtvdnVkS25XbTJvUjZqbDFGNkZWSGxzN3lIS3FTQ2R2bjdXL0ZhZSsxL1loOFFSCk8vR2lBY1c2UTJuMHhVOTBVVmwvMnlVZGhHakVKSGN2S3hCcnpCSzlBb0dCQUpqWjdaanVSVVJlMlFBbTZHMjgKaE1uZWJPc2o2MThqQm83RWIxOFFhN0Y1d3BHYWY5VVlmUEJVVDlhMnVPd0FwL0tlWGtUVFZOK2gyYkpVMVgyagp0cHF2clBKcEJJMnovQjRZa0s0dDM0ZGd0YXYrTXNPZlpWVld0cHJURUNSQmZDZTBobElvVWRMMURmVnhPRXJWClRCeEQxNWpOdGVLV0MxTXM4bVJKMHF3dEFvR0FNbFUrcDJ6RitSaEFwS3IyNGdQRm95NTlFLy9iQ3BNekdUMloKQzN2am1idnJCQTZsKzRFNFZjcGhpdkkvTlJSSnlnTkdUUnpDUmsvbnpqQ055OUNZVWZLSFo2VDZTakFzblhBYQp6eHZBdk1BRC9UZ2l4R3RxdHFIVW5wdVNmcGFyZEl5UTN5THVaWkxqRG10S0hBb1R1WTF0cFlrMGNDZ0h0OWc4CmV2RnBWOVVDZ1lBejlmMk9ISzNVR254dDZOOXM0ZVpUak9TVDRSWUVzR0JOZ1pSV0JVc0pHMkRJdkE2Sks3T3QKc3hwTWwzYUpLVWFCWmswS3FReVlrNTkrRTRkeis4UFZjVmN1bGoxQUtYRVR1a3BQaUU3ZFhlTXpjaUxSNUt4YwpvVmFoWVFXSlg0N016aFVTMWxJSVFJcmlsN0JTNmFnbTlnV2NoVkpIVGNHYVY1ZFZBdmI2VXc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
|
||||
|
||||
id_rsa.pub: c3NoLXJzYSBBQUFBQjNOemFDMXljMkVBQUFBREFRQUJBQUFCQVFDWDRJeXYrYkxISDJUcXVGRDJpYkVNRXhqTi8vWGFrMzcraWZjblpJems5SHlDMVkweC8rUmxtQVVCd1pzM0k3UDNOdnpSU014d2x5SUhuQ3poUUNyWU9SK3hJNjI5ZythY3lQQys0RWZEUEN1TUNrYzVWVHRuZWZqcFFtdVNENUVTRmNoU1YxZXEzZk5nMGxoVjZyRFRGTDNTTGhLc1pNb3E2UVpBVUJlNyszMXpRTU1MaW9WN0xsREk5dkNyVnByc3lkUDJNTytpNTNYYnlaRytFcmRFRXRnSFhzZ01jTFB5WU0vcGgxcEVqLzczVEJJZkN3Wks4M3o2cWFidXlUVjI4RzhVY3cxaUNIMjhvNStYZWNINDMySXdLTTFZK2pSWUI4aTVPNEZJbmZhNGFDNjh0RjFHR3E2SmsvYlBPU0VlSW03ZTNKUmZoVmZPOHl6Uzc3WngK
|
|
@ -0,0 +1,10 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: git-ssh-auth
|
||||
namespace: default
|
||||
type: kubernetes.io/ssh-auth
|
||||
data:
|
||||
ssh-privatekey: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBbCtDTXIvbXl4eDlrNnJoUTlvbXhEQk1ZemYvMTJwTisvb24zSjJTTTVQUjhndFdOCk1mL2taWmdGQWNHYk55T3o5emI4MFVqTWNKY2lCNXdzNFVBcTJEa2ZzU090dllQbW5Nand2dUJId3p3cmpBcEgKT1ZVN1ozbjQ2VUpya2crUkVoWElVbGRYcXQzellOSllWZXF3MHhTOTBpNFNyR1RLS3VrR1FGQVh1L3Q5YzBERApDNHFGZXk1UXlQYndxMWFhN01uVDlqRHZvdWQxMjhtUnZoSzNSQkxZQjE3SURIQ3o4bURQNllkYVJJLys5MHdTCkh3c0dTdk44K3FtbTdzazFkdkJ2RkhNTllnaDl2S09mbDNuQitOOWlNQ2pOV1BvMFdBZkl1VHVCU0ozMnVHZ3UKdkxSZFJocXVpWlAyenpraEhpSnUzdHlVWDRWWHp2TXMwdSsyY1FJREFRQUJBb0lCQVFDS2ZVZk1iM1NGL2lxWApuZHExOUhocytqejBHeUtrWFRyQUFDNU96WEZzbFVPMFNlYW1ZU0J6UTF2TmJpMks2aE9BcWJOL1kxS0ltRWQvCmlQbWpyRTlsT3pHYTVWM1lJaDUzZVFPT0NoVm1BY2Z6dXF1WHJCQ3ZHcG5PbWJKZFRiU0xPVEdoWStPYyt5YWkKY3l2NXJEZnhRa2lWRDA0WHhSQlVjSWd5dk5Ybm90UHcwbmVEVHhISDN5N3VWL0tLRE9YUjVWdmZ3RzJJelpzRQp0UUNtejRRUHNRYUpLOGdMYk1BTC8xTmcwV0oxUjI0UFhlMzlid3ZqRytnMC9MYmFrcFB4YTJ2Tjc1VmZScEJXCjdaYVd3L2ZGWStwYjdjdENCSGVFQ3E1T1I1L0lOWGJuVEFUMHJKTlM0eEdEeUR6am9hOGtGamR0bGU0bmdKU2YKaG1wbFdxa0JBb0dCQU1Md3pZRDBEMEhxTWswZ3VuWlNsTDBhK3ZzdFF5S1VwOURvTHNSRnFNeEgrakt3c1J4eApWaE1nVzQrTEpSenllV0lPOWJWajkrZUY1SUxsQXo3WithQVNYdjVVdHlVSzJyM1I4N3FCcjFTMDhjS3hlYi9hCjVualBHbkl6aEVTOHNXRExmS2N6MkhYMnF5MWlsL1NNdloyQ2JZMllLYVk4ekNrU1JDQ2FIS2ZGQW9HQkFNZHkKdWhmN29DK0Z6dUtoanUzQ3l0aU1EZGhybDVhWVBmdWxWTHNlamxYR1ZxWU1nZDRXNzg5QTdCbUJZMERzdmh6RwprRnZ0bWhzUEw3RGhEdUZ2bGtvdnVkS25XbTJvUjZqbDFGNkZWSGxzN3lIS3FTQ2R2bjdXL0ZhZSsxL1loOFFSCk8vR2lBY1c2UTJuMHhVOTBVVmwvMnlVZGhHakVKSGN2S3hCcnpCSzlBb0dCQUpqWjdaanVSVVJlMlFBbTZHMjgKaE1uZWJPc2o2MThqQm83RWIxOFFhN0Y1d3BHYWY5VVlmUEJVVDlhMnVPd0FwL0tlWGtUVFZOK2gyYkpVMVgyagp0cHF2clBKcEJJMnovQjRZa0s0dDM0ZGd0YXYrTXNPZlpWVld0cHJURUNSQmZDZTBobElvVWRMMURmVnhPRXJWClRCeEQxNWpOdGVLV0MxTXM4bVJKMHF3dEFvR0FNbFUrcDJ6RitSaEFwS3IyNGdQRm95NTlFLy9iQ3BNekdUMloKQzN2am1idnJCQTZsKzRFNFZjcGhpdkkvTlJSSnlnTkdUUnpDUmsvbnpqQ055OUNZVWZLSFo2VDZTakFzblhBYQp6eHZBdk1BRC9UZ2l4R3RxdHFIVW5wdVNmcGFyZEl5UTN5THVaWkxqRG10S0hBb1R1WTF0cFlrMGNDZ0h0OWc4CmV2RnBWOVVDZ1lBejlmMk9ISzNVR254dDZOOXM0ZVpUak9TVDRSWUVzR0JOZ1pSV0JVc0pHMkRJdkE2Sks3T3QKc3hwTWwzYUpLVWFCWmswS3FReVlrNTkrRTRkeis4UFZjVmN1bGoxQUtYRVR1a3BQaUU3ZFhlTXpjaUxSNUt4YwpvVmFoWVFXSlg0N016aFVTMWxJSVFJcmlsN0JTNmFnbTlnV2NoVkpIVGNHYVY1ZFZBdmI2VXc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
|
||||
|
||||
known_hosts: {{ . }}
|
|
@ -0,0 +1,34 @@
|
|||
apiVersion: terraform.core.oam.dev/v1beta1
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: huawei-vpc-hcl
|
||||
spec:
|
||||
hcl: |
|
||||
terraform {
|
||||
required_providers {
|
||||
huaweicloud = {
|
||||
source = "huaweicloud/huaweicloud"
|
||||
version = "~> 1.26.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "huaweicloud_vpc" "myvpc" {
|
||||
name = "myvpc"
|
||||
cidr = "192.168.0.0/16"
|
||||
}
|
||||
|
||||
resource "huaweicloud_vpc_subnet" "mysubnet" {
|
||||
name = "mysubnet"
|
||||
cidr = "192.168.0.0/16"
|
||||
gateway_ip = "192.168.0.1"
|
||||
|
||||
//dns is required for cce node installing
|
||||
primary_dns = "100.125.1.250"
|
||||
secondary_dns = "100.125.21.250"
|
||||
vpc_id = huaweicloud_vpc.myvpc.id
|
||||
}
|
||||
|
||||
writeConnectionSecretToRef:
|
||||
name: huaweicloud-vpc-conn
|
||||
namespace: default
|
|
@ -0,0 +1,13 @@
|
|||
apiVersion: terraform.core.oam.dev/v1beta1
|
||||
kind: Provider
|
||||
metadata:
|
||||
name: default
|
||||
spec:
|
||||
provider: huawei
|
||||
region: cn-north-4
|
||||
credentials:
|
||||
source: Secret
|
||||
secretRef:
|
||||
namespace: vela-system
|
||||
name: huawei-account-creds
|
||||
key: credentials
|
|
@ -0,0 +1,18 @@
|
|||
apiVersion: terraform.core.oam.dev/v1beta2
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: random-e2e-delete-provisioning-resources
|
||||
spec:
|
||||
hcl: |
|
||||
resource "random_id" "server" {
|
||||
byte_length = 8
|
||||
}
|
||||
resource "random_id" "error" {
|
||||
byte_length = -1
|
||||
}
|
||||
|
||||
inlineCredentials: true
|
||||
|
||||
writeConnectionSecretToRef:
|
||||
name: random-conn
|
||||
namespace: default
|
|
@ -0,0 +1,20 @@
|
|||
apiVersion: terraform.core.oam.dev/v1beta2
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: random-e2e-force-delete
|
||||
spec:
|
||||
hcl: |
|
||||
resource "random_id" "server" {
|
||||
byte_length = 8
|
||||
}
|
||||
|
||||
output "random_id" {
|
||||
value = random_id.server.hex
|
||||
|
||||
inlineCredentials: true
|
||||
|
||||
forceDelete: true
|
||||
|
||||
writeConnectionSecretToRef:
|
||||
name: random-conn
|
||||
namespace: default
|
|
@ -0,0 +1,15 @@
|
|||
apiVersion: terraform.core.oam.dev/v1beta2
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: random-e2e-git-creds-secret-ref
|
||||
spec:
|
||||
inlineCredentials: true
|
||||
remote: git@git-server:simple-terraform-module.git
|
||||
variable:
|
||||
algorithm: RSA
|
||||
writeConnectionSecretToRef:
|
||||
name: random-e2e-git-creds-secret-ref-conn
|
||||
gitCredentialsSecretReference:
|
||||
name: git-ssh-auth
|
||||
namespace: default
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
apiVersion: terraform.core.oam.dev/v1beta2
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: random-e2e
|
||||
spec:
|
||||
hcl: |
|
||||
resource "random_id" "server" {
|
||||
byte_length = 8
|
||||
}
|
||||
|
||||
output "random_id" {
|
||||
value = random_id.server.hex
|
||||
}
|
||||
|
||||
inlineCredentials: true
|
||||
|
||||
writeConnectionSecretToRef:
|
||||
name: random-conn
|
||||
namespace: default
|
|
@ -0,0 +1,25 @@
|
|||
apiVersion: terraform.core.oam.dev/v1beta2
|
||||
kind: Configuration
|
||||
metadata:
|
||||
name: random-e2e-custom-backend-kubernetes
|
||||
spec:
|
||||
hcl: |
|
||||
resource "random_id" "server" {
|
||||
byte_length = 8
|
||||
}
|
||||
|
||||
output "random_id" {
|
||||
value = random_id.server.hex
|
||||
}
|
||||
|
||||
backend:
|
||||
backendType: kubernetes
|
||||
kubernetes:
|
||||
secret_suffix: custom-backend-kubernetes
|
||||
namespace: a
|
||||
|
||||
inlineCredentials: true
|
||||
|
||||
writeConnectionSecretToRef:
|
||||
name: random-conn-custom-backend-kubernetes
|
||||
namespace: default
|
|
@ -0,0 +1,26 @@
|
|||
resource "aws_s3_bucket" "bucket-acl" {
|
||||
bucket = var.bucket
|
||||
acl = var.acl
|
||||
}
|
||||
|
||||
output "RESOURCE_IDENTIFIER" {
|
||||
description = "The identifier of the resource"
|
||||
value = aws_s3_bucket.bucket-acl.bucket_domain_name
|
||||
}
|
||||
|
||||
output "BUCKET_NAME" {
|
||||
value = aws_s3_bucket.bucket-acl.bucket_domain_name
|
||||
description = "The name of the S3 bucket"
|
||||
}
|
||||
|
||||
variable "bucket" {
|
||||
description = "S3 bucket name"
|
||||
default = "vela-website-2022-0614"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "acl" {
|
||||
description = "S3 bucket ACL"
|
||||
default = "private"
|
||||
type = string
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
#vAWS VPC
|
||||
|
||||
Based on the Terraform template in https://github.com/terraform-aws-modules/terraform-aws-vpc
|
|
@ -0,0 +1,88 @@
|
|||
resource "aws_vpc" "this" {
|
||||
count = var.create_vpc ? 1 : 0
|
||||
|
||||
cidr_block = var.cidr
|
||||
instance_tenancy = var.instance_tenancy
|
||||
enable_dns_hostnames = var.enable_dns_hostnames
|
||||
enable_dns_support = var.enable_dns_support
|
||||
enable_classiclink = var.enable_classiclink
|
||||
enable_classiclink_dns_support = var.enable_classiclink_dns_support
|
||||
assign_generated_ipv6_cidr_block = var.enable_ipv6
|
||||
|
||||
tags = merge(
|
||||
{ "Name" = var.name },
|
||||
var.tags,
|
||||
var.vpc_tags,
|
||||
)
|
||||
}
|
||||
|
||||
output "vpc_id" {
|
||||
description = "The ID of the VPC"
|
||||
value = try(aws_vpc.this[0].id, "")
|
||||
}
|
||||
|
||||
output "vpc_arn" {
|
||||
description = "The ARN of the VPC"
|
||||
value = try(aws_vpc.this[0].arn, "")
|
||||
}
|
||||
|
||||
output "vpc_cidr_block" {
|
||||
description = "The CIDR block of the VPC"
|
||||
value = try(aws_vpc.this[0].cidr_block, "")
|
||||
}
|
||||
|
||||
output "default_security_group_id" {
|
||||
description = "The ID of the security group created by default on VPC creation"
|
||||
value = try(aws_vpc.this[0].default_security_group_id, "")
|
||||
}
|
||||
|
||||
output "default_network_acl_id" {
|
||||
description = "The ID of the default network ACL"
|
||||
value = try(aws_vpc.this[0].default_network_acl_id, "")
|
||||
}
|
||||
|
||||
output "default_route_table_id" {
|
||||
description = "The ID of the default route table"
|
||||
value = try(aws_vpc.this[0].default_route_table_id, "")
|
||||
}
|
||||
|
||||
output "vpc_instance_tenancy" {
|
||||
description = "Tenancy of instances spin up within VPC"
|
||||
value = try(aws_vpc.this[0].instance_tenancy, "")
|
||||
}
|
||||
|
||||
output "vpc_enable_dns_support" {
|
||||
description = "Whether or not the VPC has DNS support"
|
||||
value = try(aws_vpc.this[0].enable_dns_support, "")
|
||||
}
|
||||
|
||||
output "vpc_enable_dns_hostnames" {
|
||||
description = "Whether or not the VPC has DNS hostname support"
|
||||
value = try(aws_vpc.this[0].enable_dns_hostnames, "")
|
||||
}
|
||||
|
||||
output "vpc_main_route_table_id" {
|
||||
description = "The ID of the main route table associated with this VPC"
|
||||
value = try(aws_vpc.this[0].main_route_table_id, "")
|
||||
}
|
||||
|
||||
output "vpc_ipv6_association_id" {
|
||||
description = "The association ID for the IPv6 CIDR block"
|
||||
value = try(aws_vpc.this[0].ipv6_association_id, "")
|
||||
}
|
||||
|
||||
output "vpc_ipv6_cidr_block" {
|
||||
description = "The IPv6 CIDR block"
|
||||
value = try(aws_vpc.this[0].ipv6_cidr_block, "")
|
||||
}
|
||||
|
||||
output "vpc_owner_id" {
|
||||
description = "The ID of the AWS account that owns the VPC"
|
||||
value = try(aws_vpc.this[0].owner_id, "")
|
||||
}
|
||||
|
||||
|
||||
output "name" {
|
||||
description = "The name of the VPC specified as argument to this module"
|
||||
value = var.name
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
variable "create_vpc" {
|
||||
description = "Controls if VPC should be created (it affects almost all resources)"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "name" {
|
||||
description = "Name to be used on all the resources as identifier"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "cidr" {
|
||||
description = "The CIDR block for the VPC. Default value is a valid CIDR, but not acceptable by AWS and should be overridden"
|
||||
type = string
|
||||
default = "10.0.0.0/16"
|
||||
}
|
||||
|
||||
variable "enable_ipv6" {
|
||||
description = "Requests an Amazon-provided IPv6 CIDR block with a /56 prefix length for the VPC. You cannot specify the range of IP addresses, or the size of the CIDR block."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "instance_tenancy" {
|
||||
description = "A tenancy option for instances launched into the VPC"
|
||||
type = string
|
||||
default = "default"
|
||||
}
|
||||
|
||||
variable "enable_dns_hostnames" {
|
||||
description = "Should be true to enable DNS hostnames in the VPC"
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "enable_dns_support" {
|
||||
description = "Should be true to enable DNS support in the VPC"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "enable_classiclink" {
|
||||
description = "Should be true to enable ClassicLink for the VPC. Only valid in regions and accounts that support EC2 Classic."
|
||||
type = bool
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "enable_classiclink_dns_support" {
|
||||
description = "Should be true to enable ClassicLink DNS Support for the VPC. Only valid in regions and accounts that support EC2 Classic."
|
||||
type = bool
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
description = "A map of tags to add to all resources"
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "vpc_tags" {
|
||||
description = "Additional tags for the VPC"
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
terraform {
|
||||
backend "s3" {
|
||||
bucket = "tf-state-poc-0608"
|
||||
key = "sss"
|
||||
region = "us-east-1"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
resource "random_id" "server" {
|
||||
byte_length = 8
|
||||
}
|
||||
|
||||
output "random_id" {
|
||||
value = random_id.server.hex
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
resource "random_id" "server" {
|
||||
byte_length = 8
|
||||
}
|
||||
|
||||
output "random_id" {
|
||||
value = random_id.server.hex
|
||||
}
|
|
@ -495,7 +495,7 @@ es-connection Opaque 1 7m37s
|
|||
|
||||
## KubeVela Terraform Addon
|
||||
|
||||
Terraform Controller is well integrated in [KuberVela](https://github.com/oam-dev/kubevela) as a Terraform addon. Enabling
|
||||
Terraform Controller is well integrated in [KubeVela](https://github.com/kubevela/kubevela) as a Terraform addon. Enabling
|
||||
Terraform addon and Terraform provider addon is the simplest way.
|
||||
|
||||
- Install Terraform Controller
|
||||
|
@ -519,4 +519,4 @@ $ vela addon enable terraform-alibaba ALICLOUD_ACCESS_KEY=<xxx> ALICLOUD_SECRET_
|
|||
|
||||
- Provision and Consume cloud resources
|
||||
|
||||
Try to provision and consume cloud resources by KubeVela [Cli](https://kubevela.io/docs/end-user/components/cloud-services/provider-and-consume-cloud-services) or [VelaUX](https://kubevela.io/docs/next/tutorials/consume-cloud-services).
|
||||
Try to provision and consume cloud resources by KubeVela [Cli](https://kubevela.io/docs/end-user/components/cloud-services/provider-and-consume-cloud-services) or [VelaUX](https://kubevela.io/docs/next/tutorials/consume-cloud-services).
|
||||
|
|
|
@ -3,7 +3,7 @@ package gitee
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/oam-dev/terraform-controller/e2e"
|
||||
"github.com/oam-dev/terraform-controller/e2e/normal"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -16,5 +16,5 @@ var (
|
|||
func TestGiteeConfigurationRegression(t *testing.T) {
|
||||
var retryTimes = 240
|
||||
|
||||
e2e.Regression(t, giteeConfigurationsRegression, retryTimes)
|
||||
normal.Regression(t, giteeConfigurationsRegression, retryTimes)
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue