diff --git a/cmd/reminder/app/start.go b/cmd/reminder/app/start.go index c7f71ea3ee..3d7b93321e 100644 --- a/cmd/reminder/app/start.go +++ b/cmd/reminder/app/start.go @@ -49,11 +49,9 @@ func start(cmd *cobra.Command, _ []string) error { return fmt.Errorf("unable to read config: %w", err) } - // Configuration is normalized to ensure that all values are valid - // and if they can't be fixed, an error is returned - _, err = cfg.Normalize(cmd) + err = cfg.Validate() if err != nil { - return fmt.Errorf("unable to normalize config: %w", err) + return fmt.Errorf("error validating config: %w", err) } ctx = logger.FromFlags(cfg.LoggingConfig).WithContext(ctx) diff --git a/config/reminder-config.yaml.example b/config/reminder-config.yaml.example index 08cf01f2df..5b3d0bcb44 100644 --- a/config/reminder-config.yaml.example +++ b/config/reminder-config.yaml.example @@ -16,8 +16,6 @@ recurrence: interval: "1h" batch_size: 100 - max_per_project: 10 - min_project_fetch_limit: 10 min_elapsed: "1h" database: @@ -28,8 +26,6 @@ database: dbname: minder sslmode: disable -cursor_file: "/tmp/reminder-cursor" - logging level: "debug" diff --git a/database/migrations/000060_repo_reminder.down.sql b/database/migrations/000060_repo_reminder.down.sql new file mode 100644 index 0000000000..d0232502c3 --- /dev/null +++ b/database/migrations/000060_repo_reminder.down.sql @@ -0,0 +1,17 @@ +-- Copyright 2024 Stacklok, Inc +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +ALTER TABLE repositories DROP COLUMN reminder_last_sent; + +DROP EXTENSION IF EXISTS tsm_system_rows; diff --git a/database/migrations/000060_repo_reminder.up.sql b/database/migrations/000060_repo_reminder.up.sql new file mode 100644 index 0000000000..93d11a8937 --- /dev/null +++ b/database/migrations/000060_repo_reminder.up.sql @@ -0,0 +1,17 @@ +-- Copyright 2024 Stacklok, Inc +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +ALTER TABLE repositories ADD COLUMN reminder_last_sent TIMESTAMP; + +CREATE EXTENSION IF NOT EXISTS tsm_system_rows; diff --git a/database/mock/store.go b/database/mock/store.go index 4fffb40ee8..16c0ce8c10 100644 --- a/database/mock/store.go +++ b/database/mock/store.go @@ -1032,6 +1032,21 @@ func (mr *MockStoreMockRecorder) GetQuerierWithTransaction(arg0 any) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetQuerierWithTransaction", reflect.TypeOf((*MockStore)(nil).GetQuerierWithTransaction), arg0) } +// GetRandomRepository mocks base method. +func (m *MockStore) GetRandomRepository(arg0 context.Context) (db.Repository, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRandomRepository", arg0) + ret0, _ := ret[0].(db.Repository) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRandomRepository indicates an expected call of GetRandomRepository. +func (mr *MockStoreMockRecorder) GetRandomRepository(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRandomRepository", reflect.TypeOf((*MockStore)(nil).GetRandomRepository), arg0) +} + // GetRepositoryByID mocks base method. func (m *MockStore) GetRepositoryByID(arg0 context.Context, arg1 uuid.UUID) (db.Repository, error) { m.ctrl.T.Helper() @@ -1242,6 +1257,21 @@ func (mr *MockStoreMockRecorder) ListArtifactsByRepoID(arg0, arg1 any) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListArtifactsByRepoID", reflect.TypeOf((*MockStore)(nil).ListArtifactsByRepoID), arg0, arg1) } +// ListEligibleRepositoriesAfterID mocks base method. +func (m *MockStore) ListEligibleRepositoriesAfterID(arg0 context.Context, arg1 db.ListEligibleRepositoriesAfterIDParams) ([]db.Repository, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListEligibleRepositoriesAfterID", arg0, arg1) + ret0, _ := ret[0].([]db.Repository) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListEligibleRepositoriesAfterID indicates an expected call of ListEligibleRepositoriesAfterID. +func (mr *MockStoreMockRecorder) ListEligibleRepositoriesAfterID(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListEligibleRepositoriesAfterID", reflect.TypeOf((*MockStore)(nil).ListEligibleRepositoriesAfterID), arg0, arg1) +} + // ListFlushCache mocks base method. func (m *MockStore) ListFlushCache(arg0 context.Context) ([]db.FlushCache, error) { m.ctrl.T.Helper() @@ -1481,6 +1511,21 @@ func (mr *MockStoreMockRecorder) ReleaseLock(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReleaseLock", reflect.TypeOf((*MockStore)(nil).ReleaseLock), arg0, arg1) } +// RepositoryExistsAfterID mocks base method. +func (m *MockStore) RepositoryExistsAfterID(arg0 context.Context, arg1 uuid.UUID) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RepositoryExistsAfterID", arg0, arg1) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RepositoryExistsAfterID indicates an expected call of RepositoryExistsAfterID. +func (mr *MockStoreMockRecorder) RepositoryExistsAfterID(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RepositoryExistsAfterID", reflect.TypeOf((*MockStore)(nil).RepositoryExistsAfterID), arg0, arg1) +} + // Rollback mocks base method. func (m *MockStore) Rollback(arg0 *sql.Tx) error { m.ctrl.T.Helper() @@ -1567,6 +1612,20 @@ func (mr *MockStoreMockRecorder) UpdateProvider(arg0, arg1 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateProvider", reflect.TypeOf((*MockStore)(nil).UpdateProvider), arg0, arg1) } +// UpdateReminderLastSentById mocks base method. +func (m *MockStore) UpdateReminderLastSentById(arg0 context.Context, arg1 uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateReminderLastSentById", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateReminderLastSentById indicates an expected call of UpdateReminderLastSentById. +func (mr *MockStoreMockRecorder) UpdateReminderLastSentById(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateReminderLastSentById", reflect.TypeOf((*MockStore)(nil).UpdateReminderLastSentById), arg0, arg1) +} + // UpdateRuleType mocks base method. func (m *MockStore) UpdateRuleType(arg0 context.Context, arg1 db.UpdateRuleTypeParams) (db.RuleType, error) { m.ctrl.T.Helper() diff --git a/database/query/repositories.sql b/database/query/repositories.sql index 5bb74e7532..d408898f01 100644 --- a/database/query/repositories.sql +++ b/database/query/repositories.sql @@ -16,6 +16,10 @@ INSERT INTO repositories ( provider_id ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, sqlc.arg(default_branch), sqlc.arg(license), sqlc.arg(provider_id)) RETURNING *; +-- name: GetRandomRepository :one +SELECT * FROM repositories +TABLESAMPLE SYSTEM_ROWS(1); + -- name: GetRepositoryByRepoID :one SELECT * FROM repositories WHERE repo_id = $1; @@ -45,6 +49,28 @@ WHERE project_id = $1 AND webhook_id IS NOT NULL AND (lower(provider) = lower(sqlc.narg('provider')::text) OR sqlc.narg('provider')::text IS NULL) ORDER BY repo_name; +-- name: ListEligibleRepositoriesAfterID :many +SELECT r.* FROM repositories r + INNER JOIN rule_evaluations re ON re.repository_id = r.id + INNER JOIN rule_details_eval rde ON rde.rule_eval_id = re.id +WHERE r.id > $1 +GROUP BY r.id +HAVING MIN(rde.last_updated) + sqlc.arg('min_elapsed')::interval < NOW() +ORDER BY r.id +LIMIT sqlc.narg('limit')::bigint; + +-- name: UpdateReminderLastSentById :exec +UPDATE repositories +SET reminder_last_sent = NOW() +WHERE id = $1; + +-- name: RepositoryExistsAfterID :one +SELECT EXISTS ( + SELECT 1 + FROM repositories + WHERE id > $1) +AS exists; + -- name: DeleteRepository :exec DELETE FROM repositories WHERE id = $1; diff --git a/go.mod b/go.mod index 41fd001ecf..ae1930af4a 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 github.com/hashicorp/go-version v1.6.0 github.com/itchyny/gojq v0.12.15 + github.com/jackc/pgtype v1.14.0 github.com/lib/pq v1.10.9 github.com/motemen/go-loghttp v0.0.0-20231107055348-29ae44b293f4 github.com/oapi-codegen/runtime v1.1.1 @@ -114,6 +115,7 @@ require ( github.com/gorilla/schema v1.2.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/hashicorp/go-sockaddr v1.0.5 // indirect + github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect diff --git a/go.sum b/go.sum index 20556f90b7..c3fb62f0a1 100644 --- a/go.sum +++ b/go.sum @@ -77,6 +77,7 @@ github.com/ClickHouse/ch-go v0.58.2/go.mod h1:Ap/0bEmiLa14gYjCiRkYGbXvbe8vwdrfTY github.com/ClickHouse/clickhouse-go v1.4.3 h1:iAFMa2UrQdR5bHJ2/yaSLffZkxpcOYQMCUuKeNXGdqc= github.com/ClickHouse/clickhouse-go/v2 v2.17.1 h1:ZCmAYWpu75IyEi7+Yrs/uaAjiCGY5wfW5kXo64exkX4= github.com/ClickHouse/clickhouse-go/v2 v2.17.1/go.mod h1:rkGTvFDTLqLIm0ma+13xmcCfr/08Gvs7KmFt1tgiWHQ= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o= @@ -213,6 +214,7 @@ github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUK github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= github.com/containerd/containerd v1.7.15 h1:afEHXdil9iAm03BmhjzKyXnnEBtjaLJefdU7DV0IFes= @@ -224,11 +226,13 @@ github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3 github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyberphone/json-canonicalization v0.0.0-20231011164504-785e29786b46 h1:2Dx4IHfC1yHWI12AxQDJM1QbRCDfk6M+blLzlZCXdrc= @@ -390,6 +394,7 @@ github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -553,24 +558,56 @@ github.com/itchyny/gojq v0.12.15 h1:WC1Nxbx4Ifw5U2oQWACYz32JK8G9qxNtHzrvW4KEcqI= github.com/itchyny/gojq v0.12.15/go.mod h1:uWAHCbCIla1jiNxmeT5/B5mOjSdfkCq6p8vxWg+BM10= github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= @@ -610,11 +647,13 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -640,6 +679,10 @@ github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNB github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/letsencrypt/boulder v0.0.0-20231030195133-fdaab3e21ab0 h1:DSpQ6Z1KyK3qpad3xApOgXsRhDercRf08oXl0sxJ1rM= github.com/letsencrypt/boulder v0.0.0-20231030195133-fdaab3e21ab0/go.mod h1:xOMLrRFJEoH/Uvi77TiOeoy3c6/399i3bAZia8ySkIY= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libsql/sqlite-antlr4-parser v0.0.0-20230802215326-5cb5bb604475 h1:6PfEMwfInASh9hkN83aR0j4W/eKaAZt/AURtXAXlas0= @@ -655,8 +698,13 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -809,6 +857,8 @@ github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= @@ -827,6 +877,7 @@ github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGq github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk= github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4= github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA= github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= @@ -843,6 +894,8 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/signalfx/splunk-otel-go/instrumentation/database/sql/splunksql v1.17.0 h1:DqlA0Mk2aD0xyslOe4T2wlu9NAkGpQiti3iXlHqRkk0= @@ -870,6 +923,7 @@ github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.8.3/go.mod h1:zgCeH github.com/sigstore/timestamp-authority v1.2.2 h1:X4qyutnCQqJ0apMewFyx+3t7Tws00JQ/JonBiu3QvLE= github.com/sigstore/timestamp-authority v1.2.2/go.mod h1:nEah4Eq4wpliDjlY342rXclGSO7Kb9hoRrl9tqLW13A= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0= @@ -899,6 +953,7 @@ github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AV github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -906,6 +961,7 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -990,6 +1046,7 @@ github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFi github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zitadel/oidc/v2 v2.12.0 h1:4aMTAy99/4pqNwrawEyJqhRb3yY3PtcDxnoDSryhpn4= github.com/zitadel/oidc/v2 v2.12.0/go.mod h1:LrRav74IiThHGapQgCHZOUNtnqJG0tcZKHro/91rtLw= go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= @@ -1029,6 +1086,10 @@ go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IO go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= go.step.sm/crypto v0.44.2 h1:t3p3uQ7raP2jp2ha9P6xkQF85TJZh+87xmjSLaib+jk= go.step.sm/crypto v0.44.2/go.mod h1:x1439EnFhadzhkuaGX7sz03LEMQ+jV4gRamf5LCZJQQ= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= @@ -1037,18 +1098,30 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= @@ -1104,6 +1177,7 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1154,7 +1228,9 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1162,12 +1238,14 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1205,6 +1283,7 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -1217,6 +1296,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= @@ -1237,15 +1317,19 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1254,6 +1338,7 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -1278,6 +1363,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1382,6 +1469,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= diff --git a/internal/config/reminder/config.go b/internal/config/reminder/config.go index 95e4136b5c..113d0d9ab9 100644 --- a/internal/config/reminder/config.go +++ b/internal/config/reminder/config.go @@ -16,11 +16,8 @@ package reminder import ( - "errors" - "fmt" "strings" - "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" @@ -33,25 +30,10 @@ type Config struct { RecurrenceConfig RecurrenceConfig `mapstructure:"recurrence"` EventConfig EventConfig `mapstructure:"events"` LoggingConfig LoggingConfig `mapstructure:"logging"` - CursorFile string `mapstructure:"cursor_file" default:"/tmp/reminder_cursor"` } -// Normalize normalizes the configuration -// Returns a boolean indicating if the config was modified and an error if the config is invalid -func (c *Config) Normalize(cmd *cobra.Command) (bool, error) { - err := c.validate() - if err != nil { - return c.patchConfig(cmd, err) - } - - return false, nil -} - -func (c *Config) validate() error { - if c == nil { - return errors.New("config cannot be nil") - } - +// Validate validates the configuration +func (c Config) Validate() error { err := c.RecurrenceConfig.Validate() if err != nil { return err @@ -60,21 +42,6 @@ func (c *Config) validate() error { return nil } -func (c *Config) patchConfig(cmd *cobra.Command, err error) (bool, error) { - var batchSizeErr *InvalidBatchSizeError - if errors.As(err, &batchSizeErr) { - minAllowedBatchSize := batchSizeErr.MaxPerProject * batchSizeErr.MinProjectFetchLimit - cmd.Println("⚠ WARNING: " + batchSizeErr.Error()) - cmd.Printf("Setting batch size to minimum allowed value: %d\n", minAllowedBatchSize) - - // Update the config with the minimum allowed batch size - c.RecurrenceConfig.BatchSize = minAllowedBatchSize - return true, nil - } - - return false, fmt.Errorf("invalid config: %w", err) -} - // SetViperDefaults sets the default values for the configuration to be picked up by viper func SetViperDefaults(v *viper.Viper) { v.SetEnvPrefix("reminder") @@ -84,13 +51,7 @@ func SetViperDefaults(v *viper.Viper) { // RegisterReminderFlags registers the flags for the minder cli func RegisterReminderFlags(v *viper.Viper, flags *pflag.FlagSet) error { - viperPath := "cursor_file" - if err := config.BindConfigFlag(v, flags, viperPath, "cursor-file", - v.GetString(viperPath), "DB Cursor file path for reminder", flags.String); err != nil { - return err - } - - viperPath = "logging.level" + viperPath := "logging.level" if err := config.BindConfigFlag(v, flags, viperPath, "logging-level", v.GetString(viperPath), "Logging level for reminder", flags.String); err != nil { return err diff --git a/internal/config/reminder/config_test.go b/internal/config/reminder/config_test.go index 512df7d734..4889357e55 100644 --- a/internal/config/reminder/config_test.go +++ b/internal/config/reminder/config_test.go @@ -20,7 +20,6 @@ import ( "testing" "time" - "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" "github.com/stretchr/testify/assert" @@ -34,21 +33,17 @@ func TestValidateConfig(t *testing.T) { t.Parallel() tests := []struct { - name string - config reminder.Config - normalizedConfig reminder.Config - modified bool - errMsg string + name string + config reminder.Config + errMsg string }{ { name: "ValidValues", config: reminder.Config{ RecurrenceConfig: reminder.RecurrenceConfig{ - Interval: parseTimeDuration(t, "1h"), - BatchSize: 100, - MaxPerProject: 10, - MinProjectFetchLimit: 5, - MinElapsed: parseTimeDuration(t, "1h"), + Interval: parseTimeDuration(t, "1h"), + BatchSize: 100, + MinElapsed: parseTimeDuration(t, "1h"), }, EventConfig: reminder.EventConfig{ Connection: config.DatabaseConfig{ @@ -57,37 +52,13 @@ func TestValidateConfig(t *testing.T) { }, }, }, - { - name: "InvalidBatchSize", - config: reminder.Config{ - RecurrenceConfig: reminder.RecurrenceConfig{ - Interval: parseTimeDuration(t, "1h"), - BatchSize: 10, - MaxPerProject: 10, - MinProjectFetchLimit: 5, - MinElapsed: parseTimeDuration(t, "1h"), - }, - }, - normalizedConfig: reminder.Config{ - RecurrenceConfig: reminder.RecurrenceConfig{ - Interval: parseTimeDuration(t, "1h"), - BatchSize: 50, - MaxPerProject: 10, - MinProjectFetchLimit: 5, - MinElapsed: parseTimeDuration(t, "1h"), - }, - }, - modified: true, - }, { name: "NegativeInterval", config: reminder.Config{ RecurrenceConfig: reminder.RecurrenceConfig{ - Interval: parseTimeDuration(t, "-1h"), - BatchSize: 100, - MaxPerProject: 10, - MinProjectFetchLimit: 5, - MinElapsed: parseTimeDuration(t, "1h"), + Interval: parseTimeDuration(t, "-1h"), + BatchSize: 100, + MinElapsed: parseTimeDuration(t, "1h"), }, }, errMsg: "cannot be negative", @@ -96,11 +67,9 @@ func TestValidateConfig(t *testing.T) { name: "NegativeMinElapsed", config: reminder.Config{ RecurrenceConfig: reminder.RecurrenceConfig{ - Interval: parseTimeDuration(t, "1h"), - BatchSize: 100, - MaxPerProject: 10, - MinProjectFetchLimit: 5, - MinElapsed: parseTimeDuration(t, "-1h"), + Interval: parseTimeDuration(t, "1h"), + BatchSize: 100, + MinElapsed: parseTimeDuration(t, "-1h"), }, }, errMsg: "cannot be negative", @@ -113,15 +82,11 @@ func TestValidateConfig(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - modified, err := tt.config.Normalize(&cobra.Command{}) + err := tt.config.Validate() if tt.errMsg != "" { assert.ErrorContains(t, err, tt.errMsg) } else { assert.NoError(t, err) - if tt.modified { - assert.True(t, modified) - assert.Equal(t, tt.normalizedConfig, tt.config) - } } }) } @@ -134,8 +99,6 @@ func TestReadConfig(t *testing.T) { recurrence: interval: "1m" batch_size: 100 - max_per_project: 10 - min_project_fetch_limit: 10 min_elapsed: "1h" ` @@ -152,8 +115,6 @@ recurrence: require.Equal(t, parseTimeDuration(t, "1m"), cfg.RecurrenceConfig.Interval) require.Equal(t, 100, cfg.RecurrenceConfig.BatchSize) - require.Equal(t, 10, cfg.RecurrenceConfig.MaxPerProject) - require.Equal(t, 10, cfg.RecurrenceConfig.MinProjectFetchLimit) require.Equal(t, parseTimeDuration(t, "1h"), cfg.RecurrenceConfig.MinElapsed) require.Equal(t, "info", cfg.LoggingConfig.Level) } @@ -165,8 +126,6 @@ func TestReadConfigWithCommandLineArgOverrides(t *testing.T) { recurrence: interval: "1m" batch_size: 100 - max_per_project: 10 - min_project_fetch_limit: 10 min_elapsed: "1h" logging: level: "debug" @@ -181,7 +140,7 @@ logging: require.NoError(t, reminder.RegisterReminderFlags(v, flags), "Unexpected error") - require.NoError(t, flags.Parse([]string{"--interval=1h", "--batch-size=200", "--max-per-project=20", "--min-project-fetch-limit=20", "--min-elapsed=2h"})) + require.NoError(t, flags.Parse([]string{"--interval=1h", "--batch-size=200", "--min-elapsed=2h"})) v.SetConfigType("yaml") require.NoError(t, v.ReadConfig(cfgbuf), "Unexpected error") @@ -191,8 +150,6 @@ logging: require.Equal(t, parseTimeDuration(t, "1h"), cfg.RecurrenceConfig.Interval) require.Equal(t, 200, cfg.RecurrenceConfig.BatchSize) - require.Equal(t, 20, cfg.RecurrenceConfig.MaxPerProject) - require.Equal(t, 20, cfg.RecurrenceConfig.MinProjectFetchLimit) require.Equal(t, parseTimeDuration(t, "2h"), cfg.RecurrenceConfig.MinElapsed) require.Equal(t, "debug", cfg.LoggingConfig.Level) } @@ -206,8 +163,6 @@ func TestSetViperDefaults(t *testing.T) { require.Equal(t, "reminder", v.GetEnvPrefix()) require.Equal(t, parseTimeDuration(t, "1h"), parseTimeDuration(t, v.GetString("recurrence.interval"))) require.Equal(t, 100, v.GetInt("recurrence.batch_size")) - require.Equal(t, 10, v.GetInt("recurrence.max_per_project")) - require.Equal(t, 10, v.GetInt("recurrence.min_project_fetch_limit")) require.Equal(t, parseTimeDuration(t, "1h"), parseTimeDuration(t, v.GetString("recurrence.min_elapsed"))) require.Equal(t, "reminder", v.GetString("events.sql_connection.dbname")) require.Equal(t, "reminder-event-postgres", v.GetString("events.sql_connection.dbhost")) @@ -224,8 +179,6 @@ func TestOverrideConfigByEnvVar(t *testing.T) { recurrence: interval: "1m" batch_size: 100 - max_per_project: 10 - min_project_fetch_limit: 10 min_elapsed: "1h" database: dbhost: "minder" @@ -268,8 +221,6 @@ database: require.Equal(t, parseTimeDuration(t, "1h"), cfg.RecurrenceConfig.Interval) require.Equal(t, 100, cfg.RecurrenceConfig.BatchSize) - require.Equal(t, 10, cfg.RecurrenceConfig.MaxPerProject) - require.Equal(t, 10, cfg.RecurrenceConfig.MinProjectFetchLimit) require.Equal(t, parseTimeDuration(t, "1h"), cfg.RecurrenceConfig.MinElapsed) require.Equal(t, "foobar", cfg.Database.Host) } diff --git a/internal/config/reminder/recurrence.go b/internal/config/reminder/recurrence.go index 4c9378a581..f7a6f35235 100644 --- a/internal/config/reminder/recurrence.go +++ b/internal/config/reminder/recurrence.go @@ -31,28 +31,10 @@ type RecurrenceConfig struct { // BatchSize is the number of reminders to process at once. Batch size cannot be less than // MaxPerProject * MinProjectFetchLimit. BatchSize int `mapstructure:"batch_size" default:"100"` - // MaxPerProject is the maximum number of reminders per project in a batch - MaxPerProject int `mapstructure:"max_per_project" default:"10"` - // MinProjectFetchLimit is the minimum number of projects to fetch in an iteration. Additional - // projects are fetched if there is still space in the batch. - MinProjectFetchLimit int `mapstructure:"min_project_fetch_limit" default:"10"` // MinElapsed is the minimum time after last update before sending a reminder MinElapsed time.Duration `mapstructure:"min_elapsed" default:"1h"` } -// InvalidBatchSizeError is a custom error type for the case when batch_size is less than -// max_per_project * min_project_fetch_limit -type InvalidBatchSizeError struct { - BatchSize int - MaxPerProject int - MinProjectFetchLimit int -} - -func (e *InvalidBatchSizeError) Error() string { - return fmt.Sprintf("batch_size %d cannot be less than max_per_project(%d)*min_project_fetch_limit(%d)=%d", - e.BatchSize, e.MaxPerProject, e.MinProjectFetchLimit, e.MaxPerProject*e.MinProjectFetchLimit) -} - // Validate checks that the recurrence config is valid func (r RecurrenceConfig) Validate() error { if r.MinElapsed < 0 { @@ -63,14 +45,6 @@ func (r RecurrenceConfig) Validate() error { return fmt.Errorf("interval %s cannot be negative", r.Interval) } - if r.BatchSize < r.MaxPerProject*r.MinProjectFetchLimit { - return &InvalidBatchSizeError{ - BatchSize: r.BatchSize, - MaxPerProject: r.MaxPerProject, - MinProjectFetchLimit: r.MinProjectFetchLimit, - } - } - return nil } @@ -91,22 +65,6 @@ func registerRecurrenceFlags(v *viper.Viper, flags *pflag.FlagSet) error { return err } - viperPath = "recurrence.max_per_project" - err = config.BindConfigFlag( - v, flags, viperPath, "max-per-project", v.GetInt(viperPath), - "Maximum number of reminders per project in a batch", flags.Int) - if err != nil { - return err - } - - viperPath = "recurrence.min_project_fetch_limit" - err = config.BindConfigFlag( - v, flags, viperPath, "min-project-fetch-limit", v.GetInt(viperPath), - "Minimum No. of projects to fetch in an iteration", flags.Int) - if err != nil { - return err - } - viperPath = "recurrence.min_elapsed" return config.BindConfigFlag( v, flags, viperPath, "min-elapsed", v.GetString(viperPath), diff --git a/internal/db/models.go b/internal/db/models.go index ab6bb9b2ad..aa21e0de03 100644 --- a/internal/db/models.go +++ b/internal/db/models.go @@ -581,23 +581,24 @@ type PullRequest struct { } type Repository struct { - ID uuid.UUID `json:"id"` - Provider string `json:"provider"` - ProjectID uuid.UUID `json:"project_id"` - RepoOwner string `json:"repo_owner"` - RepoName string `json:"repo_name"` - RepoID int64 `json:"repo_id"` - IsPrivate bool `json:"is_private"` - IsFork bool `json:"is_fork"` - WebhookID sql.NullInt64 `json:"webhook_id"` - WebhookUrl string `json:"webhook_url"` - DeployUrl string `json:"deploy_url"` - CloneUrl string `json:"clone_url"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DefaultBranch sql.NullString `json:"default_branch"` - License sql.NullString `json:"license"` - ProviderID uuid.UUID `json:"provider_id"` + ID uuid.UUID `json:"id"` + Provider string `json:"provider"` + ProjectID uuid.UUID `json:"project_id"` + RepoOwner string `json:"repo_owner"` + RepoName string `json:"repo_name"` + RepoID int64 `json:"repo_id"` + IsPrivate bool `json:"is_private"` + IsFork bool `json:"is_fork"` + WebhookID sql.NullInt64 `json:"webhook_id"` + WebhookUrl string `json:"webhook_url"` + DeployUrl string `json:"deploy_url"` + CloneUrl string `json:"clone_url"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DefaultBranch sql.NullString `json:"default_branch"` + License sql.NullString `json:"license"` + ProviderID uuid.UUID `json:"provider_id"` + ReminderLastSent sql.NullTime `json:"reminder_last_sent"` } type RuleDetailsAlert struct { diff --git a/internal/db/querier.go b/internal/db/querier.go index b8388fe365..2761020fa1 100644 --- a/internal/db/querier.go +++ b/internal/db/querier.go @@ -90,6 +90,7 @@ type Querier interface { GetProviderWebhooks(ctx context.Context, providerID uuid.UUID) ([]GetProviderWebhooksRow, error) GetPullRequest(ctx context.Context, arg GetPullRequestParams) (PullRequest, error) GetPullRequestByID(ctx context.Context, id uuid.UUID) (PullRequest, error) + GetRandomRepository(ctx context.Context) (Repository, error) // avoid using this, where possible use GetRepositoryByIDAndProject instead GetRepositoryByID(ctx context.Context, id uuid.UUID) (Repository, error) GetRepositoryByIDAndProject(ctx context.Context, arg GetRepositoryByIDAndProjectParams) (Repository, error) @@ -104,6 +105,7 @@ type Querier interface { GlobalListProviders(ctx context.Context) ([]Provider, error) GlobalListProvidersByClass(ctx context.Context, class ProviderClass) ([]Provider, error) ListArtifactsByRepoID(ctx context.Context, repositoryID uuid.NullUUID) ([]Artifact, error) + ListEligibleRepositoriesAfterID(ctx context.Context, arg ListEligibleRepositoriesAfterIDParams) ([]Repository, error) ListFlushCache(ctx context.Context) ([]FlushCache, error) // ListNonOrgProjects is a query that lists all non-organization projects. // projects have a boolean field is_organization that is set to true if the project is an organization. @@ -142,11 +144,13 @@ type Querier interface { // entity_execution_lock record if the lock is held by the given locked_by // value. ReleaseLock(ctx context.Context, arg ReleaseLockParams) error + RepositoryExistsAfterID(ctx context.Context, id uuid.UUID) (bool, error) SetCurrentVersion(ctx context.Context, arg SetCurrentVersionParams) error UpdateLease(ctx context.Context, arg UpdateLeaseParams) error UpdateProfile(ctx context.Context, arg UpdateProfileParams) (Profile, error) UpdateProjectMeta(ctx context.Context, arg UpdateProjectMetaParams) (Project, error) UpdateProvider(ctx context.Context, arg UpdateProviderParams) error + UpdateReminderLastSentById(ctx context.Context, id uuid.UUID) error UpdateRuleType(ctx context.Context, arg UpdateRuleTypeParams) (RuleType, error) UpsertAccessToken(ctx context.Context, arg UpsertAccessTokenParams) (ProviderAccessToken, error) UpsertArtifact(ctx context.Context, arg UpsertArtifactParams) (Artifact, error) diff --git a/internal/db/repositories.sql.go b/internal/db/repositories.sql.go index 28b392144c..347bc9a3ba 100644 --- a/internal/db/repositories.sql.go +++ b/internal/db/repositories.sql.go @@ -10,6 +10,7 @@ import ( "database/sql" "github.com/google/uuid" + "github.com/jackc/pgtype" ) const countRepositories = `-- name: CountRepositories :one @@ -39,7 +40,7 @@ INSERT INTO repositories ( default_branch, license, provider_id - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id, reminder_last_sent ` type CreateRepositoryParams struct { @@ -95,6 +96,7 @@ func (q *Queries) CreateRepository(ctx context.Context, arg CreateRepositoryPara &i.DefaultBranch, &i.License, &i.ProviderID, + &i.ReminderLastSent, ) return i, err } @@ -145,8 +147,39 @@ func (q *Queries) GetProviderWebhooks(ctx context.Context, providerID uuid.UUID) return items, nil } +const getRandomRepository = `-- name: GetRandomRepository :one +SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id, reminder_last_sent FROM repositories +TABLESAMPLE SYSTEM_ROWS(1) +` + +func (q *Queries) GetRandomRepository(ctx context.Context) (Repository, error) { + row := q.db.QueryRowContext(ctx, getRandomRepository) + var i Repository + err := row.Scan( + &i.ID, + &i.Provider, + &i.ProjectID, + &i.RepoOwner, + &i.RepoName, + &i.RepoID, + &i.IsPrivate, + &i.IsFork, + &i.WebhookID, + &i.WebhookUrl, + &i.DeployUrl, + &i.CloneUrl, + &i.CreatedAt, + &i.UpdatedAt, + &i.DefaultBranch, + &i.License, + &i.ProviderID, + &i.ReminderLastSent, + ) + return i, err +} + const getRepositoryByID = `-- name: GetRepositoryByID :one -SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id FROM repositories WHERE id = $1 +SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id, reminder_last_sent FROM repositories WHERE id = $1 ` // avoid using this, where possible use GetRepositoryByIDAndProject instead @@ -171,12 +204,13 @@ func (q *Queries) GetRepositoryByID(ctx context.Context, id uuid.UUID) (Reposito &i.DefaultBranch, &i.License, &i.ProviderID, + &i.ReminderLastSent, ) return i, err } const getRepositoryByIDAndProject = `-- name: GetRepositoryByIDAndProject :one -SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id FROM repositories WHERE id = $1 AND project_id = $2 +SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id, reminder_last_sent FROM repositories WHERE id = $1 AND project_id = $2 ` type GetRepositoryByIDAndProjectParams struct { @@ -205,12 +239,13 @@ func (q *Queries) GetRepositoryByIDAndProject(ctx context.Context, arg GetReposi &i.DefaultBranch, &i.License, &i.ProviderID, + &i.ReminderLastSent, ) return i, err } const getRepositoryByRepoID = `-- name: GetRepositoryByRepoID :one -SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id FROM repositories WHERE repo_id = $1 +SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id, reminder_last_sent FROM repositories WHERE repo_id = $1 ` func (q *Queries) GetRepositoryByRepoID(ctx context.Context, repoID int64) (Repository, error) { @@ -234,12 +269,13 @@ func (q *Queries) GetRepositoryByRepoID(ctx context.Context, repoID int64) (Repo &i.DefaultBranch, &i.License, &i.ProviderID, + &i.ReminderLastSent, ) return i, err } const getRepositoryByRepoName = `-- name: GetRepositoryByRepoName :one -SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id FROM repositories +SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id, reminder_last_sent FROM repositories WHERE repo_owner = $1 AND repo_name = $2 AND project_id = $3 AND (lower(provider) = lower($4::text) OR $4::text IS NULL) ` @@ -277,12 +313,72 @@ func (q *Queries) GetRepositoryByRepoName(ctx context.Context, arg GetRepository &i.DefaultBranch, &i.License, &i.ProviderID, + &i.ReminderLastSent, ) return i, err } +const listEligibleRepositoriesAfterID = `-- name: ListEligibleRepositoriesAfterID :many +SELECT r.id, r.provider, r.project_id, r.repo_owner, r.repo_name, r.repo_id, r.is_private, r.is_fork, r.webhook_id, r.webhook_url, r.deploy_url, r.clone_url, r.created_at, r.updated_at, r.default_branch, r.license, r.provider_id, r.reminder_last_sent FROM repositories r + INNER JOIN rule_evaluations re ON re.repository_id = r.id + INNER JOIN rule_details_eval rde ON rde.rule_eval_id = re.id +WHERE r.id > $1 +GROUP BY r.id +HAVING MIN(rde.last_updated) + $2::interval < NOW() +ORDER BY r.id +LIMIT $3::bigint +` + +type ListEligibleRepositoriesAfterIDParams struct { + ID uuid.UUID `json:"id"` + MinElapsed pgtype.Interval `json:"min_elapsed"` + Limit sql.NullInt64 `json:"limit"` +} + +func (q *Queries) ListEligibleRepositoriesAfterID(ctx context.Context, arg ListEligibleRepositoriesAfterIDParams) ([]Repository, error) { + rows, err := q.db.QueryContext(ctx, listEligibleRepositoriesAfterID, arg.ID, arg.MinElapsed, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Repository{} + for rows.Next() { + var i Repository + if err := rows.Scan( + &i.ID, + &i.Provider, + &i.ProjectID, + &i.RepoOwner, + &i.RepoName, + &i.RepoID, + &i.IsPrivate, + &i.IsFork, + &i.WebhookID, + &i.WebhookUrl, + &i.DeployUrl, + &i.CloneUrl, + &i.CreatedAt, + &i.UpdatedAt, + &i.DefaultBranch, + &i.License, + &i.ProviderID, + &i.ReminderLastSent, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listRegisteredRepositoriesByProjectIDAndProvider = `-- name: ListRegisteredRepositoriesByProjectIDAndProvider :many -SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id FROM repositories +SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id, reminder_last_sent FROM repositories WHERE project_id = $1 AND webhook_id IS NOT NULL AND (lower(provider) = lower($2::text) OR $2::text IS NULL) ORDER BY repo_name @@ -320,6 +416,7 @@ func (q *Queries) ListRegisteredRepositoriesByProjectIDAndProvider(ctx context.C &i.DefaultBranch, &i.License, &i.ProviderID, + &i.ReminderLastSent, ); err != nil { return nil, err } @@ -335,7 +432,7 @@ func (q *Queries) ListRegisteredRepositoriesByProjectIDAndProvider(ctx context.C } const listRepositoriesByProjectID = `-- name: ListRepositoriesByProjectID :many -SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id FROM repositories +SELECT id, provider, project_id, repo_owner, repo_name, repo_id, is_private, is_fork, webhook_id, webhook_url, deploy_url, clone_url, created_at, updated_at, default_branch, license, provider_id, reminder_last_sent FROM repositories WHERE project_id = $1 AND (repo_id >= $2 OR $2 IS NULL) AND lower(provider) = lower(COALESCE($3, provider)::text) @@ -382,6 +479,7 @@ func (q *Queries) ListRepositoriesByProjectID(ctx context.Context, arg ListRepos &i.DefaultBranch, &i.License, &i.ProviderID, + &i.ReminderLastSent, ); err != nil { return nil, err } @@ -395,3 +493,29 @@ func (q *Queries) ListRepositoriesByProjectID(ctx context.Context, arg ListRepos } return items, nil } + +const repositoryExistsAfterID = `-- name: RepositoryExistsAfterID :one +SELECT EXISTS ( + SELECT 1 + FROM repositories + WHERE id > $1) +AS exists +` + +func (q *Queries) RepositoryExistsAfterID(ctx context.Context, id uuid.UUID) (bool, error) { + row := q.db.QueryRowContext(ctx, repositoryExistsAfterID, id) + var exists bool + err := row.Scan(&exists) + return exists, err +} + +const updateReminderLastSentById = `-- name: UpdateReminderLastSentById :exec +UPDATE repositories +SET reminder_last_sent = NOW() +WHERE id = $1 +` + +func (q *Queries) UpdateReminderLastSentById(ctx context.Context, id uuid.UUID) error { + _, err := q.db.ExecContext(ctx, updateReminderLastSentById, id) + return err +} diff --git a/internal/reminder/reminder.go b/internal/reminder/reminder.go index 67730fe00d..a5a2781332 100644 --- a/internal/reminder/reminder.go +++ b/internal/reminder/reminder.go @@ -16,27 +16,23 @@ package reminder import ( - "bytes" "context" - "encoding/gob" + "database/sql" "errors" "fmt" - "os" "sync" "time" "github.com/ThreeDotsLabs/watermill/message" "github.com/google/uuid" + "github.com/jackc/pgtype" "github.com/rs/zerolog" + "k8s.io/apimachinery/pkg/util/sets" reminderconfig "github.com/stacklok/minder/internal/config/reminder" "github.com/stacklok/minder/internal/db" ) -func init() { - gob.Register(map[projectProviderPair]string{}) -} - // Interface is an interface over the reminder service type Interface interface { // Start starts the reminder by sending reminders at regular intervals @@ -53,8 +49,7 @@ type reminder struct { stop chan struct{} stopOnce sync.Once - projectListCursor string - repoListCursor map[projectProviderPair]string + repositoryCursor uuid.UUID ticker *time.Ticker @@ -62,27 +57,22 @@ type reminder struct { eventDBCloser driverCloser } -type projectProviderPair struct { - // Exported for gob - - ProjectId uuid.UUID - Provider string -} - // NewReminder creates a new reminder instance func NewReminder(ctx context.Context, store db.Store, config *reminderconfig.Config) (Interface, error) { - logger := zerolog.Ctx(ctx) r := &reminder{ - store: store, - cfg: config, - stop: make(chan struct{}), - repoListCursor: make(map[projectProviderPair]string), + store: store, + cfg: config, + stop: make(chan struct{}), } - err := r.restoreCursorState(ctx) + + randomRepo, err := r.store.GetRandomRepository(ctx) if err != nil { - // Non-fatal error, if we can't restore the cursor state, we'll start from scratch. - logger.Error().Err(err).Msg("error restoring cursor state") + return nil, err } + // Set to a random UUID within the database to start + r.repositoryCursor = randomRepo.ID + logger := zerolog.Ctx(ctx) + logger.Info().Msgf("initial repository cursor: %s", r.repositoryCursor) pub, cl, err := r.setupSQLPublisher(ctx) if err != nil { @@ -149,70 +139,176 @@ func (r *reminder) Stop() { }) } -// storeCursorState stores the cursor state to a file -// Not thread-safe, should be called from a single goroutine -func (r *reminder) storeCursorState(ctx context.Context) error { +func (r *reminder) sendReminders(ctx context.Context) []error { logger := zerolog.Ctx(ctx) - logger.Debug().Msg("storing cursor state") - var buf bytes.Buffer - enc := gob.NewEncoder(&buf) + // Fetch a batch of repositories + repos, err := r.getRepositoryBatch(ctx) + if err != nil { + logger.Error().Err(err).Msg("unable to fetch repositories") + return []error{err} + } + + logger.Info().Msgf("created repository batch of size: %d", len(repos)) - data := map[string]interface{}{ - "projectListCursor": r.projectListCursor, - "repoListCursor": r.repoListCursor, + // Update the reminder_last_sent for each repository to export as metrics + for _, repo := range repos { + logger.Debug().Msgf("updating reminder_last_sent for repository: %s", repo.ID) + err := r.store.UpdateReminderLastSentById(ctx, repo.ID) + if err != nil { + logger.Error().Err(err).Msgf("unable to update reminder_last_sent for repository: %s", repo.ID) + return []error{err} + } + } + + // TODO: Send the actual reminders + return nil +} + +func (r *reminder) getRepositoryBatch(ctx context.Context) ([]db.Repository, error) { + logger := zerolog.Ctx(ctx) + duration := pgtype.Interval{} + err := duration.Set(r.cfg.RecurrenceConfig.MinElapsed) + if err != nil { + return nil, err + } + + logger.Debug().Msgf("fetching repositories after cursor: %s", r.repositoryCursor) + repos, err := r.store.ListEligibleRepositoriesAfterID(ctx, db.ListEligibleRepositoriesAfterIDParams{ + ID: r.repositoryCursor, + MinElapsed: duration, + // Nope, this is not true: If we are under limit, then we are done, no need to fetch more + Limit: sql.NullInt64{ + Int64: int64(r.cfg.RecurrenceConfig.BatchSize), + Valid: true, + }, + }) + if err != nil { + return nil, err } - if err := enc.Encode(data); err != nil { - return err + + logger.Debug().Msgf("fetched %d repositories initially", len(repos)) + + intersectionPoint := -1 + var additionalRepos []db.Repository + + // Only fetch additional repositories if we are under the limit + if len(repos) < r.cfg.RecurrenceConfig.BatchSize { + additionalRepos, err = r.getAdditionalRepos(ctx, repos) + if err != nil { + return nil, err + } + + repos, intersectionPoint = r.mergeRepoBatch(repos, additionalRepos) } - return os.WriteFile(r.cfg.CursorFile, buf.Bytes(), 0600) + r.updateRepositoryCursor(ctx, repos, additionalRepos, intersectionPoint) + + return repos, nil } -// restoreCursorState restores the cursor state from a file -// Not thread-safe, should be called from a single goroutine -func (r *reminder) restoreCursorState(ctx context.Context) error { +func (r *reminder) getAdditionalRepos(ctx context.Context, alreadyFetchedRepos []db.Repository) ([]db.Repository, error) { logger := zerolog.Ctx(ctx) - logger.Debug().Msg("restoring cursor state") - if _, err := os.Stat(r.cfg.CursorFile); os.IsNotExist(err) { - return nil + if len(alreadyFetchedRepos) == 0 { + logger.Debug().Msgf("no repositories found after cursor %s, resetting cursor to zero uuid", r.repositoryCursor) + r.repositoryCursor = uuid.Nil + } else { + r.repositoryCursor = alreadyFetchedRepos[len(alreadyFetchedRepos)-1].ID + r.adjustCursorForEndOfList(ctx) } - data, err := os.ReadFile(r.cfg.CursorFile) + remaining := r.cfg.RecurrenceConfig.BatchSize - len(alreadyFetchedRepos) + duration := pgtype.Interval{} + err := duration.Set(r.cfg.RecurrenceConfig.MinElapsed) if err != nil { - return err + return nil, err } - buf := bytes.NewBuffer(data) - dec := gob.NewDecoder(buf) - - cursorData := make(map[string]interface{}) + logger.Debug().Msgf("fetching additional repositories after cursor: %s", r.repositoryCursor) + repos, err := r.store.ListEligibleRepositoriesAfterID(ctx, db.ListEligibleRepositoriesAfterIDParams{ + ID: r.repositoryCursor, + MinElapsed: duration, + Limit: sql.NullInt64{ + Int64: int64(remaining), + Valid: true, + }, + }) + if err != nil { + return nil, err + } - if err := dec.Decode(&cursorData); err != nil { - return err + if len(repos) == 0 { + logger.Debug().Msg("no additional repositories fetched") + } else { + logger.Debug().Msgf("fetched %d additional repositories", len(repos)) } - if val, ok := cursorData["projectListCursor"]; ok { - if v, ok := val.(string); ok { - r.projectListCursor = v - } else { - return fmt.Errorf("projectListCursor is not a string") + return repos, nil +} + +func (_ *reminder) mergeRepoBatch(repos []db.Repository, additionalRepos []db.Repository) ([]db.Repository, int) { + var mergedBatch []db.Repository + mergedBatch = append(mergedBatch, repos...) + reposSet := sets.New[db.Repository](repos...) + + // There may be an intersection between the two sets + // If there is an intersection, then we need to update the cursor to the last fetched + // non-common repository + intersectionPoint := -1 + for i, repo := range additionalRepos { + if reposSet.Has(repo) { + intersectionPoint = i + break } + reposSet.Insert(repo) + mergedBatch = append(mergedBatch, repo) } - if val, ok := cursorData["repoListCursor"]; ok { - if v, ok := val.(map[projectProviderPair]string); ok { - r.repoListCursor = v + return mergedBatch, intersectionPoint +} + +func (r *reminder) updateRepositoryCursor(ctx context.Context, + repos []db.Repository, + additionalRepos []db.Repository, + intersectionPoint int, +) { + if len(repos) == 0 { + // No repositories fetched + r.repositoryCursor = uuid.Nil + } else if len(additionalRepos) == 0 { + // No additional repositories fetched + r.repositoryCursor = repos[len(repos)-1].ID + } else if intersectionPoint == -1 { + // Both lists were merged without any intersection + r.repositoryCursor = additionalRepos[len(additionalRepos)-1].ID + } else { + if intersectionPoint == 0 { + // First element of additionalRepos is the intersection point + // so last element of repos is the cursor + r.repositoryCursor = repos[len(repos)-1].ID } else { - return fmt.Errorf("repoListCursor is not a map[projectProviderPair]string") + // Element before the intersection point is the cursor + r.repositoryCursor = additionalRepos[intersectionPoint-1].ID } } - return nil + r.adjustCursorForEndOfList(ctx) } -// TODO: Will be implemented in a separate PR -func (_ *reminder) sendReminders(_ context.Context) []error { - return nil +func (r *reminder) adjustCursorForEndOfList(ctx context.Context) { + logger := zerolog.Ctx(ctx) + // Check if the cursor is the last element in the db + exists, err := r.store.RepositoryExistsAfterID(ctx, r.repositoryCursor) + if err != nil { + logger.Error().Err(err).Msgf("unable to check if repository exists after cursor: %s", r.repositoryCursor) + logger.Info().Msg("resetting cursor to zero uuid") + r.repositoryCursor = uuid.Nil + return + } + + if !exists { + logger.Info().Msgf("cursor %s is at the end of the list, resetting to zero uuid", r.repositoryCursor) + r.repositoryCursor = uuid.Nil + } } diff --git a/internal/reminder/reminder_test.go b/internal/reminder/reminder_test.go index 840cbd61b9..3b2eed3d6b 100644 --- a/internal/reminder/reminder_test.go +++ b/internal/reminder/reminder_test.go @@ -16,57 +16,208 @@ package reminder import ( "context" + "database/sql" "fmt" - "path/filepath" "testing" + "time" "github.com/google/uuid" + "github.com/jackc/pgtype" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + mockdb "github.com/stacklok/minder/database/mock" reminderconfig "github.com/stacklok/minder/internal/config/reminder" + "github.com/stacklok/minder/internal/db" ) -func Test_cursorStateBackup(t *testing.T) { +func Test_getEligibleRepos(t *testing.T) { t.Parallel() - tempDirPath := t.TempDir() - cursorFilePath := filepath.Join(tempDirPath, "cursor") - repoListCursor := map[projectProviderPair]string{ + type expectedOutput struct { + repos []db.Repository + repoCursor uuid.UUID + } + + type input struct { + repos []db.Repository + batchSize int + } + + duration := pgtype.Interval{} + _ = duration.Set(time.Hour) + + tests := []struct { + name string + input input + expectedOutput expectedOutput + setup func(store *mockdb.MockStore, in input) + err string + }{ { - ProjectId: generateUUIDFromNum(t, 1), - Provider: "github", - }: "repo-cursor-1", + name: "no repos", + input: input{ + batchSize: 5, + }, + setup: func(store *mockdb.MockStore, _ input) { + store.EXPECT().ListEligibleRepositoriesAfterID(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes() + store.EXPECT().RepositoryExistsAfterID(gomock.Any(), gomock.Any()).Return(false, sql.ErrConnDone) + }, + }, { - ProjectId: generateUUIDFromNum(t, 2), - Provider: "gitlab", - }: "repo-cursor-2", - } - projectCursor := "project-cursor" + name: "error listing repos", + input: input{ + batchSize: 5, + }, + setup: func(store *mockdb.MockStore, _ input) { + store.EXPECT().ListEligibleRepositoriesAfterID(gomock.Any(), gomock.Any()).Return(nil, sql.ErrConnDone) + }, + err: sql.ErrConnDone.Error(), + }, + { + name: "single batch", + input: input{ + repos: getReposTillId(t, 2), + batchSize: 2, + }, + expectedOutput: expectedOutput{ + repos: getReposTillId(t, 2), + repoCursor: generateUUIDFromNum(t, 2), + }, + setup: func(store *mockdb.MockStore, in input) { + store.EXPECT().ListEligibleRepositoriesAfterID(gomock.Any(), gomock.Any()).Return(in.repos, nil) + store.EXPECT().RepositoryExistsAfterID(gomock.Any(), gomock.Any()).Return(true, nil) + }, + }, + { + name: "multiple batches", + input: input{ + repos: getReposTillId(t, 4), + batchSize: 4, + }, + expectedOutput: expectedOutput{ + repos: getReposTillId(t, 4), + repoCursor: generateUUIDFromNum(t, 4), + }, + setup: func(store *mockdb.MockStore, in input) { + store.EXPECT().ListEligibleRepositoriesAfterID(gomock.Any(), db.ListEligibleRepositoriesAfterIDParams{ + ID: uuid.Nil, + MinElapsed: duration, + Limit: sql.NullInt64{ + Int64: int64(4), + Valid: true, + }, + }).Return(in.repos[:2], nil) + store.EXPECT().ListEligibleRepositoriesAfterID(gomock.Any(), db.ListEligibleRepositoriesAfterIDParams{ + ID: in.repos[1].ID, + MinElapsed: duration, + Limit: sql.NullInt64{ + Int64: int64(2), + Valid: true, + }, + }).Return(in.repos[2:], nil) + store.EXPECT().RepositoryExistsAfterID(gomock.Any(), gomock.Any()).Return(true, nil).AnyTimes() + }, + }, + { + name: "multiple batches with intersection point", + input: input{ + repos: append(getReposTillId(t, 2), getReposTillId(t, 2)...), + batchSize: 4, + }, + expectedOutput: expectedOutput{ + repos: getReposTillId(t, 2), + repoCursor: uuid.Nil, + }, + setup: func(store *mockdb.MockStore, in input) { + store.EXPECT().ListEligibleRepositoriesAfterID(gomock.Any(), db.ListEligibleRepositoriesAfterIDParams{ + ID: uuid.Nil, + MinElapsed: duration, + Limit: sql.NullInt64{ + Int64: int64(4), + Valid: true, + }, + }).Return(in.repos[:2], nil) + + store.EXPECT().RepositoryExistsAfterID(gomock.Any(), in.repos[1].ID).Return(false, nil) - r := &reminder{ - cfg: &reminderconfig.Config{ - CursorFile: cursorFilePath, + store.EXPECT().ListEligibleRepositoriesAfterID(gomock.Any(), db.ListEligibleRepositoriesAfterIDParams{ + ID: uuid.Nil, + MinElapsed: duration, + Limit: sql.NullInt64{ + Int64: int64(2), + Valid: true, + }, + }).Return(in.repos[2:], nil) + + store.EXPECT().RepositoryExistsAfterID(gomock.Any(), in.repos[len(in.repos)-1].ID).Return(false, nil) + }, + }, + { + name: "multiple batches with unique repo and intersection point", + input: input{ + repos: append(getReposTillId(t, 2), + append([]db.Repository{{ID: generateUUIDFromNum(t, 3)}}, getReposTillId(t, 2)...)...), + batchSize: 5, + }, + expectedOutput: expectedOutput{ + repos: getReposTillId(t, 3), + repoCursor: uuid.Nil, + }, + setup: func(store *mockdb.MockStore, in input) { + store.EXPECT().ListEligibleRepositoriesAfterID(gomock.Any(), db.ListEligibleRepositoriesAfterIDParams{ + ID: uuid.Nil, + MinElapsed: duration, + Limit: sql.NullInt64{ + Int64: int64(5), + Valid: true, + }, + }).Return(in.repos[:2], nil) + + store.EXPECT().RepositoryExistsAfterID(gomock.Any(), in.repos[1].ID).Return(false, nil) + + store.EXPECT().ListEligibleRepositoriesAfterID(gomock.Any(), db.ListEligibleRepositoriesAfterIDParams{ + ID: uuid.Nil, + MinElapsed: duration, + Limit: sql.NullInt64{ + Int64: int64(3), + Valid: true, + }, + }).Return(in.repos[2:], nil) + + store.EXPECT().RepositoryExistsAfterID(gomock.Any(), in.repos[2].ID).Return(false, nil) + }, }, - projectListCursor: projectCursor, - repoListCursor: repoListCursor, } - ctx := context.Background() + for _, test := range tests { + test := test - err := r.storeCursorState(ctx) - require.NoError(t, err) + t.Run(test.name, func(t *testing.T) { + t.Parallel() - // Set cursors to empty values to check if they are restored - r.projectListCursor = "" - r.repoListCursor = nil + ctrl := gomock.NewController(t) + defer ctrl.Finish() + store := mockdb.NewMockStore(ctrl) + test.setup(store, test.input) + cfg := &reminderconfig.Config{ + RecurrenceConfig: reminderconfig.RecurrenceConfig{ + MinElapsed: time.Hour, + BatchSize: test.input.batchSize, + }, + } - err = r.restoreCursorState(ctx) - require.NoError(t, err) + r := createTestReminder(t, store, cfg) - require.Equal(t, projectCursor, r.projectListCursor) - require.Equal(t, len(repoListCursor), len(r.repoListCursor)) - for k, v := range repoListCursor { - require.Equal(t, v, r.repoListCursor[k]) + got, err := r.getRepositoryBatch(context.Background()) + if test.err != "" { + require.ErrorContains(t, err, test.err) + return + } + require.NoError(t, err) + require.ElementsMatch(t, got, test.expectedOutput.repos) + require.Equal(t, test.expectedOutput.repoCursor, r.repositoryCursor) + }) } } @@ -84,3 +235,23 @@ func generateUUIDFromNum(t *testing.T, num int) uuid.UUID { return u } + +func getReposTillId(t *testing.T, id int) []db.Repository { + t.Helper() + + repos := make([]db.Repository, 0, id) + for i := 1; i <= id; i++ { + repos = append(repos, db.Repository{ID: generateUUIDFromNum(t, i)}) + } + + return repos +} + +func createTestReminder(t *testing.T, store db.Store, config *reminderconfig.Config) *reminder { + t.Helper() + + return &reminder{ + store: store, + cfg: config, + } +} diff --git a/sqlc.yaml b/sqlc.yaml index c2abf32040..06543d73b7 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -24,4 +24,10 @@ packages: emit_prepared_queries: false emit_interface: true emit_exact_table_names: false - emit_empty_slices: true \ No newline at end of file + emit_empty_slices: true +overrides: + - db_type: 'pg_catalog.interval' + go_type: 'github.com/jackc/pgtype.Interval' + - db_type: 'pg_catalog.interval' + go_type: 'github.com/jackc/pgtype.NullInterval' + nullable: true \ No newline at end of file