feat: Introduces Server-Side Apply as sync option (#9711)

* 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>
This commit is contained in:
Leonardo Luz Almeida 2022-08-05 19:16:35 -04:00 committed by GitHub
parent 84bb996d12
commit 22a3b02a2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 87 additions and 11757 deletions

View file

@ -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
@ -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
@ -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")

View file

@ -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

View file

@ -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.

View file

@ -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 {

View file

@ -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
```

View file

@ -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.

3
go.mod
View file

@ -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

4
go.sum
View file

@ -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=

View file

@ -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)
];

View file

@ -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"
)
@ -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.
@ -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.
@ -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 {
@ -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.
@ -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 {
@ -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 {

View file

@ -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"
)
@ -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
}

View file

@ -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)

File diff suppressed because it is too large Load diff