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 21 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);

/* Secrets APIs */

rpc ListSecrets(ListSecretsRequest) returns (ListSecretsResponse);
rpc CreateSecrets(CreateSecretsRequest) returns (CreateSecretsResponse);
rpc UpdateSecrets(UpdateSecretsRequest) returns (UpdateSecretsResponse);
rpc DeleteSecrets(DeleteSecretsRequest) returns (DeleteSecretsResponse);
krancour marked this conversation as resolved.
Show resolved Hide resolved

/* 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 ListSecretsRequest {
string project = 1;
}

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

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

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

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

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

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

message DeleteSecretsResponse {
/* 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
9 changes: 5 additions & 4 deletions api/v1alpha1/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ const (
AliasLabelKey = "kargo.akuity.io/alias"

// Credentials
CredentialTypeLabelKey = "kargo.akuity.io/cred-type" // nolint: gosec
CredentialTypeLabelValueGit = "git"
CredentialTypeLabelValueHelm = "helm"
CredentialTypeLabelValueImage = "image"
CredentialTypeLabelKey = "kargo.akuity.io/cred-type" // nolint: gosec
CredentialTypeLabelValueGit = "git"
CredentialTypeLabelValueHelm = "helm"
CredentialTypeLabelValueImage = "image"
CredentialTypeLabelValueGeneric = "generic"
Marvin9 marked this conversation as resolved.
Show resolved Hide resolved

// Kargo core API
FreightCollectionLabelKey = "kargo.akuity.io/freight-collection"
Expand Down
63 changes: 46 additions & 17 deletions internal/api/create_credentials_v1alpha1.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
svcv1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1"
)

type credentials struct {
type specificCredentials struct {
krancour marked this conversation as resolved.
Show resolved Hide resolved
project string
name string
credType string
Expand All @@ -34,34 +34,63 @@
return nil, connect.NewError(connect.CodeUnimplemented, errSecretManagementDisabled)
}

creds := credentials{
project: req.Msg.GetProject(),
name: req.Msg.GetName(),
description: req.Msg.GetDescription(),
credType: req.Msg.GetType(),
repoURL: req.Msg.GetRepoUrl(),
repoURLIsRegex: req.Msg.GetRepoUrlIsRegex(),
username: req.Msg.GetUsername(),
password: req.Msg.GetPassword(),
credType := req.Msg.GetType()

if err := validateFieldNotEmpty("type", credType); err != nil {
return nil, err

Check warning on line 40 in internal/api/create_credentials_v1alpha1.go

View check run for this annotation

Codecov / codecov/patch

internal/api/create_credentials_v1alpha1.go#L40

Added line #L40 was not covered by tests
}

switch credType {
case kargoapi.CredentialTypeLabelValueGit,
kargoapi.CredentialTypeLabelValueHelm,
kargoapi.CredentialTypeLabelValueImage:
krancour marked this conversation as resolved.
Show resolved Hide resolved
creds := specificCredentials{
project: req.Msg.GetProject(),
name: req.Msg.GetName(),
description: req.Msg.GetDescription(),
credType: req.Msg.GetType(),
repoURL: req.Msg.GetRepoUrl(),
repoURLIsRegex: req.Msg.GetRepoUrlIsRegex(),
username: req.Msg.GetUsername(),
password: req.Msg.GetPassword(),
}

kubernetesSecret, err := s.createSpecificCredentials(ctx, creds)

if err != nil {
return nil, err
}

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

View check run for this annotation

Codecov / codecov/patch

internal/api/create_credentials_v1alpha1.go#L61-L62

Added lines #L61 - L62 were not covered by tests

return connect.NewResponse(
&svcv1alpha1.CreateCredentialsResponse{
Credentials: kubernetesSecret,
},
), nil
}

return nil, connect.NewError(
connect.CodeInvalidArgument,
errors.New("type should be one of git, helm, image"),
)
krancour marked this conversation as resolved.
Show resolved Hide resolved
}

// creates credentials used for known purpose; specifically external subscriptions
// to private git repo or helm OCI or docker image
func (s *server) createSpecificCredentials(ctx context.Context, creds specificCredentials) (*corev1.Secret, error) {
krancour marked this conversation as resolved.
Show resolved Hide resolved
if err := s.validateCredentials(creds); err != nil {
return nil, err
}

secret := credentialsToSecret(creds)

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

return connect.NewResponse(
&svcv1alpha1.CreateCredentialsResponse{
Credentials: sanitizeCredentialSecret(*secret),
},
), nil
return sanitizeCredentialSecret(*secret), nil
}

func (s *server) validateCredentials(creds credentials) error {
func (s *server) validateCredentials(creds specificCredentials) error {
if err := validateFieldNotEmpty("project", creds.project); err != nil {
return err
}
Expand Down Expand Up @@ -93,7 +122,7 @@
return validateFieldNotEmpty("password", creds.password)
}

func credentialsToSecret(creds credentials) *corev1.Secret {
func credentialsToSecret(creds specificCredentials) *corev1.Secret {
s := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: creds.project,
Expand Down
213 changes: 213 additions & 0 deletions internal/api/create_credentials_v1alpha1_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
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()

cfg := config.ServerConfigFromEnv()

cfg.SecretManagementEnabled = true

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"),
mustNewObject[corev1.Namespace]("testdata/stage.yaml"),
).Build(), nil
},
})

require.NoError(t, err)

s := &server{
client: cl,
cfg: cfg,
}

t.Run("create repo secret", func(t *testing.T) {
t.Parallel()

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

require.NoError(t, err)

respSecret := resp.Msg.GetCredentials()

assert.Equal(t, "kargo-demo", respSecret.Namespace)
assert.Equal(t, "repo", respSecret.ObjectMeta.Name)
assert.Equal(t, "my repo secret", respSecret.ObjectMeta.Annotations[kargoapi.AnnotationKeyDescription])
assert.Equal(t, "https://github.com/foo/bar", respSecret.StringData[libCreds.FieldRepoURL])
assert.Equal(t, "username", respSecret.StringData[libCreds.FieldUsername])
assert.Equal(t, redacted, respSecret.StringData[libCreds.FieldPassword])

kubernetesSecret := corev1.Secret{}

require.NoError(t, cl.Get(ctx, types.NamespacedName{
Namespace: "kargo-demo",
Name: "repo",
}, &kubernetesSecret),
)

d := kubernetesSecret.DeepCopy().Data

assert.Equal(t, "kargo-demo", kubernetesSecret.Namespace)
assert.Equal(t, "repo", kubernetesSecret.ObjectMeta.Name)
assert.Equal(t, "my repo secret", kubernetesSecret.ObjectMeta.Annotations[kargoapi.AnnotationKeyDescription])
assert.Equal(t, "https://github.com/foo/bar", string(d[libCreds.FieldRepoURL]))
assert.Equal(t, "username", string(d[libCreds.FieldUsername]))
assert.Equal(t, "password", string(d[libCreds.FieldPassword]))
})

t.Run("validate credentials", func(t *testing.T) {
t.Parallel()

invalidCreds := specificCredentials{
project: "",
name: "test",
credType: "git",
repoURL: "abc",
username: "test",
password: "test",
}

err := s.validateCredentials(invalidCreds)

require.Error(t, err)

invalidCreds = specificCredentials{
project: "kargo-demo",
name: "",
credType: "git",
repoURL: "abc",
username: "test",
password: "test",
}

err = s.validateCredentials(invalidCreds)

require.Error(t, err)

invalidCreds = specificCredentials{
project: "kargo-demo",
name: "test",
credType: "",
repoURL: "abc",
username: "test",
password: "test",
}

err = s.validateCredentials(invalidCreds)

require.Error(t, err)

invalidCreds = specificCredentials{
project: "kargo-demo",
name: "test",
credType: "invalid",
repoURL: "abc",
username: "test",
password: "test",
}

err = s.validateCredentials(invalidCreds)

require.Error(t, err)

invalidCreds = specificCredentials{
project: "kargo-demo",
name: "test",
credType: "git",
repoURL: "",
username: "test",
password: "test",
}

err = s.validateCredentials(invalidCreds)

require.Error(t, err)

invalidCreds = specificCredentials{
project: "kargo-demo",
name: "test",
credType: "git",
repoURL: "https://github.com/akuity/kargo",
username: "",
password: "test",
}

err = s.validateCredentials(invalidCreds)

require.Error(t, err)

invalidCreds = specificCredentials{
project: "kargo-demo",
name: "test",
credType: "git",
repoURL: "https://github.com/akuity/kargo",
username: "test",
password: "",
}

err = s.validateCredentials(invalidCreds)

require.Error(t, err)

validCreds := specificCredentials{
project: "kargo-demo",
name: "test",
credType: "git",
repoURL: "https://github.com/akuity/kargo",
username: "test",
password: "test",
}

err = s.validateCredentials(validCreds)

require.NoError(t, err)
})

t.Run("invalid secret", func(t *testing.T) {
t.Parallel()

_, err := s.CreateCredentials(ctx, connect.NewRequest(&svcv1alpha1.CreateCredentialsRequest{
Project: "kargo-demo",
Name: "invalid",
Description: "my invalid secret",
Type: "invalid",
}))

require.Error(t, err)
})
}
Loading
Loading