mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 17:07:16 +00:00
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:
parent
84bb996d12
commit
22a3b02a2d
13 changed files with 87 additions and 11757 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
3
go.mod
|
|
@ -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
4
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue