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

Support for server side apply #392

Open
wants to merge 49 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
fd0f790
Adds e2e test for simple server side apply merge case
redbaron Dec 10, 2021
176ebb5
Embeddable E2E tests
redbaron Dec 13, 2021
20bd0a6
First basic SSA support
redbaron Dec 13, 2021
27e77f8
Fix tests compilation
redbaron Dec 14, 2021
6884b1e
Make linter happy
redbaron Dec 14, 2021
7de82bd
Fix diffing logic so that it never diffs managed fields
redbaron Dec 14, 2021
1f77748
Cosmetic changes to address feedback
redbaron Dec 15, 2021
5ff0cd5
Remove unused field
redbaron Dec 15, 2021
a1b7202
Don't strip history in ChangeFactory.NewChangeSSA because it won't sh…
redbaron Dec 15, 2021
e7b4682
Another fix for NewChangeAgainstLastApplied to prevent history showin…
redbaron Dec 16, 2021
d17a09e
Move dryRun to PatchOpts struct
redbaron Dec 16, 2021
868d362
Make RunEmbedded to fail tests on error
redbaron Dec 16, 2021
6a42593
Adjust tests to reflect that history is not passed to rebasing rules
redbaron Dec 16, 2021
3c0ba71
Add SSA support for diff command. Check CLI flags for conflicts
redbaron Dec 16, 2021
1e81fce
Add copyright and license header
redbaron Dec 16, 2021
df4a333
Run all e2e tests as kapp subprocess. Remove unnecessary error check.
redbaron Dec 16, 2021
307bbe9
Fix diff generation for versioned renamed objects
redbaron Dec 16, 2021
8eb9d07
Support enabling SSA for E2E tests via env var globally
redbaron Dec 17, 2021
c11badb
Fix panic in delete
redbaron Dec 17, 2021
5a5d997
When SSA enabled, dry run object creation to get most accurate diff
redbaron Jan 7, 2022
5b6b056
Enable SSA during tests only if KAPPP_E2E_SSA is set to 1
redbaron Jan 7, 2022
d2a434b
Make IdentifierResources.Patch method to return Resource with strippe…
redbaron Jan 7, 2022
7745d81
Persist history using Patch instead of Update call.
redbaron Jan 7, 2022
9a6b41f
FIXME: Temporarily remove Create dry run, because it wasn't dry run a…
redbaron Jan 7, 2022
8488060
Fixme IdentifierResources.Patch method
redbaron Jan 7, 2022
de9dca5
Fixme Persist history using Patch
redbaron Jan 7, 2022
a135136
Add support for SSA to 'Add*' strategies
redbaron Jan 10, 2022
f203cec
Clear history when creating SSA change
redbaron Jan 10, 2022
977df6f
Drop ChangeSSA as it becomes exactly like ChangeImpl. Use latter inst…
redbaron Jan 10, 2022
c97d804
env.Namespace some of the tests
redbaron Jan 10, 2022
e8bcfcb
Skip diff comparison in template test.
redbaron Jan 10, 2022
1033a23
Carry on generating SSA change even if attempting to make an invalid …
redbaron Jan 10, 2022
632638e
env.Namespace more tests
redbaron Jan 10, 2022
6af080b
Skip conflict rebasing test in SSA mode
redbaron Jan 10, 2022
c01292c
env.Namespace more tests
redbaron Jan 10, 2022
c6d868e
Use JSON merge patch when recording history
redbaron Jan 10, 2022
527553b
Add SSASkip and comments
redbaron Jan 11, 2022
c98601d
Run E2E tests in SSA and non-SSA mode
redbaron Jan 11, 2022
4301a35
Fix nil panic in tests
redbaron Jan 11, 2022
2c4cb43
Remove RunEmbedded leftovers in e2e tests
redbaron Jan 11, 2022
6fd31bd
Use FieldManagerName from CLI args in the AddPlainStrategy
redbaron Jan 11, 2022
c9be433
Add support for ssa-force flag
redbaron Jan 11, 2022
8a952e0
Make linter happy
redbaron Jan 11, 2022
3a9a9fc
Remove unused file
redbaron Jan 11, 2022
389316e
Smallest nits
redbaron Jan 20, 2022
96fb26c
Localize force param value calculation
redbaron Jan 20, 2022
9795527
Duplicate SSAFlags in SSAOpts and move them back to cmd/tools
redbaron Jan 20, 2022
f94e5b3
Remove dead code
redbaron Jan 20, 2022
9c5d0ea
Don't try to resolve update conflict when using SSA
redbaron Jan 20, 2022
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
3 changes: 2 additions & 1 deletion hack/test-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ set -e -x -u
export KAPP_BINARY_PATH="$PWD/kapp"

