SecretsManager: add secure value store (#106708)

* SecretsManager: add secure value model and sql templates

Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>
Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com>
Co-authored-by: Leandro Deveikis <leandro.deveikis@gmail.com>
Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com>

* SecretsManager: secure value rest layer to use store

Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>
Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com>
Co-authored-by: Leandro Deveikis <leandro.deveikis@gmail.com>
Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com>

* SecretsManager: temporary add actor prefix to decrypters

* Remove list securevalue by namefor now

---------

Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>
Co-authored-by: Leandro Deveikis <leandro.deveikis@gmail.com>
Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com>
This commit is contained in:
Dana Axinte 2025-06-16 10:19:44 +01:00 committed by GitHub
parent ffc16ee072
commit 6097841e67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 1909 additions and 20 deletions

View File

@ -31,7 +31,7 @@ type SecureValueMetadataStorage interface {
Read(ctx context.Context, namespace xkube.Namespace, name string, opts ReadOpts) (*secretv0alpha1.SecureValue, error)
Update(ctx context.Context, sv *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error)
Delete(ctx context.Context, namespace xkube.Namespace, name string) error
List(ctx context.Context, namespace xkube.Namespace) (*secretv0alpha1.SecureValueList, error)
List(ctx context.Context, namespace xkube.Namespace) ([]secretv0alpha1.SecureValue, error)
SetStatus(ctx context.Context, namespace xkube.Namespace, name string, status secretv0alpha1.SecureValueStatus) error
SetExternalID(ctx context.Context, namespace xkube.Namespace, name string, externalID ExternalID) error
ReadForDecrypt(ctx context.Context, namespace xkube.Namespace, name string) (*DecryptSecureValue, error)

View File

@ -98,9 +98,9 @@ func (s *SecureValueRest) List(ctx context.Context, options *internalversion.Lis
fieldSelector = fields.Everything()
}
allowedSecureValues := make([]secretv0alpha1.SecureValue, 0, len(secureValueList.Items))
allowedSecureValues := make([]secretv0alpha1.SecureValue, 0, len(secureValueList))
for _, secureValue := range secureValueList.Items {
for _, secureValue := range secureValueList {
// Filter by label
if labelSelector.Matches(labels.Set(secureValue.Labels)) {
// Filter by status.phase

View File

@ -0,0 +1,51 @@
INSERT INTO {{ .Ident "secret_secure_value" }} (
{{ .Ident "guid" }},
{{ .Ident "name" }},
{{ .Ident "namespace" }},
{{ .Ident "annotations" }},
{{ .Ident "labels" }},
{{ .Ident "created" }},
{{ .Ident "created_by" }},
{{ .Ident "updated" }},
{{ .Ident "updated_by" }},
{{ .Ident "status_phase" }},
{{ if .Row.Message.Valid }}
{{ .Ident "status_message" }},
{{ end }}
{{ .Ident "description" }},
{{ if .Row.Keeper.Valid }}
{{ .Ident "keeper" }},
{{ end }}
{{ if .Row.Decrypters.Valid }}
{{ .Ident "decrypters" }},
{{ end }}
{{ if .Row.Ref.Valid }}
{{ .Ident "ref" }},
{{ end }}
{{ .Ident "external_id" }}
) VALUES (
{{ .Arg .Row.GUID }},
{{ .Arg .Row.Name }},
{{ .Arg .Row.Namespace }},
{{ .Arg .Row.Annotations }},
{{ .Arg .Row.Labels }},
{{ .Arg .Row.Created }},
{{ .Arg .Row.CreatedBy }},
{{ .Arg .Row.Updated }},
{{ .Arg .Row.UpdatedBy }},
{{ .Arg .Row.Phase }},
{{ if .Row.Message.Valid }}
{{ .Arg .Row.Message.String }},
{{ end }}
{{ .Arg .Row.Description }},
{{ if .Row.Keeper.Valid }}
{{ .Arg .Row.Keeper.String }},
{{ end }}
{{ if .Row.Decrypters.Valid }}
{{ .Arg .Row.Decrypters.String }},
{{ end }}
{{ if .Row.Ref.Valid }}
{{ .Arg .Row.Ref.String }},
{{ end }}
{{ .Arg .Row.ExternalID }}
);

View File

@ -0,0 +1,4 @@
DELETE FROM {{ .Ident "secret_secure_value" }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "name" }} = {{ .Arg .Name }}
;

View File

@ -0,0 +1,22 @@
SELECT
{{ .Ident "guid" }},
{{ .Ident "name" }},
{{ .Ident "namespace" }},
{{ .Ident "annotations" }},
{{ .Ident "labels" }},
{{ .Ident "created" }},
{{ .Ident "created_by" }},
{{ .Ident "updated" }},
{{ .Ident "updated_by" }},
{{ .Ident "status_phase" }},
{{ .Ident "status_message" }},
{{ .Ident "description" }},
{{ .Ident "keeper" }},
{{ .Ident "decrypters" }},
{{ .Ident "ref" }},
{{ .Ident "external_id" }}
FROM
{{ .Ident "secret_secure_value" }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }}
ORDER BY {{ .Ident "updated" }} DESC
;

View File

@ -0,0 +1,11 @@
{{/* this query is used to validate the keeper update or creation */}}
SELECT
{{ .Ident "name" }},
{{ .Ident "keeper" }}
FROM
{{ .Ident "secret_secure_value" }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "name" }} IN ({{ .ArgList .UsedSecureValues }})
{{ .SelectFor "UPDATE" }}
;

View File

@ -0,0 +1,25 @@
SELECT
{{ .Ident "guid" }},
{{ .Ident "name" }},
{{ .Ident "namespace" }},
{{ .Ident "annotations" }},
{{ .Ident "labels" }},
{{ .Ident "created" }},
{{ .Ident "created_by" }},
{{ .Ident "updated" }},
{{ .Ident "updated_by" }},
{{ .Ident "status_phase" }},
{{ .Ident "status_message" }},
{{ .Ident "description" }},
{{ .Ident "keeper" }},
{{ .Ident "decrypters" }},
{{ .Ident "ref" }},
{{ .Ident "external_id" }}
FROM
{{ .Ident "secret_secure_value" }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "name" }} = {{ .Arg .Name }}
{{ if .IsForUpdate }}
{{ .SelectFor "UPDATE" }}
{{ end }}
;

View File

@ -0,0 +1,10 @@
SELECT
{{ .Ident "keeper" }},
{{ .Ident "decrypters" }},
{{ .Ident "ref" }},
{{ .Ident "external_id" }}
FROM
{{ .Ident "secret_secure_value" }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "name" }} = {{ .Arg .Name }}
;

View File

