mirror of https://github.com/grafana/grafana
Secrets: Add single tenant SecureValueClient (#108099)
* Secrets: Add single tenant SecureValueClient * SecureValueClient: Rename file * SecureValueClient: Move original type to contracts package and export it by aliasingpull/108216/head
parent
a0873736aa
commit
6c69ae244e
@ -0,0 +1,231 @@ |
||||
package secret |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
|
||||
claims "github.com/grafana/authlib/types" |
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" |
||||
"k8s.io/apimachinery/pkg/runtime" |
||||
"k8s.io/apimachinery/pkg/types" |
||||
"k8s.io/apimachinery/pkg/watch" |
||||
"k8s.io/apiserver/pkg/admission" |
||||
"k8s.io/client-go/dynamic" |
||||
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" |
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts" |
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube" |
||||
) |
||||
|
||||
// SecureValueClient is a CRUD client for the secure value API.
|
||||
type SecureValueClient = contracts.SecureValueClient |
||||
|
||||
type secureValueClient struct { |
||||
namespace string |
||||
service contracts.SecureValueService |
||||
validator contracts.SecureValueValidator |
||||
} |
||||
|
||||
var _ SecureValueClient = &secureValueClient{} |
||||
|
||||
func ProvideSecureValueClient(service contracts.SecureValueService, validator contracts.SecureValueValidator) SecureValueClient { |
||||
return &secureValueClient{ |
||||
service: service, |
||||
validator: validator, |
||||
} |
||||
} |
||||
|
||||
// Client returns a resource interface that is scoped to a specific namespace.
|
||||
func (c *secureValueClient) Client(ctx context.Context, namespace string) (dynamic.ResourceInterface, error) { |
||||
return c.Namespace(namespace), nil |
||||
} |
||||
|
||||
// Namespace returns a resource interface that is scoped to a specific namespace.
|
||||
func (c *secureValueClient) Namespace(ns string) dynamic.ResourceInterface { |
||||
info, err := claims.ParseNamespace(ns) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
if len(info.Value) == 0 { |
||||
panic("namespace is required") |
||||
} |
||||
|
||||
ret := *c |
||||
ret.namespace = ns |
||||
return &ret |
||||
} |
||||
|
||||
// Create a new secure value. Options and subresources are not supported and ignored.
|
||||
func (c *secureValueClient) Create(ctx context.Context, obj *unstructured.Unstructured, _ metav1.CreateOptions, _ ...string) (*unstructured.Unstructured, error) { |
||||
if len(c.namespace) == 0 { |
||||
return nil, fmt.Errorf("namespace is required") |
||||
} |
||||
|
||||
sv, err := fromUnstructured(obj) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if sv.Namespace != c.namespace { |
||||
return nil, fmt.Errorf("namespace mismatch") |
||||
} |
||||
if errs := c.validator.Validate(sv, nil, admission.Create); len(errs) > 0 { |
||||
return nil, fmt.Errorf("invalid secure value: %w", errs.ToAggregate()) |
||||
} |
||||
|
||||
user, ok := claims.AuthInfoFrom(ctx) |
||||
if !ok { |
||||
return nil, fmt.Errorf("missing auth info in context") |
||||
} |
||||
|
||||
createdSv, err := c.service.Create(ctx, sv, user.GetUID()) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return toUnstructured(createdSv) |
||||
} |
||||
|
||||
// Get a secure value by name. Options and subresources are not supported and ignored.
|
||||
func (c *secureValueClient) Get(ctx context.Context, name string, _ metav1.GetOptions, _ ...string) (*unstructured.Unstructured, error) { |
||||
if len(c.namespace) == 0 { |
||||
return nil, fmt.Errorf("namespace is required") |
||||
} |
||||
if len(name) == 0 { |
||||
return nil, fmt.Errorf("name is required") |
||||
} |
||||
|
||||
sv, err := c.service.Read(ctx, xkube.Namespace(c.namespace), name) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return toUnstructured(sv) |
||||
} |
||||
|
||||
// Update a secure value. Options and subresources are not supported and ignored.
|
||||
func (c *secureValueClient) Update(ctx context.Context, obj *unstructured.Unstructured, _ metav1.UpdateOptions, _ ...string) (*unstructured.Unstructured, error) { |
||||
if len(c.namespace) == 0 { |
||||
return nil, fmt.Errorf("namespace is required") |
||||
} |
||||
|
||||
oldUnstructured, err := c.Get(ctx, obj.GetName(), metav1.GetOptions{}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
oldSv, err := fromUnstructured(oldUnstructured) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
sv, err := fromUnstructured(obj) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if sv.Namespace != c.namespace { |
||||
return nil, fmt.Errorf("namespace mismatch") |
||||
} |
||||
if errs := c.validator.Validate(sv, oldSv, admission.Update); len(errs) > 0 { |
||||
return nil, fmt.Errorf("invalid secure value: %w", errs.ToAggregate()) |
||||
} |
||||
|
||||
user, ok := claims.AuthInfoFrom(ctx) |
||||
if !ok { |
||||
return nil, fmt.Errorf("missing auth info in context") |
||||
} |
||||
|
||||
updatedSv, _, err := c.service.Update(ctx, sv, user.GetUID()) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return toUnstructured(updatedSv) |
||||
} |
||||
|
||||
// Delete a secure value by name. Options and subresources are not supported and ignored.
|
||||
func (c *secureValueClient) Delete(ctx context.Context, name string, _ metav1.DeleteOptions, _ ...string) error { |
||||
if len(c.namespace) == 0 { |
||||
return fmt.Errorf("namespace is required") |
||||
} |
||||
if len(name) == 0 { |
||||
return fmt.Errorf("name is required") |
||||
} |
||||
|
||||
_, err := c.service.Delete(ctx, xkube.Namespace(c.namespace), name) |
||||
return err |
||||
} |
||||
|
||||
// List all secure values in the namespace. Options and subresources are not supported and ignored.
|
||||
func (c *secureValueClient) List(ctx context.Context, _ metav1.ListOptions) (*unstructured.UnstructuredList, error) { |
||||
if len(c.namespace) == 0 { |
||||
return nil, fmt.Errorf("namespace is required") |
||||
} |
||||
|
||||
list, err := c.service.List(ctx, xkube.Namespace(c.namespace)) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
items := make([]unstructured.Unstructured, 0, len(list.Items)) |
||||
for _, sv := range list.Items { |
||||
u, err := toUnstructured(&sv) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
items = append(items, *u) |
||||
} |
||||
|
||||
return &unstructured.UnstructuredList{ |
||||
Items: items, |
||||
}, nil |
||||
} |
||||
|
||||
// DeleteCollection is not supported and returns an error.
|
||||
func (c *secureValueClient) DeleteCollection(_ context.Context, _ metav1.DeleteOptions, _ metav1.ListOptions) error { |
||||
return fmt.Errorf("deleteCollection is not supported") |
||||
} |
||||
|
||||
// Watch is not supported and returns an error.
|
||||
func (c *secureValueClient) Watch(_ context.Context, _ metav1.ListOptions) (watch.Interface, error) { |
||||
return nil, fmt.Errorf("watch is not supported") |
||||
} |
||||
|
||||
// Patch is not supported and returns an error.
|
||||
func (c *secureValueClient) Patch(_ context.Context, _ string, _ types.PatchType, _ []byte, _ metav1.PatchOptions, _ ...string) (*unstructured.Unstructured, error) { |
||||
return nil, fmt.Errorf("patch is not supported") |
||||
} |
||||
|
||||
// Apply is not supported and returns an error.
|
||||
func (c *secureValueClient) Apply(_ context.Context, _ string, _ *unstructured.Unstructured, _ metav1.ApplyOptions, _ ...string) (*unstructured.Unstructured, error) { |
||||
return nil, fmt.Errorf("apply is not supported") |
||||
} |
||||
|
||||
// UpdateStatus is not supported and returns an error.
|
||||
func (c *secureValueClient) UpdateStatus(_ context.Context, _ *unstructured.Unstructured, _ metav1.UpdateOptions) (*unstructured.Unstructured, error) { |
||||
return nil, fmt.Errorf("updateStatus is not supported") |
||||
} |
||||
|
||||
// ApplyStatus is not supported and returns an error.
|
||||
func (c *secureValueClient) ApplyStatus(_ context.Context, _ string, _ *unstructured.Unstructured, _ metav1.ApplyOptions) (*unstructured.Unstructured, error) { |
||||
return nil, fmt.Errorf("applyStatus is not supported") |
||||
} |
||||
|
||||
func toUnstructured(sv *secretv1beta1.SecureValue) (*unstructured.Unstructured, error) { |
||||
unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(sv) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &unstructured.Unstructured{Object: unstructuredObj}, nil |
||||
} |
||||
|
||||
func fromUnstructured(u *unstructured.Unstructured) (*secretv1beta1.SecureValue, error) { |
||||
sv := new(secretv1beta1.SecureValue) |
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, sv); err != nil { |
||||
return nil, err |
||||
} |
||||
return sv, nil |
||||
} |
@ -0,0 +1,109 @@ |
||||
package secret |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
"k8s.io/utils/ptr" |
||||
|
||||
secretv1beta1 "github.com/grafana/grafana/apps/secret/pkg/apis/secret/v1beta1" |
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts" |
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/testutils" |
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/validator" |
||||
) |
||||
|
||||
func TestIntegration_SecureValueClient_CRUD(t *testing.T) { |
||||
setup := testutils.Setup(t) |
||||
|
||||
validator := validator.ProvideSecureValueValidator() |
||||
|
||||
client := ProvideSecureValueClient( |
||||
setup.SecureValueService, |
||||
validator, |
||||
) |
||||
|
||||
ns := "stacks-1234" |
||||
ctx := testutils.CreateUserAuthContext(t.Context(), ns, map[string][]string{ |
||||
"securevalues:read": {"securevalues:uid:*"}, |
||||
}) |
||||
|
||||
nsClient, err := client.Client(ctx, ns) |
||||
require.NoError(t, err) |
||||
require.NotNil(t, nsClient) |
||||
|
||||
sv := &secretv1beta1.SecureValue{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: "test-sv", |
||||
Namespace: ns, |
||||
}, |
||||
Spec: secretv1beta1.SecureValueSpec{ |
||||
Description: "test-description", |
||||
Value: ptr.To(secretv1beta1.NewExposedSecureValue("test-value")), |
||||
}, |
||||
} |
||||
|
||||
unstructured, err := toUnstructured(sv) |
||||
require.NoError(t, err) |
||||
|
||||
// Create
|
||||
created, err := nsClient.Create(ctx, unstructured, metav1.CreateOptions{}) |
||||
require.NoError(t, err) |
||||
|
||||
createdSv, err := fromUnstructured(created) |
||||
require.NoError(t, err) |
||||
|
||||
require.NotEmpty(t, createdSv.UID) |
||||
require.Nil(t, createdSv.Spec.Value) |
||||
require.Equal(t, sv.Name, createdSv.Name) |
||||
require.Equal(t, sv.Namespace, createdSv.Namespace) |
||||
|
||||
// Read
|
||||
read, err := nsClient.Get(ctx, createdSv.Name, metav1.GetOptions{}) |
||||
require.NoError(t, err) |
||||
|
||||
readSv, err := fromUnstructured(read) |
||||
require.NoError(t, err) |
||||
require.EqualValues(t, createdSv, readSv) |
||||
|
||||
// Update
|
||||
updatedSv := &secretv1beta1.SecureValue{ |
||||
ObjectMeta: metav1.ObjectMeta{ |
||||
Name: createdSv.Name, |
||||
Namespace: createdSv.Namespace, |
||||
}, |
||||
Spec: secretv1beta1.SecureValueSpec{ |
||||
Description: "test-description-updated", |
||||
Value: ptr.To(secretv1beta1.NewExposedSecureValue("test-value-updated")), |
||||
}, |
||||
} |
||||
|
||||
unstructured, err = toUnstructured(updatedSv) |
||||
require.NoError(t, err) |
||||
|
||||
_, err = nsClient.Update(ctx, unstructured, metav1.UpdateOptions{}) |
||||
require.NoError(t, err) |
||||
|
||||
read, err = nsClient.Get(ctx, createdSv.Name, metav1.GetOptions{}) |
||||
require.NoError(t, err) |
||||
readSv, err = fromUnstructured(read) |
||||
require.NoError(t, err) |
||||
require.Equal(t, updatedSv.Spec.Description, readSv.Spec.Description) |
||||
require.Equal(t, updatedSv.Name, readSv.Name) |
||||
require.Equal(t, updatedSv.Namespace, readSv.Namespace) |
||||
|
||||
// List
|
||||
list, err := nsClient.List(ctx, metav1.ListOptions{}) |
||||
require.NoError(t, err) |
||||
require.Len(t, list.Items, 1) |
||||
require.Equal(t, createdSv.Name, list.Items[0].GetName()) |
||||
require.Equal(t, createdSv.Namespace, list.Items[0].GetNamespace()) |
||||
|
||||
// Delete
|
||||
err = nsClient.Delete(ctx, createdSv.Name, metav1.DeleteOptions{}) |
||||
require.NoError(t, err) |
||||
|
||||
read, err = nsClient.Get(ctx, createdSv.Name, metav1.GetOptions{}) |
||||
require.ErrorIs(t, err, contracts.ErrSecureValueNotFound) |
||||
require.Nil(t, read) |
||||
} |
Loading…
Reference in new issue