diff --git a/pkg/apis/datasource/v0alpha1/unstructured.go b/pkg/apis/datasource/v0alpha1/unstructured.go index 8d43a16f9e1..f4b38cc5213 100644 --- a/pkg/apis/datasource/v0alpha1/unstructured.go +++ b/pkg/apis/datasource/v0alpha1/unstructured.go @@ -29,7 +29,7 @@ func (u *UnstructuredSpec) Set(key string, val any) *UnstructuredSpec { if u.Object == nil { u.Object = make(map[string]any) } - if val == nil || val == "" { + if val == nil || val == "" || val == false { delete(u.Object, key) } else { u.Object[key] = val diff --git a/pkg/registry/apis/datasource/converter.go b/pkg/registry/apis/datasource/converter.go index d9ba960ba07..8d5426bfc59 100644 --- a/pkg/registry/apis/datasource/converter.go +++ b/pkg/registry/apis/datasource/converter.go @@ -8,6 +8,7 @@ import ( "maps" "strconv" "strings" + "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -28,19 +29,17 @@ type converter struct { } func (r *converter) asDataSource(ds *datasources.DataSource) (*datasourceV0.DataSource, error) { - cfg := &datasourceV0.DataSource{ + obj := &datasourceV0.DataSource{ ObjectMeta: metav1.ObjectMeta{ - Name: ds.UID, - Namespace: r.mapper(ds.OrgID), - CreationTimestamp: metav1.NewTime(ds.Created), - ResourceVersion: fmt.Sprintf("%d", ds.Updated.UnixMilli()), - Generation: int64(ds.Version), + Name: ds.UID, + Namespace: r.mapper(ds.OrgID), + Generation: int64(ds.Version), }, Spec: datasourceV0.UnstructuredSpec{}, Secure: ToInlineSecureValues(ds.Type, ds.UID, maps.Keys(ds.SecureJsonData)), } - cfg.UID = gapiutil.CalculateClusterWideUID(cfg) - cfg.Spec.SetTitle(ds.Name). + obj.UID = gapiutil.CalculateClusterWideUID(obj) + obj.Spec.SetTitle(ds.Name). SetAccess(string(ds.Access)). SetURL(ds.URL). SetDatabase(ds.Database). @@ -53,13 +52,26 @@ func (r *converter) asDataSource(ds *datasources.DataSource) (*datasourceV0.Data SetReadOnly(ds.ReadOnly). SetJSONData(ds.JsonData) - if ds.ID > 0 { - cfg.Labels = map[string]string{ - utils.LabelKeyDeprecatedInternalID: strconv.FormatInt(ds.ID, 10), + if !ds.Created.IsZero() { + obj.CreationTimestamp = metav1.NewTime(ds.Created) + } + if !ds.Updated.IsZero() { + obj.ResourceVersion = fmt.Sprintf("%d", ds.Updated.UnixMilli()) + obj.Annotations = map[string]string{ + utils.AnnoKeyUpdatedTimestamp: ds.Updated.Format(time.RFC3339), } } - return cfg, nil + if ds.APIVersion != "" { + obj.APIVersion = fmt.Sprintf("%s/%s", r.group, ds.APIVersion) + } + + if ds.ID > 0 { + obj.Labels = map[string]string{ + utils.LabelKeyDeprecatedInternalID: strconv.FormatInt(ds.ID, 10), + } + } + return obj, nil } // ToInlineSecureValues converts secure json into InlineSecureValues with reference names @@ -85,7 +97,7 @@ func ToInlineSecureValues(dsType string, dsUID string, keys iter.Seq[string]) co } func (r *converter) toAddCommand(ds *datasourceV0.DataSource) (*datasources.AddDataSourceCommand, error) { - if r.group != "" && !strings.HasPrefix(ds.APIVersion, r.group) { + if r.group != "" && ds.APIVersion != "" && !strings.HasPrefix(ds.APIVersion, r.group) { return nil, fmt.Errorf("expecting APIGroup: %s", r.group) } info, err := types.ParseNamespace(ds.Namespace) @@ -120,7 +132,7 @@ func (r *converter) toAddCommand(ds *datasourceV0.DataSource) (*datasources.AddD } func (r *converter) toUpdateCommand(ds *datasourceV0.DataSource) (*datasources.UpdateDataSourceCommand, error) { - if r.group != "" && !strings.HasPrefix(ds.APIVersion, r.group) { + if r.group != "" && ds.APIVersion != "" && !strings.HasPrefix(ds.APIVersion, r.group) { return nil, fmt.Errorf("expecting APIGroup: %s", r.group) } info, err := types.ParseNamespace(ds.Namespace) diff --git a/pkg/registry/apis/datasource/converter_test.go b/pkg/registry/apis/datasource/converter_test.go index 4a83822b4db..382c750f541 100644 --- a/pkg/registry/apis/datasource/converter_test.go +++ b/pkg/registry/apis/datasource/converter_test.go @@ -11,20 +11,22 @@ import ( "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" + "github.com/grafana/grafana/pkg/services/datasources" ) func TestConverter(t *testing.T) { t.Run("resource to command", func(t *testing.T) { - obj := &v0alpha1.DataSource{} converter := converter{ mapper: types.OrgNamespaceFormatter, dstype: "test-datasource", + group: "testdata.grafana.datasource.app", } check := []string{ - "convert-testdata-A", + "convert-resource-A", } for _, name := range check { t.Run(name, func(t *testing.T) { + obj := &v0alpha1.DataSource{} fpath := filepath.Join("testdata", name+".json") raw, err := os.ReadFile(fpath) // nolint:gosec require.NoError(t, err) @@ -32,7 +34,7 @@ func TestConverter(t *testing.T) { require.NoError(t, err) // The add command - fpath = filepath.Join("testdata", name+"-cmd-add.json") + fpath = filepath.Join("testdata", name+"-to-cmd-add.json") add, err := converter.toAddCommand(obj) require.NoError(t, err) out, err := json.MarshalIndent(add, "", " ") @@ -43,7 +45,7 @@ func TestConverter(t *testing.T) { } // The update command - fpath = filepath.Join("testdata", name+"-cmd-update.json") + fpath = filepath.Join("testdata", name+"-to-cmd-update.json") update, err := converter.toUpdateCommand(obj) require.NoError(t, err) out, err = json.MarshalIndent(update, "", " ") @@ -52,6 +54,62 @@ func TestConverter(t *testing.T) { if !assert.JSONEq(t, string(raw), string(out)) { _ = os.WriteFile(fpath, out, 0600) } + + // Round trip the update (NOTE, not all properties will be included) + ds := &datasources.DataSource{} + err = json.Unmarshal(raw, ds) // the add command is also a DataSource + require.NoError(t, err) + + roundtrip, err := converter.asDataSource(ds) + require.NoError(t, err) + + fpath = filepath.Join("testdata", name+"-to-cmd-update-roundtrip.json") + out, err = json.MarshalIndent(roundtrip, "", " ") + require.NoError(t, err) + raw, _ = os.ReadFile(fpath) // nolint:gosec + if !assert.JSONEq(t, string(raw), string(out)) { + _ = os.WriteFile(fpath, out, 0600) + } + }) + } + }) + + t.Run("db to resource", func(t *testing.T) { + converter := converter{ + mapper: types.OrgNamespaceFormatter, + dstype: "test-datasource", + group: "testdata.grafana.datasource.app", + } + check := []string{ + "convert-db-B", + } + for _, name := range check { + t.Run(name, func(t *testing.T) { + ds := &datasources.DataSource{} + fpath := filepath.Join("testdata", name+".json") + raw, err := os.ReadFile(fpath) // nolint:gosec + require.NoError(t, err) + err = json.Unmarshal(raw, ds) + require.NoError(t, err) + + // if true { + // ds.Created = time.Unix(100000000, 0).UTC() + // ds.Updated = time.Unix(500000000, 0).UTC() + // out, _ := json.MarshalIndent(ds, "", " ") + // fmt.Printf("%s\n", string(out)) + // t.FailNow() + // } + + // As an object + fpath = filepath.Join("testdata", name+"-to-resource.json") + obj, err := converter.asDataSource(ds) + require.NoError(t, err) + out, err := json.MarshalIndent(obj, "", " ") + require.NoError(t, err) + raw, _ = os.ReadFile(fpath) // nolint:gosec + if !assert.JSONEq(t, string(raw), string(out)) { + _ = os.WriteFile(fpath, out, 0600) + } }) } }) diff --git a/pkg/registry/apis/datasource/testdata/convert-db-B-to-resource.json b/pkg/registry/apis/datasource/testdata/convert-db-B-to-resource.json new file mode 100644 index 00000000000..d84a35cd755 --- /dev/null +++ b/pkg/registry/apis/datasource/testdata/convert-db-B-to-resource.json @@ -0,0 +1,39 @@ +{ + "apiVersion": "testdata.grafana.datasource.app/v2alpha1", + "metadata": { + "name": "unique-identifier", + "namespace": "org-0", + "uid": "YpaSG5GQAdxtLZtF6BqQWCeYXOhbVi5C4Cg4oILnJC0X", + "resourceVersion": "1083805200000", + "generation": 2, + "creationTimestamp": "2002-03-04T01:00:00Z", + "labels": { + "grafana.app/deprecatedInternalID": "1234" + }, + "annotations": { + "grafana.app/updatedTimestamp": "2004-05-06T01:00:00Z" + } + }, + "spec": { + "access": "proxy", + "basicAuth": true, + "basicAuthUser": "xxx", + "database": "db", + "isDefault": true, + "jsonData": { + "aaa": "bbb", + "bbb": true, + "ccc": 1.234 + }, + "readOnly": true, + "title": "Hello", + "url": "http://something/", + "user": "A", + "withCredentials": true + }, + "secure": { + "password": { + "name": "ds-c429ac622e" + } + } +} \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-db-B.json b/pkg/registry/apis/datasource/testdata/convert-db-B.json new file mode 100644 index 00000000000..8f24c787717 --- /dev/null +++ b/pkg/registry/apis/datasource/testdata/convert-db-B.json @@ -0,0 +1,27 @@ +{ + "id": 1234, + "version": 2, + "name": "Hello", + "uid": "unique-identifier", + "type": "grafana-test-datasource", + "access": "proxy", + "url": "http://something/", + "user": "A", + "database": "db", + "basicAuth": true, + "basicAuthUser": "xxx", + "withCredentials": true, + "isDefault": true, + "jsonData": { + "aaa": "bbb", + "bbb": true, + "ccc": 1.234 + }, + "secureJsonData": { + "password": "XXXX" + }, + "readOnly": true, + "apiVersion": "v2alpha1", + "created": "2002-03-04T01:00:00Z", + "updated": "2004-05-06T01:00:00Z" +} \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-testdata-A-cmd-add.json b/pkg/registry/apis/datasource/testdata/convert-resource-A-to-cmd-add.json similarity index 60% rename from pkg/registry/apis/datasource/testdata/convert-testdata-A-cmd-add.json rename to pkg/registry/apis/datasource/testdata/convert-resource-A-to-cmd-add.json index 01ddc098069..0bccc0467fd 100644 --- a/pkg/registry/apis/datasource/testdata/convert-testdata-A-cmd-add.json +++ b/pkg/registry/apis/datasource/testdata/convert-resource-A-to-cmd-add.json @@ -1,19 +1,21 @@ { - "name": "grafana-testdata-datasource", + "name": "Hello testdata", "type": "test-datasource", "access": "proxy", "url": "http://something/", "user": "", "database": "db", - "basicAuth": false, - "basicAuthUser": "", - "withCredentials": false, + "basicAuth": true, + "basicAuthUser": "xxx", + "withCredentials": true, "isDefault": true, "jsonData": { "aaa": "bbb", "bbb": true, "ccc": 1.234 }, - "secureJsonData": {}, + "secureJsonData": { + "password": "XXXX" + }, "uid": "cejobd88i85j4d" } \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-resource-A-to-cmd-update-roundtrip.json b/pkg/registry/apis/datasource/testdata/convert-resource-A-to-cmd-update-roundtrip.json new file mode 100644 index 00000000000..55d02cd7f6d --- /dev/null +++ b/pkg/registry/apis/datasource/testdata/convert-resource-A-to-cmd-update-roundtrip.json @@ -0,0 +1,29 @@ +{ + "metadata": { + "name": "cejobd88i85j4d", + "namespace": "org-0", + "uid": "boDNh7zU3nXj46rOXIJI7r44qaxjs8yy9I9dOj1MyBoX", + "generation": 2, + "creationTimestamp": null + }, + "spec": { + "access": "proxy", + "basicAuth": true, + "basicAuthUser": "xxx", + "database": "db", + "isDefault": true, + "jsonData": { + "aaa": "bbb", + "bbb": true, + "ccc": 1.234 + }, + "title": "Hello testdata", + "url": "http://something/", + "withCredentials": true + }, + "secure": { + "password": { + "name": "ds-fdb9f6717d" + } + } +} \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-testdata-A-cmd-update.json b/pkg/registry/apis/datasource/testdata/convert-resource-A-to-cmd-update.json similarity index 62% rename from pkg/registry/apis/datasource/testdata/convert-testdata-A-cmd-update.json rename to pkg/registry/apis/datasource/testdata/convert-resource-A-to-cmd-update.json index 5d4d75c77ef..0a6d779e694 100644 --- a/pkg/registry/apis/datasource/testdata/convert-testdata-A-cmd-update.json +++ b/pkg/registry/apis/datasource/testdata/convert-resource-A-to-cmd-update.json @@ -1,20 +1,22 @@ { - "name": "grafana-testdata-datasource", + "name": "Hello testdata", "type": "test-datasource", "access": "proxy", "url": "http://something/", "user": "", "database": "db", - "basicAuth": false, - "basicAuthUser": "", - "withCredentials": false, + "basicAuth": true, + "basicAuthUser": "xxx", + "withCredentials": true, "isDefault": true, "jsonData": { "aaa": "bbb", "bbb": true, "ccc": 1.234 }, - "secureJsonData": {}, + "secureJsonData": { + "password": "XXXX" + }, "uid": "cejobd88i85j4d", "version": 2 } \ No newline at end of file diff --git a/pkg/registry/apis/datasource/testdata/convert-testdata-A.json b/pkg/registry/apis/datasource/testdata/convert-resource-A.json similarity index 59% rename from pkg/registry/apis/datasource/testdata/convert-testdata-A.json rename to pkg/registry/apis/datasource/testdata/convert-resource-A.json index 0305b386786..7c4cbd78678 100644 --- a/pkg/registry/apis/datasource/testdata/convert-testdata-A.json +++ b/pkg/registry/apis/datasource/testdata/convert-resource-A.json @@ -5,14 +5,21 @@ "uid": "IGIUtEQS21DtLpBG2rSGfuDoUX8cwsGrtb5aXauYeA4X", "resourceVersion": "1745320815000", "generation": 2, - "creationTimestamp": "2025-04-22T11:20:11Z" + "creationTimestamp": "2025-04-22T11:20:11Z", + "labels": { + "grafana.app/deprecatedInternalID": "12345" + } }, "spec": { - "title": "grafana-testdata-datasource", + "title": "Hello testdata", "access": "proxy", "isDefault": true, + "readOnly": true, "url": "http://something/", "database": "db", + "basicAuth": true, + "basicAuthUser": "xxx", + "withCredentials": true, "jsonData": { "aaa": "bbb", "bbb": true, @@ -20,6 +27,6 @@ } }, "secure": { - "password": { "input": "XXXX" } + "password": { "create": "XXXX" } } } \ No newline at end of file