Skip to content

Commit

Permalink
feat: add argocd application info to promotion event (#2789)
Browse files Browse the repository at this point in the history
Signed-off-by: xiaopeng <hanxiaop8@outlook.com>
  • Loading branch information
hanxiaop authored Oct 29, 2024
1 parent fedf4aa commit f9afcdb
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 66 deletions.
62 changes: 1 addition & 61 deletions api/v1alpha1/event.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package v1alpha1

import (
"context"
"encoding/json"
"time"

authnv1 "k8s.io/api/authentication/v1"

"github.com/akuity/kargo/internal/api/user"
"github.com/akuity/kargo/internal/logging"
)

const (
Expand All @@ -27,6 +24,7 @@ const (
AnnotationKeyEventVerificationPending = "event.kargo.akuity.io/verification-pending"
AnnotationKeyEventVerificationStartTime = "event.kargo.akuity.io/verification-start-time"
AnnotationKeyEventVerificationFinishTime = "event.kargo.akuity.io/verification-finish-time"
AnnotationKeyEventApplications = "event.kargo.akuity.io/applications"
)

const (
Expand Down Expand Up @@ -106,61 +104,3 @@ func NewFreightApprovedEventAnnotations(actor string, f *Freight, stageName stri
}
return annotations
}

// NewPromotionEventAnnotations returns annotations for a Promotion related event.
// It may skip some fields when error occurred during serialization, to record event with best-effort.
func NewPromotionEventAnnotations(
ctx context.Context,
actor string,
p *Promotion,
f *Freight,
) map[string]string {
logger := logging.LoggerFromContext(ctx)

annotations := map[string]string{
AnnotationKeyEventProject: p.GetNamespace(),
AnnotationKeyEventPromotionName: p.GetName(),
AnnotationKeyEventFreightName: p.Spec.Freight,
AnnotationKeyEventStageName: p.Spec.Stage,
AnnotationKeyEventPromotionCreateTime: p.GetCreationTimestamp().Format(time.RFC3339),
}

if actor != "" {
annotations[AnnotationKeyEventActor] = actor
}
// All Promotion-related events are emitted after the promotion was created.
// Therefore, if the promotion knows who triggered it, set them as an actor.
if promoteActor, ok := p.Annotations[AnnotationKeyCreateActor]; ok {
annotations[AnnotationKeyEventActor] = promoteActor
}

if f != nil {
annotations[AnnotationKeyEventFreightCreateTime] = f.CreationTimestamp.Format(time.RFC3339)
annotations[AnnotationKeyEventFreightAlias] = f.Alias
if len(f.Commits) > 0 {
data, err := json.Marshal(f.Commits)
if err != nil {
logger.Error(err, "marshal freight commits in JSON")
} else {
annotations[AnnotationKeyEventFreightCommits] = string(data)
}
}
if len(f.Images) > 0 {
data, err := json.Marshal(f.Images)
if err != nil {
logger.Error(err, "marshal freight images in JSON")
} else {
annotations[AnnotationKeyEventFreightImages] = string(data)
}
}
if len(f.Charts) > 0 {
data, err := json.Marshal(f.Charts)
if err != nil {
logger.Error(err, "marshal freight charts in JSON")
} else {
annotations[AnnotationKeyEventFreightCharts] = string(data)
}
}
}
return annotations
}
3 changes: 2 additions & 1 deletion internal/api/promote_to_stage_v1alpha1.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

kargoapi "github.com/akuity/kargo/api/v1alpha1"
"github.com/akuity/kargo/internal/api/user"
"github.com/akuity/kargo/internal/event"
"github.com/akuity/kargo/internal/kargo"
svcv1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1"
)
Expand Down Expand Up @@ -138,7 +139,7 @@ func (s *server) recordPromotionCreatedEvent(

s.recorder.AnnotatedEventf(
p,
kargoapi.NewPromotionEventAnnotations(ctx, actor, p, f),
event.NewPromotionAnnotations(ctx, actor, p, f),
corev1.EventTypeNormal,
kargoapi.EventReasonPromotionCreated,
msg,
Expand Down
5 changes: 3 additions & 2 deletions internal/controller/promotions/promotions.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/akuity/kargo/internal/controller"
argocd "github.com/akuity/kargo/internal/controller/argocd/api/v1alpha1"
"github.com/akuity/kargo/internal/directives"
"github.com/akuity/kargo/internal/event"
"github.com/akuity/kargo/internal/indexer"
"github.com/akuity/kargo/internal/kargo"
"github.com/akuity/kargo/internal/kubeclient"
Expand Down Expand Up @@ -380,7 +381,7 @@ func (r *reconciler) Reconcile(
msg += fmt.Sprintf(": %s", newStatus.Message)
}

eventAnnotations := kargoapi.NewPromotionEventAnnotations(ctx,
eventAnnotations := event.NewPromotionAnnotations(ctx,
kargoapi.FormatEventControllerActor(r.cfg.Name()),
promo, freight)

Expand Down Expand Up @@ -608,7 +609,7 @@ func (r *reconciler) terminatePromotion(
return err
}

eventMeta := kargoapi.NewPromotionEventAnnotations(ctx, "", promo, freight)
eventMeta := event.NewPromotionAnnotations(ctx, "", promo, freight)
eventMeta[kargoapi.AnnotationKeyEventActor] = actor

r.recorder.AnnotatedEventf(
Expand Down
3 changes: 2 additions & 1 deletion internal/controller/stages/stages.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
argocd "github.com/akuity/kargo/internal/controller/argocd/api/v1alpha1"
rollouts "github.com/akuity/kargo/internal/controller/rollouts/api/v1alpha1"
"github.com/akuity/kargo/internal/directives"
kargoEvent "github.com/akuity/kargo/internal/event"
"github.com/akuity/kargo/internal/indexer"
"github.com/akuity/kargo/internal/kargo"
"github.com/akuity/kargo/internal/kubeclient"
Expand Down Expand Up @@ -916,7 +917,7 @@ func (r *reconciler) syncNormalStage(

r.recorder.AnnotatedEventf(
&promo,
kargoapi.NewPromotionEventAnnotations(
kargoEvent.NewPromotionAnnotations(
ctx,
kargoapi.FormatEventControllerActor(r.cfg.Name()),
&promo,
Expand Down
103 changes: 103 additions & 0 deletions internal/event/promotion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package event

import (
"context"
"encoding/json"
"time"

"k8s.io/apimachinery/pkg/types"

kargoapi "github.com/akuity/kargo/api/v1alpha1"
libargocd "github.com/akuity/kargo/internal/argocd"
"github.com/akuity/kargo/internal/directives"
"github.com/akuity/kargo/internal/logging"
)

// NewPromotionAnnotations returns annotations for a Promotion related event.
// It may skip some fields when error occurred during serialization, to record event with best-effort.
func NewPromotionAnnotations(
ctx context.Context,
actor string,
p *kargoapi.Promotion,
f *kargoapi.Freight,
) map[string]string {
logger := logging.LoggerFromContext(ctx)

annotations := map[string]string{
kargoapi.AnnotationKeyEventProject: p.GetNamespace(),
kargoapi.AnnotationKeyEventPromotionName: p.GetName(),
kargoapi.AnnotationKeyEventFreightName: p.Spec.Freight,
kargoapi.AnnotationKeyEventStageName: p.Spec.Stage,
kargoapi.AnnotationKeyEventPromotionCreateTime: p.GetCreationTimestamp().Format(time.RFC3339),
}

if actor != "" {
annotations[kargoapi.AnnotationKeyEventActor] = actor
}
// All Promotion-related events are emitted after the promotion was created.
// Therefore, if the promotion knows who triggered it, set them as an actor.
if promoteActor, ok := p.Annotations[kargoapi.AnnotationKeyCreateActor]; ok {
annotations[kargoapi.AnnotationKeyEventActor] = promoteActor
}

if f != nil {
annotations[kargoapi.AnnotationKeyEventFreightCreateTime] = f.CreationTimestamp.Format(time.RFC3339)
annotations[kargoapi.AnnotationKeyEventFreightAlias] = f.Alias
if len(f.Commits) > 0 {
data, err := json.Marshal(f.Commits)
if err != nil {
logger.Error(err, "marshal freight commits in JSON")
} else {
annotations[kargoapi.AnnotationKeyEventFreightCommits] = string(data)
}
}
if len(f.Images) > 0 {
data, err := json.Marshal(f.Images)
if err != nil {
logger.Error(err, "marshal freight images in JSON")
} else {
annotations[kargoapi.AnnotationKeyEventFreightImages] = string(data)
}
}
if len(f.Charts) > 0 {
data, err := json.Marshal(f.Charts)
if err != nil {
logger.Error(err, "marshal freight charts in JSON")
} else {
annotations[kargoapi.AnnotationKeyEventFreightCharts] = string(data)
}
}
}

var apps []types.NamespacedName
for _, step := range p.Spec.Steps {
if step.Uses != "argocd-update" || step.Config == nil {
continue
}
var cfg directives.ArgoCDUpdateConfig
if err := json.Unmarshal(step.Config.Raw, &cfg); err != nil {
logger.Error(err, "unmarshal ArgoCD update config")
continue
}
for _, app := range cfg.Apps {
namespacedName := types.NamespacedName{
Namespace: app.Namespace,
Name: app.Name,
}
if namespacedName.Namespace == "" {
namespacedName.Namespace = libargocd.Namespace()
}
apps = append(apps, namespacedName)
}
}
if len(apps) > 0 {
data, err := json.Marshal(apps)
if err != nil {
logger.Error(err, "marshal ArgoCD apps in JSON")
} else {
annotations[kargoapi.AnnotationKeyEventApplications] = string(data)
}
}

return annotations
}
109 changes: 109 additions & 0 deletions internal/event/promotion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package event

import (
"context"
"encoding/json"
"testing"
"time"

"github.com/stretchr/testify/require"
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"

"github.com/akuity/kargo/api/v1alpha1"
)

func TestNewPromotionAnnotations(t *testing.T) {
testCases := map[string]struct {
actor string
promotion *v1alpha1.Promotion
freight *v1alpha1.Freight
expected map[string]string
}{
"promotion with freight and argocd apps": {
actor: "test-user",
promotion: &v1alpha1.Promotion{
ObjectMeta: metav1.ObjectMeta{
Name: "test-promotion",
Namespace: "test-namespace",
CreationTimestamp: metav1.Time{
Time: time.Date(2024, 10, 22, 0, 0, 0, 0, time.UTC),
},
Annotations: map[string]string{
v1alpha1.AnnotationKeyCreateActor: "promotion-creator",
},
},
Spec: v1alpha1.PromotionSpec{
Freight: "test-freight",
Stage: "test-stage",
Steps: []v1alpha1.PromotionStep{
{
Uses: "argocd-update",
Config: &v1.JSON{Raw: []byte(`{
"apps": [
{
"name": "test-app-1"
},
{
"name": "test-app-2",
"namespace": "test-namespace"
}
]
}`)},
},
},
},
},
freight: &v1alpha1.Freight{
ObjectMeta: metav1.ObjectMeta{
CreationTimestamp: metav1.Time{
Time: time.Date(2024, 10, 22, 0, 0, 0, 0, time.UTC),
},
},
Alias: "test-alias",
Commits: []v1alpha1.GitCommit{{Tag: "test-tag"}},
Images: []v1alpha1.Image{{Tag: "test-tag"}},
Charts: []v1alpha1.Chart{{Name: "test-chart"}},
},
expected: map[string]string{
v1alpha1.AnnotationKeyEventProject: "test-namespace",
v1alpha1.AnnotationKeyEventPromotionName: "test-promotion",
v1alpha1.AnnotationKeyEventFreightName: "test-freight",
v1alpha1.AnnotationKeyEventStageName: "test-stage",
v1alpha1.AnnotationKeyEventPromotionCreateTime: "2024-10-22T00:00:00Z",
v1alpha1.AnnotationKeyEventActor: "promotion-creator",
v1alpha1.AnnotationKeyEventFreightCreateTime: "2024-10-22T00:00:00Z",
v1alpha1.AnnotationKeyEventFreightAlias: "test-alias",
v1alpha1.AnnotationKeyEventFreightCommits: `[{"tag":"test-tag"}]`,
v1alpha1.AnnotationKeyEventFreightImages: `[{"tag":"test-tag"}]`,
v1alpha1.AnnotationKeyEventFreightCharts: `[{"name":"test-chart"}]`,
v1alpha1.AnnotationKeyEventApplications: `[{"name":"test-app-1","namespace":"argocd"},` +
`{"name":"test-app-2","namespace":"test-namespace"}]`,
},
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
result := NewPromotionAnnotations(context.TODO(), tc.actor, tc.promotion, tc.freight)
require.Equal(t, len(tc.expected), len(result), "Number of annotations doesn't match")
for key, expectedValue := range tc.expected {
if key == v1alpha1.AnnotationKeyEventApplications {
expectedAppsJSON := tc.expected[v1alpha1.AnnotationKeyEventApplications]
actualAppsJSON := result[v1alpha1.AnnotationKeyEventApplications]
var expectedApps, actualApps []types.NamespacedName
err := json.Unmarshal([]byte(expectedAppsJSON), &expectedApps)
require.NoError(t, err, "Failed to unmarshal expected applications")
err = json.Unmarshal([]byte(actualAppsJSON), &actualApps)
require.NoError(t, err, "Failed to unmarshal actual applications")
require.Equal(t, expectedApps, actualApps, "Applications mismatch")
continue
}
actualValue, exists := result[key]
require.True(t, exists, "Expected annotation %s not found", key)
require.Equal(t, expectedValue, actualValue, "Annotation %s value mismatch", key)
}
})
}
}
3 changes: 2 additions & 1 deletion internal/webhook/promotion/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

kargoapi "github.com/akuity/kargo/api/v1alpha1"
kargoEvent "github.com/akuity/kargo/internal/event"
libEvent "github.com/akuity/kargo/internal/kubernetes/event"
"github.com/akuity/kargo/internal/logging"
libWebhook "github.com/akuity/kargo/internal/webhook"
Expand Down Expand Up @@ -372,7 +373,7 @@ func (w *webhook) recordPromotionCreatedEvent(
actor := kargoapi.FormatEventKubernetesUserActor(req.UserInfo)
w.recorder.AnnotatedEventf(
p,
kargoapi.NewPromotionEventAnnotations(ctx, actor, p, f),
kargoEvent.NewPromotionAnnotations(ctx, actor, p, f),
corev1.EventTypeNormal,
kargoapi.EventReasonPromotionCreated,
"Promotion created for Stage %q by %q",
Expand Down

0 comments on commit f9afcdb

Please sign in to comment.