├── .editorconfig
├── .github
├── .editorconfig
└── workflows
│ └── ci.yaml
├── .gitignore
├── .goreleaser.yml
├── .idea
├── misc.xml
├── modules.xml
├── terraform-provider-k8s.iml
└── vcs.xml
├── LICENSE
├── Makefile
├── README.md
├── docs
├── index.md
└── resources
│ └── manifest.md
├── examples
├── .gitignore
├── 0.12
│ ├── example.tf
│ ├── variables.tf
│ └── versions.tf
├── fail
│ ├── example.tf
│ └── versions.tf
└── manifests
│ ├── crontab-crd.yaml
│ ├── crontab-resource.yaml
│ ├── my-configmap.yaml
│ ├── nginx-deployment.yaml
│ ├── nginx-ingress.yaml
│ ├── nginx-namespace.yaml
│ ├── nginx-pvc.yaml
│ └── nginx-service.yaml
├── go.mod
├── go.sum
├── hack
├── .editorconfig
├── .terraformrc.tpl
├── kind.yaml
├── metallb-config.yaml
├── metallb-webhook-patch.yaml
├── setup-kind.sh
├── versions.tf
└── versions012.tf
├── k8s
├── helpers.go
├── patch.go
├── provider.go
├── provider_test.go
├── resource_k8s_manifest.go
└── resource_k8s_manifest_test.go
├── main.go
└── test
├── .editorconfig
├── manifests
├── crontab-crd.yaml
├── crontab-resource.yaml
├── my-configmap.yaml
├── nginx-deployment.yaml
├── nginx-ingress.yaml
├── nginx-namespace.yaml
├── nginx-pvc-pod.yaml
├── nginx-pvc.yaml
└── nginx-service.yaml
└── terraform
├── main.tf
├── variables.tf
└── versions.tf
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 4
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.go]
12 | indent_style = tab
13 |
14 | [{Makefile, *.mk}]
15 | indent_style = tab
16 |
--------------------------------------------------------------------------------
/.github/.editorconfig:
--------------------------------------------------------------------------------
1 | [{*.yml,*.yaml}]
2 | indent_size = 2
3 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 |
9 | jobs:
10 | integration-test:
11 | name: Integration test
12 | runs-on: ubuntu-latest
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | kube: ["1.19", "1.20", "1.21", "1.22", "1.23", "1.24", "1.25"]
17 | terraform: ["0.12.31", "0.14.11", "0.15.5", "1.0.11", "1.1.9", "1.2.9"] # skip 0.13.7 for now
18 |
19 | steps:
20 | - name: Set up Go
21 | uses: actions/setup-go@v2
22 | with:
23 | go-version: 1.19
24 |
25 | - name: Checkout
26 | uses: actions/checkout@v2
27 | with:
28 | fetch-depth: 0
29 |
30 | # See https://github.com/kubernetes-sigs/kind/releases/tag/v0.11.1
31 | - name: Determine KinD node image version
32 | id: node_image
33 | run: |
34 | case ${{ matrix.kube }} in
35 | 1.19)
36 | NODE_IMAGE=kindest/node:v1.19.16@sha256:707469aac7e6805e52c3bde2a8a8050ce2b15decff60db6c5077ba9975d28b98 ;;
37 | 1.20)
38 | NODE_IMAGE=kindest/node:v1.20.15@sha256:d67de8f84143adebe80a07672f370365ec7d23f93dc86866f0e29fa29ce026fe ;;
39 | 1.21)
40 | NODE_IMAGE=kindest/node:v1.21.14@sha256:f9b4d3d1112f24a7254d2ee296f177f628f9b4c1b32f0006567af11b91c1f301 ;;
41 | 1.22)
42 | NODE_IMAGE=kindest/node:v1.22.13@sha256:4904eda4d6e64b402169797805b8ec01f50133960ad6c19af45173a27eadf959 ;;
43 | 1.23)
44 | NODE_IMAGE=kindest/node:v1.23.10@sha256:f047448af6a656fae7bc909e2fab360c18c487ef3edc93f06d78cdfd864b2d12 ;;
45 | 1.24)
46 | NODE_IMAGE=kindest/node:v1.24.4@sha256:adfaebada924a26c2c9308edd53c6e33b3d4e453782c0063dc0028bdebaddf98 ;;
47 | 1.25)
48 | NODE_IMAGE=kindest/node:v1.25.0@sha256:428aaa17ec82ccde0131cb2d1ca6547d13cf5fdabcc0bbecf749baa935387cbf ;;
49 | esac
50 |
51 | echo "::set-output name=image::$NODE_IMAGE"
52 |
53 | - name: Create KinD cluster
54 | uses: helm/kind-action@v1.3.0
55 | with:
56 | version: v0.15.0
57 | node_image: ${{ steps.node_image.outputs.image }}
58 | config: hack/kind.yaml
59 |
60 | - name: Configure cluster
61 | run: ./hack/setup-kind.sh
62 |
63 | - name: Test
64 | run: make TERRAFORM_VERSION=${{ matrix.terraform }} EXAMPLE_DIR=test/terraform test-integration
65 |
66 | acceptance-test:
67 | name: Acceptance test
68 | runs-on: ubuntu-latest
69 | strategy:
70 | fail-fast: false
71 | matrix:
72 | kube: ["1.19", "1.20", "1.21", "1.22", "1.23", "1.24", "1.25"]
73 | terraform: ["0.12.31", "0.13.7", "0.14.11", "0.15.5", "1.0.11", "1.1.9", "1.2.9"]
74 |
75 | steps:
76 | - name: Set up Go
77 | uses: actions/setup-go@v2
78 | with:
79 | go-version: 1.19
80 |
81 | - name: Set up Terraform
82 | uses: hashicorp/setup-terraform@v1
83 | with:
84 | terraform_version: ${{ matrix.terraform }}
85 | terraform_wrapper: false # https://github.com/hashicorp/terraform-plugin-sdk/issues/742
86 |
87 | - name: Checkout
88 | uses: actions/checkout@v2
89 | with:
90 | fetch-depth: 0
91 |
92 | # See https://github.com/kubernetes-sigs/kind/releases/tag/v0.11.1
93 | - name: Determine KinD node image version
94 | id: node_image
95 | run: |
96 | case ${{ matrix.kube }} in
97 | 1.19)
98 | NODE_IMAGE=kindest/node:v1.19.16@sha256:707469aac7e6805e52c3bde2a8a8050ce2b15decff60db6c5077ba9975d28b98 ;;
99 | 1.20)
100 | NODE_IMAGE=kindest/node:v1.20.15@sha256:d67de8f84143adebe80a07672f370365ec7d23f93dc86866f0e29fa29ce026fe ;;
101 | 1.21)
102 | NODE_IMAGE=kindest/node:v1.21.14@sha256:f9b4d3d1112f24a7254d2ee296f177f628f9b4c1b32f0006567af11b91c1f301 ;;
103 | 1.22)
104 | NODE_IMAGE=kindest/node:v1.22.13@sha256:4904eda4d6e64b402169797805b8ec01f50133960ad6c19af45173a27eadf959 ;;
105 | 1.23)
106 | NODE_IMAGE=kindest/node:v1.23.10@sha256:f047448af6a656fae7bc909e2fab360c18c487ef3edc93f06d78cdfd864b2d12 ;;
107 | 1.24)
108 | NODE_IMAGE=kindest/node:v1.24.4@sha256:adfaebada924a26c2c9308edd53c6e33b3d4e453782c0063dc0028bdebaddf98 ;;
109 | 1.25)
110 | NODE_IMAGE=kindest/node:v1.25.0@sha256:428aaa17ec82ccde0131cb2d1ca6547d13cf5fdabcc0bbecf749baa935387cbf ;;
111 | esac
112 |
113 | echo "::set-output name=image::$NODE_IMAGE"
114 |
115 | - name: Create KinD cluster
116 | uses: helm/kind-action@v1.3.0
117 | with:
118 | version: v0.15.0
119 | node_image: ${{ steps.node_image.outputs.image }}
120 | config: hack/kind.yaml
121 |
122 | - name: Configure cluster
123 | run: ./hack/setup-kind.sh
124 |
125 | - name: Test
126 | env:
127 | TF_ACC: 1
128 | run: go test -v ./...
129 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /bin/
2 | /build/
3 | /dist/
4 | /vendor/
5 |
6 | # IDE integration
7 | /.vscode/*
8 | !/.vscode/launch.json
9 | !/.vscode/tasks.json
10 | /.idea/*
11 | !/.idea/codeStyles/
12 | !/.idea/copyright/
13 | !/.idea/dataSources.xml
14 | !/.idea/*.iml
15 | !/.idea/externalDependencies.xml
16 | !/.idea/go.imports.xml
17 | !/.idea/modules.xml
18 | !/.idea/runConfigurations/
19 | !/.idea/scopes/
20 | !/.idea/sqldialects.xml
21 |
22 | terraform.tfstate
23 | terraform.tfstate.backup
24 | .terraform/
25 | crash.log
26 | *.tf
27 | .terraformrc
28 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | before:
2 | hooks:
3 | - go mod tidy
4 | builds:
5 | - env:
6 | - CGO_ENABLED=0
7 | goos:
8 | - freebsd
9 | - windows
10 | - linux
11 | - darwin
12 | goarch:
13 | - amd64
14 | - '386'
15 | - arm
16 | - arm64
17 | ignore:
18 | - goos: darwin
19 | goarch: '386'
20 | binary: '{{ .ProjectName }}_v{{ .Version }}'
21 | archives:
22 | - format: zip
23 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}'
24 | checksum:
25 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS'
26 | algorithm: sha256
27 | signs:
28 | - artifacts: checksum
29 | args:
30 | # if you are using this is a GitHub action or some other automated pipeline, you
31 | # need to pass the batch flag to indicate its not interactive.
32 | - "--batch"
33 | - "--local-user"
34 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key
35 | - "--output"
36 | - "${signature}"
37 | - "--detach-sign"
38 | - "${artifact}"
39 | release:
40 | # Visit your project's GitHub Releases page to publish this release.
41 | draft: true
42 | changelog:
43 | skip: true
44 | brews:
45 | - tap:
46 | owner: banzaicloud
47 | name: homebrew-tap
48 | folder: Formula
49 | homepage: https://banzaicloud.com/
50 | description: Kubernetes Terraform provider with support for raw manifests
51 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/terraform-provider-k8s.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Eric Chiang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html
2 |
3 | OS = $(shell uname | tr A-Z a-z)
4 |
5 | BINARY_NAME = terraform-provider-k8s
6 |
7 | # Build variables
8 | BUILD_DIR ?= build
9 | VERSION ?= $(shell git describe --tags --exact-match 2>/dev/null || git symbolic-ref -q --short HEAD)
10 | COMMIT_HASH ?= $(shell git rev-parse --short HEAD 2>/dev/null)
11 | BUILD_DATE ?= $(shell date +%FT%T%z)
12 | LDFLAGS += -X main.version=${VERSION} -X main.commitHash=${COMMIT_HASH} -X main.buildDate=${BUILD_DATE}
13 | export CGO_ENABLED ?= 0
14 | ifeq (${VERBOSE}, 1)
15 | ifeq ($(filter -v,${GOARGS}),)
16 | GOARGS += -v
17 | endif
18 | TEST_FORMAT = short-verbose
19 | endif
20 |
21 | export TF_CLI_CONFIG_FILE = ${PWD}/.terraformrc
22 |
23 | # Dependency versions
24 | GOTESTSUM_VERSION = 0.3.5
25 | GOLANGCI_VERSION = 1.17.1
26 | GORELEASER_VERSION = 0.113.0
27 | TERRAFORM_VERSION = 1.0.6
28 |
29 | GOLANG_VERSION = 1.13
30 |
31 | # Add the ability to override some variables
32 | # Use with care
33 | -include override.mk
34 |
35 | .PHONY: clean
36 | clean: ## Clean builds
37 | rm -rf ${BUILD_DIR}/
38 |
39 | .PHONY: goversion
40 | goversion:
41 | ifneq (${IGNORE_GOLANG_VERSION_REQ}, 1)
42 | @printf "${GOLANG_VERSION}\n$$(go version | awk '{sub(/^go/, "", $$3);print $$3}')" | sort -t '.' -k 1,1 -k 2,2 -k 3,3 -g | head -1 | grep -q -E "^${GOLANG_VERSION}$$" || (printf "Required Go version is ${GOLANG_VERSION}\nInstalled: `go version`" && exit 1)
43 | endif
44 |
45 | .PHONY: build
46 | build: goversion ## Build a binary
47 | ifeq (${VERBOSE}, 1)
48 | go env
49 | endif
50 |
51 | go build ${GOARGS} -tags "${GOTAGS}" -ldflags "${LDFLAGS}" -o ${BUILD_DIR}/${BINARY_NAME} .
52 |
53 | .PHONY: test-integration
54 | test-integration: EXAMPLE_DIR ?= examples/0.12
55 | test-integration: build bin/terraform .terraformrc ## Execute integration tests
56 | ifneq (,$(findstring 0.12.,${TERRAFORM_VERSION}))
57 | cp build/terraform-provider-k8s .
58 | cp hack/versions012.tf ${EXAMPLE_DIR}/versions.tf
59 | bin/terraform init ${EXAMPLE_DIR}
60 | bin/terraform apply -auto-approve -input=false ${EXAMPLE_DIR}
61 | else ifneq (,$(findstring 0.13.,${TERRAFORM_VERSION}))
62 | cp build/terraform-provider-k8s .
63 | cp hack/versions012.tf ${EXAMPLE_DIR}/versions.tf
64 | bin/terraform init ${EXAMPLE_DIR}
65 | bin/terraform apply -auto-approve -input=false ${EXAMPLE_DIR}
66 | else
67 | mkdir -p build/registry.terraform.io/banzaicloud/k8s/99.99.99/${OS}_amd64
68 | cp build/terraform-provider-k8s build/registry.terraform.io/banzaicloud/k8s/99.99.99/${OS}_amd64/
69 | cp hack/versions.tf ${EXAMPLE_DIR}
70 | bin/terraform -chdir=${EXAMPLE_DIR} init
71 | bin/terraform -chdir=${EXAMPLE_DIR} apply -auto-approve -input=false
72 | endif
73 | ${MAKE} test-integration-destroy EXAMPLE_DIR=${EXAMPLE_DIR}
74 |
75 | .PHONY: test-integration-destroy
76 | test-integration-destroy: EXAMPLE_DIR ?= examples/0.12
77 | test-integration-destroy: bin/terraform .terraformrc
78 | ifneq (,$(findstring 0.12.,${TERRAFORM_VERSION}))
79 | bin/terraform destroy -auto-approve ${EXAMPLE_DIR}
80 | rm terraform-provider-k8s
81 | else ifneq (,$(findstring 0.13.,${TERRAFORM_VERSION}))
82 | bin/terraform destroy -auto-approve ${EXAMPLE_DIR}
83 | rm terraform-provider-k8s
84 | else
85 | bin/terraform -chdir=${EXAMPLE_DIR} destroy -auto-approve
86 | rm -rf ${EXAMPLE_DIR}/{.terraform,.terraform.lock.hcl}
87 | endif
88 |
89 | bin/terraform: bin/terraform-${TERRAFORM_VERSION}
90 | @ln -sf terraform-${TERRAFORM_VERSION} bin/terraform
91 | bin/terraform-${TERRAFORM_VERSION}:
92 | @mkdir -p bin
93 | ifeq (${OS}, darwin)
94 | curl -sfL https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_darwin_amd64.zip > bin/terraform.zip
95 | endif
96 | ifeq (${OS}, linux)
97 | curl -sfL https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip > bin/terraform.zip
98 | endif
99 | unzip -d bin bin/terraform.zip
100 | @mv bin/terraform $@
101 | rm bin/terraform.zip
102 |
103 | .terraformrc:
104 | sed "s|PATH|$$PWD/build|" hack/.terraformrc.tpl > .terraformrc
105 |
106 | bin/goreleaser: bin/goreleaser-${GORELEASER_VERSION}
107 | @ln -sf goreleaser-${GORELEASER_VERSION} bin/goreleaser
108 | bin/goreleaser-${GORELEASER_VERSION}:
109 | @mkdir -p bin
110 | curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | bash -s -- -b ./bin/ v${GORELEASER_VERSION}
111 | @mv bin/goreleaser $@
112 |
113 | .PHONY: release
114 | release: bin/goreleaser # Publish a release
115 | bin/goreleaser release
116 |
117 | .PHONY: list
118 | list: ## List all make targets
119 | @${MAKE} -pRrn : -f $(MAKEFILE_LIST) 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | sort
120 |
121 | .PHONY: help
122 | .DEFAULT_GOAL := help
123 | help:
124 | @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
125 |
126 | # Variable outputting/exporting rules
127 | var-%: ; @echo $($*)
128 | varexport-%: ; @echo $*=$($*)
129 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Kubernetes Terraform Provider
2 |
3 | The k8s Terraform provider enables Terraform to deploy Kubernetes resources. Unlike the [official Kubernetes provider][kubernetes-provider] it handles raw manifests, leveraging [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) and the [Unstructured API](https://pkg.go.dev/github.com/kubernetes/apimachinery/pkg/apis/meta/v1/unstructured?tab=doc) directly to allow developers to work with any Kubernetes resource natively.
4 |
5 | This project is a hard fork of [ericchiang/terraform-provider-k8s](https://github.com/ericchiang/terraform-provider-k8s).
6 |
7 | ## Installation
8 |
9 | ### The Go Get way
10 |
11 | Use `go get` to install the provider:
12 |
13 | ```
14 | go get -u github.com/banzaicloud/terraform-provider-k8s
15 | ```
16 |
17 | Register the plugin in `~/.terraformrc` (see [Documentation](https://www.terraform.io/docs/commands/cli-config.html) for Windows users):
18 |
19 | ```hcl
20 | providers {
21 | k8s = "/$GOPATH/bin/terraform-provider-k8s"
22 | }
23 | ```
24 |
25 | ### The Terraform Plugin way (enable versioning)
26 |
27 | Download a release from the [Release page](https://github.com/banzaicloud/terraform-provider-k8s/releases) and make sure the name matches the following convention:
28 |
29 | | OS | Version | Name |
30 | | ------- | ------- | --------------------------------- |
31 | | LINUX | 0.4.0 | terraform-provider-k8s_v0.4.0 |
32 | | | 0.3.0 | terraform-provider-k8s_v0.3.0 |
33 | | Windows | 0.4.0 | terraform-provider-k8s_v0.4.0.exe |
34 | | | 0.3.0 | terraform-provider-k8s_v0.3.0.exe |
35 |
36 | Install the plugin using [Terraform Third-party Plugin Documentation](https://www.terraform.io/docs/configuration/providers.html#third-party-plugins):
37 |
38 | | Operating system | User plugins directory |
39 | | ----------------- | ----------------------------- |
40 | | Windows | %APPDATA%\terraform.d\plugins |
41 | | All other systems | ~/.terraform.d/plugins |
42 |
43 | ## Usage
44 |
45 | The provider uses your default Kubernetes configuration by default, but it takes some optional configuration parameters, see the [Configuration](#configuration) section (these parameters are the same as for the [Kubernetes provider](https://www.terraform.io/docs/providers/kubernetes/index.html#authentication)).
46 |
47 | ```hcl
48 | terraform {
49 | required_providers {
50 | k8s = {
51 | version = ">= 0.8.0"
52 | source = "banzaicloud/k8s"
53 | }
54 | }
55 | }
56 |
57 | provider "k8s" {
58 | config_context = "prod-cluster"
59 | }
60 | ```
61 |
62 | The `k8s` Terraform provider introduces a single Terraform resource, a `k8s_manifest`. The resource contains a `content` field, which contains a raw manifest in JSON or YAML format.
63 |
64 | ```hcl
65 | variable "replicas" {
66 | type = "string"
67 | default = 3
68 | }
69 |
70 | data "template_file" "nginx-deployment" {
71 | template = "${file("manifests/nginx-deployment.yaml")}"
72 |
73 | vars {
74 | replicas = "${var.replicas}"
75 | }
76 | }
77 |
78 | resource "k8s_manifest" "nginx-deployment" {
79 | content = "${data.template_file.nginx-deployment.rendered}"
80 | }
81 |
82 | # creating a second resource in the nginx namespace
83 | resource "k8s_manifest" "nginx-deployment" {
84 | content = "${data.template_file.nginx-deployment.rendered}"
85 | namespace = "nginx"
86 | }
87 | ```
88 |
89 | In this case `manifests/nginx-deployment.yaml` is a templated deployment manifest.
90 |
91 | ```yaml
92 | apiVersion: apps/v1beta2
93 | kind: Deployment
94 | metadata:
95 | name: nginx-deployment
96 | labels:
97 | app: nginx
98 | spec:
99 | replicas: ${replicas}
100 | selector:
101 | matchLabels:
102 | app: nginx
103 | template:
104 | metadata:
105 | labels:
106 | app: nginx
107 | spec:
108 | containers:
109 | - name: nginx
110 | image: nginx:1.7.9
111 | ports:
112 | - containerPort: 80
113 | ```
114 |
115 | The Kubernetes resources can then be managed through Terraform.
116 |
117 | ```terminal
118 | $ terraform apply
119 | # ...
120 | Apply complete! Resources: 1 added, 1 changed, 0 destroyed.
121 | $ kubectl get deployments
122 | NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
123 | nginx-deployment 3 3 3 3 1m
124 | $ terraform apply -var 'replicas=5'
125 | # ...
126 | Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
127 | $ kubectl get deployments
128 | NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
129 | nginx-deployment 5 5 5 3 3m
130 | $ terraform destroy -force
131 | # ...
132 | Destroy complete! Resources: 2 destroyed.
133 | $ kubectl get deployments
134 | No resources found.
135 | ```
136 |
137 | **NOTE**: If the YAML formatted `content` contains multiple documents (separated by `---`) only the first non-empty document is going to be parsed. This is because Terraform is mostly designed to represent a single resource on the provider side with a Terraform resource:
138 |
139 | > resource types correspond to an infrastructure object type that is managed via a remote network API
140 | > -- [Terraform Documentation](https://www.terraform.io/docs/configuration/resources.html)
141 |
142 | You can workaround this easily with the following snippet (however we still suggest to use separate resources):
143 |
144 | ```hcl
145 | locals {
146 | resources = split("\n---\n", data.template_file.nginx.rendered)
147 | }
148 |
149 | resource "k8s_manifest" "nginx-deployment" {
150 | count = length(local.resources)
151 |
152 | content = local.resources[count.index]
153 | }
154 | ```
155 |
156 | ## Helm workflow
157 |
158 | #### Requirements
159 |
160 | - Helm 2 or Helm 3
161 |
162 | Get a versioned chart into your source code and render it
163 |
164 | ##### Helm 2
165 |
166 | ``` shell
167 | helm fetch stable/nginx-ingress --version 1.24.4 --untardir charts --untar
168 | helm template --namespace nginx-ingress .\charts\nginx-ingress --output-dir manifests/
169 | ```
170 |
171 | ##### Helm 3
172 |
173 | ``` shell
174 | helm pull stable/nginx-ingress --version 1.24.4 --untardir charts --untar
175 | helm template --namespace nginx-ingress nginx-ingress .\charts\nginx-ingress --output-dir manifests/
176 | ```
177 |
178 | Apply the `main.tf` with the k8s provider
179 |
180 | ```hcl2
181 | # terraform 0.12.x
182 | locals {
183 | nginx-ingress_files = fileset(path.module, "manifests/nginx-ingress/templates/*.yaml")
184 | }
185 |
186 | data "local_file" "nginx-ingress_files_content" {
187 | for_each = local.nginx-ingress_files
188 | filename = each.value
189 | }
190 |
191 | resource "k8s_manifest" "nginx-ingress" {
192 | for_each = data.local_file.nginx-ingress_files_content
193 | content = each.value.content
194 | namespace = "nginx"
195 | }
196 | ```
197 |
198 | ## Configuration
199 |
200 | There are generally two ways to configure the Kubernetes provider.
201 |
202 | ### File config
203 |
204 | The provider always first tries to load **a config file** from a given
205 | (or default) location. Depending on whether you have current context set
206 | this _may_ require `config_context_auth_info` and/or `config_context_cluster`
207 | and/or `config_context`.
208 |
209 | #### Setting default config context
210 |
211 | Here's an example for how to set default context and avoid all provider configuration:
212 |
213 | ```
214 | kubectl config set-context default-system \
215 | --cluster=chosen-cluster \
216 | --user=chosen-user
217 |
218 | kubectl config use-context default-system
219 | ```
220 |
221 | Read [more about `kubectl` in the official docs](https://kubernetes.io/docs/user-guide/kubectl-overview/).
222 |
223 | ### In-cluster service account token
224 |
225 | If no other configuration is specified, and when it detects it is running in a kubernetes pod,
226 | the provider will try to use the service account token from the `/var/run/secrets/kubernetes.io/serviceaccount/token` path.
227 | Detection of in-cluster execution is based on the sole availability both of the `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT` environment variables,
228 | with non empty values.
229 |
230 | ```hcl
231 | provider "k8s" {
232 | load_config_file = "false"
233 | }
234 | ```
235 |
236 | If you have any other static configuration setting specified in a config file or static configuration, in-cluster service account token will not be tried.
237 |
238 | ### Statically defined credentials
239 |
240 | An other way is **statically** define TLS certificate credentials:
241 |
242 | ```hcl
243 | provider "k8s" {
244 | load_config_file = "false"
245 |
246 | host = "https://104.196.242.174"
247 |
248 | client_certificate = "${file("~/.kube/client-cert.pem")}"
249 | client_key = "${file("~/.kube/client-key.pem")}"
250 | cluster_ca_certificate = "${file("~/.kube/cluster-ca-cert.pem")}"
251 | }
252 | ```
253 |
254 | or username and password (HTTP Basic Authorization):
255 |
256 | ```hcl
257 | provider "k8s" {
258 | load_config_file = "false"
259 |
260 | host = "https://104.196.242.174"
261 |
262 | username = "username"
263 | password = "password"
264 | }
265 | ```
266 |
267 |
268 | If you have **both** valid configuration in a config file and static configuration, the static one is used as override.
269 | i.e. any static field will override its counterpart loaded from the config.
270 |
271 | ## Argument Reference
272 |
273 | The following arguments are supported:
274 |
275 | * `host` - (Optional) The hostname (in form of URI) of Kubernetes master. Can be sourced from `KUBE_HOST`.
276 | * `username` - (Optional) The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint. Can be sourced from `KUBE_USER`.
277 | * `password` - (Optional) The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint. Can be sourced from `KUBE_PASSWORD`.
278 | * `insecure` - (Optional) Whether server should be accessed without verifying the TLS certificate. Can be sourced from `KUBE_INSECURE`. Defaults to `false`.
279 | * `client_certificate` - (Optional) PEM-encoded client certificate for TLS authentication. Can be sourced from `KUBE_CLIENT_CERT_DATA`.
280 | * `client_key` - (Optional) PEM-encoded client certificate key for TLS authentication. Can be sourced from `KUBE_CLIENT_KEY_DATA`.
281 | * `cluster_ca_certificate` - (Optional) PEM-encoded root certificates bundle for TLS authentication. Can be sourced from `KUBE_CLUSTER_CA_CERT_DATA`.
282 | * `config_path` - (Optional) Path to the kube config file. Can be sourced from `KUBE_CONFIG` or `KUBECONFIG`. Defaults to `~/.kube/config`.
283 | * `config_context` - (Optional) Context to choose from the config file. Can be sourced from `KUBE_CTX`.
284 | * `config_context_auth_info` - (Optional) Authentication info context of the kube config (name of the kubeconfig user, `--user` flag in `kubectl`). Can be sourced from `KUBE_CTX_AUTH_INFO`.
285 | * `config_context_cluster` - (Optional) Cluster context of the kube config (name of the kubeconfig cluster, `--cluster` flag in `kubectl`). Can be sourced from `KUBE_CTX_CLUSTER`.
286 | * `token` - (Optional) Token of your service account. Can be sourced from `KUBE_TOKEN`.
287 | * `load_config_file` - (Optional) By default the local config (~/.kube/config) is loaded when you use this provider. This option at false disables this behaviour which is desired when statically specifying the configuration or relying on in-cluster config. Can be sourced from `KUBE_LOAD_CONFIG_FILE`.
288 | * `exec` - (Optional) Configuration block to use an [exec-based credential plugin] (https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins), e.g. call an external command to receive user credentials.
289 | * `api_version` - (Required) API version to use when decoding the ExecCredentials resource, e.g. `client.authentication.k8s.io/v1beta1`.
290 | * `command` - (Required) Command to execute.
291 | * `args` - (Optional) List of arguments to pass when executing the plugin.
292 | * `env` - (Optional) Map of environment variables to set when executing the plugin.
293 |
294 | ## Release
295 |
296 | ```bash
297 | gpg --fingerprint $MY_EMAIL
298 | export GPG_FINGERPRINT="THEF FING ERPR INTO OFTH EPUB LICK EYOF YOU!"
299 | goreleaser release --rm-dist -p 2
300 | ```
301 |
302 | ## Testing
303 |
304 | Create a [kind](https://kind.sigs.k8s.io) cluster with the attached configuration file:
305 |
306 | ```bash
307 | kind create cluster --config hack/kind.yaml
308 | ```
309 |
310 | Once the cluster is running, run the setup script:
311 |
312 | ```bash
313 | ./hack/setup-kind.sh
314 | ```
315 |
316 | Finally, run the integration test suite:
317 |
318 | ```bash
319 | make EXAMPLE_DIR=test/terraform test-integration
320 | ```
321 |
322 | [kubernetes-provider]: https://www.terraform.io/docs/providers/kubernetes/index.html
323 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # k8s Provider
2 |
3 | The k8s Terraform provider enables Terraform to deploy Kubernetes resources. Unlike the [official Kubernetes provider][kubernetes-provider] it handles raw manifests, leveraging [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) and the [Unstructured API](https://pkg.go.dev/github.com/kubernetes/apimachinery/pkg/apis/meta/v1/unstructured?tab=doc) directly to allow developers to work with any Kubernetes resource natively.
4 |
5 | This project is a hard fork of [ericchiang/terraform-provider-k8s](https://github.com/ericchiang/terraform-provider-k8s).
6 |
7 | Use the navigation to the left to read about the available resources.
8 |
9 | ## Example Usage
10 |
11 | ```hcl
12 | terraform {
13 | required_providers {
14 | k8s = {
15 | version = ">= 0.8.0"
16 | source = "banzaicloud/k8s"
17 | }
18 | }
19 | }
20 |
21 | provider "k8s" {
22 | config_context = "prod-cluster"
23 | }
24 | ```
25 |
26 | ## Stacking with managed Kubernetes cluster resources
27 |
28 | Terraform providers for various cloud providers feature resources to spin up managed Kubernetes clusters on services such as EKS, AKS and GKE. Such resources (or data-sources) will have attributes that expose the credentials needed for the Kubernetes provider to connect to these clusters.
29 |
30 | To use these credentials with the Kubernetes provider, they can be interpolated into the respective attributes of the Kubernetes provider configuration block.
31 |
32 | **IMPORTANT WARNING**
33 | *When using interpolation to pass credentials to the Kubernetes provider from other resources, these resources SHOULD NOT be created in the same `apply` operation where Kubernetes provider resources are also used. This will lead to intermittent and unpredictable errors which are hard to debug and diagnose. The root issue lies with the order in which Terraform itself evaluates the provider blocks vs. actual resources. Please refer to [this section of Terraform docs](https://www.terraform.io/docs/configuration/providers.html#provider-configuration) for further explanation.*
34 |
35 | The best-practice in this case is to ensure that the cluster itself and the Kubernetes provider resources are managed with separate `apply` operations. Data-sources can be used to convey values between the two stages as needed.
36 |
37 | ## Authentication
38 |
39 | There are generally two ways to configure the Kubernetes provider.
40 |
41 | ### File config
42 |
43 | The provider always first tries to load **a config file** from a given
44 | (or default) location. Depending on whether you have current context set
45 | this _may_ require `config_context_auth_info` and/or `config_context_cluster`
46 | and/or `config_context`.
47 |
48 | #### Setting default config context
49 |
50 | Here's an example for how to set default context and avoid all provider configuration:
51 |
52 | ```
53 | kubectl config set-context default-system \
54 | --cluster=chosen-cluster \
55 | --user=chosen-user
56 |
57 | kubectl config use-context default-system
58 | ```
59 |
60 | Read [more about `kubectl` in the official docs](https://kubernetes.io/docs/user-guide/kubectl-overview/).
61 |
62 | ### In-cluster service account token
63 |
64 | If no other configuration is specified, and when it detects it is running in a kubernetes pod,
65 | the provider will try to use the service account token from the `/var/run/secrets/kubernetes.io/serviceaccount/token` path.
66 | Detection of in-cluster execution is based on the sole availability both of the `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT` environment variables,
67 | with non empty values.
68 |
69 | ```hcl
70 | provider "kubernetes" {
71 | load_config_file = "false"
72 | }
73 | ```
74 |
75 | If you have any other static configuration setting specified in a config file or static configuration, in-cluster service account token will not be tried.
76 |
77 | ### Statically defined credentials
78 |
79 | An other way is **statically** define TLS certificate credentials:
80 |
81 | ```hcl
82 | provider "kubernetes" {
83 | load_config_file = "false"
84 |
85 | host = "https://104.196.242.174"
86 |
87 | client_certificate = "${file("~/.kube/client-cert.pem")}"
88 | client_key = "${file("~/.kube/client-key.pem")}"
89 | cluster_ca_certificate = "${file("~/.kube/cluster-ca-cert.pem")}"
90 | }
91 | ```
92 |
93 | or username and password (HTTP Basic Authorization):
94 |
95 | ```hcl
96 | provider "kubernetes" {
97 | load_config_file = "false"
98 |
99 | host = "https://104.196.242.174"
100 |
101 | username = "username"
102 | password = "password"
103 | }
104 | ```
105 |
106 |
107 | If you have **both** valid configuration in a config file and static configuration, the static one is used as override.
108 | i.e. any static field will override its counterpart loaded from the config.
109 |
110 | ## Argument Reference
111 |
112 | The following arguments are supported:
113 |
114 | * `host` - (Optional) The hostname (in form of URI) of Kubernetes master. Can be sourced from `KUBE_HOST`.
115 | * `username` - (Optional) The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint. Can be sourced from `KUBE_USER`.
116 | * `password` - (Optional) The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint. Can be sourced from `KUBE_PASSWORD`.
117 | * `insecure` - (Optional) Whether server should be accessed without verifying the TLS certificate. Can be sourced from `KUBE_INSECURE`. Defaults to `false`.
118 | * `client_certificate` - (Optional) PEM-encoded client certificate for TLS authentication. Can be sourced from `KUBE_CLIENT_CERT_DATA`.
119 | * `client_key` - (Optional) PEM-encoded client certificate key for TLS authentication. Can be sourced from `KUBE_CLIENT_KEY_DATA`.
120 | * `cluster_ca_certificate` - (Optional) PEM-encoded root certificates bundle for TLS authentication. Can be sourced from `KUBE_CLUSTER_CA_CERT_DATA`.
121 | * `config_path` - (Optional) Path to the kube config file. Can be sourced from `KUBE_CONFIG` or `KUBECONFIG`. Defaults to `~/.kube/config`.
122 | * `config_context` - (Optional) Context to choose from the config file. Can be sourced from `KUBE_CTX`.
123 | * `config_context_auth_info` - (Optional) Authentication info context of the kube config (name of the kubeconfig user, `--user` flag in `kubectl`). Can be sourced from `KUBE_CTX_AUTH_INFO`.
124 | * `config_context_cluster` - (Optional) Cluster context of the kube config (name of the kubeconfig cluster, `--cluster` flag in `kubectl`). Can be sourced from `KUBE_CTX_CLUSTER`.
125 | * `token` - (Optional) Token of your service account. Can be sourced from `KUBE_TOKEN`.
126 | * `load_config_file` - (Optional) By default the local config (~/.kube/config) is loaded when you use this provider. This option at false disables this behaviour which is desired when statically specifying the configuration or relying on in-cluster config. Can be sourced from `KUBE_LOAD_CONFIG_FILE`.
127 | * `exec` - (Optional) Configuration block to use an [exec-based credential plugin] (https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins), e.g. call an external command to receive user credentials.
128 | * `api_version` - (Required) API version to use when decoding the ExecCredentials resource, e.g. `client.authentication.k8s.io/v1beta1`.
129 | * `command` - (Required) Command to execute.
130 | * `args` - (Optional) List of arguments to pass when executing the plugin.
131 | * `env` - (Optional) Map of environment variables to set when executing the plugin.
132 |
--------------------------------------------------------------------------------
/docs/resources/manifest.md:
--------------------------------------------------------------------------------
1 | # k8s_manifest Resource
2 |
3 | The `k8s_manifest` resource applies any kind of Kubernetes resources against the Kubernetes API server and waits until it gets provisioned (not supported for CRDs).
4 |
5 | ## Example Usage
6 |
7 | ```hcl
8 | provider "k8s" {
9 | config_context = "prod-cluster"
10 | }
11 |
12 | data "template_file" "nginx-deployment" {
13 | template = "${file("manifests/nginx-deployment.yaml")}"
14 |
15 | vars {
16 | replicas = "${var.replicas}"
17 | }
18 | }
19 |
20 | resource "k8s_manifest" "nginx-deployment" {
21 | content = "${data.template_file.nginx-deployment.rendered}"
22 | }
23 |
24 | # creating a second resource in the nginx namespace
25 | resource "k8s_manifest" "nginx-deployment" {
26 | content = "${data.template_file.nginx-deployment.rendered}"
27 | namespace = "nginx"
28 | }
29 | ```
30 |
31 | **NOTE**: If the YAML formatted `content` contains multiple documents (separated by `---`) only the first non-empty document is going to be parsed. This is because Terraform is mostly designed to represent a single resource on the provider side with a Terraform resource:
32 |
33 | > resource types correspond to an infrastructure object type that is managed via a remote network API
34 | > -- [Terraform Documentation](https://www.terraform.io/docs/configuration/resources.html)
35 |
36 | You can workaround this easily with the following snippet (however we still suggest to use separate resources):
37 |
38 | ```hcl
39 | locals {
40 | resources = split("\n---\n", data.template_file.ngnix.rendered)
41 | }
42 |
43 | resource "k8s_manifest" "nginx-deployment" {
44 | count = length(local.resources)
45 |
46 | content = local.resources[count.index]
47 | }
48 | ```
49 |
50 | ## Argument Reference
51 |
52 | The following arguments are supported:
53 |
54 | * `content` - (Required) Content defines the specification of manifest in YAML (or JSON format).
55 | * `namespace` - (Optional) Namespace defines the namespace of the resource to be created if not defined in the `content`.
56 | * `ignore_field` - (Optional) List of jq style path you don't want to update in your manifest.
57 |
58 |
59 | ## Import
60 |
61 | A resource can be imported using the namespace, groupVersion, kind, and name, e.g.
62 |
63 | ```
64 | $ terraform import k8s_manifest.example namespace::groupVersion::kind::name
65 | ```
66 |
--------------------------------------------------------------------------------
/examples/.gitignore:
--------------------------------------------------------------------------------
1 | terraform.tfstate
2 | terraform.tfstate.backup
3 | .terraform/
4 | terraform-provider-k8s
5 |
--------------------------------------------------------------------------------
/examples/0.12/example.tf:
--------------------------------------------------------------------------------
1 | data "template_file" "my-configmap" {
2 | template = file("${path.module}/../manifests/my-configmap.yaml")
3 |
4 | vars = {
5 | greeting = var.greeting
6 | }
7 | }
8 |
9 | resource "k8s_manifest" "my-configmap" {
10 | content = data.template_file.my-configmap.rendered
11 | }
12 |
13 | data "template_file" "nginx-deployment" {
14 | template = file("${path.module}/../manifests/nginx-deployment.yaml")
15 |
16 | vars = {
17 | replicas = var.replicas
18 | }
19 | }
20 |
21 | resource "k8s_manifest" "nginx-deployment" {
22 | content = data.template_file.nginx-deployment.rendered
23 | }
24 |
25 | data "template_file" "nginx-namespace" {
26 | template = file("${path.module}/../manifests/nginx-namespace.yaml")
27 | }
28 |
29 | resource "k8s_manifest" "nginx-namespace" {
30 | content = data.template_file.nginx-namespace.rendered
31 | namespace = "kube-system"
32 | }
33 |
34 | resource "k8s_manifest" "nginx-deployment-with-namespace" {
35 | content = data.template_file.nginx-deployment.rendered
36 | namespace = "nginx"
37 | }
38 |
39 | data "template_file" "nginx-service" {
40 | template = file("${path.module}/../manifests/nginx-service.yaml")
41 | }
42 |
43 | resource "k8s_manifest" "nginx-service" {
44 | content = data.template_file.nginx-service.rendered
45 | namespace = "nginx"
46 | }
47 |
48 | data "template_file" "nginx-ingress" {
49 | template = file("${path.module}/../manifests/nginx-ingress.yaml")
50 | }
51 |
52 | resource "k8s_manifest" "nginx-ingress" {
53 | content = data.template_file.nginx-ingress.rendered
54 | namespace = "nginx"
55 | }
56 |
57 | data "template_file" "nginx-pvc" {
58 | template = file("${path.module}/../manifests/nginx-pvc.yaml")
59 | }
60 |
61 | resource "k8s_manifest" "nginx-pvc" {
62 | content = data.template_file.nginx-pvc.rendered
63 | namespace = "nginx"
64 | }
65 |
66 | resource "k8s_manifest" "crontab-crd" {
67 | content = file("${path.module}/../manifests/crontab-crd.yaml")
68 | }
69 |
70 | resource "k8s_manifest" "crontab-resource" {
71 | content = file("${path.module}/../manifests/crontab-resource.yaml")
72 | namespace = "nginx"
73 | depends_on = [k8s_manifest.crontab-crd]
74 | }
75 |
--------------------------------------------------------------------------------
/examples/0.12/variables.tf:
--------------------------------------------------------------------------------
1 | variable "greeting" {
2 | type = string
3 | default = "Hello, world!"
4 | }
5 |
6 | variable "replicas" {
7 | type = string
8 | default = 3
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/examples/0.12/versions.tf:
--------------------------------------------------------------------------------
1 |
2 | terraform {
3 | required_version = ">= 0.12"
4 | }
5 |
--------------------------------------------------------------------------------
/examples/fail/example.tf:
--------------------------------------------------------------------------------
1 |
2 | resource "k8s_manifest" "nginx-deployment" {
3 | content = <<-EOT
4 | apiVersion: apps/v1
5 | kind: Deployment
6 | metadata:
7 | name: nginx-deployment
8 | labels:
9 | app: nginx
10 | spec:
11 | replicas: 2
12 | selector:
13 | matchLabels:
14 | app: nginx
15 | template:
16 | metadata:
17 | labels:
18 | app: nginx
19 | spec:
20 | containers:
21 | - name: nginx
22 | image: nginx:1.7.9
23 | ports:
24 | - containerPort: 80
25 | command: ["/bin/false"]
26 | EOT
27 | timeouts {
28 | create = "20s"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/examples/fail/versions.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_version = ">= 0.12"
3 | }
4 |
--------------------------------------------------------------------------------
/examples/manifests/crontab-crd.yaml:
--------------------------------------------------------------------------------
1 | # Deprecated in v1.16 in favor of apiextensions.k8s.io/v1
2 | apiVersion: apiextensions.k8s.io/v1beta1
3 | kind: CustomResourceDefinition
4 | metadata:
5 | # name must match the spec fields below, and be in the form: .
6 | name: crontabs.stable.example.com
7 | spec:
8 | # group name to use for REST API: /apis//
9 | group: stable.example.com
10 | # list of versions supported by this CustomResourceDefinition
11 | versions:
12 | - name: v1
13 | # Each version can be enabled/disabled by Served flag.
14 | served: true
15 | # One and only one version must be marked as the storage version.
16 | storage: true
17 | # either Namespaced or Cluster
18 | scope: Namespaced
19 | names:
20 | # plural name to be used in the URL: /apis///
21 | plural: crontabs
22 | # singular name to be used as an alias on the CLI and for display
23 | singular: crontab
24 | # kind is normally the CamelCased singular type. Your resource manifests use this.
25 | kind: CronTab
26 | # shortNames allow shorter string to match your resource on the CLI
27 | shortNames:
28 | - ct
29 | preserveUnknownFields: false
30 | validation:
31 | openAPIV3Schema:
32 | type: object
33 | properties:
34 | spec:
35 | type: object
36 | properties:
37 | cronSpec:
38 | type: string
39 | image:
40 | type: string
41 | replicas:
42 | type: integer
43 |
--------------------------------------------------------------------------------
/examples/manifests/crontab-resource.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: "stable.example.com/v1"
2 | kind: CronTab
3 | metadata:
4 | name: my-new-cron-object
5 | spec:
6 | cronSpec: "* * * * */5"
7 | image: my-awesome-cron-image
8 |
--------------------------------------------------------------------------------
/examples/manifests/my-configmap.yaml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | kind: ConfigMap
4 | apiVersion: v1
5 | metadata:
6 | name: my-configmap
7 | namespace: default
8 | data:
9 | greeting: ${greeting}
10 |
--------------------------------------------------------------------------------
/examples/manifests/nginx-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: nginx-deployment
5 | labels:
6 | app: nginx
7 | spec:
8 | replicas: ${replicas}
9 | selector:
10 | matchLabels:
11 | app: nginx
12 | template:
13 | metadata:
14 | labels:
15 | app: nginx
16 | spec:
17 | containers:
18 | - name: nginx
19 | image: nginx:1.7.9
20 | ports:
21 | - containerPort: 80
22 |
--------------------------------------------------------------------------------
/examples/manifests/nginx-ingress.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: networking.k8s.io/v1beta1
2 | kind: Ingress
3 | metadata:
4 | name: nginx-ingress
5 | spec:
6 | rules:
7 | - http:
8 | paths:
9 | - path: /
10 | backend:
11 | serviceName: nginx-service
12 | servicePort: 80
13 |
--------------------------------------------------------------------------------
/examples/manifests/nginx-namespace.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Namespace
3 | metadata:
4 | name: nginx
5 |
--------------------------------------------------------------------------------
/examples/manifests/nginx-pvc.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: PersistentVolumeClaim
3 | metadata:
4 | name: nginx
5 | spec:
6 | accessModes:
7 | - ReadWriteOnce
8 | resources:
9 | requests:
10 | storage: 500Mi
11 |
--------------------------------------------------------------------------------
/examples/manifests/nginx-service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: nginx-service
5 | spec:
6 | ports:
7 | - name: http
8 | port: 80
9 | protocol: TCP
10 | selector:
11 | app: nginx
12 | type: LoadBalancer
13 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/banzaicloud/terraform-provider-k8s
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/evanphx/json-patch v4.12.0+incompatible
7 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.21.0
8 | github.com/itchyny/gojq v0.12.9
9 | github.com/mitchellh/go-homedir v1.1.0
10 | github.com/mitchellh/mapstructure v1.5.0
11 | github.com/pkg/errors v0.9.1
12 | gopkg.in/yaml.v2 v2.4.0
13 | k8s.io/apimachinery v0.25.0
14 | k8s.io/client-go v0.25.0
15 | k8s.io/kubectl v0.25.0
16 | sigs.k8s.io/controller-runtime v0.13.0
17 | )
18 |
19 | require (
20 | cloud.google.com/go v0.104.0 // indirect
21 | cloud.google.com/go/compute v1.9.0 // indirect
22 | cloud.google.com/go/iam v0.4.0 // indirect
23 | cloud.google.com/go/storage v1.26.0 // indirect
24 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
25 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect
26 | github.com/Azure/go-autorest/autorest v0.11.28 // indirect
27 | github.com/Azure/go-autorest/autorest/adal v0.9.21 // indirect
28 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
29 | github.com/Azure/go-autorest/logger v0.2.1 // indirect
30 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect
31 | github.com/MakeNowJust/heredoc v1.0.0 // indirect
32 | github.com/PuerkitoBio/purell v1.2.0 // indirect
33 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
34 | github.com/agext/levenshtein v1.2.3 // indirect
35 | github.com/apparentlymart/go-cidr v1.1.0 // indirect
36 | github.com/apparentlymart/go-textseg v1.0.0 // indirect
37 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
38 | github.com/aws/aws-sdk-go v1.44.93 // indirect
39 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
40 | github.com/chai2010/gettext-go v1.0.2 // indirect
41 | github.com/davecgh/go-spew v1.1.1 // indirect
42 | github.com/emicklei/go-restful/v3 v3.9.0 // indirect
43 | github.com/evanphx/json-patch/v5 v5.6.0 // indirect
44 | github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
45 | github.com/fatih/camelcase v1.0.0 // indirect
46 | github.com/fatih/color v1.13.0 // indirect
47 | github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect
48 | github.com/go-errors/errors v1.4.2 // indirect
49 | github.com/go-logr/logr v1.2.3 // indirect
50 | github.com/go-openapi/jsonpointer v0.19.5 // indirect
51 | github.com/go-openapi/jsonreference v0.20.0 // indirect
52 | github.com/go-openapi/swag v0.22.3 // indirect
53 | github.com/gogo/protobuf v1.3.2 // indirect
54 | github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
55 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
56 | github.com/golang/protobuf v1.5.2 // indirect
57 | github.com/google/btree v1.1.2 // indirect
58 | github.com/google/gnostic v0.6.9 // indirect
59 | github.com/google/go-cmp v0.5.8 // indirect
60 | github.com/google/gofuzz v1.2.0 // indirect
61 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
62 | github.com/google/uuid v1.3.0 // indirect
63 | github.com/googleapis/gax-go/v2 v2.5.1 // indirect
64 | github.com/googleapis/gnostic v0.5.5 // indirect
65 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
66 | github.com/hashicorp/errwrap v1.1.0 // indirect
67 | github.com/hashicorp/go-checkpoint v0.5.0 // indirect
68 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
69 | github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 // indirect
70 | github.com/hashicorp/go-getter v1.6.2 // indirect
71 | github.com/hashicorp/go-hclog v1.3.0 // indirect
72 | github.com/hashicorp/go-multierror v1.1.1 // indirect
73 | github.com/hashicorp/go-plugin v1.4.5 // indirect
74 | github.com/hashicorp/go-safetemp v1.0.0 // indirect
75 | github.com/hashicorp/go-uuid v1.0.3 // indirect
76 | github.com/hashicorp/go-version v1.6.0 // indirect
77 | github.com/hashicorp/hc-install v0.4.0 // indirect
78 | github.com/hashicorp/hcl/v2 v2.14.0 // indirect
79 | github.com/hashicorp/logutils v1.0.0 // indirect
80 | github.com/hashicorp/terraform-exec v0.17.3 // indirect
81 | github.com/hashicorp/terraform-json v0.14.0 // indirect
82 | github.com/hashicorp/terraform-plugin-go v0.14.0 // indirect
83 | github.com/hashicorp/terraform-plugin-log v0.7.0 // indirect
84 | github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c // indirect
85 | github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 // indirect
86 | github.com/hashicorp/yamux v0.1.1 // indirect
87 | github.com/imdario/mergo v0.3.13 // indirect
88 | github.com/inconshreveable/mousetrap v1.0.1 // indirect
89 | github.com/itchyny/timefmt-go v0.1.4 // indirect
90 | github.com/jmespath/go-jmespath v0.4.0 // indirect
91 | github.com/josharian/intern v1.0.0 // indirect
92 | github.com/json-iterator/go v1.1.12 // indirect
93 | github.com/jstemmer/go-junit-report v1.0.0 // indirect
94 | github.com/klauspost/compress v1.15.9 // indirect
95 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
96 | github.com/mailru/easyjson v0.7.7 // indirect
97 | github.com/mattn/go-colorable v0.1.13 // indirect
98 | github.com/mattn/go-isatty v0.0.16 // indirect
99 | github.com/mitchellh/copystructure v1.2.0 // indirect
100 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect
101 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect
102 | github.com/mitchellh/reflectwalk v1.0.2 // indirect
103 | github.com/moby/spdystream v0.2.0 // indirect
104 | github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect
105 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
106 | github.com/modern-go/reflect2 v1.0.2 // indirect
107 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
108 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
109 | github.com/oklog/run v1.1.0 // indirect
110 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
111 | github.com/pmezard/go-difflib v1.0.0 // indirect
112 | github.com/russross/blackfriday v1.6.0 // indirect
113 | github.com/spf13/cobra v1.5.0 // indirect
114 | github.com/spf13/pflag v1.0.5 // indirect
115 | github.com/stretchr/testify v1.8.0 // indirect
116 | github.com/ulikunitz/xz v0.5.10 // indirect
117 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
118 | github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect
119 | github.com/vmihailenco/tagparser v0.1.2 // indirect
120 | github.com/xlab/treeprint v1.1.0 // indirect
121 | github.com/zclconf/go-cty v1.11.0 // indirect
122 | go.opencensus.io v0.23.0 // indirect
123 | go.starlark.net v0.0.0-20220817180228-f738f5508c12 // indirect
124 | golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
125 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
126 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
127 | golang.org/x/net v0.0.0-20220907135653-1e95f45603a7 // indirect
128 | golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 // indirect
129 | golang.org/x/sys v0.0.0-20220907062415-87db552b00fd // indirect
130 | golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect
131 | golang.org/x/text v0.3.7 // indirect
132 | golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
133 | golang.org/x/tools v0.1.12 // indirect
134 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
135 | google.golang.org/api v0.95.0 // indirect
136 | google.golang.org/appengine v1.6.7 // indirect
137 | google.golang.org/genproto v0.0.0-20220902135211-223410557253 // indirect
138 | google.golang.org/grpc v1.49.0 // indirect
139 | google.golang.org/protobuf v1.28.1 // indirect
140 | gopkg.in/inf.v0 v0.9.1 // indirect
141 | gopkg.in/yaml.v3 v3.0.1 // indirect
142 | k8s.io/api v0.25.0 // indirect
143 | k8s.io/cli-runtime v0.25.0 // indirect
144 | k8s.io/component-base v0.25.0 // indirect
145 | k8s.io/klog/v2 v2.80.0 // indirect
146 | k8s.io/kube-openapi v0.0.0-20220803164354-a70c9af30aea // indirect
147 | k8s.io/utils v0.0.0-20220823124924-e9cbc92d1a73 // indirect
148 | sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
149 | sigs.k8s.io/kustomize/api v0.12.1 // indirect
150 | sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect
151 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
152 | sigs.k8s.io/yaml v1.3.0 // indirect
153 | )
154 |
--------------------------------------------------------------------------------
/hack/.editorconfig:
--------------------------------------------------------------------------------
1 | [{*.yml,*.yaml}]
2 | indent_size = 2
3 |
--------------------------------------------------------------------------------
/hack/.terraformrc.tpl:
--------------------------------------------------------------------------------
1 | provider_installation {
2 | filesystem_mirror {
3 | path = "PATH"
4 | include = ["registry.terraform.io/banzaicloud/k8s"]
5 | }
6 | direct {
7 | exclude = ["registry.terraform.io/banzaicloud/k8s"]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/hack/kind.yaml:
--------------------------------------------------------------------------------
1 | kind: Cluster
2 | apiVersion: kind.x-k8s.io/v1alpha4
3 | nodes:
4 | - role: control-plane
5 | kubeadmConfigPatches:
6 | - |
7 | kind: InitConfiguration
8 | nodeRegistration:
9 | kubeletExtraArgs:
10 | node-labels: "ingress-ready=true"
11 | extraPortMappings:
12 | - containerPort: 80
13 | hostPort: 2080
14 | protocol: TCP
15 | - containerPort: 443
16 | hostPort: 20443
17 | protocol: TCP
18 |
--------------------------------------------------------------------------------
/hack/metallb-config.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: metallb.io/v1beta1
2 | kind: IPAddressPool
3 | metadata:
4 | name: docker
5 | namespace: metallb-system
6 | spec:
7 | addresses:
8 | - METALLB_IP_ADDRESS_RANGE
9 | ---
10 | apiVersion: metallb.io/v1beta1
11 | kind: L2Advertisement
12 | metadata:
13 | name: empty
14 | namespace: metallb-system
15 |
--------------------------------------------------------------------------------
/hack/metallb-webhook-patch.yaml:
--------------------------------------------------------------------------------
1 | webhooks:
2 | - failurePolicy: Ignore
3 | name: bgppeersvalidationwebhook.metallb.io
4 | - failurePolicy: Ignore
5 | name: addresspoolvalidationwebhook.metallb.io
6 | - failurePolicy: Ignore
7 | name: bfdprofilevalidationwebhook.metallb.io
8 | - failurePolicy: Ignore
9 | name: bgpadvertisementvalidationwebhook.metallb.io
10 | - failurePolicy: Ignore
11 | name: communityvalidationwebhook.metallb.io
12 | - failurePolicy: Ignore
13 | name: ipaddresspoolvalidationwebhook.metallb.io
14 | - failurePolicy: Ignore
15 | name: l2advertisementvalidationwebhook.metallb.io
16 |
--------------------------------------------------------------------------------
/hack/setup-kind.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Dependencies
4 | # - kind >=0.11
5 | # - k8s >= 1.19
6 |
7 | set -e
8 |
9 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
10 |
11 | METALLB_VERSION=v0.13.5
12 | INGRESS_VERSION=controller-v1.2.1
13 |
14 | # Fingers crossed this is at least a /24 range
15 | METALLB_IP_PREFIX_RANGE=$(docker network inspect kind --format '{{(index .IPAM.Config 0).Subnet}}' | sed -r 's/(.*).\/.*/\1/')
16 | METALLB_IP_ADDRESS_RANGE=$(echo "${METALLB_IP_PREFIX_RANGE}200-${METALLB_IP_PREFIX_RANGE}250" | sed "s/\./\\\./g")
17 |
18 | kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/${METALLB_VERSION}/config/manifests/metallb-native.yaml
19 | kubectl wait --namespace metallb-system --for=condition=ready pod --selector=app=metallb --timeout=90s
20 |
21 | # Disable webhooks due to timeout issues
22 | # https://github.com/metallb/metallb/issues/1597
23 | # https://github.com/metallb/metallb/issues/1540
24 | kubectl patch validatingwebhookconfigurations.admissionregistration.k8s.io metallb-webhook-configuration --patch-file ${SCRIPT_DIR}/metallb-webhook-patch.yaml
25 |
26 | sed "s/METALLB_IP_ADDRESS_RANGE/${METALLB_IP_ADDRESS_RANGE}/" "${SCRIPT_DIR}/metallb-config.yaml" | kubectl apply -f -
27 |
28 | kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/${INGRESS_VERSION}/deploy/static/provider/kind/deploy.yaml
29 | kubectl wait --namespace ingress-nginx --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=90s
30 |
31 | # Patch the ingress class to make it defaul
32 | kubectl patch ingressclass nginx -p '{"metadata": {"annotations":{"ingressclass.kubernetes.io/is-default-class": "true"}}}'
33 |
--------------------------------------------------------------------------------
/hack/versions.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_version = ">= 0.12"
3 |
4 | required_providers {
5 | k8s = {
6 | source = "banzaicloud/k8s"
7 | version = ">= 0.0.1"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/hack/versions012.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_version = ">= 0.12"
3 | }
4 |
--------------------------------------------------------------------------------
/k8s/helpers.go:
--------------------------------------------------------------------------------
1 | package k8s
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
8 | )
9 |
10 | const idSeparator = "::"
11 |
12 | func idParts(id string) (string, string, string, string, error) {
13 | parts := strings.Split(id, idSeparator)
14 | if len(parts) != 4 {
15 | err := fmt.Errorf("unexpected ID format (%q), expected %q.", id, "namespace::groupVersion::kind::name")
16 | return "", "", "", "", err
17 | }
18 |
19 | return parts[0], parts[1], parts[2], parts[3], nil
20 | }
21 |
22 | func buildId(object *unstructured.Unstructured) string {
23 | return strings.Join(
24 | []string{
25 | object.GetNamespace(),
26 | object.GroupVersionKind().GroupVersion().String(),
27 | object.GroupVersionKind().Kind,
28 | object.GetName(),
29 | },
30 | idSeparator,
31 | )
32 | }
33 |
34 | func expandStringSlice(s []interface{}) []string {
35 | result := make([]string, len(s), len(s))
36 | for k, v := range s {
37 | // Handle the Terraform parser bug which turns empty strings in lists to nil.
38 | if v == nil {
39 | result[k] = ""
40 | } else {
41 | result[k] = v.(string)
42 | }
43 | }
44 | return result
45 | }
46 |
--------------------------------------------------------------------------------
/k8s/patch.go:
--------------------------------------------------------------------------------
1 | // Copyright The Helm Authors.
2 | // Licensed under the Apache License, Version 2.0 (the "License");
3 | // you may not use this file except in compliance with the License.
4 | // You may obtain a copy of the License at
5 | // http://www.apache.org/licenses/LICENSE-2.0
6 | // Unless required by applicable law or agreed to in writing, software
7 | // distributed under the License is distributed on an "AS IS" BASIS,
8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9 | // See the License for the specific language governing permissions and
10 | // limitations under the License.
11 |
12 | // Adapted from https://github.com/helm/helm/blob/master/pkg/kube/client.go
13 | // and https://github.com/helm/helm/blob/master/pkg/kube/converter.go
14 |
15 | package k8s
16 |
17 | import (
18 | "context"
19 | "log"
20 | "sync"
21 |
22 | jsonpatch "github.com/evanphx/json-patch"
23 | "github.com/pkg/errors"
24 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
25 | "k8s.io/apimachinery/pkg/runtime"
26 | "k8s.io/apimachinery/pkg/types"
27 | "k8s.io/apimachinery/pkg/util/json"
28 | "k8s.io/apimachinery/pkg/util/strategicpatch"
29 | "k8s.io/client-go/kubernetes/scheme"
30 | "sigs.k8s.io/controller-runtime/pkg/client"
31 | )
32 |
33 | var k8sNativeScheme *runtime.Scheme
34 | var k8sNativeSchemeOnce sync.Once
35 |
36 | func patch(c client.Client, target, original, current *unstructured.Unstructured) error {
37 | patch, patchType, err := createPatch(target, original, current)
38 | if err != nil {
39 | return errors.Wrap(err, "failed to create patch")
40 | }
41 |
42 | if patch == nil || string(patch) == "{}" {
43 | log.Printf("Looks like there are no changes for %s %q", target.GroupVersionKind().String(), target.GetName())
44 | return nil
45 | }
46 |
47 | // send patch to server
48 | if err = c.Patch(context.TODO(), current, client.RawPatch(patchType, patch)); err != nil {
49 | return errors.Wrapf(err, "cannot patch %q with kind %s", target.GroupVersionKind().String(), target.GetName())
50 | }
51 | return nil
52 | }
53 |
54 | func createPatch(target, original, current *unstructured.Unstructured) ([]byte, types.PatchType, error) {
55 | oldData, err := json.Marshal(current)
56 | if err != nil {
57 | return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing current configuration")
58 | }
59 | originalData, err := json.Marshal(original)
60 | if err != nil {
61 | return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing original configuration")
62 | }
63 | newData, err := json.Marshal(target)
64 | if err != nil {
65 | return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing target configuration")
66 | }
67 |
68 | versionedObject := convert(target)
69 |
70 | // Unstructured objects, such as CRDs, may not have an not registered error
71 | // returned from ConvertToVersion. Anything that's unstructured should
72 | // use the jsonpatch.CreateMergePatch. Strategic Merge Patch is not supported
73 | // on objects like CRDs.
74 | _, isUnstructured := versionedObject.(runtime.Unstructured)
75 |
76 | if isUnstructured {
77 | // fall back to generic JSON merge patch
78 | patch, err := jsonpatch.CreateMergePatch(oldData, newData)
79 | return patch, types.MergePatchType, err
80 | }
81 |
82 | patchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObject)
83 | if err != nil {
84 | return nil, types.StrategicMergePatchType, errors.Wrap(err, "unable to create patch metadata from object")
85 | }
86 |
87 | patch, err := strategicpatch.CreateThreeWayMergePatch(originalData, newData, oldData, patchMeta, true)
88 | return patch, types.StrategicMergePatchType, err
89 | }
90 |
91 | func convert(obj *unstructured.Unstructured) runtime.Object {
92 | s := kubernetesNativeScheme()
93 | if obj, err := runtime.ObjectConvertor(s).ConvertToVersion(obj, obj.GroupVersionKind().GroupVersion()); err == nil {
94 | return obj
95 | }
96 | return obj
97 | }
98 |
99 | // kubernetesNativeScheme returns a clean *runtime.Scheme with _only_ Kubernetes
100 | // native resources added to it. This is required to break free of custom resources
101 | // that may have been added to scheme.Scheme due to Helm being used as a package in
102 | // combination with e.g. a versioned kube client. If we would not do this, the client
103 | // may attempt to perform e.g. a 3-way-merge strategy patch for custom resources.
104 | func kubernetesNativeScheme() *runtime.Scheme {
105 | k8sNativeSchemeOnce.Do(func() {
106 | k8sNativeScheme = runtime.NewScheme()
107 | scheme.AddToScheme(k8sNativeScheme)
108 | })
109 | return k8sNativeScheme
110 | }
111 |
--------------------------------------------------------------------------------
/k8s/provider.go:
--------------------------------------------------------------------------------
1 | package k8s
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "log"
7 | "net/http"
8 | "os"
9 |
10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging"
11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
12 | "github.com/mitchellh/go-homedir"
13 | _ "k8s.io/client-go/plugin/pkg/client/auth" // to import all provider auths
14 | restclient "k8s.io/client-go/rest"
15 | "k8s.io/client-go/tools/clientcmd"
16 | clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
17 | "sigs.k8s.io/controller-runtime/pkg/client"
18 | "sigs.k8s.io/controller-runtime/pkg/client/apiutil"
19 | )
20 |
21 | func Provider() *schema.Provider {
22 | p := &schema.Provider{
23 | Schema: map[string]*schema.Schema{
24 | "host": {
25 | Type: schema.TypeString,
26 | Optional: true,
27 | DefaultFunc: schema.EnvDefaultFunc("KUBE_HOST", ""),
28 | Description: "The hostname (in form of URI) of Kubernetes master.",
29 | },
30 | "username": {
31 | Type: schema.TypeString,
32 | Optional: true,
33 | DefaultFunc: schema.EnvDefaultFunc("KUBE_USER", ""),
34 | Description: "The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint.",
35 | },
36 | "password": {
37 | Type: schema.TypeString,
38 | Optional: true,
39 | DefaultFunc: schema.EnvDefaultFunc("KUBE_PASSWORD", ""),
40 | Description: "The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint.",
41 | },
42 | "insecure": {
43 | Type: schema.TypeBool,
44 | Optional: true,
45 | DefaultFunc: schema.EnvDefaultFunc("KUBE_INSECURE", false),
46 | Description: "Whether server should be accessed without verifying the TLS certificate.",
47 | },
48 | "client_certificate": {
49 | Type: schema.TypeString,
50 | Optional: true,
51 | DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_CERT_DATA", ""),
52 | Description: "PEM-encoded client certificate for TLS authentication.",
53 | },
54 | "client_key": {
55 | Type: schema.TypeString,
56 | Optional: true,
57 | DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_KEY_DATA", ""),
58 | Description: "PEM-encoded client certificate key for TLS authentication.",
59 | },
60 | "cluster_ca_certificate": {
61 | Type: schema.TypeString,
62 | Optional: true,
63 | DefaultFunc: schema.EnvDefaultFunc("KUBE_CLUSTER_CA_CERT_DATA", ""),
64 | Description: "PEM-encoded root certificates bundle for TLS authentication.",
65 | },
66 | "config_path": {
67 | Type: schema.TypeString,
68 | Optional: true,
69 | DefaultFunc: schema.MultiEnvDefaultFunc(
70 | []string{
71 | "KUBE_CONFIG",
72 | "KUBECONFIG",
73 | },
74 | "~/.kube/config"),
75 | Description: "Path to the kube config file, defaults to ~/.kube/config",
76 | },
77 | "config_context": {
78 | Type: schema.TypeString,
79 | Optional: true,
80 | DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX", ""),
81 | },
82 | "config_context_auth_info": {
83 | Type: schema.TypeString,
84 | Optional: true,
85 | DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_AUTH_INFO", ""),
86 | Description: "",
87 | },
88 | "config_context_cluster": {
89 | Type: schema.TypeString,
90 | Optional: true,
91 | DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_CLUSTER", ""),
92 | Description: "",
93 | },
94 | "token": {
95 | Type: schema.TypeString,
96 | Optional: true,
97 | DefaultFunc: schema.EnvDefaultFunc("KUBE_TOKEN", ""),
98 | Description: "Token to authenticate an service account",
99 | },
100 | "load_config_file": {
101 | Type: schema.TypeBool,
102 | Optional: true,
103 | DefaultFunc: schema.EnvDefaultFunc("KUBE_LOAD_CONFIG_FILE", true),
104 | Description: "Load local kubeconfig.",
105 | },
106 | "exec": {
107 | Type: schema.TypeList,
108 | Optional: true,
109 | MaxItems: 1,
110 | Elem: &schema.Resource{
111 | Schema: map[string]*schema.Schema{
112 | "api_version": {
113 | Type: schema.TypeString,
114 | Required: true,
115 | },
116 | "command": {
117 | Type: schema.TypeString,
118 | Required: true,
119 | },
120 | "env": {
121 | Type: schema.TypeMap,
122 | Optional: true,
123 | Elem: &schema.Schema{Type: schema.TypeString},
124 | },
125 | "args": {
126 | Type: schema.TypeList,
127 | Optional: true,
128 | Elem: &schema.Schema{Type: schema.TypeString},
129 | },
130 | },
131 | },
132 | Description: "",
133 | },
134 | },
135 |
136 | ResourcesMap: map[string]*schema.Resource{
137 | "k8s_manifest": resourceK8sManifest(),
138 | },
139 | }
140 |
141 | p.ConfigureFunc = func(d *schema.ResourceData) (interface{}, error) {
142 | terraformVersion := p.TerraformVersion
143 | if terraformVersion == "" {
144 | // Terraform 0.12 introduced this field to the protocol
145 | // We can therefore assume that if it's missing it's 0.10 or 0.11
146 | terraformVersion = "0.11+compatible"
147 | }
148 | return providerConfigure(d, terraformVersion)
149 | }
150 |
151 | return p
152 | }
153 |
154 | type ProviderConfig struct {
155 | RuntimeClient client.Client
156 | }
157 |
158 | func providerConfigure(d *schema.ResourceData, terraformVersion string) (interface{}, error) {
159 | cfg := &restclient.Config{}
160 | var err error
161 | _, configSet := d.GetOk("host")
162 | if d.Get("load_config_file").(bool) {
163 | // Config file loading
164 | cfg, err = tryLoadingConfigFile(d)
165 | if err != nil {
166 | return nil, err
167 | }
168 | } else if !configSet {
169 | // Attempt to load in-cluster config
170 | cfg, err = restclient.InClusterConfig()
171 | if err != nil {
172 | // Fallback to standard config if we are not running inside a cluster
173 | if err == restclient.ErrNotInCluster {
174 | cfg = &restclient.Config{}
175 | } else {
176 | return nil, fmt.Errorf("Failed to configure: %s", err)
177 | }
178 | }
179 | }
180 |
181 | // Overriding with static configuration
182 | cfg.UserAgent = fmt.Sprintf("HashiCorp/1.0 Terraform/%s", terraformVersion)
183 |
184 | if v, ok := d.GetOk("host"); ok {
185 | cfg.Host = v.(string)
186 | }
187 | if v, ok := d.GetOk("username"); ok {
188 | cfg.Username = v.(string)
189 | }
190 | if v, ok := d.GetOk("password"); ok {
191 | cfg.Password = v.(string)
192 | }
193 | if v, ok := d.GetOk("insecure"); ok {
194 | cfg.Insecure = v.(bool)
195 | }
196 | if v, ok := d.GetOk("cluster_ca_certificate"); ok {
197 | cfg.CAData = bytes.NewBufferString(v.(string)).Bytes()
198 | }
199 | if v, ok := d.GetOk("client_certificate"); ok {
200 | cfg.CertData = bytes.NewBufferString(v.(string)).Bytes()
201 | }
202 | if v, ok := d.GetOk("client_key"); ok {
203 | cfg.KeyData = bytes.NewBufferString(v.(string)).Bytes()
204 | }
205 | if v, ok := d.GetOk("token"); ok {
206 | cfg.BearerToken = v.(string)
207 | }
208 |
209 | if v, ok := d.GetOk("exec"); ok {
210 | exec := &clientcmdapi.ExecConfig{}
211 | if spec, ok := v.([]interface{})[0].(map[string]interface{}); ok {
212 | exec.APIVersion = spec["api_version"].(string)
213 | exec.Command = spec["command"].(string)
214 | exec.Args = expandStringSlice(spec["args"].([]interface{}))
215 | for kk, vv := range spec["env"].(map[string]interface{}) {
216 | exec.Env = append(exec.Env, clientcmdapi.ExecEnvVar{Name: kk, Value: vv.(string)})
217 | }
218 | } else {
219 | return nil, fmt.Errorf("Failed to parse exec")
220 | }
221 | cfg.ExecProvider = exec
222 | }
223 |
224 | if logging.IsDebugOrHigher() {
225 | log.Printf("[DEBUG] Enabling HTTP requests/responses tracing")
226 | cfg.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
227 | return logging.NewTransport("Kubernetes", rt)
228 | }
229 | }
230 |
231 | mapper, err := apiutil.NewDynamicRESTMapper(cfg, apiutil.WithLazyDiscovery)
232 | if err != nil {
233 | return nil, err
234 | }
235 |
236 | c, err := client.New(cfg, client.Options{
237 | Mapper: mapper,
238 | })
239 | if err != nil {
240 | return nil, fmt.Errorf("Failed to configure: %s", err)
241 | }
242 |
243 | return &ProviderConfig{
244 | RuntimeClient: c,
245 | }, nil
246 | }
247 |
248 | func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) {
249 | path, err := homedir.Expand(d.Get("config_path").(string))
250 | if err != nil {
251 | return nil, err
252 | }
253 |
254 | loader := &clientcmd.ClientConfigLoadingRules{
255 | ExplicitPath: path,
256 | }
257 |
258 | overrides := &clientcmd.ConfigOverrides{}
259 | ctxSuffix := "; default context"
260 |
261 | ctx, ctxOk := d.GetOk("config_context")
262 | authInfo, authInfoOk := d.GetOk("config_context_auth_info")
263 | cluster, clusterOk := d.GetOk("config_context_cluster")
264 | if ctxOk || authInfoOk || clusterOk {
265 | ctxSuffix = "; overriden context"
266 | if ctxOk {
267 | overrides.CurrentContext = ctx.(string)
268 | ctxSuffix += fmt.Sprintf("; config ctx: %s", overrides.CurrentContext)
269 | log.Printf("[DEBUG] Using custom current context: %q", overrides.CurrentContext)
270 | }
271 |
272 | overrides.Context = clientcmdapi.Context{}
273 | if authInfoOk {
274 | overrides.Context.AuthInfo = authInfo.(string)
275 | ctxSuffix += fmt.Sprintf("; auth_info: %s", overrides.Context.AuthInfo)
276 | }
277 | if clusterOk {
278 | overrides.Context.Cluster = cluster.(string)
279 | ctxSuffix += fmt.Sprintf("; cluster: %s", overrides.Context.Cluster)
280 | }
281 | log.Printf("[DEBUG] Using overidden context: %#v", overrides.Context)
282 | }
283 |
284 | cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, overrides)
285 | cfg, err := cc.ClientConfig()
286 | if err != nil {
287 | if pathErr, ok := err.(*os.PathError); ok && os.IsNotExist(pathErr.Err) {
288 | log.Printf("[INFO] Unable to load config file as it doesn't exist at %q", path)
289 | return nil, nil
290 | }
291 | log.Printf("[WARN] Failed to load config (%s%s): %s", path, ctxSuffix, err)
292 | }
293 |
294 | log.Printf("[INFO] Successfully loaded config file (%s%s)", path, ctxSuffix)
295 | return cfg, nil
296 | }
297 |
--------------------------------------------------------------------------------
/k8s/provider_test.go:
--------------------------------------------------------------------------------
1 | package k8s
2 |
3 | import (
4 | "context"
5 | "testing"
6 |
7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
9 | )
10 |
11 | var testAccProvider *schema.Provider
12 | var testAccProviders map[string]*schema.Provider
13 | var testAccProviderFactories map[string]func() (*schema.Provider, error)
14 |
15 | func init() {
16 | testAccProvider = Provider()
17 | testAccProviders = map[string]*schema.Provider{
18 | "k8s": testAccProvider,
19 | }
20 | testAccProviderFactories = map[string]func() (*schema.Provider, error){
21 | "k8s": func() (*schema.Provider, error) {
22 | return testAccProvider, nil
23 | },
24 | }
25 | }
26 |
27 | func TestProvider(t *testing.T) {
28 | if err := Provider().InternalValidate(); err != nil {
29 | t.Fatalf("err: %s", err)
30 | }
31 | }
32 |
33 | func TestProvider_impl(t *testing.T) {
34 | var _ *schema.Provider = Provider()
35 | }
36 |
37 | func testAccPreCheck(t *testing.T) {
38 | t.Helper()
39 |
40 | ctx := context.Background()
41 |
42 | diags := testAccProvider.Configure(ctx, terraform.NewResourceConfigRaw(nil))
43 | if diags.HasError() {
44 | t.Fatal(diags[0].Summary)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/k8s/resource_k8s_manifest.go:
--------------------------------------------------------------------------------
1 | package k8s
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "log"
8 | "strings"
9 | "time"
10 |
11 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
12 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
13 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
14 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
15 | "github.com/itchyny/gojq"
16 | "github.com/mitchellh/mapstructure"
17 | goyaml "gopkg.in/yaml.v2"
18 | apierrors "k8s.io/apimachinery/pkg/api/errors"
19 | "k8s.io/apimachinery/pkg/api/meta"
20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
22 | k8sschema "k8s.io/apimachinery/pkg/runtime/schema"
23 | "k8s.io/apimachinery/pkg/util/yaml"
24 | "k8s.io/kubectl/pkg/polymorphichelpers"
25 | "sigs.k8s.io/controller-runtime/pkg/client"
26 | )
27 |
28 | func resourceK8sManifest() *schema.Resource {
29 | return &schema.Resource{
30 | CreateContext: resourceK8sManifestCreate,
31 | ReadContext: resourceK8sManifestRead,
32 | UpdateContext: resourceK8sManifestUpdate,
33 | DeleteContext: resourceK8sManifestDelete,
34 | Importer: &schema.ResourceImporter{
35 | StateContext: resourceK8sManifestImport,
36 | },
37 | Schema: map[string]*schema.Schema{
38 | "namespace": {
39 | Type: schema.TypeString,
40 | Optional: true,
41 | Sensitive: false,
42 | ForceNew: true,
43 | },
44 | "content": {
45 | Type: schema.TypeString,
46 | Required: true,
47 | Sensitive: false,
48 | ValidateFunc: validation.StringIsNotEmpty,
49 | },
50 | "override_content": {
51 | Type: schema.TypeString,
52 | Computed: true,
53 | },
54 | "ignore_fields": {
55 | Type: schema.TypeList,
56 | Elem: &schema.Schema{Type: schema.TypeString},
57 | Description: "List of jq style path you don't want to update.",
58 | Optional: true,
59 | },
60 | "delete_cascade": {
61 | Type: schema.TypeBool,
62 | Optional: true,
63 | Sensitive: false,
64 | },
65 | },
66 | Timeouts: &schema.ResourceTimeout{
67 | Create: schema.DefaultTimeout(5 * time.Minute),
68 | Update: schema.DefaultTimeout(5 * time.Minute),
69 | Delete: schema.DefaultTimeout(5 * time.Minute),
70 | },
71 | }
72 | }
73 |
74 | func resourceK8sManifestCreate(ctx context.Context, d *schema.ResourceData, config interface{}) diag.Diagnostics {
75 | namespace := d.Get("namespace").(string)
76 | content := d.Get("content").(string)
77 |
78 | object, err := contentToObject(content)
79 | if err != nil {
80 | return diag.FromErr(err)
81 | }
82 |
83 | objectNamespace := object.GetNamespace()
84 |
85 | if namespace == "" && objectNamespace == "" {
86 | object.SetNamespace("default")
87 | } else if objectNamespace == "" {
88 | // TODO: which namespace should have a higher precedence?
89 | object.SetNamespace(namespace)
90 | }
91 |
92 | client := config.(*ProviderConfig).RuntimeClient
93 |
94 | log.Printf("[INFO] Creating new manifest: %#v", object)
95 | err = client.Create(ctx, object)
96 | if err != nil {
97 | return diag.FromErr(err)
98 | }
99 |
100 | // this must stand before the wait to avoid losing state on error
101 | d.SetId(buildId(object))
102 |
103 | err = waitForReadyStatus(ctx, d, client, object, d.Timeout(schema.TimeoutCreate))
104 | if err != nil {
105 | return diag.FromErr(err)
106 | }
107 |
108 | return resourceK8sManifestRead(ctx, d, config)
109 | }
110 |
111 | func waitForReadyStatus(ctx context.Context, d *schema.ResourceData, c client.Client, object *unstructured.Unstructured, timeout time.Duration) error {
112 | objectKey := client.ObjectKeyFromObject(object)
113 |
114 | createStateConf := &resource.StateChangeConf{
115 | Pending: []string{
116 | "pending",
117 | },
118 | Target: []string{
119 | "ready",
120 | },
121 | Refresh: func() (interface{}, string, error) {
122 | err := c.Get(ctx, objectKey, object)
123 | if err != nil {
124 | log.Printf("[DEBUG] Received error: %#v", err)
125 | return nil, "error", err
126 | }
127 |
128 | log.Printf("[DEBUG] Received object: %#v", object)
129 |
130 | if s, ok := object.Object["status"]; ok {
131 | log.Printf("[DEBUG] Object has status: %#v", s)
132 |
133 | if statusViewer, err := polymorphichelpers.StatusViewerFor(object.GetObjectKind().GroupVersionKind().GroupKind()); err == nil {
134 | _, ready, err := statusViewer.Status(object, 0)
135 | if err != nil {
136 | return nil, "error", err
137 | }
138 | if ready {
139 | return object, "ready", nil
140 | }
141 | return object, "pending", nil
142 | }
143 | log.Printf("[DEBUG] Object has no rollout status viewer")
144 |
145 | var status status
146 | err = mapstructure.Decode(s, &status)
147 | if err != nil {
148 | log.Printf("[DEBUG] Received error on decode: %#v", err)
149 | return nil, "error", err
150 | }
151 |
152 | if status.ReadyReplicas != nil {
153 | if *status.ReadyReplicas > 0 {
154 | return object, "ready", nil
155 | }
156 |
157 | return object, "pending", nil
158 | }
159 |
160 | if status.Phase != nil {
161 | if *status.Phase == "Active" || *status.Phase == "Bound" || *status.Phase == "Running" || *status.Phase == "Ready" || *status.Phase == "Online" || *status.Phase == "Healthy" {
162 | return object, "ready", nil
163 | }
164 |
165 | return object, "pending", nil
166 | }
167 |
168 | if status.LoadBalancer != nil {
169 | // LoadBalancer status may be for an Ingress or a Service having type=LoadBalancer
170 | checkLoadBalancer := true
171 | if object.GetAPIVersion() == "v1" && object.GetKind() == "Service" {
172 | specInterface, ok := object.Object["spec"]
173 | if !ok {
174 | log.Printf("[DEBUG] Received error on decode: %#v", err)
175 | return nil, "error", err
176 | }
177 | spec, ok := specInterface.(map[string]interface{})
178 | if !ok {
179 | log.Printf("[DEBUG] Received error on decode: %#v", err)
180 | return nil, "error", err
181 | }
182 | serviceType, ok := spec["type"]
183 | if !ok {
184 | log.Printf("[DEBUG] Received error on decode: %#v", err)
185 | return nil, "error", err
186 | }
187 | checkLoadBalancer = serviceType == "LoadBalancer"
188 | }
189 | if checkLoadBalancer {
190 | if len(*status.LoadBalancer) > 0 {
191 | return object, "ready", nil
192 | }
193 | return object, "pending", nil
194 | }
195 | }
196 | }
197 |
198 | return object, "ready", nil
199 | },
200 | Timeout: timeout,
201 | Delay: 5 * time.Second,
202 | MinTimeout: 5 * time.Second,
203 | ContinuousTargetOccurence: 1,
204 | }
205 |
206 | _, err := createStateConf.WaitForState()
207 | if err != nil {
208 | return fmt.Errorf("Error waiting for resource (%s) to be created: %s", d.Id(), err)
209 | }
210 |
211 | return nil
212 | }
213 |
214 | type status struct {
215 | ReadyReplicas *int
216 | Phase *string
217 | LoadBalancer *map[string]interface{}
218 | }
219 |
220 | func resourceK8sManifestRead(ctx context.Context, d *schema.ResourceData, config interface{}) diag.Diagnostics {
221 | namespace, gv, kind, name, err := idParts(d.Id())
222 | if err != nil {
223 | return diag.FromErr(err)
224 | }
225 |
226 | groupVersion, err := k8sschema.ParseGroupVersion(gv)
227 | if err != nil {
228 | log.Printf("[DEBUG] Invalid group version in resource ID: %#v", err)
229 | return diag.FromErr(err)
230 | }
231 |
232 | object := &unstructured.Unstructured{}
233 | object.SetGroupVersionKind(groupVersion.WithKind(kind))
234 | object.SetNamespace(namespace)
235 | object.SetName(name)
236 |
237 | objectKey := client.ObjectKeyFromObject(object)
238 |
239 | client := config.(*ProviderConfig).RuntimeClient
240 |
241 | log.Printf("[INFO] Reading object %s", name)
242 | err = client.Get(ctx, objectKey, object)
243 | if err != nil {
244 | if apierrors.IsNotFound(err) {
245 | log.Printf("[INFO] Object missing: %#v", object)
246 | d.SetId("")
247 | return nil
248 | }
249 | if meta.IsNoMatchError(err) {
250 | log.Printf("[INFO] Object kind missing: %#v", object)
251 | d.SetId("")
252 | return nil
253 | }
254 |
255 | log.Printf("[DEBUG] Received error: %#v", err)
256 | return diag.FromErr(err)
257 | }
258 | log.Printf("[INFO] Received object: %#v", object)
259 |
260 | ignoreFields, hasIgnoreFields := d.GetOk("ignore_fields")
261 | if hasIgnoreFields {
262 | content := d.Get("content").(string)
263 | contentModified, err := excludeIgnoreFields(ignoreFields, content)
264 | if err != nil {
265 | return diag.FromErr(err)
266 | }
267 |
268 | contentYaml, err := json2Yaml(contentModified)
269 | if err != nil {
270 | return diag.FromErr(err)
271 | }
272 |
273 | d.Set("override_content", contentYaml)
274 | }
275 |
276 | // TODO: save metadata in terraform state
277 |
278 | return nil
279 | }
280 |
281 | func resourceK8sManifestUpdate(ctx context.Context, d *schema.ResourceData, config interface{}) diag.Diagnostics {
282 | var originalData string
283 | var newData string
284 |
285 | namespace, _, _, _, err := idParts(d.Id())
286 | if err != nil {
287 | return diag.FromErr(err)
288 | }
289 |
290 | if d.HasChanges("content", "ignore_fields") {
291 | //originalDataRaw, newDataRaw := d.GetChange("content")
292 | newDataRaw := d.Get("content")
293 | originalDataRaw := d.Get("override_content")
294 |
295 | ignoreFields, hasIgnoreFields := d.GetOk("ignore_fields")
296 | if hasIgnoreFields {
297 | originalData, err = excludeIgnoreFields(ignoreFields, originalDataRaw.(string))
298 | if err != nil {
299 | return diag.FromErr(err)
300 | }
301 |
302 | newData, err = excludeIgnoreFields(ignoreFields, newDataRaw.(string))
303 | if err != nil {
304 | return diag.FromErr(err)
305 | }
306 | }
307 |
308 | log.Printf("[DEBUG] Original vs modified: %s %s", originalData, newData)
309 | modified, err := contentToObject(newData)
310 | if err != nil {
311 | return diag.FromErr(err)
312 | }
313 |
314 | original, err := contentToObject(originalData)
315 | if err != nil {
316 | return diag.FromErr(err)
317 | }
318 |
319 | objectNamespace := modified.GetNamespace()
320 |
321 | if namespace == "" && objectNamespace == "" {
322 | modified.SetNamespace("default")
323 | } else if objectNamespace == "" {
324 | // TODO: which namespace should have a higher precedence?
325 | modified.SetNamespace(namespace)
326 | }
327 |
328 | objectKey := client.ObjectKeyFromObject(modified)
329 |
330 | current := modified.DeepCopy()
331 |
332 | client := config.(*ProviderConfig).RuntimeClient
333 |
334 | err = client.Get(ctx, objectKey, current)
335 | if err != nil {
336 | log.Printf("[DEBUG] Received error: %#v", err)
337 | return diag.FromErr(err)
338 | }
339 |
340 | modified.SetResourceVersion(current.DeepCopy().GetResourceVersion())
341 |
342 | current.SetResourceVersion("")
343 | original.SetResourceVersion("")
344 |
345 | if err := patch(config.(*ProviderConfig).RuntimeClient, modified, original, current); err != nil {
346 | log.Printf("[DEBUG] Received error: %#v", err)
347 | return diag.FromErr(err)
348 | }
349 | log.Printf("[INFO] Updated object: %#v", modified)
350 |
351 | err = waitForReadyStatus(ctx, d, client, modified, d.Timeout(schema.TimeoutCreate))
352 | if err != nil {
353 | return diag.FromErr(err)
354 | }
355 | }
356 |
357 | return resourceK8sManifestRead(ctx, d, config)
358 | }
359 |
360 | func resourceK8sManifestDelete(ctx context.Context, d *schema.ResourceData, config interface{}) diag.Diagnostics {
361 | namespace, gv, kind, name, err := idParts(d.Id())
362 | if err != nil {
363 | return diag.FromErr(err)
364 | }
365 |
366 | groupVersion, err := k8sschema.ParseGroupVersion(gv)
367 | if err != nil {
368 | log.Printf("[DEBUG] Invalid group version in resource ID: %#v", err)
369 | return diag.FromErr(err)
370 | }
371 |
372 | currentObject := &unstructured.Unstructured{}
373 | currentObject.SetGroupVersionKind(groupVersion.WithKind(kind))
374 | currentObject.SetNamespace(namespace)
375 | currentObject.SetName(name)
376 |
377 | objectKey := client.ObjectKeyFromObject(currentObject)
378 |
379 | deleteCascade := d.Get("delete_cascade").(bool)
380 | deleteOptions := []client.DeleteOption{}
381 | if deleteCascade {
382 | deleteOptions = append(deleteOptions, client.PropagationPolicy(metav1.DeletePropagationForeground))
383 | }
384 |
385 | client := config.(*ProviderConfig).RuntimeClient
386 |
387 | log.Printf("[INFO] Deleting object %s", name)
388 | err = client.Delete(ctx, currentObject, deleteOptions...)
389 | if err != nil {
390 | log.Printf("[DEBUG] Received error: %#v", err)
391 | return diag.FromErr(err)
392 | }
393 |
394 | createStateConf := &resource.StateChangeConf{
395 | Pending: []string{
396 | "deleting",
397 | },
398 | Target: []string{
399 | "deleted",
400 | },
401 | Refresh: func() (interface{}, string, error) {
402 | err := client.Get(ctx, objectKey, currentObject)
403 | if err != nil {
404 | log.Printf("[INFO] error when deleting object %s: %+v", name, err)
405 | if apierrors.IsNotFound(err) {
406 | return currentObject, "deleted", nil
407 | }
408 | return nil, "error", err
409 |
410 | }
411 | return currentObject, "deleting", nil
412 | },
413 | Timeout: d.Timeout(schema.TimeoutDelete),
414 | Delay: 5 * time.Second,
415 | MinTimeout: 5 * time.Second,
416 | ContinuousTargetOccurence: 1,
417 | }
418 |
419 | _, err = createStateConf.WaitForState()
420 | if err != nil {
421 | return diag.FromErr(fmt.Errorf("Error waiting for resource (%s) to be deleted: %s", d.Id(), err))
422 | }
423 |
424 | log.Printf("[INFO] Deleted object: %#v", currentObject)
425 |
426 | return nil
427 | }
428 |
429 | func resourceK8sManifestImport(ctx context.Context, d *schema.ResourceData, config interface{}) ([]*schema.ResourceData, error) {
430 | namespace, gv, kind, name, err := idParts(d.Id())
431 | if err != nil {
432 | return nil, err
433 | }
434 |
435 | groupVersion, err := k8sschema.ParseGroupVersion(gv)
436 | if err != nil {
437 | log.Printf("[DEBUG] Invalid group version in resource ID: %#v", err)
438 | return nil, err
439 | }
440 |
441 | object := &unstructured.Unstructured{}
442 | object.SetGroupVersionKind(groupVersion.WithKind(kind))
443 | object.SetNamespace(namespace)
444 | object.SetName(name)
445 |
446 | objectKey := client.ObjectKeyFromObject(object)
447 |
448 | client := config.(*ProviderConfig).RuntimeClient
449 |
450 | err = client.Get(ctx, objectKey, object)
451 | if err != nil {
452 | log.Printf("[DEBUG] Received error: %#v", err)
453 | return nil, err
454 | }
455 |
456 | resource := schema.ResourceData{}
457 | resource.SetId(d.Id())
458 |
459 | return []*schema.ResourceData{&resource}, nil
460 | }
461 |
462 | func contentToObject(content string) (*unstructured.Unstructured, error) {
463 | decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(content), 4096)
464 |
465 | var object *unstructured.Unstructured
466 |
467 | for {
468 | err := decoder.Decode(&object)
469 | if err != nil {
470 | return nil, fmt.Errorf("Failed to unmarshal manifest: %s", err)
471 | }
472 |
473 | if object != nil {
474 | return object, nil
475 | }
476 | }
477 | }
478 |
479 | func excludeIgnoreFields(ignoreFieldsRaw interface{}, content string) (string, error) {
480 | var contentModified []byte
481 | var ignoreFields []string
482 |
483 | for _, j := range ignoreFieldsRaw.([]interface{}) {
484 | ignoreFields = append(ignoreFields, j.(string))
485 | }
486 |
487 | for _, i := range ignoreFields {
488 | query, err := gojq.Parse(fmt.Sprintf("del(%s)", i))
489 | if err != nil {
490 | log.Printf("[DEBUG] Received error: %#v", err)
491 | return "", err
492 | }
493 |
494 | if len(contentModified) > 0 {
495 | d, err := yaml2GoData(string(contentModified))
496 | if err != nil {
497 | log.Printf("[DEBUG] Received error: %#v", err)
498 | return "", err
499 | }
500 |
501 | v, _ := query.Run(d).Next()
502 | if err, ok := v.(error); ok {
503 | log.Printf("[DEBUG] Received error: %#v", err)
504 | return "", err
505 | }
506 |
507 | contentModified, err = gojq.Marshal(v)
508 |
509 | } else {
510 | d, err := yaml2GoData(content)
511 | if err != nil {
512 | log.Printf("[DEBUG] Received error: %#v", err)
513 | return "", err
514 | }
515 |
516 | v, _ := query.Run(d).Next()
517 | if err, ok := v.(error); ok {
518 | log.Printf("[DEBUG] !!!Received error: %#v", err)
519 | return "", err
520 | }
521 |
522 | contentModified, err = gojq.Marshal(v)
523 | }
524 |
525 | if err != nil {
526 | log.Printf("[DEBUG] Received error from jq: %#v", err)
527 | }
528 | }
529 | return string(contentModified), nil
530 | }
531 |
532 | func yaml2GoData(i string) (map[string]interface{}, error) {
533 | var body map[string]interface{}
534 | decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(i), 4096)
535 | err := decoder.Decode(&body)
536 |
537 | return body, err
538 | }
539 |
540 | func json2Yaml(i string) (string, error) {
541 | var body interface{}
542 | err := json.Unmarshal([]byte(i), &body)
543 | if err != nil {
544 | return "", err
545 | }
546 |
547 | data, err := goyaml.Marshal(body)
548 |
549 | return string(data), err
550 | }
551 |
--------------------------------------------------------------------------------
/k8s/resource_k8s_manifest_test.go:
--------------------------------------------------------------------------------
1 | package k8s
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "testing"
8 |
9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
11 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
12 | "github.com/mitchellh/mapstructure"
13 | k8sapierrors "k8s.io/apimachinery/pkg/api/errors"
14 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
15 | k8sschema "k8s.io/apimachinery/pkg/runtime/schema"
16 | "k8s.io/kubectl/pkg/polymorphichelpers"
17 | "sigs.k8s.io/controller-runtime/pkg/client"
18 | )
19 |
20 | // TestAccK8sManifest_basic tests the basic functionality of the k8s_manifest resource.
21 | func TestAccK8sManifest_basic(t *testing.T) {
22 | name := fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum))
23 |
24 | resource.Test(t, resource.TestCase{
25 | PreCheck: func() { testAccPreCheck(t) },
26 | ProviderFactories: testAccProviderFactories,
27 | CheckDestroy: testAccCheckK8sManifestDestroy,
28 | Steps: []resource.TestStep{
29 | { // First, create the resource.
30 | Config: testAccK8sManifest_basic(name),
31 | Check: resource.ComposeAggregateTestCheckFunc(
32 | testAccCheckK8sManifestReady("k8s_manifest.test"),
33 | ),
34 | },
35 | { // Then modify it.
36 | Config: testAccK8sManifest_modified(name),
37 | Check: resource.ComposeAggregateTestCheckFunc(
38 | testAccCheckK8sManifestReady("k8s_manifest.test"),
39 | ),
40 | },
41 | },
42 | })
43 | }
44 |
45 | // testAccK8sManifest_basic provides the terraform config
46 | // used to test basic functionality of the k8s_manifest resource.
47 | func testAccK8sManifest_basic(name string) string {
48 | return fmt.Sprintf(`resource "k8s_manifest" "test" {
49 | ignore_fields = [".nothing"] # Temporary workaround, see https://github.com/banzaicloud/terraform-provider-k8s/issues/84
50 | content = <.
5 | name: crontabs.stable.example.com
6 | spec:
7 | # group name to use for REST API: /apis//
8 | group: stable.example.com
9 | # list of versions supported by this CustomResourceDefinition
10 | versions:
11 | - name: v1
12 | # Each version can be enabled/disabled by Served flag.
13 | served: true
14 | # One and only one version must be marked as the storage version.
15 | storage: true
16 | schema:
17 | openAPIV3Schema:
18 | type: object
19 | properties:
20 | spec:
21 | type: object
22 | properties:
23 | cronSpec:
24 | type: string
25 | image:
26 | type: string
27 | replicas:
28 | type: integer
29 | # either Namespaced or Cluster
30 | scope: Namespaced
31 | names:
32 | # plural name to be used in the URL: /apis///
33 | plural: crontabs
34 | # singular name to be used as an alias on the CLI and for display
35 | singular: crontab
36 | # kind is normally the CamelCased singular type. Your resource manifests use this.
37 | kind: CronTab
38 | # shortNames allow shorter string to match your resource on the CLI
39 | shortNames:
40 | - ct
41 |
--------------------------------------------------------------------------------
/test/manifests/crontab-resource.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: "stable.example.com/v1"
2 | kind: CronTab
3 | metadata:
4 | name: my-new-cron-object
5 | spec:
6 | cronSpec: "* * * * */5"
7 | image: my-awesome-cron-image
8 |
--------------------------------------------------------------------------------
/test/manifests/my-configmap.yaml:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | kind: ConfigMap
4 | apiVersion: v1
5 | metadata:
6 | name: my-configmap
7 | namespace: default
8 | data:
9 | greeting: ${greeting}
10 |
--------------------------------------------------------------------------------
/test/manifests/nginx-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: nginx-deployment
5 | labels:
6 | app: nginx
7 | spec:
8 | replicas: ${replicas}
9 | selector:
10 | matchLabels:
11 | app: nginx
12 | template:
13 | metadata:
14 | labels:
15 | app: nginx
16 | spec:
17 | containers:
18 | - name: nginx
19 | image: nginx:1.7.9
20 | ports:
21 | - containerPort: 80
22 |
--------------------------------------------------------------------------------
/test/manifests/nginx-ingress.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: networking.k8s.io/v1
2 | kind: Ingress
3 | metadata:
4 | name: nginx-ingress
5 | spec:
6 | rules:
7 | - http:
8 | paths:
9 | - pathType: ImplementationSpecific
10 | path: /
11 | backend:
12 | service:
13 | name: nginx-service
14 | port:
15 | number: 80
16 |
--------------------------------------------------------------------------------
/test/manifests/nginx-namespace.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Namespace
3 | metadata:
4 | name: nginx
5 |
--------------------------------------------------------------------------------
/test/manifests/nginx-pvc-pod.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Pod
3 | metadata:
4 | name: nginx-pvc-pod
5 | labels:
6 | app: nginx
7 | spec:
8 | containers:
9 | - name: nginx
10 | image: nginx:1.7.9
11 | volumes:
12 | - name: vol
13 | persistentVolumeClaim:
14 | claimName: nginx
15 |
--------------------------------------------------------------------------------
/test/manifests/nginx-pvc.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: PersistentVolumeClaim
3 | metadata:
4 | name: nginx
5 | spec:
6 | accessModes:
7 | - ReadWriteOnce
8 | resources:
9 | requests:
10 | storage: 500Mi
11 |
--------------------------------------------------------------------------------
/test/manifests/nginx-service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: nginx-service
5 | spec:
6 | ports:
7 | - name: http
8 | port: 80
9 | protocol: TCP
10 | selector:
11 | app: nginx
12 | type: LoadBalancer
13 |
--------------------------------------------------------------------------------
/test/terraform/main.tf:
--------------------------------------------------------------------------------
1 | data "template_file" "my-configmap" {
2 | template = file("${path.module}/../manifests/my-configmap.yaml")
3 |
4 | vars = {
5 | greeting = var.greeting
6 | }
7 | }
8 |
9 | resource "k8s_manifest" "my-configmap" {
10 | content = data.template_file.my-configmap.rendered
11 | }
12 |
13 | data "template_file" "nginx-deployment" {
14 | template = file("${path.module}/../manifests/nginx-deployment.yaml")
15 |
16 | vars = {
17 | replicas = var.replicas
18 | }
19 | }
20 |
21 | resource "k8s_manifest" "nginx-deployment" {
22 | content = data.template_file.nginx-deployment.rendered
23 | }
24 |
25 | data "template_file" "nginx-namespace" {
26 | template = file("${path.module}/../manifests/nginx-namespace.yaml")
27 | }
28 |
29 | resource "k8s_manifest" "nginx-namespace" {
30 | content = data.template_file.nginx-namespace.rendered
31 | namespace = "kube-system"
32 | }
33 |
34 | resource "k8s_manifest" "nginx-deployment-with-namespace" {
35 | content = data.template_file.nginx-deployment.rendered
36 | namespace = "nginx"
37 | }
38 |
39 | data "template_file" "nginx-service" {
40 | template = file("${path.module}/../manifests/nginx-service.yaml")
41 | }
42 |
43 | resource "k8s_manifest" "nginx-service" {
44 | content = data.template_file.nginx-service.rendered
45 | namespace = "nginx"
46 | }
47 |
48 | data "template_file" "nginx-ingress" {
49 | template = file("${path.module}/../manifests/nginx-ingress.yaml")
50 | }
51 |
52 | resource "k8s_manifest" "nginx-ingress" {
53 | content = data.template_file.nginx-ingress.rendered
54 | namespace = "nginx"
55 | }
56 |
57 | data "template_file" "nginx-pvc" {
58 | template = file("${path.module}/../manifests/nginx-pvc.yaml")
59 | }
60 |
61 | resource "k8s_manifest" "nginx-pvc" {
62 | content = data.template_file.nginx-pvc.rendered
63 | namespace = "nginx"
64 | }
65 |
66 | # Kind local storage provisioner doesn't work without binding the PVC
67 | resource "k8s_manifest" "nginx-pvc-pod" {
68 | content = file("${path.module}/../manifests/nginx-pvc-pod.yaml")
69 | namespace = "nginx"
70 | }
71 |
72 | resource "k8s_manifest" "crontab-crd" {
73 | content = file("${path.module}/../manifests/crontab-crd.yaml")
74 | }
75 |
76 | resource "k8s_manifest" "crontab-resource" {
77 | content = file("${path.module}/../manifests/crontab-resource.yaml")
78 | namespace = "nginx"
79 | depends_on = [k8s_manifest.crontab-crd]
80 | }
81 |
--------------------------------------------------------------------------------
/test/terraform/variables.tf:
--------------------------------------------------------------------------------
1 | variable "greeting" {
2 | type = string
3 | default = "Hello, world!"
4 | }
5 |
6 | variable "replicas" {
7 | type = string
8 | default = 3
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/test/terraform/versions.tf:
--------------------------------------------------------------------------------
1 |
2 | terraform {
3 | required_version = ">= 0.12"
4 | }
5 |
--------------------------------------------------------------------------------