@ -0,0 +1,30 @@
UPDATE
{{ .Ident "secret_secure_value" }}
SET
{{ .Ident "guid" }} = {{ .Arg .Row.GUID }},
{{ .Ident "name" }} = {{ .Arg .Row.Name }},
{{ .Ident "namespace" }} = {{ .Arg .Row.Namespace }},
{{ .Ident "annotations" }} = {{ .Arg .Row.Annotations }},
{{ .Ident "labels" }} = {{ .Arg .Row.Labels }},
{{ .Ident "created" }} = {{ .Arg .Row.Created }},
{{ .Ident "created_by" }} = {{ .Arg .Row.CreatedBy }},
{{ .Ident "updated" }} = {{ .Arg .Row.Updated }},
{{ .Ident "updated_by" }} = {{ .Arg .Row.UpdatedBy }},
{{ .Ident "status_phase" }} = {{ .Arg .Row.Phase }},
{{ if .Row.Message.Valid }}
{{ .Ident "status_message" }} = {{ .Arg .Row.Message.String }},
{{ end }}
{{ .Ident "description" }} = {{ .Arg .Row.Description }},
{{ if .Row.Keeper.Valid }}
{{ .Ident "keeper" }} = {{ .Arg .Row.Keeper.String }},
{{ end }}
{{ if .Row.Decrypters.Valid }}
{{ .Ident "decrypters" }} = {{ .Arg .Row.Decrypters.String }},
{{ end }}
{{ if .Row.Ref.Valid }}
{{ .Ident "ref" }} = {{ .Arg .Row.Ref.String }},
{{ end }}
{{ .Ident "external_id" }} = {{ .Arg .Row.ExternalID }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Row.Namespace }} AND
{{ .Ident "name" }} = {{ .Arg .Row.Name }}
;

View File

@ -0,0 +1,7 @@
UPDATE
{{ .Ident "secret_secure_value" }}
SET
{{ .Ident "external_id" }} = {{ .Arg .ExternalID }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "name" }} = {{ .Arg .Name }}
;

View File

@ -0,0 +1,8 @@
UPDATE
{{ .Ident "secret_secure_value" }}
SET
{{ .Ident "status_phase" }} = {{ .Arg .Phase }},
{{ .Ident "status_message" }} = {{ .Arg .Message }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "name" }} = {{ .Arg .Name }}
;

View File

