Skip to content

Commit 4781676

Browse files
committed
Merge branch 'rds-iam-instance-identifier-projection' into 'master'
Add RDS IAM instance identifier to config projection See merge request postgres-ai/database-lab!1106
2 parents 4d74dee + 762cda9 commit 4781676

5 files changed

Lines changed: 186 additions & 20 deletions

File tree

engine/internal/rdsrefresh/dblab.go

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"gitlab.com/postgres-ai/database-lab/v3/pkg/client/dblabapi"
1717
"gitlab.com/postgres-ai/database-lab/v3/pkg/log"
1818
"gitlab.com/postgres-ai/database-lab/v3/pkg/models"
19+
"gitlab.com/postgres-ai/database-lab/v3/pkg/util/projection"
1920
)
2021

2122
const (
@@ -170,19 +171,44 @@ func (c *DBLabClient) WaitForRefreshComplete(ctx context.Context, pollInterval,
170171
}
171172
}
172173

174+
// SourceConfigUpdate contains source database connection parameters for config update.
175+
type SourceConfigUpdate struct {
176+
Host string
177+
Port int
178+
DBName string
179+
Username string
180+
Password string
181+
// RDSIAMDBInstance is the RDS DB instance identifier for IAM auth. When empty, this field is omitted from the config update.
182+
RDSIAMDBInstance string
183+
}
184+
173185
// UpdateSourceConfig updates the source database connection in DBLab config.
174186
// DBLab automatically reloads the configuration after the update.
175-
func (c *DBLabClient) UpdateSourceConfig(ctx context.Context, host string, port int, dbname, username, password string) error {
176-
port64 := int64(port)
177-
updateReq := models.ConfigProjection{
178-
Host: &host,
187+
func (c *DBLabClient) UpdateSourceConfig(ctx context.Context, update SourceConfigUpdate) error {
188+
port64 := int64(update.Port)
189+
proj := models.ConfigProjection{
190+
Host: &update.Host,
179191
Port: &port64,
180-
DBName: &dbname,
181-
Username: &username,
182-
Password: &password,
192+
DBName: &update.DBName,
193+
Username: &update.Username,
194+
Password: &update.Password,
195+
}
196+
197+
if update.RDSIAMDBInstance != "" {
198+
proj.RDSIAMDBInstance = &update.RDSIAMDBInstance
199+
}
200+
201+
nested := map[string]interface{}{}
202+
203+
// defensive error check: StoreJSON only fails if target is not an addressable struct,
204+
// which cannot happen here since proj is always a valid ConfigProjection value.
205+
if err := projection.StoreJSON(&proj, nested, projection.StoreOptions{
206+
Groups: []string{"default", "sensitive"},
207+
}); err != nil {
208+
return fmt.Errorf("failed to build config projection: %w", err)
183209
}
184210

185-
bodyBytes, err := json.Marshal(updateReq)
211+
bodyBytes, err := json.Marshal(nested)
186212
if err != nil {
187213
return fmt.Errorf("failed to marshal config update: %w", err)
188214
}

engine/internal/rdsrefresh/dblab_test.go

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/stretchr/testify/require"
1717

1818
"gitlab.com/postgres-ai/database-lab/v3/pkg/models"
19+
"gitlab.com/postgres-ai/database-lab/v3/pkg/util/projection"
1920
)
2021

2122
func TestDBLabClientHealth(t *testing.T) {
@@ -130,7 +131,13 @@ func TestDBLabClientUpdateSourceConfig(t *testing.T) {
130131
assert.Equal(t, http.MethodPost, r.Method)
131132
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
132133

133-
err := json.NewDecoder(r.Body).Decode(&receivedConfig)
134+
var nested map[string]interface{}
135+
err := json.NewDecoder(r.Body).Decode(&nested)
136+
require.NoError(t, err)
137+
138+
err = projection.LoadJSON(&receivedConfig, nested, projection.LoadOptions{
139+
Groups: []string{"default", "sensitive"},
140+
})
134141
require.NoError(t, err)
135142

136143
w.WriteHeader(http.StatusOK)
@@ -140,14 +147,48 @@ func TestDBLabClientUpdateSourceConfig(t *testing.T) {
140147
client, err := NewDBLabClient(&DBLabConfig{APIEndpoint: server.URL, Token: "test-token"})
141148
require.NoError(t, err)
142149

143-
err = client.UpdateSourceConfig(context.Background(), "clone-host.rds.amazonaws.com", 5432, "postgres", "dbuser", "dbpass")
150+
err = client.UpdateSourceConfig(context.Background(), SourceConfigUpdate{
151+
Host: "clone-host.rds.amazonaws.com", Port: 5432, DBName: "postgres",
152+
Username: "dbuser", Password: "dbpass", RDSIAMDBInstance: "my-rds-clone",
153+
})
144154
require.NoError(t, err)
145155

146156
assert.Equal(t, "clone-host.rds.amazonaws.com", *receivedConfig.Host)
147157
assert.Equal(t, int64(5432), *receivedConfig.Port)
148158
assert.Equal(t, "postgres", *receivedConfig.DBName)
149159
assert.Equal(t, "dbuser", *receivedConfig.Username)
150160
assert.Equal(t, "dbpass", *receivedConfig.Password)
161+
assert.Equal(t, "my-rds-clone", *receivedConfig.RDSIAMDBInstance)
162+
})
163+
164+
t.Run("successful without rds iam instance", func(t *testing.T) {
165+
var receivedConfig models.ConfigProjection
166+
167+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
168+
var nested map[string]interface{}
169+
err := json.NewDecoder(r.Body).Decode(&nested)
170+
require.NoError(t, err)
171+
172+
err = projection.LoadJSON(&receivedConfig, nested, projection.LoadOptions{
173+
Groups: []string{"default", "sensitive"},
174+
})
175+
require.NoError(t, err)
176+
177+
w.WriteHeader(http.StatusOK)
178+
}))
179+
defer server.Close()
180+
181+
client, err := NewDBLabClient(&DBLabConfig{APIEndpoint: server.URL, Token: "test-token"})
182+
require.NoError(t, err)
183+
184+
err = client.UpdateSourceConfig(context.Background(), SourceConfigUpdate{
185+
Host: "host.rds.amazonaws.com", Port: 5432, DBName: "postgres",
186+
Username: "dbuser", Password: "dbpass",
187+
})
188+
require.NoError(t, err)
189+
190+
assert.Equal(t, "host.rds.amazonaws.com", *receivedConfig.Host)
191+
assert.Nil(t, receivedConfig.RDSIAMDBInstance)
151192
})
152193

