diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 9a9712a51c0..5676566fd1a 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -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 diff --git a/pkg/registry/apis/iam/legacy/service_account.go b/pkg/registry/apis/iam/legacy/service_account.go index b3b47f25fe7..09760a05cb8 100644 --- a/pkg/registry/apis/iam/legacy/service_account.go +++ b/pkg/registry/apis/iam/legacy/service_account.go @@ -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 { diff --git a/pkg/registry/apis/iam/legacy/sql.go b/pkg/registry/apis/iam/legacy/sql.go index f502dee7d86..7a6ec18a561 100644 --- a/pkg/registry/apis/iam/legacy/sql.go +++ b/pkg/registry/apis/iam/legacy/sql.go @@ -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 -} diff --git a/pkg/registry/apis/iam/legacy/sql_test.go b/pkg/registry/apis/iam/legacy/sql_test.go index 62b4c89edd7..be5de89e593 100644 --- a/pkg/registry/apis/iam/legacy/sql_test.go +++ b/pkg/registry/apis/iam/legacy/sql_test.go @@ -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)), }), }, }, diff --git a/pkg/registry/apis/iam/legacy/team.go b/pkg/registry/apis/iam/legacy/team.go index e1dc0909869..51e68506fbd 100644 --- a/pkg/registry/apis/iam/legacy/team.go +++ b/pkg/registry/apis/iam/legacy/team.go @@ -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 { diff --git a/pkg/registry/apis/iam/legacy/team_binding.go b/pkg/registry/apis/iam/legacy/team_binding.go index 3cf13e779e6..330c4f48062 100644 --- a/pkg/registry/apis/iam/legacy/team_binding.go +++ b/pkg/registry/apis/iam/legacy/team_binding.go @@ -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 { diff --git a/pkg/registry/apis/iam/legacy/user.go b/pkg/registry/apis/iam/legacy/user.go index 0e3ec62388f..905a48e0e60 100644 --- a/pkg/registry/apis/iam/legacy/user.go +++ b/pkg/registry/apis/iam/legacy/user.go @@ -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 { diff --git a/pkg/storage/legacysql/time.go b/pkg/storage/legacysql/time.go new file mode 100644 index 00000000000..7e0d52c1d90 --- /dev/null +++ b/pkg/storage/legacysql/time.go @@ -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 +} diff --git a/pkg/storage/legacysql/time_test.go b/pkg/storage/legacysql/time_test.go new file mode 100644 index 00000000000..b92ae7fc094 --- /dev/null +++ b/pkg/storage/legacysql/time_test.go @@ -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 +}