SecretsManager: Introduce keeper store (#105557)
* SecretsManager: Introduce secret database wrapper Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com> Co-authored-by: Leandro Deveikis <leandro.deveikis@gmail.com> Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com> * SecretsManager: Introduce db migrator with keeper table Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com> Co-authored-by: Leandro Deveikis <leandro.deveikis@gmail.com> Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com> * SecretsManager: Introduce keeper store Co-authored-by: Leandro Deveikis <leandro.deveikis@gmail.com> Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com> * new line * without query listByNameSecureValue * remove unused extractSecureValues for now * SecretsManager: Add keeper integration tests Co-authored-by: Leandro Deveikis <leandro.deveikis@gmail.com> Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com> --------- Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com> Co-authored-by: Leandro Deveikis <leandro.deveikis@gmail.com> Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>
This commit is contained in:
co-authored by
PoorlyDefinedBehaviour
Leandro Deveikis
Matheus Macabu
parent
c5de567c8c
commit
7f2923d4ed
@@ -177,6 +177,7 @@
|
|||||||
/pkg/tests/apis/query @grafana/grafana-datasources-core-services
|
/pkg/tests/apis/query @grafana/grafana-datasources-core-services
|
||||||
/pkg/tests/apis/alerting @grafana/grafana-app-platform-squad @grafana/alerting-backend
|
/pkg/tests/apis/alerting @grafana/grafana-app-platform-squad @grafana/alerting-backend
|
||||||
/pkg/tests/api/correlations/ @grafana/datapro
|
/pkg/tests/api/correlations/ @grafana/datapro
|
||||||
|
/pkg/tests/apis/secret/ @grafana/grafana-operator-experience-squad
|
||||||
/pkg/tsdb/grafanads/ @grafana/grafana-backend-group
|
/pkg/tsdb/grafanads/ @grafana/grafana-backend-group
|
||||||
/pkg/tsdb/opentsdb/ @grafana/partner-datasources
|
/pkg/tsdb/opentsdb/ @grafana/partner-datasources
|
||||||
/pkg/util/ @grafana/grafana-backend-group
|
/pkg/util/ @grafana/grafana-backend-group
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
INSERT INTO {{ .Ident "secret_keeper" }} (
|
||||||
|
{{ .Ident "guid" }},
|
||||||
|
{{ .Ident "name" }},
|
||||||
|
{{ .Ident "namespace" }},
|
||||||
|
{{ .Ident "annotations" }},
|
||||||
|
{{ .Ident "labels" }},
|
||||||
|
{{ .Ident "created" }},
|
||||||
|
{{ .Ident "created_by" }},
|
||||||
|
{{ .Ident "updated" }},
|
||||||
|
{{ .Ident "updated_by" }},
|
||||||
|
{{ .Ident "description" }},
|
||||||
|
{{ .Ident "type" }},
|
||||||
|
{{ .Ident "payload" }}
|
||||||
|
) 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.Description }},
|
||||||
|
{{ .Arg .Row.Type }},
|
||||||
|
{{ .Arg .Row.Payload }}
|
||||||
|
);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
DELETE FROM {{ .Ident "secret_keeper" }}
|
||||||
|
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
|
||||||
|
{{ .Ident "name" }} = {{ .Arg .Name }}
|
||||||
|
;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
SELECT
|
||||||
|
{{ .Ident "guid" }},
|
||||||
|
{{ .Ident "name" }},
|
||||||
|
{{ .Ident "namespace" }},
|
||||||
|
{{ .Ident "annotations" }},
|
||||||
|
{{ .Ident "labels" }},
|
||||||
|
{{ .Ident "created" }},
|
||||||
|
{{ .Ident "created_by" }},
|
||||||
|
{{ .Ident "updated" }},
|
||||||
|
{{ .Ident "updated_by" }},
|
||||||
|
{{ .Ident "description" }},
|
||||||
|
{{ .Ident "type" }},
|
||||||
|
{{ .Ident "payload" }}
|
||||||
|
FROM
|
||||||
|
{{ .Ident "secret_keeper" }}
|
||||||
|
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }}
|
||||||
|
ORDER BY {{ .Ident "updated" }} DESC
|
||||||
|
;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{{/* this query is used to validate the keeper update or creation */}}
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
{{ .Ident "name" }}
|
||||||
|
FROM
|
||||||
|
{{ .Ident "secret_keeper" }}
|
||||||
|
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
|
||||||
|
{{ .Ident "name" }} IN ({{ .ArgList .KeeperNames }})
|
||||||
|
{{ .SelectFor "UPDATE" }}
|
||||||
|
;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
SELECT
|
||||||
|
{{ .Ident "guid" }},
|
||||||
|
{{ .Ident "name" }},
|
||||||
|
{{ .Ident "namespace" }},
|
||||||
|
{{ .Ident "annotations" }},
|
||||||
|
{{ .Ident "labels" }},
|
||||||
|
{{ .Ident "created" }},
|
||||||
|
{{ .Ident "created_by" }},
|
||||||
|
{{ .Ident "updated" }},
|
||||||
|
{{ .Ident "updated_by" }},
|
||||||
|
{{ .Ident "description" }},
|
||||||
|
{{ .Ident "type" }},
|
||||||
|
{{ .Ident "payload" }}
|
||||||
|
FROM
|
||||||
|
{{ .Ident "secret_keeper" }}
|
||||||
|
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
|
||||||
|
{{ .Ident "name" }} = {{ .Arg .Name }}
|
||||||
|
{{ if .IsForUpdate }}
|
||||||
|
{{ .SelectFor "UPDATE" }}
|
||||||
|
{{ end }}
|
||||||
|
;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
UPDATE
|
||||||
|
{{ .Ident "secret_keeper" }}
|
||||||
|
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 "description" }} = {{ .Arg .Row.Description }},
|
||||||
|
{{ .Ident "type" }} = {{ .Arg .Row.Type }},
|
||||||
|
{{ .Ident "payload" }} = {{ .Arg .Row.Payload }}
|
||||||
|
WHERE {{ .Ident "namespace" }} = {{ .Arg .Row.Namespace }} AND
|
||||||
|
{{ .Ident "name" }} = {{ .Arg .Row.Name }}
|
||||||
|
;
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package metadata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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/xkube"
|
||||||
|
"github.com/grafana/grafana/pkg/storage/secret/migrator"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type keeperDB 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
|
||||||
|
|
||||||
|
// Spec
|
||||||
|
Description string
|
||||||
|
Type string
|
||||||
|
Payload string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*keeperDB) TableName() string {
|
||||||
|
return migrator.TableNameKeeper
|
||||||
|
}
|
||||||
|
|
||||||
|
// toKubernetes maps a DB row into a Kubernetes resource (metadata + spec).
|
||||||
|
func (kp *keeperDB) toKubernetes() (*secretv0alpha1.Keeper, error) {
|
||||||
|
annotations := make(map[string]string, 0)
|
||||||
|
if kp.Annotations != "" {
|
||||||
|
if err := json.Unmarshal([]byte(kp.Annotations), &annotations); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal annotations: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
labels := make(map[string]string, 0)
|
||||||
|
if kp.Labels != "" {
|
||||||
|
if err := json.Unmarshal([]byte(kp.Labels), &labels); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal labels: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource := &secretv0alpha1.Keeper{
|
||||||
|
Spec: secretv0alpha1.KeeperSpec{
|
||||||
|
Description: kp.Description,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain provider configs
|
||||||
|
provider := toProvider(secretv0alpha1.KeeperType(kp.Type), kp.Payload)
|
||||||
|
switch v := provider.(type) {
|
||||||
|
case *secretv0alpha1.AWSKeeperConfig:
|
||||||
|
resource.Spec.AWS = v
|
||||||
|
case *secretv0alpha1.AzureKeeperConfig:
|
||||||
|
resource.Spec.Azure = v
|
||||||
|
case *secretv0alpha1.GCPKeeperConfig:
|
||||||
|
resource.Spec.GCP = v
|
||||||
|
case *secretv0alpha1.HashiCorpKeeperConfig:
|
||||||
|
resource.Spec.HashiCorp = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(kp.Updated, 0).UTC()
|
||||||
|
|
||||||
|
meta.SetUID(types.UID(kp.GUID))
|
||||||
|
meta.SetName(kp.Name)
|
||||||
|
meta.SetNamespace(kp.Namespace)
|
||||||
|
meta.SetAnnotations(annotations)
|
||||||
|
meta.SetLabels(labels)
|
||||||
|
meta.SetCreatedBy(kp.CreatedBy)
|
||||||
|
meta.SetCreationTimestamp(metav1.NewTime(time.Unix(kp.Created, 0).UTC()))
|
||||||
|
meta.SetUpdatedBy(kp.UpdatedBy)
|
||||||
|
meta.SetUpdatedTimestamp(&updated)
|
||||||
|
meta.SetResourceVersionInt64(kp.Updated)
|
||||||
|
|
||||||
|
return resource, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// toKeeperCreateRow maps a Kubernetes resource into a DB row for new resources being created/inserted.
|
||||||
|
func toKeeperCreateRow(kp *secretv0alpha1.Keeper, actorUID string) (*keeperDB, error) {
|
||||||
|
row, err := toKeeperRow(kp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to map to row: %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
|
||||||
|
}
|
||||||
|
|
||||||
|
// toKeeperUpdateRow maps a Kubernetes resource into a DB row for existing resources being updated.
|
||||||
|
func toKeeperUpdateRow(currentRow *keeperDB, newKeeper *secretv0alpha1.Keeper, actorUID string) (*keeperDB, error) {
|
||||||
|
row, err := toKeeperRow(newKeeper)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to map to row: %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
|
||||||
|
}
|
||||||
|
|
||||||
|
// toKeeperRow maps a Kubernetes Keeper resource into a Keeper DB row.
|
||||||
|
func toKeeperRow(kp *secretv0alpha1.Keeper) (*keeperDB, error) {
|
||||||
|
var annotations string
|
||||||
|
if len(kp.Annotations) > 0 {
|
||||||
|
cleanedAnnotations := xkube.CleanAnnotations(kp.Annotations)
|
||||||
|
if len(cleanedAnnotations) > 0 {
|
||||||
|
kp.Annotations = make(map[string]string) // Safety: reset to prohibit use of kp.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(kp.Labels) > 0 {
|
||||||
|
encodedLabels, err := json.Marshal(kp.Labels)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encode labels: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
labels = string(encodedLabels)
|
||||||
|
}
|
||||||
|
|
||||||
|
meta, err := utils.MetaAccessor(kp)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
keeperType, keeperPayload, err := toTypeAndPayload(kp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to obtain type and payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &keeperDB{
|
||||||
|
// Kubernetes Metadata
|
||||||
|
GUID: string(kp.UID),
|
||||||
|
Name: kp.Name,
|
||||||
|
Namespace: kp.Namespace,
|
||||||
|
Annotations: annotations,
|
||||||
|
Labels: labels,
|
||||||
|
Created: meta.GetCreationTimestamp().Unix(),
|
||||||
|
CreatedBy: meta.GetCreatedBy(),
|
||||||
|
Updated: updatedTimestamp,
|
||||||
|
UpdatedBy: meta.GetUpdatedBy(),
|
||||||
|
|
||||||
|
// Spec
|
||||||
|
Description: kp.Spec.Description,
|
||||||
|
Type: keeperType.String(),
|
||||||
|
Payload: keeperPayload,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// toTypeAndPayload obtain keeper type and payload from a Kubernetes Keeper resource.
|
||||||
|
// TODO: Move as method of KeeperSpec
|
||||||
|
func toTypeAndPayload(kp *secretv0alpha1.Keeper) (secretv0alpha1.KeeperType, string, error) {
|
||||||
|
if kp.Spec.AWS != nil {
|
||||||
|
payload, err := json.Marshal(kp.Spec.AWS.AWSCredentials)
|
||||||
|
return secretv0alpha1.AWSKeeperType, string(payload), err
|
||||||
|
} else if kp.Spec.Azure != nil {
|
||||||
|
payload, err := json.Marshal(kp.Spec.Azure)
|
||||||
|
return secretv0alpha1.AzureKeeperType, string(payload), err
|
||||||
|
} else if kp.Spec.GCP != nil {
|
||||||
|
payload, err := json.Marshal(kp.Spec.GCP)
|
||||||
|
return secretv0alpha1.GCPKeeperType, string(payload), err
|
||||||
|
} else if kp.Spec.HashiCorp != nil {
|
||||||
|
payload, err := json.Marshal(kp.Spec.HashiCorp)
|
||||||
|
return secretv0alpha1.HashiCorpKeeperType, string(payload), err
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", "", fmt.Errorf("no keeper type found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// toProvider maps a KeeperType and payload into a provider config struct.
|
||||||
|
// TODO: Move as method of KeeperType
|
||||||
|
func toProvider(keeperType secretv0alpha1.KeeperType, payload string) secretv0alpha1.KeeperConfig {
|
||||||
|
switch keeperType {
|
||||||
|
case secretv0alpha1.AWSKeeperType:
|
||||||
|
aws := &secretv0alpha1.AWSKeeperConfig{}
|
||||||
|
if err := json.Unmarshal([]byte(payload), aws); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return aws
|
||||||
|
case secretv0alpha1.AzureKeeperType:
|
||||||
|
azure := &secretv0alpha1.AzureKeeperConfig{}
|
||||||
|
if err := json.Unmarshal([]byte(payload), azure); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return azure
|
||||||
|
case secretv0alpha1.GCPKeeperType:
|
||||||
|
gcp := &secretv0alpha1.GCPKeeperConfig{}
|
||||||
|
if err := json.Unmarshal([]byte(payload), gcp); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return gcp
|
||||||
|
case secretv0alpha1.HashiCorpKeeperType:
|
||||||
|
hashicorp := &secretv0alpha1.HashiCorpKeeperConfig{}
|
||||||
|
if err := json.Unmarshal([]byte(payload), hashicorp); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return hashicorp
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,50 +2,276 @@ package metadata
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
claims "github.com/grafana/authlib/types"
|
|
||||||
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
|
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/contracts"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
|
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProvideKeeperMetadataStorage(db db.DB, features featuremgmt.FeatureToggles, accessClient claims.AccessClient) (contracts.KeeperMetadataStorage, error) {
|
// keeperMetadataStorage is the actual implementation of the keeper metadata storage.
|
||||||
|
type keeperMetadataStorage struct {
|
||||||
|
db contracts.Database
|
||||||
|
dialect sqltemplate.Dialect
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ contracts.KeeperMetadataStorage = (*keeperMetadataStorage)(nil)
|
||||||
|
|
||||||
|
func ProvideKeeperMetadataStorage(db contracts.Database, features featuremgmt.FeatureToggles) (contracts.KeeperMetadataStorage, error) {
|
||||||
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) ||
|
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) ||
|
||||||
!features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) {
|
!features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) {
|
||||||
return &keeperMetadataStorage{}, nil
|
return &keeperMetadataStorage{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &keeperMetadataStorage{db: db, accessClient: accessClient}, nil
|
return &keeperMetadataStorage{
|
||||||
}
|
db: db,
|
||||||
|
dialect: sqltemplate.DialectForDriver(db.DriverName()),
|
||||||
// keeperMetadataStorage is the actual implementation of the keeper metadata storage.
|
}, nil
|
||||||
type keeperMetadataStorage struct {
|
|
||||||
db db.DB
|
|
||||||
accessClient claims.AccessClient
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *keeperMetadataStorage) Create(ctx context.Context, keeper *secretv0alpha1.Keeper, actorUID string) (*secretv0alpha1.Keeper, error) {
|
func (s *keeperMetadataStorage) Create(ctx context.Context, keeper *secretv0alpha1.Keeper, actorUID string) (*secretv0alpha1.Keeper, error) {
|
||||||
return nil, nil
|
row, err := toKeeperCreateRow(keeper, actorUID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create row: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := createKeeper{
|
||||||
|
SQLTemplate: sqltemplate.New(s.dialect),
|
||||||
|
Row: row,
|
||||||
|
}
|
||||||
|
|
||||||
|
query, err := sqltemplate.Execute(sqlKeeperCreate, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("execute template %q: %w", sqlKeeperCreate.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.db.Transaction(ctx, func(ctx context.Context) error {
|
||||||
|
result, err := s.db.ExecContext(ctx, query, req.GetArgs()...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inserting 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, keeper.Name, keeper.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("db failure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
createdKeeper, err := row.toKubernetes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert to kubernetes object: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdKeeper, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *keeperMetadataStorage) Read(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (*secretv0alpha1.Keeper, error) {
|
func (s *keeperMetadataStorage) Read(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (*secretv0alpha1.Keeper, error) {
|
||||||
return nil, nil
|
keeperDB, err := s.read(ctx, namespace.String(), name, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keeper, err := keeperDB.toKubernetes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert to kubernetes object: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keeper, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *keeperMetadataStorage) read(ctx context.Context, namespace, name string, opts contracts.ReadOpts) (*keeperDB, error) {
|
||||||
|
req := &readKeeper{
|
||||||
|
SQLTemplate: sqltemplate.New(s.dialect),
|
||||||
|
Namespace: namespace,
|
||||||
|
Name: name,
|
||||||
|
IsForUpdate: opts.ForUpdate,
|
||||||
|
}
|
||||||
|
|
||||||
|
query, err := sqltemplate.Execute(sqlKeeperRead, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("execute template %q: %w", sqlKeeperRead.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := s.db.QueryContext(ctx, query, req.GetArgs()...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getting row for %s in namespace %s: %w", name, namespace, err)
|
||||||
|
}
|
||||||
|
defer func() { _ = res.Close() }()
|
||||||
|
|
||||||
|
if !res.Next() {
|
||||||
|
return nil, contracts.ErrKeeperNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
var keeper keeperDB
|
||||||
|
err = res.Scan(
|
||||||
|
&keeper.GUID, &keeper.Name, &keeper.Namespace, &keeper.Annotations, &keeper.Labels, &keeper.Created,
|
||||||
|
&keeper.CreatedBy, &keeper.Updated, &keeper.UpdatedBy, &keeper.Description, &keeper.Type, &keeper.Payload,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to scan keeper row: %w", err)
|
||||||
|
}
|
||||||
|
if err := res.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("read rows error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &keeper, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *keeperMetadataStorage) Update(ctx context.Context, newKeeper *secretv0alpha1.Keeper, actorUID string) (*secretv0alpha1.Keeper, error) {
|
func (s *keeperMetadataStorage) Update(ctx context.Context, newKeeper *secretv0alpha1.Keeper, actorUID string) (*secretv0alpha1.Keeper, error) {
|
||||||
return nil, nil
|
var newRow *keeperDB
|
||||||
|
|
||||||
|
err := s.db.Transaction(ctx, func(ctx context.Context) error {
|
||||||
|
// Read old value first.
|
||||||
|
oldKeeperRow, err := s.read(ctx, newKeeper.Namespace, newKeeper.Name, contracts.ReadOpts{ForUpdate: true})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate an update row model.
|
||||||
|
var updateErr error
|
||||||
|
newRow, updateErr = toKeeperUpdateRow(oldKeeperRow, newKeeper, actorUID)
|
||||||
|
if updateErr != nil {
|
||||||
|
return fmt.Errorf("failed to map into update row: %w", updateErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update query with new model.
|
||||||
|
req := &updateKeeper{
|
||||||
|
SQLTemplate: sqltemplate.New(s.dialect),
|
||||||
|
Row: newRow,
|
||||||
|
}
|
||||||
|
|
||||||
|
query, err := sqltemplate.Execute(sqlKeeperUpdate, req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("execute template %q: %w", sqlKeeperUpdate.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, newKeeper.Name, newKeeper.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("db failure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keeper, err := newRow.toKubernetes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert to kubernetes object: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keeper, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *keeperMetadataStorage) Delete(ctx context.Context, namespace xkube.Namespace, name string) error {
|
func (s *keeperMetadataStorage) Delete(ctx context.Context, namespace xkube.Namespace, name string) error {
|
||||||
|
req := deleteKeeper{
|
||||||
|
SQLTemplate: sqltemplate.New(s.dialect),
|
||||||
|
Namespace: namespace.String(),
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
|
|
||||||
|
query, err := sqltemplate.Execute(sqlKeeperDelete, req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("execute template %q: %w", sqlKeeperDelete.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := s.db.ExecContext(ctx, query, req.GetArgs()...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("deleting row: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting rows affected: %w", err)
|
||||||
|
}
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return contracts.ErrKeeperNotFound
|
||||||
|
} else if rowsAffected != 1 {
|
||||||
|
return fmt.Errorf("expected 1 row affected, got %d for %s on %s", rowsAffected, name, namespace)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *keeperMetadataStorage) List(ctx context.Context, namespace xkube.Namespace) ([]secretv0alpha1.Keeper, error) {
|
func (s *keeperMetadataStorage) List(ctx context.Context, namespace xkube.Namespace) ([]secretv0alpha1.Keeper, error) {
|
||||||
return nil, nil
|
req := listKeeper{
|
||||||
|
SQLTemplate: sqltemplate.New(s.dialect),
|
||||||
|
Namespace: namespace.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
query, err := sqltemplate.Execute(sqlKeeperList, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("execute template %q: %w", sqlKeeperList.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := s.db.QueryContext(ctx, query, req.GetArgs()...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("listing keepers %q: %w", sqlKeeperList.Name(), err)
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
|
||||||
|
keepers := make([]secretv0alpha1.Keeper, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var row keeperDB
|
||||||
|
err = rows.Scan(
|
||||||
|
&row.GUID, &row.Name, &row.Namespace, &row.Annotations, &row.Labels, &row.Created,
|
||||||
|
&row.CreatedBy, &row.Updated, &row.UpdatedBy, &row.Description, &row.Type, &row.Payload,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading keeper row: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keeper, err := row.toKubernetes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert to kubernetes object: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keepers = append(keepers, *keeper)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("read rows error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keepers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *keeperMetadataStorage) GetKeeperConfig(ctx context.Context, namespace string, name *string, opts contracts.ReadOpts) (secretv0alpha1.KeeperConfig, error) {
|
func (s *keeperMetadataStorage) GetKeeperConfig(ctx context.Context, namespace string, name *string, opts contracts.ReadOpts) (secretv0alpha1.KeeperConfig, error) {
|
||||||
return nil, nil
|
// Check if keeper is the systemwide one.
|
||||||
|
if name == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load keeper config from metadata store, or TODO: keeper cache.
|
||||||
|
kp, err := s.read(ctx, namespace, *name, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
keeperConfig := toProvider(secretv0alpha1.KeeperType(kp.Type), kp.Payload)
|
||||||
|
|
||||||
|
// TODO: this would be a good place to check if credentials are secure values and load them.
|
||||||
|
return keeperConfig, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,344 @@
|
|||||||
|
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 Test_KeeperMetadataStorage_GetKeeperConfig(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
defaultKeeperName := "kp-test"
|
||||||
|
defaultKeeperNS := "default"
|
||||||
|
|
||||||
|
testKeeper := &secretv0alpha1.Keeper{
|
||||||
|
Spec: secretv0alpha1.KeeperSpec{
|
||||||
|
Description: "description",
|
||||||
|
AWS: &secretv0alpha1.AWSKeeperConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testKeeper.Name = defaultKeeperName
|
||||||
|
testKeeper.Namespace = defaultKeeperNS
|
||||||
|
|
||||||
|
t.Run("get the system keeper config", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
keeperMetadataStorage := initStorage(t)
|
||||||
|
|
||||||
|
// get system keeper config
|
||||||
|
keeperConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, defaultKeeperNS, nil, contracts.ReadOpts{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, keeperConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("get test keeper config", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
keeperMetadataStorage := initStorage(t)
|
||||||
|
|
||||||
|
//
|
||||||
|
_, err := keeperMetadataStorage.Create(ctx, testKeeper, "testuser")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
keeperConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, defaultKeeperNS, &defaultKeeperName, contracts.ReadOpts{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, keeperConfig)
|
||||||
|
require.NotEmpty(t, keeperConfig.Type())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("get test keeper config when listing", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
keeperMetadataStorage := initStorage(t)
|
||||||
|
|
||||||
|
//
|
||||||
|
_, err := keeperMetadataStorage.Create(ctx, testKeeper, "testuser")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
keeperList, err := keeperMetadataStorage.List(ctx, xkube.Namespace(defaultKeeperNS))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, keeperList)
|
||||||
|
|
||||||
|
require.Len(t, keeperList, 1)
|
||||||
|
keeper := keeperList[0]
|
||||||
|
require.Equal(t, "kp-test", keeper.Name)
|
||||||
|
require.Equal(t, "default", keeper.Namespace)
|
||||||
|
require.Equal(t, "description", keeper.Spec.Description)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("create a keeper and then delete it", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
keeperMetadataStorage := initStorage(t)
|
||||||
|
|
||||||
|
keeperTest := "kp-test2"
|
||||||
|
keeperNamespaceTest := "ns"
|
||||||
|
|
||||||
|
testKeeper := &secretv0alpha1.Keeper{
|
||||||
|
Spec: secretv0alpha1.KeeperSpec{
|
||||||
|
Description: "another description",
|
||||||
|
AWS: &secretv0alpha1.AWSKeeperConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testKeeper.Name = keeperTest
|
||||||
|
testKeeper.Namespace = keeperNamespaceTest
|
||||||
|
|
||||||
|
// create the keeper
|
||||||
|
_, err := keeperMetadataStorage.Create(ctx, testKeeper, "testuser")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// we are able to get it
|
||||||
|
keeperConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, keeperNamespaceTest, &keeperTest, contracts.ReadOpts{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, keeperConfig)
|
||||||
|
require.NotEmpty(t, keeperConfig.Type())
|
||||||
|
|
||||||
|
// now we delete it
|
||||||
|
delErr := keeperMetadataStorage.Delete(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest)
|
||||||
|
require.NoError(t, delErr)
|
||||||
|
|
||||||
|
// and we shouldn't be able to get it again
|
||||||
|
_, getErr := keeperMetadataStorage.GetKeeperConfig(ctx, keeperNamespaceTest, &keeperTest, contracts.ReadOpts{})
|
||||||
|
require.Errorf(t, getErr, "keeper not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("create, update and validate keeper", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
keeperMetadataStorage := initStorage(t)
|
||||||
|
|
||||||
|
keeperTest := "kp-test3"
|
||||||
|
keeperNamespaceTest := "ns"
|
||||||
|
|
||||||
|
// Create initial keeper
|
||||||
|
initialKeeper := &secretv0alpha1.Keeper{
|
||||||
|
Spec: secretv0alpha1.KeeperSpec{
|
||||||
|
Description: "initial description",
|
||||||
|
AWS: &secretv0alpha1.AWSKeeperConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
initialKeeper.Name = keeperTest
|
||||||
|
initialKeeper.Namespace = keeperNamespaceTest
|
||||||
|
|
||||||
|
// Create the keeper
|
||||||
|
_, err := keeperMetadataStorage.Create(ctx, initialKeeper, "testuser")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Validate that the description was set
|
||||||
|
keeper, err := keeperMetadataStorage.Read(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest, contracts.ReadOpts{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "initial description", keeper.Spec.Description)
|
||||||
|
|
||||||
|
// Update the keeper with new values
|
||||||
|
updatedKeeper := &secretv0alpha1.Keeper{
|
||||||
|
Spec: secretv0alpha1.KeeperSpec{
|
||||||
|
Description: "updated description",
|
||||||
|
AWS: &secretv0alpha1.AWSKeeperConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
updatedKeeper.Name = keeperTest
|
||||||
|
updatedKeeper.Namespace = keeperNamespaceTest
|
||||||
|
|
||||||
|
// Perform the update
|
||||||
|
_, err = keeperMetadataStorage.Update(ctx, updatedKeeper, "testuser")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Validate updated values
|
||||||
|
updatedConfig, err := keeperMetadataStorage.GetKeeperConfig(ctx, keeperNamespaceTest, &keeperTest, contracts.ReadOpts{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, updatedConfig)
|
||||||
|
require.NotEmpty(t, updatedConfig.Type())
|
||||||
|
|
||||||
|
// Validate that the description was updated
|
||||||
|
updatedKeeper, err = keeperMetadataStorage.Read(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest, contracts.ReadOpts{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "updated description", updatedKeeper.Spec.Description)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("update keeper with different AWS configuration", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
keeperMetadataStorage := initStorage(t)
|
||||||
|
|
||||||
|
keeperTest := "kp-test4"
|
||||||
|
keeperNamespaceTest := "ns"
|
||||||
|
|
||||||
|
// Create initial keeper with first AWS config
|
||||||
|
initialKeeper := &secretv0alpha1.Keeper{
|
||||||
|
Spec: secretv0alpha1.KeeperSpec{
|
||||||
|
Description: "initial description",
|
||||||
|
AWS: &secretv0alpha1.AWSKeeperConfig{
|
||||||
|
AWSCredentials: secretv0alpha1.AWSCredentials{
|
||||||
|
AccessKeyID: secretv0alpha1.CredentialValue{
|
||||||
|
ValueFromEnv: "AWS_ACCESS_KEY_ID_1",
|
||||||
|
},
|
||||||
|
SecretAccessKey: secretv0alpha1.CredentialValue{
|
||||||
|
ValueFromEnv: "AWS_SECRET_ACCESS_KEY_1",
|
||||||
|
},
|
||||||
|
KMSKeyID: "kms-key-id-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
initialKeeper.Name = keeperTest
|
||||||
|
initialKeeper.Namespace = keeperNamespaceTest
|
||||||
|
|
||||||
|
// Create the keeper
|
||||||
|
_, err := keeperMetadataStorage.Create(ctx, initialKeeper, "testuser")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify initial AWS config
|
||||||
|
keeper, err := keeperMetadataStorage.Read(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest, contracts.ReadOpts{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "AWS_ACCESS_KEY_ID_1", keeper.Spec.AWS.AccessKeyID.ValueFromEnv)
|
||||||
|
require.Equal(t, "AWS_SECRET_ACCESS_KEY_1", keeper.Spec.AWS.SecretAccessKey.ValueFromEnv)
|
||||||
|
require.Equal(t, "kms-key-id-1", keeper.Spec.AWS.KMSKeyID)
|
||||||
|
|
||||||
|
// Update with new AWS config
|
||||||
|
updatedKeeper := &secretv0alpha1.Keeper{
|
||||||
|
Spec: secretv0alpha1.KeeperSpec{
|
||||||
|
Description: "updated description",
|
||||||
|
AWS: &secretv0alpha1.AWSKeeperConfig{
|
||||||
|
AWSCredentials: secretv0alpha1.AWSCredentials{
|
||||||
|
AccessKeyID: secretv0alpha1.CredentialValue{
|
||||||
|
ValueFromEnv: "AWS_ACCESS_KEY_ID_2",
|
||||||
|
},
|
||||||
|
SecretAccessKey: secretv0alpha1.CredentialValue{
|
||||||
|
ValueFromEnv: "AWS_SECRET_ACCESS_KEY_2",
|
||||||
|
},
|
||||||
|
KMSKeyID: "kms-key-id-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
updatedKeeper.Name = keeperTest
|
||||||
|
updatedKeeper.Namespace = keeperNamespaceTest
|
||||||
|
|
||||||
|
// Perform the update
|
||||||
|
_, err = keeperMetadataStorage.Update(ctx, updatedKeeper, "testuser")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify updated AWS config
|
||||||
|
updatedKeeper, err = keeperMetadataStorage.Read(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest, contracts.ReadOpts{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "AWS_ACCESS_KEY_ID_2", updatedKeeper.Spec.AWS.AccessKeyID.ValueFromEnv)
|
||||||
|
require.Equal(t, "AWS_SECRET_ACCESS_KEY_2", updatedKeeper.Spec.AWS.SecretAccessKey.ValueFromEnv)
|
||||||
|
require.Equal(t, "kms-key-id-2", updatedKeeper.Spec.AWS.KMSKeyID)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("list keepers in empty namespace", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
keeperMetadataStorage := initStorage(t)
|
||||||
|
|
||||||
|
keeperList, err := keeperMetadataStorage.List(ctx, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Empty(t, keeperList)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("read non-existent keeper", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
keeperMetadataStorage := initStorage(t)
|
||||||
|
|
||||||
|
_, err := keeperMetadataStorage.Read(ctx, "ns", "non-existent", contracts.ReadOpts{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, contracts.ErrKeeperNotFound, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("update keeper with different namespace", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
keeperMetadataStorage := initStorage(t)
|
||||||
|
|
||||||
|
keeperTest := "kp-test5"
|
||||||
|
keeperNamespaceTest := "ns1"
|
||||||
|
|
||||||
|
// Create initial keeper
|
||||||
|
initialKeeper := &secretv0alpha1.Keeper{
|
||||||
|
Spec: secretv0alpha1.KeeperSpec{
|
||||||
|
Description: "initial description",
|
||||||
|
AWS: &secretv0alpha1.AWSKeeperConfig{
|
||||||
|
AWSCredentials: secretv0alpha1.AWSCredentials{
|
||||||
|
AccessKeyID: secretv0alpha1.CredentialValue{
|
||||||
|
ValueFromEnv: "AWS_ACCESS_KEY_ID",
|
||||||
|
},
|
||||||
|
SecretAccessKey: secretv0alpha1.CredentialValue{
|
||||||
|
ValueFromEnv: "AWS_SECRET_ACCESS_KEY",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
initialKeeper.Name = keeperTest
|
||||||
|
initialKeeper.Namespace = keeperNamespaceTest
|
||||||
|
|
||||||
|
// Create the keeper
|
||||||
|
_, err := keeperMetadataStorage.Create(ctx, initialKeeper, "testuser")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Try to update with different namespace
|
||||||
|
updatedKeeper := initialKeeper.DeepCopy()
|
||||||
|
updatedKeeper.Namespace = "ns2"
|
||||||
|
updatedKeeper.Spec.Description = "updated description"
|
||||||
|
|
||||||
|
_, err = keeperMetadataStorage.Update(ctx, updatedKeeper, "testuser")
|
||||||
|
// should this return contracts.ErrKeeperNotFound directly?
|
||||||
|
// require.Equal(t, contracts.ErrKeeperNotFound, err)
|
||||||
|
require.Error(t, err, "db failure: keeper not found")
|
||||||
|
|
||||||
|
// Verify original keeper is unchanged
|
||||||
|
keeper, err := keeperMetadataStorage.Read(ctx, xkube.Namespace(keeperNamespaceTest), keeperTest, contracts.ReadOpts{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "initial description", keeper.Spec.Description)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("update non-existent keeper", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
keeperMetadataStorage := initStorage(t)
|
||||||
|
|
||||||
|
nonExistentKeeper := &secretv0alpha1.Keeper{
|
||||||
|
Spec: secretv0alpha1.KeeperSpec{
|
||||||
|
Description: "some description",
|
||||||
|
AWS: &secretv0alpha1.AWSKeeperConfig{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
nonExistentKeeper.Name = "non-existent"
|
||||||
|
nonExistentKeeper.Namespace = "ns"
|
||||||
|
|
||||||
|
_, err := keeperMetadataStorage.Update(ctx, nonExistentKeeper, "testuser")
|
||||||
|
require.Error(t, err, "db failure: keeper not found")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func initStorage(t *testing.T) contracts.KeeperMetadataStorage {
|
||||||
|
testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
|
||||||
|
db := database.ProvideDatabase(testDB)
|
||||||
|
features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform)
|
||||||
|
|
||||||
|
// Initialize the keeper storage
|
||||||
|
keeperMetadataStorage, err := ProvideKeeperMetadataStorage(db, features)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return keeperMetadataStorage
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package metadata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed data/*.sql
|
||||||
|
sqlTemplatesFS embed.FS
|
||||||
|
|
||||||
|
sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `data/*.sql`))
|
||||||
|
|
||||||
|
// The SQL Commands
|
||||||
|
sqlKeeperCreate = mustTemplate("keeper_create.sql")
|
||||||
|
sqlKeeperRead = mustTemplate("keeper_read.sql")
|
||||||
|
sqlKeeperUpdate = mustTemplate("keeper_update.sql")
|
||||||
|
sqlKeeperList = mustTemplate("keeper_list.sql")
|
||||||
|
sqlKeeperDelete = mustTemplate("keeper_delete.sql")
|
||||||
|
|
||||||
|
sqlKeeperListByName = mustTemplate("keeper_listByName.sql")
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustTemplate(filename string) *template.Template {
|
||||||
|
if t := sqlTemplates.Lookup(filename); t != nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
panic(fmt.Sprintf("template file not found: %s", filename))
|
||||||
|
}
|
||||||
|
|
||||||
|
/************************/
|
||||||
|
/**-- Keeper Queries --**/
|
||||||
|
/************************/
|
||||||
|
|
||||||
|
// Create
|
||||||
|
type createKeeper struct {
|
||||||
|
sqltemplate.SQLTemplate
|
||||||
|
Row *keeperDB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate is only used if we use `dbutil` from `unifiedstorage`
|
||||||
|
func (r createKeeper) Validate() error {
|
||||||
|
return nil // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read
|
||||||
|
type readKeeper struct {
|
||||||
|
sqltemplate.SQLTemplate
|
||||||
|
Namespace string
|
||||||
|
Name string
|
||||||
|
IsForUpdate bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate is only used if we use `dbutil` from `unifiedstorage`
|
||||||
|
func (r readKeeper) Validate() error {
|
||||||
|
return nil // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update
|
||||||
|
type updateKeeper struct {
|
||||||
|
sqltemplate.SQLTemplate
|
||||||
|
Row *keeperDB
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate is only used if we use `dbutil` from `unifiedstorage`
|
||||||
|
func (r updateKeeper) Validate() error {
|
||||||
|
return nil // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
// List
|
||||||
|
type listKeeper struct {
|
||||||
|
sqltemplate.SQLTemplate
|
||||||
|
Namespace string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate is only used if we use `dbutil` from `unifiedstorage`
|
||||||
|
func (r listKeeper) Validate() error {
|
||||||
|
return nil // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
type deleteKeeper struct {
|
||||||
|
sqltemplate.SQLTemplate
|
||||||
|
Namespace string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate is only used if we use `dbutil` from `unifiedstorage`
|
||||||
|
func (r deleteKeeper) Validate() error {
|
||||||
|
return nil // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is used at keeper store to validate create & update operations
|
||||||
|
type listByNameKeeper struct {
|
||||||
|
sqltemplate.SQLTemplate
|
||||||
|
Namespace string
|
||||||
|
KeeperNames []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate is only used if we use `dbutil` from `unifiedstorage`
|
||||||
|
func (r listByNameKeeper) Validate() error {
|
||||||
|
return nil // TODO
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package metadata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKeeperQueries(t *testing.T) {
|
||||||
|
mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{
|
||||||
|
RootDir: "testdata",
|
||||||
|
Templates: map[*template.Template][]mocks.TemplateTestCase{
|
||||||
|
sqlKeeperCreate: {
|
||||||
|
{
|
||||||
|
Name: "create",
|
||||||
|
Data: &createKeeper{
|
||||||
|
SQLTemplate: mocks.NewTestingSQLTemplate(),
|
||||||
|
Row: &keeperDB{
|
||||||
|
GUID: "abc",
|
||||||
|
Name: "name",
|
||||||
|
Namespace: "ns",
|
||||||
|
Annotations: `{"x":"XXXX"}`,
|
||||||
|
Labels: `{"a":"AAA", "b", "BBBB"}`,
|
||||||
|
Created: 1234,
|
||||||
|
CreatedBy: "user:ryan",
|
||||||
|
Updated: 5678,
|
||||||
|
UpdatedBy: "user:cameron",
|
||||||
|
Description: "description",
|
||||||
|
Type: "sql",
|
||||||
|
Payload: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sqlKeeperDelete: {
|
||||||
|
{
|
||||||
|
Name: "delete",
|
||||||
|
Data: &deleteKeeper{
|
||||||
|
SQLTemplate: mocks.NewTestingSQLTemplate(),
|
||||||
|
Name: "name",
|
||||||
|
Namespace: "ns",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sqlKeeperList: {
|
||||||
|
{
|
||||||
|
Name: "list",
|
||||||
|
Data: &listKeeper{
|
||||||
|
SQLTemplate: mocks.NewTestingSQLTemplate(),
|
||||||
|
Namespace: "ns",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sqlKeeperRead: {
|
||||||
|
{
|
||||||
|
Name: "read",
|
||||||
|
Data: &readKeeper{
|
||||||
|
SQLTemplate: mocks.NewTestingSQLTemplate(),
|
||||||
|
Name: "name",
|
||||||
|
Namespace: "ns",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "read-for-update",
|
||||||
|
Data: &readKeeper{
|
||||||
|
SQLTemplate: mocks.NewTestingSQLTemplate(),
|
||||||
|
Name: "name",
|
||||||
|
Namespace: "ns",
|
||||||
|
IsForUpdate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sqlKeeperUpdate: {
|
||||||
|
{
|
||||||
|
Name: "update",
|
||||||
|
Data: &updateKeeper{
|
||||||
|
SQLTemplate: mocks.NewTestingSQLTemplate(),
|
||||||
|
Row: &keeperDB{
|
||||||
|
GUID: "abc",
|
||||||
|
Name: "name",
|
||||||
|
Namespace: "ns",
|
||||||
|
Annotations: `{"x":"XXXX"}`,
|
||||||
|
Labels: `{"a":"AAA", "b", "BBBB"}`,
|
||||||
|
Created: 1234,
|
||||||
|
CreatedBy: "user:ryan",
|
||||||
|
Updated: 5678,
|
||||||
|
UpdatedBy: "user:cameron",
|
||||||
|
Description: "description",
|
||||||
|
Type: "sql",
|
||||||
|
Payload: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sqlKeeperListByName: {
|
||||||
|
{
|
||||||
|
Name: "list",
|
||||||
|
Data: listByNameKeeper{
|
||||||
|
SQLTemplate: mocks.NewTestingSQLTemplate(),
|
||||||
|
Namespace: "ns",
|
||||||
|
KeeperNames: []string{"a", "b"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
INSERT INTO `secret_keeper` (
|
||||||
|
`guid`,
|
||||||
|
`name`,
|
||||||
|
`namespace`,
|
||||||
|
`annotations`,
|
||||||
|
`labels`,
|
||||||
|
`created`,
|
||||||
|
`created_by`,
|
||||||
|
`updated`,
|
||||||
|
`updated_by`,
|
||||||
|
`description`,
|
||||||
|
`type`,
|
||||||
|
`payload`
|
||||||
|
) VALUES (
|
||||||
|
'abc',
|
||||||
|
'name',
|
||||||
|
'ns',
|
||||||
|
'{"x":"XXXX"}',
|
||||||
|
'{"a":"AAA", "b", "BBBB"}',
|
||||||
|
1234,
|
||||||
|
'user:ryan',
|
||||||
|
5678,
|
||||||
|
'user:cameron',
|
||||||
|
'description',
|
||||||
|
'sql',
|
||||||
|
''
|
||||||
|
);
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
DELETE FROM `secret_keeper`
|
||||||
|
WHERE `namespace` = 'ns' AND
|
||||||
|
`name` = 'name'
|
||||||
|
;
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
SELECT
|
||||||
|
`guid`,
|
||||||
|
`name`,
|
||||||
|
`namespace`,
|
||||||
|
`annotations`,
|
||||||
|
`labels`,
|
||||||
|
`created`,
|
||||||
|
`created_by`,
|
||||||
|
`updated`,
|
||||||
|
`updated_by`,
|
||||||
|
`description`,
|
||||||
|
`type`,
|
||||||
|
`payload`
|
||||||
|
FROM
|
||||||
|
`secret_keeper`
|
||||||
|
WHERE `namespace` = 'ns'
|
||||||
|
ORDER BY `updated` DESC
|
||||||
|
;
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
SELECT
|
||||||
|
`name`
|
||||||
|
FROM
|
||||||
|
`secret_keeper`
|
||||||
|
WHERE `namespace` = 'ns' AND
|
||||||
|
`name` IN ('a', 'b')
|
||||||
|
FOR UPDATE
|
||||||
|
;
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
SELECT
|
||||||
|
`guid`,
|
||||||
|
`name`,
|
||||||
|
`namespace`,
|
||||||
|
`annotations`,
|
||||||
|
`labels`,
|
||||||
|
`created`,
|
||||||
|
`created_by`,
|
||||||
|
`updated`,
|
||||||
|
`updated_by`,
|
||||||
|
`description`,
|
||||||
|
`type`,
|
||||||
|
`payload`
|
||||||
|
FROM
|
||||||
|
`secret_keeper`
|
||||||
|
WHERE `namespace` = 'ns' AND
|
||||||
|
`name` = 'name'
|
||||||
|
FOR UPDATE
|
||||||
|
;
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
SELECT
|
||||||
|
`guid`,
|
||||||
|
`name`,
|
||||||
|
`namespace`,
|
||||||
|
`annotations`,
|
||||||
|
`labels`,
|
||||||
|
`created`,
|
||||||
|
`created_by`,
|
||||||
|
`updated`,
|
||||||
|
`updated_by`,
|
||||||
|
`description`,
|
||||||
|
`type`,
|
||||||
|
`payload`
|
||||||
|
FROM
|
||||||
|
`secret_keeper`
|
||||||
|
WHERE `namespace` = 'ns' AND
|
||||||
|
`name` = 'name'
|
||||||
|
;
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
UPDATE
|
||||||
|
`secret_keeper`
|
||||||
|
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',
|
||||||
|
`description` = 'description',
|
||||||
|
`type` = 'sql',
|
||||||
|
`payload` = ''
|
||||||
|
WHERE `namespace` = 'ns' AND
|
||||||
|
`name` = 'name'
|
||||||
|
;
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
INSERT INTO "secret_keeper" (
|
||||||
|
"guid",
|
||||||
|
"name",
|
||||||
|
"namespace",
|
||||||
|
"annotations",
|
||||||
|
"labels",
|
||||||
|
"created",
|
||||||
|
"created_by",
|
||||||
|
"updated",
|
||||||
|
"updated_by",
|
||||||
|
"description",
|
||||||
|
"type",
|
||||||
|
"payload"
|
||||||
|
) VALUES (
|
||||||
|
'abc',
|
||||||
|
'name',
|
||||||
|
'ns',
|
||||||
|
'{"x":"XXXX"}',
|
||||||
|
'{"a":"AAA", "b", "BBBB"}',
|
||||||
|
1234,
|
||||||
|
'user:ryan',
|
||||||
|
5678,
|
||||||
|
'user:cameron',
|
||||||
|
'description',
|
||||||
|
'sql',
|
||||||
|
''
|
||||||
|
);
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
DELETE FROM "secret_keeper"
|
||||||
|
WHERE "namespace" = 'ns' AND
|
||||||
|
"name" = 'name'
|
||||||
|
;
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
SELECT
|
||||||
|
"guid",
|
||||||
|
"name",
|
||||||
|
"namespace",
|
||||||
|
"annotations",
|
||||||
|
"labels",
|
||||||
|
"created",
|
||||||
|
"created_by",
|
||||||
|
"updated",
|
||||||
|
"updated_by",
|
||||||
|
"description",
|
||||||
|
"type",
|
||||||
|
"payload"
|
||||||
|
FROM
|
||||||
|
"secret_keeper"
|
||||||
|
WHERE "namespace" = 'ns'
|
||||||
|
ORDER BY "updated" DESC
|
||||||
|
;
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
SELECT
|
||||||
|
"name"
|
||||||
|
FROM
|
||||||
|
"secret_keeper"
|
||||||
|
WHERE "namespace" = 'ns' AND
|
||||||
|
"name" IN ('a', 'b')
|
||||||
|
FOR UPDATE
|
||||||
|
;
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
SELECT
|
||||||
|
"guid",
|
||||||
|
"name",
|
||||||
|
"namespace",
|
||||||
|
"annotations",
|
||||||
|
"labels",
|
||||||
|
"created",
|
||||||
|
"created_by",
|
||||||
|
"updated",
|
||||||
|
"updated_by",
|
||||||
|
"description",
|
||||||
|
"type",
|
||||||
|
"payload"
|
||||||
|
FROM
|
||||||
|
"secret_keeper"
|
||||||
|
WHERE "namespace" = 'ns' AND
|
||||||
|
"name" = 'name'
|
||||||
|
FOR UPDATE
|
||||||
|
;
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
SELECT
|
||||||
|
"guid",
|
||||||
|
"name",
|
||||||
|
"namespace",
|
||||||
|
"annotations",
|
||||||
|
"labels",
|
||||||
|
"created",
|
||||||
|
"created_by",
|
||||||
|
"updated",
|
||||||
|
"updated_by",
|
||||||
|
"description",
|
||||||
|
"type",
|
||||||
|
"payload"
|
||||||
|
FROM
|
||||||
|
"secret_keeper"
|
||||||
|
WHERE "namespace" = 'ns' AND
|
||||||
|
"name" = 'name'
|
||||||
|
;
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
UPDATE
|
||||||
|
"secret_keeper"
|
||||||
|
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',
|
||||||
|
"description" = 'description',
|
||||||
|
"type" = 'sql',
|
||||||
|
"payload" = ''
|
||||||
|
WHERE "namespace" = 'ns' AND
|
||||||
|
"name" = 'name'
|
||||||
|
;
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
INSERT INTO "secret_keeper" (
|
||||||
|
"guid",
|
||||||
|
"name",
|
||||||
|
"namespace",
|
||||||
|
"annotations",
|
||||||
|
"labels",
|
||||||
|
"created",
|
||||||
|
"created_by",
|
||||||
|
"updated",
|
||||||
|
"updated_by",
|
||||||
|
"description",
|
||||||
|
"type",
|
||||||
|
"payload"
|
||||||
|
) VALUES (
|
||||||
|
'abc',
|
||||||
|
'name',
|
||||||
|
'ns',
|
||||||
|
'{"x":"XXXX"}',
|
||||||
|
'{"a":"AAA", "b", "BBBB"}',
|
||||||
|
1234,
|
||||||
|
'user:ryan',
|
||||||
|
5678,
|
||||||
|
'user:cameron',
|
||||||
|
'description',
|
||||||
|
'sql',
|
||||||
|
''
|
||||||
|
);
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
DELETE FROM "secret_keeper"
|
||||||
|
WHERE "namespace" = 'ns' AND
|
||||||
|
"name" = 'name'
|
||||||
|
;
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
SELECT
|
||||||
|
"guid",
|
||||||
|
"name",
|
||||||
|
"namespace",
|
||||||
|
"annotations",
|
||||||
|
"labels",
|
||||||
|
"created",
|
||||||
|
"created_by",
|
||||||
|
"updated",
|
||||||
|
"updated_by",
|
||||||
|
"description",
|
||||||
|
"type",
|
||||||
|
"payload"
|
||||||
|
FROM
|
||||||
|
"secret_keeper"
|
||||||
|
WHERE "namespace" = 'ns'
|
||||||
|
ORDER BY "updated" DESC
|
||||||
|
;
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
SELECT
|
||||||
|
"name"
|
||||||
|
FROM
|
||||||
|
"secret_keeper"
|
||||||
|
WHERE "namespace" = 'ns' AND
|
||||||
|
"name" IN ('a', 'b')
|
||||||
|
;
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
SELECT
|
||||||
|
"guid",
|
||||||
|
"name",
|
||||||
|
"namespace",
|
||||||
|
"annotations",
|
||||||
|
"labels",
|
||||||
|
"created",
|
||||||
|
"created_by",
|
||||||
|
"updated",
|
||||||
|
"updated_by",
|
||||||
|
"description",
|
||||||
|
"type",
|
||||||
|
"payload"
|
||||||
|
FROM
|
||||||
|
"secret_keeper"
|
||||||
|
WHERE "namespace" = 'ns' AND
|
||||||
|
"name" = 'name'
|
||||||
|
;
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
SELECT
|
||||||
|
"guid",
|
||||||
|
"name",
|
||||||
|
"namespace",
|
||||||
|
"annotations",
|
||||||
|
"labels",
|
||||||
|
"created",
|
||||||
|
"created_by",
|
||||||
|
"updated",
|
||||||
|
"updated_by",
|
||||||
|
"description",
|
||||||
|
"type",
|
||||||
|
"payload"
|
||||||
|
FROM
|
||||||
|
"secret_keeper"
|
||||||
|
WHERE "namespace" = 'ns' AND
|
||||||
|
"name" = 'name'
|
||||||
|
;
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
UPDATE
|
||||||
|
"secret_keeper"
|
||||||
|
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',
|
||||||
|
"description" = 'description',
|
||||||
|
"type" = 'sql',
|
||||||
|
"payload" = ''
|
||||||
|
WHERE "namespace" = 'ns' AND
|
||||||
|
"name" = 'name'
|
||||||
|
;
|
||||||
@@ -0,0 +1,569 @@
|
|||||||
|
package secret
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"math/rand/v2"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/tests/apis"
|
||||||
|
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
var gvrKeepers = schema.GroupVersionResource{
|
||||||
|
Group: secretv0alpha1.GROUP,
|
||||||
|
Version: secretv0alpha1.VERSION,
|
||||||
|
Resource: secretv0alpha1.KeeperResourceInfo.GetName(),
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegrationKeeper(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||||
|
AppModeProduction: false, // required for experimental APIs
|
||||||
|
EnableFeatureToggles: []string{
|
||||||
|
// Required to start the example service
|
||||||
|
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
|
||||||
|
featuremgmt.FlagSecretsManagementAppPlatform,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
permissions := map[string]ResourcePermission{ResourceKeepers: {Actions: ActionsAllKeepers}}
|
||||||
|
|
||||||
|
genericUserEditor := mustCreateUsers(t, helper, permissions).Editor
|
||||||
|
|
||||||
|
client := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||||
|
User: genericUserEditor,
|
||||||
|
GVR: gvrKeepers,
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("reading a keeper that does not exist returns a 404", func(t *testing.T) {
|
||||||
|
raw, err := client.Resource.Get(ctx, "some-keeper-that-does-not-exist", metav1.GetOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, raw)
|
||||||
|
|
||||||
|
var statusErr *apierrors.StatusError
|
||||||
|
require.True(t, errors.As(err, &statusErr))
|
||||||
|
require.Equal(t, "keeper.secret.grafana.app \"some-keeper-that-does-not-exist\" not found", err.Error())
|
||||||
|
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("deleting a keeper that does not exist returns an error", func(t *testing.T) {
|
||||||
|
err := client.Resource.Delete(ctx, "some-keeper-that-does-not-exist", metav1.DeleteOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
var statusErr *apierrors.StatusError
|
||||||
|
require.True(t, errors.As(err, &statusErr))
|
||||||
|
require.Equal(t, "keeper.secret.grafana.app \"some-keeper-that-does-not-exist\" not found", err.Error())
|
||||||
|
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("creating a keeper returns it", func(t *testing.T) {
|
||||||
|
raw := mustGenerateKeeper(t, helper, genericUserEditor, nil, "testdata/keeper-aws-generate.yaml")
|
||||||
|
|
||||||
|
keeper := new(secretv0alpha1.Keeper)
|
||||||
|
err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, keeper)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, keeper)
|
||||||
|
|
||||||
|
require.NotEmpty(t, keeper.Spec.Description)
|
||||||
|
require.NotEmpty(t, keeper.Spec.AWS)
|
||||||
|
require.Empty(t, keeper.Spec.Azure)
|
||||||
|
|
||||||
|
t.Run("and creating another keeper with the same name in the same namespace returns an error", func(t *testing.T) {
|
||||||
|
testKeeper := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml")
|
||||||
|
testKeeper.SetName(raw.GetName())
|
||||||
|
|
||||||
|
raw, err := client.Resource.Create(ctx, testKeeper, metav1.CreateOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, raw)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("and reading th keeper returns it same as if when it was created", func(t *testing.T) {
|
||||||
|
raw, err := client.Resource.Get(ctx, keeper.Name, metav1.GetOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, raw)
|
||||||
|
|
||||||
|
anotherKeeper := new(secretv0alpha1.Keeper)
|
||||||
|
err = runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, anotherKeeper)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, anotherKeeper)
|
||||||
|
|
||||||
|
require.EqualValues(t, keeper, anotherKeeper)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("and listing keepers returns the created keeper", func(t *testing.T) {
|
||||||
|
rawList, err := client.Resource.List(ctx, metav1.ListOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rawList)
|
||||||
|
require.GreaterOrEqual(t, len(rawList.Items), 1)
|
||||||
|
require.Equal(t, keeper.Name, rawList.Items[0].GetName())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("and updating the keeper replaces the spec fields and returns them", func(t *testing.T) {
|
||||||
|
newRaw := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml")
|
||||||
|
newRaw.SetName(raw.GetName())
|
||||||
|
newRaw.Object["spec"].(map[string]any)["description"] = "New description"
|
||||||
|
newRaw.Object["metadata"].(map[string]any)["annotations"] = map[string]any{"newAnnotation": "newValue"}
|
||||||
|
|
||||||
|
updatedRaw, err := client.Resource.Update(ctx, newRaw, metav1.UpdateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, updatedRaw)
|
||||||
|
|
||||||
|
updatedKeeper := new(secretv0alpha1.Keeper)
|
||||||
|
err = runtime.DefaultUnstructuredConverter.FromUnstructured(updatedRaw.Object, updatedKeeper)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, updatedKeeper)
|
||||||
|
|
||||||
|
require.NotEqualValues(t, updatedKeeper.Spec, keeper.Spec)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("and updating the keeper to reference securevalues that does not exist returns an error", func(t *testing.T) {
|
||||||
|
t.Skip("skipping because storing credentials as securevalues is not implemented yet")
|
||||||
|
newRaw := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml")
|
||||||
|
newRaw.SetName(raw.GetName())
|
||||||
|
newRaw.Object["spec"].(map[string]any)["aws"] = map[string]any{
|
||||||
|
"accessKeyId": map[string]any{
|
||||||
|
"secureValueName": "securevalue-does-not-exist-1",
|
||||||
|
},
|
||||||
|
"secretAccessKey": map[string]any{
|
||||||
|
"secureValueName": "securevalue-does-not-exist-2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedRaw, err := client.Resource.Update(ctx, newRaw, metav1.UpdateOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, updatedRaw)
|
||||||
|
require.Contains(t, err.Error(), "securevalue-does-not-exist-1")
|
||||||
|
require.Contains(t, err.Error(), "securevalue-does-not-exist-2")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("creating an invalid keeper fails validation and returns an error", func(t *testing.T) {
|
||||||
|
testData := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml")
|
||||||
|
testData.Object["spec"].(map[string]any)["description"] = ""
|
||||||
|
|
||||||
|
raw, err := client.Resource.Create(ctx, testData, metav1.CreateOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, raw)
|
||||||
|
|
||||||
|
var statusErr *apierrors.StatusError
|
||||||
|
require.True(t, errors.As(err, &statusErr))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("creating a keeper with a provider then changing the provider does not return an error", func(t *testing.T) {
|
||||||
|
rawAWS := mustGenerateKeeper(t, helper, genericUserEditor, nil, "testdata/keeper-aws-generate.yaml")
|
||||||
|
|
||||||
|
testDataKeeperGCP := rawAWS.DeepCopy()
|
||||||
|
testDataKeeperGCP.Object["spec"].(map[string]any)["aws"] = nil
|
||||||
|
testDataKeeperGCP.Object["spec"].(map[string]any)["gcp"] = map[string]any{
|
||||||
|
"projectId": "project-id",
|
||||||
|
"credentialsFile": "/path/to/file.json",
|
||||||
|
}
|
||||||
|
|
||||||
|
rawGCP, err := client.Resource.Update(ctx, testDataKeeperGCP, metav1.UpdateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rawGCP)
|
||||||
|
|
||||||
|
require.NotEqualValues(t, rawAWS.Object["spec"], rawGCP.Object["spec"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("creating a keeper that references securevalues that does not exist returns an error", func(t *testing.T) {
|
||||||
|
t.Skip("skipping because storing credentials as securevalues is not implemented yet")
|
||||||
|
testDataKeeper := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml")
|
||||||
|
testDataKeeper.Object["spec"].(map[string]any)["aws"] = map[string]any{
|
||||||
|
"accessKeyId": map[string]any{
|
||||||
|
"secureValueName": "securevalue-does-not-exist-1",
|
||||||
|
},
|
||||||
|
"secretAccessKey": map[string]any{
|
||||||
|
"secureValueName": "securevalue-does-not-exist-2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := client.Resource.Create(ctx, testDataKeeper, metav1.CreateOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, raw)
|
||||||
|
require.Contains(t, err.Error(), "securevalue-does-not-exist-1")
|
||||||
|
require.Contains(t, err.Error(), "securevalue-does-not-exist-2")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("deleting a keeper that exists does not return an error", func(t *testing.T) {
|
||||||
|
generatePrefix := "generated-"
|
||||||
|
|
||||||
|
testData := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml")
|
||||||
|
testData.SetGenerateName(generatePrefix)
|
||||||
|
|
||||||
|
raw, err := client.Resource.Create(ctx, testData, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, raw)
|
||||||
|
|
||||||
|
name := raw.GetName()
|
||||||
|
require.True(t, strings.HasPrefix(name, generatePrefix))
|
||||||
|
|
||||||
|
err = client.Resource.Delete(ctx, name, metav1.DeleteOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("and then trying to read it returns a 404 error", func(t *testing.T) {
|
||||||
|
raw, err := client.Resource.Get(ctx, name, metav1.GetOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, raw)
|
||||||
|
|
||||||
|
var statusErr *apierrors.StatusError
|
||||||
|
require.True(t, errors.As(err, &statusErr))
|
||||||
|
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("and listing keepers returns an empty list", func(t *testing.T) {
|
||||||
|
rawList, err := client.Resource.List(ctx, metav1.ListOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rawList)
|
||||||
|
require.Empty(t, rawList.Items)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("creating keepers in multiple namespaces", func(t *testing.T) {
|
||||||
|
permissions := map[string]ResourcePermission{
|
||||||
|
ResourceKeepers: {Actions: ActionsAllKeepers},
|
||||||
|
}
|
||||||
|
|
||||||
|
editorOrgA := mustCreateUsers(t, helper, permissions).Editor
|
||||||
|
editorOrgB := mustCreateUsers(t, helper, permissions).Editor
|
||||||
|
|
||||||
|
keeperOrgA := mustGenerateKeeper(t, helper, editorOrgA, nil, "testdata/keeper-aws-generate.yaml")
|
||||||
|
keeperOrgB := mustGenerateKeeper(t, helper, editorOrgB, nil, "testdata/keeper-aws-generate.yaml")
|
||||||
|
|
||||||
|
clientOrgA := helper.GetResourceClient(apis.ResourceClientArgs{User: editorOrgA, GVR: gvrKeepers})
|
||||||
|
clientOrgB := helper.GetResourceClient(apis.ResourceClientArgs{User: editorOrgB, GVR: gvrKeepers})
|
||||||
|
|
||||||
|
// Create
|
||||||
|
t.Run("creating a keeper with the same name as one from another namespace does not return an error", func(t *testing.T) {
|
||||||
|
// OrgA creating a keeper with the same name from OrgB.
|
||||||
|
testData := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml")
|
||||||
|
testData.SetName(keeperOrgB.GetName())
|
||||||
|
|
||||||
|
raw, err := clientOrgA.Resource.Create(ctx, testData, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, raw)
|
||||||
|
|
||||||
|
// OrgA creating a keeper with the same name from OrgB.
|
||||||
|
testData = helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml")
|
||||||
|
testData.SetName(keeperOrgA.GetName())
|
||||||
|
|
||||||
|
raw, err = clientOrgB.Resource.Create(ctx, testData, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, raw)
|
||||||
|
|
||||||
|
require.NoError(t, clientOrgA.Resource.Delete(ctx, keeperOrgB.GetName(), metav1.DeleteOptions{}))
|
||||||
|
require.NoError(t, clientOrgB.Resource.Delete(ctx, keeperOrgA.GetName(), metav1.DeleteOptions{}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Read
|
||||||
|
t.Run("fetching a keeper from another namespace returns not found", func(t *testing.T) {
|
||||||
|
var statusErr *apierrors.StatusError
|
||||||
|
|
||||||
|
// OrgA trying to fetch keeper from OrgB.
|
||||||
|
raw, err := clientOrgA.Resource.Get(ctx, keeperOrgB.GetName(), metav1.GetOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, raw)
|
||||||
|
require.True(t, errors.As(err, &statusErr))
|
||||||
|
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code))
|
||||||
|
|
||||||
|
// OrgB trying to fetch keeper from OrgA.
|
||||||
|
raw, err = clientOrgB.Resource.Get(ctx, keeperOrgA.GetName(), metav1.GetOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, raw)
|
||||||
|
require.True(t, errors.As(err, &statusErr))
|
||||||
|
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update
|
||||||
|
t.Run("updating a keeper from another namespace returns not found", func(t *testing.T) {
|
||||||
|
var statusErr *apierrors.StatusError
|
||||||
|
|
||||||
|
// OrgA trying to update securevalue from OrgB.
|
||||||
|
testData := helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml")
|
||||||
|
testData.SetName(keeperOrgB.GetName())
|
||||||
|
testData.Object["spec"].(map[string]any)["description"] = "New description"
|
||||||
|
|
||||||
|
raw, err := clientOrgA.Resource.Update(ctx, testData, metav1.UpdateOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, raw)
|
||||||
|
require.True(t, errors.As(err, &statusErr))
|
||||||
|
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code))
|
||||||
|
|
||||||
|
// OrgB trying to update keeper from OrgA.
|
||||||
|
testData = helper.LoadYAMLOrJSONFile("testdata/keeper-aws-generate.yaml")
|
||||||
|
testData.SetName(keeperOrgA.GetName())
|
||||||
|
testData.Object["spec"].(map[string]any)["description"] = "New description"
|
||||||
|
|
||||||
|
raw, err = clientOrgB.Resource.Update(ctx, testData, metav1.UpdateOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, raw)
|
||||||
|
require.True(t, errors.As(err, &statusErr))
|
||||||
|
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
t.Run("deleting a keeper from another namespace returns an error and does not delete it", func(t *testing.T) {
|
||||||
|
var statusErr *apierrors.StatusError
|
||||||
|
|
||||||
|
// OrgA trying to delete keeper from OrgB.
|
||||||
|
err := clientOrgA.Resource.Delete(ctx, keeperOrgB.GetName(), metav1.DeleteOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.True(t, errors.As(err, &statusErr))
|
||||||
|
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code))
|
||||||
|
|
||||||
|
// Check that it still exists from the perspective of OrgB.
|
||||||
|
raw, err := clientOrgB.Resource.Get(ctx, keeperOrgB.GetName(), metav1.GetOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, raw)
|
||||||
|
|
||||||
|
// OrgB trying to delete keeper from OrgA.
|
||||||
|
err = clientOrgB.Resource.Delete(ctx, keeperOrgA.GetName(), metav1.DeleteOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, http.StatusNotFound, int(statusErr.Status().Code))
|
||||||
|
|
||||||
|
// Check that it still exists from the perspective of OrgA.
|
||||||
|
raw, err = clientOrgA.Resource.Get(ctx, keeperOrgA.GetName(), metav1.GetOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, raw)
|
||||||
|
})
|
||||||
|
|
||||||
|
// List
|
||||||
|
t.Run("listing keeper from a namespace does not return the ones from another namespace", func(t *testing.T) {
|
||||||
|
// OrgA listing keeper.
|
||||||
|
listOrgA, err := clientOrgA.Resource.List(ctx, metav1.ListOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, listOrgA)
|
||||||
|
require.Len(t, listOrgA.Items, 1)
|
||||||
|
require.Equal(t, *keeperOrgA, listOrgA.Items[0])
|
||||||
|
|
||||||
|
// OrgB listing keeper.
|
||||||
|
listOrgB, err := clientOrgB.Resource.List(ctx, metav1.ListOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, listOrgB)
|
||||||
|
require.Len(t, listOrgB.Items, 1)
|
||||||
|
require.Equal(t, *keeperOrgB, listOrgB.Items[0])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("keeper actions without having required permissions", func(t *testing.T) {
|
||||||
|
// Create users on a random org without specifying secrets-related permissions.
|
||||||
|
editorP := mustCreateUsers(t, helper, nil).Editor
|
||||||
|
|
||||||
|
clientP := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||||
|
User: editorP,
|
||||||
|
GVR: gvrKeepers,
|
||||||
|
})
|
||||||
|
|
||||||
|
// GET
|
||||||
|
rawGet, err := clientP.Resource.Get(ctx, "some-keeper", metav1.GetOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, rawGet)
|
||||||
|
|
||||||
|
var statusGetErr *apierrors.StatusError
|
||||||
|
require.True(t, errors.As(err, &statusGetErr))
|
||||||
|
require.EqualValues(t, http.StatusForbidden, statusGetErr.Status().Code)
|
||||||
|
|
||||||
|
// LIST
|
||||||
|
rawList, err := clientP.Resource.List(ctx, metav1.ListOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, rawList)
|
||||||
|
|
||||||
|
var statusListErr *apierrors.StatusError
|
||||||
|
require.True(t, errors.As(err, &statusListErr))
|
||||||
|
require.EqualValues(t, http.StatusForbidden, statusListErr.Status().Code)
|
||||||
|
|
||||||
|
// CREATE
|
||||||
|
testKeeper := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml") // to pass validation before authz.
|
||||||
|
rawCreate, err := clientP.Resource.Create(ctx, testKeeper, metav1.CreateOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, rawCreate)
|
||||||
|
|
||||||
|
var statusCreateErr *apierrors.StatusError
|
||||||
|
require.True(t, errors.As(err, &statusCreateErr))
|
||||||
|
require.EqualValues(t, http.StatusForbidden, statusCreateErr.Status().Code)
|
||||||
|
|
||||||
|
// UPDATE
|
||||||
|
testKeeper.SetName("test") // to pass validation before authz.
|
||||||
|
rawUpdate, err := clientP.Resource.Update(ctx, testKeeper, metav1.UpdateOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, rawUpdate)
|
||||||
|
|
||||||
|
var statusUpdateErr *apierrors.StatusError
|
||||||
|
require.True(t, errors.As(err, &statusUpdateErr))
|
||||||
|
require.EqualValues(t, http.StatusForbidden, statusUpdateErr.Status().Code)
|
||||||
|
|
||||||
|
// DELETE
|
||||||
|
err = clientP.Resource.Delete(ctx, "some-keeper", metav1.DeleteOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
var statusDeleteErr *apierrors.StatusError
|
||||||
|
require.True(t, errors.As(err, &statusDeleteErr))
|
||||||
|
require.EqualValues(t, http.StatusForbidden, statusDeleteErr.Status().Code)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("keeper actions with permissions but with limited scope", func(t *testing.T) {
|
||||||
|
suffix := strconv.FormatInt(rand.Int64(), 10)
|
||||||
|
|
||||||
|
// Fix the Keeper names.
|
||||||
|
keeperName := "kp-" + suffix
|
||||||
|
testKeeper := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml")
|
||||||
|
testKeeper.SetName(keeperName)
|
||||||
|
|
||||||
|
keeperNameAnother := "kp-another-" + suffix
|
||||||
|
testKeeperAnother := helper.LoadYAMLOrJSONFile("testdata/keeper-gcp-generate.yaml")
|
||||||
|
testKeeperAnother.SetName(keeperNameAnother)
|
||||||
|
|
||||||
|
// Fix the org ID because we will create another user with scope "all" permissions on the same org, to compare.
|
||||||
|
orgID := rand.Int64() + 2
|
||||||
|
|
||||||
|
// Permissions which allow any action, but scoped actions (get, update, delete) only on `keeperName` and NO OTHER Keeper.
|
||||||
|
scopedLimitedPermissions := map[string]ResourcePermission{
|
||||||
|
ResourceKeepers: {
|
||||||
|
Actions: ActionsAllKeepers,
|
||||||
|
Name: keeperName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create users (+ client) with permission to manage ONLY the Keeper `keeperName`.
|
||||||
|
editorLimited := mustCreateUsersWithOrg(t, helper, orgID, scopedLimitedPermissions).Editor
|
||||||
|
|
||||||
|
clientScopedLimited := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||||
|
User: editorLimited,
|
||||||
|
GVR: gvrKeepers,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create users (+ client) with permission to manage ANY Keepers.
|
||||||
|
scopedAllPermissions := map[string]ResourcePermission{
|
||||||
|
ResourceKeepers: {
|
||||||
|
Actions: ActionsAllKeepers,
|
||||||
|
Name: "*", // this or not sending a `Name` have the same effect.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
editorAll := mustCreateUsersWithOrg(t, helper, orgID, scopedAllPermissions).Editor
|
||||||
|
|
||||||
|
clientScopedAll := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||||
|
User: editorAll,
|
||||||
|
GVR: gvrKeepers,
|
||||||
|
})
|
||||||
|
|
||||||
|
// For create, we don't have actual granular permissions, so we can use any client that has unscoped create permissions.
|
||||||
|
// This is because when we don't know yet what the name of the resource will be, the request comes with an empty value.
|
||||||
|
// And thus the authorizer can't do granular checks.
|
||||||
|
t.Run("CREATE", func(t *testing.T) {
|
||||||
|
rawCreateLimited, err := clientScopedAll.Resource.Create(ctx, testKeeper, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rawCreateLimited)
|
||||||
|
|
||||||
|
rawCreateLimited, err = clientScopedAll.Resource.Create(ctx, testKeeperAnother, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rawCreateLimited)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("READ", func(t *testing.T) {
|
||||||
|
// Retrieve `keeperName` from the limited client.
|
||||||
|
rawGetLimited, err := clientScopedLimited.Resource.Get(ctx, keeperName, metav1.GetOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rawGetLimited)
|
||||||
|
require.Equal(t, rawGetLimited.GetUID(), rawGetLimited.GetUID())
|
||||||
|
|
||||||
|
// Retrieve `keeperName` from the scope-all client.
|
||||||
|
rawGetAll, err := clientScopedAll.Resource.Get(ctx, keeperName, metav1.GetOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rawGetAll)
|
||||||
|
require.Equal(t, rawGetAll.GetUID(), rawGetLimited.GetUID())
|
||||||
|
|
||||||
|
// Even though we can create it, we cannot retrieve `keeperNameAnother` from the limited client.
|
||||||
|
rawGetLimited, err = clientScopedLimited.Resource.Get(ctx, keeperNameAnother, metav1.GetOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, rawGetLimited)
|
||||||
|
|
||||||
|
var statusGetErr *apierrors.StatusError
|
||||||
|
require.True(t, errors.As(err, &statusGetErr))
|
||||||
|
require.EqualValues(t, http.StatusForbidden, statusGetErr.Status().Code)
|
||||||
|
|
||||||
|
// Retrieve `keeperNameAnother` from the scope-all client.
|
||||||
|
rawGetAll, err = clientScopedAll.Resource.Get(ctx, keeperNameAnother, metav1.GetOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rawGetAll)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("LIST", func(t *testing.T) {
|
||||||
|
// List Keepers from the limited client should return only 1.
|
||||||
|
rawList, err := clientScopedLimited.Resource.List(ctx, metav1.ListOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rawList)
|
||||||
|
require.Len(t, rawList.Items, 1)
|
||||||
|
require.Equal(t, keeperName, rawList.Items[0].GetName())
|
||||||
|
|
||||||
|
// List Keepers from the scope-all client should return all of them.
|
||||||
|
rawList, err = clientScopedAll.Resource.List(ctx, metav1.ListOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rawList)
|
||||||
|
require.Len(t, rawList.Items, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("UPDATE", func(t *testing.T) {
|
||||||
|
// Update `keeperName` from the limited client.
|
||||||
|
testKeeperUpdate := testKeeper.DeepCopy()
|
||||||
|
testKeeperUpdate.Object["spec"].(map[string]any)["description"] = "keeper-description-1234"
|
||||||
|
|
||||||
|
rawUpdate, err := clientScopedLimited.Resource.Update(ctx, testKeeperUpdate, metav1.UpdateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rawUpdate)
|
||||||
|
|
||||||
|
// Try to update `keeperNameAnother` from the limited client.
|
||||||
|
testKeeperAnotherUpdate := testKeeperAnother.DeepCopy()
|
||||||
|
testKeeperAnotherUpdate.Object["spec"].(map[string]any)["description"] = "keeper-description-5678"
|
||||||
|
|
||||||
|
rawUpdate, err = clientScopedLimited.Resource.Update(ctx, testKeeperAnotherUpdate, metav1.UpdateOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, rawUpdate)
|
||||||
|
|
||||||
|
var statusUpdateErr *apierrors.StatusError
|
||||||
|
require.True(t, errors.As(err, &statusUpdateErr))
|
||||||
|
require.EqualValues(t, http.StatusForbidden, statusUpdateErr.Status().Code)
|
||||||
|
|
||||||
|
// Update `keeperNameAnother` from the scope-all client.
|
||||||
|
rawUpdate, err = clientScopedAll.Resource.Update(ctx, testKeeperAnotherUpdate, metav1.UpdateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rawUpdate)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Keep this last for cleaning up the resources.
|
||||||
|
t.Run("DELETE", func(t *testing.T) {
|
||||||
|
// Try to delete `keeperNameAnother` from the limited client.
|
||||||
|
err := clientScopedLimited.Resource.Delete(ctx, keeperNameAnother, metav1.DeleteOptions{})
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
var statusDeleteErr *apierrors.StatusError
|
||||||
|
require.True(t, errors.As(err, &statusDeleteErr))
|
||||||
|
require.EqualValues(t, http.StatusForbidden, statusDeleteErr.Status().Code)
|
||||||
|
|
||||||
|
// Delete `keeperNameAnother` from the scope-all client.
|
||||||
|
err = clientScopedAll.Resource.Delete(ctx, keeperNameAnother, metav1.DeleteOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Delete `keeperName` from the limited client.
|
||||||
|
err = clientScopedLimited.Resource.Delete(ctx, keeperName, metav1.DeleteOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package secret
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"math/rand/v2"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/registry/apis/secret"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
|
"github.com/grafana/grafana/pkg/services/team"
|
||||||
|
"github.com/grafana/grafana/pkg/tests/apis"
|
||||||
|
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||||
|
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ResourceSecureValues = "secret.securevalues"
|
||||||
|
ResourceKeepers = "secret.keepers"
|
||||||
|
|
||||||
|
ActionsAllKeepers = []string{
|
||||||
|
secret.ActionSecretKeepersCreate,
|
||||||
|
secret.ActionSecretKeepersWrite,
|
||||||
|
secret.ActionSecretKeepersRead,
|
||||||
|
secret.ActionSecretKeepersDelete,
|
||||||
|
}
|
||||||
|
ActionsAllSecureValues = []string{
|
||||||
|
secret.ActionSecretSecureValuesCreate,
|
||||||
|
secret.ActionSecretSecureValuesWrite,
|
||||||
|
secret.ActionSecretSecureValuesRead,
|
||||||
|
secret.ActionSecretSecureValuesDelete,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type ResourcePermission struct {
|
||||||
|
Actions []string
|
||||||
|
Name string // empty or "*" for all
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
testsuite.Run(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegrationDiscoveryClient(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("skipping integration test")
|
||||||
|
}
|
||||||
|
|
||||||
|
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||||
|
AppModeProduction: false, // required for experimental APIs
|
||||||
|
EnableFeatureToggles: []string{
|
||||||
|
// Required to start the example service
|
||||||
|
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
|
||||||
|
featuremgmt.FlagSecretsManagementAppPlatform,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("check discovery client", func(t *testing.T) {
|
||||||
|
disco := helper.NewDiscoveryClient()
|
||||||
|
|
||||||
|
resources, err := disco.ServerResourcesForGroupVersion("secret.grafana.app/v0alpha1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
v1Disco, err := json.MarshalIndent(resources, "", " ")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var apiResourceList map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(v1Disco, &apiResourceList))
|
||||||
|
|
||||||
|
groupVersion, ok := apiResourceList["groupVersion"].(string)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, "secret.grafana.app/v0alpha1", groupVersion)
|
||||||
|
|
||||||
|
apiResources, ok := apiResourceList["resources"].([]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Len(t, apiResources, 2) // securevalue + keeper + (subresources...)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustCreateUsersWithOrg(t *testing.T, helper *apis.K8sTestHelper, orgID int64, permissionMap map[string]ResourcePermission) apis.OrgUsers {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
permissions := make([]resourcepermissions.SetResourcePermissionCommand, 0, len(permissionMap))
|
||||||
|
for resource, permission := range permissionMap {
|
||||||
|
permissions = append(permissions, resourcepermissions.SetResourcePermissionCommand{
|
||||||
|
Actions: permission.Actions,
|
||||||
|
Resource: resource,
|
||||||
|
ResourceAttribute: "uid",
|
||||||
|
ResourceID: cmp.Or(permission.Name, "*"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
orgName := "org-" + strconv.FormatInt(orgID, 10)
|
||||||
|
|
||||||
|
userSuffix := strconv.FormatInt(rand.Int64(), 10)
|
||||||
|
|
||||||
|
// Add here admin or viewer if necessary.
|
||||||
|
editor := helper.CreateUser("editor-"+userSuffix, orgName, org.RoleEditor, permissions)
|
||||||
|
|
||||||
|
staff := helper.CreateTeam("staff-"+userSuffix, "staff-"+userSuffix+"@"+orgName, editor.Identity.GetOrgID())
|
||||||
|
|
||||||
|
// Also call this method for each new user.
|
||||||
|
helper.AddOrUpdateTeamMember(editor, staff.ID, team.PermissionTypeMember)
|
||||||
|
|
||||||
|
return apis.OrgUsers{
|
||||||
|
Editor: editor,
|
||||||
|
Staff: staff,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustCreateUsers(t *testing.T, helper *apis.K8sTestHelper, permissionMap map[string]ResourcePermission) apis.OrgUsers {
|
||||||
|
orgID := rand.Int64() + 2 // if it is 0, becomes 2 and not 1.
|
||||||
|
return mustCreateUsersWithOrg(t, helper, orgID, permissionMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGenerateKeeper(t *testing.T, helper *apis.K8sTestHelper, user apis.User, specType map[string]any, testFile string) *unstructured.Unstructured {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
require.NotEmpty(t, testFile, "testFile must not be empty")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
keeperClient := helper.GetResourceClient(apis.ResourceClientArgs{
|
||||||
|
User: user,
|
||||||
|
GVR: gvrKeepers,
|
||||||
|
})
|
||||||
|
|
||||||
|
testKeeper := helper.LoadYAMLOrJSONFile(testFile)
|
||||||
|
if specType != nil {
|
||||||
|
testKeeper.Object["spec"] = specType
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := keeperClient.Resource.Create(ctx, testKeeper, metav1.CreateOptions{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, raw)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, keeperClient.Resource.Delete(ctx, raw.GetName(), metav1.DeleteOptions{}))
|
||||||
|
})
|
||||||
|
|
||||||
|
return raw
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: secret.grafana.app/v0alpha1
|
||||||
|
kind: Keeper
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
xx: XXX
|
||||||
|
labels:
|
||||||
|
aa: AAA
|
||||||
|
spec:
|
||||||
|
description: AWS XYZ value
|
||||||
|
aws:
|
||||||
|
accessKeyId:
|
||||||
|
valueFromEnv: ACCESS_KEY_ID_XYZ
|
||||||
|
secretAccessKey:
|
||||||
|
valueFromEnv: SECRET_ACCESS_KEY_XYZ
|
||||||
|
kmsKeyId: kmsKeyId-xyz
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: secret.grafana.app/v0alpha1
|
||||||
|
kind: Keeper
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
xx: XXX
|
||||||
|
yy: YYY
|
||||||
|
labels:
|
||||||
|
aa: AAA
|
||||||
|
bb: BBB
|
||||||
|
spec:
|
||||||
|
description: GCP XYZ value
|
||||||
|
gcp:
|
||||||
|
projectId: project-id
|
||||||
|
credentialsFile: /path/to/file.json
|
||||||
Reference in New Issue
Block a user