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: allow project scoped generic kubernetes secrets #2975

Merged
merged 25 commits into from
Jan 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
96bf6db
feat: allow project scoped generic kubernetes secrets
Marvin9 Nov 20, 2024
94376ad
Merge remote-tracking branch 'origin' into Marvin9/feat-generic-secrets
Marvin9 Nov 20, 2024
87dc905
feat: update generic secret
Marvin9 Nov 21, 2024
9f0556f
fix: lint
Marvin9 Nov 21, 2024
fa98a71
chore: add tests
Marvin9 Nov 21, 2024
58e7eed
fix: lint
Marvin9 Nov 21, 2024
cad6f83
fix: lint
Marvin9 Nov 21, 2024
166e713
Merge remote-tracking branch 'origin' into Marvin9/feat-generic-secrets
Marvin9 Dec 10, 2024
d513593
chore: update tests
Marvin9 Dec 10, 2024
cbed6c4
Merge remote-tracking branch 'origin' into Marvin9/feat-generic-secrets
Marvin9 Dec 17, 2024
5798f96
chore(ui): address review
Marvin9 Dec 17, 2024
c8dad5b
Merge remote-tracking branch 'origin' into Marvin9/feat-generic-secrets
Marvin9 Dec 18, 2024
67c2047
chore(ui): review updates
Marvin9 Dec 18, 2024
7b537e1
chore(ui): update modal title
Marvin9 Jan 3, 2025
41c4bd1
chore: split endpoints for generic secrets
Marvin9 Jan 7, 2025
0adb936
Merge remote-tracking branch 'origin' into Marvin9/feat-generic-secrets
Marvin9 Jan 7, 2025
1a6b684
chore: updates
Marvin9 Jan 7, 2025
b73e927
chore: reuse secrets endpoint for cred delete
Marvin9 Jan 7, 2025
2faf24f
add tests
Marvin9 Jan 8, 2025
dd5a73f
chore: add tests and sanitize secret fn
Marvin9 Jan 8, 2025
17da553
chore: add tests
Marvin9 Jan 8, 2025
cef1b28
update service.proto
krancour Jan 10, 2025
c965eb4
run codegen
krancour Jan 11, 2025
ad01bb6
endpoint cleanup
krancour Jan 11, 2025
793f29e
small ui tweaks to work with updated api endpoints
krancour Jan 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}

Check warning on line 30 in internal/api/create_project_secret_v1alpha1.go

View check run for this annotation

Codecov / codecov/patch

internal/api/create_project_secret_v1alpha1.go#L29-L30

Added lines #L29 - L30 were not covered by tests

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
}

Check warning on line 41 in internal/api/create_project_secret_v1alpha1.go

View check run for this annotation

Codecov / codecov/patch

internal/api/create_project_secret_v1alpha1.go#L40-L41

Added lines #L40 - L41 were not covered by tests

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

Check warning on line 46 in internal/api/create_project_secret_v1alpha1.go

View check run for this annotation

Codecov / codecov/patch

internal/api/create_project_secret_v1alpha1.go#L45-L46

Added lines #L45 - L46 were not covered by tests

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
}

Check warning on line 58 in internal/api/create_project_secret_v1alpha1.go

View check run for this annotation

Codecov / codecov/patch

internal/api/create_project_secret_v1alpha1.go#L57-L58

Added lines #L57 - L58 were not covered by tests

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

Check warning on line 62 in internal/api/create_project_secret_v1alpha1.go

View check run for this annotation

Codecov / codecov/patch

internal/api/create_project_secret_v1alpha1.go#L61-L62

Added lines #L61 - L62 were not covered by tests

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

Check warning on line 67 in internal/api/create_project_secret_v1alpha1.go

View check run for this annotation

Codecov / codecov/patch

internal/api/create_project_secret_v1alpha1.go#L65-L67

Added lines #L65 - L67 were not covered by tests

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
Loading