From 6e91017a206620ea2d882fe01b014b0433b03437 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Mon, 21 Dec 2020 14:38:46 +0100 Subject: [PATCH 1/2] add recreate option for update-policy directive --- internal/commands/apply.go | 1 + internal/commands/directives.go | 9 ++++- internal/commands/directives_test.go | 13 ++++++- internal/remote/client.go | 57 +++++++++++++++++++++++----- site/content/reference/directives.md | 3 +- 5 files changed, 70 insertions(+), 13 deletions(-) diff --git a/internal/commands/apply.go b/internal/commands/apply.go index c39b80e6..c29c7dd8 100644 --- a/internal/commands/apply.go +++ b/internal/commands/apply.go @@ -114,6 +114,7 @@ func doApply(args []string, config applyCommandConfig) error { opts := config.syncOptions opts.DisableUpdateFn = newUpdatePolicy().disableUpdate + opts.RecreateUpdateFn = newUpdatePolicy().recreateUpdate if !opts.DryRun && len(objects) > 0 { msg := fmt.Sprintf("will synchronize %d object(s)", len(objects)) diff --git a/internal/commands/directives.go b/internal/commands/directives.go index 763a351f..1e860aec 100644 --- a/internal/commands/directives.go +++ b/internal/commands/directives.go @@ -26,8 +26,9 @@ import ( ) const ( - policyNever = "never" - policyDefault = "default" + policyNever = "never" + policyRecreate = "recreate" + policyDefault = "default" ) // isSet return true if the annotation name specified as directive is equal to the supplied value. @@ -64,6 +65,10 @@ func (u *updatePolicy) disableUpdate(ob model.K8sMeta) bool { return isSet(ob, model.QbecNames.Directives.UpdatePolicy, policyNever, []string{policyDefault}) } +func (u *updatePolicy) recreateUpdate(ob model.K8sMeta) bool { + return isSet(ob, model.QbecNames.Directives.UpdatePolicy, policyRecreate, []string{policyDefault}) +} + func newUpdatePolicy() *updatePolicy { return &updatePolicy{} } diff --git a/internal/commands/directives_test.go b/internal/commands/directives_test.go index ebf885ea..d2caa486 100644 --- a/internal/commands/directives_test.go +++ b/internal/commands/directives_test.go @@ -81,7 +81,7 @@ func TestDirectivesIsSet(t *testing.T) { } } -func TestDirectivesUpdatePolicy(t *testing.T) { +func TestDirectivesUpdatePolicyNever(t *testing.T) { up := newUpdatePolicy() a := assert.New(t) ret := up.disableUpdate(k8sMetaWithAnnotations("ConfigMap", "foo", "bar", nil)) @@ -92,6 +92,17 @@ func TestDirectivesUpdatePolicy(t *testing.T) { a.True(ret) } +func TestDirectivesUpdatePolicyRecreate(t *testing.T) { + up := newUpdatePolicy() + a := assert.New(t) + ret := up.recreateUpdate(k8sMetaWithAnnotations("ConfigMap", "foo", "bar", nil)) + a.False(ret) + ret = up.recreateUpdate(k8sMetaWithAnnotations("ConfigMap", "foo", "bar", map[string]interface{}{ + "directives.qbec.io/update-policy": "recreate", + })) + a.True(ret) +} + func TestDirectivesDeletePolicy(t *testing.T) { dp := newDeletePolicy(func(gvk schema.GroupVersionKind) (bool, error) { return gvk.Kind == "ConfigMap", nil diff --git a/internal/remote/client.go b/internal/remote/client.go index 2dc8e2a2..caa6fef8 100644 --- a/internal/remote/client.go +++ b/internal/remote/client.go @@ -34,6 +34,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" apiTypes "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" ) @@ -64,11 +65,12 @@ type ConditionFunc func(obj model.K8sMeta) bool // SyncOptions provides the caller with options for the sync operation. type SyncOptions struct { - DryRun bool // do not actually create or update objects, return what would happen - DisableCreate bool // only update objects if they exist, do not create new ones - DisableUpdateFn ConditionFunc // do not update an existing object - WaitOptions TypeWaitOptions // opts for waiting - ShowSecrets bool // show secrets in patches and creations + DryRun bool // do not actually create or update objects, return what would happen + DisableCreate bool // only update objects if they exist, do not create new ones + DisableUpdateFn ConditionFunc // do not update an existing object + RecreateUpdateFn ConditionFunc // recreate existing object on update + WaitOptions TypeWaitOptions // opts for waiting + ShowSecrets bool // show secrets in patches and creations } // DeleteOptions provides the caller with options for the delete operation. @@ -647,6 +649,37 @@ func (c *Client) maybeCreate(obj model.K8sLocalObject, opts SyncOptions) (*updat return result, nil } +func (c *Client) doRecreate(obj model.K8sLocalObject, opts SyncOptions) (*updateResult, error) { + ri, err := c.resourceInterfaceWithDefaultNs(obj.GroupVersionKind(), obj.GetNamespace()) + if err != nil { + return nil, errors.Wrap(err, "get resource interface") + } + + sio.Debugln("delete " + c.DisplayName(obj)) + pp := metav1.DeletePropagationForeground + err = ri.Delete(obj.GetName(), &metav1.DeleteOptions{PropagationPolicy: &pp}) + if err != nil && !apiErrors.IsNotFound(err) { + return nil, err + } + + watcher, err := ri.Watch(metav1.ListOptions{ + FieldSelector: "metadata.name=" + obj.GetName(), + }) + if err != nil { + return nil, err + } + + sio.Debugln("wait " + c.DisplayName(obj)) + for { + ev := <-watcher.ResultChan() + if ev.Type == watch.Deleted { + break + } + } + watcher.Stop() + return c.maybeCreate(obj, opts) +} + func (c *Client) maybeUpdate(obj model.K8sLocalObject, remObj *unstructured.Unstructured, opts SyncOptions) (*updateResult, error) { if opts.DisableUpdateFn(model.NewK8sObject(remObj.Object)) { return &updateResult{ @@ -686,10 +719,16 @@ func (c *Client) maybeUpdate(obj model.K8sLocalObject, remObj *unstructured.Unst } var result *updateResult - if opts.DryRun { - result, err = p.getPatchContents(remObj, obj) - } else { - result, err = p.patch(remObj, obj) + + patch, err := p.getPatchContents(remObj, obj) + if err != nil || opts.DryRun { + return patch, err } + if patch.SkipReason != identicalObjects && opts.RecreateUpdateFn(model.NewK8sObject(remObj.Object)) { + return c.doRecreate(obj, opts) + } + + result, err = p.patch(remObj, obj) + return result, err } diff --git a/site/content/reference/directives.md b/site/content/reference/directives.md index aeafa673..d4e2097d 100644 --- a/site/content/reference/directives.md +++ b/site/content/reference/directives.md @@ -26,10 +26,11 @@ object to remove this annotation will not work. #### `directives.qbec.io/update-policy` * Annotation source: in-cluster object. -* Allowed values: `"default"`, `"never"` +* Allowed values: `"default"`, `"never"`, `"recreate"` * Default value: `"default"` when set to `"never"`, indicates that the specific object should never be updated. +when set to `"recreate"`, indicates that the specific object should be recreated instad of updating. If you want qbec to update this object, you need to remove the annotation from the in-cluster object. Changing the source object to remove this annotation will not work. From c4ceba0b36ce59ce4d0229465a66574236d3f4ff Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Thu, 24 Dec 2020 01:33:22 +0100 Subject: [PATCH 2/2] add timeout awaitng for deleteion --- internal/commands/apply.go | 1 + internal/remote/client.go | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/commands/apply.go b/internal/commands/apply.go index c29c7dd8..046bcc4a 100644 --- a/internal/commands/apply.go +++ b/internal/commands/apply.go @@ -295,6 +295,7 @@ func newApplyCommand(cp configProvider) *cobra.Command { if err != nil { return newUsageError(fmt.Sprintf("invalid wait timeout: %s, %v", waitTime, err)) } + config.syncOptions.WaitOptions.Timeout = config.waitTimeout if config.syncOptions.DryRun { config.wait = false config.waitAll = false diff --git a/internal/remote/client.go b/internal/remote/client.go index caa6fef8..c8066914 100644 --- a/internal/remote/client.go +++ b/internal/remote/client.go @@ -662,8 +662,13 @@ func (c *Client) doRecreate(obj model.K8sLocalObject, opts SyncOptions) (*update return nil, err } + waitTime := int64(opts.WaitOptions.Timeout.Seconds()) + if waitTime == 0 { + waitTime = int64(2 * time.Minute) + } watcher, err := ri.Watch(metav1.ListOptions{ - FieldSelector: "metadata.name=" + obj.GetName(), + TimeoutSeconds: &waitTime, + FieldSelector: "metadata.name=" + obj.GetName(), }) if err != nil { return nil, err