Skip to content

Commit

Permalink
feat(api/cli): allow aborting AnalysisRun
Browse files Browse the repository at this point in the history
Signed-off-by: Hidde Beydals <hidde@hhh.computer>
  • Loading branch information
hiddeco committed Mar 14, 2024
1 parent a59ad84 commit 6c3309e
Show file tree
Hide file tree
Showing 11 changed files with 1,135 additions and 615 deletions.
13 changes: 12 additions & 1 deletion api/service/v1alpha1/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ service KargoService {
rpc UpdateFreightAlias(UpdateFreightAliasRequest) returns (UpdateFreightAliasResponse);

/* Verification APIs */

rpc Reverify(ReverifyRequest) returns (ReverifyResponse);
rpc AbortVerification(AbortVerificationRequest) returns (AbortVerificationResponse);

/* Warehouse APIs */

Expand Down Expand Up @@ -377,13 +379,22 @@ message UpdateFreightAliasResponse {

message ReverifyRequest {
string project = 1;
string name = 2;
string stage = 2;
}

message ReverifyResponse {
/* explicitly empty */
}

message AbortVerificationRequest {
string project = 1;
string stage = 2;
}

message AbortVerificationResponse {
/* explicitly empty */
}

message ListWarehousesRequest {
string project = 1;
}
Expand Down
48 changes: 48 additions & 0 deletions api/v1alpha1/stage_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,54 @@ func ClearStageReverify(
return clearObjectAnnotation(ctx, c, &newStage, AnnotationKeyReverify)
}

// AbortStageFreightVerification forces aborting the verification of the
// Freight associated with a Stage by setting an AnnotationKeyAbort
// annotation on the Stage, causing the controller to abort the verification.
// The annotation value is the identifier of the existing VerificationInfo for
// the Stage.
func AbortStageFreightVerification(
ctx context.Context,
c client.Client,
namespacedName types.NamespacedName,
) error {
stage, err := GetStage(ctx, c, namespacedName)
if err != nil || stage == nil {
if stage == nil {
err = fmt.Errorf("Stage %q in namespace %q not found", namespacedName.Name, namespacedName.Namespace)
}
return err
}

curFreight := stage.Status.CurrentFreight
if curFreight == nil {
return errors.New("stage has no current freight")
}
if curFreight.VerificationInfo == nil {
return errors.New("stage has no existing verification info")
}
if stage.Status.CurrentFreight.VerificationInfo.Phase.IsTerminal() {
// The verification is already in a terminal phase, so we can skip the
// abort request.
return nil
}
if curFreight.VerificationInfo.ID == "" {
return fmt.Errorf("stage verification info has no ID")
}

patchBytes := []byte(
fmt.Sprintf(
`{"metadata":{"annotations":{"%s":"%s"}}}`,
AnnotationKeyAbort,
stage.Status.CurrentFreight.VerificationInfo.ID,
),
)
patch := client.RawPatch(types.MergePatchType, patchBytes)
if err := c.Patch(ctx, stage, patch); err != nil {
return fmt.Errorf("patch annotation: %w", err)
}
return nil
}

// ClearStageAbort is called by the Stage controller to clear the
// AnnotationKeyAbort annotation on the Stage (if present). A client (e.g.
// UI) who requested an abort of the Stage verification, can wait
Expand Down
138 changes: 138 additions & 0 deletions api/v1alpha1/stage_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,144 @@ func TestClearStageReverify(t *testing.T) {
})
}