@ -23,6 +23,15 @@ var (
sqlKeeperListByName = mustTemplate("keeper_listByName.sql")
sqlSecureValueRead = mustTemplate("secure_value_read.sql")
sqlSecureValueList = mustTemplate("secure_value_list.sql")
sqlSecureValueCreate = mustTemplate("secure_value_create.sql")
sqlSecureValueDelete = mustTemplate("secure_value_delete.sql")
sqlSecureValueUpdate = mustTemplate("secure_value_update.sql")
sqlSecureValueUpdateExternalId = mustTemplate("secure_value_updateExternalId.sql")
sqlSecureValueUpdateStatus = mustTemplate("secure_value_updateStatus.sql")
sqlSecureValueReadForDecrypt = mustTemplate("secure_value_read_for_decrypt.sql")
sqlSecureValueOutboxAppend = mustTemplate("secure_value_outbox_append.sql")
sqlSecureValueOutboxReceiveN = mustTemplate("secure_value_outbox_receiveN.sql")
sqlSecureValueOutboxDelete = mustTemplate("secure_value_outbox_delete.sql")
@ -110,6 +119,102 @@ func (r listByNameKeeper) Validate() error {
return nil // TODO
}
/******************************/
/**-- Secure Value Queries --**/
/******************************/
type readSecureValue struct {
sqltemplate.SQLTemplate
Namespace string
Name string
IsForUpdate bool
}
// Validate is only used if we use `dbutil` from `unifiedstorage`
func (r readSecureValue) Validate() error {
return nil // TODO
}
type listSecureValue struct {
sqltemplate.SQLTemplate
Namespace string
}
// Validate is only used if we use `dbutil` from `unifiedstorage`
func (r listSecureValue) Validate() error {
return nil // TODO
}
type createSecureValue struct {
sqltemplate.SQLTemplate
Row *secureValueDB
}
// Validate is only used if we use `dbutil` from `unifiedstorage`
func (r createSecureValue) Validate() error {
return nil // TODO
}
// Delete
type deleteSecureValue struct {
sqltemplate.SQLTemplate
Namespace string
Name string
}
// Validate is only used if we use `dbutil` from `unifiedstorage`
func (r deleteSecureValue) Validate() error {
return nil // TODO
}
// Update externalId
type updateExternalIdSecureValue struct {
sqltemplate.SQLTemplate
Namespace string
Name string
ExternalID string
}
// Validate is only used if we use `dbutil` from `unifiedstorage`
func (r updateExternalIdSecureValue) Validate() error {
return nil // TODO
}
// Update secure value
type updateSecureValue struct {
sqltemplate.SQLTemplate
Namespace string
Name string
Row *secureValueDB
}
// Validate is only used if we use `dbutil` from `unifiedstorage`
func (r updateSecureValue) Validate() error {
return nil // TODO
}
// update status message
type updateStatusSecureValue struct {
sqltemplate.SQLTemplate
Namespace string
Name string
Phase string
Message string
}
// Validate is only used if we use `dbutil` from `unifiedstorage`
func (r updateStatusSecureValue) Validate() error {
return nil // TODO
}
type readSecureValueForDecrypt struct {
sqltemplate.SQLTemplate
Namespace string
Name string
}
func (r readSecureValueForDecrypt) Validate() error { return nil }
/*************************************/
/**-- Secure Value Outbox Queries --**/
/*************************************/

View File

@ -6,6 +6,7 @@ import (
"text/template"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks"
"k8s.io/utils/ptr"
)
func TestKeeperQueries(t *testing.T) {
@ -108,6 +109,189 @@ func TestKeeperQueries(t *testing.T) {
})
}
func TestSecureValueQueries(t *testing.T) {
mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{
RootDir: "testdata",
Templates: map[*template.Template][]mocks.TemplateTestCase{
sqlSecureValueRead: {
{
Name: "read",
Data: &readSecureValue{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Name: "name",
Namespace: "ns",
},
},
{
Name: "read-for-update",
Data: &readSecureValue{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Name: "name",
Namespace: "ns",
IsForUpdate: true,
},
},
},
sqlSecureValueList: {
{
Name: "list",
Data: &listSecureValue{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Namespace: "ns",
},
},
},
sqlSecureValueCreate: {
{
Name: "create-null",
Data: &createSecureValue{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Row: &secureValueDB{
GUID: "abc",
Name: "name",
Namespace: "ns",
Annotations: `{"x":"XXXX"}`,
Labels: `{"a":"AAA", "b", "BBBB"}`,
Created: 1234,
CreatedBy: "user:ryan",
Updated: 5678,
UpdatedBy: "user:cameron",
Phase: "creating",
Message: toNullString(nil),
Description: "description",
Keeper: toNullString(nil),
Decrypters: toNullString(nil),
Ref: toNullString(nil),
ExternalID: "extId",
},
},
},
{
Name: "create-not-null",
Data: &createSecureValue{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Row: &secureValueDB{
GUID: "abc",
Name: "name",
Namespace: "ns",
Annotations: `{"x":"XXXX"}`,
Labels: `{"a":"AAA", "b", "BBBB"}`,
Created: 1234,
CreatedBy: "user:ryan",
Updated: 5678,
UpdatedBy: "user:cameron",
Phase: "creating",
Message: toNullString(ptr.To("message_test")),
Description: "description",
Keeper: toNullString(ptr.To("keeper_test")),
Decrypters: toNullString(ptr.To("decrypters_test")),
Ref: toNullString(ptr.To("ref_test")),
ExternalID: "extId",
},
},
},
},
sqlSecureValueDelete: {
{
Name: "delete",
Data: &deleteSecureValue{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Name: "name",
Namespace: "ns",
},
},
},
sqlSecureValueUpdate: {
{
Name: "update-null",
Data: &updateSecureValue{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Name: "name",
Namespace: "ns",
Row: &secureValueDB{
GUID: "abc",
Name: "name",
Namespace: "ns",
Annotations: `{"x":"XXXX"}`,
Labels: `{"a":"AAA", "b", "BBBB"}`,
Created: 1234,
CreatedBy: "user:ryan",
Updated: 5678,
UpdatedBy: "user:cameron",
Phase: "creating",
Message: toNullString(nil),
Description: "description",
Keeper: toNullString(nil),
Decrypters: toNullString(nil),
Ref: toNullString(nil),
ExternalID: "extId",
},
},
},
{
Name: "update-not-null",
Data: &updateSecureValue{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Name: "name",
Namespace: "ns",
Row: &secureValueDB{
GUID: "abc",
Name: "name",
Namespace: "ns",
Annotations: `{"x":"XXXX"}`,
Labels: `{"a":"AAA", "b", "BBBB"}`,
Created: 1234,
CreatedBy: "user:ryan",
Updated: 5678,
UpdatedBy: "user:cameron",
Phase: "creating",
Message: toNullString(ptr.To("message_test")),
Description: "description",
Keeper: toNullString(ptr.To("keeper_test")),
Decrypters: toNullString(ptr.To("decrypters_test")),
Ref: toNullString(ptr.To("ref_test")),
ExternalID: "extId",
},
},
},
},
sqlSecureValueUpdateExternalId: {
{
Name: "updateExternalId",
Data: &updateExternalIdSecureValue{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Name: "name",
Namespace: "ns",
ExternalID: "extId",
},
},
},
sqlSecureValueUpdateStatus: {
{
Name: "updateStatus",
Data: &updateStatusSecureValue{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Name: "name",
Namespace: "ns",
Phase: "Succeeded",
Message: "message-1",
},
},
},
sqlSecureValueReadForDecrypt: {
{
Name: "read-for-decrypt",
Data: &readSecureValueForDecrypt{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Name: "name",
Namespace: "ns",
},
},
},
},
})
}
func TestSecureValueOutboxQueries(t *testing.T) {
mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{
RootDir: "testdata",

View File

@ -0,0 +1,277 @@
package metadata
import (
"database/sql"
"encoding/json"
"fmt"
"time"
"github.com/google/uuid"
"github.com/grafana/grafana/pkg/apimachinery/utils"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/grafana/grafana/pkg/storage/secret/migrator"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
type secureValueDB struct {
// Kubernetes Metadata
GUID string
Name string
Namespace string
Annotations string // map[string]string
Labels string // map[string]string
Created int64
CreatedBy string
Updated int64
UpdatedBy string
// Kubernetes Status
Phase string
Message sql.NullString
// Spec
Description string
Keeper sql.NullString
Decrypters sql.NullString
Ref sql.NullString
ExternalID string
}
func (*secureValueDB) TableName() string {
return migrator.TableNameSecureValue
}
// toKubernetes maps a DB row into a Kubernetes resource (metadata + spec).
func (sv *secureValueDB) toKubernetes() (*secretv0alpha1.SecureValue, error) {
annotations := make(map[string]string, 0)
if sv.Annotations != "" {
if err := json.Unmarshal([]byte(sv.Annotations), &annotations); err != nil {
return nil, fmt.Errorf("failed to unmarshal annotations: %w", err)
}
}
labels := make(map[string]string, 0)
if sv.Labels != "" {
if err := json.Unmarshal([]byte(sv.Labels), &labels); err != nil {
return nil, fmt.Errorf("failed to unmarshal labels: %w", err)
}
}
decrypters := make([]string, 0)
if sv.Decrypters.Valid && sv.Decrypters.String != "" {
if err := json.Unmarshal([]byte(sv.Decrypters.String), &decrypters); err != nil {
return nil, fmt.Errorf("failed to unmarshal decrypters: %w", err)
}
}
resource := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Description: sv.Description,
Decrypters: decrypters,
},
Status: secretv0alpha1.SecureValueStatus{
Phase: secretv0alpha1.SecureValuePhase(sv.Phase),
ExternalID: sv.ExternalID,
},
}
if sv.Keeper.Valid {
resource.Spec.Keeper = &sv.Keeper.String
}
if sv.Ref.Valid {
resource.Spec.Ref = &sv.Ref.String
}
if sv.Message.Valid {
resource.Status.Message = sv.Message.String
}
resource.Status.Phase = secretv0alpha1.SecureValuePhase(sv.Phase)
resource.Status.ExternalID = sv.ExternalID
// Set all meta fields here for consistency.
meta, err := utils.MetaAccessor(resource)
if err != nil {
return nil, fmt.Errorf("failed to get meta accessor: %w", err)
}
updated := time.Unix(sv.Updated, 0).UTC()
meta.SetUID(types.UID(sv.GUID))
meta.SetName(sv.Name)
meta.SetNamespace(sv.Namespace)
meta.SetAnnotations(annotations)
meta.SetLabels(labels)
meta.SetCreatedBy(sv.CreatedBy)
meta.SetCreationTimestamp(metav1.NewTime(time.Unix(sv.Created, 0).UTC()))
meta.SetUpdatedBy(sv.UpdatedBy)
meta.SetUpdatedTimestamp(&updated)
meta.SetResourceVersionInt64(sv.Updated)
return resource, nil
}
// toCreateRow maps a Kubernetes resource into a DB row for new resources being created/inserted.
func toCreateRow(sv *secretv0alpha1.SecureValue, actorUID string) (*secureValueDB, error) {
row, err := toRow(sv, "")
if err != nil {
return nil, fmt.Errorf("failed to convert SecureValue to secureValueDB: %w", err)
}
now := time.Now().UTC().Unix()
row.GUID = uuid.New().String()
row.Created = now
row.CreatedBy = actorUID
row.Updated = now
row.UpdatedBy = actorUID
return row, nil
}
// toUpdateRow maps a Kubernetes resource into a DB row for existing resources being updated.
func toUpdateRow(currentRow *secureValueDB, newSecureValue *secretv0alpha1.SecureValue, actorUID, externalID string) (*secureValueDB, error) {
row, err := toRow(newSecureValue, externalID)
if err != nil {
return nil, fmt.Errorf("failed to create: %w", err)
}
now := time.Now().UTC().Unix()
row.GUID = currentRow.GUID
row.Created = currentRow.Created
row.CreatedBy = currentRow.CreatedBy
row.Updated = now
row.UpdatedBy = actorUID
return row, nil
}
// toRow maps a Kubernetes resource into a DB row.
func toRow(sv *secretv0alpha1.SecureValue, externalID string) (*secureValueDB, error) {
var annotations string
if len(sv.Annotations) > 0 {
cleanedAnnotations := xkube.CleanAnnotations(sv.Annotations)
if len(cleanedAnnotations) > 0 {
sv.Annotations = make(map[string]string) // Safety: reset to prohibit use of sv.Annotations further.
encodedAnnotations, err := json.Marshal(cleanedAnnotations)
if err != nil {
return nil, fmt.Errorf("failed to encode annotations: %w", err)
}
annotations = string(encodedAnnotations)
}
}
var labels string
if len(sv.Labels) > 0 {
encodedLabels, err := json.Marshal(sv.Labels)
if err != nil {
return nil, fmt.Errorf("failed to encode labels: %w", err)
}
labels = string(encodedLabels)
}
var decrypters *string
if len(sv.Spec.Decrypters) > 0 {
encodedDecrypters, err := json.Marshal(sv.Spec.Decrypters)
if err != nil {
return nil, fmt.Errorf("failed to encode decrypters: %w", err)
}
rawDecrypters := string(encodedDecrypters)
decrypters = &rawDecrypters
}
meta, err := utils.MetaAccessor(sv)
if err != nil {
return nil, fmt.Errorf("failed to get meta accessor: %w", err)
}
if meta.GetFolder() != "" {
return nil, fmt.Errorf("folders are not supported")
}
updatedTimestamp, err := meta.GetResourceVersionInt64()
if err != nil {
return nil, fmt.Errorf("failed to get resource version: %w", err)
}
var statusMessage *string
if sv.Status.Message != "" {
statusMessage = &sv.Status.Message
}
return &secureValueDB{
GUID: string(sv.UID),
Name: sv.Name,
Namespace: sv.Namespace,
Annotations: annotations,
Labels: labels,
Created: meta.GetCreationTimestamp().UnixMilli(),
CreatedBy: meta.GetCreatedBy(),
Updated: updatedTimestamp,
UpdatedBy: meta.GetUpdatedBy(),
Phase: string(sv.Status.Phase),
Message: toNullString(statusMessage),
Description: sv.Spec.Description,
Keeper: toNullString(sv.Spec.Keeper),
Decrypters: toNullString(decrypters),
Ref: toNullString(sv.Spec.Ref),
ExternalID: externalID,
}, nil
}
// DTO for `secureValueForDecrypt` query result, only what we need.
type secureValueForDecrypt struct {
Keeper sql.NullString
Decrypters sql.NullString
Ref sql.NullString
ExternalID string
}
// to Decrypt maps a DB row into a DecryptSecureValue object needed for decryption.
func (sv *secureValueForDecrypt) toDecrypt() (*contracts.DecryptSecureValue, error) {
decrypters := make([]string, 0)
if sv.Decrypters.Valid && sv.Decrypters.String != "" {
if err := json.Unmarshal([]byte(sv.Decrypters.String), &decrypters); err != nil {
return nil, fmt.Errorf("failed to unmarshal decrypters: %w", err)
}
}
decryptSecureValue := &contracts.DecryptSecureValue{
Decrypters: decrypters,
ExternalID: sv.ExternalID,
}
if sv.Keeper.Valid && sv.Keeper.String != "" {
decryptSecureValue.Keeper = &sv.Keeper.String
}
if sv.Ref.Valid && sv.Ref.String != "" {
decryptSecureValue.Ref = sv.Ref.String
}
return decryptSecureValue, nil
}
// toNullString returns a sql.NullString struct given a *string
// assumes that "" (empty string) is a valid string
func toNullString(s *string) sql.NullString {
if s == nil {
return sql.NullString{
String: "",
Valid: false,
}
}
return sql.NullString{
String: *s,
Valid: true,
}
}

View File

@ -2,59 +2,418 @@ package metadata
import (
"context"
claims "github.com/grafana/authlib/types"
"fmt"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/storage/unified/sql"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
func ProvideSecureValueMetadataStorage(db db.DB, features featuremgmt.FeatureToggles, accessClient claims.AccessClient) (contracts.SecureValueMetadataStorage, error) {
var _ contracts.SecureValueMetadataStorage = (*secureValueMetadataStorage)(nil)
func ProvideSecureValueMetadataStorage(db contracts.Database, features featuremgmt.FeatureToggles) (contracts.SecureValueMetadataStorage, error) {
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) ||
!features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) {
return &secureValueMetadataStorage{}, nil
}
return &secureValueMetadataStorage{db: db, accessClient: accessClient}, nil
return &secureValueMetadataStorage{
db: db,
dialect: sqltemplate.DialectForDriver(db.DriverName()),
}, nil
}
// secureValueMetadataStorage is the actual implementation of the secure value metadata storage.
// secureValueMetadataStorage is the actual implementation of the secure value (metadata) storage.
type secureValueMetadataStorage struct {
db db.DB
accessClient claims.AccessClient
db contracts.Database
dialect sqltemplate.Dialect
}
func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error) {
return nil, nil
sv.Status.Phase = secretv0alpha1.SecureValuePhasePending
sv.Status.Message = "Creating secure value"
row, err := toCreateRow(sv, actorUID)
if err != nil {
return nil, fmt.Errorf("to create row: %w", err)
}
req := createSecureValue{
SQLTemplate: sqltemplate.New(s.dialect),
Row: row,
}
query, err := sqltemplate.Execute(sqlSecureValueCreate, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueCreate.Name(), err)
}
err = s.db.Transaction(ctx, func(ctx context.Context) error {
if row.Keeper.Valid {
// Validate before inserting that the chosen `keeper` exists.
// -- This is a copy of KeeperMetadataStore.read, which is not public at the moment, and is not defined in contract.KeeperMetadataStorage
req := &readKeeper{
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: row.Namespace,
Name: row.Keeper.String,
IsForUpdate: true,
}
query, err := sqltemplate.Execute(sqlKeeperRead, req)
if err != nil {
return fmt.Errorf("execute template %q: %w", sqlKeeperRead.Name(), err)
}
res, err := s.db.QueryContext(ctx, query, req.GetArgs()...)
if err != nil {
return fmt.Errorf("getting row: %w", err)
}
defer func() { _ = res.Close() }()
if !res.Next() {
return contracts.ErrKeeperNotFound
}
}
res, err := s.db.ExecContext(ctx, query, req.GetArgs()...)
if err != nil {
if sql.IsRowAlreadyExistsError(err) {
return fmt.Errorf("namespace=%+v name=%+v %w", sv.Namespace, sv.Name, contracts.ErrSecureValueAlreadyExists)
}
return fmt.Errorf("inserting row: %w", err)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("getting rows affected: %w", err)
}
if rowsAffected != 1 {
return fmt.Errorf("expected 1 row affected, got %d for %s on %s", rowsAffected, row.Name, row.Namespace)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("db failure: %w", err)
}
createdSecureValue, err := row.toKubernetes()
if err != nil {
return nil, fmt.Errorf("convert to kubernetes object: %w", err)
}
return createdSecureValue, nil
}
func (s *secureValueMetadataStorage) Read(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (*secretv0alpha1.SecureValue, error) {
return nil, nil
secureValue, err := s.read(ctx, namespace, name, opts)
if err != nil {
return nil, err
}
secureValueKub, err := secureValue.toKubernetes()
if err != nil {
return nil, fmt.Errorf("convert to kubernetes object: %w", err)
}
return secureValueKub, nil
}
func (s *secureValueMetadataStorage) Update(ctx context.Context, newSecureValue *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error) {
return nil, nil
var newRow *secureValueDB
err := s.db.Transaction(ctx, func(ctx context.Context) error {
read, err := s.read(ctx, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, contracts.ReadOpts{ForUpdate: true})
if err != nil {
return fmt.Errorf("reading secure value: %w", err)
}
// TODO: Confirm the ExternalID should come from the read model.
var updateErr error
newRow, updateErr = toUpdateRow(&read, newSecureValue, actorUID, read.ExternalID)
if updateErr != nil {
return fmt.Errorf("model to update row: %w", updateErr)
}
if newRow.Keeper.Valid {
// Validate before updating that the new `keeper` exists.
// -- This is a copy of KeeperMetadataStore.read, which is not public at the moment, and is not defined in contract.KeeperMetadataStorage
req := &readKeeper{
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: newRow.Namespace,
Name: newRow.Keeper.String,
IsForUpdate: true,
}
query, err := sqltemplate.Execute(sqlKeeperRead, req)
if err != nil {
return fmt.Errorf("execute template %q: %w", sqlKeeperRead.Name(), err)
}
res, err := s.db.QueryContext(ctx, query, req.GetArgs()...)
if err != nil {
return fmt.Errorf("getting row: %w", err)
}
defer func() { _ = res.Close() }()
if !res.Next() {
return contracts.ErrKeeperNotFound
}
}
req := &updateSecureValue{
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: newRow.Namespace,
Name: newRow.Name,
Row: newRow,
}
query, err := sqltemplate.Execute(sqlSecureValueUpdate, req)
if err != nil {
return fmt.Errorf("execute template %q: %w", sqlSecureValueUpdate.Name(), err)
}
result, err := s.db.ExecContext(ctx, query, req.GetArgs()...)
if err != nil {
return fmt.Errorf("updating row: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("getting rows affected: %w", err)
}
if rowsAffected != 1 {
return fmt.Errorf("expected 1 row affected, got %d for %s on %s", rowsAffected, newRow.Name, newRow.Namespace)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("db failure: %w", err)
}
secureValue, err := newRow.toKubernetes()
if err != nil {
return nil, fmt.Errorf("convert to kubernetes object: %w", err)
}
return secureValue, nil
}
func (s *secureValueMetadataStorage) Delete(ctx context.Context, namespace xkube.Namespace, name string) error {
req := deleteSecureValue{
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace.String(),
Name: name,
}
query, err := sqltemplate.Execute(sqlSecureValueDelete, req)
if err != nil {
return fmt.Errorf("execute template %q: %w", sqlSecureValueDelete.Name(), err)
}
res, err := s.db.ExecContext(ctx, query, req.GetArgs()...)
if err != nil {
return fmt.Errorf("deleting secure value row: %w", err)
}
if rowsAffected, err := res.RowsAffected(); err != nil || rowsAffected != 1 {
return fmt.Errorf("deleting secure value rowsAffected=%d error=%w", rowsAffected, err)
}
return nil
}
func (s *secureValueMetadataStorage) List(ctx context.Context, namespace xkube.Namespace) (*secretv0alpha1.SecureValueList, error) {
return nil, nil
}
func (s *secureValueMetadataStorage) List(ctx context.Context, namespace xkube.Namespace) ([]secretv0alpha1.SecureValue, error) {
req := listSecureValue{
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace.String(),
}
func (s *secureValueMetadataStorage) SetStatus(ctx context.Context, namespace xkube.Namespace, name string, status secretv0alpha1.SecureValueStatus) error {
return nil
q, err := sqltemplate.Execute(sqlSecureValueList, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueList.Name(), err)
}
rows, err := s.db.QueryContext(ctx, q, req.GetArgs()...)
if err != nil {
return nil, fmt.Errorf("listing secure values: %w", err)
}
defer func() { _ = rows.Close() }()
secureValues := make([]secretv0alpha1.SecureValue, 0)
for rows.Next() {
row := secureValueDB{}
err = rows.Scan(&row.GUID,
&row.Name, &row.Namespace, &row.Annotations,
&row.Labels,
&row.Created, &row.CreatedBy,
&row.Updated, &row.UpdatedBy,
&row.Phase, &row.Message,
&row.Description, &row.Keeper, &row.Decrypters,
&row.Ref, &row.ExternalID,
)
if err != nil {
return nil, fmt.Errorf("error reading secure value row: %w", err)
}
secureValue, err := row.toKubernetes()
if err != nil {
return nil, fmt.Errorf("convert to kubernetes object: %w", err)
}
secureValues = append(secureValues, *secureValue)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("read rows error: %w", err)
}
return secureValues, nil
}
func (s *secureValueMetadataStorage) SetExternalID(ctx context.Context, namespace xkube.Namespace, name string, externalID contracts.ExternalID) error {
req := updateExternalIdSecureValue{
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace.String(),
Name: name,
ExternalID: externalID.String(),
}
q, err := sqltemplate.Execute(sqlSecureValueUpdateExternalId, req)
if err != nil {
return fmt.Errorf("execute template %q: %w", sqlSecureValueUpdateExternalId.Name(), err)
}
res, err := s.db.ExecContext(ctx, q, req.GetArgs()...)
if err != nil {
return fmt.Errorf("setting secure value external id: namespace=%+v name=%+v externalID=%+v %w", namespace, name, externalID, err)
}
// validate modified cound
modifiedCount, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("getting updated rows update external id secure value: %w", err)
}
if modifiedCount > 1 {
return fmt.Errorf("secureValueMetadataStorage.SetExternalID: modified more than one secret, this is a bug, check the where condition: modifiedCount=%d", modifiedCount)
}
return nil
}
func (s *secureValueMetadataStorage) SetStatus(ctx context.Context, namespace xkube.Namespace, name string, status secretv0alpha1.SecureValueStatus) error {
req := updateStatusSecureValue{
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace.String(),
Name: name,
Phase: string(status.Phase),
Message: status.Message,
}
q, err := sqltemplate.Execute(sqlSecureValueUpdateStatus, req)
if err != nil {
return fmt.Errorf("execute template %q: %w", sqlSecureValueUpdateStatus.Name(), err)
}
res, err := s.db.ExecContext(ctx, q, req.GetArgs()...)
if err != nil {
return fmt.Errorf("setting secure value status to Succeeded id: namespace=%+v name=%+v %w", namespace, name, err)
}
// validate modified cound
modifiedCount, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("getting updated rows update status secure value: %w", err)
}
if modifiedCount > 1 {
return fmt.Errorf("secureValueMetadataStorage.SetExternalID: modified more than one secret, this is a bug, check the where condition: modifiedCount=%d", modifiedCount)
}
return nil
}
func (s *secureValueMetadataStorage) ReadForDecrypt(ctx context.Context, namespace xkube.Namespace, name string) (*contracts.DecryptSecureValue, error) {
return nil, nil
req := readSecureValueForDecrypt{
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace.String(),
Name: name,
}
query, err := sqltemplate.Execute(sqlSecureValueReadForDecrypt, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueReadForDecrypt.Name(), err)
}
res, err := s.db.QueryContext(ctx, query, req.GetArgs()...)
if err != nil {
return nil, fmt.Errorf("reading row: %w", err)
}
defer func() { _ = res.Close() }()
var row secureValueForDecrypt
if !res.Next() {
return nil, contracts.ErrSecureValueNotFound
}
if err := res.Scan(
&row.Keeper, &row.Decrypters,
&row.Ref, &row.ExternalID); err != nil {
return nil, fmt.Errorf("failed to scan secure value row: %w", err)
}
if err := res.Err(); err != nil {
return nil, fmt.Errorf("read rows error: %w", err)
}
secureValue, err := row.toDecrypt()
if err != nil {
return nil, fmt.Errorf("convert to kubernetes object: %w", err)
}
return secureValue, nil
}
func (s *secureValueMetadataStorage) read(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (secureValueDB, error) {
req := readSecureValue{
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace.String(),
Name: name,
IsForUpdate: opts.ForUpdate,
}
query, err := sqltemplate.Execute(sqlSecureValueRead, req)
if err != nil {
return secureValueDB{}, fmt.Errorf("execute template %q: %w", sqlSecureValueRead.Name(), err)
}
res, err := s.db.QueryContext(ctx, query, req.GetArgs()...)
if err != nil {
return secureValueDB{}, fmt.Errorf("reading row: %w", err)
}
defer func() { _ = res.Close() }()
var secureValue secureValueDB
if !res.Next() {
return secureValueDB{}, contracts.ErrSecureValueNotFound
}
if err := res.Scan(
&secureValue.GUID, &secureValue.Name, &secureValue.Namespace,
&secureValue.Annotations, &secureValue.Labels,
&secureValue.Created, &secureValue.CreatedBy,
&secureValue.Updated, &secureValue.UpdatedBy,
&secureValue.Phase, &secureValue.Message,
&secureValue.Description, &secureValue.Keeper, &secureValue.Decrypters, &secureValue.Ref, &secureValue.ExternalID); err != nil {
return secureValueDB{}, fmt.Errorf("failed to scan secure value row: %w", err)
}
if err := res.Err(); err != nil {
return secureValueDB{}, fmt.Errorf("read rows error: %w", err)
}
return secureValue, nil
}

View File

@ -0,0 +1,141 @@
package metadata
import (
"context"
"testing"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/storage/secret/database"
"github.com/grafana/grafana/pkg/storage/secret/migrator"
"github.com/stretchr/testify/require"
)
func createTestKeeper(t *testing.T, ctx context.Context, keeperStorage contracts.KeeperMetadataStorage, name, namespace string) string {
t.Helper()
testKeeper := &secretv0alpha1.Keeper{
Spec: secretv0alpha1.KeeperSpec{
Description: "test keeper description",
AWS: &secretv0alpha1.AWSKeeperConfig{},
},
}
testKeeper.Name = name
testKeeper.Namespace = namespace
// Create the keeper
_, err := keeperStorage.Create(ctx, testKeeper, "testuser")
require.NoError(t, err)
return name
}
func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) {
ctx := context.Background()
testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
db := database.ProvideDatabase(testDB)
features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform)
// Initialize the secure value storage
secureValueStorage, err := ProvideSecureValueMetadataStorage(db, features)
require.NoError(t, err)
// Initialize the keeper storage
keeperStorage, err := ProvideKeeperMetadataStorage(db, features)
require.NoError(t, err)
t.Run("create and read a secure value", func(t *testing.T) {
// First create a keeper
keeperName := createTestKeeper(t, ctx, keeperStorage, "test-keeper", "default")
// Create a test secure value
testSecureValue := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Description: "test description",
Value: "test-value",
Keeper: &keeperName,
},
}
testSecureValue.Name = "sv-test"
testSecureValue.Namespace = "default"
// Create the secure value
createdSecureValue, err := secureValueStorage.Create(ctx, testSecureValue, "testuser")
require.NoError(t, err)
require.NotNil(t, createdSecureValue)
require.Equal(t, "sv-test", createdSecureValue.Name)
require.Equal(t, "default", createdSecureValue.Namespace)
require.Equal(t, "test description", createdSecureValue.Spec.Description)
require.Equal(t, keeperName, *createdSecureValue.Spec.Keeper)
require.Equal(t, secretv0alpha1.SecureValuePhasePending, createdSecureValue.Status.Phase)
// Read the secure value back
readSecureValue, err := secureValueStorage.Read(ctx, xkube.Namespace("default"), "sv-test", contracts.ReadOpts{})
require.NoError(t, err)
require.NotNil(t, readSecureValue)
require.Equal(t, "sv-test", readSecureValue.Name)
require.Equal(t, "default", readSecureValue.Namespace)
require.Equal(t, "test description", readSecureValue.Spec.Description)
require.Equal(t, keeperName, *readSecureValue.Spec.Keeper)
require.Equal(t, secretv0alpha1.SecureValuePhasePending, readSecureValue.Status.Phase)
// List secure values and verify our value is in the list
secureValues, err := secureValueStorage.List(ctx, xkube.Namespace("default"))
require.NoError(t, err)
require.NotEmpty(t, secureValues)
// Find our secure value in the list
var found bool
for _, sv := range secureValues {
if sv.Name == "sv-test" {
found = true
require.Equal(t, "default", sv.Namespace)
require.Equal(t, "test description", sv.Spec.Description)
require.Equal(t, keeperName, *sv.Spec.Keeper)
require.Equal(t, secretv0alpha1.SecureValuePhasePending, sv.Status.Phase)
break
}
}
require.True(t, found, "secure value not found in list")
})
t.Run("create, read, delete and verify secure value", func(t *testing.T) {
// First create a keeper
keeperName := createTestKeeper(t, ctx, keeperStorage, "test-keeper-2", "default")
// Create a test secure value
testSecureValue := &secretv0alpha1.SecureValue{
Spec: secretv0alpha1.SecureValueSpec{
Description: "test description 2",
Value: "test-value-2",
Keeper: &keeperName,
},
}
testSecureValue.Name = "sv-test-2"
testSecureValue.Namespace = "default"
// Create the secure value
createdSecureValue, err := secureValueStorage.Create(ctx, testSecureValue, "testuser")
require.NoError(t, err)
require.NotNil(t, createdSecureValue)
// Read the secure value to verify it exists
readSecureValue, err := secureValueStorage.Read(ctx, xkube.Namespace("default"), "sv-test-2", contracts.ReadOpts{})
require.NoError(t, err)
require.NotNil(t, readSecureValue)
require.Equal(t, "sv-test-2", readSecureValue.Name)
// Delete the secure value
err = secureValueStorage.Delete(ctx, xkube.Namespace("default"), "sv-test-2")
require.NoError(t, err)
// Try to read the deleted secure value - should return error
_, err = secureValueStorage.Read(ctx, xkube.Namespace("default"), "sv-test-2", contracts.ReadOpts{})
require.Error(t, err)
require.Equal(t, contracts.ErrSecureValueNotFound, err)
})
}

