From 8a589b7128b03bc3873d0bb4d9e6aecdf9601001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20=20Godoy=20Hern=C3=A1ndez?= Date: Fri, 23 Apr 2021 23:55:07 -0400 Subject: [PATCH 1/4] Add replace --- kubernetes/resource_kubectl_manifest.go | 184 ++++++++++++++++++++++-- 1 file changed, 171 insertions(+), 13 deletions(-) diff --git a/kubernetes/resource_kubectl_manifest.go b/kubernetes/resource_kubectl_manifest.go index ea0af2e2..73df4f03 100644 --- a/kubernetes/resource_kubectl_manifest.go +++ b/kubernetes/resource_kubectl_manifest.go @@ -22,6 +22,8 @@ import ( apiregistration "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" "k8s.io/kubectl/pkg/cmd/apply" k8sdelete "k8s.io/kubectl/pkg/cmd/delete" + "k8s.io/kubectl/pkg/cmd/replace" + cmdutil "k8s.io/kubectl/pkg/cmd/util" "github.com/cenkalti/backoff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -55,12 +57,19 @@ func resourceKubectlManifest() *schema.Resource { if kubectlApplyRetryCount > 0 { retryConfig := backoff.WithMaxRetries(exponentialBackoffConfig, kubectlApplyRetryCount) retryErr := backoff.Retry(func() error { - err := resourceKubectlManifestApply(ctx, d, meta) - if err != nil { - log.Printf("[ERROR] creating manifest failed: %+v", err) + if d.Get("replace").(bool) { + err := resourceKubectlManifestReplace(ctx, d, meta) + if err != nil { + log.Printf("[ERROR] creating manifest failed: %+v", err) + } + return err + } else { + err := resourceKubectlManifestApply(ctx, d, meta) + if err != nil { + log.Printf("[ERROR] creating manifest failed: %+v", err) + } + return err } - - return err }, retryConfig) if retryErr != nil { @@ -69,8 +78,14 @@ func resourceKubectlManifest() *schema.Resource { return nil } else { - if applyErr := resourceKubectlManifestApply(ctx, d, meta); applyErr != nil { - return diag.FromErr(applyErr) + if d.Get("replace").(bool) { + if applyErr := resourceKubectlManifestReplace(ctx, d, meta); applyErr != nil { + return diag.FromErr(applyErr) + } + } else { + if applyErr := resourceKubectlManifestApply(ctx, d, meta); applyErr != nil { + return diag.FromErr(applyErr) + } } return nil @@ -98,11 +113,19 @@ func resourceKubectlManifest() *schema.Resource { if kubectlApplyRetryCount > 0 { retryConfig := backoff.WithMaxRetries(exponentialBackoffConfig, kubectlApplyRetryCount) retryErr := backoff.Retry(func() error { - err := resourceKubectlManifestApply(ctx, d, meta) - if err != nil { - log.Printf("[ERROR] updating manifest failed: %+v", err) + if d.Get("replace").(bool) { + err := resourceKubectlManifestReplace(ctx, d, meta) + if err != nil { + log.Printf("[ERROR] updating manifest failed: %+v", err) + } + return err + } else { + err := resourceKubectlManifestApply(ctx, d, meta) + if err != nil { + log.Printf("[ERROR] updating manifest failed: %+v", err) + } + return err } - return err }, retryConfig) if retryErr != nil { @@ -111,8 +134,14 @@ func resourceKubectlManifest() *schema.Resource { return nil } else { - if applyErr := resourceKubectlManifestApply(ctx, d, meta); applyErr != nil { - return diag.FromErr(applyErr) + if d.Get("replace").(bool) { + if applyErr := resourceKubectlManifestReplace(ctx, d, meta); applyErr != nil { + return diag.FromErr(applyErr) + } + } else { + if applyErr := resourceKubectlManifestApply(ctx, d, meta); applyErr != nil { + return diag.FromErr(applyErr) + } } return nil @@ -432,6 +461,12 @@ metadata: Optional: true, Default: true, }, + "replace": { + Type: schema.TypeBool, + Description: "Default to false (replace). Set this flag to do a kubectl replace instead of apply.", + Optional: true, + Default: true, + }, }, } } @@ -580,6 +615,129 @@ func resourceKubectlManifestApply(ctx context.Context, d *schema.ResourceData, m return resourceKubectlManifestReadUsingClient(ctx, d, meta, restClient.ResourceInterface, manifest) } +func resourceKubectlManifestReplace(ctx context.Context, d *schema.ResourceData, meta interface{}) error { + + yaml := d.Get("yaml_body").(string) + manifest, err := parseYaml(yaml) + if err != nil { + return fmt.Errorf("failed to parse kubernetes resource: %+v", err) + } + + if overrideNamespace, ok := d.GetOk("override_namespace"); ok { + manifest.unstruct.SetNamespace(overrideNamespace.(string)) + } + + log.Printf("[DEBUG] %v replace kubernetes resource:\n%s", manifest, yaml) + + // Create a client to talk to the resource API based on the APIVersion and Kind + // defined in the YAML + restClient := getRestClientFromUnstructured(manifest, meta.(*KubeProvider)) + if restClient.Error != nil { + return fmt.Errorf("%v failed to create kubernetes rest client for update of resource: %+v", manifest, restClient.Error) + } + + // Update the resource in Kubernetes, using a temp file + yamlJson, err := manifest.unstruct.MarshalJSON() + if err != nil { + return fmt.Errorf("%v failed to convert object to json: %+v", manifest, err) + } + + yamlParsed, err := yamlWriter.JSONToYAML(yamlJson) + if err != nil { + return fmt.Errorf("%v failed to convert json to yaml: %+v", manifest, err) + } + + yaml = string(yamlParsed) + + tmpfile, _ := ioutil.TempFile("", "*kubectl_manifest.yaml") + _, _ = tmpfile.Write([]byte(yaml)) + _ = tmpfile.Close() + + replaceOptions := replace.NewReplaceOptions(genericclioptions.IOStreams{ + In: strings.NewReader(yaml), + Out: log.Writer(), + ErrOut: log.Writer(), + }) + replaceOptions.Builder = func() *k8sresource.Builder { + return k8sresource.NewBuilder(k8sresource.RESTClientGetter(meta.(*KubeProvider))) + } + replaceOptions.DeleteOptions = &k8sdelete.DeleteOptions{ + FilenameOptions: k8sresource.FilenameOptions{ + Filenames: []string{tmpfile.Name()}, + }, + } + + if manifest.hasNamespace() { + replaceOptions.Namespace = manifest.unstruct.GetNamespace() + } + + log.Printf("[INFO] %s perform replace of manifest", manifest) + + f := cmdutil.NewFactory(nil) + + err = replaceOptions.Run(f) + _ = os.Remove(tmpfile.Name()) + if err != nil { + return fmt.Errorf("%v failed to run apply: %+v", manifest, err) + } + + log.Printf("[INFO] %v manifest applied, fetch resource from kubernetes", manifest) + + // get the resource from Kubernetes + response, err := restClient.ResourceInterface.Get(ctx, manifest.unstruct.GetName(), meta_v1.GetOptions{}) + if err != nil { + return fmt.Errorf("%v failed to fetch resource from kubernetes: %+v", manifest, err) + } + + // get selfLink or generate (for Kubernetes 1.20+) + selfLink := response.GetSelfLink() + if len(selfLink) == 0 { + selfLink = generateSelfLink( + response.GetAPIVersion(), + response.GetNamespace(), + response.GetKind(), + response.GetName()) + } + + d.SetId(selfLink) + log.Printf("[DEBUG] %v fetched successfully, set id to: %v", manifest, d.Id()) + + // Capture the UID and Resource_version at time of update + // this allows us to diff these against the actual values + // read in by the 'resourceKubectlManifestRead' + _ = d.Set("uid", response.GetUID()) + _ = d.Set("resource_version", response.GetResourceVersion()) + + comparisonOutput, err := getLiveManifestFilteredForUserProvidedOnly(d, manifest.unstruct, response) + if err != nil { + return fmt.Errorf("%v failed to compare maps of manifest vs version in kubernetes: %+v", manifest, err) + } + + _ = d.Set("yaml_incluster", comparisonOutput) + + if d.Get("wait_for_rollout").(bool) { + timeout := d.Timeout(schema.TimeoutCreate) + + if manifest.unstruct.GetKind() == "Deployment" { + log.Printf("[INFO] %v waiting for deployment rollout for %vmin", manifest, timeout.Minutes()) + err = resource.RetryContext(ctx, timeout, + waitForDeploymentReplicasFunc(ctx, meta.(*KubeProvider), manifest.unstruct.GetNamespace(), manifest.unstruct.GetName())) + if err != nil { + return err + } + } else if manifest.unstruct.GetKind() == "APIService" && manifest.unstruct.GetAPIVersion() == "apiregistration.k8s.io/v1" { + log.Printf("[INFO] %v waiting for APIService rollout for %vmin", manifest, timeout.Minutes()) + err = resource.RetryContext(ctx, timeout, + waitForAPIServiceAvailableFunc(ctx, meta.(*KubeProvider), manifest.unstruct.GetName())) + if err != nil { + return err + } + } + } + + return resourceKubectlManifestReadUsingClient(ctx, d, meta, restClient.ResourceInterface, manifest) +} + func resourceKubectlManifestRead(ctx context.Context, d *schema.ResourceData, meta interface{}) error { yaml := d.Get("yaml_body").(string) manifest, err := parseYaml(yaml) From 303ef35497e6f2160503aaa5dc1b5e5d5f85d620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20=20Godoy=20Hern=C3=A1ndez?= Date: Sun, 25 Apr 2021 12:52:29 -0400 Subject: [PATCH 2/4] Fix missing replaceOptions --- docs/resources/kubectl_manifest.md | 1 + kubernetes/resource_kubectl_manifest.go | 43 ++++++++++++++++++------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/docs/resources/kubectl_manifest.md b/docs/resources/kubectl_manifest.md index 16be3227..68ecd83c 100644 --- a/docs/resources/kubectl_manifest.md +++ b/docs/resources/kubectl_manifest.md @@ -44,6 +44,7 @@ YAML * `validate_schema` - Optional. Setting to `false` will mimic `kubectl apply --validate=false` mode. Default `true`. * `wait` - Optional. Set this flag to wait or not for finalized to complete for deleted objects. Default `false`. * `wait_for_rollout` - Optional. Set this flag to wait or not for Deployments and APIService to complete rollout. Default `true`. +* `replace` - Optional. Set this flag to do a `kubectl replace --force` instead of apply. Default `false`. ## Attribute Reference diff --git a/kubernetes/resource_kubectl_manifest.go b/kubernetes/resource_kubectl_manifest.go index 73df4f03..776e75af 100644 --- a/kubernetes/resource_kubectl_manifest.go +++ b/kubernetes/resource_kubectl_manifest.go @@ -4,15 +4,16 @@ import ( "context" "encoding/json" "fmt" + "io/ioutil" + "os" + "sort" + "time" + "github.com/gavinbunney/terraform-provider-kubectl/flatten" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "io/ioutil" "k8s.io/cli-runtime/pkg/printers" "k8s.io/kubectl/pkg/validation" - "os" - "sort" - "time" "log" "strings" @@ -34,6 +35,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" meta_v1_unstruct "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" k8sschema "k8s.io/apimachinery/pkg/runtime/schema" yamlWriter "sigs.k8s.io/yaml" @@ -79,8 +81,8 @@ func resourceKubectlManifest() *schema.Resource { return nil } else { if d.Get("replace").(bool) { - if applyErr := resourceKubectlManifestReplace(ctx, d, meta); applyErr != nil { - return diag.FromErr(applyErr) + if replaceErr := resourceKubectlManifestReplace(ctx, d, meta); replaceErr != nil { + return diag.FromErr(replaceErr) } } else { if applyErr := resourceKubectlManifestApply(ctx, d, meta); applyErr != nil { @@ -463,7 +465,7 @@ metadata: }, "replace": { Type: schema.TypeBool, - Description: "Default to false (replace). Set this flag to do a kubectl replace instead of apply.", + Description: "Default to false (replace). Set this flag to do a 'kubectl replace --force' instead of apply.", Optional: true, Default: true, }, @@ -653,15 +655,36 @@ func resourceKubectlManifestReplace(ctx context.Context, d *schema.ResourceData, _, _ = tmpfile.Write([]byte(yaml)) _ = tmpfile.Close() + kubeConfigFlags := genericclioptions.NewConfigFlags(true) + f := cmdutil.NewFactory(kubeConfigFlags) + replaceOptions := replace.NewReplaceOptions(genericclioptions.IOStreams{ In: strings.NewReader(yaml), Out: log.Writer(), ErrOut: log.Writer(), }) + + recorder, err := replaceOptions.RecordFlags.ToRecorder() + if err != nil { + return err + } + replaceOptions.Recorder = recorder + + printer, err := replaceOptions.PrintFlags.ToPrinter() + if err != nil { + return err + } + replaceOptions.PrintObj = func(obj runtime.Object) error { + return printer.PrintObj(obj, replaceOptions.Out) + } + replaceOptions.Builder = func() *k8sresource.Builder { - return k8sresource.NewBuilder(k8sresource.RESTClientGetter(meta.(*KubeProvider))) + return f.NewBuilder() } + replaceOptions.DeleteOptions = &k8sdelete.DeleteOptions{ + ForceDeletion: true, + IgnoreNotFound: true, FilenameOptions: k8sresource.FilenameOptions{ Filenames: []string{tmpfile.Name()}, }, @@ -673,12 +696,10 @@ func resourceKubectlManifestReplace(ctx context.Context, d *schema.ResourceData, log.Printf("[INFO] %s perform replace of manifest", manifest) - f := cmdutil.NewFactory(nil) - err = replaceOptions.Run(f) _ = os.Remove(tmpfile.Name()) if err != nil { - return fmt.Errorf("%v failed to run apply: %+v", manifest, err) + return fmt.Errorf("%v failed to run replace: %+v", manifest, err) } log.Printf("[INFO] %v manifest applied, fetch resource from kubernetes", manifest) From 78a8d1722062ee2f8360e059928965a9a490e895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20=20Godoy=20Hern=C3=A1ndez?= Date: Mon, 26 Apr 2021 14:31:36 -0400 Subject: [PATCH 3/4] Rename argument reference `replace` to `force_replace` --- docs/resources/kubectl_manifest.md | 2 +- kubernetes/resource_kubectl_manifest.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/resources/kubectl_manifest.md b/docs/resources/kubectl_manifest.md index 68ecd83c..a1e8c079 100644 --- a/docs/resources/kubectl_manifest.md +++ b/docs/resources/kubectl_manifest.md @@ -44,7 +44,7 @@ YAML * `validate_schema` - Optional. Setting to `false` will mimic `kubectl apply --validate=false` mode. Default `true`. * `wait` - Optional. Set this flag to wait or not for finalized to complete for deleted objects. Default `false`. * `wait_for_rollout` - Optional. Set this flag to wait or not for Deployments and APIService to complete rollout. Default `true`. -* `replace` - Optional. Set this flag to do a `kubectl replace --force` instead of apply. Default `false`. +* `force_replace` - Optional. Set this flag to do a `kubectl replace --force` instead of apply. Default `false`. ## Attribute Reference diff --git a/kubernetes/resource_kubectl_manifest.go b/kubernetes/resource_kubectl_manifest.go index 776e75af..24a8668c 100644 --- a/kubernetes/resource_kubectl_manifest.go +++ b/kubernetes/resource_kubectl_manifest.go @@ -59,7 +59,7 @@ func resourceKubectlManifest() *schema.Resource { if kubectlApplyRetryCount > 0 { retryConfig := backoff.WithMaxRetries(exponentialBackoffConfig, kubectlApplyRetryCount) retryErr := backoff.Retry(func() error { - if d.Get("replace").(bool) { + if d.Get("force_replace").(bool) { err := resourceKubectlManifestReplace(ctx, d, meta) if err != nil { log.Printf("[ERROR] creating manifest failed: %+v", err) @@ -80,7 +80,7 @@ func resourceKubectlManifest() *schema.Resource { return nil } else { - if d.Get("replace").(bool) { + if d.Get("force_replace").(bool) { if replaceErr := resourceKubectlManifestReplace(ctx, d, meta); replaceErr != nil { return diag.FromErr(replaceErr) } @@ -115,7 +115,7 @@ func resourceKubectlManifest() *schema.Resource { if kubectlApplyRetryCount > 0 { retryConfig := backoff.WithMaxRetries(exponentialBackoffConfig, kubectlApplyRetryCount) retryErr := backoff.Retry(func() error { - if d.Get("replace").(bool) { + if d.Get("force_replace").(bool) { err := resourceKubectlManifestReplace(ctx, d, meta) if err != nil { log.Printf("[ERROR] updating manifest failed: %+v", err) @@ -136,7 +136,7 @@ func resourceKubectlManifest() *schema.Resource { return nil } else { - if d.Get("replace").(bool) { + if d.Get("force_replace").(bool) { if applyErr := resourceKubectlManifestReplace(ctx, d, meta); applyErr != nil { return diag.FromErr(applyErr) } @@ -463,9 +463,9 @@ metadata: Optional: true, Default: true, }, - "replace": { + "force_replace": { Type: schema.TypeBool, - Description: "Default to false (replace). Set this flag to do a 'kubectl replace --force' instead of apply.", + Description: "Default to false (force_replace). Set this flag to do a 'kubectl replace --force' instead of apply.", Optional: true, Default: true, }, From b9d09362ed95bc95d4ac131b0a167a03cbaa37e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20=20Godoy=20Hern=C3=A1ndez?= Date: Mon, 26 Apr 2021 14:33:53 -0400 Subject: [PATCH 4/4] Set force_replace to false --- go.mod | 3 +++ kubernetes/resource_kubectl_manifest.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 1d02047c..9a9db402 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.14 require ( github.com/cenkalti/backoff v2.1.1+incompatible + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/hashicorp/go-plugin v1.3.0 github.com/hashicorp/hcl/v2 v2.6.0 github.com/hashicorp/terraform v0.12.29 @@ -14,6 +15,8 @@ require ( github.com/stretchr/testify v1.5.1 github.com/zclconf/go-cty v1.2.1 github.com/zclconf/go-cty-yaml v1.0.1 + golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect + google.golang.org/appengine v1.6.6 // indirect google.golang.org/grpc v1.32.0 gopkg.in/yaml.v2 v2.2.8 k8s.io/api v0.18.12 diff --git a/kubernetes/resource_kubectl_manifest.go b/kubernetes/resource_kubectl_manifest.go index 24a8668c..0db2a3f3 100644 --- a/kubernetes/resource_kubectl_manifest.go +++ b/kubernetes/resource_kubectl_manifest.go @@ -467,7 +467,7 @@ metadata: Type: schema.TypeBool, Description: "Default to false (force_replace). Set this flag to do a 'kubectl replace --force' instead of apply.", Optional: true, - Default: true, + Default: false, }, }, }