func TestAbortStageFreightVerification(t *testing.T) {
scheme := k8sruntime.NewScheme()
require.NoError(t, SchemeBuilder.AddToScheme(scheme))

t.Run("not found", func(t *testing.T) {
c := fake.NewClientBuilder().WithScheme(scheme).Build()

err := AbortStageFreightVerification(context.TODO(), c, types.NamespacedName{
Namespace: "fake-namespace",
Name: "fake-stage",
})
require.ErrorContains(t, err, "not found")
})

t.Run("missing current freight", func(t *testing.T) {
c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
&Stage{
ObjectMeta: metav1.ObjectMeta{
Name: "fake-stage",
Namespace: "fake-namespace",
},
},
).Build()

err := AbortStageFreightVerification(context.TODO(), c, types.NamespacedName{
Namespace: "fake-namespace",
Name: "fake-stage",
})
require.ErrorContains(t, err, "stage has no current freight")
})

t.Run("missing verification info", func(t *testing.T) {
c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
&Stage{
ObjectMeta: metav1.ObjectMeta{
Name: "fake-stage",
Namespace: "fake-namespace",
},
Status: StageStatus{
CurrentFreight: &FreightReference{},
},
},
).Build()

err := AbortStageFreightVerification(context.TODO(), c, types.NamespacedName{
Namespace: "fake-namespace",
Name: "fake-stage",
})
require.ErrorContains(t, err, "stage has no existing verification info")
})

t.Run("missing verification info ID", func(t *testing.T) {
c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
&Stage{
ObjectMeta: metav1.ObjectMeta{
Name: "fake-stage",
Namespace: "fake-namespace",
},
Status: StageStatus{
CurrentFreight: &FreightReference{
VerificationInfo: &VerificationInfo{},
},
},
},
).Build()

err := AbortStageFreightVerification(context.TODO(), c, types.NamespacedName{
Namespace: "fake-namespace",
Name: "fake-stage",
})
require.ErrorContains(t, err, "stage verification info has no ID")
})

t.Run("verification in terminal phase", func(t *testing.T) {
c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
&Stage{
ObjectMeta: metav1.ObjectMeta{
Name: "fake-stage",
Namespace: "fake-namespace",
},
Status: StageStatus{
CurrentFreight: &FreightReference{
VerificationInfo: &VerificationInfo{
ID: "fake-id",
Phase: VerificationPhaseError,
},
},
},
},
).Build()

err := AbortStageFreightVerification(context.TODO(), c, types.NamespacedName{
Namespace: "fake-namespace",
Name: "fake-stage",
})
require.NoError(t, err)

stage, err := GetStage(context.TODO(), c, types.NamespacedName{
Namespace: "fake-namespace",
Name: "fake-stage",
})
require.NoError(t, err)
_, ok := stage.Annotations[AnnotationKeyAbort]
require.False(t, ok)
})

t.Run("success", func(t *testing.T) {
c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
&Stage{
ObjectMeta: metav1.ObjectMeta{
Name: "fake-stage",
Namespace: "fake-namespace",
},
Status: StageStatus{
CurrentFreight: &FreightReference{
VerificationInfo: &VerificationInfo{
ID: "fake-id",
},
},
},
},
).Build()

err := AbortStageFreightVerification(context.TODO(), c, types.NamespacedName{
Namespace: "fake-namespace",
Name: "fake-stage",
})
require.NoError(t, err)

stage, err := GetStage(context.TODO(), c, types.NamespacedName{
Namespace: "fake-namespace",
Name: "fake-stage",
})
require.NoError(t, err)
require.Equal(t, "fake-id", stage.Annotations[AnnotationKeyAbort])
})
}

