Skip to content
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

add recreate option for update-policy directive #189

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions internal/commands/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -294,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
Expand Down
9 changes: 7 additions & 2 deletions internal/commands/directives.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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{}
}
Expand Down
13 changes: 12 additions & 1 deletion internal/commands/directives_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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
Expand Down
62 changes: 53 additions & 9 deletions internal/remote/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -647,6 +649,42 @@ 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
}

waitTime := int64(opts.WaitOptions.Timeout.Seconds())
if waitTime == 0 {
waitTime = int64(2 * time.Minute)
}
watcher, err := ri.Watch(metav1.ListOptions{
TimeoutSeconds: &waitTime,
FieldSelector: "metadata.name=" + obj.GetName(),
})
if err != nil {
Copy link
Contributor

@gotwarlost gotwarlost Oct 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this correct? The delete logic says if err != nil && !apiErrors.IsNotFound(err) which means a non-existent object will pass that gate as it should.

What will happen if you start watching that non-existent object? What does the watcher return?

Also I think the watcher should be used as an optimization. That is, the invariant we are looking for is that "this object doesn't exist before I try to create it". We should make sure that this invariant is true before calling create

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{
Expand Down Expand Up @@ -686,10 +724,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
}
3 changes: 2 additions & 1 deletion site/content/reference/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down