Skip to content

Commit

Permalink
feat: Introduces Server-Side Apply as sync option (argoproj#9711)
Browse files Browse the repository at this point in the history
* feat: Introduces Server-Side Apply as sync option

Signed-off-by: Leonardo Luz Almeida <leonardo_almeida@intuit.com>

* add docs

Signed-off-by: Leonardo Luz Almeida <leonardo_almeida@intuit.com>

* Implement the structured-merge diff when ssa is enabled

Signed-off-by: Leonardo Luz Almeida <leonardo_almeida@intuit.com>

* update gitops-engine

Signed-off-by: Leonardo Luz Almeida <leonardo_almeida@intuit.com>

* update gitops-engine

Signed-off-by: Leonardo Luz Almeida <leonardo_almeida@intuit.com>

* go mod tidy

Signed-off-by: Leonardo Luz Almeida <leonardo_almeida@intuit.com>

* Add server-side apply option to the UI

Signed-off-by: Leonardo Luz Almeida <leonardo_almeida@intuit.com>

* update gitops-engine to master

Signed-off-by: Leonardo Luz Almeida <leonardo_almeida@intuit.com>

* fix live default values

Signed-off-by: Leonardo Luz Almeida <leonardo_almeida@intuit.com>
  • Loading branch information
leoluz authored and Ashutosh committed Aug 11, 2022
1 parent e525840 commit 1bd89ba
Show file tree
Hide file tree
Showing 13 changed files with 87 additions and 11,757 deletions.
5 changes: 5 additions & 0 deletions cmd/argocd/commands/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1383,6 +1383,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
strategy string
force bool
replace bool
serverSideApply bool
async bool
retryLimit int64
retryBackoffDuration time.Duration
Expand Down Expand Up @@ -1507,6 +1508,9 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
if replace {
items = append(items, common.SyncOptionReplace)
}
if serverSideApply {
items = append(items, common.SyncOptionServerSideApply)
}

if len(items) == 0 {
// for prevent send even empty array if not need
Expand Down Expand Up @@ -1606,6 +1610,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
command.Flags().StringVar(&strategy, "strategy", "", "Sync strategy (one of: apply|hook)")
command.Flags().BoolVar(&force, "force", false, "Use a force apply")
command.Flags().BoolVar(&replace, "replace", false, "Use a kubectl create/replace instead apply")
command.Flags().BoolVar(&serverSideApply, "server-side", false, "Use server-side apply while syncing the application")
command.Flags().BoolVar(&async, "async", false, "Do not wait for application to sync before continuing")
command.Flags().StringVar(&local, "local", "", "Path to a local directory. When this flag is present no git queries will be made")
command.Flags().StringVar(&localRepoRoot, "local-repo-root", "/", "Path to the repository root. Used together with --local allows setting the repository root")
Expand Down
2 changes: 2 additions & 0 deletions common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ const (
ArgoCDAdminUsername = "admin"
// ArgoCDUserAgentName is the default user-agent name used by the gRPC API client library and grpc-gateway
ArgoCDUserAgentName = "argocd-client"
// ArgoCDSSAManager is the default argocd manager name used by server-side apply syncs
ArgoCDSSAManager = "argocd-controller"
// AuthCookieName is the HTTP cookie name where we store our auth token
AuthCookieName = "argocd.token"
// StateCookieName is the HTTP cookie name that holds temporary nonce tokens for CSRF protection
Expand Down
6 changes: 6 additions & 0 deletions controller/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,12 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionUnknownError, Message: err.Error(), LastTransitionTime: &now})
}
diffConfigBuilder.WithGVKParser(gvkParser)
diffConfigBuilder.WithManager(common.ArgoCDSSAManager)

// enable structured merge diff if application syncs with server-side apply
if app.Spec.SyncPolicy != nil && app.Spec.SyncPolicy.SyncOptions.HasOption("ServerSideApply=true") {
diffConfigBuilder.WithStructuredMergeDiff(true)
}

