Add bridge backend for sync
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -507,6 +507,60 @@ ORDER BY
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Connector) Update(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
encryptionKey cipher.EncryptionKey,
|
||||
) error {
|
||||
q := `
|
||||
UPDATE connectors
|
||||
SET
|
||||
settings = @settings,
|
||||
encrypted_connection = @encrypted_connection,
|
||||
updated_at = @updated_at
|
||||
WHERE
|
||||
%s
|
||||
AND id = @id
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
if c.Connection == nil {
|
||||
return fmt.Errorf("connection is nil")
|
||||
}
|
||||
|
||||
c.extractSlackSettings()
|
||||
|
||||
connection, err := json.Marshal(c.Connection)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot marshal connection: %w", err)
|
||||
}
|
||||
|
||||
encryptedConnection, err := cipher.Encrypt(connection, encryptionKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot encrypt connection: %w", err)
|
||||
}
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": c.ID,
|
||||
"settings": c.Settings,
|
||||
"encrypted_connection": encryptedConnection,
|
||||
"updated_at": c.UpdatedAt,
|
||||
}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
|
||||
_, err = conn.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot update connector: %w", err)
|
||||
}
|
||||
|
||||
c.EncryptedConnection = encryptedConnection
|
||||
c.populateSlackSettings()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Connectors) decryptConnections(encryptionKey cipher.EncryptionKey) error {
|
||||
for _, cnnctr := range *c {
|
||||
if len(cnnctr.EncryptedConnection) == 0 {
|
||||
|
||||
7
pkg/coredata/migrations/20260129T120000Z.sql
Normal file
7
pkg/coredata/migrations/20260129T120000Z.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- Add sync tracking fields to iam_scim_bridges table
|
||||
ALTER TABLE iam_scim_bridges ADD COLUMN last_synced_at TIMESTAMP WITH TIME ZONE;
|
||||
ALTER TABLE iam_scim_bridges ADD COLUMN next_sync_at TIMESTAMP WITH TIME ZONE DEFAULT NOW();
|
||||
ALTER TABLE iam_scim_bridges ADD COLUMN sync_error TEXT;
|
||||
|
||||
-- Create index for efficient polling of bridges due for sync
|
||||
CREATE INDEX idx_iam_scim_bridges_next_sync ON iam_scim_bridges (next_sync_at) WHERE state = 'ACTIVE';
|
||||
8
pkg/coredata/migrations/20260201T120000Z.sql
Normal file
8
pkg/coredata/migrations/20260201T120000Z.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- Add failure tracking columns to iam_scim_bridges table
|
||||
ALTER TABLE iam_scim_bridges ADD COLUMN consecutive_failures INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE iam_scim_bridges ADD COLUMN total_sync_count INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE iam_scim_bridges ADD COLUMN total_failure_count INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- Drop old index and create new one that covers all processable states
|
||||
DROP INDEX IF EXISTS idx_iam_scim_bridges_next_sync;
|
||||
CREATE INDEX idx_iam_scim_bridges_next_sync ON iam_scim_bridges (next_sync_at) WHERE state IN ('ACTIVE', 'FAILED', 'SYNCING');
|
||||
@@ -29,19 +29,27 @@ import (
|
||||
|
||||
type (
|
||||
SCIMBridge struct {
|
||||
ID gid.GID `db:"id"`
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
ScimConfigurationID gid.GID `db:"scim_configuration_id"`
|
||||
ConnectorID *gid.GID `db:"connector_id"`
|
||||
Type SCIMBridgeType `db:"type"`
|
||||
State SCIMBridgeState `db:"state"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
ID gid.GID `db:"id"`
|
||||
OrganizationID gid.GID `db:"organization_id"`
|
||||
ScimConfigurationID gid.GID `db:"scim_configuration_id"`
|
||||
ConnectorID *gid.GID `db:"connector_id"`
|
||||
Type SCIMBridgeType `db:"type"`
|
||||
State SCIMBridgeState `db:"state"`
|
||||
LastSyncedAt *time.Time `db:"last_synced_at"`
|
||||
NextSyncAt *time.Time `db:"next_sync_at"`
|
||||
SyncError *string `db:"sync_error"`
|
||||
ConsecutiveFailures int `db:"consecutive_failures"`
|
||||
TotalSyncCount int `db:"total_sync_count"`
|
||||
TotalFailureCount int `db:"total_failure_count"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
SCIMBridges []*SCIMBridge
|
||||
)
|
||||
|
||||
var ErrNoSCIMBridgeAvailable = errors.New("no SCIM bridge available for sync")
|
||||
|
||||
func (s *SCIMBridge) CursorKey(orderBy SCIMBridgeOrderField) page.CursorKey {
|
||||
switch orderBy {
|
||||
case SCIMBridgeOrderFieldCreatedAt:
|
||||
@@ -79,6 +87,12 @@ SELECT
|
||||
connector_id,
|
||||
type,
|
||||
state,
|
||||
last_synced_at,
|
||||
next_sync_at,
|
||||
sync_error,
|
||||
consecutive_failures,
|
||||
total_sync_count,
|
||||
total_failure_count,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -127,6 +141,12 @@ SELECT
|
||||
connector_id,
|
||||
type,
|
||||
state,
|
||||
last_synced_at,
|
||||
next_sync_at,
|
||||
sync_error,
|
||||
consecutive_failures,
|
||||
total_sync_count,
|
||||
total_failure_count,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -175,6 +195,12 @@ SELECT
|
||||
connector_id,
|
||||
type,
|
||||
state,
|
||||
last_synced_at,
|
||||
next_sync_at,
|
||||
sync_error,
|
||||
consecutive_failures,
|
||||
total_sync_count,
|
||||
total_failure_count,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
@@ -223,6 +249,12 @@ INSERT INTO iam_scim_bridges (
|
||||
connector_id,
|
||||
type,
|
||||
state,
|
||||
last_synced_at,
|
||||
next_sync_at,
|
||||
sync_error,
|
||||
consecutive_failures,
|
||||
total_sync_count,
|
||||
total_failure_count,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
@@ -233,6 +265,12 @@ INSERT INTO iam_scim_bridges (
|
||||
@connector_id,
|
||||
@type,
|
||||
@state,
|
||||
@last_synced_at,
|
||||
@next_sync_at,
|
||||
@sync_error,
|
||||
@consecutive_failures,
|
||||
@total_sync_count,
|
||||
@total_failure_count,
|
||||
@created_at,
|
||||
@updated_at
|
||||
)
|
||||
@@ -246,6 +284,12 @@ INSERT INTO iam_scim_bridges (
|
||||
"connector_id": s.ConnectorID,
|
||||
"type": s.Type,
|
||||
"state": s.State,
|
||||
"last_synced_at": s.LastSyncedAt,
|
||||
"next_sync_at": s.NextSyncAt,
|
||||
"sync_error": s.SyncError,
|
||||
"consecutive_failures": s.ConsecutiveFailures,
|
||||
"total_sync_count": s.TotalSyncCount,
|
||||
"total_failure_count": s.TotalFailureCount,
|
||||
"created_at": s.CreatedAt,
|
||||
"updated_at": s.UpdatedAt,
|
||||
}
|
||||
@@ -268,6 +312,12 @@ UPDATE iam_scim_bridges
|
||||
SET
|
||||
connector_id = @connector_id,
|
||||
state = @state,
|
||||
last_synced_at = @last_synced_at,
|
||||
next_sync_at = @next_sync_at,
|
||||
sync_error = @sync_error,
|
||||
consecutive_failures = @consecutive_failures,
|
||||
total_sync_count = @total_sync_count,
|
||||
total_failure_count = @total_failure_count,
|
||||
updated_at = @updated_at
|
||||
WHERE
|
||||
%s
|
||||
@@ -277,10 +327,16 @@ WHERE
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{
|
||||
"id": s.ID,
|
||||
"connector_id": s.ConnectorID,
|
||||
"state": s.State,
|
||||
"updated_at": s.UpdatedAt,
|
||||
"id": s.ID,
|
||||
"connector_id": s.ConnectorID,
|
||||
"state": s.State,
|
||||
"last_synced_at": s.LastSyncedAt,
|
||||
"next_sync_at": s.NextSyncAt,
|
||||
"sync_error": s.SyncError,
|
||||
"consecutive_failures": s.ConsecutiveFailures,
|
||||
"total_sync_count": s.TotalSyncCount,
|
||||
"total_failure_count": s.TotalFailureCount,
|
||||
"updated_at": s.UpdatedAt,
|
||||
}
|
||||
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
@@ -293,6 +349,63 @@ WHERE
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SCIMBridge) LoadNextForSyncSkipLocked(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
staleSyncThreshold time.Duration,
|
||||
) error {
|
||||
staleCutoff := time.Now().Add(-staleSyncThreshold)
|
||||
|
||||
q := `
|
||||
SELECT
|
||||
id,
|
||||
organization_id,
|
||||
scim_configuration_id,
|
||||
connector_id,
|
||||
type,
|
||||
state,
|
||||
last_synced_at,
|
||||
next_sync_at,
|
||||
sync_error,
|
||||
consecutive_failures,
|
||||
total_sync_count,
|
||||
total_failure_count,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM
|
||||
iam_scim_bridges
|
||||
WHERE
|
||||
(state IN (@state_active, @state_failed) AND (next_sync_at IS NULL OR next_sync_at <= NOW()))
|
||||
OR (state = @state_syncing AND updated_at < @stale_cutoff)
|
||||
ORDER BY
|
||||
next_sync_at ASC NULLS FIRST
|
||||
LIMIT 1
|
||||
FOR UPDATE SKIP LOCKED
|
||||
`
|
||||
args := pgx.StrictNamedArgs{
|
||||
"state_active": SCIMBridgeStateActive,
|
||||
"state_failed": SCIMBridgeStateFailed,
|
||||
"state_syncing": SCIMBridgeStateSyncing,
|
||||
"stale_cutoff": staleCutoff,
|
||||
}
|
||||
|
||||
rows, err := conn.Query(ctx, q, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot query iam_scim_bridges: %w", err)
|
||||
}
|
||||
|
||||
bridge, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[SCIMBridge])
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrNoSCIMBridgeAvailable
|
||||
}
|
||||
return fmt.Errorf("cannot collect scim_bridge: %w", err)
|
||||
}
|
||||
|
||||
*s = bridge
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SCIMBridge) Delete(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
|
||||
@@ -22,9 +22,11 @@ import (
|
||||
type SCIMBridgeState string
|
||||
|
||||
const (
|
||||
SCIMBridgeStatePending SCIMBridgeState = "PENDING"
|
||||
SCIMBridgeStateActive SCIMBridgeState = "ACTIVE"
|
||||
SCIMBridgeStateFailed SCIMBridgeState = "FAILED"
|
||||
SCIMBridgeStatePending SCIMBridgeState = "PENDING"
|
||||
SCIMBridgeStateActive SCIMBridgeState = "ACTIVE"
|
||||
SCIMBridgeStateSyncing SCIMBridgeState = "SYNCING"
|
||||
SCIMBridgeStateFailed SCIMBridgeState = "FAILED"
|
||||
SCIMBridgeStateDisabled SCIMBridgeState = "DISABLED"
|
||||
)
|
||||
|
||||
func (s SCIMBridgeState) String() string {
|
||||
@@ -47,8 +49,12 @@ func (s *SCIMBridgeState) Scan(value any) error {
|
||||
*s = SCIMBridgeStatePending
|
||||
case "ACTIVE":
|
||||
*s = SCIMBridgeStateActive
|
||||
case "SYNCING":
|
||||
*s = SCIMBridgeStateSyncing
|
||||
case "FAILED":
|
||||
*s = SCIMBridgeStateFailed
|
||||
case "DISABLED":
|
||||
*s = SCIMBridgeStateDisabled
|
||||
default:
|
||||
return fmt.Errorf("invalid SCIMBridgeState value: %q", str)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user