153194
t.Run("error on non-2xx status", func(t *testing.T) {
@@ -160,7 +201,9 @@ func TestDBLabClientUpdateSourceConfig(t *testing.T) {
160201
client, err := NewDBLabClient(&DBLabConfig{APIEndpoint: server.URL, Token: "test-token"})
161202
require.NoError(t, err)
162203

163-
err = client.UpdateSourceConfig(context.Background(), "host", 5432, "db", "user", "pass")
204+
err = client.UpdateSourceConfig(context.Background(), SourceConfigUpdate{
205+
Host: "host", Port: 5432, DBName: "db", Username: "user", Password: "pass",
206+
})
164207
require.Error(t, err)
165208
assert.Contains(t, err.Error(), "invalid configuration")
166209
})
@@ -175,7 +218,9 @@ func TestDBLabClientUpdateSourceConfig(t *testing.T) {
175218
client, err := NewDBLabClient(&DBLabConfig{APIEndpoint: server.URL, Token: "test-token"})
176219
require.NoError(t, err)
177220

178-
err = client.UpdateSourceConfig(context.Background(), "host", 5432, "db", "user", "pass")
221+
err = client.UpdateSourceConfig(context.Background(), SourceConfigUpdate{
222+
Host: "host", Port: 5432, DBName: "db", Username: "user", Password: "pass",
223+
})
179224
require.Error(t, err)
180225
assert.Contains(t, err.Error(), "internal server error")
181226
})