View File

@ -0,0 +1,35 @@
INSERT INTO `secret_secure_value` (
`guid`,
`name`,
`namespace`,
`annotations`,
`labels`,
`created`,
`created_by`,
`updated`,
`updated_by`,
`status_phase`,
`status_message`,
`description`,
`keeper`,
`decrypters`,
`ref`,
`external_id`
) VALUES (
'abc',
'name',
'ns',
'{"x":"XXXX"}',
'{"a":"AAA", "b", "BBBB"}',
1234,
'user:ryan',
5678,
'user:cameron',
'creating',
'message_test',
'description',
'keeper_test',
'decrypters_test',
'ref_test',
'extId'
);

View File

@ -0,0 +1,27 @@
INSERT INTO `secret_secure_value` (
`guid`,
`name`,
`namespace`,
`annotations`,
`labels`,
`created`,
`created_by`,
`updated`,
`updated_by`,
`status_phase`,
`description`,
`external_id`
) VALUES (
'abc',
'name',
'ns',
'{"x":"XXXX"}',
'{"a":"AAA", "b", "BBBB"}',
1234,
'user:ryan',
5678,
'user:cameron',
'creating',
'description',
'extId'
);

View File

@ -0,0 +1,4 @@
DELETE FROM `secret_secure_value`
WHERE `namespace` = 'ns' AND
`name` = 'name'
;

