Skip to content

Commit

Permalink
add support for kapp.k14s.io/create-strategy annotation
Browse files Browse the repository at this point in the history
  • Loading branch information
cppforlife committed Mar 18, 2020
1 parent 5a9ccff commit 3e3bb17
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 6 deletions.
4 changes: 4 additions & 0 deletions docs/apply.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ Related: [ownership label rules](config.md) and [label scoping rules](config.md)

### Controlling apply via resource annotations

- `kapp.k14s.io/create-strategy` annotation controls create behaviour (rarely necessary)

Possible values `` (default), `fallback-on-update`. In some cases creation of a resource may conflict with that resource being created in the cluster by other means (often automated). An example of that is creation of default ServiceAccount by kapp racing with Kubernetes service accounts controller doing the same thing. By specifying `fallback-on-update` value, kapp will catch resource creation conflicts and apply resource as an update.

- `kapp.k14s.io/update-strategy` annotation controls update behaviour

Possible values: `` (default), `fallback-on-replace`, `always-replace`. In some cases entire resources or subset resource fields are immutable which forces kapp users to specify how to apply wanted update.
Expand Down
83 changes: 77 additions & 6 deletions pkg/kapp/clusterapply/add_or_update_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import (
)

const (
createStrategyAnnKey = "kapp.k14s.io/create-strategy"
createStrategyCreateAnnValue = ""
createStrategyFallbackOnUpdateAnnValue = "fallback-on-update"

updateStrategyAnnKey = "kapp.k14s.io/update-strategy"
updateStrategyUpdateAnnValue = ""
updateStrategyFallbackOnReplaceAnnValue = "fallback-on-replace"
Expand All @@ -37,14 +41,41 @@ func (c AddOrUpdateChange) Apply() error {

switch op {
case ctldiff.ChangeOpAdd:
createdRes, err := c.identifiedResources.Create(c.change.NewResource())
if err != nil {
return err
newRes := c.change.NewResource()

strategy, found := newRes.Annotations()[createStrategyAnnKey]
if !found {
strategy = createStrategyCreateAnnValue
}

err = c.recordAppliedResource(createdRes)
if err != nil {
return err
switch strategy {
case createStrategyCreateAnnValue:
createdRes, err := c.identifiedResources.Create(newRes)
if err != nil {
return err
}

err = c.recordAppliedResource(createdRes)
if err != nil {
return err
}

case createStrategyFallbackOnUpdateAnnValue:
createdRes, err := c.identifiedResources.Create(newRes)
if err != nil {
if errors.IsAlreadyExists(err) {
return c.tryToUpdateAfterCreateConflict()
}
return err
}

err = c.recordAppliedResource(createdRes)
if err != nil {
return err
}

default:
return fmt.Errorf("Unknown create strategy: %s", strategy)
}

case ctldiff.ChangeOpUpdate:
Expand Down Expand Up @@ -163,6 +194,46 @@ func (a AddOrUpdateChange) tryToResolveUpdateConflict(origErr error) error {
return fmt.Errorf(errMsgPrefix+"(tried multiple times): %s", origErr)
}

func (a AddOrUpdateChange) tryToUpdateAfterCreateConflict() error {
var lastUpdateErr error

for i := 0; i < 10; i++ {
latestExistingRes, err := a.identifiedResources.Get(a.change.NewResource())
if err != nil {
return err
}

changeSet := a.changeSetFactory.New([]ctlres.Resource{latestExistingRes},
[]ctlres.Resource{a.change.AppliedResource()})

recalcChanges, err := changeSet.Calculate()
if err != nil {
return err
}

if len(recalcChanges) != 1 {
return fmt.Errorf("Expected exactly one change when recalculating conflicting change")
}
if recalcChanges[0].Op() != ctldiff.ChangeOpUpdate {
return fmt.Errorf("Expected recalculated change to be an update")
}

updatedRes, err := a.identifiedResources.Update(recalcChanges[0].NewResource())
if err != nil {
if errors.IsConflict(err) {
lastUpdateErr = err
continue
}
return err
}

return a.recordAppliedResource(updatedRes)
}

return fmt.Errorf("Failed to update (after trying to create) "+
"due to resource conflict (tried multiple times): %s", lastUpdateErr)
}

type SpecificResource interface {
IsDoneApplying() ctlresm.DoneApplyState
}
Expand Down
70 changes: 70 additions & 0 deletions test/e2e/create_fallback_on_update_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package e2e

import (
"strings"
"testing"

uitest "github.com/cppforlife/go-cli-ui/ui/test"
)

func TestCreateFallbackOnUpdate(t *testing.T) {
env := BuildEnv(t)
logger := Logger{}
kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger}

objNs := env.Namespace + "-create-fallback-on-update"
yaml1 := strings.Replace(`
---
apiVersion: v1
kind: Namespace
metadata:
name: __ns__
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: __ns__
annotations:
kapp.k14s.io/create-strategy: fallback-on-update
imagePullSecrets:
- name: pull-secret
`, "__ns__", objNs, -1)

name := "test-create-fallback-on-update"
cleanUp := func() {
kapp.RunWithOpts([]string{"delete", "-a", name}, RunOpts{AllowError: true})
}

cleanUp()
defer cleanUp()

logger.Section("deploy expecting service account creation to fail", func() {
yamlNoCreateStrategy := strings.Replace(yaml1, "create-strategy", "create-strategy.xxx", -1)

_, err := kapp.RunWithOpts([]string{"deploy", "-f", "-", "-a", name},
RunOpts{AllowError: true, StdinReader: strings.NewReader(yamlNoCreateStrategy)})

if !strings.Contains(err.Error(), `serviceaccounts "default" already exists`) {
t.Fatalf("Expected serviceaccount to be already created, but error was: %s", err)
}

cleanUp()
})

logger.Section("deploy with create-strategy annotation", func() {
kapp.RunWithOpts([]string{"deploy", "-f", "-", "-a", name},
RunOpts{StdinReader: strings.NewReader(yaml1)})
})

logger.Section("deploy second time with expected no changes", func() {
out, _ := kapp.RunWithOpts([]string{"deploy", "-f", "-", "-a", name, "--json"},
RunOpts{StdinReader: strings.NewReader(yaml1)})

resp := uitest.JSONUIFromBytes(t, []byte(out))

if len(resp.Tables[0].Rows) != 0 {
t.Fatalf("Expected to see no changes, but did not: '%s'", out)
}
})
}

0 comments on commit 3e3bb17

Please sign in to comment.