// it is necessary to ignore the error at this point to avoid creating duplicated
// application conditions as argo.StateDiffs will validate this diffConfig again.
Expand Down
2 changes: 2 additions & 0 deletions controller/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
sync.WithResourceModificationChecker(syncOp.SyncOptions.HasOption("ApplyOutOfSyncOnly=true"), compareResult.diffResultList),
sync.WithPrunePropagationPolicy(&prunePropagationPolicy),
sync.WithReplace(syncOp.SyncOptions.HasOption(common.SyncOptionReplace)),
sync.WithServerSideApply(syncOp.SyncOptions.HasOption(common.SyncOptionServerSideApply)),
sync.WithServerSideApplyManager(cdcommon.ArgoCDSSAManager),
)

if err != nil {
Expand Down
1 change: 1 addition & 0 deletions docs/user-guide/commands/argocd_app_sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ argocd app sync [APPNAME... | -l selector] [flags]
--retry-limit int Max number of allowed sync retries
--revision string Sync to a specific revision. Preserves parameter overrides
-l, --selector string Sync apps that match this label
--server-side Use server-side apply while syncing the application
--strategy string Sync strategy (one of: apply|hook)
--timeout uint Time out after this many seconds
```
Expand Down
27 changes: 27 additions & 0 deletions docs/user-guide/sync-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,33 @@ metadata:
argocd.argoproj.io/sync-options: Replace=true
```

## Server-Side Apply

By default, ArgoCD executes `kubectl apply` operation to apply the configuration stored in Git. This is a client
side operation that relies on `kubectl.kubernetes.io/last-applied-configuration` annotation to store the previous
resource state. In some cases the resource is too big to fit in 262144 bytes allowed annotation size. In this case
server-side apply can be used to avoid this issue as the annotation is not used in this case.

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
syncPolicy:
syncOptions:
- ServerSideApply=true
```

If the `ServerSideApply=true` sync option is set the ArgoCD will use `kubectl apply --server-side` command to apply changes.

This can also be configured at individual resource level.
```yaml
metadata:
annotations:
argocd.argoproj.io/sync-options: ServerSideApply=true
```

Note: [`Replace=true`](#replace-resource-instead-of-applying-changes) takes precedence over `ServerSideApply=true`.

## Fail the sync if a shared resource is found

By default, ArgoCD will apply all manifests found in the git path configured in the Application regardless if the resources defined in the yamls are already applied by another Application. If the `FailOnSharedResource` sync option is set, ArgoCD will fail the sync whenever it finds a resource in the current Application that is already applied in the cluster by another Application.
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ require (
)

replace (
// TODO release gitops-engine and remove the line bellow
github.com/argoproj/gitops-engine => github.com/argoproj/gitops-engine v0.7.1-0.20220803145758-6cde7989d534

// https://github.com/golang/go/issues/33546#issuecomment-519656923
github.com/go-check/check => github.com/go-check/check v0.0.0-20180628173108-788fd7840127

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ github.com/antonmedv/expr v1.8.9/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmH
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/appscode/go v0.0.0-20190808133642-1d4ef1f1c1e0/go.mod h1:iy07dV61Z7QQdCKJCIvUoDL21u6AIceRhZzyleh2ymc=
github.com/argoproj/gitops-engine v0.7.1-0.20220712234257-67ddccd3cc95 h1:6gKRONJktnW7DoH4vJGm7AL3cbwTgVRmk5TZWvHfM90=
github.com/argoproj/gitops-engine v0.7.1-0.20220712234257-67ddccd3cc95/go.mod h1:Ojs8A9Zt6h28nHzVAtlegdm52U2jWWnrk5D46B9C3Tw=
github.com/argoproj/gitops-engine v0.7.1-0.20220803145758-6cde7989d534 h1:O4DzCr5vvhqg89ius4RLutDXjRwXceCHxnDm6GgydE8=
github.com/argoproj/gitops-engine v0.7.1-0.20220803145758-6cde7989d534/go.mod h1:73eQGDBy7/fxdtNuDenMmxIU8hCT5oaeZCYwGd5HgBg=
github.com/argoproj/notifications-engine v0.3.1-0.20220430155844-567361917320 h1:XDjtTfccs4rSOT1n+i1zV9RpxQdKky1b4YBic16E0qY=
github.com/argoproj/notifications-engine v0.3.1-0.20220430155844-567361917320/go.mod h1:R3zlopt+/juYlebQc9Jarn9vBQ2xZruWOWjUNkfGY9M=
github.com/argoproj/pkg v0.11.1-0.20211203175135-36c59d8fafe0 h1:Cfp7rO/HpVxnwlRqJe0jHiBbZ77ZgXhB6HWlYD02Xdc=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const syncOptions: Array<(props: ApplicationSyncOptionProps) => React.ReactNode>
props => booleanOption('PruneLast', 'Prune Last', false, props, false),
props => booleanOption('ApplyOutOfSyncOnly', 'Apply Out of Sync Only', false, props, false),
props => booleanOption('RespectIgnoreDifferences', 'Respect Ignore Differences', false, props, false),
props => booleanOption('ServerSideApply', 'Server-Side Apply', false, props, false),
props => selectOption('PrunePropagationPolicy', 'Prune Propagation Policy', 'foreground', ['foreground', 'background', 'orphan'], props)
];

Expand Down
38 changes: 36 additions & 2 deletions util/argo/diff/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/argoproj/gitops-engine/pkg/diff"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/argoproj/gitops-engine/pkg/utils/kube/scheme"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

Expand Down Expand Up @@ -80,6 +81,20 @@ func (b *DiffConfigBuilder) WithGVKParser(parser *k8smanagedfields.GvkParser) *D
return b
}

// WithStructuredMergeDiff defines if the diff should be calculated using structured
// merge.
func (b *DiffConfigBuilder) WithStructuredMergeDiff(smd bool) *DiffConfigBuilder {
b.diffConfig.structuredMergeDiff = smd
return b
}

// WithManager defines the manager that should be using during structured
// merge diffs.
func (b *DiffConfigBuilder) WithManager(manager string) *DiffConfigBuilder {
b.diffConfig.manager = manager
return b
}

// Build will first validate the current state of the diff config and return the
// DiffConfig implementation if no errors are found. Will return nil and the error
// details otherwise.
Expand Down Expand Up @@ -113,11 +128,18 @@ type DiffConfig interface {
// StateCache is used when retrieving the diff from the cache.
StateCache() *appstatecache.Cache
IgnoreAggregatedRoles() bool
// Logger used during the diff
// Logger used during the diff.
Logger() *logr.Logger
// GVKParser returns a parser able to build a TypedValue used in
// structured merge diffs.
GVKParser() *k8smanagedfields.GvkParser
// StructuredMergeDiff defines if the diff should be calculated using
// structured merge diffs. Will use standard 3-way merge diffs if
// returns false.
StructuredMergeDiff() bool
// Manager returns the manager that should be used by the diff while
// calculating the structured merge diff.
Manager() string
}

// diffConfig defines the configurations used while applying diffs.
Expand All @@ -132,6 +154,8 @@ type diffConfig struct {
ignoreAggregatedRoles bool
logger *logr.Logger
gvkParser *k8smanagedfields.GvkParser
structuredMergeDiff bool
manager string
}

func (c *diffConfig) Ignores() []v1alpha1.ResourceIgnoreDifferences {
Expand Down Expand Up @@ -164,6 +188,12 @@ func (c *diffConfig) Logger() *logr.Logger {
func (c *diffConfig) GVKParser() *k8smanagedfields.GvkParser {
return c.gvkParser
}
func (c *diffConfig) StructuredMergeDiff() bool {
return c.structuredMergeDiff
}
func (c *diffConfig) Manager() string {
return c.manager
}

// Validate will check the current state of this diffConfig and return
// error if it finds any required configuration missing.
Expand Down Expand Up @@ -217,9 +247,13 @@ func StateDiffs(lives, configs []*unstructured.Unstructured, diffConfig DiffConf
if err != nil {
return nil, err
}

diffOpts := []diff.Option{
diff.WithNormalizer(diffNormalizer),
diff.IgnoreAggregatedRoles(diffConfig.IgnoreAggregatedRoles()),
diff.WithStructuredMergeDiff(diffConfig.StructuredMergeDiff()),
diff.WithGVKParser(diffConfig.GVKParser()),
diff.WithManager(diffConfig.Manager()),
}

if diffConfig.Logger() != nil {
Expand Down Expand Up @@ -323,7 +357,7 @@ func preDiffNormalize(lives, targets []*unstructured.Unstructured, diffConfig Di
idc := NewIgnoreDiffConfig(diffConfig.Ignores(), diffConfig.Overrides())
ok, ignoreDiff := idc.HasIgnoreDifference(gvk.Group, gvk.Kind, target.GetName(), target.GetNamespace())
if ok && len(ignoreDiff.ManagedFieldsManagers) > 0 {
pt := managedfields.ResolveParseableType(gvk, diffConfig.GVKParser())
pt := scheme.ResolveParseableType(gvk, diffConfig.GVKParser())
var err error
live, target, err = managedfields.Normalize(live, target, ignoreDiff.ManagedFieldsManagers, pt)
if err != nil {
Expand Down
61 changes: 0 additions & 61 deletions util/argo/managedfields/managed_fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@ package managedfields
import (
"bytes"
"fmt"
"reflect"
"sync"

v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
k8smanagedfields "k8s.io/apimachinery/pkg/util/managedfields"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/typed"
)
Expand Down Expand Up @@ -125,60 +121,3 @@ func trustedManager(curManager string, trustedManagers []string) bool {
}
return false
}

func ResolveParseableType(gvk schema.GroupVersionKind, parser *k8smanagedfields.GvkParser) *typed.ParseableType {
if parser == nil {
return &typed.DeducedParseableType
}
pt := resolverFromStaticParser(gvk, parser)
if pt == nil {
return parser.Type(gvk)
}
return pt
}

func resolverFromStaticParser(gvk schema.GroupVersionKind, parser *k8smanagedfields.GvkParser) *typed.ParseableType {
gvkNameMap := getGvkMap(parser)
name := gvkNameMap[gvk]

p := StaticParser()
if p == nil || name == "" {
return nil
}
pt := p.Type(name)
if pt.IsValid() {
return &pt
}
return nil
}

var gvkMap map[schema.GroupVersionKind]string
var extractOnce sync.Once

func getGvkMap(parser *k8smanagedfields.GvkParser) map[schema.GroupVersionKind]string {
extractOnce.Do(func() {
gvkMap = extractGvkMap(parser)
})
return gvkMap
}

func extractGvkMap(parser *k8smanagedfields.GvkParser) map[schema.GroupVersionKind]string {
results := make(map[schema.GroupVersionKind]string)

value := reflect.ValueOf(parser)
gvkValue := reflect.Indirect(value).FieldByName("gvks")
iter := gvkValue.MapRange()
for iter.Next() {
group := iter.Key().FieldByName("Group").String()
version := iter.Key().FieldByName("Version").String()
kind := iter.Key().FieldByName("Kind").String()
gvk := schema.GroupVersionKind{
Group: group,
Version: version,
Kind: kind,
}
name := iter.Value().String()
results[gvk] = name
}
return results
}
3 changes: 2 additions & 1 deletion util/argo/managedfields/managed_fields_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import (

"github.com/argoproj/argo-cd/v2/util/argo/managedfields"
"github.com/argoproj/argo-cd/v2/util/argo/testdata"
"github.com/argoproj/gitops-engine/pkg/utils/kube/scheme"
)

func TestNormalize(t *testing.T) {

parser := managedfields.StaticParser()
parser := scheme.StaticParser()
t.Run("will remove conflicting fields if managed by trusted managers", func(t *testing.T) {
// given
desiredState := StrToUnstructured(testdata.DesiredDeploymentYaml)
Expand Down

0 comments on commit 1bd89ba

Please sign in to comment.