View File

@ -0,0 +1,22 @@
SELECT
`guid`,
`name`,
`namespace`,
`annotations`,
`labels`,
`created`,
`created_by`,
`updated`,
`updated_by`,
`status_phase`,
`status_message`,
`description`,
`keeper`,
`decrypters`,
`ref`,
`external_id`
FROM
`secret_secure_value`
WHERE `namespace` = 'ns'
ORDER BY `updated` DESC
;

View File

@ -0,0 +1,23 @@
SELECT
`guid`,
`name`,
`namespace`,
`annotations`,
`labels`,
`created`,
`created_by`,
`updated`,
`updated_by`,
`status_phase`,
`status_message`,
`description`,
`keeper`,
`decrypters`,
`ref`,
`external_id`
FROM
`secret_secure_value`
WHERE `namespace` = 'ns' AND
`name` = 'name'
FOR UPDATE
;

View File

@ -0,0 +1,22 @@
SELECT
`guid`,
`name`,
`namespace`,
`annotations`,
`labels`,
`created`,
`created_by`,
`updated`,
`updated_by`,
`status_phase`,
`status_message`,
`description`,
`keeper`,
`decrypters`,
`ref`,
`external_id`
FROM
`secret_secure_value`
WHERE `namespace` = 'ns' AND
`name` = 'name'
;

