Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add labels on Application's k8s events (#11381) #18160

Merged
merged 6 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions controller/appcontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -939,7 +939,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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was wondering if we had a better context than the TODO() one here and elsewhere.

}
}
return
Expand Down Expand Up @@ -1442,7 +1442,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 @@ -1774,11 +1774,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 @@ -1936,8 +1936,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 @@ -2277,4 +2276,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 @@ -2307,7 +2307,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 @@ -1104,3 +1105,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