diff --git a/.run/Template Go Test.run.xml b/.run/Template Go Test.run.xml
index 8bd97a59..063dca67 100644
--- a/.run/Template Go Test.run.xml
+++ b/.run/Template Go Test.run.xml
@@ -4,7 +4,7 @@
-
+
diff --git a/.run/TestAccKubectl_Patch.run.xml b/.run/TestAccKubectl_Patch.run.xml
new file mode 100644
index 00000000..1c18e5bd
--- /dev/null
+++ b/.run/TestAccKubectl_Patch.run.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.run/kubectl.run.xml b/.run/kubectl.run.xml
new file mode 100644
index 00000000..61b2463f
--- /dev/null
+++ b/.run/kubectl.run.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cmd/kubectl/main.go b/cmd/kubectl/main.go
new file mode 100644
index 00000000..58e5f82e
--- /dev/null
+++ b/cmd/kubectl/main.go
@@ -0,0 +1,17 @@
+package main
+
+import (
+ "k8s.io/component-base/cli"
+ "k8s.io/kubectl/pkg/cmd"
+ "k8s.io/kubectl/pkg/cmd/util"
+)
+
+// This is a placeholder launching kubectl main executable.
+// Useful in order to debug some things
+func main() {
+ command := cmd.NewDefaultKubectlCommand()
+ if err := cli.RunNoErrOutput(command); err != nil {
+ // Pretty-print the error and exit with an error.
+ util.CheckErr(err)
+ }
+}
diff --git a/docs/index.md b/docs/index.md
old mode 100755
new mode 100644
diff --git a/go.mod b/go.mod
index 1da0de86..69c4a4e8 100644
--- a/go.mod
+++ b/go.mod
@@ -4,22 +4,22 @@ go 1.18
require (
github.com/cenkalti/backoff/v4 v4.1.1
- github.com/hashicorp/go-plugin v1.4.4
github.com/hashicorp/hcl/v2 v2.12.0
github.com/hashicorp/terraform v0.12.29
- github.com/hashicorp/terraform-plugin-go v0.9.1
github.com/hashicorp/terraform-plugin-sdk/v2 v2.16.0
github.com/icza/dyno v0.0.0-20200205103839-49cb13720835
github.com/mitchellh/go-homedir v1.1.0
+ github.com/mitchellh/mapstructure v1.5.0
github.com/stretchr/testify v1.7.0
+ github.com/thedevsaddam/gojsonq/v2 v2.5.2
github.com/zclconf/go-cty v1.10.0
github.com/zclconf/go-cty-yaml v1.0.2
- google.golang.org/grpc v1.46.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.24.0
k8s.io/apimachinery v0.24.0
k8s.io/cli-runtime v0.24.0
k8s.io/client-go v0.24.0
+ k8s.io/component-base v0.24.0
k8s.io/kube-aggregator v0.21.3
k8s.io/kubectl v0.24.0
sigs.k8s.io/yaml v1.2.0
@@ -43,6 +43,8 @@ require (
github.com/bmatcuk/doublestar v1.1.5 // indirect
github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd // indirect
+ github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/emicklei/go-restful v2.9.5+incompatible // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
@@ -70,12 +72,14 @@ require (
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect
github.com/hashicorp/go-hclog v1.2.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/hashicorp/go-plugin v1.4.4 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.4.0 // indirect
github.com/hashicorp/hc-install v0.3.2 // indirect
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.16.1 // indirect
github.com/hashicorp/terraform-json v0.13.0 // indirect
+ github.com/hashicorp/terraform-plugin-go v0.9.1 // indirect
github.com/hashicorp/terraform-plugin-log v0.4.0 // indirect
github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896 // indirect
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 // indirect
@@ -86,13 +90,13 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
+ github.com/lithammer/dedent v1.1.0 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
- github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
@@ -100,14 +104,15 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/oklog/run v1.0.0 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday v1.5.2 // indirect
github.com/spf13/cobra v1.4.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
- github.com/thedevsaddam/gojsonq/v2 v2.5.2 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect
github.com/vmihailenco/tagparser v0.1.1 // indirect
@@ -122,15 +127,18 @@ require (
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368 // indirect
+ google.golang.org/grpc v1.46.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
- k8s.io/component-base v0.24.0 // indirect
+ k8s.io/component-helpers v0.24.0 // indirect
k8s.io/klog/v2 v2.60.1 // indirect
k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect
+ k8s.io/metrics v0.24.0 // indirect
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
sigs.k8s.io/kustomize/api v0.11.4 // indirect
+ sigs.k8s.io/kustomize/kustomize/v4 v4.5.4 // indirect
sigs.k8s.io/kustomize/kyaml v0.13.6 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
)
diff --git a/go.sum b/go.sum
index 41987d5a..552315a2 100644
--- a/go.sum
+++ b/go.sum
@@ -194,11 +194,13 @@ github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd h1:uVsMphB1eRx7xB1njzL3fuMdWRN8HtVzoUOItHMwv5c=
github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/dnaeon/go-vcr v0.0.0-20180920040454-5637cf3d8a31/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
+github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68=
github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@@ -330,6 +332,7 @@ github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e h1:KhcknUwkWHKZPbFy2P7jH5LKJ3La+0ZeknkkmrSgqb0=
github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -555,6 +558,7 @@ github.com/likexian/gokit v0.20.15/go.mod h1:kn+nTv3tqh6yhor9BC4Lfiu58SmH8NmQ2Pm
github.com/likexian/simplejson-go v0.0.0-20190409170913-40473a74d76d/go.mod h1:Typ1BfnATYtZ/+/shXfFYLrovhFyuKvzwrdOnIDHlmg=
github.com/likexian/simplejson-go v0.0.0-20190419151922-c1f9f0b4f084/go.mod h1:U4O1vIJvIKwbMZKUJ62lppfdvkCdVd2nfMimHK81eec=
github.com/likexian/simplejson-go v0.0.0-20190502021454-d8787b4bfa0b/go.mod h1:3BWwtmKP9cXWwYCr5bkoVDEfLywacOv0s06OBEDpyt8=
+github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY=
github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=
github.com/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82/go.mod h1:y54tfGmO3NKssKveTEFFzH8C/akrSOy/iW9qEAUDV84=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@@ -634,6 +638,7 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758=
@@ -656,6 +661,7 @@ github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/packer-community/winrmcp v0.0.0-20180102160824-81144009af58/go.mod h1:f6Izs6JvFTdnRbziASagjZ2vmf55NSIkC/weStxCHqk=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -1324,6 +1330,7 @@ k8s.io/code-generator v0.24.0/go.mod h1:dpVhs00hTuTdTY6jvVxvTFCk6gSMrtfRydbhZwHI
k8s.io/component-base v0.21.3/go.mod h1:kkuhtfEHeZM6LkX0saqSK8PbdO7A0HigUngmhhrwfGQ=
k8s.io/component-base v0.24.0 h1:h5jieHZQoHrY/lHG+HyrSbJeyfuitheBvqvKwKHVC0g=
k8s.io/component-base v0.24.0/go.mod h1:Dgazgon0i7KYUsS8krG8muGiMVtUZxG037l1MKyXgrA=
+k8s.io/component-helpers v0.24.0 h1:hZIHGfdd55thhqd9oxjDTw68OAPauDMJ+8hC69aNw1I=
k8s.io/component-helpers v0.24.0/go.mod h1:Q2SlLm4h6g6lPTC9GMMfzdywfLSvJT2f1hOnnjaWD8c=
k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
@@ -1342,6 +1349,7 @@ k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 h1:Gii5eqf+GmIEwGNKQYQClC
k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42/go.mod h1:Z/45zLw8lUo4wdiUkI+v/ImEGAvu3WatcZl3lPMR4Rk=
k8s.io/kubectl v0.24.0 h1:nA+WtMLVdXUs4wLogGd1mPTAesnLdBpCVgCmz3I7dXo=
k8s.io/kubectl v0.24.0/go.mod h1:pdXkmCyHiRTqjYfyUJiXtbVNURhv0/Q1TyRhy2d5ic0=
+k8s.io/metrics v0.24.0 h1:nsFLJBDgj+B8mXvVBWFxTZBRRDJ8uTdf4C/Gedjy9BA=
k8s.io/metrics v0.24.0/go.mod h1:jrLlFGdKl3X+szubOXPG0Lf2aVxuV3QJcbsgVRAM6fI=
k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
@@ -1356,6 +1364,7 @@ sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz
sigs.k8s.io/kustomize/api v0.11.4 h1:/0Mr3kfBBNcNPOW5Qwk/3eb8zkswCwnqQxxKtmrTkRo=
sigs.k8s.io/kustomize/api v0.11.4/go.mod h1:k+8RsqYbgpkIrJ4p9jcdPqe8DprLxFUUO0yNOq8C+xI=
sigs.k8s.io/kustomize/cmd/config v0.10.6/go.mod h1:/S4A4nUANUa4bZJ/Edt7ZQTyKOY9WCER0uBS1SW2Rco=
+sigs.k8s.io/kustomize/kustomize/v4 v4.5.4 h1:rzGrL+DA4k8bT6SMz7/U+2z3iiZf1t2RaYJWx8OeTmE=
sigs.k8s.io/kustomize/kustomize/v4 v4.5.4/go.mod h1:Zo/Xc5FKD6sHl0lilbrieeGeZHVYCA4BzxeAaLI05Bg=
sigs.k8s.io/kustomize/kyaml v0.13.6 h1:eF+wsn4J7GOAXlvajv6OknSunxpcOBQQqsnPxObtkGs=
sigs.k8s.io/kustomize/kyaml v0.13.6/go.mod h1:yHP031rn1QX1lr/Xd934Ri/xdVNG8BE2ECa78Ht/kEg=
diff --git a/kubernetes/provider.go b/kubernetes/provider.go
index 945335ad..9b5b028b 100644
--- a/kubernetes/provider.go
+++ b/kubernetes/provider.go
@@ -12,6 +12,7 @@ import (
k8sresource "k8s.io/cli-runtime/pkg/resource"
"k8s.io/client-go/discovery"
diskcached "k8s.io/client-go/discovery/cached/disk"
+ "k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
_ "k8s.io/client-go/plugin/pkg/client/auth"
restclient "k8s.io/client-go/rest"
@@ -166,6 +167,7 @@ func Provider() *schema.Provider {
ResourcesMap: map[string]*schema.Resource{
"kubectl_manifest": resourceKubectlManifest(),
"kubectl_server_version": resourceKubectlServerVersion(),
+ "kubectl_patch": resourceKubectlPatch(),
},
}
@@ -216,6 +218,18 @@ func (p *KubeProvider) ToRESTMapper() (meta.RESTMapper, error) {
return nil, fmt.Errorf("no restmapper")
}
+func (p *KubeProvider) DynamicClient() (dynamic.Interface, error) {
+ config, err := p.ToRESTConfig()
+ if err != nil {
+ return nil, err
+ }
+ dynClient, err := dynamic.NewForConfig(config)
+ if err != nil {
+ return nil, err
+ }
+
+ return dynClient, err
+}
var kubectlApplyRetryCount uint64
diff --git a/kubernetes/resource_kubectl_manifest.go b/kubernetes/resource_kubectl_manifest.go
index 78883227..6c632c6a 100644
--- a/kubernetes/resource_kubectl_manifest.go
+++ b/kubernetes/resource_kubectl_manifest.go
@@ -30,7 +30,7 @@ import (
"k8s.io/kubectl/pkg/cmd/apply"
k8sdelete "k8s.io/kubectl/pkg/cmd/delete"
- backoff "github.com/cenkalti/backoff/v4"
+ "github.com/cenkalti/backoff/v4"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
apps_v1 "k8s.io/api/apps/v1"
@@ -894,14 +894,14 @@ func waitForFields(ctx context.Context, provider *RestClientResult, conditions [
return resource.NonRetryableError(err)
}
- //convert to json and create a json query object from it
+ // convert to json and create a json query object from it
yamlJson, err := rawResponse.MarshalJSON()
if err != nil {
return resource.NonRetryableError(err)
}
gq := gojsonq.New().FromString(string(yamlJson))
for _, c := range conditions {
- //find the key
+ // find the key
v := gq.Reset().Find(c.Key)
if v == nil {
return resource.RetryableError(fmt.Errorf("key %s was not found in the resource %s", c.Key, name))
diff --git a/kubernetes/resource_kubectl_manifest_test.go b/kubernetes/resource_kubectl_manifest_test.go
index 03032218..d97e9eee 100644
--- a/kubernetes/resource_kubectl_manifest_test.go
+++ b/kubernetes/resource_kubectl_manifest_test.go
@@ -75,7 +75,7 @@ YAML
}
`
- //start := time.Now()
+ // start := time.Now()
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
@@ -83,7 +83,7 @@ YAML
Steps: []resource.TestStep{
{
Config: config,
- //todo: improve checking
+ // todo: improve checking
},
},
})
@@ -531,12 +531,12 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
{
description: "Simple map with string value",
userProvided: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
},
liveManifest: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
},
- expectedFields: "test1=test2",
+ expectedFields: "test1=readUnstructuredFromK8s",
expectedFingerprint: "9369bac4ce5d012a79110117b871e20bb3484dab079d1471ee5981da42fb4a30",
expectedDrift: false,
},
@@ -544,18 +544,18 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
// Ensure skippable fields are skipped
description: "Simple map with string value and Skippable fields",
userProvided: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"metadata": map[string]interface{}{
"resourceVersion": "1245",
},
},
liveManifest: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"metadata": map[string]interface{}{
"resourceVersion": "1245",
},
},
- expectedFields: "test1=test2",
+ expectedFields: "test1=readUnstructuredFromK8s",
expectedFingerprint: "9369bac4ce5d012a79110117b871e20bb3484dab079d1471ee5981da42fb4a30",
expectedDrift: false,
},
@@ -563,14 +563,14 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
// Ensure ignored fields are skipped
description: "Simple map with string value and ignored fields",
userProvided: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"ignoreThis": "1245",
},
liveManifest: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"ignoreThis": "1245",
},
- expectedFields: "test1=test2",
+ expectedFields: "test1=readUnstructuredFromK8s",
expectedFingerprint: "9369bac4ce5d012a79110117b871e20bb3484dab079d1471ee5981da42fb4a30",
ignored: []string{"ignoreThis"},
expectedDrift: false,
@@ -579,18 +579,18 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
// Ensure ignored sub fields are skipped
description: "Simple map with string value and ignored fields",
userProvided: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"ignore": map[string]string{
"this": "5432",
},
},
liveManifest: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"ignore": map[string]string{
"this": "1245",
},
},
- expectedFields: "test1=test2",
+ expectedFields: "test1=readUnstructuredFromK8s",
expectedFingerprint: "9369bac4ce5d012a79110117b871e20bb3484dab079d1471ee5981da42fb4a30",
ignored: []string{"ignore.this"},
expectedDrift: false,
@@ -599,18 +599,18 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
// Ensure ignored sub fields are skipped
description: "Simple map with string ignore nested fields",
userProvided: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"ignore": map[string]string{
"this": "5432",
},
},
liveManifest: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"ignore": map[string]string{
"this": "1245",
},
},
- expectedFields: "test1=test2",
+ expectedFields: "test1=readUnstructuredFromK8s",
expectedFingerprint: "9369bac4ce5d012a79110117b871e20bb3484dab079d1471ee5981da42fb4a30",
ignored: []string{"ignore"},
expectedDrift: false,
@@ -619,13 +619,13 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
// Ensure ignored sub fields are skipped
description: "Simple map with string ignore highly nested fields",
userProvided: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"ignore": map[string]string{
"this": "5432",
},
},
liveManifest: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"ignore": map[string]interface{}{
"this": "1245",
"also": map[string]string{
@@ -633,7 +633,7 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
},
},
},
- expectedFields: "test1=test2",
+ expectedFields: "test1=readUnstructuredFromK8s",
expectedFingerprint: "9369bac4ce5d012a79110117b871e20bb3484dab079d1471ee5981da42fb4a30",
ignored: []string{"ignore"},
expectedDrift: false,
@@ -642,18 +642,18 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
// Ensure nested `map[string]string` are supported
description: "Map with nested map[string]string",
userProvided: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"nest": map[string]string{
"bob": "bill",
},
},
liveManifest: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"nest": map[string]string{
"bob": "bill",
},
},
- expectedFields: "nest.bob=bill,test1=test2",
+ expectedFields: "nest.bob=bill,test1=readUnstructuredFromK8s",
expectedFingerprint: "3101bf7d8f32b48993efa15e0fdd439237e63ef093d23e92deb9b8485e3faa03",
expectedDrift: false,
},
@@ -661,7 +661,7 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
// Ensure nested `map[string]string` with different ordering are supported
description: "Map with nested map[string]string with different ordering",
userProvided: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"nest": map[string]string{
"bob1": "bill",
"bob2": "bill",
@@ -669,21 +669,21 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
},
},
liveManifest: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"nest": map[string]string{
"bob2": "bill",
"bob1": "bill",
"bob3": "bill",
},
},
- expectedFields: "nest.bob1=bill,nest.bob2=bill,nest.bob3=bill,test1=test2",
+ expectedFields: "nest.bob1=bill,nest.bob2=bill,nest.bob3=bill,test1=readUnstructuredFromK8s",
expectedFingerprint: "0ad7f5a7682d24a2105a457f9093ab406d9a3c92a14d1e67e25ac0a1fea79ca9",
expectedDrift: false,
},
{
description: "Map with nested map[string]string with nested slice",
userProvided: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"nest": map[string]interface{}{
"bob1": []interface{}{
"a",
@@ -693,7 +693,7 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
},
},
liveManifest: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"nest": map[string]interface{}{
"bob1": []interface{}{
"c",
@@ -702,14 +702,14 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
},
},
},
- expectedFields: "nest.bob1.#=3,nest.bob1.0=c,nest.bob1.1=b,nest.bob1.2=a,test1=test2",
+ expectedFields: "nest.bob1.#=3,nest.bob1.0=c,nest.bob1.1=b,nest.bob1.2=a,test1=readUnstructuredFromK8s",
expectedFingerprint: "7c234055ab3af4bfc4541b4f11ebe41f089f65ff2276454783fd066c4e890bb9",
expectedDrift: true,
},
{
description: "Map with nested map[string]string with nested array and nested map",
userProvided: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"nest": map[string]interface{}{
"bob1": []interface{}{
map[string]string{
@@ -726,7 +726,7 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
},
},
liveManifest: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"nest": map[string]interface{}{
"bob1": []interface{}{
map[string]string{
@@ -742,7 +742,7 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
},
},
},
- expectedFields: "nest.bob1.#=2,nest.bob1.0.1=1,nest.bob1.0.2=2,nest.bob1.0.3=3,nest.bob1.1.1=1,nest.bob1.1.2=2,nest.bob1.1.3=3,test1=test2",
+ expectedFields: "nest.bob1.#=2,nest.bob1.0.1=1,nest.bob1.0.2=2,nest.bob1.0.3=3,nest.bob1.1.1=1,nest.bob1.1.2=2,nest.bob1.1.3=3,test1=readUnstructuredFromK8s",
expectedFingerprint: "f3efd8721cbfa6421a4230c6fffdac94d63a51e57097a45979972e6654a992da",
expectedDrift: false,
},
@@ -750,14 +750,14 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
// Ensure ordering of the fields doesn't affect matching
description: "Different Ordering",
userProvided: map[string]interface{}{
- "ztest1": "test2",
- "afield": "test2",
+ "ztest1": "readUnstructuredFromK8s",
+ "afield": "readUnstructuredFromK8s",
},
liveManifest: map[string]interface{}{
- "afield": "test2",
- "ztest1": "test2",
+ "afield": "readUnstructuredFromK8s",
+ "ztest1": "readUnstructuredFromK8s",
},
- expectedFields: "afield=test2,ztest1=test2",
+ expectedFields: "afield=readUnstructuredFromK8s,ztest1=readUnstructuredFromK8s",
expectedFingerprint: "6ddd159d93a55b78442c74cacfff5a2afb04ead770f87ac0af1b7471e71ddead",
expectedDrift: false,
},
@@ -768,15 +768,15 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
"ztest1": []string{
"1", "2",
},
- "afield": "test2",
+ "afield": "readUnstructuredFromK8s",
},
liveManifest: map[string]interface{}{
- "afield": "test2",
+ "afield": "readUnstructuredFromK8s",
"ztest1": []string{
"1", "2",
},
},
- expectedFields: "afield=test2,ztest1.#=2,ztest1.0=1,ztest1.1=2",
+ expectedFields: "afield=readUnstructuredFromK8s,ztest1.#=2,ztest1.0=1,ztest1.1=2",
expectedFingerprint: "d09ba05ec3c744be7174243acfd2370a6d0dabfbe7980bc5ee02c0790d383960",
expectedDrift: false,
},
@@ -784,15 +784,15 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
// Ensure fields added to the `liveManifest` which aren't present in the `originl` are ignored
description: "Ignore additional fields",
userProvided: map[string]interface{}{
- "afield": "test2",
+ "afield": "readUnstructuredFromK8s",
},
liveManifest: map[string]interface{}{
- "afield": "test2",
+ "afield": "readUnstructuredFromK8s",
"ztest1": []string{
"1", "2",
},
},
- expectedFields: "afield=test2",
+ expectedFields: "afield=readUnstructuredFromK8s",
expectedFingerprint: "18cf5c716095e42b64da5d4929c605022b6799fb3866bf9f1d12f4e30d40c185",
expectedDrift: false,
},
@@ -800,13 +800,13 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
// Ensure that fields present in the `userProvided` but missing in the `liveManifest` are skipped
description: "Handle removed fields",
userProvided: map[string]interface{}{
- "afield": "test2",
- "igetlost": "test2",
+ "afield": "readUnstructuredFromK8s",
+ "igetlost": "readUnstructuredFromK8s",
},
liveManifest: map[string]interface{}{
- "afield": "test2",
+ "afield": "readUnstructuredFromK8s",
},
- expectedFields: "afield=test2",
+ expectedFields: "afield=readUnstructuredFromK8s",
expectedFingerprint: "18cf5c716095e42b64da5d4929c605022b6799fb3866bf9f1d12f4e30d40c185",
expectedDrift: true,
},
@@ -839,7 +839,7 @@ func TestGetLiveManifestFilteredForUserProvidedOnly(t *testing.T) {
// Ensure that the updated value fo the `liveManifest` object is taken for the `willchange` field
description: "Map with nested map[string]string with updated field",
userProvided: map[string]interface{}{
- "test1": "test2",
+ "test1": "readUnstructuredFromK8s",
"nest": map[string]string{
"willchange": "bill",
},
diff --git a/kubernetes/resource_kubectl_patch.go b/kubernetes/resource_kubectl_patch.go
new file mode 100644
index 00000000..5651d4fa
--- /dev/null
+++ b/kubernetes/resource_kubectl_patch.go
@@ -0,0 +1,223 @@
+package kubernetes
+
+import (
+ "context"
+ "fmt"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+ "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/apimachinery/pkg/util/yaml"
+ "k8s.io/cli-runtime/pkg/resource"
+ cmdutil "k8s.io/kubectl/pkg/cmd/util"
+ "log"
+ "reflect"
+ "strings"
+)
+
+var patchTypes = map[string]types.PatchType{"json": types.JSONPatchType, "merge": types.MergePatchType, "strategic": types.StrategicMergePatchType}
+
+func resourceKubectlPatchRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ _, err := readUnstructuredFromK8s(
+ meta.(*KubeProvider),
+ d.Get("name").(string),
+ d.Get("namespace").(string),
+ d.Get("type").(string),
+ )
+ return diag.FromErr(err)
+}
+
+func resourceKubectlPatch() *schema.Resource {
+ return &schema.Resource{
+ CreateContext: resourceKubectlPatchCreate,
+ ReadContext: resourceKubectlPatchRead,
+ DeleteContext: func(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ return nil
+ },
+ UpdateContext: func(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ return nil
+ },
+ Schema: map[string]*schema.Schema{
+ "type": {
+ Type: schema.TypeString,
+ Description: "Object to patch, i.e. secret, configmap, etc",
+ Required: true,
+ ForceNew: true,
+ },
+ "name": {
+ Type: schema.TypeString,
+ Description: "Name of the object which should be patched",
+ Required: true,
+ ForceNew: true,
+ },
+ "namespace": {
+ Type: schema.TypeString,
+ Description: "Namespace of the object which should be patched",
+ Optional: true,
+ ForceNew: true,
+ },
+ "patch_type": {
+ Type: schema.TypeString,
+ Description: "Type of the patch. Can be json, merge, strategic",
+ Default: "strategic",
+ Optional: true,
+ },
+ "patch": {
+ Type: schema.TypeString,
+ Description: "The patch to be applied to the resource JSON file.",
+ Required: true,
+ },
+ "field_manager": {
+ Type: schema.TypeString,
+ Description: "Field manager value (who is applying the change)",
+ Default: "terraform_kubectl_patch",
+ Optional: true,
+ },
+ "patch_condition": {
+ Type: schema.TypeMap,
+ Description: "If not empty, kubectl_patch will check for a given condition before running the apply operation",
+ Optional: true,
+ },
+ "fail_if_unchanged": {
+ Type: schema.TypeBool,
+ Description: "If set to true, the operation will fail if the contents of the target object were not changed. Defaults to false",
+ Optional: true,
+ Default: false,
+ },
+ },
+ }
+}
+func resourceKubectlPatchCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
+ var err error
+ provider := meta.(*KubeProvider)
+ factory := cmdutil.NewFactory(provider)
+
+ patchType := patchTypes[strings.ToLower(d.Get("patch_type").(string))]
+ if patchType == "" {
+ log.Printf("[ERROR] invalid patch type: %+v", d.Get("patch_type"))
+ return diag.FromErr(fmt.Errorf("Unsupported patch type %v", d.Get("patch_type")))
+ }
+ objectType := d.Get("type").(string)
+ objectName := d.Get("name").(string)
+ namespace := d.Get("namespace").(string)
+ if namespace == "" {
+ namespace = "default"
+ }
+ patchBytes := []byte(d.Get("patch").(string))
+ patchBytes, err = yaml.ToJSON(patchBytes)
+ if err != nil {
+ log.Printf("[ERROR] invalid yaml xxx: %+v", err)
+ return diag.FromErr(err)
+ }
+
+ r := factory.NewBuilder().
+ Unstructured().
+ ContinueOnError().
+ NamespaceParam(namespace).DefaultNamespace().
+ ResourceTypeOrNameArgs(
+ false,
+ objectType,
+ objectName).
+ Flatten().
+ Do()
+ if err := r.Err(); err != nil {
+ return diag.FromErr(err)
+ }
+ err = r.Visit(func(info *resource.Info, err error) error {
+ if err != nil {
+ return err
+ }
+ mapping := info.ResourceMapping()
+ client, err := factory.UnstructuredClientForMapping(mapping)
+ if err != nil {
+ return err
+ }
+ helper := resource.
+ NewHelper(client, mapping).
+ // DryRun(false).
+ WithFieldManager(d.Get("field_manager").(string))
+ patchedObj, err := helper.Patch(
+ info.Namespace,
+ info.Name,
+ patchType,
+ patchBytes,
+ nil,
+ )
+ if err != nil {
+ return err
+ }
+ // check if there is a requirement for an object to be changed
+ if d.Get("fail_if_unchanged").(bool) {
+ didPatch := !reflect.DeepEqual(info.Object, patchedObj)
+ if !didPatch {
+ return fmt.Errorf("object was not affected by the patch")
+ }
+ }
+ rawObject, err := runtime.DefaultUnstructuredConverter.ToUnstructured(patchedObj)
+ if err != nil {
+ return err
+ }
+ // find the object id
+ id, found, err := unstructured.NestedString(rawObject, "metadata", "uid")
+ switch {
+ case err != nil:
+ return err
+ case !found:
+ return fmt.Errorf("object not found post patch")
+ default:
+ d.SetId(id)
+ }
+
+ return nil
+ })
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ return resourceKubectlPatchRead(ctx, d, meta)
+}
+
+// readUnstructuredFromK8s returns an unstructured runtime object from kubernetes
+func readUnstructuredFromK8s(provider *KubeProvider, name, namespace, objectType string) (runtime.Object, error) {
+ factory := cmdutil.NewFactory(provider)
+ r := factory.NewBuilder().
+ Unstructured().
+ NamespaceParam(namespace).DefaultNamespace().
+ ResourceTypeOrNameArgs(true, objectType, name).
+ // ContinueOnError().
+ Latest().
+ Flatten().
+ // TransformRequests(o.transformRequests). //needed? kind of. Called by .infos()
+ Do()
+ if false {
+ r.IgnoreErrors(errors.IsNotFound)
+ }
+ if err := r.Err(); err != nil {
+ return nil, err
+ }
+ var obj runtime.Object
+ err := r.Visit(func(info *resource.Info, err error) error {
+ if err != nil {
+ return err
+ }
+ // obtain the mapping
+ mapping := info.ResourceMapping()
+
+ client, err := factory.UnstructuredClientForMapping(mapping)
+ if err != nil {
+ return err
+ }
+
+ helper := resource.NewHelper(client, mapping)
+ obj, err = helper.Get(namespace, name)
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ return obj, err
+}
diff --git a/kubernetes/resource_kubectl_patch_test.go b/kubernetes/resource_kubectl_patch_test.go
new file mode 100644
index 00000000..245b897c
--- /dev/null
+++ b/kubernetes/resource_kubectl_patch_test.go
@@ -0,0 +1,87 @@
+package kubernetes
+
+import (
+ "bytes"
+ "fmt"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/apimachinery/pkg/runtime"
+ "log"
+ "testing"
+ "text/template"
+)
+
+// parses go template with any variables attached
+func parseGoTemplate(fileName string, data any) string {
+ t := template.Must(template.ParseFiles(fileName))
+ var out bytes.Buffer
+ err := t.Execute(&out, data)
+ if err != nil {
+ // todo: logging
+ log.Printf("[ERROR] cannot render go template: %+v", err)
+ return ""
+ }
+ return out.String()
+}
+func TestAccKubectl_Patch(t *testing.T) {
+ // start := time.Now()
+ const objectName = "patch-demo-simple"
+ resource.ParallelTest(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ Providers: testAccProviders,
+ CheckDestroy: testAccCheckkubectlDestroy,
+ Steps: []resource.TestStep{
+ {
+ Config: parseGoTemplate("test_files/patch/simple_01.tf", map[string]string{
+ "namespace": "default",
+ "name": objectName,
+ }),
+ Check: func(state *terraform.State) error {
+ // obtain the name, type, ns from the state
+ name, objType, ns, err := nameNsFromState(state, "kubectl_patch.test")
+ if err != nil {
+ return err
+ }
+ rawObject, err := readUnstructuredFromK8s(
+ testAccProvider.Meta().(*KubeProvider),
+ name,
+ ns,
+ objType)
+ if err != nil {
+ return err
+ }
+ // check that the patch worked correctly
+ unstruct, err := runtime.DefaultUnstructuredConverter.ToUnstructured(rawObject)
+ if err != nil {
+ return err
+ }
+ replicas, b, err := unstructured.NestedInt64(unstruct, "spec", "replicas")
+ switch {
+ case err != nil:
+ return err
+ case !b:
+ return fmt.Errorf("not found")
+ case replicas != 2:
+ return fmt.Errorf("Invalid value for spec.replica. Wanted v, got %v", replicas)
+ }
+ return nil
+ },
+ },
+ },
+ })
+}
+
+func nameNsFromState(state *terraform.State, resourceName string) (name, objectType, ns string, err error) {
+ rs, ok := state.RootModule().Resources[resourceName]
+ if !ok {
+ err = fmt.Errorf("not found %v", resourceName)
+ return
+ }
+
+ attributes := rs.Primary.Attributes
+ name = attributes["name"]
+ objectType = attributes["type"]
+ ns = attributes["namespace"]
+ return
+}
diff --git a/kubernetes/test_files/patch/simple_01.tf b/kubernetes/test_files/patch/simple_01.tf
new file mode 100644
index 00000000..8cef9c07
--- /dev/null
+++ b/kubernetes/test_files/patch/simple_01.tf
@@ -0,0 +1,32 @@
+resource "kubectl_manifest" "deployment" {
+ yaml_body = </namespaces//s/"
+//
+// "/apis//namespaces//s/"
//
// The selfLink attribute is not available in Kubernetes 1.20+ so we need
// to generate a consistent, unique ID for our Terraform resources.