View File

@ -0,0 +1,10 @@
SELECT
`keeper`,
`decrypters`,
`ref`,
`external_id`
FROM
`secret_secure_value`
WHERE `namespace` = 'ns' AND
`name` = 'name'
;

View File

@ -0,0 +1,22 @@
UPDATE
`secret_secure_value`
SET
`guid` = 'abc',
`name` = 'name',
`namespace` = 'ns',
`annotations` = '{"x":"XXXX"}',
`labels` = '{"a":"AAA", "b", "BBBB"}',
`created` = 1234,
`created_by` = 'user:ryan',
`updated` = 5678,
`updated_by` = 'user:cameron',
`status_phase` = 'creating',
`status_message` = 'message_test',
`description` = 'description',
`keeper` = 'keeper_test',
`decrypters` = 'decrypters_test',
`ref` = 'ref_test',
`external_id` = 'extId'
WHERE `namespace` = 'ns' AND
`name` = 'name'
;

View File

@ -0,0 +1,18 @@
UPDATE
`secret_secure_value`
SET
`guid` = 'abc',
`name` = 'name',
`namespace` = 'ns',
`annotations` = '{"x":"XXXX"}',
`labels` = '{"a":"AAA", "b", "BBBB"}',
`created` = 1234,
`created_by` = 'user:ryan',
`updated` = 5678,
`updated_by` = 'user:cameron',
`status_phase` = 'creating',
`description` = 'description',
`external_id` = 'extId'
WHERE `namespace` = 'ns' AND
`name` = 'name'
;

