use config from secrets service
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
secret "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
@@ -15,18 +16,7 @@ type GenericDataSource struct {
|
||||
Spec GenericDataSourceSpec `json:"spec"`
|
||||
|
||||
// Secure values placeholder (true for fields that exist)
|
||||
Secure map[string]SecureValue `json:"secure,omitempty"`
|
||||
}
|
||||
|
||||
type SecureValue struct {
|
||||
// The input is only valid for writing the value -- it is replaced on read
|
||||
Input string `json:"input,omitempty"`
|
||||
|
||||
// The name identifier for this secure value
|
||||
Reference string `json:"ref,omitempty"`
|
||||
|
||||
// Value for write, this will remove the secret value
|
||||
Remove bool `json:"remove,omitempty"`
|
||||
Secure secret.InlineSecureValues `json:"secure,omitempty"`
|
||||
}
|
||||
|
||||
// DsAccess represents how the datasource connects to the remote service
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
@@ -78,7 +79,7 @@ func (in *GenericDataSource) DeepCopyInto(out *GenericDataSource) {
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
if in.Secure != nil {
|
||||
in, out := &in.Secure, &out.Secure
|
||||
*out = make(map[string]SecureValue, len(*in))
|
||||
*out = make(map[string]secretv0alpha1.InlineSecureValue, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
@@ -182,19 +183,3 @@ func (in *HealthCheckResult) DeepCopyObject() runtime.Object {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SecureValue) DeepCopyInto(out *SecureValue) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecureValue.
|
||||
func (in *SecureValue) DeepCopy() *SecureValue {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SecureValue)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.GenericDataSourceList": schema_pkg_apis_datasource_v0alpha1_GenericDataSourceList(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.GenericDataSourceSpec": schema_pkg_apis_datasource_v0alpha1_GenericDataSourceSpec(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.HealthCheckResult": schema_pkg_apis_datasource_v0alpha1_HealthCheckResult(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.SecureValue": schema_pkg_apis_datasource_v0alpha1_SecureValue(ref),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +163,7 @@ func schema_pkg_apis_datasource_v0alpha1_GenericDataSource(ref common.ReferenceC
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.SecureValue"),
|
||||
Ref: ref("github.com/grafana/grafana/pkg/apis/secret/v0alpha1.InlineSecureValue"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -175,7 +174,7 @@ func schema_pkg_apis_datasource_v0alpha1_GenericDataSource(ref common.ReferenceC
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.GenericDataSourceSpec", "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.SecureValue", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.GenericDataSourceSpec", "github.com/grafana/grafana/pkg/apis/secret/v0alpha1.InlineSecureValue", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,36 +366,3 @@ func schema_pkg_apis_datasource_v0alpha1_HealthCheckResult(ref common.ReferenceC
|
||||
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_datasource_v0alpha1_SecureValue(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"input": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "The input is only valid for writing the value -- it is replaced on read",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"ref": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "The name identifier for this secure value",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"remove": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Value for write, this will remove the secret value",
|
||||
Type: []string{"boolean"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/datasource/v0alpha1,GenericDataSourceSpec,User
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/datasource/v0alpha1,SecureValue,Reference
|
||||
API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/pkg/apis/datasource/v0alpha1,GenericDataSourceList,ListMeta
|
||||
|
||||
@@ -6,9 +6,10 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
|
||||
)
|
||||
|
||||
func TestExposedSecureValue(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package v0alpha1
|
||||
|
||||
// Access secure values inside any resource
|
||||
// +k8s:openapi-gen=true
|
||||
type InlineSecureValue struct {
|
||||
// Create a secure value
|
||||
// +k8s:validation:minLength=1
|
||||
// +k8s:validation:maxLength=24576
|
||||
Create ExposedSecureValue `json:"create,omitempty"`
|
||||
|
||||
// Reference a shared secret (enterprise only)
|
||||
Reference string `json:"ref,omitempty"`
|
||||
|
||||
// The resolved UID within the secret service
|
||||
UID string `json:"uid,omitempty"`
|
||||
|
||||
// Remove this value -- cascading delete to the secret service if necessary
|
||||
Remove bool `json:"remove,omitempty"`
|
||||
}
|
||||
|
||||
// Collection of secure values
|
||||
// +k8s:openapi-gen=true
|
||||
type InlineSecureValues = map[string]InlineSecureValue
|
||||
@@ -179,6 +179,22 @@ func (in *HashiCorpKeeperConfig) DeepCopy() *HashiCorpKeeperConfig {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *InlineSecureValue) DeepCopyInto(out *InlineSecureValue) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InlineSecureValue.
|
||||
func (in *InlineSecureValue) DeepCopy() *InlineSecureValue {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(InlineSecureValue)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Keeper) DeepCopyInto(out *Keeper) {
|
||||
*out = *in
|
||||
|
||||
@@ -25,6 +25,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
||||
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.GCPKeeperConfig": schema_pkg_apis_secret_v0alpha1_GCPKeeperConfig(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpCredentials": schema_pkg_apis_secret_v0alpha1_HashiCorpCredentials(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.HashiCorpKeeperConfig": schema_pkg_apis_secret_v0alpha1_HashiCorpKeeperConfig(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.InlineSecureValue": schema_pkg_apis_secret_v0alpha1_InlineSecureValue(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.Keeper": schema_pkg_apis_secret_v0alpha1_Keeper(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.KeeperList": schema_pkg_apis_secret_v0alpha1_KeeperList(ref),
|
||||
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1.KeeperSpec": schema_pkg_apis_secret_v0alpha1_KeeperSpec(ref),
|
||||
@@ -355,6 +356,47 @@ func schema_pkg_apis_secret_v0alpha1_HashiCorpKeeperConfig(ref common.ReferenceC
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_secret_v0alpha1_InlineSecureValue(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Access secure values inside any resource",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"create": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Create a secure value",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"ref": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Reference a shared secret (enterprise only)",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"uid": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "The resolved UID within the secret service",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"remove": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Remove this value -- cascading delete to the secret service if necessary",
|
||||
Type: []string{"boolean"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_secret_v0alpha1_Keeper(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
|
||||
@@ -3,6 +3,7 @@ API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alp
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,AzureCredentials,ClientID
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,AzureCredentials,TenantID
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,GCPCredentials,ProjectID
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,InlineSecureValue,Reference
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,KeeperSpec,Azure
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,KeeperSpec,HashiCorp
|
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,SecureValueStatus,ExternalID
|
||||
|
||||
@@ -2,6 +2,7 @@ package datasource
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
secret "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
|
||||
@@ -68,6 +70,12 @@ func (r *converter) asGenericDataSource(ds *datasources.DataSource) (*v0alpha1.G
|
||||
}
|
||||
cfg.UID = gapiutil.CalculateClusterWideUID(cfg)
|
||||
|
||||
if ds.ID > 0 {
|
||||
cfg.Labels = map[string]string{
|
||||
utils.LabelKeyDeprecatedInternalID: strconv.FormatInt(ds.ID, 10),
|
||||
}
|
||||
}
|
||||
|
||||
if ds.JsonData != nil {
|
||||
val, ok := ds.JsonData.Interface().(map[string]any)
|
||||
if !ok {
|
||||
@@ -77,10 +85,10 @@ func (r *converter) asGenericDataSource(ds *datasources.DataSource) (*v0alpha1.G
|
||||
}
|
||||
|
||||
if ds.SecureJsonData != nil {
|
||||
cfg.Secure = make(map[string]v0alpha1.SecureValue)
|
||||
cfg.Secure = make(secret.InlineSecureValues)
|
||||
for k := range ds.SecureJsonData {
|
||||
cfg.Secure[k] = v0alpha1.SecureValue{
|
||||
Reference: "~", // ????
|
||||
cfg.Secure[k] = secret.InlineSecureValue{
|
||||
UID: "????", // ????
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,8 +174,8 @@ func toSecureJsonData(ds *v0alpha1.GenericDataSource) map[string]string {
|
||||
|
||||
secure := map[string]string{}
|
||||
for k, v := range ds.Secure {
|
||||
if v.Input != "" {
|
||||
secure[k] = v.Input
|
||||
if v.Create != "" {
|
||||
secure[k] = v.Create.DangerouslyExposeAndConsumeValue()
|
||||
}
|
||||
if v.Remove {
|
||||
secure[k] = "" // Weirdly, this is the best we can do with the legacy API :(
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
"bbb": true,
|
||||
"ccc": 1.234
|
||||
},
|
||||
"secureJsonData": {
|
||||
"password": "XXXX"
|
||||
},
|
||||
"secureJsonData": {},
|
||||
"uid": "cejobd88i85j4d"
|
||||
}
|
||||
@@ -14,9 +14,7 @@
|
||||
"bbb": true,
|
||||
"ccc": 1.234
|
||||
},
|
||||
"secureJsonData": {
|
||||
"password": "XXXX"
|
||||
},
|
||||
"secureJsonData": {},
|
||||
"uid": "cejobd88i85j4d",
|
||||
"version": 2
|
||||
}
|
||||
@@ -4,21 +4,23 @@ import (
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
common "k8s.io/kube-openapi/pkg/common"
|
||||
openapi "k8s.io/kube-openapi/pkg/common"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
spec "k8s.io/kube-openapi/pkg/validation/spec"
|
||||
|
||||
data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
secret "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
|
||||
)
|
||||
|
||||
// This should eventually live in grafana-app-sdk
|
||||
func GetOpenAPIDefinitions(builders []APIGroupBuilder) common.GetOpenAPIDefinitions {
|
||||
return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
||||
defs := v0alpha1.GetOpenAPIDefinitions(ref) // common grafana apis
|
||||
func GetOpenAPIDefinitions(builders []APIGroupBuilder) openapi.GetOpenAPIDefinitions {
|
||||
return func(ref openapi.ReferenceCallback) map[string]openapi.OpenAPIDefinition {
|
||||
defs := common.GetOpenAPIDefinitions(ref) // common grafana apis
|
||||
maps.Copy(defs, data.GetOpenAPIDefinitions(ref))
|
||||
maps.Copy(defs, secret.GetOpenAPIDefinitions(ref)) // Expose secret reference to all resources
|
||||
// TODO: add timerange to upstream SDK setup
|
||||
maps.Copy(defs, map[string]common.OpenAPIDefinition{
|
||||
maps.Copy(defs, map[string]openapi.OpenAPIDefinition{
|
||||
"github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1.TimeRange": {
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
|
||||
@@ -254,10 +254,16 @@ type GetDataSourcesByTypeQuery struct {
|
||||
// GetDataSourceQuery will get a DataSource based on OrgID as well as the UID (preferred), ID, or Name.
|
||||
// At least one of the UID, ID, or Name properties must be set in addition to OrgID.
|
||||
type GetDataSourceQuery struct {
|
||||
ID int64
|
||||
UID string
|
||||
// Deprecated: use UID
|
||||
ID int64
|
||||
|
||||
// The datasource unique id
|
||||
UID string
|
||||
|
||||
// Deprecated: Use UID
|
||||
Name string
|
||||
|
||||
// Required
|
||||
OrgID int64
|
||||
}
|
||||
|
||||
|
||||
@@ -481,12 +481,15 @@ func (s *Service) UpdateDataSource(ctx context.Context, cmd *datasources.UpdateD
|
||||
|
||||
query := &datasources.GetDataSourceQuery{
|
||||
ID: cmd.ID,
|
||||
UID: cmd.UID,
|
||||
OrgID: cmd.OrgID,
|
||||
}
|
||||
dataSource, err = s.SQLStore.GetDataSource(ctx, query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.UID = dataSource.UID
|
||||
cmd.ID = dataSource.ID
|
||||
|
||||
// Validate the command
|
||||
jd, err := cmd.JsonData.ToDB()
|
||||
|
||||
@@ -61,7 +61,7 @@ func (ss *SqlStore) GetDataSource(ctx context.Context, query *datasources.GetDat
|
||||
}
|
||||
|
||||
func (ss *SqlStore) getDataSource(_ context.Context, query *datasources.GetDataSourceQuery, sess *db.Session) (*datasources.DataSource, error) {
|
||||
if query.OrgID == 0 || (query.ID == 0 && len(query.Name) == 0 && len(query.UID) == 0) {
|
||||
if query.OrgID == 0 {
|
||||
return nil, datasources.ErrDataSourceIdentifierNotSet
|
||||
}
|
||||
|
||||
@@ -69,6 +69,8 @@ func (ss *SqlStore) getDataSource(_ context.Context, query *datasources.GetDataS
|
||||
if err := util.ValidateUID(query.UID); err != nil {
|
||||
logDeprecatedInvalidDsUid(ss.logger, query.UID, query.Name, "read", fmt.Errorf("invalid UID"))
|
||||
}
|
||||
} else if query.ID == 0 {
|
||||
return nil, datasources.ErrDataSourceIdentifierNotSet
|
||||
}
|
||||
|
||||
datasource := &datasources.DataSource{Name: query.Name, OrgID: query.OrgID, ID: query.ID, UID: query.UID}
|
||||
@@ -324,11 +326,13 @@ func (ss *SqlStore) UpdateDataSource(ctx context.Context, cmd *datasources.Updat
|
||||
cmd.JsonData = simplejson.New()
|
||||
}
|
||||
|
||||
if cmd.UID != "" {
|
||||
if err := util.ValidateUID(cmd.UID); err != nil {
|
||||
logDeprecatedInvalidDsUid(ss.logger, cmd.UID, cmd.Name, "update", err)
|
||||
return datasources.ErrDataSourceUIDInvalid.Errorf("invalid UID for datasource %s: %w", cmd.Name, err)
|
||||
}
|
||||
if cmd.OrgID == 0 || cmd.ID == 0 || cmd.UID == "" {
|
||||
return datasources.ErrDataSourceIdentifierNotSet
|
||||
}
|
||||
|
||||
if err := util.ValidateUID(cmd.UID); err != nil {
|
||||
logDeprecatedInvalidDsUid(ss.logger, cmd.UID, cmd.Name, "update", err)
|
||||
return datasources.ErrDataSourceUIDInvalid.Errorf("invalid UID for datasource %s: %w", cmd.Name, err)
|
||||
}
|
||||
|
||||
ds = &datasources.DataSource{
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
apiVersion: testdata.datasource.grafana.app/v0alpha1
|
||||
kind: GenericDataSource
|
||||
metadata:
|
||||
name: sample-testdata
|
||||
spec:
|
||||
title: Sample datasource
|
||||
access: proxy
|
||||
isDefault: true
|
||||
jsonData:
|
||||
key: value
|
||||
hello: 10
|
||||
world: false
|
||||
secure:
|
||||
sampleA:
|
||||
create: secret value here # replaced with UID on write
|
||||
sampleB:
|
||||
uid: XYZ # will not exist
|
||||
Reference in New Issue
Block a user