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: Adding ClusterAnalysisTemplate support for Stage verifications #2673

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ lint-go: install-golangci-lint

.PHONY: format-go
format-go:
golangci-lint run --fix
$(GOLANGCI_LINT) run --fix

.PHONY: lint-proto
lint-proto: install-buf
Expand Down
29 changes: 29 additions & 0 deletions api/service/v1alpha1/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ service KargoService {
rpc ListAnalysisTemplates(ListAnalysisTemplatesRequest) returns (ListAnalysisTemplatesResponse);
rpc GetAnalysisTemplate(GetAnalysisTemplateRequest) returns (GetAnalysisTemplateResponse);
rpc DeleteAnalysisTemplate(DeleteAnalysisTemplateRequest) returns (DeleteAnalysisTemplateResponse);
rpc ListClusterAnalysisTemplates(ListClusterAnalysisTemplatesRequest) returns (ListClusterAnalysisTemplatesResponse);
rpc GetClusterAnalysisTemplate(GetClusterAnalysisTemplateRequest) returns (GetClusterAnalysisTemplateResponse);
rpc DeleteClusterAnalysisTemplate(DeleteClusterAnalysisTemplateRequest) returns (DeleteClusterAnalysisTemplateResponse);
rpc GetAnalysisRun(GetAnalysisRunRequest) returns (GetAnalysisRunResponse);

rpc ListAnalysisTemplateConfigMaps(ListAnalysisTemplateConfigMapsRequest) returns (ListAnalysisTemplateConfigMapsResponse);
Expand Down Expand Up @@ -613,6 +616,24 @@ message GetAnalysisTemplateResponse {
}
}

message ListClusterAnalysisTemplatesRequest {}

message ListClusterAnalysisTemplatesResponse {
repeated github.com.akuity.kargo.internal.controller.rollouts.api.v1alpha1.ClusterAnalysisTemplate cluster_analysis_templates = 1 [json_name = "clusteranalysisTemplates"];
}

message GetClusterAnalysisTemplateRequest {
string name = 2;
RawFormat format = 3;
}

message GetClusterAnalysisTemplateResponse {
oneof result {
github.com.akuity.kargo.internal.controller.rollouts.api.v1alpha1.ClusterAnalysisTemplate cluster_analysis_template = 1 [json_name = "clusterAnalysisTemplate"];
bytes raw = 2;
}
}

