Chore: Use IAM DBTime utility in dashboard migrator (#112589)

This commit is contained in:
Ryan McKinley
2025-10-17 17:48:30 +03:00
committed by GitHub
parent 4cf11b721a
commit 7c6afeabec
9 changed files with 247 additions and 109 deletions
@@ -15,7 +15,6 @@ import (
"k8s.io/utils/ptr"
claims "github.com/grafana/authlib/types"
dashboardOG "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard"
dashboardV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
dashboardV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
@@ -293,12 +292,12 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo
var orgId int64
var folder_uid sql.NullString
var title string
var updated time.Time
var updated legacysql.DBTime
var updatedBy sql.NullString
var updatedByID sql.NullInt64
var deleted sql.NullTime
var created time.Time
var created legacysql.DBTime
var createdBy sql.NullString
var createdByID sql.NullInt64
var message sql.NullString
@@ -338,13 +337,13 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows, history bool) (*dashboardRo
dash.Namespace = a.namespacer(orgId)
dash.APIVersion = fmt.Sprintf("%s/%s", dashboardV1.GROUP, apiVersion.String)
dash.UID = gapiutil.CalculateClusterWideUID(dash)
dash.SetCreationTimestamp(metav1.NewTime(created))
dash.SetCreationTimestamp(metav1.NewTime(created.Time))
meta, err := utils.MetaAccessor(dash)
if err != nil {
a.log.Debug("failed to get meta accessor for dashboard", "error", err, "uid", dash.UID, "name", dash.Name, "version", version)
return nil, err
}
meta.SetUpdatedTimestamp(&updated)
meta.SetUpdatedTimestamp(&updated.Time)
meta.SetCreatedBy(getUserID(createdBy, createdByID))
meta.SetUpdatedBy(getUserID(updatedBy, updatedByID))
meta.SetDeprecatedInternalID(dashboard_id) //nolint:staticcheck
@@ -121,8 +121,8 @@ type CreateServiceAccountCommand struct {
Role string
IsDisabled bool
OrgID int64
Created DBTime
Updated DBTime
Created legacysql.DBTime
Updated legacysql.DBTime
LastSeenAt time.Time
}
@@ -335,8 +335,8 @@ func (s *legacySQLStore) CreateServiceAccount(ctx context.Context, ns claims.Nam
now := time.Now().UTC()
lastSeenAt := now.AddDate(-10, 0, 0) // Set last seen 10 years ago like in user service
cmd.Created = NewDBTime(now)
cmd.Updated = NewDBTime(now)
cmd.Created = legacysql.NewDBTime(now)
cmd.Updated = legacysql.NewDBTime(now)
cmd.LastSeenAt = lastSeenAt
if ns.OrgID == 0 {
-54
View File
@@ -2,11 +2,9 @@ package legacy
import (
"context"
"database/sql/driver"
"embed"
"fmt"
"text/template"
"time"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/storage/legacysql"
@@ -67,55 +65,3 @@ func mustTemplate(filename string) *template.Template {
}
panic(fmt.Sprintf("template file not found: %s", filename))
}
type DBTime struct {
time.Time
}
func NewDBTime(t time.Time) DBTime {
return DBTime{Time: t}
}
func (t DBTime) Value() (driver.Value, error) {
if t.IsZero() {
return nil, nil
}
return t.Format(time.DateTime), nil
}
func (t DBTime) String() string {
if t.IsZero() {
return ""
}
return t.Format(time.DateTime)
}
func (t *DBTime) Scan(value interface{}) error {
if value == nil {
t.Time = time.Time{}
return nil
}
var parsedTime time.Time
var err error
switch v := value.(type) {
case []byte:
parsedTime, err = time.Parse(time.DateTime, string(v))
case string:
parsedTime, err = time.Parse(time.DateTime, v)
case time.Time:
parsedTime = v
default:
return fmt.Errorf("could not scan type %T into DBTime", value)
}
if err != nil {
return fmt.Errorf("could not parse time: %w", err)
}
t.Time = parsedTime
return nil
}
+25 -25
View File
@@ -215,8 +215,8 @@ func TestIdentityQueries(t *testing.T) {
Data: createTeamMember(&CreateTeamMemberCommand{
TeamID: 1,
UserID: 1,
Created: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Updated: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Created: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Updated: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
External: false,
Permission: team.PermissionTypeMember,
}),
@@ -226,8 +226,8 @@ func TestIdentityQueries(t *testing.T) {
Data: createTeamMember(&CreateTeamMemberCommand{
TeamID: 1,
UserID: 1,
Created: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Updated: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Created: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Updated: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
External: false,
Permission: team.PermissionTypeAdmin,
}),
@@ -425,8 +425,8 @@ func TestIdentityQueries(t *testing.T) {
Email: "team1@example.com",
IsProvisioned: false,
OrgID: 1,
Created: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Updated: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Created: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Updated: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
}),
},
{
@@ -438,8 +438,8 @@ func TestIdentityQueries(t *testing.T) {
IsProvisioned: true,
ExternalUID: "team-2-uid",
OrgID: 1,
Created: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Updated: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Created: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Updated: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
}),
},
},
@@ -452,7 +452,7 @@ func TestIdentityQueries(t *testing.T) {
Email: "team1@example.com",
IsProvisioned: true,
ExternalUID: "team-1-uid",
Updated: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Updated: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
}),
},
},
@@ -471,8 +471,8 @@ func TestIdentityQueries(t *testing.T) {
OrgID: 1,
UserID: 123,
Role: "Viewer",
Created: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Updated: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Created: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Updated: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
}),
},
{
@@ -481,8 +481,8 @@ func TestIdentityQueries(t *testing.T) {
OrgID: 2,
UserID: 456,
Role: "Admin",
Created: NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
Updated: NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
Created: legacysql.NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
Updated: legacysql.NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
}),
},
},
@@ -501,9 +501,9 @@ func TestIdentityQueries(t *testing.T) {
IsProvisioned: false,
Salt: "randomsalt",
Rands: "randomrands",
Created: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Updated: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
LastSeenAt: NewDBTime(time.Date(2013, 1, 1, 12, 0, 0, 0, time.UTC)),
Created: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Updated: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
LastSeenAt: legacysql.NewDBTime(time.Date(2013, 1, 1, 12, 0, 0, 0, time.UTC)),
Role: "Viewer",
}),
},
@@ -521,9 +521,9 @@ func TestIdentityQueries(t *testing.T) {
IsProvisioned: true,
Salt: "adminsalt",
Rands: "adminrands",
Created: NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
Updated: NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
LastSeenAt: NewDBTime(time.Date(2013, 2, 1, 10, 30, 0, 0, time.UTC)),
Created: legacysql.NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
Updated: legacysql.NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
LastSeenAt: legacysql.NewDBTime(time.Date(2013, 2, 1, 10, 30, 0, 0, time.UTC)),
Role: "Admin",
}),
},
@@ -538,8 +538,8 @@ func TestIdentityQueries(t *testing.T) {
Login: "sa-1-service-account-1",
IsDisabled: false,
OrgID: 1,
Created: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Updated: NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Created: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
Updated: legacysql.NewDBTime(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)),
LastSeenAt: time.Date(2013, 1, 1, 12, 0, 0, 0, time.UTC),
}),
},
@@ -552,8 +552,8 @@ func TestIdentityQueries(t *testing.T) {
Login: "sa-2-disabled-service-account",
IsDisabled: true,
OrgID: 2,
Created: NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
Updated: NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
Created: legacysql.NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
Updated: legacysql.NewDBTime(time.Date(2023, 2, 1, 10, 30, 0, 0, time.UTC)),
LastSeenAt: time.Date(2013, 2, 1, 10, 30, 0, 0, time.UTC),
}),
},
@@ -570,7 +570,7 @@ func TestIdentityQueries(t *testing.T) {
IsDisabled: true,
EmailVerified: false,
Role: "Editor",
Updated: NewDBTime(time.Date(2023, 1, 1, 13, 0, 0, 0, time.UTC)),
Updated: legacysql.NewDBTime(time.Date(2023, 1, 1, 13, 0, 0, 0, time.UTC)),
}),
},
},
@@ -581,7 +581,7 @@ func TestIdentityQueries(t *testing.T) {
OrgID: 1,
UserID: 123,
Role: "Admin",
Updated: NewDBTime(time.Date(2023, 1, 1, 14, 0, 0, 0, time.UTC)),
Updated: legacysql.NewDBTime(time.Date(2023, 1, 1, 14, 0, 0, 0, time.UTC)),
}),
},
},
+6 -6
View File
@@ -180,8 +180,8 @@ type CreateTeamCommand struct {
UID string
Name string
OrgID int64
Created DBTime
Updated DBTime
Created legacysql.DBTime
Updated legacysql.DBTime
Email string
ExternalID string
IsProvisioned bool
@@ -215,8 +215,8 @@ func (r createTeamQuery) Validate() error {
func (s *legacySQLStore) CreateTeam(ctx context.Context, ns claims.NamespaceInfo, cmd CreateTeamCommand) (*CreateTeamResult, error) {
now := time.Now().UTC()
cmd.Created = NewDBTime(now)
cmd.Updated = NewDBTime(now)
cmd.Created = legacysql.NewDBTime(now)
cmd.Updated = legacysql.NewDBTime(now)
cmd.OrgID = ns.OrgID
if cmd.OrgID == 0 {
@@ -267,7 +267,7 @@ func (s *legacySQLStore) CreateTeam(ctx context.Context, ns claims.NamespaceInfo
type UpdateTeamCommand struct {
UID string
Name string
Updated DBTime
Updated legacysql.DBTime
Email string
ExternalID string
IsProvisioned bool
@@ -301,7 +301,7 @@ func (r updateTeamQuery) Validate() error {
func (s *legacySQLStore) UpdateTeam(ctx context.Context, ns claims.NamespaceInfo, cmd UpdateTeamCommand) (*UpdateTeamResult, error) {
now := time.Now().UTC()
cmd.Updated = NewDBTime(now)
cmd.Updated = legacysql.NewDBTime(now)
sql, err := s.sql(ctx)
if err != nil {
+4 -4
View File
@@ -132,8 +132,8 @@ type CreateTeamMemberCommand struct {
UserID int64
UserUID string
OrgID int64
Created DBTime
Updated DBTime
Created legacysql.DBTime
Updated legacysql.DBTime
External bool
Permission team.PermissionType
}
@@ -164,8 +164,8 @@ func (r createTeamMemberQuery) Validate() error {
func (s *legacySQLStore) CreateTeamMember(ctx context.Context, ns claims.NamespaceInfo, cmd CreateTeamMemberCommand) (*CreateTeamMemberResult, error) {
now := time.Now().UTC()
cmd.Created = NewDBTime(now)
cmd.Updated = NewDBTime(now)
cmd.Created = legacysql.NewDBTime(now)
cmd.Updated = legacysql.NewDBTime(now)
cmd.OrgID = ns.OrgID
if cmd.OrgID == 0 {
+11 -11
View File
@@ -303,9 +303,9 @@ type CreateUserCommand struct {
IsProvisioned bool
Salt string
Rands string
Created DBTime
Updated DBTime
LastSeenAt DBTime
Created legacysql.DBTime
Updated legacysql.DBTime
LastSeenAt legacysql.DBTime
Role string
}
@@ -317,15 +317,15 @@ type CreateOrgUserCommand struct {
OrgID int64
UserID int64
Role string
Created DBTime
Updated DBTime
Created legacysql.DBTime
Updated legacysql.DBTime
}
type UpdateOrgUserCommand struct {
OrgID int64
UserID int64
Role string
Updated DBTime
Updated legacysql.DBTime
}
type DeleteUserCommand struct {
@@ -395,9 +395,9 @@ func (s *legacySQLStore) CreateUser(ctx context.Context, ns claims.NamespaceInfo
cmd.Salt = salt
cmd.Rands = rands
cmd.Created = NewDBTime(now)
cmd.Updated = NewDBTime(now)
cmd.LastSeenAt = NewDBTime(lastSeenAt)
cmd.Created = legacysql.NewDBTime(now)
cmd.Updated = legacysql.NewDBTime(now)
cmd.LastSeenAt = legacysql.NewDBTime(lastSeenAt)
sql, err := s.sql(ctx)
if err != nil {
@@ -616,7 +616,7 @@ type UpdateUserCommand struct {
IsDisabled bool
EmailVerified bool
Role string
Updated DBTime
Updated legacysql.DBTime
}
type UpdateUserResult struct {
@@ -662,7 +662,7 @@ func (r updateOrgUserQuery) Validate() error {
// UpdateUser implements LegacyIdentityStore.
func (s *legacySQLStore) UpdateUser(ctx context.Context, ns claims.NamespaceInfo, cmd UpdateUserCommand) (*UpdateUserResult, error) {
now := time.Now().UTC()
cmd.Updated = NewDBTime(now)
cmd.Updated = legacysql.NewDBTime(now)
sql, err := s.sql(ctx)
if err != nil {
+59
View File
@@ -0,0 +1,59 @@
package legacysql
import (
"database/sql/driver"
"fmt"
"time"
)
type DBTime struct {
time.Time
}
func NewDBTime(t time.Time) DBTime {
return DBTime{Time: t}
}
func (t DBTime) Value() (driver.Value, error) {
if t.IsZero() {
return nil, nil
}
return t.Format(time.DateTime), nil
}
func (t DBTime) String() string {
if t.IsZero() {
return ""
}
return t.Format(time.DateTime)
}
func (t *DBTime) Scan(value interface{}) error {
if value == nil {
t.Time = time.Time{}
return nil
}
var parsedTime time.Time
var err error
switch v := value.(type) {
case []byte:
parsedTime, err = time.Parse(time.DateTime, string(v))
case string:
parsedTime, err = time.Parse(time.DateTime, v)
case time.Time:
parsedTime = v
default:
return fmt.Errorf("could not scan type %T into DBTime", value)
}
if err != nil {
return fmt.Errorf("could not parse time: %w", err)
}
t.Time = parsedTime
return nil
}
+134
View File
@@ -0,0 +1,134 @@
package legacysql
import (
"fmt"
"testing"
"time"
)
func TestDBTime_ValueAndString_ZeroAndNonZero(t *testing.T) {
zero := NewDBTime(time.Time{})
if v, err := zero.Value(); err != nil {
t.Fatalf("unexpected error for zero Value: %v", err)
} else if v != nil {
t.Fatalf("expected nil driver.Value for zero time, got %#v", v)
}
if s := zero.String(); s != "" {
t.Fatalf("expected empty string for zero DBTime.String(), got %q", s)
}
tt := time.Date(2023, 10, 5, 13, 14, 15, 0, time.UTC)
dt := NewDBTime(tt)
val, err := dt.Value()
if err != nil {
t.Fatalf("unexpected error for non-zero Value: %v", err)
}
strVal, ok := val.(string)
if !ok {
t.Fatalf("expected Value to be string, got %T", val)
}
expected := tt.Format(time.DateTime)
if strVal != expected {
t.Fatalf("Value() = %q, want %q", strVal, expected)
}
if s := dt.String(); s != expected {
t.Fatalf("String() = %q, want %q", s, expected)
}
}
func TestDBTime_Scan_VariousInputs(t *testing.T) {
base := time.Date(2022, 12, 31, 23, 59, 59, 0, time.UTC)
layout := time.DateTime
formatted := base.Format(layout)
tests := []struct {
name string
input interface{}
wantTime time.Time
wantIsZero bool
expectError bool
}{
{"nil sets zero", nil, time.Time{}, true, false},
{"scan from string", formatted, base, false, false},
{"scan from []byte", []byte(formatted), base, false, false},
{"scan from time.Time", base, base, false, false},
{"invalid parse", "not-a-time", time.Time{}, true, true},
{"unsupported type", 12345, time.Time{}, true, true},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var dt DBTime
err := dt.Scan(tc.input)
if tc.expectError {
if err == nil {
t.Fatalf("expected error, got nil")
}
// when expecting error, no further checks
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if tc.wantIsZero {
if !dt.IsZero() {
t.Fatalf("expected DBTime to be zero, got %v", dt.Time)
}
return
}
if dt.IsZero() {
t.Fatalf("expected non-zero time, got zero")
}
// Compare times using Equal to ignore monotonic clock bits
if !dt.Equal(tc.wantTime) {
t.Fatalf("scanned time = %v, want %v", dt.Time, tc.wantTime)
}
})
}
}
func TestDBTime_Scan_ParseErrorDetails(t *testing.T) {
var dt DBTime
err := dt.Scan("garbage")
if err == nil {
t.Fatalf("expected parse error, got nil")
}
// error message should mention parse
if !contains(err.Error(), "could not parse time") {
t.Fatalf("unexpected error message: %v", err)
}
}
func TestDBTime_Scan_UnsupportedTypeMessage(t *testing.T) {
var dt DBTime
typ := struct{}{}
err := dt.Scan(typ)
if err == nil {
t.Fatalf("expected unsupported-type error, got nil")
}
want := fmt.Sprintf("could not scan type %T into DBTime", typ)
if err.Error() != want {
t.Fatalf("error = %q, want %q", err.Error(), want)
}
}
// small helper to avoid importing strings for a single contains check
func contains(s, substr string) bool {
return len(substr) == 0 || (len(s) >= len(substr) && indexOf(s, substr) >= 0)
}
func indexOf(s, substr string) int {
n := len(s)
m := len(substr)
if m == 0 {
return 0
}
for i := 0; i <= n-m; i++ {
if s[i:i+m] == substr {
return i
}
}
return -1
}