func TestClearStageAbort(t *testing.T) {
scheme := k8sruntime.NewScheme()
require.NoError(t, SchemeBuilder.AddToScheme(scheme))
Expand Down
38 changes: 38 additions & 0 deletions internal/api/abort_verification_v1alpha1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package api

import (
"context"

"connectrpc.com/connect"
"sigs.k8s.io/controller-runtime/pkg/client"

kargoapi "github.com/akuity/kargo/api/v1alpha1"
svcv1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1"
)

func (s *server) AbortVerification(
ctx context.Context,
req *connect.Request[svcv1alpha1.AbortVerificationRequest],
) (*connect.Response[svcv1alpha1.AbortVerificationResponse], error) {
project := req.Msg.GetProject()
if err := validateFieldNotEmpty("project", project); err != nil {
return nil, err
}
stage := req.Msg.GetStage()
if err := validateFieldNotEmpty("stage", stage); err != nil {
return nil, err
}

if err := s.validateProjectExists(ctx, project); err != nil {
return nil, err
}

objKey := client.ObjectKey{
Namespace: project,
Name: stage,
}
if err := kargoapi.AbortStageFreightVerification(ctx, s.client, objKey); err != nil {
return nil, err
}
return connect.NewResponse(&svcv1alpha1.AbortVerificationResponse{}), nil
}
6 changes: 3 additions & 3 deletions internal/api/reverify_v1alpha1.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ func (s *server) Reverify(
if err := validateFieldNotEmpty("project", project); err != nil {
return nil, err
}
name := req.Msg.GetName()
if err := validateFieldNotEmpty("name", name); err != nil {
stage := req.Msg.GetStage()
if err := validateFieldNotEmpty("stage", stage); err != nil {
return nil, err
}

Expand All @@ -29,7 +29,7 @@ func (s *server) Reverify(

objKey := client.ObjectKey{
Namespace: project,
Name: name,
Name: stage,
}
if err := kargoapi.ReverifyStageFreight(ctx, s.client, objKey); err != nil {
return nil, err
Expand Down
37 changes: 31 additions & 6 deletions internal/cli/cmd/verify/stage.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type verifyStageOptions struct {

Project string
Name string
Abort bool
}

func newVerifyStageCommand(cfg config.CLIConfig) *cobra.Command {
Expand All @@ -29,16 +30,23 @@ func newVerifyStageCommand(cfg config.CLIConfig) *cobra.Command {
}

cmd := &cobra.Command{
Use: "stage [--project=project] (NAME)",
Short: "Run the verification of the stage's current freight",
Use: "stage [--project=project] (NAME) [--abort]",
Short: "(Re)run or abort the verification of the stage's current freight",
Args: option.ExactArgs(1),
Example: `
# Run the verification of the stage's current freight
# Rerun the verification of the stage's current freight
kargo verify stage --project=my-project my-stage
# Run the verification of a stage in the default project
# Rerun the verification of a stage in the default project
kargo config set-project my-project
kargo verify stage my-stage
# Abort the verification of a stage's current freight
kargo verify stage --project=my-project my-stage --abort
# Abort the verification of a stage in the default project
kargo config set-project my-project
kargo verify stage my-stage --abort
`,
RunE: func(cmd *cobra.Command, args []string) error {
cmdOpts.complete(args)
Expand All @@ -65,6 +73,7 @@ func (o *verifyStageOptions) addFlags(cmd *cobra.Command) {
cmd.Flags(), &o.Project, o.Config.Project,
"The project the stage belongs to. If not set, the default project will be used.",
)
cmd.Flags().BoolVar(&o.Abort, "abort", false, "If set, the verification will be aborted.")
}

// complete sets the options from the command arguments.
Expand Down Expand Up @@ -94,16 +103,32 @@ func (o *verifyStageOptions) run(ctx context.Context) error {
return fmt.Errorf("get client from config: %w", err)
}

if o.Abort {
if _, err := kargoSvcCli.AbortVerification(
ctx,
connect.NewRequest(
&v1alpha1.AbortVerificationRequest{
Project: o.Project,
Stage: o.Name,
},
),
); err != nil {
return fmt.Errorf("abort verification: %w", err)
}
return nil
}

if _, err := kargoSvcCli.Reverify(
ctx,
connect.NewRequest(
&v1alpha1.ReverifyRequest{
Project: o.Project,
Name: o.Name,
Stage: o.Name,
},
),
); err != nil {
return fmt.Errorf("verify stage: %w", err)
return fmt.Errorf("reverify stage: %w", err)
}

return nil
}
Loading

0 comments on commit 6c3309e

Please sign in to comment.