message GetAnalysisRunRequest {
string namespace = 1;
string name = 2;
Expand All @@ -635,6 +656,14 @@ message DeleteAnalysisTemplateResponse {
/* explicitly empty */
}

message DeleteClusterAnalysisTemplateRequest {
string name = 2;
}

message DeleteClusterAnalysisTemplateResponse {
/* explicitly empty */
}

message ListProjectEventsRequest {
string project = 1;
}
Expand Down
485 changes: 258 additions & 227 deletions api/v1alpha1/generated.pb.go

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions api/v1alpha1/generated.proto

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions api/v1alpha1/stage_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,11 @@ type AnalysisTemplateReference struct {
//
// +kubebuilder:validation:Required
Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
// ClusterScope determines whether the template is an
// AnalysisTemplate or a ClusterAnalysisTemplate resource
//
// +kubebuilder:validation:Optional
ClusterScope bool `json:"clusterScope" protobuf:"varint,2,opt,name=clusterScope"`
}

// AnalysisRunMetadata contains optional metadata that should be applied to all
Expand Down
5 changes: 5 additions & 0 deletions charts/kargo/resources/crds/kargo.akuity.io_stages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ spec:
description: AnalysisTemplateReference is a reference to an
AnalysisTemplate.
properties:
clusterScope:
description: |-
ClusterScope determines whether the template is an
AnalysisTemplate or a ClusterAnalysisTemplate resource
type: boolean
name:
description: |-
Name is the name of the AnalysisTemplate in the same project/namespace as
Expand Down
1 change: 1 addition & 0 deletions charts/kargo/templates/api/cluster-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ rules:
- argoproj.io
resources:
- analysistemplates
- clusteranalysistemplates
verbs:
- "*"
{{- end }}
Expand Down
8 changes: 8 additions & 0 deletions charts/kargo/templates/controller/cluster-roles.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ rules:
- get
- list
- watch
- apiGroups:
- argoproj.io
resources:
- clusteranalysistemplates
verbs:
- get
- list
- watch
- apiGroups:
- argoproj.io
resources:
Expand Down
7 changes: 7 additions & 0 deletions charts/kargo/templates/users/cluster-roles.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ rules:
- analysistemplates
verbs:
- "*" # full access to analysistemplates
- apiGroups:
- argoproj.io
resources:
- clusteranalysistemplates
verbs:
- "*" # full access to clusteranalysistemplates
{{- end }}
---
apiVersion: rbac.authorization.k8s.io/v1
Expand Down Expand Up @@ -120,6 +126,7 @@ rules:
resources:
- analysisruns
- analysistemplates
- clusteranalysistemplates
verbs:
- get
- list
Expand Down
23 changes: 20 additions & 3 deletions docs/docs/15-concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,11 +291,11 @@ processes that should be executed after a `Promotion` has successfully deployed
healthy state.

Verification processes are defined through _references_ to one or more
[Argo Rollouts `AnalysisTemplate` resources](https://argoproj.github.io/argo-rollouts/features/analysis/)
that reside in the same `Project`/`Namespace` as the `Stage` resource.
[Argo Rollouts `AnalysisTemplate` or `ClusterAnalysisTemplate` resources](https://argoproj.github.io/argo-rollouts/features/analysis/).
`AnalysisTemplate` resources must reside in the same `Project`/`Namespace` as the `Stage` resource but `ClusterAnalysisTemplate` can be referenced by any `Stage`.

:::info
Argo Rollouts `AnalysisTemplate` resources (and the `AnalysisRun` resources that
Argo Rollouts `AnalysisTemplate` and `ClusterAnalysisTemplate` resources (and the `AnalysisRun` resources that
are spawned from them) were intentionally built to be re-usable in contexts
other than Argo Rollouts. Re-using this resource type to define verification
processes means those processes benefit from this rich and battle-tested feature
Expand Down Expand Up @@ -344,6 +344,23 @@ spec:
value: bar
```

To refer to a `ClusterAnalysisTemplate` that exists across all namespaces,
use the `clusterScope` option.

```yaml
apiVersion: kargo.akuity.io/v1alpha1
kind: Stage
metadata:
name: test
namespace: kargo-demo
spec:
# ...
verification:
analysisTemplates:
- name: kargo-demo
clusterScope: true
```

An `AnalysisTemplate` could be as simple as the following, which merely executes
a Kubernetes `Job` that is defined inline:

Expand Down
40 changes: 40 additions & 0 deletions internal/api/delete_clusteranalysistemplate_v1alpha1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package api

import (
"context"
"errors"
"fmt"

"connectrpc.com/connect"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/akuity/kargo/internal/controller/rollouts/api/v1alpha1"
svcv1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1"
)

func (s *server) DeleteClusterAnalysisTemplate(
ctx context.Context,
req *connect.Request[svcv1alpha1.DeleteClusterAnalysisTemplateRequest],
) (*connect.Response[svcv1alpha1.DeleteClusterAnalysisTemplateResponse], error) {
if !s.cfg.RolloutsIntegrationEnabled {
return nil, connect.NewError(
connect.CodeUnimplemented,
errors.New("Argo Rollouts integration is not enabled"),
)
}

name := req.Msg.GetName()
if err := validateFieldNotEmpty("name", name); err != nil {
return nil, err
}

if err := s.client.Delete(ctx, &v1alpha1.ClusterAnalysisTemplate{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
}); err != nil {
return nil, fmt.Errorf("delete ClusterAnalysisTemplate: %w", err)
}

return connect.NewResponse(&svcv1alpha1.DeleteClusterAnalysisTemplateResponse{}), nil
}
108 changes: 108 additions & 0 deletions internal/api/delete_clusteranalysistemplate_v1alpha1_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package api

import (
"context"
"fmt"
"testing"

"connectrpc.com/connect"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

"github.com/akuity/kargo/internal/api/config"
"github.com/akuity/kargo/internal/api/kubernetes"
"github.com/akuity/kargo/internal/api/validation"
rollouts "github.com/akuity/kargo/internal/controller/rollouts/api/v1alpha1"
svcv1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1"
)

func TestDeleteClusterAnalysisTemplate(t *testing.T) {
testCases := map[string]struct {
req *svcv1alpha1.DeleteClusterAnalysisTemplateRequest
rolloutsDisabled bool
errExpected bool
expectedCode connect.Code
}{
"empty name": {
req: &svcv1alpha1.DeleteClusterAnalysisTemplateRequest{
Name: "",
},
errExpected: true,
expectedCode: connect.CodeInvalidArgument,
},
"existing ClusterAnalysisTemplate": {
req: &svcv1alpha1.DeleteClusterAnalysisTemplateRequest{
Name: "test",
},
},
"non-existing ClusterAnalysisTemplate": {
req: &svcv1alpha1.DeleteClusterAnalysisTemplateRequest{
Name: "non-existing",
},
errExpected: true,
expectedCode: connect.CodeUnknown,
},
"Argo Rollouts integration is not enabled": {
req: &svcv1alpha1.DeleteClusterAnalysisTemplateRequest{
Name: "test",
},
rolloutsDisabled: true,
errExpected: true,
expectedCode: connect.CodeUnimplemented,
},
}
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()

ctx := context.Background()

cfg := config.ServerConfigFromEnv()
if testCase.rolloutsDisabled {
cfg.RolloutsIntegrationEnabled = false
}

client, err := kubernetes.NewClient(
ctx,
&rest.Config{},
kubernetes.ClientOptions{
SkipAuthorization: true,
NewInternalClient: func(
_ context.Context,
_ *rest.Config,
scheme *runtime.Scheme,
) (client.Client, error) {
return fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(
mustNewObject[rollouts.ClusterAnalysisTemplate]("testdata/clusteranalysistemplate.yaml"),
).
Build(), nil
},
},
)
require.NoError(t, err)

svr := &server{
client: client,
cfg: cfg,
externalValidateProjectFn: validation.ValidateProject,
}
_, err = (svr).DeleteClusterAnalysisTemplate(ctx, connect.NewRequest(testCase.req))
if testCase.errExpected {
require.Error(t, err)
fmt.Printf("actual: %s, expected: %s", connect.CodeOf(err), testCase.expectedCode)
require.Equal(t, testCase.expectedCode, connect.CodeOf(err))
return
}
require.NoError(t, err)

at, err := rollouts.GetClusterAnalysisTemplate(ctx, client, testCase.req.Name)
require.NoError(t, err)
require.Nil(t, at)
})
}
}
Loading