Skip to content

Commit

Permalink
feat: Add labels on Application's k8s events (argoproj#11381) (argopr…
Browse files Browse the repository at this point in the history
…oj#18160)

* Add labels on Application's k8s events

Signed-off-by: Siddhesh Ghadi <[email protected]>

* Fix typo

Signed-off-by: Siddhesh Ghadi <[email protected]>

* Add new cm keys & doc

Signed-off-by: Siddhesh Ghadi <[email protected]>

* Fix typo

Signed-off-by: Siddhesh Ghadi <[email protected]>

* correct rebase changes

Signed-off-by: Siddhesh Ghadi <[email protected]>

* Fix linting

Signed-off-by: Siddhesh Ghadi <[email protected]>

---------

Signed-off-by: Siddhesh Ghadi <[email protected]>
  • Loading branch information
svghadi authored and ggjulio committed Jul 21, 2024
1 parent 8f5c497 commit b68ce4f
Show file tree
Hide file tree
Showing 11 changed files with 344 additions and 14 deletions.
16 changes: 10 additions & 6 deletions controller/appcontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -937,7 +937,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 @@ -1440,7 +1440,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 @@ -1772,11 +1772,11 @@ func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, new
logCtx := getAppLog(orig)
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 @@ -1934,8 +1934,7 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *
target = desiredCommitSHA
}
message := fmt.Sprintf("Initiated automated sync to '%s'", target)

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 @@ -2275,4 +2274,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, eventInfo, message, "", eventLabels)
}

type ClusterFilterFunction func(c *appv1.Cluster, distributionFunction sharding.DistributionFunction) bool
10 changes: 10 additions & 0 deletions docs/operator-manual/argocd-cm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,16 @@ 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 to Kubernetes events generated for Applications.
# The keys are compared against the Application and its AppProject. If matched,
# the corresponding labels are added to the generated event.
# In case of a conflict between labels on the Application and AppProject,
# the Application label values are prioritized and added to the event. Supports wildcards.
resource.includeEventLabelKeys: team,env*
# An optional comma-separated list of metadata.labels keys to exclude from Kubernetes events generated for Applications. Supports wildcards.
resource.excludeEventLabelKeys: environment,bu


resource.compareoptions: |
# if ignoreAggregatedRoles set to true then differences caused by aggregated roles in RBAC resources are ignored.
ignoreAggregatedRoles: true
Expand Down
16 changes: 16 additions & 0 deletions docs/operator-manual/declarative-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,22 @@ data:

Custom Labels configured with `resource.customLabels` (comma separated string) will be displayed in the UI (for any resource that defines them).

## Labels on Application Events

An optional comma-separated list of `metadata.labels` keys can be configured with `resource.includeEventLabelKeys` to add to Kubernetes events generated for Argo CD Applications. When events are generated for Applications containing the specified labels, the controller adds the matching labels to the event. This establishes an easy link between the event and the application, allowing for filtering using labels. In case of conflict between labels on the Application and AppProject, the Application label values are prioritized and added to the event.

```yaml
resource.includeEventLabelKeys: team,env*
```

To exclude certain labels from events, use the `resource.excludeEventLabelKeys` key, which takes a comma-separated list of `metadata.labels` keys.

```yaml
resource.excludeEventLabelKeys: environment,bu
```

Both `resource.includeEventLabelKeys` and `resource.excludeEventLabelKeys` support wildcards.

## SSO & RBAC

* SSO configuration details: [SSO](./user-management/index.md)
Expand Down
3 changes: 2 additions & 1 deletion server/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -2305,7 +2305,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
64 changes: 64 additions & 0 deletions test/e2e/app_k8s_events_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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.includeEventLabelKeys keys set in argocd-cm
func TestLabelsOnAppK8sEvents(t *testing.T) {
expectedLabels := map[string]string{"app": "test", "environment": "dev"}

Given(t).
Timeout(60).
Path("two-nice-pods").
When().
SetParamInSettingConfigMap("resource.includeEventLabelKeys", "app,team,env*").
SetParamInSettingConfigMap("resource.excludeEventLabelKeys", "team").
CreateApp("--label=app=test", "--label=environment=dev", "--label=team=A", "--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.includeEventLabelKeys 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=environment=dev", "--label=team=A", "--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)
}
})
}
47 changes: 47 additions & 0 deletions util/argo/argo.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
applicationsv1 "github.com/argoproj/argo-cd/v2/pkg/client/listers/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
"github.com/argoproj/argo-cd/v2/util/db"
"github.com/argoproj/argo-cd/v2/util/glob"
"github.com/argoproj/argo-cd/v2/util/io"
"github.com/argoproj/argo-cd/v2/util/settings"
)
Expand Down Expand Up @@ -1102,3 +1103,49 @@ func IsValidContainerName(name string) bool {
validationErrors := apimachineryvalidation.NameIsDNSLabel(name, false)
return len(validationErrors) == 0
}