./hack/test.sh
./hack/test-e2e.sh
KAPP_E2E_SSA=0 ./hack/test-e2e.sh
KAPP_E2E_SSA=1 ./hack/test-e2e.sh

echo ALL SUCCESS
120 changes: 79 additions & 41 deletions pkg/kapp/clusterapply/add_or_update_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
package clusterapply

import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/types"
"time"

ctldiff "github.com/k14s/kapp/pkg/kapp/diff"
ctlres "github.com/k14s/kapp/pkg/kapp/resources"
"github.com/k14s/kapp/pkg/kapp/util"
"k8s.io/apimachinery/pkg/api/errors"
)

Expand All @@ -26,7 +27,10 @@ const (
)

type AddOrUpdateChangeOpts struct {
DefaultUpdateStrategy string
DefaultUpdateStrategy string
ServerSideApply bool
ServerSideForceConflict bool
FieldManagerName string
}

type AddOrUpdateChange struct {
Expand Down Expand Up @@ -131,7 +135,7 @@ func (c AddOrUpdateChange) tryToResolveUpdateConflict(
changeSet := c.changeSetFactory.New([]ctlres.Resource{latestExistingRes},
[]ctlres.Resource{c.change.AppliedResource()})

recalcChanges, err := changeSet.Calculate()
recalcChanges, err := changeSet.Calculate(context.TODO())
if err != nil {
return err
}
Expand Down Expand Up @@ -172,7 +176,7 @@ func (c AddOrUpdateChange) tryToUpdateAfterCreateConflict() error {
changeSet := c.changeSetFactory.New([]ctlres.Resource{latestExistingRes},
[]ctlres.Resource{c.change.AppliedResource()})

recalcChanges, err := changeSet.Calculate()
recalcChanges, err := changeSet.Calculate(context.TODO())
if err != nil {
return err
}
Expand Down Expand Up @@ -218,36 +222,14 @@ func (c AddOrUpdateChange) recordAppliedResource(savedRes ctlres.Resource) error
return fmt.Errorf("Calculating change after the save: %s", err)
}

// first time, try using memory copy
latestResWithHistory := &savedResWithHistory

return util.Retry(time.Second, time.Minute, func() (bool, error) {
// subsequent times try to retrieve latest copy,
// for example, ServiceAccount seems to change immediately
if latestResWithHistory == nil {
res, err := c.identifiedResources.Get(savedRes)
if err != nil {
return false, err
}

resWithHistory := c.changeFactory.NewResourceWithHistory(res)
latestResWithHistory = &resWithHistory
}

// Record last applied change on the latest version of a resource
latestResWithHistoryUpdated, err := latestResWithHistory.RecordLastAppliedResource(applyChange)
if err != nil {
return true, fmt.Errorf("Recording last applied resource: %s", err)
}

_, err = c.identifiedResources.Update(latestResWithHistoryUpdated)
if err != nil {
latestResWithHistory = nil // Get again
return false, fmt.Errorf("Saving record of last applied resource: %s", err)
}
// Record last applied change on the latest version of a resource
recordHistoryPatch, err := savedResWithHistory.RecordLastAppliedResource(applyChange)
if err != nil {
return fmt.Errorf("Recording last applied resource: %s", err)
}

return true, nil
})
_, err = c.identifiedResources.Patch(savedRes, types.MergePatchType, recordHistoryPatch, ctlres.PatchOpts{DryRun: false})
Copy link
Contributor

Choose a reason for hiding this comment

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

i guess this is safe-ish to do because metadata.annotations will always be present because kapp adds identity annotation.

(it's worth noting that "patch" verb is required in permissions in addition to update.)

Copy link
Contributor

Choose a reason for hiding this comment

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

DryRun: false is not necessary

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Even if metadata.annotations is not present MergePatch will create it. It would be a problem if we used JsonPatch.

return err
}

type AddPlainStrategy struct {
Expand All @@ -263,6 +245,26 @@ func (c AddPlainStrategy) Apply() error {
return err
}

// Create is recorded in the metadata.fieldManagers as
// Update operation creating distinct field manager from Apply operation,
// which means that these fields wont be updateable using SSA.
// To fix it, we change operation to be "Apply"
// See https://github.com/kubernetes/kubernetes/issues/107417 for details
if c.aou.opts.ServerSideApply {
createdRes, err = c.aou.identifiedResources.Patch(createdRes, types.JSONPatchType, []byte(`
[
{ "op": "test", "path": "/metadata/managedFields/0/manager", "value": "`+c.aou.opts.FieldManagerName+`" },
{ "op": "replace", "path": "/metadata/managedFields/0/operation", "value": "Apply" }
]
`), ctlres.PatchOpts{DryRun: false})
if err != nil {
// TODO: potentially patch can fail if '"op": "test"' fails, which can happen if another
// controller changes managedFields. We
Copy link
Contributor

Choose a reason for hiding this comment

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

comment got cut off...

return err
}

}

return c.aou.recordAppliedResource(createdRes)
Copy link
Contributor

Choose a reason for hiding this comment

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

there is a gap of time between create succeeding and patch succeeding where other changes may be applied to the resource. recordAppliedResource on the result of patch which means that we would capture such changes as "correlated" to initial submission. im not sure if this matters at all when ssa is enabled, but definitely seems suspicious if we keep it. at the least this deserves a comment, explaining why its ok for ssa.

}

Expand All @@ -275,13 +277,27 @@ func (c AddOrFallbackOnUpdateStrategy) Op() ClusterChangeApplyStrategyOp {
return createStrategyFallbackOnUpdateAnnValue
}

func (c AddOrFallbackOnUpdateStrategy) Apply() error {
createdRes, err := c.aou.identifiedResources.Create(c.newRes)
if err != nil {
if errors.IsAlreadyExists(err) {
return c.aou.tryToUpdateAfterCreateConflict()
func (c AddOrFallbackOnUpdateStrategy) Apply() (err error) {
var createdRes ctlres.Resource
if c.aou.opts.ServerSideApply {
resBytes, err := c.newRes.AsYAMLBytes()
if err != nil {
return err
}

// Apply patch is like upsert, combining create + update, no need to fallback on error
createdRes, err = c.aou.identifiedResources.Patch(c.newRes, types.ApplyPatchType, resBytes, ctlres.PatchOpts{DryRun: false})
if err != nil {
return err
}
} else {
createdRes, err = c.aou.identifiedResources.Create(c.newRes)
if err != nil {
if errors.IsAlreadyExists(err) {
return c.aou.tryToUpdateAfterCreateConflict()
}
return err
}
return err
}

return c.aou.recordAppliedResource(createdRes)
Expand All @@ -295,7 +311,17 @@ type UpdatePlainStrategy struct {
func (c UpdatePlainStrategy) Op() ClusterChangeApplyStrategyOp { return updateStrategyPlainAnnValue }

func (c UpdatePlainStrategy) Apply() error {
updatedRes, err := c.aou.identifiedResources.Update(c.newRes)
var updatedRes ctlres.Resource
var err error

if c.aou.opts.ServerSideApply {
updatedRes, err = ctlres.WithIdentityAnnotation(c.newRes, func(r ctlres.Resource) (ctlres.Resource, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

it seems that we have two types of Patch operations: one where we are working with a resource, and one where we are working with some "minimal/raw" patch. instead of exposing WithIdentityAnnotation outside of IdentifiedResources resources class, lets keep existing Patch method to work with Resource class (instead of []byte) and change it so that it can add annotation on the fly. lets also add PatchRaw which does take []byte. this should result in keeping WithIdentityAnnotation as private.

resBytes, _ := r.AsYAMLBytes()
redbaron marked this conversation as resolved.
Show resolved Hide resolved
return c.aou.identifiedResources.Patch(r, types.ApplyPatchType, resBytes, ctlres.PatchOpts{DryRun: false})
})
} else {
updatedRes, err = c.aou.identifiedResources.Update(c.newRes)
Copy link
Contributor

Choose a reason for hiding this comment

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

should we be tryToResolveUpdateConflict doing when SSA is on?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right. tryToResolveUpdateConflict recalculates Change from a new existing resource. In case of SSA only conflict we can have is when we are conflicting with another fieldManager, no amount of new diffing going to help here.

}
if err != nil {
if errors.IsConflict(err) {
return c.aou.tryToResolveUpdateConflict(err, func(err error) error { return err })
Expand Down Expand Up @@ -323,8 +349,20 @@ func (c UpdateOrFallbackOnReplaceStrategy) Apply() error {
return err
}

updatedRes, err := c.aou.identifiedResources.Update(c.newRes)
var updatedRes ctlres.Resource
var err error

if c.aou.opts.ServerSideApply {
updatedRes, err = ctlres.WithIdentityAnnotation(c.newRes, func(r ctlres.Resource) (ctlres.Resource, error) {
resBytes, _ := r.AsYAMLBytes()
return c.aou.identifiedResources.Patch(r, types.ApplyPatchType, resBytes, ctlres.PatchOpts{DryRun: false})
})
} else {
updatedRes, err = c.aou.identifiedResources.Update(c.newRes)
}

if err != nil {
//TODO: find out if SSA conflicts worth retrying
if errors.IsConflict(err) {
return c.aou.tryToResolveUpdateConflict(err, replaceIfIsInvalidErrFunc)
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/kapp/clusterapply/delete_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,6 @@ func (c DeleteOrphanStrategy) Apply() error {
return err
}

_, err = c.d.identifiedResources.Patch(c.res, types.JSONPatchType, patchJSON)
_, err = c.d.identifiedResources.Patch(c.res, types.JSONPatchType, patchJSON, ctlres.PatchOpts{DryRun: false})
redbaron marked this conversation as resolved.
Show resolved Hide resolved
return err
}
14 changes: 11 additions & 3 deletions pkg/kapp/cmd/app/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
ctlcap "github.com/k14s/kapp/pkg/kapp/clusterapply"
cmdcore "github.com/k14s/kapp/pkg/kapp/cmd/core"
cmdtools "github.com/k14s/kapp/pkg/kapp/cmd/tools"
"github.com/k14s/kapp/pkg/kapp/cmd/tools/ssa"
ctlconf "github.com/k14s/kapp/pkg/kapp/config"
ctldiff "github.com/k14s/kapp/pkg/kapp/diff"
ctldgraph "github.com/k14s/kapp/pkg/kapp/diffgraph"
Expand Down Expand Up @@ -59,7 +60,14 @@ func NewDeleteCmd(o *DeleteOptions, flagsFactory cmdcore.FlagsFactory) *cobra.Co
func (o *DeleteOptions) Run() error {
failingAPIServicesPolicy := o.ResourceTypesFlags.FailingAPIServicePolicy()

app, supportObjs, err := Factory(o.depsFactory, o.AppFlags, o.ResourceTypesFlags, o.logger)
// When orphan strategy used, delete operation issues PATCH command to delete labels,
// which passes field manager name to K8S API.
// This name is not persisted anywhere in managedFields and it's value doesn't really matter, therefore there
// is no need to expose --ssa-field-manager CLI flag in the delete command. Set it to something reasonable.
ssaFlags := ssa.SSAFlags{
FieldManagerName: "kapp-shouldnt-be-seen-anywhere",
}
app, supportObjs, err := Factory(o.depsFactory, o.AppFlags, o.ResourceTypesFlags, o.logger, &ssaFlags)
if err != nil {
return err
}
Expand Down Expand Up @@ -189,10 +197,10 @@ func (o *DeleteOptions) calculateAndPresentChanges(existingResources []ctlres.Re
)

{ // Figure out changes for X existing resources -> 0 new resources
changeFactory := ctldiff.NewChangeFactory(nil, nil)
changeFactory := ctldiff.NewChangeFactory(nil, nil, supportObjs.IdentifiedResources)
changeSetFactory := ctldiff.NewChangeSetFactory(o.DiffFlags.ChangeSetOpts, changeFactory)

changes, err := changeSetFactory.New(existingResources, nil).Calculate()
changes, err := changeSetFactory.New(existingResources, nil).Calculate(nil)
if err != nil {
return ctlcap.ClusterChangeSet{}, nil, changesSummary{}, err
}
Expand Down
33 changes: 26 additions & 7 deletions pkg/kapp/cmd/app/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
package app

import (
"context"
"fmt"
"github.com/k14s/kapp/pkg/kapp/cmd/tools/ssa"
"sort"
"strings"

Expand Down Expand Up @@ -40,18 +42,26 @@ type DeployOptions struct {
DeployFlags DeployFlags
ResourceTypesFlags ResourceTypesFlags
LabelFlags LabelFlags
SSAFlags ssa.SSAFlags
}

func NewDeployOptions(ui ui.UI, depsFactory cmdcore.DepsFactory, logger logger.Logger) *DeployOptions {
return &DeployOptions{ui: ui, depsFactory: depsFactory, logger: logger}
}

const diffFlagsPrefix = "diff"

func NewDeployCmd(o *DeployOptions, flagsFactory cmdcore.FlagsFactory) *cobra.Command {
cmd := &cobra.Command{
Use: "deploy",
Aliases: []string{"d", "dep"},
Short: "Deploy app",
RunE: func(_ *cobra.Command, _ []string) error { return o.Run() },
RunE: func(_ *cobra.Command, _ []string) error {
return o.Run()
},
PreRunE: func(cmd *cobra.Command, _ []string) error {
return o.ValidateAndAdjustFlags(cmd)
},
Annotations: map[string]string{
cmdcore.AppHelpGroup.Key: cmdcore.AppHelpGroup.Value,
},
Expand All @@ -72,20 +82,29 @@ func NewDeployCmd(o *DeployOptions, flagsFactory cmdcore.FlagsFactory) *cobra.Co

o.AppFlags.Set(cmd, flagsFactory)
o.FileFlags.Set(cmd)
o.DiffFlags.SetWithPrefix("diff", cmd)
o.ResourceFilterFlags.Set(cmd)
o.ApplyFlags.SetWithDefaults("", ApplyFlagsDeployDefaults, cmd)
o.DiffFlags.SetWithPrefix(diffFlagsPrefix, cmd)

o.DeployFlags.Set(cmd)
o.ResourceTypesFlags.Set(cmd)
o.LabelFlags.Set(cmd)
o.SSAFlags.Set(cmd)

return cmd
}

func (o *DeployOptions) ValidateAndAdjustFlags(cmd *cobra.Command) error {
AdjustApplyFlags(o.SSAFlags, &o.ApplyFlags)
return cmdtools.AdjustDiffFlags(o.SSAFlags, &o.DiffFlags, diffFlagsPrefix, cmd)
}

func (o *DeployOptions) Run() error {
ctx := context.Background()

failingAPIServicesPolicy := o.ResourceTypesFlags.FailingAPIServicePolicy()

app, supportObjs, err := Factory(o.depsFactory, o.AppFlags, o.ResourceTypesFlags, o.logger)
app, supportObjs, err := Factory(o.depsFactory, o.AppFlags, o.ResourceTypesFlags, o.logger, &o.SSAFlags)
if err != nil {
return err
}
Expand Down Expand Up @@ -140,7 +159,7 @@ func (o *DeployOptions) Run() error {
}

clusterChangeSet, clusterChangesGraph, hasNoChanges, changeSummary, err :=
o.calculateAndPresentChanges(existingResources, newResources, conf, supportObjs)
o.calculateAndPresentChanges(ctx, existingResources, newResources, conf, supportObjs)
if err != nil {
if o.DiffFlags.UI && clusterChangesGraph != nil {
return o.presentDiffUI(clusterChangesGraph)
Expand Down Expand Up @@ -315,19 +334,19 @@ func (o *DeployOptions) existingResources(newResources []ctlres.Resource,
return resourceFilter.Apply(existingResources), o.existingPodResources(existingResources), nil
}

func (o *DeployOptions) calculateAndPresentChanges(existingResources,
func (o *DeployOptions) calculateAndPresentChanges(ctx context.Context, existingResources,
newResources []ctlres.Resource, conf ctlconf.Conf, supportObjs FactorySupportObjs) (
ctlcap.ClusterChangeSet, *ctldgraph.ChangeGraph, bool, string, error) {

var clusterChangeSet ctlcap.ClusterChangeSet

{ // Figure out changes for X existing resources -> X new resources
changeFactory := ctldiff.NewChangeFactory(conf.RebaseMods(), conf.DiffAgainstLastAppliedFieldExclusionMods())
changeFactory := ctldiff.NewChangeFactory(conf.RebaseMods(), conf.DiffAgainstLastAppliedFieldExclusionMods(), supportObjs.IdentifiedResources)
changeSetFactory := ctldiff.NewChangeSetFactory(o.DiffFlags.ChangeSetOpts, changeFactory)

changes, err := ctldiff.NewChangeSetWithVersionedRs(
existingResources, newResources, conf.TemplateRules(),
o.DiffFlags.ChangeSetOpts, changeFactory).Calculate()
o.DiffFlags.ChangeSetOpts, changeFactory).Calculate(ctx)
if err != nil {
return clusterChangeSet, nil, false, "", err
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/kapp/cmd/app/deploy_flag_help_sections.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ var (
Title: "Diff Flags:",
PrefixMatch: "diff",
}
SSAFlagGroup = cobrautil.FlagHelpSection{
Title: "Server side apply Flags:",
redbaron marked this conversation as resolved.
Show resolved Hide resolved
PrefixMatch: "ssa",
}
ApplyFlagGroup = cobrautil.FlagHelpSection{
Title: "Apply Flags:",
PrefixMatch: "apply",
Expand Down Expand Up @@ -58,6 +62,7 @@ func setDeployCmdFlags(cmd *cobra.Command) {
cmd.SetUsageTemplate(cobrautil.FlagHelpSectionsUsageTemplate([]cobrautil.FlagHelpSection{
CommonFlagGroup,
DiffFlagGroup,
SSAFlagGroup,
ApplyFlagGroup,
WaitFlagGroup,
ResourceFilterFlagGroup,
Expand Down
Loading