View File

@ -0,0 +1,7 @@
UPDATE
`secret_secure_value`
SET
`external_id` = 'extId'
WHERE `namespace` = 'ns' AND
`name` = 'name'
;

View File

@ -0,0 +1,8 @@
UPDATE
`secret_secure_value`
SET
`status_phase` = 'Succeeded',
`status_message` = 'message-1'
WHERE `namespace` = 'ns' AND
`name` = 'name'
;

View File

@ -0,0 +1,35 @@
INSERT INTO "secret_secure_value" (
"guid",
"name",
"namespace",
"annotations",
"labels",
"created",
"created_by",
"updated",
"updated_by",
"status_phase",
"status_message",
"description",
"keeper",
"decrypters",
"ref",
"external_id"
) VALUES (
'abc',
'name',
'ns',
'{"x":"XXXX"}',
'{"a":"AAA", "b", "BBBB"}',
1234,
'user:ryan',
5678,
'user:cameron',
'creating',
'message_test',
'description',
'keeper_test',
'decrypters_test',
'ref_test',
'extId'
);

View File

@ -0,0 +1,27 @@
INSERT INTO "secret_secure_value" (
"guid",
"name",
"namespace",
"annotations",
"labels",
"created",
"created_by",
"updated",
"updated_by",
"status_phase",
"description",
"external_id"
) VALUES (
'abc',
'name',
'ns',
'{"x":"XXXX"}',
'{"a":"AAA", "b", "BBBB"}',
1234,
'user:ryan',
5678,
'user:cameron',
'creating',
'description',
'extId'
);

View File

@ -0,0 +1,4 @@
DELETE FROM "secret_secure_value"
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

View File

@ -0,0 +1,22 @@
SELECT
"guid",
"name",
"namespace",
"annotations",
"labels",
"created",
"created_by",
"updated",
"updated_by",
"status_phase",
"status_message",
"description",
"keeper",
"decrypters",
"ref",
"external_id"
FROM
"secret_secure_value"
WHERE "namespace" = 'ns'
ORDER BY "updated" DESC
;

View File

@ -0,0 +1,23 @@
SELECT
"guid",
"name",
"namespace",
"annotations",
"labels",
"created",
"created_by",
"updated",
"updated_by",
"status_phase",
"status_message",
"description",
"keeper",
"decrypters",
"ref",
"external_id"
FROM
"secret_secure_value"
WHERE "namespace" = 'ns' AND
"name" = 'name'
FOR UPDATE
;

View File

@ -0,0 +1,22 @@
SELECT
"guid",
"name",
"namespace",
"annotations",
"labels",
"created",
"created_by",
"updated",
"updated_by",
"status_phase",
"status_message",
"description",
"keeper",
"decrypters",
"ref",
"external_id"
FROM
"secret_secure_value"
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

View File

@ -0,0 +1,10 @@
SELECT
"keeper",
"decrypters",
"ref",
"external_id"
FROM
"secret_secure_value"
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

View File

@ -0,0 +1,22 @@
UPDATE
"secret_secure_value"
SET
"guid" = 'abc',
"name" = 'name',
"namespace" = 'ns',
"annotations" = '{"x":"XXXX"}',
"labels" = '{"a":"AAA", "b", "BBBB"}',
"created" = 1234,
"created_by" = 'user:ryan',
"updated" = 5678,
"updated_by" = 'user:cameron',
"status_phase" = 'creating',
"status_message" = 'message_test',
"description" = 'description',
"keeper" = 'keeper_test',
"decrypters" = 'decrypters_test',
"ref" = 'ref_test',
"external_id" = 'extId'
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

View File

@ -0,0 +1,18 @@
UPDATE
"secret_secure_value"
SET
"guid" = 'abc',
"name" = 'name',
"namespace" = 'ns',
"annotations" = '{"x":"XXXX"}',
"labels" = '{"a":"AAA", "b", "BBBB"}',
"created" = 1234,
"created_by" = 'user:ryan',
"updated" = 5678,
"updated_by" = 'user:cameron',
"status_phase" = 'creating',
"description" = 'description',
"external_id" = 'extId'
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

