Skip to content

Commit

Permalink
feat: set promotion actor to the annotation (#1872)
Browse files Browse the repository at this point in the history
Signed-off-by: Sunghoon Kang <[email protected]>
  • Loading branch information
Sunghoon Kang authored Apr 19, 2024
1 parent 83a3b3c commit fe958c8
Show file tree
Hide file tree
Showing 9 changed files with 84 additions and 8 deletions.
4 changes: 4 additions & 0 deletions api/v1alpha1/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package v1alpha1
import "encoding/json"

const (
// AnnotationKeyCreateActor is an annotation key that can be injected to a resource
// by the Kargo controlplane to indicate the actor that created the resource.
AnnotationKeyCreateActor = "kargo.akuity.io/create-actor"

// AnnotationKeyRefresh is an annotation key that can be set on a resource
// to trigger a refresh of the resource by the controller. The value of the
// annotation is interpreted as a token, and any change to the value of the
Expand Down
7 changes: 7 additions & 0 deletions api/v1alpha1/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,16 @@ func NewPromotionEventAnnotations(
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
Expand Down
2 changes: 1 addition & 1 deletion internal/api/promote_to_stage_subscribers_v1alpha1.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func (s *server) PromoteToStageSubscribers(
promoteErrs := make([]error, 0, len(subscribers))
createdPromos := make([]*kargoapi.Promotion, 0, len(subscribers))
for _, subscriber := range subscribers {
newPromo := kargo.NewPromotion(subscriber, freight.Name)
newPromo := kargo.NewPromotion(ctx, subscriber, freight.Name)
if subscriber.Spec.PromotionMechanisms == nil {
// Avoid creating a Promotion if the subscriber has no
// PromotionMechanisms, and is a "control flow" Stage.
Expand Down
2 changes: 1 addition & 1 deletion internal/api/promote_to_stage_v1alpha1.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func (s *server) PromoteToStage(
return nil, err
}

promotion := kargo.NewPromotion(*stage, freight.Name)
promotion := kargo.NewPromotion(ctx, *stage, freight.Name)
if err := s.createPromotionFn(ctx, &promotion); err != nil {
return nil, fmt.Errorf("create promotion: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/controller/stages/stages.go
Original file line number Diff line number Diff line change
Expand Up @@ -989,7 +989,7 @@ func (r *reconciler) syncNormalStage(

logger.Debug("auto-promotion will proceed")

promo := kargo.NewPromotion(*stage, latestFreight.Name)
promo := kargo.NewPromotion(ctx, *stage, latestFreight.Name)
if err :=
r.createPromotionFn(ctx, &promo); err != nil {
return status, fmt.Errorf(
Expand Down
19 changes: 16 additions & 3 deletions internal/kargo/kargo.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kargo

import (
"context"
"fmt"
"strings"

Expand All @@ -11,6 +12,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/predicate"

kargoapi "github.com/akuity/kargo/api/v1alpha1"
"github.com/akuity/kargo/internal/api/user"
)

const (
Expand All @@ -22,7 +24,11 @@ const (

// NewPromotion returns a new Promotion from a given stage and freight with our
// naming convention.
func NewPromotion(stage kargoapi.Stage, freight string) kargoapi.Promotion {
func NewPromotion(
ctx context.Context,
stage kargoapi.Stage,
freight string,
) kargoapi.Promotion {
shortHash := freight
if len(shortHash) > 7 {
shortHash = freight[0:7]
Expand All @@ -32,14 +38,21 @@ func NewPromotion(stage kargoapi.Stage, freight string) kargoapi.Promotion {
shortStageName = shortStageName[0:maxStageNamePrefixLength]
}

annotations := make(map[string]string, 1)
// Put actor information to track on the controller side
if u, ok := user.InfoFromContext(ctx); ok {
annotations[kargoapi.AnnotationKeyCreateActor] = kargoapi.FormatEventUserActor(u)
}

// ulid.Make() is pseudo-random, not crypto-random, but we don't care.
// We just want a unique ID that can be sorted lexicographically
promoName := strings.ToLower(fmt.Sprintf("%s.%s.%s", shortStageName, ulid.Make(), shortHash))

promotion := kargoapi.Promotion{
ObjectMeta: metav1.ObjectMeta{
Name: promoName,
Namespace: stage.Namespace,
Name: promoName,
Namespace: stage.Namespace,
Annotations: annotations,
},
Spec: &kargoapi.PromotionSpec{
Stage: stage.Name,
Expand Down
3 changes: 2 additions & 1 deletion internal/kargo/kargo_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kargo

import (
"context"
"strings"
"testing"

Expand Down Expand Up @@ -64,7 +65,7 @@ func TestNewPromotion(t *testing.T) {
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
promo := NewPromotion(tc.stage, tc.freight)
promo := NewPromotion(context.TODO(), tc.stage, tc.freight)
require.Equal(t, tc.freight, promo.Spec.Freight)
require.Equal(t, tc.stage.Name, promo.Spec.Stage)
require.Equal(t, tc.freight, promo.Spec.Freight)
Expand Down
40 changes: 39 additions & 1 deletion internal/webhook/promotion/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"

admissionv1 "k8s.io/api/admission/v1"
authzv1 "k8s.io/api/authorization/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -35,7 +36,8 @@ var (
)

type webhook struct {
client client.Client
client client.Client
decoder *admission.Decoder

recorder record.EventRecorder

Expand Down Expand Up @@ -85,6 +87,7 @@ func SetupWebhookWithManager(
w := newWebhook(
cfg,
mgr.GetClient(),
admission.NewDecoder(mgr.GetScheme()),
libEvent.NewRecorder(ctx, mgr.GetScheme(), mgr.GetClient(), "promotion-webhook"),
)
return ctrl.NewWebhookManagedBy(mgr).
Expand All @@ -97,10 +100,12 @@ func SetupWebhookWithManager(
func newWebhook(
cfg libWebhook.Config,
kubeClient client.Client,
decoder *admission.Decoder,
recorder record.EventRecorder,
) *webhook {
w := &webhook{
client: kubeClient,
decoder: decoder,
recorder: recorder,
}
w.getFreightFn = kargoapi.GetFreight
Expand All @@ -115,7 +120,40 @@ func newWebhook(
}

func (w *webhook) Default(ctx context.Context, obj runtime.Object) error {
req, err := w.admissionRequestFromContextFn(ctx)
if err != nil {
return fmt.Errorf("get admission request from context: %w", err)
}

promo := obj.(*kargoapi.Promotion) // nolint: forcetypeassert
var oldPromo *kargoapi.Promotion
// We need to decode old object manually since controller-runtime doesn't decode it for us.
if req.Operation == admissionv1.Update {
oldPromo = &kargoapi.Promotion{}
if err = w.decoder.DecodeRaw(req.OldObject, oldPromo); err != nil {
return fmt.Errorf("decode old object: %w", err)
}
}

if promo.Annotations == nil {
promo.Annotations = make(map[string]string, 1)
}
if req.Operation == admissionv1.Create {
// Set actor as an admission request's user info when the promotion is created
// to allow controllers to track who created it.
if !w.isRequestFromKargoControlplaneFn(req) {
promo.Annotations[kargoapi.AnnotationKeyCreateActor] =
kargoapi.FormatEventKubernetesUserActor(req.UserInfo)
}
} else if req.Operation == admissionv1.Update {
// Ensure actor annotation immutability
if oldActor, ok := oldPromo.Annotations[kargoapi.AnnotationKeyCreateActor]; ok {
promo.Annotations[kargoapi.AnnotationKeyCreateActor] = oldActor
} else {
delete(promo.Annotations, kargoapi.AnnotationKeyCreateActor)
}
}

stage, err := w.getStageFn(
ctx,
w.client,
Expand Down
13 changes: 13 additions & 0 deletions internal/webhook/promotion/webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func TestNewWebhook(t *testing.T) {
w := newWebhook(
libWebhook.Config{},
kubeClient,
admission.NewDecoder(kubeClient.Scheme()),
&fakeevent.EventRecorder{},
)
// Assert that all overridable behaviors were initialized to a default:
Expand All @@ -48,6 +49,9 @@ func TestDefault(t *testing.T) {
{
name: "error getting stage",
webhook: &webhook{
admissionRequestFromContextFn: func(context.Context) (admission.Request, error) {
return admission.Request{}, nil
},
getStageFn: func(
context.Context,
client.Client,
Expand All @@ -64,6 +68,9 @@ func TestDefault(t *testing.T) {
{
name: "stage not found",
webhook: &webhook{
admissionRequestFromContextFn: func(context.Context) (admission.Request, error) {
return admission.Request{}, nil
},
getStageFn: func(
context.Context,
client.Client,
Expand All @@ -80,6 +87,9 @@ func TestDefault(t *testing.T) {
{
name: "stage without promotion mechanisms",
webhook: &webhook{
admissionRequestFromContextFn: func(context.Context) (admission.Request, error) {
return admission.Request{}, nil
},
getStageFn: func(
context.Context,
client.Client,
Expand All @@ -98,6 +108,9 @@ func TestDefault(t *testing.T) {
{
name: "success",
webhook: &webhook{
admissionRequestFromContextFn: func(context.Context) (admission.Request, error) {
return admission.Request{}, nil
},
getStageFn: func(
context.Context,
client.Client,
Expand Down

0 comments on commit fe958c8

Please sign in to comment.