// GetAppEventLabels returns a map of labels to add to a K8s event.
// The Application and its AppProject labels are compared against the `resource.includeEventLabelKeys` key in argocd-cm.
// If matched, the corresponding labels are returned to be added to the generated event. In case of a conflict
// between labels on the Application and AppProject, the Application label values are prioritized and added to the event.
// Furthermore, labels specified in `resource.excludeEventLabelKeys` in argocd-cm are removed from the event labels, if they were included.
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)

// Get all app & app-project labels
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)
}

// Filter out event labels to include
inKeys := settingsManager.GetIncludeEventLabelKeys()
for k, v := range labels {
found := glob.MatchStringInList(inKeys, k, false)
if found {
eventLabels[k] = v
}
}

// Remove excluded event labels
exKeys := settingsManager.GetExcludeEventLabelKeys()
for k := range eventLabels {
found := glob.MatchStringInList(exKeys, k, false)
if found {
delete(eventLabels, k)
}
}

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

func TestGetAppEventLabels(t *testing.T) {
tests := []struct {
name string
cmInEventLabelKeys string
cmExEventLabelKeys string
appLabels map[string]string
projLabels map[string]string
expectedEventLabels map[string]string
}{
{
name: "no label keys in cm - no event labels",
cmInEventLabelKeys: "",
appLabels: map[string]string{"team": "A", "tier": "frontend"},
projLabels: map[string]string{"environment": "dev"},
expectedEventLabels: nil,
},
{
name: "label keys in cm, no labels on app & proj - no event labels",
cmInEventLabelKeys: "team, environment",
appLabels: nil,
projLabels: nil,
expectedEventLabels: nil,
},
{
name: "labels on app, no labels on proj - event labels matched on app only",
cmInEventLabelKeys: "team, environment",
appLabels: map[string]string{"team": "A", "tier": "frontend"},
projLabels: nil,
expectedEventLabels: map[string]string{"team": "A"},
},
{
name: "no labels on app, labels on proj - event labels matched on proj only",
cmInEventLabelKeys: "team, environment",
appLabels: nil,
projLabels: map[string]string{"environment": "dev"},
expectedEventLabels: map[string]string{"environment": "dev"},
},
{
name: "labels on app & proj with conflicts - event labels matched on both app & proj and app labels prioritized on conflict",
cmInEventLabelKeys: "team, environment",
appLabels: map[string]string{"team": "A", "environment": "stage", "tier": "frontend"},
projLabels: map[string]string{"environment": "dev"},
expectedEventLabels: map[string]string{"team": "A", "environment": "stage"},
},
{
name: "wildcard support - matched all labels",
cmInEventLabelKeys: "*",
appLabels: map[string]string{"team": "A", "tier": "frontend"},
projLabels: map[string]string{"environment": "dev"},
expectedEventLabels: map[string]string{"team": "A", "tier": "frontend", "environment": "dev"},
},
{
name: "exclude event labels",
cmInEventLabelKeys: "example.com/team,tier,env*",
cmExEventLabelKeys: "tie*",
appLabels: map[string]string{"example.com/team": "A", "tier": "frontend"},
projLabels: map[string]string{"environment": "dev"},
expectedEventLabels: map[string]string{"example.com/team": "A", "environment": "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.includeEventLabelKeys": tt.cmInEventLabelKeys,
"resource.excludeEventLabelKeys": tt.cmExEventLabelKeys,
},
}

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(tt.expectedEventLabels), len(eventLabels))
for ek, ev := range tt.expectedEventLabels {
v, found := eventLabels[ek]
assert.True(t, found)
assert.Equal(t, ev, v)
}
})
}
}
Loading

0 comments on commit b68ce4f

Please sign in to comment.