Skip to content

Support kubectl replace --force #93

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/resources/kubectl_manifest.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
* `force_replace` - Optional. Set this flag to do a `kubectl replace --force` instead of apply. Default `false`.

## Attribute Reference

Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
213 changes: 196 additions & 17 deletions kubernetes/resource_kubectl_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -22,6 +23,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"
Expand All @@ -32,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"

Expand All @@ -55,12 +59,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("force_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 {
Expand All @@ -69,8 +80,14 @@ func resourceKubectlManifest() *schema.Resource {

return nil
} else {
if applyErr := resourceKubectlManifestApply(ctx, d, meta); applyErr != nil {
return diag.FromErr(applyErr)
if d.Get("force_replace").(bool) {
if replaceErr := resourceKubectlManifestReplace(ctx, d, meta); replaceErr != nil {
return diag.FromErr(replaceErr)
}
} else {
if applyErr := resourceKubectlManifestApply(ctx, d, meta); applyErr != nil {
return diag.FromErr(applyErr)
}
}

return nil
Expand Down Expand Up @@ -98,11 +115,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("force_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 {
Expand All @@ -111,8 +136,14 @@ func resourceKubectlManifest() *schema.Resource {

return nil
} else {
if applyErr := resourceKubectlManifestApply(ctx, d, meta); applyErr != nil {
return diag.FromErr(applyErr)
if d.Get("force_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
Expand Down Expand Up @@ -432,6 +463,12 @@ metadata:
Optional: true,
Default: true,
},
"force_replace": {
Type: schema.TypeBool,
Description: "Default to false (force_replace). Set this flag to do a 'kubectl replace --force' instead of apply.",
Optional: true,
Default: false,
},
},
}
}
Expand Down Expand Up @@ -580,6 +617,148 @@ 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()

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 f.NewBuilder()
}

replaceOptions.DeleteOptions = &k8sdelete.DeleteOptions{
ForceDeletion: true,
IgnoreNotFound: true,
FilenameOptions: k8sresource.FilenameOptions{
Filenames: []string{tmpfile.Name()},
},
}

if manifest.hasNamespace() {
replaceOptions.Namespace = manifest.unstruct.GetNamespace()
}

log.Printf("[INFO] %s perform replace of manifest", manifest)

err = replaceOptions.Run(f)
_ = os.Remove(tmpfile.Name())
if err != nil {
return fmt.Errorf("%v failed to run replace: %+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)
Expand Down