View File

@ -0,0 +1,7 @@
UPDATE
"secret_secure_value"
SET
"external_id" = 'extId'
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

View File

@ -0,0 +1,8 @@
UPDATE
"secret_secure_value"
SET
"status_phase" = 'Succeeded',
"status_message" = 'message-1'
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

View File

@ -0,0 +1,35 @@
INSERT INTO "secret_secure_value" (
"guid",
"name",
"namespace",
"annotations",
"labels",
"created",
"created_by",
"updated",
"updated_by",
"status_phase",
"status_message",
"description",
"keeper",
"decrypters",
"ref",
"external_id"
) VALUES (
'abc',
'name',
'ns',
'{"x":"XXXX"}',
'{"a":"AAA", "b", "BBBB"}',
1234,
'user:ryan',
5678,
'user:cameron',
'creating',
'message_test',
'description',
'keeper_test',
'decrypters_test',
'ref_test',
'extId'
);

View File

@ -0,0 +1,27 @@
INSERT INTO "secret_secure_value" (
"guid",
"name",
"namespace",
"annotations",
"labels",
"created",
"created_by",
"updated",
"updated_by",
"status_phase",
"description",
"external_id"
) VALUES (
'abc',
'name',
'ns',
'{"x":"XXXX"}',
'{"a":"AAA", "b", "BBBB"}',
1234,
'user:ryan',
5678,
'user:cameron',
'creating',
'description',
'extId'
);

View File

@ -0,0 +1,4 @@
DELETE FROM "secret_secure_value"
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

View File

@ -0,0 +1,22 @@
SELECT
"guid",
"name",
"namespace",
"annotations",
"labels",
"created",
"created_by",
"updated",
"updated_by",
"status_phase",
"status_message",
"description",
"keeper",
"decrypters",
"ref",
"external_id"
FROM
"secret_secure_value"
WHERE "namespace" = 'ns'
ORDER BY "updated" DESC
;

View File

@ -0,0 +1,22 @@
SELECT
"guid",
"name",
"namespace",
"annotations",
"labels",
"created",
"created_by",
"updated",
"updated_by",
"status_phase",
"status_message",
"description",
"keeper",
"decrypters",
"ref",
"external_id"
FROM
"secret_secure_value"
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

View File

@ -0,0 +1,22 @@
SELECT
"guid",
"name",
"namespace",
"annotations",
"labels",
"created",
"created_by",
"updated",
"updated_by",
"status_phase",
"status_message",
"description",
"keeper",
"decrypters",
"ref",
"external_id"
FROM
"secret_secure_value"
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

View File

@ -0,0 +1,10 @@
SELECT
"keeper",
"decrypters",
"ref",
"external_id"
FROM
"secret_secure_value"
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

View File

@ -0,0 +1,22 @@
UPDATE
"secret_secure_value"
SET
"guid" = 'abc',
"name" = 'name',
"namespace" = 'ns',
"annotations" = '{"x":"XXXX"}',
"labels" = '{"a":"AAA", "b", "BBBB"}',
"created" = 1234,
"created_by" = 'user:ryan',
"updated" = 5678,
"updated_by" = 'user:cameron',
"status_phase" = 'creating',
"status_message" = 'message_test',
"description" = 'description',
"keeper" = 'keeper_test',
"decrypters" = 'decrypters_test',
"ref" = 'ref_test',
"external_id" = 'extId'
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

View File

@ -0,0 +1,18 @@
UPDATE
"secret_secure_value"
SET
"guid" = 'abc',
"name" = 'name',
"namespace" = 'ns',
"annotations" = '{"x":"XXXX"}',
"labels" = '{"a":"AAA", "b", "BBBB"}',
"created" = 1234,
"created_by" = 'user:ryan',
"updated" = 5678,
"updated_by" = 'user:cameron',
"status_phase" = 'creating',
"description" = 'description',
"external_id" = 'extId'
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

View File

@ -0,0 +1,7 @@
UPDATE
"secret_secure_value"
SET
"external_id" = 'extId'
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

View File

@ -0,0 +1,8 @@
UPDATE
"secret_secure_value"
SET
"status_phase" = 'Succeeded',
"status_message" = 'message-1'
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

View File

@ -13,6 +13,7 @@ import (
const (
TableNameKeeper = "secret_keeper"
TableNameSecureValue = "secret_secure_value"
TableNameSecureValueOutbox = "secret_secure_value_outbox"
TableNameEncryptedValue = "secret_encrypted_value"
)
@ -69,6 +70,36 @@ func (*SecretDB) AddMigration(mg *migrator.Migrator) {
},
})
tables = append(tables, migrator.Table{
Name: TableNameSecureValue,
Columns: []*migrator.Column{
// Kubernetes Metadata
{Name: "guid", Type: migrator.DB_NVarchar, Length: 36, IsPrimaryKey: true}, // Fixed size of a UUID.
{Name: "name", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Limit enforced by K8s.
{Name: "namespace", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Limit enforced by K8s.
{Name: "annotations", Type: migrator.DB_Text, Nullable: true},
{Name: "labels", Type: migrator.DB_Text, Nullable: true},
{Name: "created", Type: migrator.DB_BigInt, Nullable: false},
{Name: "created_by", Type: migrator.DB_Text, Nullable: false},
{Name: "updated", Type: migrator.DB_BigInt, Nullable: false}, // Used as RV (ResourceVersion)
{Name: "updated_by", Type: migrator.DB_Text, Nullable: false},
// Kubernetes Status
{Name: "status_phase", Type: migrator.DB_Text, Nullable: false},
{Name: "status_message", Type: migrator.DB_Text, Nullable: true},
// Spec
{Name: "description", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Chosen arbitrarily, but should be enough.
{Name: "keeper", Type: migrator.DB_NVarchar, Length: 253, Nullable: true}, // Keeper name, if not set, use default keeper.
{Name: "decrypters", Type: migrator.DB_Text, Nullable: true},
{Name: "ref", Type: migrator.DB_NVarchar, Length: 1024, Nullable: true}, // Reference to third-party storage secret path.Chosen arbitrarily, but should be enough.
{Name: "external_id", Type: migrator.DB_Text, Nullable: false},
},
Indices: []*migrator.Index{
{Cols: []string{"namespace", "name"}, Type: migrator.UniqueIndex},
},
})
tables = append(tables, migrator.Table{
Name: TableNameEncryptedValue,
Columns: []*migrator.Column{

View File

@ -0,0 +1,15 @@
apiVersion: secret.grafana.app/v0alpha1
kind: SecureValue
metadata:
annotations:
xx: XXX
yy: YYY
labels:
aa: AAA
bb: BBB
spec:
description: This is a secret
value: this is super duper secure
decrypters:
- actor_k6
- actor_synthetic-monitoring

View File

@ -0,0 +1,16 @@
apiVersion: secret.grafana.app/v0alpha1
kind: SecureValue
metadata:
annotations:
xx: XXX
yy: YYY
labels:
aa: AAA
bb: BBB
spec:
description: XYZ value
keeper: my-keeper-1
value: super duper secure
decrypters:
- actor_k6
- actor_synthetic-monitoring