mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 17:07:16 +00:00
Support an automated sync policy upon detection of OutOfSync status from git (#571)
This commit is contained in:
parent
e29d5b9634
commit
fd510e7933
15 changed files with 1090 additions and 380 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.8.1
|
||||
0.9.0
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
// load the gcp plugin (required to authenticate against GKE clusters).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||
// load the oidc plugin (required to authenticate with OpenID Connect).
|
||||
|
|
@ -23,8 +24,6 @@ import (
|
|||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
)
|
||||
|
||||
|
|
@ -67,27 +66,14 @@ func newCommand() *cobra.Command {
|
|||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
|
||||
// TODO (amatyushentsev): Use config map to store controller configuration
|
||||
controllerConfig := controller.ApplicationControllerConfig{
|
||||
Namespace: namespace,
|
||||
InstanceID: "",
|
||||
}
|
||||
db := db.NewDB(namespace, kubeClient)
|
||||
resyncDuration := time.Duration(appResyncPeriod) * time.Second
|
||||
repoClientset := reposerver.NewRepositoryServerClientset(repoServerAddress)
|
||||
kubectlCmd := kube.KubectlCmd{}
|
||||
appStateManager := controller.NewAppStateManager(db, appClient, repoClientset, namespace, kubectlCmd)
|
||||
|
||||
appController := controller.NewApplicationController(
|
||||
namespace,
|
||||
kubeClient,
|
||||
appClient,
|
||||
repoClientset,
|
||||
db,
|
||||
kubectlCmd,
|
||||
appStateManager,
|
||||
resyncDuration,
|
||||
&controllerConfig)
|
||||
resyncDuration)
|
||||
secretController := controller.NewSecretController(kubeClient, repoClientset, resyncDuration, namespace)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
|
|
|||
|
|
@ -119,6 +119,18 @@ func NewApplicationCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.
|
|||
if len(appOpts.valuesFiles) > 0 {
|
||||
app.Spec.Source.ValuesFiles = appOpts.valuesFiles
|
||||
}
|
||||
switch appOpts.syncPolicy {
|
||||
case "automated":
|
||||
app.Spec.SyncPolicy = &argoappv1.SyncPolicy{
|
||||
Automated: &argoappv1.SyncPolicyAutomated{
|
||||
Prune: appOpts.autoPrune,
|
||||
},
|
||||
}
|
||||
case "none", "":
|
||||
app.Spec.SyncPolicy = nil
|
||||
default:
|
||||
log.Fatalf("Invalid sync-policy: %s", appOpts.syncPolicy)
|
||||
}
|
||||
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
|
||||
defer util.Close(conn)
|
||||
appCreateRequest := application.ApplicationCreateRequest{
|
||||
|
|
@ -182,6 +194,16 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
|||
if len(app.Spec.Source.ValuesFiles) > 0 {
|
||||
fmt.Printf(printOpFmtStr, "Helm Values:", strings.Join(app.Spec.Source.ValuesFiles, ","))
|
||||
}
|
||||
var syncPolicy string
|
||||
if app.Spec.SyncPolicy != nil && app.Spec.SyncPolicy.Automated != nil {
|
||||
syncPolicy = "Automated"
|
||||
if app.Spec.SyncPolicy.Automated.Prune {
|
||||
syncPolicy += " (Prune)"
|
||||
}
|
||||
} else {
|
||||
syncPolicy = "<none>"
|
||||
}
|
||||
fmt.Printf(printOpFmtStr, "Sync Policy:", syncPolicy)
|
||||
|
||||
if len(app.Status.Conditions) > 0 {
|
||||
fmt.Println()
|
||||
|
|
@ -313,6 +335,17 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
|||
app.Spec.Destination.Namespace = appOpts.destNamespace
|
||||
case "project":
|
||||
app.Spec.Project = appOpts.project
|
||||
case "sync-policy":
|
||||
switch appOpts.syncPolicy {
|
||||
case "automated":
|
||||
app.Spec.SyncPolicy = &argoappv1.SyncPolicy{
|
||||
Automated: &argoappv1.SyncPolicyAutomated{},
|
||||
}
|
||||
case "none":
|
||||
app.Spec.SyncPolicy = nil
|
||||
default:
|
||||
log.Fatalf("Invalid sync-policy: %s", appOpts.syncPolicy)
|
||||
}
|
||||
}
|
||||
})
|
||||
if visited == 0 {
|
||||
|
|
@ -320,6 +353,13 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
|||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
if c.Flags().Changed("auto-prune") {
|
||||
if app.Spec.SyncPolicy == nil || app.Spec.SyncPolicy.Automated == nil {
|
||||
log.Fatal("Cannot set --auto-prune: application not configured with automatic sync")
|
||||
}
|
||||
app.Spec.SyncPolicy.Automated.Prune = appOpts.autoPrune
|
||||
}
|
||||
|
||||
setParameterOverrides(app, appOpts.parameters)
|
||||
oldOverrides := app.Spec.Source.ComponentParameterOverrides
|
||||
updatedSpec, err := appIf.UpdateSpec(context.Background(), &application.ApplicationUpdateSpecRequest{
|
||||
|
|
@ -358,6 +398,8 @@ type appOptions struct {
|
|||
parameters []string
|
||||
valuesFiles []string
|
||||
project string
|
||||
syncPolicy string
|
||||
autoPrune bool
|
||||
}
|
||||
|
||||
func addAppFlags(command *cobra.Command, opts *appOptions) {
|
||||
|
|
@ -370,6 +412,8 @@ func addAppFlags(command *cobra.Command, opts *appOptions) {
|
|||
command.Flags().StringArrayVarP(&opts.parameters, "parameter", "p", []string{}, "set a parameter override (e.g. -p guestbook=image=example/guestbook:latest)")
|
||||
command.Flags().StringArrayVar(&opts.valuesFiles, "values", []string{}, "Helm values file(s) to use")
|
||||
command.Flags().StringVar(&opts.project, "project", "", "Application project name")
|
||||
command.Flags().StringVar(&opts.syncPolicy, "sync-policy", "", "Set the sync policy (one of: automated, none)")
|
||||
command.Flags().BoolVar(&opts.autoPrune, "auto-prune", false, "Set automatic pruning when sync is automated")
|
||||
}
|
||||
|
||||
// NewApplicationUnsetCommand returns a new instance of an `argocd app unset` command
|
||||
|
|
|
|||
|
|
@ -14,9 +14,6 @@ import (
|
|||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
|
|
@ -71,30 +68,28 @@ func NewApplicationController(
|
|||
kubeClientset kubernetes.Interface,
|
||||
applicationClientset appclientset.Interface,
|
||||
repoClientset reposerver.Clientset,
|
||||
db db.ArgoDB,
|
||||
kubectl kube.Kubectl,
|
||||
appStateManager AppStateManager,
|
||||
appResyncPeriod time.Duration,
|
||||
config *ApplicationControllerConfig,
|
||||
) *ApplicationController {
|
||||
appRefreshQueue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
|
||||
appOperationQueue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
|
||||
return &ApplicationController{
|
||||
db := db.NewDB(namespace, kubeClientset)
|
||||
kubectlCmd := kube.KubectlCmd{}
|
||||
appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectlCmd)
|
||||
ctrl := ApplicationController{
|
||||
namespace: namespace,
|
||||
kubeClientset: kubeClientset,
|
||||
kubectl: kubectl,
|
||||
kubectl: kubectlCmd,
|
||||
applicationClientset: applicationClientset,
|
||||
repoClientset: repoClientset,
|
||||
appRefreshQueue: appRefreshQueue,
|
||||
appOperationQueue: appOperationQueue,
|
||||
appRefreshQueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
|
||||
appOperationQueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
|
||||
appStateManager: appStateManager,
|
||||
appInformer: newApplicationInformer(applicationClientset, appRefreshQueue, appOperationQueue, appResyncPeriod, config),
|
||||
db: db,
|
||||
statusRefreshTimeout: appResyncPeriod,
|
||||
forceRefreshApps: make(map[string]bool),
|
||||
forceRefreshAppsMutex: &sync.Mutex{},
|
||||
auditLogger: argo.NewAuditLogger(namespace, kubeClientset, "application-controller"),
|
||||
}
|
||||
ctrl.appInformer = ctrl.newApplicationInformer()
|
||||
return &ctrl
|
||||
}
|
||||
|
||||
// Run starts the Application CRD controller.
|
||||
|
|
@ -371,11 +366,12 @@ func (ctrl *ApplicationController) setAppCondition(app *appv1.Application, condi
|
|||
}
|
||||
|
||||
func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Application) {
|
||||
logCtx := log.WithField("application", app.Name)
|
||||
var state *appv1.OperationState
|
||||
// Recover from any unexpected panics and automatically set the status to be failed
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
logCtx.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
state.Phase = appv1.OperationError
|
||||
if rerr, ok := r.(error); ok {
|
||||
state.Message = rerr.Error()
|
||||
|
|
@ -392,20 +388,20 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
|||
// again. To detect this, always retrieve the latest version to ensure it is not stale.
|
||||
freshApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(ctrl.namespace).Get(app.ObjectMeta.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
log.Errorf("Failed to retrieve latest application state: %v", err)
|
||||
logCtx.Errorf("Failed to retrieve latest application state: %v", err)
|
||||
return
|
||||
}
|
||||
if !isOperationInProgress(freshApp) {
|
||||
log.Infof("Skipping operation on stale application state (%s)", app.ObjectMeta.Name)
|
||||
logCtx.Infof("Skipping operation on stale application state")
|
||||
return
|
||||
}
|
||||
app = freshApp
|
||||
state = app.Status.OperationState.DeepCopy()
|
||||
log.Infof("Resuming in-progress operation. app: %s, phase: %s, message: %s", app.ObjectMeta.Name, state.Phase, state.Message)
|
||||
logCtx.Infof("Resuming in-progress operation. phase: %s, message: %s", state.Phase, state.Message)
|
||||
} else {
|
||||
state = &appv1.OperationState{Phase: appv1.OperationRunning, Operation: *app.Operation, StartedAt: metav1.Now()}
|
||||
ctrl.setOperationState(app, state)
|
||||
log.Infof("Initialized new operation. app: %s, operation: %v", app.ObjectMeta.Name, *app.Operation)
|
||||
logCtx.Infof("Initialized new operation: %v", *app.Operation)
|
||||
}
|
||||
ctrl.appStateManager.SyncAppState(app, state)
|
||||
|
||||
|
|
@ -530,6 +526,12 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
|||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{Type: appv1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
}
|
||||
|
||||
syncErrCond := ctrl.autoSync(app, comparisonResult)
|
||||
if syncErrCond != nil {
|
||||
conditions = append(conditions, *syncErrCond)
|
||||
}
|
||||
|
||||
ctrl.updateAppStatus(app, comparisonResult, healthState, parameters, conditions)
|
||||
return
|
||||
}
|
||||
|
|
@ -588,6 +590,7 @@ func (ctrl *ApplicationController) refreshAppConditions(app *appv1.Application)
|
|||
appv1.ApplicationConditionUnknownError: true,
|
||||
appv1.ApplicationConditionComparisonError: true,
|
||||
appv1.ApplicationConditionSharedResourceWarning: true,
|
||||
appv1.ApplicationConditionSyncError: true,
|
||||
}
|
||||
appConditions := make([]appv1.ApplicationCondition, 0)
|
||||
for i := 0; i < len(app.Status.Conditions); i++ {
|
||||
|
|
@ -691,33 +694,67 @@ func (ctrl *ApplicationController) updateAppStatus(
|
|||
}
|
||||
}
|
||||
|
||||
func newApplicationInformer(
|
||||
appClientset appclientset.Interface,
|
||||
appQueue workqueue.RateLimitingInterface,
|
||||
appOperationQueue workqueue.RateLimitingInterface,
|
||||
appResyncPeriod time.Duration,
|
||||
config *ApplicationControllerConfig) cache.SharedIndexInformer {
|
||||
// autoSync will initiate a sync operation for an application configured with automated sync
|
||||
func (ctrl *ApplicationController) autoSync(app *appv1.Application, comparisonResult *appv1.ComparisonResult) *appv1.ApplicationCondition {
|
||||
if app.Spec.SyncPolicy == nil || app.Spec.SyncPolicy.Automated == nil {
|
||||
return nil
|
||||
}
|
||||
logCtx := log.WithFields(log.Fields{"application": app.Name})
|
||||
if app.Operation != nil {
|
||||
logCtx.Infof("Skipping auto-sync: another operation is in progress")
|
||||
return nil
|
||||
}
|
||||
// Only perform auto-sync if we detect OutOfSync status. This is to prevent us from attempting
|
||||
// a sync when application is already in a Synced or Unknown state
|
||||
if comparisonResult.Status != appv1.ComparisonStatusOutOfSync {
|
||||
logCtx.Infof("Skipping auto-sync: application status is %s", comparisonResult.Status)
|
||||
return nil
|
||||
}
|
||||
desiredCommitSHA := comparisonResult.Revision
|
||||
// It is possible for manifests to remain OutOfSync even after a sync/kubectl apply (e.g.
|
||||
// auto-sync with pruning disabled). We need to ensure that we do not keep Syncing an
|
||||
// application in an infinite loop. To detect this, we only attempt the Sync if the revision
|
||||
// and parameter overrides do *not* appear in the application's most recent history.
|
||||
historyLen := len(app.Status.History)
|
||||
if historyLen > 0 {
|
||||
mostRecent := app.Status.History[historyLen-1]
|
||||
if mostRecent.Revision == desiredCommitSHA && reflect.DeepEqual(app.Spec.Source.ComponentParameterOverrides, mostRecent.ComponentParameterOverrides) {
|
||||
logCtx.Infof("Skipping auto-sync: most recent sync already to %s", desiredCommitSHA)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// If a sync failed, the revision will not make it's way into application history. We also need
|
||||
// to check the operationState to see if the last operation was the one we just attempted.
|
||||
if app.Status.OperationState != nil && app.Status.OperationState.SyncResult != nil {
|
||||
if app.Status.OperationState.SyncResult.Revision == desiredCommitSHA {
|
||||
logCtx.Warnf("Skipping auto-sync: failed previous sync attempt to %s", desiredCommitSHA)
|
||||
message := fmt.Sprintf("Failed sync attempt to %s: %s", desiredCommitSHA, app.Status.OperationState.Message)
|
||||
return &appv1.ApplicationCondition{Type: appv1.ApplicationConditionSyncError, Message: message}
|
||||
}
|
||||
}
|
||||
|
||||
appInformerFactory := appinformers.NewFilteredSharedInformerFactory(
|
||||
appClientset,
|
||||
appResyncPeriod,
|
||||
config.Namespace,
|
||||
func(options *metav1.ListOptions) {
|
||||
var instanceIDReq *labels.Requirement
|
||||
var err error
|
||||
if config.InstanceID != "" {
|
||||
instanceIDReq, err = labels.NewRequirement(common.LabelKeyApplicationControllerInstanceID, selection.Equals, []string{config.InstanceID})
|
||||
} else {
|
||||
instanceIDReq, err = labels.NewRequirement(common.LabelKeyApplicationControllerInstanceID, selection.DoesNotExist, nil)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
options.FieldSelector = fields.Everything().String()
|
||||
labelSelector := labels.NewSelector().Add(*instanceIDReq)
|
||||
options.LabelSelector = labelSelector.String()
|
||||
op := appv1.Operation{
|
||||
Sync: &appv1.SyncOperation{
|
||||
Revision: desiredCommitSHA,
|
||||
Prune: app.Spec.SyncPolicy.Automated.Prune,
|
||||
},
|
||||
}
|
||||
appIf := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace)
|
||||
_, err := argo.SetAppOperation(context.Background(), appIf, ctrl.auditLogger, app.Name, &op)
|
||||
if err != nil {
|
||||
logCtx.Errorf("Failed to initiate auto-sync to %s: %v", desiredCommitSHA, err)
|
||||
return &appv1.ApplicationCondition{Type: appv1.ApplicationConditionSyncError, Message: err.Error()}
|
||||
}
|
||||
logCtx.Infof("Initiated auto-sync to %s", desiredCommitSHA)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) newApplicationInformer() cache.SharedIndexInformer {
|
||||
appInformerFactory := appinformers.NewFilteredSharedInformerFactory(
|
||||
ctrl.applicationClientset,
|
||||
ctrl.statusRefreshTimeout,
|
||||
ctrl.namespace,
|
||||
func(options *metav1.ListOptions) {},
|
||||
)
|
||||
informer := appInformerFactory.Argoproj().V1alpha1().Applications().Informer()
|
||||
informer.AddEventHandler(
|
||||
|
|
@ -725,23 +762,32 @@ func newApplicationInformer(
|
|||
AddFunc: func(obj interface{}) {
|
||||
key, err := cache.MetaNamespaceKeyFunc(obj)
|
||||
if err == nil {
|
||||
appQueue.Add(key)
|
||||
appOperationQueue.Add(key)
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
ctrl.appOperationQueue.Add(key)
|
||||
}
|
||||
},
|
||||
UpdateFunc: func(old, new interface{}) {
|
||||
key, err := cache.MetaNamespaceKeyFunc(new)
|
||||
if err == nil {
|
||||
appQueue.Add(key)
|
||||
appOperationQueue.Add(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
oldApp, oldOK := old.(*appv1.Application)
|
||||
newApp, newOK := new.(*appv1.Application)
|
||||
if oldOK && newOK {
|
||||
if toggledAutomatedSync(oldApp, newApp) {
|
||||
log.WithField("application", newApp.Name).Info("Enabled automated sync")
|
||||
ctrl.forceAppRefresh(newApp.Name)
|
||||
}
|
||||
}
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
ctrl.appOperationQueue.Add(key)
|
||||
},
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
// IndexerInformer uses a delta queue, therefore for deletes we have to use this
|
||||
// key function.
|
||||
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
|
||||
if err == nil {
|
||||
appQueue.Add(key)
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -752,3 +798,17 @@ func newApplicationInformer(
|
|||
func isOperationInProgress(app *appv1.Application) bool {
|
||||
return app.Status.OperationState != nil && !app.Status.OperationState.Phase.Completed()
|
||||
}
|
||||
|
||||
// toggledAutomatedSync tests if an app went from auto-sync disabled to enabled.
|
||||
// if it was toggled to be enabled, the informer handler will force a refresh
|
||||
func toggledAutomatedSync(old *appv1.Application, new *appv1.Application) bool {
|
||||
if new.Spec.SyncPolicy == nil || new.Spec.SyncPolicy.Automated == nil {
|
||||
return false
|
||||
}
|
||||
// auto-sync is enabled. check if it was previously disabled
|
||||
if old.Spec.SyncPolicy == nil || old.Spec.SyncPolicy.Automated == nil {
|
||||
return true
|
||||
}
|
||||
// nothing changed
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
141
controller/appcontroller_test.go
Normal file
141
controller/appcontroller_test.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
|
||||
reposerver "github.com/argoproj/argo-cd/reposerver/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func newFakeController(apps ...runtime.Object) *ApplicationController {
|
||||
kubeClientset := fake.NewSimpleClientset()
|
||||
appClientset := appclientset.NewSimpleClientset(apps...)
|
||||
repoClientset := reposerver.Clientset{}
|
||||
return NewApplicationController(
|
||||
"argocd",
|
||||
kubeClientset,
|
||||
appClientset,
|
||||
&repoClientset,
|
||||
time.Minute,
|
||||
)
|
||||
}
|
||||
|
||||
var fakeApp = `
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: my-app
|
||||
namespace: argocd
|
||||
spec:
|
||||
destination:
|
||||
namespace: dummy-namespace
|
||||
server: https://localhost:6443
|
||||
project: default
|
||||
source:
|
||||
path: some/path
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
syncPolicy:
|
||||
automated: {}
|
||||
status:
|
||||
history:
|
||||
- deployedAt: 2018-09-08T09:16:50Z
|
||||
id: 0
|
||||
params: []
|
||||
revision: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
`
|
||||
|
||||
func newFakeApp() *argoappv1.Application {
|
||||
var app argoappv1.Application
|
||||
err := yaml.Unmarshal([]byte(fakeApp), &app)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &app
|
||||
}
|
||||
|
||||
func TestAutoSync(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(app)
|
||||
compRes := argoappv1.ComparisonResult{
|
||||
Status: argoappv1.ComparisonStatusOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &compRes)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications("argocd").Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, app.Operation)
|
||||
assert.NotNil(t, app.Operation.Sync)
|
||||
assert.False(t, app.Operation.Sync.Prune)
|
||||
}
|
||||
|
||||
func TestSkipAutoSync(t *testing.T) {
|
||||
// Verify we skip when we previously synced to it in our most recent history
|
||||
// Set current to 'aaaaa', desired to 'aaaa' and mark system OutOfSync
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(app)
|
||||
compRes := argoappv1.ComparisonResult{
|
||||
Status: argoappv1.ComparisonStatusOutOfSync,
|
||||
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &compRes)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications("argocd").Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
|
||||
// Verify we skip when we are already Synced (even if revision is different)
|
||||
app = newFakeApp()
|
||||
ctrl = newFakeController(app)
|
||||
compRes = argoappv1.ComparisonResult{
|
||||
Status: argoappv1.ComparisonStatusSynced,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond = ctrl.autoSync(app, &compRes)
|
||||
assert.Nil(t, cond)
|
||||
app, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications("argocd").Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
|
||||
// Verify we skip when auto-sync is disabled
|
||||
app = newFakeApp()
|
||||
app.Spec.SyncPolicy = nil
|
||||
ctrl = newFakeController(app)
|
||||
compRes = argoappv1.ComparisonResult{
|
||||
Status: argoappv1.ComparisonStatusOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond = ctrl.autoSync(app, &compRes)
|
||||
assert.Nil(t, cond)
|
||||
app, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications("argocd").Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
|
||||
// Verify we skip when previous sync attempt failed and return error condition
|
||||
// Set current to 'aaaaa', desired to 'bbbbb' and add 'bbbbb' to failure history
|
||||
app = newFakeApp()
|
||||
app.Status.OperationState = &argoappv1.OperationState{
|
||||
Phase: argoappv1.OperationFailed,
|
||||
SyncResult: &argoappv1.SyncOperationResult{
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
},
|
||||
}
|
||||
ctrl = newFakeController(app)
|
||||
compRes = argoappv1.ComparisonResult{
|
||||
Status: argoappv1.ComparisonStatusOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond = ctrl.autoSync(app, &compRes)
|
||||
assert.NotNil(t, cond)
|
||||
app, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications("argocd").Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
|
|
@ -318,6 +318,9 @@ func (s *ksonnetAppStateManager) CompareAppState(app *v1alpha1.Application, revi
|
|||
Resources: resources,
|
||||
Status: comparisonStatus,
|
||||
}
|
||||
if manifestInfo != nil {
|
||||
compResult.Revision = manifestInfo.Revision
|
||||
}
|
||||
return &compResult, manifestInfo, conditions, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -117,6 +117,9 @@ message ApplicationSpec {
|
|||
|
||||
// Project is a application project name. Empty name means that application belongs to 'default' project.
|
||||
optional string project = 3;
|
||||
|
||||
// SyncPolicy controls when a sync will be performed
|
||||
optional SyncPolicy syncPolicy = 4;
|
||||
}
|
||||
|
||||
// ApplicationStatus contains information about application status in target environment.
|
||||
|
|
@ -194,6 +197,8 @@ message ComparisonResult {
|
|||
optional string status = 5;
|
||||
|
||||
repeated ResourceState resources = 6;
|
||||
|
||||
optional string revision = 7;
|
||||
}
|
||||
|
||||
// ComponentParameter contains information about component parameter value
|
||||
|
|
@ -366,7 +371,8 @@ message RollbackOperation {
|
|||
|
||||
// SyncOperation contains sync operation details.
|
||||
message SyncOperation {
|
||||
// Revision is the git revision in which to sync the application to
|
||||
// Revision is the git revision in which to sync the application to.
|
||||
// If omitted, will use the revision specified in app spec.
|
||||
optional string revision = 1;
|
||||
|
||||
// Prune deletes resources that are no longer tracked in git
|
||||
|
|
@ -391,12 +397,24 @@ message SyncOperationResult {
|
|||
repeated HookStatus hooks = 3;
|
||||
}
|
||||
|
||||
// SyncStrategy indicates the
|
||||
// SyncPolicy controls when a sync will be performed in response to updates in git
|
||||
message SyncPolicy {
|
||||
// Automated will keep an application synced to the target revision
|
||||
optional SyncPolicyAutomated automated = 1;
|
||||
}
|
||||
|
||||
// SyncPolicyAutomated controls the behavior of an automated sync
|
||||
message SyncPolicyAutomated {
|
||||
// Prune will prune resources automatically as part of automated sync (default: false)
|
||||
optional bool prune = 1;
|
||||
}
|
||||
|
||||
// SyncStrategy controls the manner in which a sync is performed
|
||||
message SyncStrategy {
|
||||
// Apply wil perform a `kubectl apply` to perform the sync. This is the default strategy
|
||||
// Apply wil perform a `kubectl apply` to perform the sync.
|
||||
optional SyncStrategyApply apply = 1;
|
||||
|
||||
// Hook will submit any referenced resources to perform the sync
|
||||
// Hook will submit any referenced resources to perform the sync. This is the default strategy
|
||||
optional SyncStrategyHook hook = 2;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ import (
|
|||
|
||||
// SyncOperation contains sync operation details.
|
||||
type SyncOperation struct {
|
||||
// Revision is the git revision in which to sync the application to
|
||||
// Revision is the git revision in which to sync the application to.
|
||||
// If omitted, will use the revision specified in app spec.
|
||||
Revision string `json:"revision,omitempty" protobuf:"bytes,1,opt,name=revision"`
|
||||
// Prune deletes resources that are no longer tracked in git
|
||||
Prune bool `json:"prune,omitempty" protobuf:"bytes,2,opt,name=prune"`
|
||||
|
|
@ -78,11 +79,23 @@ type OperationState struct {
|
|||
FinishedAt *metav1.Time `json:"finishedAt" protobuf:"bytes,7,opt,name=finishedAt"`
|
||||
}
|
||||
|
||||
// SyncStrategy indicates the
|
||||
// SyncPolicy controls when a sync will be performed in response to updates in git
|
||||
type SyncPolicy struct {
|
||||
// Automated will keep an application synced to the target revision
|
||||
Automated *SyncPolicyAutomated `json:"automated,omitempty" protobuf:"bytes,1,opt,name=automated"`
|
||||
}
|
||||
|
||||
// SyncPolicyAutomated controls the behavior of an automated sync
|
||||
type SyncPolicyAutomated struct {
|
||||
// Prune will prune resources automatically as part of automated sync (default: false)
|
||||
Prune bool `json:"prune,omitempty" protobuf:"bytes,1,opt,name=prune"`
|
||||
}
|
||||
|
||||
// SyncStrategy controls the manner in which a sync is performed
|
||||
type SyncStrategy struct {
|
||||
// Apply wil perform a `kubectl apply` to perform the sync. This is the default strategy
|
||||
// Apply wil perform a `kubectl apply` to perform the sync.
|
||||
Apply *SyncStrategyApply `json:"apply,omitempty" protobuf:"bytes,1,opt,name=apply"`
|
||||
// Hook will submit any referenced resources to perform the sync
|
||||
// Hook will submit any referenced resources to perform the sync. This is the default strategy
|
||||
Hook *SyncStrategyHook `json:"hook,omitempty" protobuf:"bytes,2,opt,name=hook"`
|
||||
}
|
||||
|
||||
|
|
@ -218,6 +231,8 @@ type ApplicationSpec struct {
|
|||
Destination ApplicationDestination `json:"destination" protobuf:"bytes,2,name=destination"`
|
||||
// Project is a application project name. Empty name means that application belongs to 'default' project.
|
||||
Project string `json:"project" protobuf:"bytes,3,name=project"`
|
||||
// SyncPolicy controls when a sync will be performed
|
||||
SyncPolicy *SyncPolicy `json:"syncPolicy" protobuf:"bytes,4,name=syncPolicy"`
|
||||
}
|
||||
|
||||
// ComponentParameter contains information about component parameter value
|
||||
|
|
@ -283,8 +298,10 @@ const (
|
|||
ApplicationConditionDeletionError = "DeletionError"
|
||||
// ApplicationConditionInvalidSpecError indicates that application source is invalid
|
||||
ApplicationConditionInvalidSpecError = "InvalidSpecError"
|
||||
// ApplicationComparisonError indicates controller failed to compare application state
|
||||
// ApplicationConditionComparisonError indicates controller failed to compare application state
|
||||
ApplicationConditionComparisonError = "ComparisonError"
|
||||
// ApplicationConditionSyncError indicates controller failed to automatically sync the application
|
||||
ApplicationConditionSyncError = "SyncError"
|
||||
// ApplicationConditionUnknownError indicates an unknown controller error
|
||||
ApplicationConditionUnknownError = "UnknownError"
|
||||
// ApplicationConditionSharedResourceWarning indicates that controller detected resources which belongs to more than one application
|
||||
|
|
@ -305,6 +322,7 @@ type ComparisonResult struct {
|
|||
ComparedTo ApplicationSource `json:"comparedTo" protobuf:"bytes,2,opt,name=comparedTo"`
|
||||
Status ComparisonStatus `json:"status" protobuf:"bytes,5,opt,name=status,casttype=ComparisonStatus"`
|
||||
Resources []ResourceState `json:"resources" protobuf:"bytes,6,opt,name=resources"`
|
||||
Revision string `json:"revision" protobuf:"bytes,7,opt,name=revision"`
|
||||
}
|
||||
|
||||
type HealthStatus struct {
|
||||
|
|
|
|||
|
|
@ -235,6 +235,15 @@ func (in *ApplicationSpec) DeepCopyInto(out *ApplicationSpec) {
|
|||
*out = *in
|
||||
in.Source.DeepCopyInto(&out.Source)
|
||||
out.Destination = in.Destination
|
||||
if in.SyncPolicy != nil {
|
||||
in, out := &in.SyncPolicy, &out.SyncPolicy
|
||||
if *in == nil {
|
||||
*out = nil
|
||||
} else {
|
||||
*out = new(SyncPolicy)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -799,6 +808,47 @@ func (in *SyncOperationResult) DeepCopy() *SyncOperationResult {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SyncPolicy) DeepCopyInto(out *SyncPolicy) {
|
||||
*out = *in
|
||||
if in.Automated != nil {
|
||||
in, out := &in.Automated, &out.Automated
|
||||
if *in == nil {
|
||||
*out = nil
|
||||
} else {
|
||||
*out = new(SyncPolicyAutomated)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncPolicy.
|
||||
func (in *SyncPolicy) DeepCopy() *SyncPolicy {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SyncPolicy)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SyncPolicyAutomated) DeepCopyInto(out *SyncPolicyAutomated) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncPolicyAutomated.
|
||||
func (in *SyncPolicyAutomated) DeepCopy() *SyncPolicyAutomated {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SyncPolicyAutomated)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SyncStrategy) DeepCopyInto(out *SyncStrategy) {
|
||||
*out = *in
|
||||
|
|
|
|||
|
|
@ -705,70 +705,51 @@ func (s *Server) getRepo(ctx context.Context, repoURL string) *appv1.Repository
|
|||
|
||||
// Sync syncs an application to its target state
|
||||
func (s *Server) Sync(ctx context.Context, syncReq *ApplicationSyncRequest) (*appv1.Application, error) {
|
||||
a, err := s.appclientset.ArgoprojV1alpha1().Applications(s.ns).Get(*syncReq.Name, metav1.GetOptions{})
|
||||
appIf := s.appclientset.ArgoprojV1alpha1().Applications(s.ns)
|
||||
a, err := appIf.Get(*syncReq.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !s.enf.EnforceClaims(ctx.Value("claims"), "applications", "sync", appRBACName(*a)) {
|
||||
return nil, grpc.ErrPermissionDenied
|
||||
}
|
||||
return s.setAppOperation(ctx, *syncReq.Name, "sync", func(app *appv1.Application) (*appv1.Operation, error) {
|
||||
syncOp := appv1.SyncOperation{
|
||||
if a.Spec.SyncPolicy != nil && a.Spec.SyncPolicy.Automated != nil {
|
||||
if syncReq.Revision != "" && syncReq.Revision != a.Spec.Source.TargetRevision {
|
||||
return nil, status.Errorf(codes.FailedPrecondition, "Cannot sync to %s: auto-sync currently set to %s", syncReq.Revision, a.Spec.Source.TargetRevision)
|
||||
}
|
||||
}
|
||||
|
||||
op := appv1.Operation{
|
||||
Sync: &appv1.SyncOperation{
|
||||
Revision: syncReq.Revision,
|
||||
Prune: syncReq.Prune,
|
||||
DryRun: syncReq.DryRun,
|
||||
SyncStrategy: syncReq.Strategy,
|
||||
}
|
||||
return &appv1.Operation{
|
||||
Sync: &syncOp,
|
||||
}, nil
|
||||
})
|
||||
},
|
||||
}
|
||||
return argo.SetAppOperation(ctx, appIf, s.auditLogger, *syncReq.Name, &op)
|
||||
}
|
||||
|
||||
func (s *Server) Rollback(ctx context.Context, rollbackReq *ApplicationRollbackRequest) (*appv1.Application, error) {
|
||||
a, err := s.appclientset.ArgoprojV1alpha1().Applications(s.ns).Get(*rollbackReq.Name, metav1.GetOptions{})
|
||||
appIf := s.appclientset.ArgoprojV1alpha1().Applications(s.ns)
|
||||
a, err := appIf.Get(*rollbackReq.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !s.enf.EnforceClaims(ctx.Value("claims"), "applications", "rollback", appRBACName(*a)) {
|
||||
return nil, grpc.ErrPermissionDenied
|
||||
}
|
||||
return s.setAppOperation(ctx, *rollbackReq.Name, "rollback", func(app *appv1.Application) (*appv1.Operation, error) {
|
||||
return &appv1.Operation{
|
||||
Rollback: &appv1.RollbackOperation{
|
||||
ID: rollbackReq.ID,
|
||||
Prune: rollbackReq.Prune,
|
||||
DryRun: rollbackReq.DryRun,
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) setAppOperation(ctx context.Context, appName string, operationName string, operationCreator func(app *appv1.Application) (*appv1.Operation, error)) (*appv1.Application, error) {
|
||||
for {
|
||||
a, err := s.Get(ctx, &ApplicationQuery{Name: &appName})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if a.Operation != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "another operation is already in progress")
|
||||
}
|
||||
op, err := operationCreator(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.Operation = op
|
||||
a.Status.OperationState = nil
|
||||
_, err = s.appclientset.ArgoprojV1alpha1().Applications(s.ns).Update(a)
|
||||
if err != nil && apierr.IsConflict(err) {
|
||||
log.Warnf("Failed to set operation for app '%s' due to update conflict. Retrying again...", appName)
|
||||
} else {
|
||||
if err == nil {
|
||||
s.logEvent(a, ctx, argo.EventReasonResourceUpdated, operationName)
|
||||
}
|
||||
return a, err
|
||||
}
|
||||
if a.Spec.SyncPolicy != nil && a.Spec.SyncPolicy.Automated != nil {
|
||||
return nil, status.Errorf(codes.FailedPrecondition, "Rollback cannot be initiated when auto-sync is enabled")
|
||||
}
|
||||
op := appv1.Operation{
|
||||
Rollback: &appv1.RollbackOperation{
|
||||
ID: rollbackReq.ID,
|
||||
Prune: rollbackReq.Prune,
|
||||
DryRun: rollbackReq.DryRun,
|
||||
},
|
||||
}
|
||||
return argo.SetAppOperation(ctx, appIf, s.auditLogger, *rollbackReq.Name, &op)
|
||||
}
|
||||
|
||||
func (s *Server) TerminateOperation(ctx context.Context, termOpReq *OperationTerminateRequest) (*OperationTerminateResponse, error) {
|
||||
|
|
|
|||
|
|
@ -2104,6 +2104,9 @@
|
|||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/v1alpha1ApplicationSource"
|
||||
},
|
||||
"syncPolicy": {
|
||||
"$ref": "#/definitions/v1alpha1SyncPolicy"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -2223,6 +2226,9 @@
|
|||
"$ref": "#/definitions/v1alpha1ResourceState"
|
||||
}
|
||||
},
|
||||
"revision": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
@ -2534,8 +2540,8 @@
|
|||
"title": "Prune deletes resources that are no longer tracked in git"
|
||||
},
|
||||
"revision": {
|
||||
"type": "string",
|
||||
"title": "Revision is the git revision in which to sync the application to"
|
||||
"description": "Revision is the git revision in which to sync the application to.\nIf omitted, will use the revision specified in app spec.",
|
||||
"type": "string"
|
||||
},
|
||||
"syncStrategy": {
|
||||
"$ref": "#/definitions/v1alpha1SyncStrategy"
|
||||
|
|
@ -2566,9 +2572,29 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1SyncPolicy": {
|
||||
"type": "object",
|
||||
"title": "SyncPolicy controls when a sync will be performed in response to updates in git",
|
||||
"properties": {
|
||||
"automated": {
|
||||
"$ref": "#/definitions/v1alpha1SyncPolicyAutomated"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1SyncPolicyAutomated": {
|
||||
"type": "object",
|
||||
"title": "SyncPolicyAutomated controls the behavior of an automated sync",
|
||||
"properties": {
|
||||
"prune": {
|
||||
"type": "boolean",
|
||||
"format": "boolean",
|
||||
"title": "Prune will prune resources automatically as part of automated sync (default: false)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1SyncStrategy": {
|
||||
"type": "object",
|
||||
"title": "SyncStrategy indicates the",
|
||||
"title": "SyncStrategy controls the manner in which a sync is performed",
|
||||
"properties": {
|
||||
"apply": {
|
||||
"$ref": "#/definitions/v1alpha1SyncStrategyApply"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import (
|
|||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -21,9 +23,6 @@ import (
|
|||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/argoproj/argo-cd/cmd/argocd/commands"
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/controller"
|
||||
|
|
@ -41,7 +40,6 @@ import (
|
|||
"github.com/argoproj/argo-cd/util/cache"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/rbac"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
|
@ -58,7 +56,6 @@ type Fixture struct {
|
|||
AppClient appclientset.Interface
|
||||
DB db.ArgoDB
|
||||
Namespace string
|
||||
InstanceID string
|
||||
RepoServerAddress string
|
||||
ApiServerAddress string
|
||||
Enforcer *rbac.Enforcer
|
||||
|
|
@ -278,7 +275,6 @@ func NewFixture() (*Fixture, error) {
|
|||
DB: db,
|
||||
KubeClient: kubeClient,
|
||||
Namespace: namespace,
|
||||
InstanceID: namespace,
|
||||
Enforcer: enforcer,
|
||||
}
|
||||
err = fixture.setup()
|
||||
|
|
@ -288,7 +284,7 @@ func NewFixture() (*Fixture, error) {
|
|||
return fixture, nil
|
||||
}
|
||||
|
||||
// CreateApp creates application with appropriate controller instance id.
|
||||
// CreateApp creates application
|
||||
func (f *Fixture) CreateApp(t *testing.T, application *v1alpha1.Application) *v1alpha1.Application {
|
||||
application = application.DeepCopy()
|
||||
application.Name = fmt.Sprintf("e2e-test-%v", time.Now().Unix())
|
||||
|
|
@ -297,7 +293,6 @@ func (f *Fixture) CreateApp(t *testing.T, application *v1alpha1.Application) *v1
|
|||
labels = make(map[string]string)
|
||||
application.ObjectMeta.Labels = labels
|
||||
}
|
||||
labels[common.LabelKeyApplicationControllerInstanceID] = f.InstanceID
|
||||
|
||||
application.Spec.Source.ComponentParameterOverrides = append(
|
||||
application.Spec.Source.ComponentParameterOverrides,
|
||||
|
|
@ -312,19 +307,12 @@ func (f *Fixture) CreateApp(t *testing.T, application *v1alpha1.Application) *v1
|
|||
|
||||
// createController creates new controller instance
|
||||
func (f *Fixture) createController() *controller.ApplicationController {
|
||||
appStateManager := controller.NewAppStateManager(
|
||||
f.DB, f.AppClient, reposerver.NewRepositoryServerClientset(f.RepoServerAddress), f.Namespace, kube.KubectlCmd{})
|
||||
|
||||
return controller.NewApplicationController(
|
||||
f.Namespace,
|
||||
f.KubeClient,
|
||||
f.AppClient,
|
||||
reposerver.NewRepositoryServerClientset(f.RepoServerAddress),
|
||||
f.DB,
|
||||
kube.KubectlCmd{},
|
||||
appStateManager,
|
||||
10*time.Second,
|
||||
&controller.ApplicationControllerConfig{Namespace: f.Namespace, InstanceID: f.InstanceID})
|
||||
10*time.Second)
|
||||
}
|
||||
|
||||
func (f *Fixture) NewApiClientset() (argocdclient.Client, error) {
|
||||
|
|
@ -380,10 +368,10 @@ func WaitUntil(t *testing.T, condition wait.ConditionFunc) {
|
|||
|
||||
type FakeGitClientFactory struct{}
|
||||
|
||||
func (f *FakeGitClientFactory) NewClient(repoURL, path, username, password, sshPrivateKey string) git.Client {
|
||||
func (f *FakeGitClientFactory) NewClient(repoURL, path, username, password, sshPrivateKey string) (git.Client, error) {
|
||||
return &FakeGitClient{
|
||||
root: path,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FakeGitClient is a test git client implementation which always clone local test repo.
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"k8s.io/api/core/v1"
|
||||
apierr "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
|
|
@ -29,6 +30,7 @@ import (
|
|||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/argoproj/argo-cd/util/session"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -444,3 +446,35 @@ func verifyGenerateManifests(ctx context.Context, repoRes *argoappv1.Repository,
|
|||
}
|
||||
return conditions
|
||||
}
|
||||
|
||||
// SetAppOperation updates an application with the specified operation, retrying conflict errors
|
||||
func SetAppOperation(ctx context.Context, appIf v1alpha1.ApplicationInterface, audit *AuditLogger, appName string, op *argoappv1.Operation) (*argoappv1.Application, error) {
|
||||
for {
|
||||
a, err := appIf.Get(appName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if a.Operation != nil {
|
||||
return nil, status.Errorf(codes.FailedPrecondition, "another operation is already in progress")
|
||||
}
|
||||
a.Operation = op
|
||||
a.Status.OperationState = nil
|
||||
a, err = appIf.Update(a)
|
||||
var action string
|
||||
if op.Sync != nil {
|
||||
action = "sync"
|
||||
} else if op.Rollback != nil {
|
||||
action = "rollback"
|
||||
} else {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "Operation unspecified")
|
||||
}
|
||||
if err == nil {
|
||||
audit.LogAppEvent(a, EventInfo{Reason: EventReasonResourceUpdated, Action: action, Username: session.Username(ctx)}, v1.EventTypeNormal)
|
||||
return a, nil
|
||||
}
|
||||
if !apierr.IsConflict(err) {
|
||||
return nil, err
|
||||
}
|
||||
log.Warnf("Failed to set operation for app '%s' due to update conflict. Retrying again...", appName)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,8 +39,22 @@ func (l *AuditLogger) logEvent(objMeta metav1.ObjectMeta, gvk schema.GroupVersio
|
|||
} else {
|
||||
message = fmt.Sprintf("Unknown user executed action %s", info.Action)
|
||||
}
|
||||
logCtx := log.WithFields(log.Fields{
|
||||
"type": eventType,
|
||||
"action": info.Action,
|
||||
"reason": info.Reason,
|
||||
"username": info.Username,
|
||||
})
|
||||
switch gvk.Kind {
|
||||
case "Application":
|
||||
logCtx = logCtx.WithField("application", objMeta.Name)
|
||||
case "AppProject":
|
||||
logCtx = logCtx.WithField("project", objMeta.Name)
|
||||
default:
|
||||
logCtx = logCtx.WithField("name", objMeta.Name)
|
||||
}
|
||||
t := metav1.Time{Time: time.Now()}
|
||||
_, err := l.kIf.CoreV1().Events(l.ns).Create(&v1.Event{
|
||||
event := v1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("%v.%x", objMeta.Name, t.UnixNano()),
|
||||
},
|
||||
|
|
@ -62,9 +76,12 @@ func (l *AuditLogger) logEvent(objMeta metav1.ObjectMeta, gvk schema.GroupVersio
|
|||
Type: eventType,
|
||||
Action: info.Action,
|
||||
Reason: info.Reason,
|
||||
})
|
||||
}
|
||||
logCtx.Info(message)
|
||||
_, err := l.kIf.CoreV1().Events(l.ns).Create(&event)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to create audit event: %v", err)
|
||||
logCtx.Errorf("Unable to create audit event: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue