Skip to content

Commit

Permalink
Add labels on Application's k8s events
Browse files Browse the repository at this point in the history
Signed-off-by: Siddhesh Ghadi <[email protected]>
  • Loading branch information
svghadi committed May 10, 2024
1 parent 968dc1a commit 8c63b0c
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 13 deletions.
15 changes: 10 additions & 5 deletions controller/appcontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,7 @@ func (ctrl *ApplicationController) processAppOperationQueueItem() (processNext b
Message: err.Error(),
})
message := fmt.Sprintf("Unable to delete application resources: %v", err.Error())
ctrl.auditLogger.LogAppEvent(app, argo.EventInfo{Reason: argo.EventReasonStatusRefreshed, Type: v1.EventTypeWarning}, message, "")
ctrl.logAppEvent(app, argo.EventInfo{Reason: argo.EventReasonStatusRefreshed, Type: v1.EventTypeWarning}, message, context.TODO())
}
}
return
Expand Down Expand Up @@ -1435,7 +1435,7 @@ func (ctrl *ApplicationController) setOperationState(app *appv1.Application, sta
eventInfo.Type = v1.EventTypeWarning
messages = append(messages, "failed:", state.Message)
}
ctrl.auditLogger.LogAppEvent(app, eventInfo, strings.Join(messages, " "), "")
ctrl.logAppEvent(app, eventInfo, strings.Join(messages, " "), context.TODO())
ctrl.metricsServer.IncSync(app, state)
}
}
Expand Down Expand Up @@ -1768,11 +1768,11 @@ func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, new
logCtx := log.WithFields(log.Fields{"application": orig.QualifiedName()})
if orig.Status.Sync.Status != newStatus.Sync.Status {
message := fmt.Sprintf("Updated sync status: %s -> %s", orig.Status.Sync.Status, newStatus.Sync.Status)
ctrl.auditLogger.LogAppEvent(orig, argo.EventInfo{Reason: argo.EventReasonResourceUpdated, Type: v1.EventTypeNormal}, message, "")
ctrl.logAppEvent(orig, argo.EventInfo{Reason: argo.EventReasonResourceUpdated, Type: v1.EventTypeNormal}, message, context.TODO())
}
if orig.Status.Health.Status != newStatus.Health.Status {
message := fmt.Sprintf("Updated health status: %s -> %s", orig.Status.Health.Status, newStatus.Health.Status)
ctrl.auditLogger.LogAppEvent(orig, argo.EventInfo{Reason: argo.EventReasonResourceUpdated, Type: v1.EventTypeNormal}, message, "")
ctrl.logAppEvent(orig, argo.EventInfo{Reason: argo.EventReasonResourceUpdated, Type: v1.EventTypeNormal}, message, context.TODO())
}
var newAnnotations map[string]string
if orig.GetAnnotations() != nil {
Expand Down Expand Up @@ -1924,7 +1924,7 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *
ctrl.writeBackToInformer(updatedApp)
}
message := fmt.Sprintf("Initiated automated sync to '%s'", desiredCommitSHA)
ctrl.auditLogger.LogAppEvent(app, argo.EventInfo{Reason: argo.EventReasonOperationStarted, Type: v1.EventTypeNormal}, message, "")
ctrl.logAppEvent(app, argo.EventInfo{Reason: argo.EventReasonOperationStarted, Type: v1.EventTypeNormal}, message, context.TODO())
logCtx.Info(message)
return nil, setOpTime
}
Expand Down Expand Up @@ -2264,4 +2264,9 @@ func (ctrl *ApplicationController) getAppList(options metav1.ListOptions) (*appv
return appList, nil
}

func (ctrl *ApplicationController) logAppEvent(a *appv1.Application, eventInfo argo.EventInfo, message string, ctx context.Context) {
eventLabels := argo.GetAppEventLabels(a, applisters.NewAppProjectLister(ctrl.projInformer.GetIndexer()), ctrl.namespace, ctrl.settingsMgr, ctrl.db, ctx)
ctrl.auditLogger.LogAppEvent(a, argo.EventInfo{Reason: argo.EventReasonStatusRefreshed, Type: v1.EventTypeWarning}, message, "", eventLabels)
}

type ClusterFilterFunction func(c *appv1.Cluster, distributionFunction sharding.DistributionFunction) bool
7 changes: 7 additions & 0 deletions docs/operator-manual/argocd-cm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,13 @@ data:
# An optional comma-separated list of metadata.labels to observe in the UI.
resource.customLabels: tier

# An optional comma-separated list of metadata.labels keys to add onto k8s events generated for Applications.
# The keys are compared against Application and it's AppProject, if matched,
# the corresponding labels are added on the generated event.
# In case of conflict between labels on Application and AppProject,
# the Application label values are prioritized and added to the event.
resource.eventLabelKeys: team,env

resource.compareoptions: |
# if ignoreAggregatedRoles set to true then differences caused by aggregated roles in RBAC resources are ignored.
ignoreAggregatedRoles: true
Expand Down
3 changes: 2 additions & 1 deletion server/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -2208,7 +2208,8 @@ func (s *Server) logAppEvent(a *appv1.Application, ctx context.Context, reason s
user = "Unknown user"
}
message := fmt.Sprintf("%s %s", user, action)
s.auditLogger.LogAppEvent(a, eventInfo, message, user)
eventLabels := argo.GetAppEventLabels(a, applisters.NewAppProjectLister(s.projInformer.GetIndexer()), s.ns, s.settingsMgr, s.db, ctx)
s.auditLogger.LogAppEvent(a, eventInfo, message, user, eventLabels)
}

func (s *Server) logResourceEvent(res *appv1.ResourceNode, ctx context.Context, reason string, action string) {
Expand Down
63 changes: 63 additions & 0 deletions test/e2e/app_k8s_events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package e2e

import (
"context"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

. "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
. "github.com/argoproj/argo-cd/v2/test/e2e/fixture"
. "github.com/argoproj/argo-cd/v2/test/e2e/fixture/app"
)

// resource.eventLabelKeys keys set in argocd-cm
func TestLabelsOnAppK8sEvents(t *testing.T) {
expectedLabels := map[string]string{"app": "test", "env": "dev"}

Given(t).
Timeout(60).
Path("two-nice-pods").
When().
SetParamInSettingConfigMap("resource.eventLabelKeys", "app,env").
CreateApp("--label=app=test", "--label=env=dev", "--label=tier=ui").
Sync().
Then().
Expect(SyncStatusIs(SyncStatusCodeSynced)).
And(func(app *Application) {
events, err := KubeClientset.CoreV1().Events(app.Namespace).List(context.Background(), metav1.ListOptions{
FieldSelector: fmt.Sprintf("involvedObject.name=%s,involvedObject.kind=Application", app.Name),
})
assert.NoError(t, err)
for _, event := range events.Items {
for k, v := range event.Labels {
ev, found := expectedLabels[k]
assert.True(t, found)
assert.Equal(t, ev, v)
}
}
})
}

// resource.eventLabelKeys keys not set in argocd-cm
func TestNoLabelsOnAppK8sEvents(t *testing.T) {
Given(t).
Timeout(60).
Path("two-nice-pods").
When().
CreateApp("--label=app=test", "--label=env=dev", "--label=tier=ui").
Sync().
Then().
Expect(SyncStatusIs(SyncStatusCodeSynced)).
And(func(app *Application) {
events, err := KubeClientset.CoreV1().Events(app.Namespace).List(context.Background(), metav1.ListOptions{
FieldSelector: fmt.Sprintf("involvedObject.name=%s,involvedObject.kind=Application", app.Name),
})
assert.NoError(t, err)
for _, event := range events.Items {
assert.Nil(t, event.Labels)
}
})
}
34 changes: 34 additions & 0 deletions util/argo/argo.go
Original file line number Diff line number Diff line change
Expand Up @@ -1110,3 +1110,37 @@ func IsValidContainerName(name string) bool {
validationErrors := apimachineryvalidation.NameIsDNSLabel(name, false)
return len(validationErrors) == 0
}

// GetAppEventLabels returns a map of labels to add to k8s event.
// The Application and it's AppProject labels are compared against `resource.eventLabelKeys` key in argocd-cm,
// if matched, the corresponding labels are returned to add on the generated event. In case of conflict
// between labels on Application and AppProject, the Application label values are prioritized and added to the event.
func GetAppEventLabels(app *argoappv1.Application, projLister applicationsv1.AppProjectLister, ns string, settingsManager *settings.SettingsManager, db db.ArgoDB, ctx context.Context) map[string]string {
eventLabels := make(map[string]string)

labels := app.Labels
if labels == nil {
labels = make(map[string]string)
}
proj, err := GetAppProject(app, projLister, ns, settingsManager, db, ctx)
if err == nil {
for k, v := range proj.Labels {
_, found := labels[k]
if !found {
labels[k] = v
}
}
} else {
log.Warn(err)
}

keys := settingsManager.GetEventLabelKeys()
for _, k := range keys {
v, found := labels[k]
if found {
eventLabels[k] = v
}
}

return eventLabels
}
108 changes: 108 additions & 0 deletions util/argo/argo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1565,3 +1565,111 @@ func TestAugmentSyncMsg(t *testing.T) {
})
}
}

func TestGetAppEventLabels(t *testing.T) {
tests := []struct {
name string
cmEventLabelKeys string
appLabels map[string]string
projLabels map[string]string
expectedEventLabels map[string]string
}{
{
name: "no label keys in cm",
cmEventLabelKeys: "",
appLabels: nil,
projLabels: nil,
expectedEventLabels: nil,
},
{
name: "no label keys in cm",
cmEventLabelKeys: "",
appLabels: map[string]string{"team": "A", "tier": "frontend"},
projLabels: map[string]string{"env": "dev"},
expectedEventLabels: nil,
},
{
name: "label keys in cm, no labels on app & proj",
cmEventLabelKeys: "team,env",
appLabels: nil,
projLabels: nil,
expectedEventLabels: nil,
},
{
name: "label keys in cm, labels on app, no labels on proj",
cmEventLabelKeys: "team,env",
appLabels: map[string]string{"team": "A", "tier": "frontend"},
projLabels: nil,
expectedEventLabels: map[string]string{"team": "A"},
},
{
name: "label keys in cm, no labels on app, labels on proj",
cmEventLabelKeys: "team,env",
appLabels: nil,
projLabels: map[string]string{"env": "dev"},
expectedEventLabels: map[string]string{"env": "dev"},
},
{
name: "label keys in cm, labels on app & proj",
cmEventLabelKeys: "team,env",
appLabels: map[string]string{"team": "A", "tier": "frontend"},
projLabels: map[string]string{"env": "dev"},
expectedEventLabels: map[string]string{"team": "A", "env": "dev"},
},
{
name: "app & proj label conflicts",
cmEventLabelKeys: "example.com/team,env",
appLabels: map[string]string{"example.com/team": "A", "tier": "frontend"},
projLabels: map[string]string{"example.com/team": "B", "env": "dev"},
expectedEventLabels: map[string]string{"example.com/team": "A", "env": "dev"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cm := corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-cm",
Namespace: test.FakeArgoCDNamespace,
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
Data: map[string]string{
"resource.eventLabelKeys": tt.cmEventLabelKeys,
},
}

proj := &argoappv1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: test.FakeArgoCDNamespace,
Labels: tt.projLabels,
},
}

var app argoappv1.Application
app.Name = "test-app"
app.Namespace = test.FakeArgoCDNamespace
app.Labels = tt.appLabels
appClientset := appclientset.NewSimpleClientset(proj)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
indexers := cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}
informer := v1alpha1.NewAppProjectInformer(appClientset, test.FakeArgoCDNamespace, 0, indexers)
go informer.Run(ctx.Done())
cache.WaitForCacheSync(ctx.Done(), informer.HasSynced)

kubeClient := fake.NewSimpleClientset(&cm)
settingsMgr := settings.NewSettingsManager(context.Background(), kubeClient, test.FakeArgoCDNamespace)
argoDB := db.NewDB("default", settingsMgr, kubeClient)

eventLabels := GetAppEventLabels(&app, applisters.NewAppProjectLister(informer.GetIndexer()), test.FakeArgoCDNamespace, settingsMgr, argoDB, ctx)
assert.Equal(t, len(eventLabels), len(tt.expectedEventLabels))
for ek, ev := range tt.expectedEventLabels {
v, found := eventLabels[ek]
assert.True(t, found)
assert.Equal(t, ev, v)
}
})
}
}
13 changes: 7 additions & 6 deletions util/argo/audit_logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const (
EventReasonOperationCompleted = "OperationCompleted"
)

func (l *AuditLogger) logEvent(objMeta ObjectRef, gvk schema.GroupVersionKind, info EventInfo, message string, logFields map[string]interface{}) {
func (l *AuditLogger) logEvent(objMeta ObjectRef, gvk schema.GroupVersionKind, info EventInfo, message string, logFields map[string]interface{}, eventLabels map[string]string) {
logCtx := log.WithFields(log.Fields{
"type": info.Type,
"reason": info.Reason,
Expand Down Expand Up @@ -80,6 +80,7 @@ func (l *AuditLogger) logEvent(objMeta ObjectRef, gvk schema.GroupVersionKind, i
event := v1.Event{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%v.%x", objMeta.Name, t.UnixNano()),
Labels: eventLabels,
Annotations: logFieldStrings,
},
Source: v1.EventSource{
Expand Down Expand Up @@ -108,7 +109,7 @@ func (l *AuditLogger) logEvent(objMeta ObjectRef, gvk schema.GroupVersionKind, i
}
}

func (l *AuditLogger) LogAppEvent(app *v1alpha1.Application, info EventInfo, message, user string) {
func (l *AuditLogger) LogAppEvent(app *v1alpha1.Application, info EventInfo, message, user string, eventLabels map[string]string) {
objectMeta := ObjectRef{
Name: app.ObjectMeta.Name,
Namespace: app.ObjectMeta.Namespace,
Expand All @@ -123,7 +124,7 @@ func (l *AuditLogger) LogAppEvent(app *v1alpha1.Application, info EventInfo, mes
fields["user"] = user
}
fields["spec"] = app.Spec
l.logEvent(objectMeta, v1alpha1.ApplicationSchemaGroupVersionKind, info, message, fields)
l.logEvent(objectMeta, v1alpha1.ApplicationSchemaGroupVersionKind, info, message, fields, eventLabels)
}

func (l *AuditLogger) LogAppSetEvent(app *v1alpha1.ApplicationSet, info EventInfo, message, user string) {
Expand All @@ -137,7 +138,7 @@ func (l *AuditLogger) LogAppSetEvent(app *v1alpha1.ApplicationSet, info EventInf
if user != "" {
fields["user"] = user
}
l.logEvent(objectMeta, v1alpha1.ApplicationSetSchemaGroupVersionKind, info, message, fields)
l.logEvent(objectMeta, v1alpha1.ApplicationSetSchemaGroupVersionKind, info, message, fields, nil)
}

func (l *AuditLogger) LogResourceEvent(res *v1alpha1.ResourceNode, info EventInfo, message, user string) {
Expand All @@ -155,7 +156,7 @@ func (l *AuditLogger) LogResourceEvent(res *v1alpha1.ResourceNode, info EventInf
Group: res.Group,
Version: res.Version,
Kind: res.Kind,
}, info, message, fields)
}, info, message, fields, nil)
}

func (l *AuditLogger) LogAppProjEvent(proj *v1alpha1.AppProject, info EventInfo, message, user string) {
Expand All @@ -169,7 +170,7 @@ func (l *AuditLogger) LogAppProjEvent(proj *v1alpha1.AppProject, info EventInfo,
if user != "" {
fields["user"] = user
}
l.logEvent(objectMeta, v1alpha1.AppProjectSchemaGroupVersionKind, info, message, nil)
l.logEvent(objectMeta, v1alpha1.AppProjectSchemaGroupVersionKind, info, message, nil, nil)
}

func NewAuditLogger(ns string, kIf kubernetes.Interface, component string) *AuditLogger {
Expand Down
2 changes: 1 addition & 1 deletion util/argo/audit_logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func TestLogAppEvent(t *testing.T) {
}

output := captureLogEntries(func() {
logger.LogAppEvent(&app, ei, "This is a test message", "")
logger.LogAppEvent(&app, ei, "This is a test message", "", nil)
})

assert.Contains(t, output, "level=info")
Expand Down
18 changes: 18 additions & 0 deletions util/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,8 @@ const (
resourceIgnoreResourceUpdatesEnabledKey = "resource.ignoreResourceUpdatesEnabled"
// resourceCustomLabelKey is the key to a custom label to show in node info, if present
resourceCustomLabelsKey = "resource.customLabels"
// resourceEventLabelKeys is the key to labels to be added onto Application k8s events if present on an Application or it's AppProject
resourceEventLabelKeys = "resource.eventLabelKeys"
// kustomizeBuildOptionsKey is a string of kustomize build parameters
kustomizeBuildOptionsKey = "kustomize.buildOptions"
// kustomizeVersionKeyPrefix is a kustomize version key prefix
Expand Down Expand Up @@ -2221,3 +2223,19 @@ func (mgr *SettingsManager) GetResourceCustomLabels() ([]string, error) {
}
return []string{}, nil
}

func (mgr *SettingsManager) GetEventLabelKeys() []string {
labelKeys := []string{}
argoCDCM, err := mgr.getConfigMap()
if err != nil {
log.Error(fmt.Errorf("failed getting configmap: %v", err))
return labelKeys
}
if value, ok := argoCDCM.Data[resourceEventLabelKeys]; ok {
if value != "" {
value = strings.ReplaceAll(value, " ", "")
labelKeys = strings.Split(value, ",")
}
}
return labelKeys
}
Loading

0 comments on commit 8c63b0c

Please sign in to comment.