SecretsManager: Bootstrap API service (#102444)
Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Co-authored-by: Michael Mandrus <michael.mandrus@grafana.com>
This commit is contained in:
co-authored by
Dana Axinte
Michael Mandrus
parent
3f2ef07872
commit
3aba5cb2b7
@@ -126,6 +126,7 @@
|
||||
/pkg/apimachinery/errutil/ @grafana/grafana-backend-group
|
||||
/pkg/promlib @grafana/oss-big-tent
|
||||
/pkg/storage/ @grafana/grafana-search-and-storage
|
||||
/pkg/storage/secret/ @grafana/grafana-operator-experience-squad
|
||||
/pkg/services/annotations/ @grafana/grafana-search-and-storage
|
||||
/pkg/services/apikey/ @grafana/identity-squad
|
||||
/pkg/services/cleanup/ @grafana/grafana-backend-group
|
||||
@@ -736,6 +737,7 @@ embed.go @grafana/grafana-as-code
|
||||
/pkg/registry/apis/ @grafana/grafana-app-platform-squad
|
||||
/pkg/registry/apis/alerting @grafana/grafana-app-platform-squad @grafana/alerting-backend
|
||||
/pkg/registry/apis/query @grafana/grafana-datasources-core-services
|
||||
/pkg/registry/apis/secret @grafana/grafana-operator-experience-squad
|
||||
/pkg/registry/apis/userstorage @grafana/grafana-app-platform-squad @grafana/plugins-platform-backend
|
||||
/pkg/registry/apps/advisor @grafana/plugins-platform-backend
|
||||
/pkg/codegen/ @grafana/grafana-as-code
|
||||
|
||||
@@ -36,7 +36,7 @@ require (
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432 // indirect
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
|
||||
@@ -69,8 +69,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafana/grafana-app-sdk v0.31.0 h1:/mFCcx+YqG8cWAi9hePDJQxIdtXDClDIDRgZwHkksFk=
|
||||
github.com/grafana/grafana-app-sdk v0.31.0/go.mod h1:Xw00NL7qpRLo5r3Gn48Bl1Xn2n4eUDI5pYf/wMufKWs=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432 h1:/0MLOGx9Ow7ihR4smlUYHFvomXBpdpf/jLWHKNfEUiI=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432/go.mod h1:A/SJ9CiAWNOdeD/IezNwRaDZusLKq0z6dTfhKDgZw5Y=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979 h1:B7kt2We4CVCWRJGgaIyx8lhZqTeDAk6bspOkGYWoB/I=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979/go.mod h1:A/SJ9CiAWNOdeD/IezNwRaDZusLKq0z6dTfhKDgZw5Y=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY=
|
||||
@@ -109,10 +109,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
|
||||
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
||||
@@ -208,7 +208,8 @@ require (
|
||||
github.com/grafana/grafana/apps/investigations v0.0.0-20250220163425-b4c4b9abbdc8 // @fcjack @matryer
|
||||
github.com/grafana/grafana/apps/playlist v0.0.0-20250220164708-c8d4ff28a450 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979 // @grafana/grafana-app-platform-squad
|
||||
github.com/grafana/grafana/pkg/apis/secret v0.0.0-20250319110241-5a004939da2a // @grafana/grafana-operator-experience-squad
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295 // @grafana/grafana-app-platform-squad
|
||||
|
||||
// This needs to be here for other projects that import grafana/grafana
|
||||
@@ -562,8 +563,6 @@ require (
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.49.0 // indirect
|
||||
github.com/bluele/gcache v0.0.2 // indirect
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.3 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.22.0 // indirect
|
||||
github.com/onsi/gomega v1.36.1 // indirect
|
||||
github.com/open-feature/go-sdk-contrib/providers/ofrep v0.1.5 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -1588,8 +1588,10 @@ github.com/grafana/grafana/apps/playlist v0.0.0-20250220164708-c8d4ff28a450 h1:h
|
||||
github.com/grafana/grafana/apps/playlist v0.0.0-20250220164708-c8d4ff28a450/go.mod h1:KKIsWpbv88Lwwcvdjon73zFL7vNJvuXLtsSoUjJErTw=
|
||||
github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 h1:9qOLpC21AmXZqZ6rUhrBWl2mVqS3CzV53pzw0BCuHt0=
|
||||
github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:deLQ/ywLvpVGbncRGUA4UDGt8a5Ei9sivOP+x6AQ2ko=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432 h1:/0MLOGx9Ow7ihR4smlUYHFvomXBpdpf/jLWHKNfEUiI=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432/go.mod h1:A/SJ9CiAWNOdeD/IezNwRaDZusLKq0z6dTfhKDgZw5Y=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979 h1:B7kt2We4CVCWRJGgaIyx8lhZqTeDAk6bspOkGYWoB/I=
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979/go.mod h1:A/SJ9CiAWNOdeD/IezNwRaDZusLKq0z6dTfhKDgZw5Y=
|
||||
github.com/grafana/grafana/pkg/apis/secret v0.0.0-20250319110241-5a004939da2a h1:dMllTcE0R1qvV4rWDehQzxNiHaale1yCzXsVkub07D0=
|
||||
github.com/grafana/grafana/pkg/apis/secret v0.0.0-20250319110241-5a004939da2a/go.mod h1:K/fP4kODJmABug5b90PhACUZD6Xh/veEz2b1VRKNyuA=
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295 h1:ivbywO8ZnmzDDkn169qUb9REsCGYAA7H8W6VfqSmCEw=
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295/go.mod h1:OugmKouuvgWeOI8Kghram2Pv8b/1SuXRR8/iW068uLk=
|
||||
github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0=
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/query"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/userstorage"
|
||||
)
|
||||
|
||||
@@ -27,6 +28,7 @@ func ProvideRegistryServiceSink(
|
||||
_ *query.QueryAPIBuilder,
|
||||
_ *notifications.NotificationsAPIBuilder,
|
||||
_ *userstorage.UserStorageAPIBuilder,
|
||||
_ *secret.SecretAPIBuilder,
|
||||
_ *provisioning.APIBuilder,
|
||||
) *Service {
|
||||
return &Service{}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
)
|
||||
|
||||
type KeeperMetadataStorage interface {
|
||||
Create(ctx context.Context, keeper *secretv0alpha1.Keeper) (*secretv0alpha1.Keeper, error)
|
||||
Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv0alpha1.Keeper, error)
|
||||
Update(ctx context.Context, keeper *secretv0alpha1.Keeper) (*secretv0alpha1.Keeper, error)
|
||||
Delete(ctx context.Context, namespace xkube.Namespace, name string) error
|
||||
List(ctx context.Context, namespace xkube.Namespace, options *internalversion.ListOptions) (*secretv0alpha1.KeeperList, error)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package contracts
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
)
|
||||
|
||||
type SecureValueMetadataStorage interface {
|
||||
Create(ctx context.Context, sv *secretv0alpha1.SecureValue) (*secretv0alpha1.SecureValue, error)
|
||||
Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv0alpha1.SecureValue, error)
|
||||
Update(ctx context.Context, sv *secretv0alpha1.SecureValue) (*secretv0alpha1.SecureValue, error)
|
||||
Delete(ctx context.Context, namespace xkube.Namespace, name string) error
|
||||
List(ctx context.Context, namespace xkube.Namespace, options *internalversion.ListOptions) (*secretv0alpha1.SecureValueList, error)
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
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"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
"k8s.io/kube-openapi/pkg/common"
|
||||
|
||||
claims "github.com/grafana/authlib/types"
|
||||
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/reststorage"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
var (
|
||||
_ builder.APIGroupBuilder = (*SecretAPIBuilder)(nil)
|
||||
_ builder.APIGroupMutation = (*SecretAPIBuilder)(nil)
|
||||
_ builder.APIGroupValidation = (*SecretAPIBuilder)(nil)
|
||||
_ builder.APIGroupRouteProvider = (*SecretAPIBuilder)(nil)
|
||||
)
|
||||
|
||||
type SecretAPIBuilder struct {
|
||||
tracer tracing.Tracer
|
||||
secureValueMetadataStorage contracts.SecureValueMetadataStorage
|
||||
keeperMetadataStorage contracts.KeeperMetadataStorage
|
||||
accessClient claims.AccessClient
|
||||
decryptersAllowList map[string]struct{}
|
||||
}
|
||||
|
||||
func NewSecretAPIBuilder(
|
||||
tracer tracing.Tracer,
|
||||
secureValueMetadataStorage contracts.SecureValueMetadataStorage,
|
||||
keeperMetadataStorage contracts.KeeperMetadataStorage,
|
||||
accessClient claims.AccessClient,
|
||||
decryptersAllowList map[string]struct{},
|
||||
) *SecretAPIBuilder {
|
||||
return &SecretAPIBuilder{tracer, secureValueMetadataStorage, keeperMetadataStorage, accessClient, decryptersAllowList}
|
||||
}
|
||||
|
||||
func RegisterAPIService(
|
||||
features featuremgmt.FeatureToggles,
|
||||
cfg *setting.Cfg,
|
||||
apiregistration builder.APIRegistrar,
|
||||
tracer tracing.Tracer,
|
||||
secureValueMetadataStorage contracts.SecureValueMetadataStorage,
|
||||
keeperMetadataStorage contracts.KeeperMetadataStorage,
|
||||
accessClient claims.AccessClient,
|
||||
accessControlService accesscontrol.Service,
|
||||
) (*SecretAPIBuilder, error) {
|
||||
// Skip registration unless opting into experimental apis and the secrets management app platform flag.
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) ||
|
||||
!features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
builder := NewSecretAPIBuilder(
|
||||
tracer,
|
||||
secureValueMetadataStorage,
|
||||
keeperMetadataStorage,
|
||||
accessClient,
|
||||
nil, // OSS does not need an allow list.
|
||||
)
|
||||
|
||||
apiregistration.RegisterAPI(builder)
|
||||
|
||||
return builder, nil
|
||||
}
|
||||
|
||||
// GetGroupVersion returns the tuple of `group` and `version` for the API which uniquely identifies it.
|
||||
func (b *SecretAPIBuilder) GetGroupVersion() schema.GroupVersion {
|
||||
return secretv0alpha1.SchemeGroupVersion
|
||||
}
|
||||
|
||||
// InstallSchema is called by the `apiserver` which exposes the defined kinds.
|
||||
func (b *SecretAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||
err := secretv0alpha1.AddKnownTypes(scheme, secretv0alpha1.VERSION)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Link this version to the internal representation.
|
||||
// This is used for server-side-apply (PATCH), and avoids the error:
|
||||
// "no kind is registered for the type"
|
||||
err = secretv0alpha1.AddKnownTypes(scheme, runtime.APIVersionInternal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Internal Kubernetes metadata API. Presumably to display the available APIs?
|
||||
// e.g. http://localhost:3000/apis/secret.grafana.app/v0alpha1
|
||||
metav1.AddToGroupVersion(scheme, secretv0alpha1.SchemeGroupVersion)
|
||||
|
||||
// This sets the priority in case we have multiple versions.
|
||||
// By default Kubernetes will only let you use `kubectl get <resource>` with one version.
|
||||
// In case there are multiple versions, we'd need to pass the full path with the `--raw` flag.
|
||||
if err := scheme.SetVersionPriority(secretv0alpha1.SchemeGroupVersion); err != nil {
|
||||
return fmt.Errorf("scheme set version priority: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAPIGroupInfo is called when creating a generic API server for this group of kinds.
|
||||
func (b *SecretAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error {
|
||||
secureValueResource := secretv0alpha1.SecureValuesResourceInfo
|
||||
keeperResource := secretv0alpha1.KeeperResourceInfo
|
||||
|
||||
// rest.Storage is a generic interface for RESTful storage services.
|
||||
// The constructors need to at least implement this interface, but will most likely implement
|
||||
// other interfaces that equal to different operations like `get`, `list` and so on.
|
||||
secureRestStorage := map[string]rest.Storage{
|
||||
// Default path for `securevalue`.
|
||||
// The `reststorage.SecureValueRest` struct will implement interfaces for CRUDL operations on `securevalue`.
|
||||
secureValueResource.StoragePath(): reststorage.NewSecureValueRest(b.secureValueMetadataStorage, secureValueResource),
|
||||
|
||||
// The `reststorage.KeeperRest` struct will implement interfaces for CRUDL operations on `keeper`.
|
||||
keeperResource.StoragePath(): reststorage.NewKeeperRest(b.keeperMetadataStorage, keeperResource),
|
||||
}
|
||||
|
||||
apiGroupInfo.VersionedResourcesStorageMap[secretv0alpha1.VERSION] = secureRestStorage
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOpenAPIDefinitions, is this only for documentation?
|
||||
func (b *SecretAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
|
||||
return secretv0alpha1.GetOpenAPIDefinitions
|
||||
}
|
||||
|
||||
// GetAuthorizer decides whether the request is allowed, denied or no opinion based on credentials and request attributes.
|
||||
// Usually most resource are stored in folders (e.g. alerts, dashboards), which allows users to manage permissions at folder level,
|
||||
// rather than at resource level which also has the benefit of lowering the load on AuthZ side, since instead of storing access to
|
||||
// a single dashboard, you'd store access to all dashboards in a specific folder.
|
||||
// For Secrets, this is not the case, but if we want to make it so, we need to update this ResourceAuthorizer to check the containing folder.
|
||||
// If we ever want to do that, get guidance from IAM first as well.
|
||||
func (b *SecretAPIBuilder) GetAuthorizer() authorizer.Authorizer {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register additional routes with the server.
|
||||
func (b *SecretAPIBuilder) GetAPIRoutes() *builder.APIRoutes {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate is called in `Create`, `Update` and `Delete` REST funcs, if the body calls the argument `rest.ValidateObjectFunc`.
|
||||
func (b *SecretAPIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
|
||||
obj := a.GetObject()
|
||||
operation := a.GetOperation()
|
||||
|
||||
if obj == nil || operation == admission.Connect {
|
||||
return nil // This is normal for sub-resource
|
||||
}
|
||||
|
||||
groupKind := obj.GetObjectKind().GroupVersionKind().GroupKind()
|
||||
|
||||
// Generic validations for all kinds. At this point the name+namespace must not be empty.
|
||||
if a.GetName() == "" {
|
||||
return apierrors.NewInvalid(
|
||||
groupKind,
|
||||
a.GetName(),
|
||||
field.ErrorList{field.Required(field.NewPath("metadata", "name"), "a `name` is required")},
|
||||
)
|
||||
}
|
||||
|
||||
if a.GetNamespace() == "" {
|
||||
return apierrors.NewInvalid(
|
||||
groupKind,
|
||||
a.GetName(),
|
||||
field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "a `namespace` is required")},
|
||||
)
|
||||
}
|
||||
|
||||
switch typedObj := obj.(type) {
|
||||
case *secretv0alpha1.SecureValue:
|
||||
var oldObj *secretv0alpha1.SecureValue
|
||||
|
||||
if a.GetOldObject() != nil {
|
||||
var ok bool
|
||||
|
||||
oldObj, ok = a.GetOldObject().(*secretv0alpha1.SecureValue)
|
||||
if !ok {
|
||||
return apierrors.NewBadRequest(fmt.Sprintf("old object is not a SecureValue, found %T", a.GetOldObject()))
|
||||
}
|
||||
}
|
||||
|
||||
if errs := reststorage.ValidateSecureValue(typedObj, oldObj, operation, b.decryptersAllowList); len(errs) > 0 {
|
||||
return apierrors.NewInvalid(groupKind, a.GetName(), errs)
|
||||
}
|
||||
|
||||
return nil
|
||||
case *secretv0alpha1.Keeper:
|
||||
if errs := reststorage.ValidateKeeper(typedObj, operation); len(errs) > 0 {
|
||||
return apierrors.NewInvalid(groupKind, a.GetName(), errs)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return apierrors.NewBadRequest(fmt.Sprintf("unknown spec %T", obj))
|
||||
}
|
||||
|
||||
func (b *SecretAPIBuilder) Mutate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error {
|
||||
obj := a.GetObject()
|
||||
operation := a.GetOperation()
|
||||
|
||||
if obj == nil || operation == admission.Connect {
|
||||
return nil // This is normal for sub-resource
|
||||
}
|
||||
|
||||
// When creating a resource and the name is empty, we need to generate one.
|
||||
if operation == admission.Create && a.GetName() == "" {
|
||||
generatedName, err := util.GetRandomString(8)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate random string: %w", err)
|
||||
}
|
||||
|
||||
switch typedObj := obj.(type) {
|
||||
case *secretv0alpha1.SecureValue:
|
||||
optionalPrefix := typedObj.GenerateName
|
||||
if optionalPrefix == "" {
|
||||
optionalPrefix = "sv-"
|
||||
}
|
||||
|
||||
typedObj.Name = optionalPrefix + generatedName
|
||||
|
||||
case *secretv0alpha1.Keeper:
|
||||
optionalPrefix := typedObj.GenerateName
|
||||
if optionalPrefix == "" {
|
||||
optionalPrefix = "kp-"
|
||||
}
|
||||
|
||||
typedObj.Name = optionalPrefix + generatedName
|
||||
}
|
||||
}
|
||||
|
||||
// On any mutation to a `SecureValue`, override the `phase` as `Pending` and an empty `message`.
|
||||
if operation == admission.Create || operation == admission.Update {
|
||||
sv, ok := obj.(*secretv0alpha1.SecureValue)
|
||||
if ok && sv != nil {
|
||||
sv.Status.Phase = secretv0alpha1.SecureValuePhasePending
|
||||
sv.Status.Message = ""
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package reststorage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
)
|
||||
|
||||
var (
|
||||
_ rest.Scoper = (*KeeperRest)(nil)
|
||||
_ rest.SingularNameProvider = (*KeeperRest)(nil)
|
||||
_ rest.Getter = (*KeeperRest)(nil)
|
||||
_ rest.Lister = (*KeeperRest)(nil)
|
||||
_ rest.Storage = (*KeeperRest)(nil)
|
||||
_ rest.Creater = (*KeeperRest)(nil)
|
||||
_ rest.Updater = (*KeeperRest)(nil)
|
||||
_ rest.GracefulDeleter = (*KeeperRest)(nil)
|
||||
)
|
||||
|
||||
// KeeperRest is an ddimplementation of CRUDL operations on a `keeper` backed by TODO.
|
||||
type KeeperRest struct {
|
||||
storage contracts.KeeperMetadataStorage
|
||||
resource utils.ResourceInfo
|
||||
tableConverter rest.TableConvertor
|
||||
}
|
||||
|
||||
// NewKeeperRest is a returns a constructed `*KeeperRest`.
|
||||
func NewKeeperRest(storage contracts.KeeperMetadataStorage, resource utils.ResourceInfo) *KeeperRest {
|
||||
return &KeeperRest{storage, resource, resource.TableConverter()}
|
||||
}
|
||||
|
||||
// New returns an empty `*Keeper` that is used by the `Create` method.
|
||||
func (s *KeeperRest) New() runtime.Object {
|
||||
return s.resource.NewFunc()
|
||||
}
|
||||
|
||||
// Destroy is called when? [TODO]
|
||||
func (s *KeeperRest) Destroy() {}
|
||||
|
||||
// NamespaceScoped returns `true` because the storage is namespaced (== org).
|
||||
func (s *KeeperRest) NamespaceScoped() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetSingularName is used by `kubectl` discovery to have singular name representation of resources.
|
||||
func (s *KeeperRest) GetSingularName() string {
|
||||
return s.resource.GetSingularName()
|
||||
}
|
||||
|
||||
// NewList returns an empty `*KeeperList` that is used by the `List` method.
|
||||
func (s *KeeperRest) NewList() runtime.Object {
|
||||
return s.resource.NewListFunc()
|
||||
}
|
||||
|
||||
// ConvertToTable is used by Kubernetes and converts objects to `metav1.Table`.
|
||||
func (s *KeeperRest) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
|
||||
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
|
||||
}
|
||||
|
||||
// List calls the inner `store` (persistence) and returns a list of `Keepers` within a `namespace` filtered by the `options`.
|
||||
func (s *KeeperRest) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||
_, ok := request.NamespaceFrom(ctx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing namespace")
|
||||
}
|
||||
|
||||
return &secretv0alpha1.KeeperList{Items: make([]secretv0alpha1.Keeper, 0)}, nil
|
||||
}
|
||||
|
||||
// Get calls the inner `store` (persistence) and returns a `Keeper` by `name`.
|
||||
func (s *KeeperRest) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
_, ok := request.NamespaceFrom(ctx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing namespace")
|
||||
}
|
||||
|
||||
return nil, s.resource.NewNotFound(name)
|
||||
}
|
||||
|
||||
// Create a new `Keeper`. Does some validation and allows empty `name` (generated).
|
||||
func (s *KeeperRest) Create(
|
||||
ctx context.Context,
|
||||
obj runtime.Object,
|
||||
createValidation rest.ValidateObjectFunc,
|
||||
options *metav1.CreateOptions,
|
||||
) (runtime.Object, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Update a `Keeper`'s `value`. The second return parameter indicates whether the resource was newly created.
|
||||
func (s *KeeperRest) Update(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
objInfo rest.UpdatedObjectInfo,
|
||||
createValidation rest.ValidateObjectFunc,
|
||||
updateValidation rest.ValidateObjectUpdateFunc,
|
||||
forceAllowCreate bool,
|
||||
options *metav1.UpdateOptions,
|
||||
) (runtime.Object, bool, error) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// Delete calls the inner `store` (persistence) in order to delete the `Keeper`.
|
||||
// The second return parameter `bool` indicates whether the delete was intant or not. It always is for `Keepers`.
|
||||
func (s *KeeperRest) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
_, ok := request.NamespaceFrom(ctx)
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("missing namespace")
|
||||
}
|
||||
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
// ValidateKeeper does basic spec validation of a keeper.
|
||||
func ValidateKeeper(keeper *secretv0alpha1.Keeper, operation admission.Operation) field.ErrorList {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package reststorage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
)
|
||||
|
||||
var (
|
||||
_ rest.Scoper = (*SecureValueRest)(nil)
|
||||
_ rest.SingularNameProvider = (*SecureValueRest)(nil)
|
||||
_ rest.Getter = (*SecureValueRest)(nil)
|
||||
_ rest.Lister = (*SecureValueRest)(nil)
|
||||
_ rest.Storage = (*SecureValueRest)(nil)
|
||||
_ rest.Creater = (*SecureValueRest)(nil)
|
||||
_ rest.Updater = (*SecureValueRest)(nil)
|
||||
_ rest.GracefulDeleter = (*SecureValueRest)(nil)
|
||||
)
|
||||
|
||||
// SecureValueRest is an implementation of CRUDL operations on a `securevalue` backed by a persistence layer `store`.
|
||||
type SecureValueRest struct {
|
||||
storage contracts.SecureValueMetadataStorage
|
||||
resource utils.ResourceInfo
|
||||
tableConverter rest.TableConvertor
|
||||
}
|
||||
|
||||
// NewSecureValueRest is a returns a constructed `*SecureValueRest`.
|
||||
func NewSecureValueRest(storage contracts.SecureValueMetadataStorage, resource utils.ResourceInfo) *SecureValueRest {
|
||||
return &SecureValueRest{storage, resource, resource.TableConverter()}
|
||||
}
|
||||
|
||||
// New returns an empty `*SecureValue` that is used by the `Create` method.
|
||||
func (s *SecureValueRest) New() runtime.Object {
|
||||
return s.resource.NewFunc()
|
||||
}
|
||||
|
||||
// Destroy is called when? [TODO]
|
||||
func (s *SecureValueRest) Destroy() {}
|
||||
|
||||
// NamespaceScoped returns `true` because the storage is namespaced (== org).
|
||||
func (s *SecureValueRest) NamespaceScoped() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetSingularName is used by `kubectl` discovery to have singular name representation of resources.
|
||||
func (s *SecureValueRest) GetSingularName() string {
|
||||
return s.resource.GetSingularName()
|
||||
}
|
||||
|
||||
// NewList returns an empty `*SecureValueList` that is used by the `List` method.
|
||||
func (s *SecureValueRest) NewList() runtime.Object {
|
||||
return s.resource.NewListFunc()
|
||||
}
|
||||
|
||||
// ConvertToTable is used by Kubernetes and converts objects to `metav1.Table`.
|
||||
func (s *SecureValueRest) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
|
||||
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
|
||||
}
|
||||
|
||||
// List calls the inner `store` (persistence) and returns a list of `securevalues` within a `namespace` filtered by the `options`.
|
||||
func (s *SecureValueRest) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||
_, ok := request.NamespaceFrom(ctx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing namespace")
|
||||
}
|
||||
|
||||
return &secretv0alpha1.SecureValueList{Items: make([]secretv0alpha1.SecureValue, 0)}, nil
|
||||
}
|
||||
|
||||
// Get calls the inner `store` (persistence) and returns a `securevalue` by `name`. It will NOT return the decrypted `value`.
|
||||
func (s *SecureValueRest) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
_, ok := request.NamespaceFrom(ctx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing namespace")
|
||||
}
|
||||
|
||||
return nil, s.resource.NewNotFound(name)
|
||||
}
|
||||
|
||||
// Create a new `securevalue`. Does some validation and allows empty `name` (generated).
|
||||
func (s *SecureValueRest) Create(
|
||||
ctx context.Context,
|
||||
obj runtime.Object,
|
||||
createValidation rest.ValidateObjectFunc,
|
||||
options *metav1.CreateOptions,
|
||||
) (runtime.Object, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Update a `securevalue`'s `value`. The second return parameter indicates whether the resource was newly created.
|
||||
// Currently does not support "create on update" functionality. If the securevalue does not yet exist, it returns an error.
|
||||
func (s *SecureValueRest) Update(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
objInfo rest.UpdatedObjectInfo,
|
||||
createValidation rest.ValidateObjectFunc,
|
||||
updateValidation rest.ValidateObjectUpdateFunc,
|
||||
forceAllowCreate bool,
|
||||
options *metav1.UpdateOptions,
|
||||
) (runtime.Object, bool, error) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// Delete calls the inner `store` (persistence) in order to delete the `securevalue`.
|
||||
// The second return parameter `bool` indicates whether the delete was instant or not. It always is for `securevalues`.
|
||||
func (s *SecureValueRest) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
_, ok := request.NamespaceFrom(ctx)
|
||||
if !ok {
|
||||
return nil, false, fmt.Errorf("missing namespace")
|
||||
}
|
||||
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
// ValidateSecureValue does basic spec validation of a securevalue.
|
||||
func ValidateSecureValue(sv, oldSv *secretv0alpha1.SecureValue, operation admission.Operation, decryptersAllowList map[string]struct{}) field.ErrorList {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package xkube
|
||||
|
||||
// Namespace is a newtype of string that improves type safety.
|
||||
type Namespace string
|
||||
|
||||
func (n Namespace) String() string {
|
||||
return string(n)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/registry/apis/iam"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/provisioning"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/query"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/service"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/userstorage"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
||||
@@ -36,5 +37,6 @@ var WireSet = wire.NewSet(
|
||||
service.RegisterAPIService,
|
||||
query.RegisterAPIService,
|
||||
notifications.RegisterAPIService,
|
||||
secret.RegisterAPIService,
|
||||
userstorage.RegisterAPIService,
|
||||
)
|
||||
|
||||
@@ -159,6 +159,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/user/userimpl"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
legacydualwrite "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
secretmetadata "github.com/grafana/grafana/pkg/storage/secret/metadata"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
unifiedsearch "github.com/grafana/grafana/pkg/storage/unified/search"
|
||||
"github.com/grafana/grafana/pkg/tsdb/azuremonitor"
|
||||
@@ -402,6 +403,9 @@ var wireBasicSet = wire.NewSet(
|
||||
connectors.ProvideOrgRoleMapper,
|
||||
wire.Bind(new(user.Verifier), new(*userimpl.Verifier)),
|
||||
authz.WireSet,
|
||||
// Secrets Manager
|
||||
secretmetadata.ProvideSecureValueMetadataStorage,
|
||||
secretmetadata.ProvideKeeperMetadataStorage,
|
||||
// Unified storage
|
||||
resource.ProvideStorageMetrics,
|
||||
resource.ProvideIndexMetrics,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
claims "github.com/grafana/authlib/types"
|
||||
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
)
|
||||
|
||||
func ProvideKeeperMetadataStorage(db db.DB, features featuremgmt.FeatureToggles, accessClient claims.AccessClient) (contracts.KeeperMetadataStorage, error) {
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) ||
|
||||
!features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) {
|
||||
return &keeperMetadataStorage{}, nil
|
||||
}
|
||||
|
||||
return &keeperMetadataStorage{db: db, accessClient: accessClient}, nil
|
||||
}
|
||||
|
||||
// keeperMetadataStorage is the actual implementation of the keeper metadata storage.
|
||||
type keeperMetadataStorage struct {
|
||||
db db.DB
|
||||
accessClient claims.AccessClient
|
||||
}
|
||||
|
||||
func (s *keeperMetadataStorage) Create(ctx context.Context, keeper *secretv0alpha1.Keeper) (*secretv0alpha1.Keeper, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *keeperMetadataStorage) Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv0alpha1.Keeper, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *keeperMetadataStorage) Update(ctx context.Context, newKeeper *secretv0alpha1.Keeper) (*secretv0alpha1.Keeper, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *keeperMetadataStorage) Delete(ctx context.Context, namespace xkube.Namespace, name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *keeperMetadataStorage) List(ctx context.Context, namespace xkube.Namespace, options *internalversion.ListOptions) (*secretv0alpha1.KeeperList, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
claims "github.com/grafana/authlib/types"
|
||||
|
||||
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||
)
|
||||
|
||||
func ProvideSecureValueMetadataStorage(db db.DB, features featuremgmt.FeatureToggles, accessClient claims.AccessClient) (contracts.SecureValueMetadataStorage, error) {
|
||||
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) ||
|
||||
!features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) {
|
||||
return &secureValueMetadataStorage{}, nil
|
||||
}
|
||||
|
||||
return &secureValueMetadataStorage{db: db, accessClient: accessClient}, nil
|
||||
}
|
||||
|
||||
// secureValueMetadataStorage is the actual implementation of the secure value metadata storage.
|
||||
type secureValueMetadataStorage struct {
|
||||
db db.DB
|
||||
accessClient claims.AccessClient
|
||||
}
|
||||
|
||||
func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv0alpha1.SecureValue) (*secretv0alpha1.SecureValue, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *secureValueMetadataStorage) Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv0alpha1.SecureValue, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *secureValueMetadataStorage) Update(ctx context.Context, newSecureValue *secretv0alpha1.SecureValue) (*secretv0alpha1.SecureValue, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *secureValueMetadataStorage) Delete(ctx context.Context, namespace xkube.Namespace, name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *secureValueMetadataStorage) List(ctx context.Context, namespace xkube.Namespace, options *internalversion.ListOptions) (*secretv0alpha1.SecureValueList, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -17,7 +17,7 @@ require (
|
||||
github.com/grafana/authlib/types v0.0.0-20250224151205-5ef97131cc82
|
||||
github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible
|
||||
github.com/grafana/grafana/apps/dashboard v0.0.0-20250317130411-3f270d1de043
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295
|
||||
github.com/grafana/grafana/pkg/storage/unified/resource v0.0.0-20250317130411-3f270d1de043
|
||||
github.com/stretchr/testify v1.10.0
|
||||
|
||||
@@ -17,7 +17,7 @@ require (
|
||||
github.com/grafana/grafana v11.4.0-00010101000000-000000000000+incompatible
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.272.0
|
||||
github.com/grafana/grafana/apps/dashboard v0.0.0-20250317130411-3f270d1de043
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250312121619-f64be062c432
|
||||
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250314071911-14e2784e6979
|
||||
github.com/grafana/grafana/pkg/apiserver v0.0.0-20250220154326-6e5de80ef295 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1
|
||||
|
||||
Reference in New Issue
Block a user