Skip to content

Commit

Permalink
feat: allow project scoped generic kubernetes secrets (#2975)
Browse files Browse the repository at this point in the history
Signed-off-by: Mayursinh Sarvaiya <[email protected]>
Signed-off-by: Kent Rancourt <[email protected]>
Co-authored-by: Kent Rancourt <[email protected]>
  • Loading branch information
Marvin9 and krancour authored Jan 11, 2025
1 parent 13b615e commit dfbf526
Show file tree
Hide file tree
Showing 27 changed files with 4,590 additions and 2,260 deletions.
47 changes: 47 additions & 0 deletions api/service/v1alpha1/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ service KargoService {
rpc ListCredentials(ListCredentialsRequest) returns (ListCredentialsResponse);
rpc UpdateCredentials(UpdateCredentialsRequest) returns (UpdateCredentialsResponse);

/* Project Secrets APIs */

rpc ListProjectSecrets(ListProjectSecretsRequest) returns (ListProjectSecretsResponse);
rpc CreateProjectSecret(CreateProjectSecretRequest) returns (CreateProjectSecretResponse);
rpc UpdateProjectSecret(UpdateProjectSecretRequest) returns (UpdateProjectSecretResponse);
rpc DeleteProjectSecret(DeleteProjectSecretRequest) returns (DeleteProjectSecretResponse);

/* Analysis APIs */

rpc ListAnalysisTemplates(ListAnalysisTemplatesRequest) returns (ListAnalysisTemplatesResponse);
Expand Down Expand Up @@ -532,10 +539,50 @@ message RefreshWarehouseResponse {
github.com.akuity.kargo.api.v1alpha1.Warehouse warehouse = 1;
}

message ListProjectSecretsRequest {
string project = 1;
}

message ListProjectSecretsResponse {
repeated k8s.io.api.core.v1.Secret secrets = 1;
}

message CreateProjectSecretRequest {
string project = 1;
string name = 2;
string description = 3;
map<string, string> data = 4;
}

message CreateProjectSecretResponse {
k8s.io.api.core.v1.Secret secret = 1;
}

message UpdateProjectSecretRequest {
string project = 1;
string name = 2;
string description = 3;
map<string, string> data = 4;
}

message UpdateProjectSecretResponse {
k8s.io.api.core.v1.Secret secret = 1;
}

message DeleteProjectSecretRequest {
string project = 1;
string name = 2;
}

message DeleteProjectSecretResponse {
/* explicitly empty */
}

message CreateCredentialsRequest {
string project = 1;
string name = 2;
string description = 8;
// type is git, helm, image
string type = 3;
string repo_url = 4 [json_name = "repoURL"];
bool repo_url_is_regex = 5 [json_name = "repoURLIsRegex"];
Expand Down
3 changes: 3 additions & 0 deletions api/v1alpha1/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const (
CredentialTypeLabelValueHelm = "helm"
CredentialTypeLabelValueImage = "image"

// Project Secrets
ProjectSecretLabelKey = "kargo.akuity.io/project-secret" // nolint: gosec

// Kargo core API
FreightCollectionLabelKey = "kargo.akuity.io/freight-collection"
ProjectLabelKey = "kargo.akuity.io/project"
Expand Down
4 changes: 2 additions & 2 deletions internal/api/create_credentials_v1alpha1.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func (s *server) CreateCredentials(
return nil, err
}

secret := credentialsToSecret(creds)
secret := credentialsToK8sSecret(creds)
if err := s.client.Create(ctx, secret); err != nil {
return nil, fmt.Errorf("create secret: %w", err)
}
Expand Down Expand Up @@ -93,7 +93,7 @@ func (s *server) validateCredentials(creds credentials) error {
return validateFieldNotEmpty("password", creds.password)
}

func credentialsToSecret(creds credentials) *corev1.Secret {
func credentialsToK8sSecret(creds credentials) *corev1.Secret {
s := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: creds.project,
Expand Down
189 changes: 189 additions & 0 deletions internal/api/create_credentials_v1alpha1_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package api

import (
"context"
"testing"

"connectrpc.com/connect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

kargoapi "github.com/akuity/kargo/api/v1alpha1"
"github.com/akuity/kargo/internal/api/config"
"github.com/akuity/kargo/internal/api/kubernetes"
libCreds "github.com/akuity/kargo/internal/credentials"
svcv1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1"
)

func TestCreateCredentials(t *testing.T) {
ctx := context.Background()

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

s := &server{
client: cl,
cfg: config.ServerConfig{SecretManagementEnabled: true},
}

resp, err := s.CreateCredentials(
ctx,
connect.NewRequest(
&svcv1alpha1.CreateCredentialsRequest{
Project: "kargo-demo",
Name: "creds",
Description: "my credentials",
Type: "git",
RepoUrl: "https://github.com/example/repo",
Username: "username",
Password: "password",
},
),
)
require.NoError(t, err)

creds := resp.Msg.GetCredentials()
assert.Equal(t, "kargo-demo", creds.Namespace)
assert.Equal(t, "creds", creds.ObjectMeta.Name)
assert.Equal(t, "my credentials", creds.ObjectMeta.Annotations[kargoapi.AnnotationKeyDescription])
assert.Equal(t, "https://github.com/example/repo", creds.StringData[libCreds.FieldRepoURL])
assert.Equal(t, "username", creds.StringData[libCreds.FieldUsername])
assert.Equal(t, redacted, creds.StringData[libCreds.FieldPassword])

secret := corev1.Secret{}
err = cl.Get(
ctx,
types.NamespacedName{
Namespace: "kargo-demo",
Name: "creds",
},
&secret,
)
require.NoError(t, err)

data := secret.Data
assert.Equal(t, "kargo-demo", secret.Namespace)
assert.Equal(t, "creds", secret.ObjectMeta.Name)
assert.Equal(t, "my credentials", secret.ObjectMeta.Annotations[kargoapi.AnnotationKeyDescription])
assert.Equal(t, "https://github.com/example/repo", string(data[libCreds.FieldRepoURL]))
assert.Equal(t, "username", string(data[libCreds.FieldUsername]))
assert.Equal(t, "password", string(data[libCreds.FieldPassword]))
}

func TestValidateCredentials(t *testing.T) {
s := &server{}

err := s.validateCredentials(
credentials{
project: "",
name: "test",
credType: "git",
repoURL: "abc",
username: "test",
password: "test",
},
)
require.Error(t, err)

err = s.validateCredentials(
credentials{
project: "kargo-demo",
name: "",
credType: "git",
repoURL: "abc",
username: "test",
password: "test",
},
)
require.Error(t, err)

err = s.validateCredentials(
credentials{
project: "kargo-demo",
name: "test",
credType: "",
repoURL: "abc",
username: "test",
password: "test",
},
)
require.Error(t, err)

err = s.validateCredentials(
credentials{
project: "kargo-demo",
name: "test",
credType: "invalid",
repoURL: "abc",
username: "test",
password: "test",
},
)
require.Error(t, err)

err = s.validateCredentials(
credentials{
project: "kargo-demo",
name: "test",
credType: "git",
repoURL: "",
username: "test",
password: "test",
},
)
require.Error(t, err)

err = s.validateCredentials(
credentials{
project: "kargo-demo",
name: "test",
credType: "git",
repoURL: "https://github.com/akuity/kargo",
username: "",
password: "test",
},
)
require.Error(t, err)

err = s.validateCredentials(
credentials{
project: "kargo-demo",
name: "test",
credType: "git",
repoURL: "https://github.com/akuity/kargo",
username: "test",
password: "",
},
)
require.Error(t, err)

err = s.validateCredentials(
credentials{
project: "kargo-demo",
name: "test",
credType: "git",
repoURL: "https://github.com/akuity/kargo",
username: "test",
password: "test",
},
)
require.NoError(t, err)
}
97 changes: 97 additions & 0 deletions internal/api/create_project_secret_v1alpha1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package api

import (
"context"
"errors"
"fmt"

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

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

type projectSecret struct {
project string
name string
description string
data map[string]string
}

func (s *server) CreateProjectSecret(
ctx context.Context,
req *connect.Request[svcv1alpha1.CreateProjectSecretRequest],
) (*connect.Response[svcv1alpha1.CreateProjectSecretResponse], error) {
// Check if secret management is enabled
if !s.cfg.SecretManagementEnabled {
return nil, connect.NewError(connect.CodeUnimplemented, errSecretManagementDisabled)
}

projSecret := projectSecret{
project: req.Msg.GetProject(),
name: req.Msg.GetName(),
data: req.Msg.GetData(),
description: req.Msg.GetDescription(),
}

if err := s.validateProjectSecret(projSecret); err != nil {
return nil, err
}

secret := s.projectSecretToK8sSecret(projSecret)
if err := s.client.Create(ctx, secret); err != nil {
return nil, fmt.Errorf("create secret: %w", err)
}

return connect.NewResponse(
&svcv1alpha1.CreateProjectSecretResponse{
Secret: sanitizeProjectSecret(*secret),
},
), nil
}

func (s *server) validateProjectSecret(projSecret projectSecret) error {
if err := validateFieldNotEmpty("project", projSecret.project); err != nil {
return err
}

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

if len(projSecret.data) == 0 {
return connect.NewError(connect.CodeInvalidArgument,
errors.New("cannot create empty secret"))
}

return nil
}

func (s *server) projectSecretToK8sSecret(projSecret projectSecret) *corev1.Secret {
secretsData := map[string][]byte{}

for key, value := range projSecret.data {
secretsData[key] = []byte(value)
}

secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: projSecret.project,
Name: projSecret.name,
Labels: map[string]string{
kargoapi.ProjectSecretLabelKey: kargoapi.LabelTrueValue,
},
},
Data: secretsData,
}

if projSecret.description != "" {
secret.Annotations = map[string]string{
kargoapi.AnnotationKeyDescription: projSecret.description,
}
}

return secret
}
Loading

0 comments on commit dfbf526

Please sign in to comment.