secure value update

This commit is contained in:
Ryan McKinley
2025-06-20 00:18:14 +03:00
parent 9bbe311bdf
commit 1ea3b5440c
11 changed files with 143 additions and 31 deletions
+10 -5
View File
@@ -15,13 +15,18 @@ type GenericDataSource struct {
Spec GenericDataSourceSpec `json:"spec"`
// Secure values placeholder (true for fields that exist)
Secure map[string]bool `json:"secure,omitempty"`
Secure map[string]SecureValue `json:"secure,omitempty"`
}
// // swagger:ignore
// Password string `json:"-"`
type SecureValue struct {
// The input is only valid for writing the value -- it is replaced on read
Input string `json:"input,omitempty"`
// // swagger:ignore
// BasicAuthPassword string `json:"-"`
// 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"`
}
// DsAccess represents how the datasource connects to the remote service
@@ -78,7 +78,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]bool, len(*in))
*out = make(map[string]SecureValue, len(*in))
for key, val := range *in {
(*out)[key] = val
}
@@ -182,3 +182,19 @@ 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,6 +20,7 @@ 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),
}
}
@@ -162,9 +163,8 @@ func schema_pkg_apis_datasource_v0alpha1_GenericDataSource(ref common.ReferenceC
Allows: true,
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: false,
Type: []string{"boolean"},
Format: "",
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.SecureValue"),
},
},
},
@@ -175,7 +175,7 @@ func schema_pkg_apis_datasource_v0alpha1_GenericDataSource(ref common.ReferenceC
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1.GenericDataSourceSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
"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"},
}
}
@@ -367,3 +367,36 @@ 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,2 +1,3 @@
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
+37 -10
View File
@@ -2,6 +2,7 @@ package datasource
import (
"fmt"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -15,6 +16,8 @@ import (
type converter struct {
mapper request.NamespaceMapper
group string // the expected group
dstype string // the expected pluginId
}
func asConnection(ds *datasources.DataSource, ns string) (*v0alpha1.DataSourceConnection, error) {
@@ -73,9 +76,11 @@ func (r *converter) asGenericDataSource(ds *datasources.DataSource) (*v0alpha1.G
}
if ds.SecureJsonData != nil {
cfg.Secure = make(map[string]bool)
cfg.Secure = make(map[string]v0alpha1.SecureValue)
for k := range ds.SecureJsonData {
cfg.Secure[k] = true
cfg.Secure[k] = v0alpha1.SecureValue{
Reference: "~", // ????
}
}
}
@@ -83,11 +88,15 @@ func (r *converter) asGenericDataSource(ds *datasources.DataSource) (*v0alpha1.G
}
func (r *converter) toAddCommand(ds *v0alpha1.GenericDataSource) (*datasources.AddDataSourceCommand, error) {
if r.group != "" && !strings.HasPrefix(ds.APIVersion, r.group) {
return nil, fmt.Errorf("expecting APIGroup: %s", r.group)
}
cmd := &datasources.AddDataSourceCommand{
Name: ds.Spec.Title,
UID: ds.Name,
Type: r.dstype,
Type: "???", // TODO... group > datasource type????
Access: datasources.DsAccess(ds.Spec.Access),
URL: ds.Spec.URL,
Database: ds.Spec.Database,
@@ -103,21 +112,21 @@ func (r *converter) toAddCommand(ds *v0alpha1.GenericDataSource) (*datasources.A
cmd.JsonData = simplejson.NewFromAny(ds.Spec.JsonData.Object)
}
if len(ds.Secure) > 0 {
cmd.SecureJsonData = map[string]string{
"TODO": "values",
}
}
cmd.SecureJsonData = toSecureJsonData(ds)
return cmd, nil
}
func (r *converter) toUpdateCommand(ds *v0alpha1.GenericDataSource) (*datasources.UpdateDataSourceCommand, error) {
if r.group != "" && !strings.HasPrefix(ds.APIVersion, r.group) {
return nil, fmt.Errorf("expecting APIGroup: %s", r.group)
}
cmd := &datasources.UpdateDataSourceCommand{
Name: ds.Spec.Title,
UID: ds.Name,
Type: r.dstype,
Type: "???", // TODO... group > datasource type????
Access: datasources.DsAccess(ds.Spec.Access),
URL: ds.Spec.URL,
Database: ds.Spec.Database,
@@ -132,8 +141,26 @@ func (r *converter) toUpdateCommand(ds *v0alpha1.GenericDataSource) (*datasource
if len(ds.Spec.JsonData.Object) > 0 {
cmd.JsonData = simplejson.NewFromAny(ds.Spec.JsonData.Object)
}
cmd.SecureJsonData = toSecureJsonData(ds)
// Update specific things
// The only thing differnet from the add command???
cmd.Version = int(ds.Generation)
return cmd, nil
}
func toSecureJsonData(ds *v0alpha1.GenericDataSource) map[string]string {
if ds == nil || len(ds.Secure) < 1 {
return nil
}
secure := map[string]string{}
for k, v := range ds.Secure {
if v.Input != "" {
secure[k] = v.Input
}
if v.Remove {
secure[k] = "" // Weirdly, this is the best we can do with the legacy API :(
}
}
return secure
}
@@ -16,14 +16,17 @@ import (
func TestConverter(t *testing.T) {
t.Run("resource to command", func(t *testing.T) {
obj := &v0alpha1.GenericDataSource{}
converter := converter{mapper: types.OrgNamespaceFormatter}
converter := converter{
mapper: types.OrgNamespaceFormatter,
dstype: "test-datasource",
}
check := []string{
"convert-testdata-A",
}
for _, name := range check {
t.Run(name, func(t *testing.T) {
fpath := filepath.Join("testdata", name+"-input.json")
raw, err := os.ReadFile(fpath)
raw, err := os.ReadFile(fpath) // nolint:gosec
require.NoError(t, err)
err = json.Unmarshal(raw, obj)
require.NoError(t, err)
@@ -34,9 +37,9 @@ func TestConverter(t *testing.T) {
require.NoError(t, err)
out, err := json.MarshalIndent(add, "", " ")
require.NoError(t, err)
raw, _ = os.ReadFile(fpath)
raw, _ = os.ReadFile(fpath) // nolint:gosec
if !assert.JSONEq(t, string(raw), string(out)) {
os.WriteFile(fpath, out, 0600)
_ = os.WriteFile(fpath, out, 0600)
}
// The update command
@@ -45,9 +48,9 @@ func TestConverter(t *testing.T) {
require.NoError(t, err)
out, err = json.MarshalIndent(update, "", " ")
require.NoError(t, err)
raw, _ = os.ReadFile(fpath)
raw, _ = os.ReadFile(fpath) // nolint:gosec
if !assert.JSONEq(t, string(raw), string(out)) {
os.WriteFile(fpath, out, 0600)
_ = os.WriteFile(fpath, out, 0600)
}
})
}
@@ -86,6 +86,21 @@ func (s *legacyStorage) Update(ctx context.Context, name string, objInfo rest.Up
return nil, false, fmt.Errorf("expected a datasource object")
}
oldDS, ok := obj.(*v0alpha1.GenericDataSource)
if !ok {
return nil, false, fmt.Errorf("expected a datasource object (old)")
}
// Keep all the old secure values
if len(oldDS.Secure) > 0 {
for k, v := range oldDS.Secure {
_, found := ds.Secure[k]
if !found {
ds.Secure[k] = v
}
}
}
ds, err = s.datasources.UpdateDataSource(ctx, ds)
return ds, false, err
}
@@ -75,12 +75,17 @@ type cachingDatasourceProvider struct {
}
func (q *cachingDatasourceProvider) GetDatasourceProvider(pluginJson plugins.JSONData) PluginDatasourceProvider {
group, _ := plugins.GetDatasourceGroupNameFromPluginID(pluginJson.ID)
return &scopedDatasourceProvider{
plugin: pluginJson,
dsService: q.dsService,
dsCache: q.dsCache,
contextProvider: q.contextProvider,
converter: q.converter,
converter: &converter{
mapper: q.converter.mapper,
dstype: pluginJson.ID,
group: group,
},
}
}
@@ -18,5 +18,8 @@
"bbb": true,
"ccc": 1.234
}
},
"secure": {
"password": { "input": "XXXX" }
}
}
@@ -1,6 +1,6 @@
{
"name": "grafana-testdata-datasource",
"type": "???",
"type": "test-datasource",
"access": "proxy",
"url": "http://something/",
"database": "db",
@@ -14,7 +14,9 @@
"bbb": true,
"ccc": 1.234
},
"secureJsonData": null,
"secureJsonData": {
"password": "XXXX"
},
"uid": "cejobd88i85j4d",
"apiVersion": "",
"IsPrunable": false
@@ -1,6 +1,6 @@
{
"name": "grafana-testdata-datasource",
"type": "???",
"type": "test-datasource",
"access": "proxy",
"url": "http://something/",
"user": "",
@@ -14,7 +14,9 @@
"bbb": true,
"ccc": 1.234
},
"secureJsonData": null,
"secureJsonData": {
"password": "XXXX"
},
"version": 2,
"uid": "cejobd88i85j4d",
"apiVersion": "",