diff --git a/controller/appcontroller.go b/controller/appcontroller.go index 128995659abc5..416c6b041306b 100644 --- a/controller/appcontroller.go +++ b/controller/appcontroller.go @@ -10,6 +10,7 @@ import ( "time" log "github.com/sirupsen/logrus" + "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -46,6 +47,7 @@ type ApplicationController struct { namespace string kubeClientset kubernetes.Interface applicationClientset appclientset.Interface + auditLogger *argo.AuditLogger appRefreshQueue workqueue.RateLimitingInterface appOperationQueue workqueue.RateLimitingInterface appInformer cache.SharedIndexInformer @@ -88,6 +90,7 @@ func NewApplicationController( statusRefreshTimeout: appResyncPeriod, forceRefreshApps: make(map[string]bool), forceRefreshAppsMutex: &sync.Mutex{}, + auditLogger: argo.NewAuditLogger(namespace, kubeClientset, "appcontroller"), } } @@ -283,6 +286,7 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic Type: appv1.ApplicationConditionDeletionError, Message: err.Error(), }) + ctrl.auditLogger.LogAppEvent(app, argo.EventInfo{Reason: argo.EventReasonStatusRefreshed, Action: "refresh_status"}, v1.EventTypeWarning) } else { log.Infof("Successfully deleted resources for application %s", app.Name) } @@ -396,6 +400,7 @@ func (ctrl *ApplicationController) setOperationState(app *appv1.Application, sta // If operation is completed, clear the operation field to indicate no operation is // in progress. patch["operation"] = nil + ctrl.auditLogger.LogAppEvent(app, argo.EventInfo{Reason: argo.EventReasonResourceUpdated, Action: "refresh_status"}, v1.EventTypeNormal) } if reflect.DeepEqual(app.Status.OperationState, state) { log.Infof("No operation updates necessary to '%s'. Skipping patch", app.Name) diff --git a/server/application/application.go b/server/application/application.go index fad483f8f36a5..ef45d66eedb7e 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -32,6 +32,7 @@ import ( "github.com/argoproj/argo-cd/util/db" "github.com/argoproj/argo-cd/util/grpc" "github.com/argoproj/argo-cd/util/rbac" + "github.com/argoproj/argo-cd/util/session" ) // Server provides a Application service @@ -44,6 +45,7 @@ type Server struct { appComparator controller.AppStateManager enf *rbac.Enforcer projectLock *util.KeyLock + auditLogger *argo.AuditLogger } // NewServer returns a new instance of the Application service @@ -66,6 +68,7 @@ func NewServer( appComparator: controller.NewAppStateManager(db, appclientset, repoClientset, namespace), enf: enf, projectLock: projectLock, + auditLogger: argo.NewAuditLogger(namespace, kubeclientset, "argocd-server"), } } @@ -128,6 +131,10 @@ func (s *Server) Create(ctx context.Context, q *ApplicationCreateRequest) (*appv } } } + + if err == nil { + s.logEvent(out, ctx, argo.EventReasonResourceCreated, "create") + } return out, err } @@ -215,11 +222,20 @@ func (s *Server) ListResourceEvents(ctx context.Context, q *ApplicationResourceE return nil, err } - fieldSelector := fields.SelectorFromSet(map[string]string{ - "involvedObject.name": q.ResourceName, - "involvedObject.uid": q.ResourceUID, - "involvedObject.namespace": namespace, - }).String() + var fieldSelector string + if q.ResourceName == "" && q.ResourceUID == "" { + fieldSelector = fields.SelectorFromSet(map[string]string{ + "involvedObject.name": a.Name, + "involvedObject.uid": string(a.UID), + "involvedObject.namespace": namespace, + }).String() + } else { + fieldSelector = fields.SelectorFromSet(map[string]string{ + "involvedObject.name": q.ResourceName, + "involvedObject.uid": q.ResourceUID, + "involvedObject.namespace": namespace, + }).String() + } log.Infof("Querying for resource events with field selector: %s", fieldSelector) opts := metav1.ListOptions{FieldSelector: fieldSelector} @@ -300,6 +316,9 @@ func (s *Server) UpdateSpec(ctx context.Context, q *ApplicationUpdateSpecRequest return nil, err } _, err = s.appclientset.ArgoprojV1alpha1().Applications(s.ns).Patch(*q.Name, types.MergePatchType, patch) + if err != nil { + s.logEvent(a, ctx, argo.EventReasonResourceUpdated, "update") + } return &q.Spec, err } @@ -355,6 +374,7 @@ func (s *Server) Delete(ctx context.Context, q *ApplicationDeleteRequest) (*Appl return nil, err } + s.logEvent(a, ctx, argo.EventReasonResourceDeleted, "delete") return &ApplicationResponse{}, nil } @@ -570,7 +590,7 @@ func (s *Server) Sync(ctx context.Context, syncReq *ApplicationSyncRequest) (*ap if !s.enf.EnforceClaims(ctx.Value("claims"), "applications", "sync", appRBACName(*a)) { return nil, grpc.ErrPermissionDenied } - return s.setAppOperation(ctx, *syncReq.Name, func(app *appv1.Application) (*appv1.Operation, error) { + return s.setAppOperation(ctx, *syncReq.Name, "sync", func(app *appv1.Application) (*appv1.Operation, error) { syncOp := appv1.SyncOperation{ Revision: syncReq.Revision, Prune: syncReq.Prune, @@ -591,7 +611,7 @@ func (s *Server) Rollback(ctx context.Context, rollbackReq *ApplicationRollbackR if !s.enf.EnforceClaims(ctx.Value("claims"), "applications", "rollback", appRBACName(*a)) { return nil, grpc.ErrPermissionDenied } - return s.setAppOperation(ctx, *rollbackReq.Name, func(app *appv1.Application) (*appv1.Operation, error) { + return s.setAppOperation(ctx, *rollbackReq.Name, "rollback", func(app *appv1.Application) (*appv1.Operation, error) { return &appv1.Operation{ Rollback: &appv1.RollbackOperation{ ID: rollbackReq.ID, @@ -602,7 +622,7 @@ func (s *Server) Rollback(ctx context.Context, rollbackReq *ApplicationRollbackR }) } -func (s *Server) setAppOperation(ctx context.Context, appName string, operationCreator func(app *appv1.Application) (*appv1.Operation, error)) (*appv1.Application, error) { +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 { @@ -621,6 +641,9 @@ func (s *Server) setAppOperation(ctx context.Context, appName string, operationC 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 } } @@ -652,7 +675,13 @@ func (s *Server) TerminateOperation(ctx context.Context, termOpReq *OperationTer a, err = s.appclientset.ArgoprojV1alpha1().Applications(s.ns).Get(*termOpReq.Name, metav1.GetOptions{}) if err != nil { return nil, err + } else { + s.logEvent(a, ctx, argo.EventReasonResourceUpdated, "terminateop") } } return nil, status.Errorf(codes.Internal, "Failed to terminate app. Too many conflicts") } + +func (s *Server) logEvent(a *appv1.Application, ctx context.Context, reason string, action string) { + s.auditLogger.LogAppEvent(a, argo.EventInfo{Reason: reason, Action: action, Username: session.Username(ctx)}, v1.EventTypeNormal) +} diff --git a/server/project/project.go b/server/project/project.go index 742f963b92a4a..5232421c50c9b 100644 --- a/server/project/project.go +++ b/server/project/project.go @@ -14,9 +14,12 @@ import ( "github.com/argoproj/argo-cd/util/git" "github.com/argoproj/argo-cd/util/grpc" "github.com/argoproj/argo-cd/util/rbac" + "github.com/argoproj/argo-cd/util/session" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" ) // Server provides a Project service @@ -24,12 +27,14 @@ type Server struct { ns string enf *rbac.Enforcer appclientset appclientset.Interface + auditLogger *argo.AuditLogger projectLock *util.KeyLock } // NewServer returns a new instance of the Project service -func NewServer(ns string, appclientset appclientset.Interface, enf *rbac.Enforcer, projectLock *util.KeyLock) *Server { - return &Server{enf: enf, appclientset: appclientset, ns: ns, projectLock: projectLock} +func NewServer(ns string, kubeclientset kubernetes.Interface, appclientset appclientset.Interface, enf *rbac.Enforcer, projectLock *util.KeyLock) *Server { + auditLogger := argo.NewAuditLogger(ns, kubeclientset, "argocd-server") + return &Server{enf: enf, appclientset: appclientset, ns: ns, projectLock: projectLock, auditLogger: auditLogger} } // Create a new project. @@ -44,7 +49,11 @@ func (s *Server) Create(ctx context.Context, q *ProjectCreateRequest) (*v1alpha1 if err != nil { return nil, err } - return s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Create(q.Project) + res, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Create(q.Project) + if err == nil { + s.logEvent(res, ctx, argo.EventReasonResourceCreated, "create") + } + return res, err } // List returns list of projects @@ -187,7 +196,11 @@ func (s *Server) Update(ctx context.Context, q *ProjectUpdateRequest) (*v1alpha1 codes.InvalidArgument, "following source repos are used by one or more application and cannot be removed: %s", strings.Join(removedSrcUsed, ";")) } - return s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Update(q.Project) + res, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Update(q.Project) + if err == nil { + s.logEvent(res, ctx, argo.EventReasonResourceUpdated, "update") + } + return res, err } // Delete deletes a project @@ -199,6 +212,11 @@ func (s *Server) Delete(ctx context.Context, q *ProjectQuery) (*EmptyResponse, e s.projectLock.Lock(q.Name) defer s.projectLock.Unlock(q.Name) + p, err := s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Get(q.Name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + appsList, err := s.appclientset.ArgoprojV1alpha1().Applications(s.ns).List(metav1.ListOptions{}) if err != nil { return nil, err @@ -207,5 +225,13 @@ func (s *Server) Delete(ctx context.Context, q *ProjectQuery) (*EmptyResponse, e if len(apps) > 0 { return nil, status.Errorf(codes.InvalidArgument, "project is referenced by %d applications", len(apps)) } - return &EmptyResponse{}, s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Delete(q.Name, &metav1.DeleteOptions{}) + err = s.appclientset.ArgoprojV1alpha1().AppProjects(s.ns).Delete(q.Name, &metav1.DeleteOptions{}) + if err == nil { + s.logEvent(p, ctx, argo.EventReasonResourceDeleted, "delete") + } + return &EmptyResponse{}, err +} + +func (s *Server) logEvent(p *v1alpha1.AppProject, ctx context.Context, reason string, action string) { + s.auditLogger.LogAppProjEvent(p, argo.EventInfo{Reason: reason, Action: action, Username: session.Username(ctx)}, v1.EventTypeNormal) } diff --git a/server/project/project.pb.go b/server/project/project.pb.go index 1ffb2bb3b327c..3abdcd3b1db93 100644 --- a/server/project/project.pb.go +++ b/server/project/project.pb.go @@ -25,6 +25,7 @@ import math "math" import _ "github.com/gogo/protobuf/gogoproto" import _ "google.golang.org/genproto/googleapis/api/annotations" import _ "k8s.io/api/core/v1" +import _ "k8s.io/apimachinery/pkg/apis/meta/v1" import github_com_argoproj_argo_cd_pkg_apis_application_v1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" import context "golang.org/x/net/context" @@ -881,36 +882,36 @@ var ( func init() { proto.RegisterFile("server/project/project.proto", fileDescriptorProject) } var fileDescriptorProject = []byte{ - // 484 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x94, 0x4d, 0x6b, 0x14, 0x31, - 0x18, 0xc7, 0x89, 0xd6, 0x15, 0xe3, 0x2b, 0xa1, 0xd5, 0x3a, 0x6d, 0x57, 0x19, 0x2f, 0xb2, 0x68, - 0xc2, 0xd6, 0x83, 0xc5, 0x9b, 0x2f, 0x45, 0x0a, 0x1e, 0x74, 0xc5, 0x8b, 0x08, 0x25, 0x9d, 0x79, - 0x48, 0xd3, 0xdd, 0x9d, 0xc4, 0x24, 0x3b, 0x52, 0x8a, 0x97, 0xe2, 0xcd, 0xa3, 0x1f, 0xc2, 0xbb, - 0x9f, 0xc2, 0xa3, 0xe0, 0x17, 0x90, 0xc5, 0x0f, 0x22, 0xf3, 0xec, 0x44, 0xbb, 0xdd, 0xae, 0xa7, - 0xa1, 0xa7, 0x79, 0x26, 0xc9, 0xe4, 0xf7, 0x9b, 0x27, 0x7f, 0x42, 0x57, 0x3d, 0xb8, 0x12, 0x9c, - 0xb0, 0xce, 0xec, 0x41, 0x16, 0xe2, 0x93, 0x5b, 0x67, 0x82, 0x61, 0xe7, 0xeb, 0xd7, 0x64, 0x51, - 0x19, 0x65, 0x70, 0x4c, 0x54, 0xd5, 0x64, 0x3a, 0x59, 0x55, 0xc6, 0xa8, 0x01, 0x08, 0x69, 0xb5, - 0x90, 0x45, 0x61, 0x82, 0x0c, 0xda, 0x14, 0xbe, 0x9e, 0x4d, 0xfb, 0x1b, 0x9e, 0x6b, 0x83, 0xb3, - 0x99, 0x71, 0x20, 0xca, 0xae, 0x50, 0x50, 0x80, 0x93, 0x01, 0xf2, 0x7a, 0xcd, 0x96, 0xd2, 0x61, - 0x77, 0xb4, 0xc3, 0x33, 0x33, 0x14, 0xd2, 0x21, 0x62, 0x0f, 0x8b, 0xfb, 0x59, 0x2e, 0x6c, 0x5f, - 0x55, 0x1f, 0x7b, 0x21, 0xad, 0x1d, 0xe8, 0x0c, 0x37, 0x17, 0x65, 0x57, 0x0e, 0xec, 0xae, 0x9c, - 0xd9, 0x2a, 0xfd, 0x40, 0x17, 0x5f, 0x4e, 0x6c, 0x9f, 0x3a, 0x90, 0x01, 0x7a, 0xf0, 0x7e, 0x04, - 0x3e, 0xb0, 0x6d, 0x1a, 0xff, 0x62, 0x99, 0xdc, 0x26, 0x77, 0x2f, 0xae, 0x6f, 0xf2, 0x7f, 0x50, - 0x1e, 0xa1, 0x58, 0x6c, 0x67, 0x39, 0xb7, 0x7d, 0xc5, 0x2b, 0x28, 0x3f, 0x02, 0xe5, 0x11, 0xca, - 0x1f, 0x5b, 0x5b, 0x43, 0x7a, 0x71, 0xd7, 0x34, 0xa5, 0x97, 0xea, 0xb1, 0x57, 0x23, 0x70, 0xfb, - 0x8c, 0xd1, 0x85, 0x42, 0x0e, 0x01, 0x69, 0x17, 0x7a, 0x58, 0x1f, 0x91, 0x7b, 0x63, 0xf3, 0xd3, - 0x94, 0xbb, 0x4a, 0x2f, 0x6f, 0x0e, 0x6d, 0xd8, 0xef, 0x81, 0xb7, 0xa6, 0xf0, 0xb0, 0xfe, 0xed, - 0x1c, 0xbd, 0x52, 0xaf, 0x7a, 0x0d, 0xae, 0xd4, 0x19, 0xb0, 0xcf, 0x84, 0xb6, 0x26, 0x3d, 0x63, - 0x6b, 0x3c, 0x06, 0xe0, 0xa4, 0x5e, 0x26, 0xcd, 0xd8, 0xa5, 0x2b, 0x87, 0x3f, 0x7f, 0x7f, 0x39, - 0xb3, 0x94, 0x5e, 0xc3, 0x6c, 0x94, 0xdd, 0x98, 0x3a, 0xff, 0x88, 0x74, 0xd8, 0x21, 0xa1, 0x0b, - 0x2f, 0xb4, 0x0f, 0x6c, 0xe9, 0xb8, 0x0b, 0xb6, 0x37, 0xd9, 0x6a, 0xc4, 0xa1, 0x22, 0xa4, 0xcb, - 0xe8, 0xc1, 0xd8, 0x8c, 0x07, 0xfb, 0x44, 0xe8, 0xd9, 0xe7, 0x30, 0xd7, 0xa1, 0xa1, 0x3e, 0xdc, - 0x42, 0xfe, 0x4d, 0x76, 0xe3, 0x38, 0x5f, 0x1c, 0x54, 0xa9, 0xf9, 0xc8, 0xbe, 0x12, 0xda, 0x9a, - 0x04, 0x66, 0xf6, 0x64, 0xa6, 0x82, 0xd4, 0x94, 0xd1, 0x43, 0x34, 0xea, 0x26, 0xf7, 0xa2, 0x91, - 0x03, 0x6b, 0xbc, 0x0e, 0xc6, 0x69, 0xf0, 0xe2, 0x20, 0x2a, 0x0c, 0x21, 0xc8, 0x5c, 0x06, 0xc9, - 0x51, 0xb3, 0x3a, 0xb5, 0x77, 0xb4, 0xf5, 0x0c, 0x06, 0x10, 0x60, 0x5e, 0xcb, 0xae, 0xff, 0x1d, - 0x9e, 0xca, 0x63, 0x7a, 0x07, 0x89, 0x6b, 0x9d, 0x95, 0x93, 0x89, 0x08, 0x78, 0xb2, 0xf1, 0x7d, - 0xdc, 0x26, 0x3f, 0xc6, 0x6d, 0xf2, 0x6b, 0xdc, 0x26, 0x6f, 0x3b, 0xff, 0xbb, 0x34, 0xa6, 0xef, - 0xb3, 0x9d, 0x16, 0x5e, 0x0e, 0x0f, 0xfe, 0x04, 0x00, 0x00, 0xff, 0xff, 0x95, 0xb1, 0xb7, 0x37, - 0xe8, 0x04, 0x00, 0x00, + // 487 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xbc, 0x94, 0xcd, 0x6e, 0x13, 0x31, + 0x10, 0xc7, 0x65, 0x28, 0x41, 0x98, 0x4f, 0x59, 0x2d, 0x94, 0xa5, 0x04, 0xb4, 0xa7, 0x2a, 0x52, + 0x6d, 0xa5, 0xe5, 0x50, 0x71, 0xe3, 0xa3, 0x42, 0x95, 0x38, 0x40, 0x10, 0x12, 0xe2, 0x52, 0xb9, + 0xde, 0x91, 0xe3, 0x26, 0xbb, 0x36, 0xb6, 0xb3, 0x28, 0x42, 0x5c, 0x2a, 0x6e, 0x1c, 0x79, 0x04, + 0x6e, 0x3c, 0x09, 0x47, 0x24, 0x5e, 0x00, 0x45, 0x3c, 0x08, 0x5a, 0x67, 0x4d, 0x9a, 0xa6, 0xe1, + 0xb4, 0xe2, 0x94, 0xc9, 0xf8, 0xe3, 0xff, 0x9b, 0xf1, 0x7f, 0x07, 0x6f, 0x38, 0xb0, 0x25, 0x58, + 0x66, 0xac, 0x3e, 0x02, 0xe1, 0xe3, 0x2f, 0x35, 0x56, 0x7b, 0x4d, 0x2e, 0xd6, 0x7f, 0x93, 0x55, + 0xa9, 0xa5, 0x0e, 0x39, 0x56, 0x45, 0xd3, 0xe5, 0x64, 0x43, 0x6a, 0x2d, 0x87, 0xc0, 0xb8, 0x51, + 0x8c, 0x17, 0x85, 0xf6, 0xdc, 0x2b, 0x5d, 0xb8, 0x7a, 0x35, 0x1d, 0xec, 0x3a, 0xaa, 0x74, 0x58, + 0x15, 0xda, 0x02, 0x2b, 0xbb, 0x4c, 0x42, 0x01, 0x96, 0x7b, 0xc8, 0xea, 0x3d, 0x0f, 0x66, 0x7b, + 0x72, 0x2e, 0xfa, 0xaa, 0x00, 0x3b, 0x66, 0x66, 0x20, 0xab, 0x84, 0x63, 0x39, 0x78, 0x7e, 0xd6, + 0xa9, 0x7d, 0xa9, 0x7c, 0x7f, 0x74, 0x48, 0x85, 0xce, 0x19, 0xb7, 0x01, 0xec, 0x28, 0x04, 0x5b, + 0x22, 0x9b, 0x9d, 0xe6, 0xc6, 0x0c, 0x95, 0x08, 0x48, 0xac, 0xec, 0xf2, 0xa1, 0xe9, 0xf3, 0x85, + 0xab, 0xd2, 0xf7, 0x78, 0xf5, 0xc5, 0xb4, 0xc6, 0x27, 0x16, 0xb8, 0x87, 0x1e, 0xbc, 0x1b, 0x81, + 0xf3, 0xe4, 0x00, 0xc7, 0xda, 0xd7, 0xd1, 0x7d, 0xb4, 0x79, 0x79, 0x7b, 0x8f, 0xce, 0x44, 0x69, + 0x14, 0x0d, 0xc1, 0x81, 0xc8, 0xa8, 0x19, 0x48, 0x5a, 0x89, 0xd2, 0x13, 0xa2, 0x34, 0x8a, 0xd2, + 0x47, 0xc6, 0xd4, 0x22, 0xbd, 0x78, 0x6b, 0x9a, 0xe2, 0x2b, 0x75, 0xee, 0xe5, 0x08, 0xec, 0x98, + 0x10, 0xbc, 0x52, 0xf0, 0x1c, 0x82, 0xda, 0xa5, 0x5e, 0x88, 0x4f, 0xc0, 0xbd, 0x36, 0xd9, 0xff, + 0x84, 0xbb, 0x8e, 0xaf, 0xee, 0xe5, 0xc6, 0x8f, 0x7b, 0xe0, 0x8c, 0x2e, 0x1c, 0x6c, 0x7f, 0xbb, + 0x80, 0xaf, 0xd5, 0xbb, 0x5e, 0x81, 0x2d, 0x95, 0x00, 0xf2, 0x19, 0xe1, 0xd6, 0xb4, 0x67, 0xe4, + 0x2e, 0x8d, 0xb6, 0x39, 0xab, 0x97, 0x49, 0x33, 0x74, 0xe9, 0x9d, 0xe3, 0x9f, 0xbf, 0xbf, 0x9c, + 0x5b, 0x4b, 0x6f, 0x04, 0x47, 0x95, 0xdd, 0xe8, 0x55, 0xf7, 0x10, 0x75, 0xc8, 0x31, 0xc2, 0x2b, + 0xcf, 0x95, 0xf3, 0x64, 0xed, 0x34, 0x4b, 0x68, 0x6f, 0xb2, 0xdf, 0x08, 0x43, 0xa5, 0x90, 0xae, + 0x07, 0x0e, 0x42, 0x16, 0x38, 0xc8, 0x27, 0x84, 0xcf, 0x3f, 0x83, 0xa5, 0x0c, 0x0d, 0xf5, 0xe1, + 0x5e, 0xd0, 0xbf, 0x4d, 0x6e, 0x9d, 0xd6, 0x67, 0x1f, 0x2a, 0xd7, 0x7c, 0x24, 0x5f, 0x11, 0x6e, + 0x4d, 0x0d, 0xb3, 0xf8, 0x32, 0x73, 0x46, 0x6a, 0x8a, 0x68, 0x27, 0x10, 0x6d, 0x25, 0x9b, 0x8b, + 0x44, 0x51, 0xbe, 0xfa, 0x94, 0x33, 0xee, 0x39, 0x0d, 0x88, 0xd5, 0x8b, 0xbd, 0xc1, 0xad, 0xa7, + 0x30, 0x04, 0x0f, 0xcb, 0xda, 0x75, 0xf3, 0x6f, 0x7a, 0xce, 0x8b, 0xb1, 0xfe, 0xce, 0xb2, 0xfa, + 0x1f, 0xef, 0x7e, 0x9f, 0xb4, 0xd1, 0x8f, 0x49, 0x1b, 0xfd, 0x9a, 0xb4, 0xd1, 0xdb, 0xce, 0xbf, + 0x86, 0xc5, 0xfc, 0xf4, 0x3b, 0x6c, 0x85, 0xa1, 0xb0, 0xf3, 0x27, 0x00, 0x00, 0xff, 0xff, 0x59, + 0x47, 0x12, 0x67, 0x16, 0x05, 0x00, 0x00, } diff --git a/server/project/project.pb.gw.go b/server/project/project.pb.gw.go index dde14b1802473..23347f42826cd 100644 --- a/server/project/project.pb.gw.go +++ b/server/project/project.pb.gw.go @@ -336,9 +336,9 @@ var ( pattern_ProjectService_Get_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "projects", "name"}, "")) - pattern_ProjectService_Update_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "repositories", "project.metadata.name"}, "")) + pattern_ProjectService_Update_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "projects", "project.metadata.name"}, "")) - pattern_ProjectService_Delete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "repositories", "name"}, "")) + pattern_ProjectService_Delete_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3}, []string{"api", "v1", "projects", "name"}, "")) ) var ( diff --git a/server/project/project.proto b/server/project/project.proto index 42ab6e6c92ecc..c28a95d566025 100644 --- a/server/project/project.proto +++ b/server/project/project.proto @@ -9,6 +9,7 @@ package project; import "gogoproto/gogo.proto"; import "google/api/annotations.proto"; import "k8s.io/api/core/v1/generated.proto"; +import "k8s.io/apimachinery/pkg/apis/meta/v1/generated.proto"; import "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1/generated.proto"; @@ -52,13 +53,13 @@ service ProjectService { // Update updates a project rpc Update(ProjectUpdateRequest) returns (github.com.argoproj.argo_cd.pkg.apis.application.v1alpha1.AppProject) { option (google.api.http) = { - put: "/api/v1/repositories/{project.metadata.name}" + put: "/api/v1/projects/{project.metadata.name}" body: "*" }; } // Delete deletes a project rpc Delete(ProjectQuery) returns (EmptyResponse) { - option (google.api.http).delete = "/api/v1/repositories/{name}"; + option (google.api.http).delete = "/api/v1/projects/{name}"; } } diff --git a/server/project/project_test.go b/server/project/project_test.go index 119f2fc44b592..c62d967686934 100644 --- a/server/project/project_test.go +++ b/server/project/project_test.go @@ -39,7 +39,7 @@ func TestProjectServer(t *testing.T) { Spec: v1alpha1.ApplicationSpec{Project: "test", Destination: v1alpha1.ApplicationDestination{Namespace: "ns3", Server: "https://server3"}}, } - projectServer := NewServer("default", apps.NewSimpleClientset(&existingProj, &existingApp), enforcer, util.NewKeyLock()) + projectServer := NewServer("default", fake.NewSimpleClientset(), apps.NewSimpleClientset(&existingProj, &existingApp), enforcer, util.NewKeyLock()) updatedProj := existingProj.DeepCopy() updatedProj.Spec.Destinations = updatedProj.Spec.Destinations[1:] @@ -55,7 +55,7 @@ func TestProjectServer(t *testing.T) { Spec: v1alpha1.ApplicationSpec{Project: "test", Destination: v1alpha1.ApplicationDestination{Namespace: "ns1", Server: "https://server1"}}, } - projectServer := NewServer("default", apps.NewSimpleClientset(&existingProj, &existingApp), enforcer, util.NewKeyLock()) + projectServer := NewServer("default", fake.NewSimpleClientset(), apps.NewSimpleClientset(&existingProj, &existingApp), enforcer, util.NewKeyLock()) updatedProj := existingProj.DeepCopy() updatedProj.Spec.Destinations = updatedProj.Spec.Destinations[1:] @@ -72,7 +72,7 @@ func TestProjectServer(t *testing.T) { Spec: v1alpha1.ApplicationSpec{Project: "test"}, } - projectServer := NewServer("default", apps.NewSimpleClientset(&existingProj, &existingApp), enforcer, util.NewKeyLock()) + projectServer := NewServer("default", fake.NewSimpleClientset(), apps.NewSimpleClientset(&existingProj, &existingApp), enforcer, util.NewKeyLock()) updatedProj := existingProj.DeepCopy() updatedProj.Spec.SourceRepos = []string{} @@ -88,7 +88,7 @@ func TestProjectServer(t *testing.T) { Spec: v1alpha1.ApplicationSpec{Project: "test", Source: v1alpha1.ApplicationSource{RepoURL: "https://github.com/argoproj/argo-cd.git"}}, } - projectServer := NewServer("default", apps.NewSimpleClientset(&existingProj, &existingApp), enforcer, util.NewKeyLock()) + projectServer := NewServer("default", fake.NewSimpleClientset(), apps.NewSimpleClientset(&existingProj, &existingApp), enforcer, util.NewKeyLock()) updatedProj := existingProj.DeepCopy() updatedProj.Spec.SourceRepos = []string{} @@ -100,7 +100,7 @@ func TestProjectServer(t *testing.T) { }) t.Run("TestDeleteProjectSuccessful", func(t *testing.T) { - projectServer := NewServer("default", apps.NewSimpleClientset(&existingProj), enforcer, util.NewKeyLock()) + projectServer := NewServer("default", fake.NewSimpleClientset(), apps.NewSimpleClientset(&existingProj), enforcer, util.NewKeyLock()) _, err := projectServer.Delete(context.Background(), &ProjectQuery{Name: "test"}) @@ -113,7 +113,7 @@ func TestProjectServer(t *testing.T) { Spec: v1alpha1.ApplicationSpec{Project: "test"}, } - projectServer := NewServer("default", apps.NewSimpleClientset(&existingProj, &existingApp), enforcer, util.NewKeyLock()) + projectServer := NewServer("default", fake.NewSimpleClientset(), apps.NewSimpleClientset(&existingProj, &existingApp), enforcer, util.NewKeyLock()) _, err := projectServer.Delete(context.Background(), &ProjectQuery{Name: "test"}) diff --git a/server/server.go b/server/server.go index 71887982401ab..aaf3398669765 100644 --- a/server/server.go +++ b/server/server.go @@ -330,7 +330,7 @@ func (a *ArgoCDServer) newGRPCServer() *grpc.Server { sessionService := session.NewServer(a.sessionMgr) projectLock := util.NewKeyLock() applicationService := application.NewServer(a.Namespace, a.KubeClientset, a.AppClientset, a.RepoClientset, db, a.enf, projectLock) - projectService := project.NewServer(a.Namespace, a.AppClientset, a.enf, projectLock) + projectService := project.NewServer(a.Namespace, a.KubeClientset, a.AppClientset, a.enf, projectLock) settingsService := settings.NewServer(a.settingsMgr) accountService := account.NewServer(a.sessionMgr, a.settingsMgr) version.RegisterVersionServiceServer(grpcS, &version.Server{}) diff --git a/server/swagger.json b/server/swagger.json index 1f76e963d1a8e..e9f596dba90a1 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -691,44 +691,51 @@ } } } - } - }, - "/api/v1/repositories": { - "get": { + }, + "delete": { "tags": [ - "RepositoryService" + "ProjectService" ], - "summary": "List returns list of repos", - "operationId": "ListMixin2", + "summary": "Delete deletes a project", + "operationId": "DeleteMixin3", "parameters": [ { "type": "string", - "name": "repo", - "in": "query" + "name": "name", + "in": "path", + "required": true } ], "responses": { "200": { "description": "(empty)", "schema": { - "$ref": "#/definitions/v1alpha1RepositoryList" + "$ref": "#/definitions/projectEmptyResponse" } } } - }, - "post": { + } + }, + "/api/v1/projects/{project.metadata.name}": { + "put": { "tags": [ - "RepositoryService" + "ProjectService" ], - "summary": "Create creates a repo", - "operationId": "CreateMixin2", + "summary": "Update updates a project", + "operationId": "UpdateMixin3", "parameters": [ + { + "type": "string", + "name": "project.metadata.name", + "in": "path", + "required": true + }, { "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/v1alpha1Repository" + "$ref": "#/definitions/projectProjectUpdateRequest" } } ], @@ -736,57 +743,48 @@ "200": { "description": "(empty)", "schema": { - "$ref": "#/definitions/v1alpha1Repository" + "$ref": "#/definitions/v1alpha1AppProject" } } } } }, - "/api/v1/repositories/{name}": { - "delete": { + "/api/v1/repositories": { + "get": { "tags": [ - "ProjectService" + "RepositoryService" ], - "summary": "Delete deletes a project", - "operationId": "DeleteMixin3", + "summary": "List returns list of repos", + "operationId": "ListMixin2", "parameters": [ { "type": "string", - "name": "name", - "in": "path", - "required": true + "name": "repo", + "in": "query" } ], "responses": { "200": { "description": "(empty)", "schema": { - "$ref": "#/definitions/projectEmptyResponse" + "$ref": "#/definitions/v1alpha1RepositoryList" } } } - } - }, - "/api/v1/repositories/{project.metadata.name}": { - "put": { + }, + "post": { "tags": [ - "ProjectService" + "RepositoryService" ], - "summary": "Update updates a project", - "operationId": "UpdateMixin3", + "summary": "Create creates a repo", + "operationId": "CreateMixin2", "parameters": [ - { - "type": "string", - "name": "project.metadata.name", - "in": "path", - "required": true - }, { "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/projectProjectUpdateRequest" + "$ref": "#/definitions/v1alpha1Repository" } } ], @@ -794,7 +792,7 @@ "200": { "description": "(empty)", "schema": { - "$ref": "#/definitions/v1alpha1AppProject" + "$ref": "#/definitions/v1alpha1Repository" } } } diff --git a/test/e2e/app_management_test.go b/test/e2e/app_management_test.go index d03d53c334bb2..29c28d72e8348 100644 --- a/test/e2e/app_management_test.go +++ b/test/e2e/app_management_test.go @@ -11,12 +11,34 @@ import ( // 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). + + "github.com/argoproj/argo-cd/util/argo" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/fields" _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" ) func TestAppManagement(t *testing.T) { + assertAppHasEvent := func(a *v1alpha1.Application, action string, reason string) { + list, err := fixture.KubeClient.CoreV1().Events(fixture.Namespace).List(metav1.ListOptions{ + FieldSelector: fields.SelectorFromSet(map[string]string{ + "involvedObject.name": a.Name, + "involvedObject.uid": string(a.UID), + "involvedObject.namespace": fixture.Namespace, + }).String(), + }) + if err != nil { + t.Fatalf("Unable to get app events %v", err) + } + for i := range list.Items { + event := list.Items[i] + if event.Reason == reason && event.Action == action { + return + } + } + t.Errorf("Unable to find event with reason=%s; action=%s", reason, action) + } + testApp := &v1alpha1.Application{ Spec: v1alpha1.ApplicationSpec{ Source: v1alpha1.ApplicationSource{ @@ -52,6 +74,7 @@ func TestAppManagement(t *testing.T) { assert.Equal(t, ".", app.Spec.Source.Path) assert.Equal(t, fixture.Namespace, app.Spec.Destination.Namespace) assert.Equal(t, fixture.Config.Host, app.Spec.Destination.Server) + assertAppHasEvent(app, "create", argo.EventReasonResourceCreated) }) t.Run("TestAppDeletion", func(t *testing.T) { @@ -66,6 +89,7 @@ func TestAppManagement(t *testing.T) { assert.NotNil(t, err) assert.True(t, errors.IsNotFound(err)) + assertAppHasEvent(app, "delete", argo.EventReasonResourceDeleted) }) t.Run("TestTrackAppStateAndSyncApp", func(t *testing.T) { @@ -80,6 +104,7 @@ func TestAppManagement(t *testing.T) { if err != nil { t.Fatalf("Unable to sync app %v", err) } + assertAppHasEvent(app, "sync", argo.EventReasonResourceUpdated) WaitUntil(t, func() (done bool, err error) { app, err = fixture.AppClient.ArgoprojV1alpha1().Applications(fixture.Namespace).Get(app.ObjectMeta.Name, metav1.GetOptions{}) @@ -120,6 +145,8 @@ func TestAppManagement(t *testing.T) { t.Fatalf("Unable to sync app %v", err) } + assertAppHasEvent(app, "rollback", argo.EventReasonResourceUpdated) + WaitUntil(t, func() (done bool, err error) { app, err = fixture.AppClient.ArgoprojV1alpha1().Applications(fixture.Namespace).Get(app.ObjectMeta.Name, metav1.GetOptions{}) return err == nil && app.Status.ComparisonResult.Status == v1alpha1.ComparisonStatusSynced, err diff --git a/test/e2e/project_management_test.go b/test/e2e/project_management_test.go index 51d15e916bd09..c60be26feb820 100644 --- a/test/e2e/project_management_test.go +++ b/test/e2e/project_management_test.go @@ -7,12 +7,34 @@ import ( "time" "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" + "github.com/argoproj/argo-cd/util/argo" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" ) func TestProjectManagement(t *testing.T) { + assertProjHasEvent := func(a *v1alpha1.AppProject, action string, reason string) { + list, err := fixture.KubeClient.CoreV1().Events(fixture.Namespace).List(metav1.ListOptions{ + FieldSelector: fields.SelectorFromSet(map[string]string{ + "involvedObject.name": a.Name, + "involvedObject.uid": string(a.UID), + "involvedObject.namespace": fixture.Namespace, + }).String(), + }) + if err != nil { + t.Fatalf("Unable to get app events %v", err) + } + for i := range list.Items { + event := list.Items[i] + if event.Reason == reason && event.Action == action { + return + } + } + t.Errorf("Unable to find event with reason=%s; action=%s", reason, action) + } + t.Run("TestProjectCreation", func(t *testing.T) { projectName := "proj-" + strconv.FormatInt(time.Now().Unix(), 10) _, err := fixture.RunCli("proj", "create", projectName, @@ -39,11 +61,13 @@ func TestProjectManagement(t *testing.T) { assert.Equal(t, 1, len(proj.Spec.SourceRepos)) assert.Equal(t, "https://github.com/argoproj/argo-cd.git", proj.Spec.SourceRepos[0]) + + assertProjHasEvent(proj, "create", argo.EventReasonResourceCreated) }) t.Run("TestProjectDeletion", func(t *testing.T) { projectName := "proj-" + strconv.FormatInt(time.Now().Unix(), 10) - _, err := fixture.AppClient.ArgoprojV1alpha1().AppProjects(fixture.Namespace).Create(&v1alpha1.AppProject{ObjectMeta: metav1.ObjectMeta{Name: projectName}}) + proj, err := fixture.AppClient.ArgoprojV1alpha1().AppProjects(fixture.Namespace).Create(&v1alpha1.AppProject{ObjectMeta: metav1.ObjectMeta{Name: projectName}}) if err != nil { t.Fatalf("Unable to create project %v", err) } @@ -55,6 +79,7 @@ func TestProjectManagement(t *testing.T) { _, err = fixture.AppClient.ArgoprojV1alpha1().AppProjects(fixture.Namespace).Get(projectName, metav1.GetOptions{}) assert.True(t, errors.IsNotFound(err)) + assertProjHasEvent(proj, "delete", argo.EventReasonResourceDeleted) }) t.Run("TestSetProject", func(t *testing.T) { @@ -84,6 +109,7 @@ func TestProjectManagement(t *testing.T) { assert.Equal(t, "https://192.168.99.100:8443", proj.Spec.Destinations[1].Server) assert.Equal(t, "service", proj.Spec.Destinations[1].Namespace) + assertProjHasEvent(proj, "update", argo.EventReasonResourceUpdated) }) t.Run("TestAddProjectDestination", func(t *testing.T) { @@ -118,6 +144,7 @@ func TestProjectManagement(t *testing.T) { assert.Equal(t, "https://192.168.99.100:8443", proj.Spec.Destinations[0].Server) assert.Equal(t, "test1", proj.Spec.Destinations[0].Namespace) + assertProjHasEvent(proj, "update", argo.EventReasonResourceUpdated) }) t.Run("TestRemoveProjectDestination", func(t *testing.T) { @@ -158,6 +185,7 @@ func TestProjectManagement(t *testing.T) { } assert.Equal(t, projectName, proj.Name) assert.Equal(t, 0, len(proj.Spec.Destinations)) + assertProjHasEvent(proj, "update", argo.EventReasonResourceUpdated) }) t.Run("TestAddProjectSource", func(t *testing.T) { @@ -216,5 +244,6 @@ func TestProjectManagement(t *testing.T) { } assert.Equal(t, projectName, proj.Name) assert.Equal(t, 0, len(proj.Spec.SourceRepos)) + assertProjHasEvent(proj, "update", argo.EventReasonResourceUpdated) }) } diff --git a/util/argo/audit_logger.go b/util/argo/audit_logger.go new file mode 100644 index 0000000000000..c3c1b013e82fd --- /dev/null +++ b/util/argo/audit_logger.go @@ -0,0 +1,85 @@ +package argo + +import ( + log "github.com/sirupsen/logrus" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes" + + "fmt" + "time" + + "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1" +) + +type AuditLogger struct { + kIf kubernetes.Interface + component string + ns string +} + +type EventInfo struct { + Action string + Reason string + Username string +} + +const ( + EventReasonStatusRefreshed = "StatusRefreshed" + EventReasonResourceCreated = "ResourceCreated" + EventReasonResourceUpdated = "ResourceUpdated" + EventReasonResourceDeleted = "ResourceDeleted" +) + +func (l *AuditLogger) logEvent(objMeta metav1.ObjectMeta, gvk schema.GroupVersionKind, info EventInfo, eventType string) { + var message string + if info.Username != "" { + message = fmt.Sprintf("User %s executed action %s", info.Username, info.Action) + } else { + message = fmt.Sprintf("Unknown user executed action %s", info.Action) + } + t := metav1.Time{Time: time.Now()} + _, err := l.kIf.CoreV1().Events(l.ns).Create(&v1.Event{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%v.%x", objMeta.Name, t.UnixNano()), + }, + Source: v1.EventSource{ + Component: l.component, + }, + InvolvedObject: v1.ObjectReference{ + Kind: gvk.Kind, + Name: objMeta.Name, + Namespace: objMeta.Namespace, + ResourceVersion: objMeta.ResourceVersion, + APIVersion: gvk.Version, + UID: objMeta.UID, + }, + FirstTimestamp: t, + LastTimestamp: t, + Count: 1, + Message: message, + Type: eventType, + Action: info.Action, + Reason: info.Reason, + }) + if err != nil { + log.Errorf("Unable to create audit event: %v", err) + } +} + +func (l *AuditLogger) LogAppEvent(app *v1alpha1.Application, info EventInfo, eventType string) { + l.logEvent(app.ObjectMeta, v1alpha1.ApplicationSchemaGroupVersionKind, info, eventType) +} + +func (l *AuditLogger) LogAppProjEvent(proj *v1alpha1.AppProject, info EventInfo, eventType string) { + l.logEvent(proj.ObjectMeta, v1alpha1.AppProjectSchemaGroupVersionKind, info, eventType) +} + +func NewAuditLogger(ns string, kIf kubernetes.Interface, component string) *AuditLogger { + return &AuditLogger{ + ns: ns, + kIf: kIf, + component: component, + } +} diff --git a/util/session/sessionmanager.go b/util/session/sessionmanager.go index 3f4cfac0cf063..51878e82ad145 100644 --- a/util/session/sessionmanager.go +++ b/util/session/sessionmanager.go @@ -174,6 +174,28 @@ func (mgr *SessionManager) VerifyToken(tokenString string) (jwt.Claims, error) { } } +func stringFromMap(input map[string]interface{}, key string) string { + if val, ok := input[key]; ok { + if res, ok := val.(string); ok { + return res + } + } + return "" +} + +func Username(ctx context.Context) string { + if claims, ok := ctx.Value("claims").(*jwt.MapClaims); ok { + mapClaims := *claims + switch stringFromMap(mapClaims, "iss") { + case SessionManagerClaimsIssuer: + return stringFromMap(mapClaims, "sub") + default: + return stringFromMap(mapClaims, "email") + } + } + return "" +} + // MakeCookieMetadata generates a string representing a Web cookie. Yum! func MakeCookieMetadata(key, value string, flags ...string) string { components := []string{