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.