engine/internal/rdsrefresh/refresher.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,14 +180,14 @@ func (r *Refresher) Run(ctx context.Context) *RefreshResult {
180180
// step 6: update DBLab config with RDS clone endpoint
181181
log.Msg("updating DBLab config...")
182182

183-
if err := r.dblab.UpdateSourceConfig(
184-
ctx,
185-
clone.Endpoint,
186-
int(clone.Port),
187-
r.cfg.Source.DBName,
188-
r.cfg.Source.Username,
189-
r.cfg.Source.Password,
190-
); err != nil {
183+
if err := r.dblab.UpdateSourceConfig(ctx, SourceConfigUpdate{
184+
Host: clone.Endpoint,
185+
Port: int(clone.Port),
186+
DBName: r.cfg.Source.DBName,
187+
Username: r.cfg.Source.Username,
188+
Password: r.cfg.Source.Password,
189+
RDSIAMDBInstance: clone.Identifier,
190+
}); err != nil {
191191
result.Error = fmt.Errorf("failed to update DBLab config: %w", err)
192192
return result
193193
}

engine/internal/rdsrefresh/refresher_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@ import (
1212
"testing"
1313
"time"
1414

15+
"github.com/aws/aws-sdk-go-v2/aws"
16+
awsrds "github.com/aws/aws-sdk-go-v2/service/rds"
17+
"github.com/aws/aws-sdk-go-v2/service/rds/types"
1518
"github.com/stretchr/testify/assert"
1619
"github.com/stretchr/testify/require"
1720

1821
"gitlab.com/postgres-ai/database-lab/v3/pkg/models"
22+
"gitlab.com/postgres-ai/database-lab/v3/pkg/util/projection"
1923
)
2024

2125
func TestRefreshResult(t *testing.T) {
@@ -205,3 +209,93 @@ func TestRefreshWorkflow(t *testing.T) {
205209
assert.LessOrEqual(t, result.Duration, elapsed+100*time.Millisecond)
206210
})
207211
}
212+
213+
func TestRefreshWorkflowPassesCloneIdentifier(t *testing.T) {
214+
t.Run("passes clone identifier as rds iam db instance", func(t *testing.T) {
215+
var receivedConfig models.ConfigProjection
216+
217+
statusCallCount := 0
218+
refreshTriggered := false
219+
220+
dblabServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
221+
switch r.URL.Path {
222+
case "/healthz":
223+
w.WriteHeader(http.StatusOK)
224+
_, _ = w.Write([]byte(`{"status":"ok"}`))
225+
case "/status":
226+
statusCallCount++
227+
228+
var status models.RetrievalStatus
229+
230+
switch {
231+
case !refreshTriggered || statusCallCount <= 2:
232+
status = models.Finished
233+
case statusCallCount == 3:
234+
status = models.Refreshing
235+
default:
236+
status = models.Finished
237+
}
238+
239+
resp := &models.InstanceStatus{Retrieving: models.Retrieving{Status: status}}
240+
_ = json.NewEncoder(w).Encode(resp)
241+
case "/admin/config":
242+
var nested map[string]interface{}
243+
require.NoError(t, json.NewDecoder(r.Body).Decode(&nested))
244+
require.NoError(t, projection.LoadJSON(&receivedConfig, nested, projection.LoadOptions{Groups: []string{"default", "sensitive"}}))
245+
w.WriteHeader(http.StatusOK)
246+
case "/full-refresh":
247+
refreshTriggered = true
248+
statusCallCount = 0
249+
_, _ = w.Write([]byte(`{"status":"OK"}`))
250+
default:
251+
w.WriteHeader(http.StatusNotFound)
252+
}
253+
}))
254+
defer dblabServer.Close()
255+
256+
mock := &mockRDSAPI{
257+
describeDBInstancesFunc: func(ctx context.Context, params *awsrds.DescribeDBInstancesInput, optFns ...func(*awsrds.Options)) (*awsrds.DescribeDBInstancesOutput, error) {
258+
return &awsrds.DescribeDBInstancesOutput{
259+
DBInstances: []types.DBInstance{{
260+
DBInstanceIdentifier: aws.String(aws.ToString(params.DBInstanceIdentifier)),
261+
DBInstanceStatus: aws.String("available"),
262+
Engine: aws.String("postgres"),
263+
EngineVersion: aws.String("15.4"),
264+
Endpoint: &types.Endpoint{Address: aws.String("clone.rds.amazonaws.com"), Port: aws.Int32(5432)},
265+
}},
266+
}, nil
267+
},
268+
restoreDBInstanceFunc: func(ctx context.Context, params *awsrds.RestoreDBInstanceFromDBSnapshotInput, optFns ...func(*awsrds.Options)) (*awsrds.RestoreDBInstanceFromDBSnapshotOutput, error) {
269+
return &awsrds.RestoreDBInstanceFromDBSnapshotOutput{}, nil
270+
},
271+
modifyDBInstanceFunc: func(ctx context.Context, params *awsrds.ModifyDBInstanceInput, optFns ...func(*awsrds.Options)) (*awsrds.ModifyDBInstanceOutput, error) {
272+
return &awsrds.ModifyDBInstanceOutput{}, nil
273+
},
274+
deleteDBInstanceFunc: func(ctx context.Context, params *awsrds.DeleteDBInstanceInput, optFns ...func(*awsrds.Options)) (*awsrds.DeleteDBInstanceOutput, error) {
275+
return &awsrds.DeleteDBInstanceOutput{}, nil
276+
},
277+
}
278+
279+
cfg := &Config{
280+
Source: SourceConfig{Type: "rds", Identifier: "test-db", DBName: "postgres", Username: "dbuser", Password: "dbpass", SnapshotIdentifier: "snap-123"},
281+
RDSClone: RDSCloneConfig{InstanceClass: "db.t3.medium"},
282+
DBLab: DBLabConfig{APIEndpoint: dblabServer.URL, Token: "test-token", PollInterval: Duration(100 * time.Millisecond), Timeout: Duration(5 * time.Second)},
283+
}
284+
285+
rdsClient := NewRDSClientWithAPI(mock, cfg)
286+
dblabClient, err := NewDBLabClient(&cfg.DBLab)
287+
require.NoError(t, err)
288+
289+
refresher := &Refresher{cfg: cfg, rds: rdsClient, dblab: dblabClient}
290+
result := refresher.Run(context.Background())
291+
292+
require.True(t, result.Success, "expected success but got error: %v", result.Error)
293+
require.NotNil(t, receivedConfig.RDSIAMDBInstance, "RDSIAMDBInstance should be set in config update")
294+
assert.Contains(t, *receivedConfig.RDSIAMDBInstance, cloneNamePrefix)
295+
assert.Equal(t, "clone.rds.amazonaws.com", *receivedConfig.Host)
296+
assert.Equal(t, int64(5432), *receivedConfig.Port)
297+
assert.Equal(t, "postgres", *receivedConfig.DBName)
298+
assert.Equal(t, "dbuser", *receivedConfig.Username)
299+
assert.Equal(t, "dbpass", *receivedConfig.Password)
300+
})
301+
}

engine/pkg/models/configuration.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ type ConfigProjection struct {
2828
RestoreCustomOptions []interface{} `proj:"retrieval.spec.logicalRestore.options.customOptions"`
2929
IgnoreDumpErrors *bool `proj:"retrieval.spec.logicalDump.options.ignoreErrors"`
3030
IgnoreRestoreErrors *bool `proj:"retrieval.spec.logicalRestore.options.ignoreErrors"`
31+
RDSIAMDBInstance *string `proj:"retrieval.spec.logicalDump.options.source.rdsIam.dbInstanceIdentifier"`
3132
}

0 commit comments

Comments
 (0)