2018-02-20 23:23:37 +00:00
package controller
import (
"context"
2018-11-17 01:10:04 +00:00
"crypto/tls"
2018-03-09 10:01:15 +00:00
"encoding/json"
"fmt"
2018-07-07 07:54:06 +00:00
"reflect"
2018-05-15 07:36:11 +00:00
"runtime/debug"
2018-04-11 19:53:33 +00:00
"sync"
2018-05-05 00:01:57 +00:00
"time"
2018-04-11 19:53:33 +00:00
2018-11-17 01:10:04 +00:00
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/reflection"
"google.golang.org/grpc/status"
2018-11-09 17:58:07 +00:00
2018-11-17 01:10:04 +00:00
"github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus"
2018-03-09 10:01:15 +00:00
log "github.com/sirupsen/logrus"
2018-11-17 01:10:04 +00:00
2018-07-24 15:48:13 +00:00
"k8s.io/api/core/v1"
2018-05-16 23:30:28 +00:00
"k8s.io/apimachinery/pkg/api/errors"
2018-02-26 15:43:35 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2018-03-07 06:05:07 +00:00
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2018-10-05 17:18:12 +00:00
"k8s.io/apimachinery/pkg/runtime/schema"
2018-05-03 22:55:01 +00:00
"k8s.io/apimachinery/pkg/types"
2018-02-20 23:23:37 +00:00
"k8s.io/apimachinery/pkg/util/runtime"
2018-07-12 19:39:46 +00:00
"k8s.io/apimachinery/pkg/util/strategicpatch"
2018-02-20 23:23:37 +00:00
"k8s.io/apimachinery/pkg/util/wait"
2018-04-11 19:53:33 +00:00
"k8s.io/apimachinery/pkg/watch"
2018-02-20 23:23:37 +00:00
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
2018-07-07 07:54:06 +00:00
"github.com/argoproj/argo-cd/common"
2018-11-17 01:10:04 +00:00
"github.com/argoproj/argo-cd/controller/services"
2018-07-07 07:54:06 +00:00
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
appinformers "github.com/argoproj/argo-cd/pkg/client/informers/externalversions"
2018-07-09 17:45:03 +00:00
"github.com/argoproj/argo-cd/reposerver"
"github.com/argoproj/argo-cd/util/argo"
2018-11-17 01:10:04 +00:00
cache_util "github.com/argoproj/argo-cd/util/cache"
2018-07-07 07:54:06 +00:00
"github.com/argoproj/argo-cd/util/db"
2018-11-17 01:10:04 +00:00
grpc_util "github.com/argoproj/argo-cd/util/grpc"
2018-07-09 17:13:56 +00:00
"github.com/argoproj/argo-cd/util/health"
2018-07-07 07:54:06 +00:00
"github.com/argoproj/argo-cd/util/kube"
2018-11-17 01:10:04 +00:00
settings_util "github.com/argoproj/argo-cd/util/settings"
tlsutil "github.com/argoproj/argo-cd/util/tls"
2018-02-20 23:23:37 +00:00
)
2018-04-11 19:53:33 +00:00
const (
2018-05-11 18:50:32 +00:00
watchResourcesRetryTimeout = 10 * time . Second
updateOperationStateTimeout = 1 * time . Second
2018-04-11 19:53:33 +00:00
)
2018-02-20 23:23:37 +00:00
// ApplicationController is the controller for application resources.
type ApplicationController struct {
2018-04-11 19:53:33 +00:00
namespace string
kubeClientset kubernetes . Interface
2018-09-10 17:14:14 +00:00
kubectl kube . Kubectl
2018-04-11 19:53:33 +00:00
applicationClientset appclientset . Interface
2018-07-24 15:48:13 +00:00
auditLogger * argo . AuditLogger
2018-05-11 18:50:32 +00:00
appRefreshQueue workqueue . RateLimitingInterface
appOperationQueue workqueue . RateLimitingInterface
2018-04-11 19:53:33 +00:00
appInformer cache . SharedIndexInformer
2018-05-11 18:50:32 +00:00
appStateManager AppStateManager
2018-04-11 19:53:33 +00:00
statusRefreshTimeout time . Duration
2018-07-09 17:45:03 +00:00
repoClientset reposerver . Clientset
2018-05-14 18:36:08 +00:00
db db . ArgoDB
2018-04-11 19:53:33 +00:00
forceRefreshApps map [ string ] bool
forceRefreshAppsMutex * sync . Mutex
2018-11-17 01:10:04 +00:00
appResources cache_util . Cache
2018-02-26 15:43:35 +00:00
}
type ApplicationControllerConfig struct {
InstanceID string
Namespace string
2018-02-20 23:23:37 +00:00
}
// NewApplicationController creates new instance of ApplicationController.
2018-02-22 18:56:14 +00:00
func NewApplicationController (
2018-04-11 19:53:33 +00:00
namespace string ,
2018-02-22 18:56:14 +00:00
kubeClientset kubernetes . Interface ,
applicationClientset appclientset . Interface ,
2018-07-09 17:45:03 +00:00
repoClientset reposerver . Clientset ,
2018-02-26 15:43:35 +00:00
appResyncPeriod time . Duration ,
2018-03-07 06:05:07 +00:00
) * ApplicationController {
2018-11-09 17:58:07 +00:00
settingsMgr := settings_util . NewSettingsManager ( kubeClientset , namespace )
db := db . NewDB ( namespace , settingsMgr , kubeClientset )
2018-09-11 21:28:53 +00:00
kubectlCmd := kube . KubectlCmd { }
appStateManager := NewAppStateManager ( db , applicationClientset , repoClientset , namespace , kubectlCmd )
ctrl := ApplicationController {
2018-04-11 19:53:33 +00:00
namespace : namespace ,
kubeClientset : kubeClientset ,
2018-09-11 21:28:53 +00:00
kubectl : kubectlCmd ,
2018-04-11 19:53:33 +00:00
applicationClientset : applicationClientset ,
2018-07-09 17:45:03 +00:00
repoClientset : repoClientset ,
2018-09-11 21:28:53 +00:00
appRefreshQueue : workqueue . NewRateLimitingQueue ( workqueue . DefaultControllerRateLimiter ( ) ) ,
appOperationQueue : workqueue . NewRateLimitingQueue ( workqueue . DefaultControllerRateLimiter ( ) ) ,
2018-05-11 18:50:32 +00:00
appStateManager : appStateManager ,
2018-05-14 18:36:08 +00:00
db : db ,
2018-04-11 19:53:33 +00:00
statusRefreshTimeout : appResyncPeriod ,
forceRefreshApps : make ( map [ string ] bool ) ,
forceRefreshAppsMutex : & sync . Mutex { } ,
2018-07-31 18:15:44 +00:00
auditLogger : argo . NewAuditLogger ( namespace , kubeClientset , "application-controller" ) ,
2018-11-17 01:10:04 +00:00
appResources : cache_util . NewInMemoryCache ( 24 * time . Hour ) ,
2018-02-20 23:23:37 +00:00
}
2018-09-11 21:28:53 +00:00
ctrl . appInformer = ctrl . newApplicationInformer ( )
return & ctrl
2018-02-20 23:23:37 +00:00
}
2018-11-17 01:10:04 +00:00
func ( ctrl * ApplicationController ) setAppResources ( appName string , resources [ ] appv1 . ResourceState ) {
err := ctrl . appResources . Set ( & cache_util . Item { Object : resources , Key : appName } )
if err != nil {
log . Warnf ( "Unable to save app resources state in cache: %v" , err )
}
}
func ( ctrl * ApplicationController ) Resources ( ctx context . Context , q * services . ResourcesQuery ) ( * services . ResourcesResponse , error ) {
resources := make ( [ ] appv1 . ResourceState , 0 )
if q . ApplicationName == nil {
return nil , status . Errorf ( codes . InvalidArgument , "application name is not specified" )
}
err := ctrl . appResources . Get ( * q . ApplicationName , & resources )
if err == nil {
items := make ( [ ] * appv1 . ResourceState , 0 )
for i := range resources {
res := resources [ i ]
obj , err := res . TargetObject ( )
if err != nil {
return nil , err
}
if obj == nil {
obj , err = res . LiveObject ( )
if err != nil {
return nil , err
}
}
if obj == nil {
return nil , fmt . Errorf ( "both live and target objects are nil" )
}
gvk := obj . GroupVersionKind ( )
if q . Version != nil && gvk . Version != * q . Version {
continue
}
if q . Group != nil && gvk . Group != * q . Group {
continue
}
if q . Kind != nil && gvk . Kind != * q . Kind {
continue
}
var data map [ string ] interface { }
res . LiveState , data = hideSecretData ( res . LiveState , nil )
res . TargetState , _ = hideSecretData ( res . TargetState , data )
res . ChildLiveResources = hideNodesSecrets ( res . ChildLiveResources )
items = append ( items , & res )
}
return & services . ResourcesResponse { Items : items } , nil
}
return & services . ResourcesResponse { Items : make ( [ ] * appv1 . ResourceState , 0 ) } , nil
}
func toString ( val interface { } ) string {
if val == nil {
return ""
}
return fmt . Sprintf ( "%s" , val )
}
// hideSecretData checks if given object kind is Secret, replaces data keys with stars and returns unchanged data map. The method additionally check if data key if different
// from corresponding key of optional parameter `otherData` and adds extra star to keep information about difference. So if secret data is out of sync user still can see which
// fields are different.
func hideSecretData ( state string , otherData map [ string ] interface { } ) ( string , map [ string ] interface { } ) {
obj , err := appv1 . UnmarshalToUnstructured ( state )
if err == nil {
if obj != nil && obj . GetKind ( ) == kube . SecretKind {
if data , ok , err := unstructured . NestedMap ( obj . Object , "data" ) ; err == nil && ok {
unchangedData := make ( map [ string ] interface { } )
for k , v := range data {
unchangedData [ k ] = v
}
for k := range data {
replacement := "********"
if otherData != nil {
if val , ok := otherData [ k ] ; ok && toString ( val ) != toString ( data [ k ] ) {
replacement = replacement + "*"
}
}
data [ k ] = replacement
}
_ = unstructured . SetNestedMap ( obj . Object , data , "data" )
newState , err := json . Marshal ( obj )
if err == nil {
return string ( newState ) , unchangedData
}
}
}
}
return state , nil
}
func hideNodesSecrets ( nodes [ ] appv1 . ResourceNode ) [ ] appv1 . ResourceNode {
for i := range nodes {
node := nodes [ i ]
node . State , _ = hideSecretData ( node . State , nil )
node . Children = hideNodesSecrets ( node . Children )
nodes [ i ] = node
}
return nodes
}
2018-02-20 23:23:37 +00:00
// Run starts the Application CRD controller.
2018-05-11 18:50:32 +00:00
func ( ctrl * ApplicationController ) Run ( ctx context . Context , statusProcessors int , operationProcessors int ) {
2018-02-20 23:23:37 +00:00
defer runtime . HandleCrash ( )
2018-05-11 18:50:32 +00:00
defer ctrl . appRefreshQueue . ShutDown ( )
2018-02-20 23:23:37 +00:00
go ctrl . appInformer . Run ( ctx . Done ( ) )
if ! cache . WaitForCacheSync ( ctx . Done ( ) , ctrl . appInformer . HasSynced ) {
log . Error ( "Timed out waiting for caches to sync" )
return
}
2018-09-10 15:20:17 +00:00
go ctrl . watchAppsResources ( )
2018-05-11 18:50:32 +00:00
for i := 0 ; i < statusProcessors ; i ++ {
go wait . Until ( func ( ) {
for ctrl . processAppRefreshQueueItem ( ) {
}
} , time . Second , ctx . Done ( ) )
}
for i := 0 ; i < operationProcessors ; i ++ {
go wait . Until ( func ( ) {
for ctrl . processAppOperationQueueItem ( ) {
}
} , time . Second , ctx . Done ( ) )
2018-02-20 23:23:37 +00:00
}
<- ctx . Done ( )
}
2018-11-17 01:10:04 +00:00
func ( ctrl * ApplicationController ) CreateGRPC ( tlsConfCustomizer tlsutil . ConfigCustomizer ) ( * grpc . Server , error ) {
// generate TLS cert
hosts := [ ] string {
"localhost" ,
"application-controller" ,
}
cert , err := tlsutil . GenerateX509KeyPair ( tlsutil . CertOptions {
Hosts : hosts ,
Organization : "Argo CD" ,
IsCA : true ,
} )
if err != nil {
return nil , err
}
tlsConfig := & tls . Config { Certificates : [ ] tls . Certificate { * cert } }
tlsConfCustomizer ( tlsConfig )
logEntry := log . NewEntry ( log . New ( ) )
server := grpc . NewServer (
grpc . Creds ( credentials . NewTLS ( tlsConfig ) ) ,
grpc . StreamInterceptor ( grpc_middleware . ChainStreamServer (
grpc_logrus . StreamServerInterceptor ( logEntry ) ,
grpc_util . PanicLoggerStreamServerInterceptor ( logEntry ) ,
) ) ,
grpc . UnaryInterceptor ( grpc_middleware . ChainUnaryServer (
grpc_logrus . UnaryServerInterceptor ( logEntry ) ,
grpc_util . PanicLoggerUnaryServerInterceptor ( logEntry ) ,
) ) ,
)
services . RegisterApplicationServiceServer ( server , ctrl )
reflection . Register ( server )
return server , nil
}
2018-04-11 19:53:33 +00:00
func ( ctrl * ApplicationController ) forceAppRefresh ( appName string ) {
ctrl . forceRefreshAppsMutex . Lock ( )
defer ctrl . forceRefreshAppsMutex . Unlock ( )
ctrl . forceRefreshApps [ appName ] = true
}
func ( ctrl * ApplicationController ) isRefreshForced ( appName string ) bool {
ctrl . forceRefreshAppsMutex . Lock ( )
defer ctrl . forceRefreshAppsMutex . Unlock ( )
_ , ok := ctrl . forceRefreshApps [ appName ]
if ok {
delete ( ctrl . forceRefreshApps , appName )
}
return ok
}
// watchClusterResources watches for resource changes annotated with application label on specified cluster and schedule corresponding app refresh.
func ( ctrl * ApplicationController ) watchClusterResources ( ctx context . Context , item appv1 . Cluster ) {
2018-09-10 15:20:17 +00:00
retryUntilSucceed ( func ( ) ( err error ) {
defer func ( ) {
if r := recover ( ) ; r != nil {
err = fmt . Errorf ( "Recovered from panic: %v\n" , r )
}
} ( )
config := item . RESTConfig ( )
2018-10-05 17:18:12 +00:00
watchStartTime := time . Now ( )
ch , err := ctrl . kubectl . WatchResources ( ctx , config , "" , func ( gvk schema . GroupVersionKind ) metav1 . ListOptions {
ops := metav1 . ListOptions { }
if ! kube . IsCRDGroupVersionKind ( gvk ) {
ops . LabelSelector = common . LabelApplicationName
}
return ops
} )
2018-04-11 19:53:33 +00:00
if err != nil {
return err
}
for event := range ch {
eventObj := event . Object . ( * unstructured . Unstructured )
2018-10-05 17:18:12 +00:00
if kube . IsCRD ( eventObj ) {
// restart if new CRD has been created after watch started
if event . Type == watch . Added && watchStartTime . Before ( eventObj . GetCreationTimestamp ( ) . Time ) {
return fmt . Errorf ( "Restarting the watch because a new CRD was added." )
} else if event . Type == watch . Deleted {
return fmt . Errorf ( "Restarting the watch because a CRD was deleted." )
}
}
2018-04-11 19:53:33 +00:00
objLabels := eventObj . GetLabels ( )
if objLabels == nil {
objLabels = make ( map [ string ] string )
}
if appName , ok := objLabels [ common . LabelApplicationName ] ; ok {
ctrl . forceAppRefresh ( appName )
2018-05-11 18:50:32 +00:00
ctrl . appRefreshQueue . Add ( ctrl . namespace + "/" + appName )
2018-04-11 19:53:33 +00:00
}
}
return fmt . Errorf ( "resource updates channel has closed" )
2018-09-10 15:20:17 +00:00
} , fmt . Sprintf ( "watch app resources on %s" , item . Server ) , ctx , watchResourcesRetryTimeout )
2018-04-11 19:53:33 +00:00
}
2018-09-10 15:20:17 +00:00
func isClusterHasApps ( apps [ ] interface { } , cluster * appv1 . Cluster ) bool {
for _ , obj := range apps {
if app , ok := obj . ( * appv1 . Application ) ; ok && app . Spec . Destination . Server == cluster . Server {
return true
}
}
return false
}
2018-05-31 20:44:19 +00:00
// WatchAppsResources watches for resource changes annotated with application label on all registered clusters and schedule corresponding app refresh.
2018-04-11 19:53:33 +00:00
func ( ctrl * ApplicationController ) watchAppsResources ( ) {
2018-09-10 15:20:17 +00:00
watchingClusters := make ( map [ string ] struct {
cancel context . CancelFunc
cluster * appv1 . Cluster
} )
2018-04-11 19:53:33 +00:00
retryUntilSucceed ( func ( ) error {
2018-09-10 15:20:17 +00:00
clusterEventCallback := func ( event * db . ClusterEvent ) {
info , ok := watchingClusters [ event . Cluster . Server ]
hasApps := isClusterHasApps ( ctrl . appInformer . GetStore ( ) . List ( ) , event . Cluster )
// cluster resources must be watched only if cluster has at least one app
if ( event . Type == watch . Deleted || ! hasApps ) && ok {
info . cancel ( )
2018-04-11 19:53:33 +00:00
delete ( watchingClusters , event . Cluster . Server )
2018-09-10 15:20:17 +00:00
} else if event . Type != watch . Deleted && ! ok && hasApps {
2018-04-11 19:53:33 +00:00
ctx , cancel := context . WithCancel ( context . Background ( ) )
2018-09-10 15:20:17 +00:00
watchingClusters [ event . Cluster . Server ] = struct {
cancel context . CancelFunc
cluster * appv1 . Cluster
} {
cancel : cancel ,
cluster : event . Cluster ,
}
2018-04-11 19:53:33 +00:00
go ctrl . watchClusterResources ( ctx , * event . Cluster )
}
2018-09-10 15:20:17 +00:00
}
onAppModified := func ( obj interface { } ) {
if app , ok := obj . ( * appv1 . Application ) ; ok {
var cluster * appv1 . Cluster
info , infoOk := watchingClusters [ app . Spec . Destination . Server ]
if infoOk {
cluster = info . cluster
} else {
cluster , _ = ctrl . db . GetCluster ( context . Background ( ) , app . Spec . Destination . Server )
}
if cluster != nil {
// trigger cluster event every time when app created/deleted to either start or stop watching resources
clusterEventCallback ( & db . ClusterEvent { Cluster : cluster , Type : watch . Modified } )
}
}
}
ctrl . appInformer . AddEventHandler ( cache . ResourceEventHandlerFuncs { AddFunc : onAppModified , DeleteFunc : onAppModified } )
return ctrl . db . WatchClusters ( context . Background ( ) , clusterEventCallback )
2018-04-11 19:53:33 +00:00
} , "watch clusters" , context . Background ( ) , watchResourcesRetryTimeout )
<- context . Background ( ) . Done ( )
}
// retryUntilSucceed keep retrying given action with specified timeout until action succeed or specified context is done.
func retryUntilSucceed ( action func ( ) error , desc string , ctx context . Context , timeout time . Duration ) {
ctxCompleted := false
go func ( ) {
select {
case <- ctx . Done ( ) :
ctxCompleted = true
}
} ( )
for {
err := action ( )
if err == nil {
return
}
if err != nil {
if ctxCompleted {
log . Infof ( "Stop retrying %s" , desc )
return
} else {
2018-08-21 20:53:42 +00:00
log . Warnf ( "Failed to %s: %+v, retrying in %v" , desc , err , timeout )
2018-04-11 19:53:33 +00:00
time . Sleep ( timeout )
}
}
}
}
2018-05-16 23:30:28 +00:00
func ( ctrl * ApplicationController ) processAppOperationQueueItem ( ) ( processNext bool ) {
2018-05-11 18:50:32 +00:00
appKey , shutdown := ctrl . appOperationQueue . Get ( )
if shutdown {
2018-05-16 23:30:28 +00:00
processNext = false
return
} else {
processNext = true
2018-05-11 18:50:32 +00:00
}
2018-05-16 23:30:28 +00:00
defer func ( ) {
if r := recover ( ) ; r != nil {
log . Errorf ( "Recovered from panic: %+v\n%s" , r , debug . Stack ( ) )
}
ctrl . appOperationQueue . Done ( appKey )
} ( )
2018-05-11 18:50:32 +00:00
obj , exists , err := ctrl . appInformer . GetIndexer ( ) . GetByKey ( appKey . ( string ) )
if err != nil {
log . Errorf ( "Failed to get application '%s' from informer index: %+v" , appKey , err )
2018-05-16 23:30:28 +00:00
return
2018-05-11 18:50:32 +00:00
}
if ! exists {
// This happens after app was deleted, but the work queue still had an entry for it.
2018-05-16 23:30:28 +00:00
return
2018-05-11 18:50:32 +00:00
}
app , ok := obj . ( * appv1 . Application )
if ! ok {
log . Warnf ( "Key '%s' in index is not an application" , appKey )
2018-05-16 23:30:28 +00:00
return
}
if app . Operation != nil {
ctrl . processRequestedAppOperation ( app )
} else if app . DeletionTimestamp != nil && app . CascadedDeletion ( ) {
ctrl . finalizeApplicationDeletion ( app )
2018-05-11 18:50:32 +00:00
}
2018-05-16 23:30:28 +00:00
return
}
func ( ctrl * ApplicationController ) finalizeApplicationDeletion ( app * appv1 . Application ) {
2018-09-24 15:52:43 +00:00
logCtx := log . WithField ( "application" , app . Name )
logCtx . Infof ( "Deleting resources" )
2018-05-16 23:30:28 +00:00
// Get refreshed application info, since informer app copy might be stale
app , err := ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( app . Namespace ) . Get ( app . Name , metav1 . GetOptions { } )
if err != nil {
if ! errors . IsNotFound ( err ) {
2018-09-24 15:52:43 +00:00
logCtx . Errorf ( "Unable to get refreshed application info prior deleting resources: %v" , err )
2018-05-16 23:30:28 +00:00
}
return
2018-05-15 07:36:11 +00:00
}
2018-05-11 18:50:32 +00:00
2018-05-16 23:30:28 +00:00
clst , err := ctrl . db . GetCluster ( context . Background ( ) , app . Spec . Destination . Server )
if err == nil {
config := clst . RESTConfig ( )
2018-10-29 05:46:13 +00:00
err = kube . DeleteResourcesWithLabel ( config , app . Spec . Destination . Namespace , common . LabelApplicationName , app . Name )
2018-05-16 23:30:28 +00:00
if err == nil {
app . SetCascadedDeletion ( false )
var patch [ ] byte
patch , err = json . Marshal ( map [ string ] interface { } {
"metadata" : map [ string ] interface { } {
"finalizers" : app . Finalizers ,
} ,
} )
if err == nil {
_ , err = ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( app . Namespace ) . Patch ( app . Name , types . MergePatchType , patch )
}
}
}
if err != nil {
ctrl . setAppCondition ( app , appv1 . ApplicationCondition {
Type : appv1 . ApplicationConditionDeletionError ,
Message : err . Error ( ) ,
} )
2018-09-24 15:52:43 +00:00
message := fmt . Sprintf ( "Unable to delete application resources: %v" , err )
ctrl . auditLogger . LogAppEvent ( app , argo . EventInfo { Reason : argo . EventReasonStatusRefreshed , Type : v1 . EventTypeWarning } , message )
2018-05-16 23:30:28 +00:00
} else {
2018-09-24 15:52:43 +00:00
logCtx . Info ( "Successfully deleted resources" )
2018-05-16 23:30:28 +00:00
}
}
func ( ctrl * ApplicationController ) setAppCondition ( app * appv1 . Application , condition appv1 . ApplicationCondition ) {
index := - 1
for i , exiting := range app . Status . Conditions {
if exiting . Type == condition . Type {
index = i
break
}
}
if index > - 1 {
app . Status . Conditions [ index ] = condition
} else {
app . Status . Conditions = append ( app . Status . Conditions , condition )
}
var patch [ ] byte
patch , err := json . Marshal ( map [ string ] interface { } {
"status" : map [ string ] interface { } {
"conditions" : app . Status . Conditions ,
} ,
} )
if err == nil {
_ , err = ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( app . Namespace ) . Patch ( app . Name , types . MergePatchType , patch )
}
if err != nil {
log . Errorf ( "Unable to set application condition: %v" , err )
}
}
func ( ctrl * ApplicationController ) processRequestedAppOperation ( app * appv1 . Application ) {
2018-09-11 21:28:53 +00:00
logCtx := log . WithField ( "application" , app . Name )
2018-07-07 07:54:06 +00:00
var state * appv1 . OperationState
2018-05-15 07:36:11 +00:00
// Recover from any unexpected panics and automatically set the status to be failed
defer func ( ) {
if r := recover ( ) ; r != nil {
2018-09-11 21:28:53 +00:00
logCtx . Errorf ( "Recovered from panic: %+v\n%s" , r , debug . Stack ( ) )
2018-05-15 18:35:10 +00:00
state . Phase = appv1 . OperationError
2018-05-15 07:36:11 +00:00
if rerr , ok := r . ( error ) ; ok {
state . Message = rerr . Error ( )
2018-05-11 18:50:32 +00:00
} else {
2018-05-15 07:36:11 +00:00
state . Message = fmt . Sprintf ( "%v" , r )
2018-05-11 18:50:32 +00:00
}
2018-07-14 00:13:31 +00:00
ctrl . setOperationState ( app , state )
2018-05-15 07:36:11 +00:00
}
} ( )
2018-07-14 00:13:31 +00:00
if isOperationInProgress ( app ) {
// If we get here, we are about process an operation but we notice it is already in progress.
// We need to detect if the app object we pulled off the informer is stale and doesn't
// reflect the fact that the operation is completed. We don't want to perform the operation
// again. To detect this, always retrieve the latest version to ensure it is not stale.
2018-05-15 07:36:11 +00:00
freshApp , err := ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( ctrl . namespace ) . Get ( app . ObjectMeta . Name , metav1 . GetOptions { } )
if err != nil {
2018-09-11 21:28:53 +00:00
logCtx . Errorf ( "Failed to retrieve latest application state: %v" , err )
2018-05-16 23:30:28 +00:00
return
2018-05-15 07:36:11 +00:00
}
2018-07-14 00:13:31 +00:00
if ! isOperationInProgress ( freshApp ) {
2018-09-11 21:28:53 +00:00
logCtx . Infof ( "Skipping operation on stale application state" )
2018-05-16 23:30:28 +00:00
return
2018-05-15 07:36:11 +00:00
}
2018-07-07 07:54:06 +00:00
app = freshApp
state = app . Status . OperationState . DeepCopy ( )
2018-09-11 21:28:53 +00:00
logCtx . Infof ( "Resuming in-progress operation. phase: %s, message: %s" , state . Phase , state . Message )
2018-05-15 07:36:11 +00:00
} else {
2018-07-07 07:54:06 +00:00
state = & appv1 . OperationState { Phase : appv1 . OperationRunning , Operation : * app . Operation , StartedAt : metav1 . Now ( ) }
2018-07-14 00:13:31 +00:00
ctrl . setOperationState ( app , state )
2018-09-11 21:28:53 +00:00
logCtx . Infof ( "Initialized new operation: %v" , * app . Operation )
2018-05-15 07:36:11 +00:00
}
2018-07-07 07:54:06 +00:00
ctrl . appStateManager . SyncAppState ( app , state )
2018-07-14 00:13:31 +00:00
if state . Phase == appv1 . OperationRunning {
// It's possible for an app to be terminated while we were operating on it. We do not want
// to clobber the Terminated state with Running. Get the latest app state to check for this.
freshApp , err := ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( ctrl . namespace ) . Get ( app . ObjectMeta . Name , metav1 . GetOptions { } )
if err == nil {
if freshApp . Status . OperationState != nil && freshApp . Status . OperationState . Phase == appv1 . OperationTerminating {
state . Phase = appv1 . OperationTerminating
state . Message = "operation is terminating"
// after this, we will get requeued to the workqueue, but next time the
// SyncAppState will operate in a Terminating phase, allowing the worker to perform
// cleanup (e.g. delete jobs, workflows, etc...)
}
}
}
ctrl . setOperationState ( app , state )
2018-07-12 19:39:46 +00:00
if state . Phase . Completed ( ) {
// if we just completed an operation, force a refresh so that UI will report up-to-date
// sync/health information
ctrl . forceAppRefresh ( app . ObjectMeta . Name )
}
2018-05-11 18:50:32 +00:00
}
2018-07-14 00:13:31 +00:00
func ( ctrl * ApplicationController ) setOperationState ( app * appv1 . Application , state * appv1 . OperationState ) {
2018-05-11 18:50:32 +00:00
retryUntilSucceed ( func ( ) error {
2018-05-15 07:36:11 +00:00
if state . Phase == "" {
// expose any bugs where we neglect to set phase
panic ( "no phase was set" )
}
2018-07-14 00:13:31 +00:00
if state . Phase . Completed ( ) {
now := metav1 . Now ( )
state . FinishedAt = & now
2018-05-15 07:36:11 +00:00
}
2018-07-14 00:13:31 +00:00
patch := map [ string ] interface { } {
2018-05-11 18:50:32 +00:00
"status" : map [ string ] interface { } {
"operationState" : state ,
} ,
2018-07-14 00:13:31 +00:00
}
if state . Phase . Completed ( ) {
// If operation is completed, clear the operation field to indicate no operation is
// in progress.
patch [ "operation" ] = nil
}
if reflect . DeepEqual ( app . Status . OperationState , state ) {
log . Infof ( "No operation updates necessary to '%s'. Skipping patch" , app . Name )
return nil
}
patchJSON , err := json . Marshal ( patch )
2018-05-15 07:36:11 +00:00
if err != nil {
return err
}
appClient := ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( ctrl . namespace )
2018-07-14 00:13:31 +00:00
_ , err = appClient . Patch ( app . Name , types . MergePatchType , patchJSON )
2018-05-15 07:36:11 +00:00
if err != nil {
return err
2018-05-11 18:50:32 +00:00
}
2018-07-07 07:54:06 +00:00
log . Infof ( "updated '%s' operation (phase: %s)" , app . Name , state . Phase )
2018-09-24 15:52:43 +00:00
if state . Phase . Completed ( ) {
eventInfo := argo . EventInfo { Reason : argo . EventReasonOperationCompleted }
var message string
if state . Phase . Successful ( ) {
eventInfo . Type = v1 . EventTypeNormal
message = "Operation succeeded"
} else {
eventInfo . Type = v1 . EventTypeWarning
message = fmt . Sprintf ( "Operation failed: %v" , state . Message )
}
ctrl . auditLogger . LogAppEvent ( app , eventInfo , message )
}
2018-05-15 07:36:11 +00:00
return nil
2018-05-11 18:50:32 +00:00
} , "Update application operation state" , context . Background ( ) , updateOperationStateTimeout )
}
2018-05-16 23:30:28 +00:00
func ( ctrl * ApplicationController ) processAppRefreshQueueItem ( ) ( processNext bool ) {
2018-05-11 18:50:32 +00:00
appKey , shutdown := ctrl . appRefreshQueue . Get ( )
2018-02-20 23:23:37 +00:00
if shutdown {
2018-05-16 23:30:28 +00:00
processNext = false
return
2018-02-20 23:23:37 +00:00
}
2018-07-12 19:39:46 +00:00
processNext = true
2018-05-16 23:30:28 +00:00
defer func ( ) {
if r := recover ( ) ; r != nil {
log . Errorf ( "Recovered from panic: %+v\n%s" , r , debug . Stack ( ) )
}
ctrl . appRefreshQueue . Done ( appKey )
} ( )
2018-02-22 18:56:14 +00:00
obj , exists , err := ctrl . appInformer . GetIndexer ( ) . GetByKey ( appKey . ( string ) )
if err != nil {
log . Errorf ( "Failed to get application '%s' from informer index: %+v" , appKey , err )
2018-05-16 23:30:28 +00:00
return
2018-02-22 18:56:14 +00:00
}
if ! exists {
// This happens after app was deleted, but the work queue still had an entry for it.
2018-05-16 23:30:28 +00:00
return
2018-02-22 18:56:14 +00:00
}
app , ok := obj . ( * appv1 . Application )
if ! ok {
log . Warnf ( "Key '%s' in index is not an application" , appKey )
2018-05-16 23:30:28 +00:00
return
2018-02-22 18:56:14 +00:00
}
2018-07-12 19:39:46 +00:00
if ! ctrl . needRefreshAppStatus ( app , ctrl . statusRefreshTimeout ) {
2018-07-12 02:12:30 +00:00
return
}
2018-07-10 21:45:18 +00:00
2018-07-12 02:12:30 +00:00
app = app . DeepCopy ( )
conditions , hasErrors := ctrl . refreshAppConditions ( app )
if hasErrors {
2018-07-12 21:01:28 +00:00
comparisonResult := app . Status . ComparisonResult . DeepCopy ( )
comparisonResult . Status = appv1 . ComparisonStatusUnknown
health := app . Status . Health . DeepCopy ( )
health . Status = appv1 . HealthStatusUnknown
ctrl . updateAppStatus ( app , comparisonResult , health , nil , conditions )
2018-07-12 02:12:30 +00:00
return
}
2018-07-10 21:45:18 +00:00
2018-11-17 01:10:04 +00:00
comparisonResult , manifestInfo , resources , compConditions , err := ctrl . appStateManager . CompareAppState ( app , "" , nil )
2018-07-12 02:12:30 +00:00
if err != nil {
conditions = append ( conditions , appv1 . ApplicationCondition { Type : appv1 . ApplicationConditionComparisonError , Message : err . Error ( ) } )
} else {
conditions = append ( conditions , compConditions ... )
}
2018-07-10 21:45:18 +00:00
2018-07-12 19:39:46 +00:00
var parameters [ ] * appv1 . ComponentParameter
2018-07-12 02:12:30 +00:00
if manifestInfo != nil {
2018-07-12 19:39:46 +00:00
parameters = manifestInfo . Params
2018-07-12 02:12:30 +00:00
}
2018-11-17 01:10:04 +00:00
healthState , err := setApplicationHealth ( ctrl . kubectl , comparisonResult , resources )
2018-07-12 02:12:30 +00:00
if err != nil {
conditions = append ( conditions , appv1 . ApplicationCondition { Type : appv1 . ApplicationConditionComparisonError , Message : err . Error ( ) } )
2018-02-22 18:56:14 +00:00
}
2018-11-17 01:10:04 +00:00
ctrl . setAppResources ( app . Name , resources )
2018-09-11 21:28:53 +00:00
syncErrCond := ctrl . autoSync ( app , comparisonResult )
if syncErrCond != nil {
conditions = append ( conditions , * syncErrCond )
}
2018-07-12 19:39:46 +00:00
ctrl . updateAppStatus ( app , comparisonResult , healthState , parameters , conditions )
2018-05-16 23:30:28 +00:00
return
2018-02-20 23:23:37 +00:00
}
2018-07-12 19:39:46 +00:00
// needRefreshAppStatus answers if application status needs to be refreshed.
// Returns true if application never been compared, has changed or comparison result has expired.
func ( ctrl * ApplicationController ) needRefreshAppStatus ( app * appv1 . Application , statusRefreshTimeout time . Duration ) bool {
2018-09-24 15:52:43 +00:00
logCtx := log . WithFields ( log . Fields { "application" : app . Name } )
2018-07-12 19:39:46 +00:00
var reason string
2018-09-10 17:58:13 +00:00
expired := app . Status . ComparisonResult . ComparedAt . Add ( statusRefreshTimeout ) . Before ( time . Now ( ) . UTC ( ) )
2018-07-12 19:39:46 +00:00
if ctrl . isRefreshForced ( app . Name ) {
reason = "force refresh"
2018-09-10 17:58:13 +00:00
} else if app . Status . ComparisonResult . Status == appv1 . ComparisonStatusUnknown && expired {
2018-07-12 19:39:46 +00:00
reason = "comparison status unknown"
} else if ! app . Spec . Source . Equals ( app . Status . ComparisonResult . ComparedTo ) {
reason = "spec.source differs"
2018-09-10 17:58:13 +00:00
} else if expired {
2018-07-12 19:39:46 +00:00
reason = fmt . Sprintf ( "comparison expired. comparedAt: %v, expiry: %v" , app . Status . ComparisonResult . ComparedAt , statusRefreshTimeout )
}
if reason != "" {
2018-09-24 15:52:43 +00:00
logCtx . Infof ( "Refreshing app status (%s)" , reason )
2018-07-12 19:39:46 +00:00
return true
}
return false
}
2018-07-09 17:45:03 +00:00
func ( ctrl * ApplicationController ) refreshAppConditions ( app * appv1 . Application ) ( [ ] appv1 . ApplicationCondition , bool ) {
2018-07-10 21:45:18 +00:00
conditions := make ( [ ] appv1 . ApplicationCondition , 0 )
2018-07-09 17:45:03 +00:00
proj , err := argo . GetAppProject ( & app . Spec , ctrl . applicationClientset , ctrl . namespace )
if err != nil {
2018-07-10 21:45:18 +00:00
if errors . IsNotFound ( err ) {
conditions = append ( conditions , appv1 . ApplicationCondition {
Type : appv1 . ApplicationConditionInvalidSpecError ,
Message : fmt . Sprintf ( "Application referencing project %s which does not exist" , app . Spec . Project ) ,
} )
} else {
conditions = append ( conditions , appv1 . ApplicationCondition {
Type : appv1 . ApplicationConditionUnknownError ,
Message : err . Error ( ) ,
} )
}
2018-07-09 17:45:03 +00:00
} else {
2018-07-10 21:45:18 +00:00
specConditions , err := argo . GetSpecErrors ( context . Background ( ) , & app . Spec , proj , ctrl . repoClientset , ctrl . db )
2018-07-09 17:45:03 +00:00
if err != nil {
2018-07-10 21:45:18 +00:00
conditions = append ( conditions , appv1 . ApplicationCondition {
Type : appv1 . ApplicationConditionUnknownError ,
Message : err . Error ( ) ,
} )
2018-07-09 17:45:03 +00:00
} else {
2018-07-10 21:45:18 +00:00
conditions = append ( conditions , specConditions ... )
2018-07-09 17:45:03 +00:00
}
}
2018-07-10 21:45:18 +00:00
// List of condition types which have to be reevaluated by controller; all remaining conditions should stay as is.
reevaluateTypes := map [ appv1 . ApplicationConditionType ] bool {
2018-07-11 20:00:48 +00:00
appv1 . ApplicationConditionInvalidSpecError : true ,
appv1 . ApplicationConditionUnknownError : true ,
appv1 . ApplicationConditionComparisonError : true ,
appv1 . ApplicationConditionSharedResourceWarning : true ,
2018-09-11 21:28:53 +00:00
appv1 . ApplicationConditionSyncError : true ,
2018-07-10 21:45:18 +00:00
}
appConditions := make ( [ ] appv1 . ApplicationCondition , 0 )
2018-07-09 17:45:03 +00:00
for i := 0 ; i < len ( app . Status . Conditions ) ; i ++ {
condition := app . Status . Conditions [ i ]
2018-07-10 21:45:18 +00:00
if _ , ok := reevaluateTypes [ condition . Type ] ; ! ok {
appConditions = append ( appConditions , condition )
2018-07-09 17:45:03 +00:00
}
}
2018-07-10 21:45:18 +00:00
hasErrors := false
for i := range conditions {
condition := conditions [ i ]
appConditions = append ( appConditions , condition )
if condition . IsError ( ) {
hasErrors = true
2018-07-09 17:45:03 +00:00
}
2018-05-03 22:55:01 +00:00
2018-05-07 15:38:25 +00:00
}
2018-07-10 21:45:18 +00:00
return appConditions , hasErrors
2018-05-07 15:38:25 +00:00
}
2018-07-09 17:13:56 +00:00
// setApplicationHealth updates the health statuses of all resources performed in the comparison
2018-11-17 01:10:04 +00:00
func setApplicationHealth ( kubectl kube . Kubectl , comparisonResult * appv1 . ComparisonResult , resources [ ] appv1 . ResourceState ) ( * appv1 . HealthStatus , error ) {
2018-07-23 18:32:35 +00:00
var savedErr error
2018-07-09 17:13:56 +00:00
appHealth := appv1 . HealthStatus { Status : appv1 . HealthStatusHealthy }
2018-07-12 19:40:21 +00:00
if comparisonResult . Status == appv1 . ComparisonStatusUnknown {
appHealth . Status = appv1 . HealthStatusUnknown
}
2018-11-17 01:10:04 +00:00
for i , resource := range resources {
2018-07-09 17:13:56 +00:00
if resource . LiveState == "null" {
resource . Health = appv1 . HealthStatus { Status : appv1 . HealthStatusMissing }
} else {
var obj unstructured . Unstructured
err := json . Unmarshal ( [ ] byte ( resource . LiveState ) , & obj )
if err != nil {
return nil , err
}
2018-09-10 17:14:14 +00:00
healthState , err := health . GetAppHealth ( kubectl , & obj )
2018-07-23 18:32:35 +00:00
if err != nil && savedErr == nil {
savedErr = err
2018-07-09 17:13:56 +00:00
}
resource . Health = * healthState
}
2018-11-17 01:10:04 +00:00
resources [ i ] = resource
comparisonResult . Resources [ i ] . Health = resource . Health
2018-07-09 17:13:56 +00:00
if health . IsWorse ( appHealth . Status , resource . Health . Status ) {
appHealth . Status = resource . Health . Status
}
}
2018-07-23 18:32:35 +00:00
return & appHealth , savedErr
2018-07-09 17:13:56 +00:00
}
2018-07-12 19:39:46 +00:00
// updateAppStatus persists updates to application status. Detects if there patch
2018-05-07 15:38:25 +00:00
func ( ctrl * ApplicationController ) updateAppStatus (
2018-07-12 19:39:46 +00:00
app * appv1 . Application ,
comparisonResult * appv1 . ComparisonResult ,
healthState * appv1 . HealthStatus ,
parameters [ ] * appv1 . ComponentParameter ,
conditions [ ] appv1 . ApplicationCondition ,
) {
2018-09-24 15:52:43 +00:00
logCtx := log . WithFields ( log . Fields { "application" : app . Name } )
2018-07-12 19:39:46 +00:00
modifiedApp := app . DeepCopy ( )
if comparisonResult != nil {
modifiedApp . Status . ComparisonResult = * comparisonResult
2018-09-24 15:52:43 +00:00
if app . Status . ComparisonResult . Status != comparisonResult . Status {
message := fmt . Sprintf ( "Updated sync status: %s -> %s" , app . Status . ComparisonResult . Status , comparisonResult . Status )
ctrl . auditLogger . LogAppEvent ( app , argo . EventInfo { Reason : argo . EventReasonResourceUpdated , Type : v1 . EventTypeNormal } , message )
}
logCtx . Infof ( "Comparison result: prev: %s. current: %s" , app . Status . ComparisonResult . Status , comparisonResult . Status )
2018-07-12 19:39:46 +00:00
}
if healthState != nil {
2018-09-24 15:52:43 +00:00
if modifiedApp . Status . Health . Status != healthState . Status {
message := fmt . Sprintf ( "Updated health status: %s -> %s" , modifiedApp . Status . Health . Status , healthState . Status )
ctrl . auditLogger . LogAppEvent ( app , argo . EventInfo { Reason : argo . EventReasonResourceUpdated , Type : v1 . EventTypeNormal } , message )
}
2018-07-12 19:39:46 +00:00
modifiedApp . Status . Health = * healthState
}
if parameters != nil {
modifiedApp . Status . Parameters = make ( [ ] appv1 . ComponentParameter , len ( parameters ) )
for i := range parameters {
modifiedApp . Status . Parameters [ i ] = * parameters [ i ]
}
}
if conditions != nil {
modifiedApp . Status . Conditions = conditions
}
origBytes , err := json . Marshal ( app )
if err != nil {
2018-09-24 15:52:43 +00:00
logCtx . Errorf ( "Error updating (marshal orig app): %v" , err )
2018-07-12 19:39:46 +00:00
return
}
modifiedBytes , err := json . Marshal ( modifiedApp )
if err != nil {
2018-09-24 15:52:43 +00:00
logCtx . Errorf ( "Error updating (marshal modified app): %v" , err )
2018-07-12 19:39:46 +00:00
return
}
patch , err := strategicpatch . CreateTwoWayMergePatch ( origBytes , modifiedBytes , appv1 . Application { } )
if err != nil {
2018-09-24 15:52:43 +00:00
logCtx . Errorf ( "Error calculating patch for update: %v" , err )
2018-07-12 19:39:46 +00:00
return
}
if string ( patch ) == "{}" {
2018-09-24 15:52:43 +00:00
logCtx . Infof ( "No status changes. Skipping patch" )
2018-07-12 19:39:46 +00:00
return
2018-05-03 22:55:01 +00:00
}
2018-07-12 19:39:46 +00:00
appClient := ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( app . Namespace )
_ , err = appClient . Patch ( app . Name , types . MergePatchType , patch )
2018-02-22 18:56:14 +00:00
if err != nil {
2018-09-24 15:52:43 +00:00
logCtx . Warnf ( "Error updating application: %v" , err )
2018-04-11 19:53:33 +00:00
} else {
2018-09-24 15:52:43 +00:00
logCtx . Infof ( "Update successful" )
2018-02-22 18:56:14 +00:00
}
}
2018-09-11 21:28:53 +00:00
// 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
2018-09-24 15:52:43 +00:00
2018-09-11 21:28:53 +00:00
// 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
2018-09-24 15:52:43 +00:00
// and parameter overrides are different from our most recent sync operation.
if alreadyAttemptedSync ( app , desiredCommitSHA ) {
if app . Status . OperationState . Phase != appv1 . OperationSucceeded {
2018-09-11 21:28:53 +00:00
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 }
}
2018-09-24 15:52:43 +00:00
logCtx . Infof ( "Skipping auto-sync: most recent sync already to %s" , desiredCommitSHA )
return nil
2018-09-11 21:28:53 +00:00
}
2018-02-26 15:43:35 +00:00
2018-09-11 21:28:53 +00:00
op := appv1 . Operation {
Sync : & appv1 . SyncOperation {
2018-09-24 15:52:43 +00:00
Revision : desiredCommitSHA ,
Prune : app . Spec . SyncPolicy . Automated . Prune ,
ParameterOverrides : app . Spec . Source . ComponentParameterOverrides ,
2018-02-26 15:43:35 +00:00
} ,
2018-09-11 21:28:53 +00:00
}
appIf := ctrl . applicationClientset . ArgoprojV1alpha1 ( ) . Applications ( app . Namespace )
2018-11-17 01:10:04 +00:00
_ , err := argo . SetAppOperation ( appIf , app . Name , & op )
2018-09-11 21:28:53 +00:00
if err != nil {
logCtx . Errorf ( "Failed to initiate auto-sync to %s: %v" , desiredCommitSHA , err )
return & appv1 . ApplicationCondition { Type : appv1 . ApplicationConditionSyncError , Message : err . Error ( ) }
}
2018-09-24 15:52:43 +00:00
message := fmt . Sprintf ( "Initiated automated sync to '%s'" , desiredCommitSHA )
ctrl . auditLogger . LogAppEvent ( app , argo . EventInfo { Reason : argo . EventReasonOperationStarted , Type : v1 . EventTypeNormal } , message )
logCtx . Info ( message )
2018-09-11 21:28:53 +00:00
return nil
}
2018-09-24 15:52:43 +00:00
// alreadyAttemptedSync returns whether or not the most recent sync was performed against the
// commitSHA and with the same parameter overrides which are currently set in the app
func alreadyAttemptedSync ( app * appv1 . Application , commitSHA string ) bool {
if app . Status . OperationState == nil || app . Status . OperationState . Operation . Sync == nil || app . Status . OperationState . SyncResult == nil {
return false
}
if app . Status . OperationState . SyncResult . Revision != commitSHA {
return false
}
if ! reflect . DeepEqual ( appv1 . ParameterOverrides ( app . Spec . Source . ComponentParameterOverrides ) , app . Status . OperationState . Operation . Sync . ParameterOverrides ) {
return false
}
return true
}
2018-09-11 21:28:53 +00:00
func ( ctrl * ApplicationController ) newApplicationInformer ( ) cache . SharedIndexInformer {
appInformerFactory := appinformers . NewFilteredSharedInformerFactory (
ctrl . applicationClientset ,
ctrl . statusRefreshTimeout ,
ctrl . namespace ,
func ( options * metav1 . ListOptions ) { } ,
2018-02-20 23:23:37 +00:00
)
informer := appInformerFactory . Argoproj ( ) . V1alpha1 ( ) . Applications ( ) . Informer ( )
informer . AddEventHandler (
cache . ResourceEventHandlerFuncs {
AddFunc : func ( obj interface { } ) {
key , err := cache . MetaNamespaceKeyFunc ( obj )
if err == nil {
2018-09-11 21:28:53 +00:00
ctrl . appRefreshQueue . Add ( key )
ctrl . appOperationQueue . Add ( key )
2018-02-20 23:23:37 +00:00
}
} ,
UpdateFunc : func ( old , new interface { } ) {
key , err := cache . MetaNamespaceKeyFunc ( new )
2018-09-11 21:28:53 +00:00
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 )
}
2018-02-20 23:23:37 +00:00
}
2018-09-11 21:28:53 +00:00
ctrl . appRefreshQueue . Add ( key )
ctrl . appOperationQueue . Add ( key )
2018-02-20 23:23:37 +00:00
} ,
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 {
2018-09-11 21:28:53 +00:00
ctrl . appRefreshQueue . Add ( key )
2018-02-20 23:23:37 +00:00
}
} ,
} ,
)
return informer
}
2018-07-07 07:54:06 +00:00
2018-07-14 00:13:31 +00:00
func isOperationInProgress ( app * appv1 . Application ) bool {
2018-07-07 07:54:06 +00:00
return app . Status . OperationState != nil && ! app . Status . OperationState . Phase . Completed ( )
}
2018-09-11 21:28:53 +00:00
// 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
}