Skip to content

Commit 68507fc

Browse files
authored
fix(wfctl): honor explicit resource adoption
Honor adopt_existing for typed resource drivers while preserving built-in DNS adoption behavior.
1 parent 2ef2381 commit 68507fc

3 files changed

Lines changed: 206 additions & 5 deletions

File tree

cmd/wfctl/iac_typed_adapter.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,10 @@ func (d *typedResourceDriver) SensitiveKeys() []string {
692692
return append([]string(nil), resp.GetKeys()...)
693693
}
694694

695+
func (d *typedResourceDriver) SupportsConfigAdoption() bool {
696+
return true
697+
}
698+
695699
// Troubleshoot satisfies interfaces.Troubleshooter (optional). gRPC
696700
// Unimplemented (the legitimate negative signal when the plugin's
697701
// driver does not implement Troubleshoot) is translated to

cmd/wfctl/infra_apply.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -637,17 +637,23 @@ func adoptExistingResources(ctx context.Context, provider interfaces.IaCProvider
637637
if _, exists := currentByName[spec.Name]; exists {
638638
continue
639639
}
640-
builtinAdoptable := hasBuiltInAdoptionRef(spec.Type)
640+
explicitAdoptable := hasBuiltInAdoptionRef(spec.Type) || boolFromAny(spec.Config["adopt_existing"])
641641
driver, ok := drivers[spec.Type]
642642
if !ok {
643643
var err error
644644
driver, err = provider.ResourceDriver(spec.Type)
645645
if err != nil {
646-
if !builtinAdoptable {
646+
if !explicitAdoptable {
647647
continue
648648
}
649649
return nil, fmt.Errorf("%s/%s: resolve resource driver: %w", spec.Type, spec.Name, err)
650650
}
651+
if driver == nil {
652+
if !explicitAdoptable {
653+
continue
654+
}
655+
return nil, fmt.Errorf("%s/%s: resolve resource driver: driver returned nil", spec.Type, spec.Name)
656+
}
651657
drivers[spec.Type] = driver
652658
}
653659
ref, adoptable, err := adoptionRefForSpec(driver, spec)
@@ -700,9 +706,6 @@ func adoptionRefForSpec(driver interfaces.ResourceDriver, spec interfaces.Resour
700706
if locator, ok := driver.(interfaces.ResourceAdoptionLocator); ok {
701707
return locator.AdoptionRef(spec)
702708
}
703-
if !hasBuiltInAdoptionRef(spec.Type) {
704-
return interfaces.ResourceRef{}, false, nil
705-
}
706709
if spec.Type == "infra.dns" {
707710
ref := interfaces.ResourceRef{Name: spec.Name, Type: spec.Type}
708711
if domain, _ := spec.Config["domain"].(string); domain != "" {
@@ -712,6 +715,15 @@ func adoptionRefForSpec(driver interfaces.ResourceDriver, spec interfaces.Resour
712715
}
713716
return ref, true, nil
714717
}
718+
if boolFromAny(spec.Config["adopt_existing"]) {
719+
if spec.Name == "" {
720+
return interfaces.ResourceRef{}, false, fmt.Errorf("%s adoption requires resource name", spec.Type)
721+
}
722+
if !driverSupportsConfigAdoption(driver) {
723+
return interfaces.ResourceRef{}, false, fmt.Errorf("%s/%s: adopt_existing requires a driver that supports name-based adoption", spec.Type, spec.Name)
724+
}
725+
return interfaces.ResourceRef{Name: spec.Name, Type: spec.Type}, true, nil
726+
}
715727
return interfaces.ResourceRef{}, false, nil
716728
}
717729

@@ -724,6 +736,20 @@ func hasBuiltInAdoptionRef(resourceType string) bool {
724736
}
725737
}
726738

739+
type configAdoptionSupporter interface {
740+
SupportsConfigAdoption() bool
741+
}
742+
743+
func driverSupportsConfigAdoption(driver interfaces.ResourceDriver) bool {
744+
if supporter, ok := driver.(configAdoptionSupporter); ok {
745+
return supporter.SupportsConfigAdoption()
746+
}
747+
if supporter, ok := driver.(interfaces.UpsertSupporter); ok {
748+
return supporter.SupportsUpsert()
749+
}
750+
return false
751+
}
752+
727753
func isIaCNotFound(err error) bool {
728754
if err == nil {
729755
return false

cmd/wfctl/infra_apply_test.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,50 @@ func (d *readDriver) AdoptionRef(spec interfaces.ResourceSpec) (interfaces.Resou
127127
return interfaces.ResourceRef{Name: spec.Name, Type: spec.Type, ProviderID: providerID}, true, nil
128128
}
129129

130+
type configAdoptDriver struct {
131+
readOut *interfaces.ResourceOutput
132+
readErr error
133+
reads []interfaces.ResourceRef
134+
expectedProviderID string
135+
supportsConfigAdoption bool
136+
}
137+
138+
func (d *configAdoptDriver) Create(_ context.Context, spec interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) {
139+
return &interfaces.ResourceOutput{Name: spec.Name, Type: spec.Type}, nil
140+
}
141+
142+
func (d *configAdoptDriver) Read(_ context.Context, ref interfaces.ResourceRef) (*interfaces.ResourceOutput, error) {
143+
d.reads = append(d.reads, ref)
144+
if d.expectedProviderID != "" && ref.ProviderID != d.expectedProviderID {
145+
return nil, interfaces.ErrResourceNotFound
146+
}
147+
return d.readOut, d.readErr
148+
}
149+
150+
func (d *configAdoptDriver) Update(_ context.Context, ref interfaces.ResourceRef, spec interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) {
151+
return &interfaces.ResourceOutput{Name: spec.Name, Type: spec.Type, ProviderID: ref.ProviderID}, nil
152+
}
153+
154+
func (d *configAdoptDriver) Delete(_ context.Context, _ interfaces.ResourceRef) error { return nil }
155+
156+
func (d *configAdoptDriver) Diff(_ context.Context, _ interfaces.ResourceSpec, _ *interfaces.ResourceOutput) (*interfaces.DiffResult, error) {
157+
return &interfaces.DiffResult{NeedsUpdate: true}, nil
158+
}
159+
160+
func (d *configAdoptDriver) HealthCheck(_ context.Context, _ interfaces.ResourceRef) (*interfaces.HealthResult, error) {
161+
return nil, nil
162+
}
163+
164+
func (d *configAdoptDriver) Scale(_ context.Context, ref interfaces.ResourceRef, _ int) (*interfaces.ResourceOutput, error) {
165+
return &interfaces.ResourceOutput{Name: ref.Name, Type: ref.Type, ProviderID: ref.ProviderID}, nil
166+
}
167+
168+
func (d *configAdoptDriver) SensitiveKeys() []string { return nil }
169+
170+
func (d *configAdoptDriver) SupportsConfigAdoption() bool {
171+
return d.supportsConfigAdoption
172+
}
173+
130174
type readBackedProvider struct {
131175
applyCapture
132176
driver interfaces.ResourceDriver
@@ -955,6 +999,133 @@ func TestApplyWithProvider_AdoptsResourceThroughDriverLocator(t *testing.T) {
955999
}
9561000
}
9571001

1002+
func TestApplyWithProvider_AdoptsResourceWhenConfigAdoptExistingTrue(t *testing.T) {
1003+
spec := interfaces.ResourceSpec{
1004+
Name: "wfcompute-stg-db",
1005+
Type: "infra.database",
1006+
Config: map[string]any{
1007+
"adopt_existing": true,
1008+
"engine": "pg",
1009+
"version": "18",
1010+
},
1011+
}
1012+
driver := &configAdoptDriver{
1013+
supportsConfigAdoption: true,
1014+
readOut: &interfaces.ResourceOutput{
1015+
Name: "wfcompute-stg-db",
1016+
Type: "infra.database",
1017+
ProviderID: "db-123",
1018+
Outputs: map[string]any{
1019+
"name": "wfcompute-stg-db",
1020+
"engine": "pg",
1021+
},
1022+
},
1023+
}
1024+
provider := &readBackedProvider{driver: driver}
1025+
store := &fakeStateStore{}
1026+
1027+
if err := applyWithProviderAndStore(t.Context(), provider, "digitalocean", []interfaces.ResourceSpec{spec}, nil, store, io.Discard, "", "", nil); err != nil {
1028+
t.Fatalf("applyWithProviderAndStore: %v", err)
1029+
}
1030+
if len(driver.reads) != 1 {
1031+
t.Fatalf("driver reads = %d, want 1", len(driver.reads))
1032+
}
1033+
if driver.reads[0] != (interfaces.ResourceRef{Name: "wfcompute-stg-db", Type: "infra.database"}) {
1034+
t.Fatalf("read ref = %+v, want name/type only", driver.reads[0])
1035+
}
1036+
store.mu.Lock()
1037+
defer store.mu.Unlock()
1038+
if len(store.saved) != 1 || store.saved[0].ProviderID != "db-123" {
1039+
t.Fatalf("saved states = %+v, want adopted db-123", store.saved)
1040+
}
1041+
provider.mu.Lock()
1042+
appliedPlan := provider.appliedPlan
1043+
provider.mu.Unlock()
1044+
if appliedPlan == nil || len(appliedPlan.Actions) != 1 || appliedPlan.Actions[0].Action != "update" {
1045+
t.Fatalf("applied plan = %#v, want update after adoption", appliedPlan)
1046+
}
1047+
}
1048+
1049+
func TestApplyWithProvider_ConfigAdoptionRejectsUnsupportedDriver(t *testing.T) {
1050+
spec := interfaces.ResourceSpec{
1051+
Name: "wfcompute-stg-db",
1052+
Type: "infra.database",
1053+
Config: map[string]any{
1054+
"adopt_existing": true,
1055+
},
1056+
}
1057+
driver := &configAdoptDriver{}
1058+
provider := &readBackedProvider{driver: driver}
1059+
1060+
err := applyWithProviderAndStore(t.Context(), provider, "digitalocean", []interfaces.ResourceSpec{spec}, nil, &fakeStateStore{}, io.Discard, "", "", nil)
1061+
if err == nil {
1062+
t.Fatal("expected unsupported config adoption error")
1063+
}
1064+
if !strings.Contains(err.Error(), "adopt_existing requires a driver that supports name-based adoption") {
1065+
t.Fatalf("error = %v, want unsupported config adoption failure", err)
1066+
}
1067+
if len(driver.reads) != 0 {
1068+
t.Fatalf("driver reads = %d, want 0", len(driver.reads))
1069+
}
1070+
}
1071+
1072+
func TestApplyWithProvider_ConfigAdoptionRejectsNilDriver(t *testing.T) {
1073+
spec := interfaces.ResourceSpec{
1074+
Name: "wfcompute-stg-db",
1075+
Type: "infra.database",
1076+
Config: map[string]any{
1077+
"adopt_existing": true,
1078+
},
1079+
}
1080+
provider := &readBackedProvider{}
1081+
1082+
err := applyWithProviderAndStore(t.Context(), provider, "digitalocean", []interfaces.ResourceSpec{spec}, nil, &fakeStateStore{}, io.Discard, "", "", nil)
1083+
if err == nil {
1084+
t.Fatal("expected nil driver resolution error")
1085+
}
1086+
if !strings.Contains(err.Error(), "resolve resource driver: driver returned nil") {
1087+
t.Fatalf("error = %v, want nil driver resolution failure", err)
1088+
}
1089+
provider.mu.Lock()
1090+
defer provider.mu.Unlock()
1091+
if provider.applyCalled {
1092+
t.Fatal("Apply should not be called when explicit adoption driver resolution fails")
1093+
}
1094+
}
1095+
1096+
func TestApplyWithProvider_DNSAdoptionPreservesBuiltInRefWithAdoptExisting(t *testing.T) {
1097+
spec := interfaces.ResourceSpec{
1098+
Name: "site-dns",
1099+
Type: "infra.dns",
1100+
Config: map[string]any{
1101+
"adopt_existing": true,
1102+
"domain": "example.com",
1103+
},
1104+
}
1105+
driver := &configAdoptDriver{
1106+
expectedProviderID: "example.com",
1107+
readOut: &interfaces.ResourceOutput{
1108+
Name: "site-dns",
1109+
Type: "infra.dns",
1110+
ProviderID: "do-domain-123",
1111+
Outputs: map[string]any{
1112+
"domain": "example.com",
1113+
},
1114+
},
1115+
}
1116+
provider := &readBackedProvider{driver: driver}
1117+
1118+
if err := applyWithProviderAndStore(t.Context(), provider, "digitalocean", []interfaces.ResourceSpec{spec}, nil, &fakeStateStore{}, io.Discard, "", "", nil); err != nil {
1119+
t.Fatalf("applyWithProviderAndStore: %v", err)
1120+
}
1121+
if len(driver.reads) != 1 {
1122+
t.Fatalf("driver reads = %d, want 1", len(driver.reads))
1123+
}
1124+
if driver.reads[0] != (interfaces.ResourceRef{Name: "site-dns", Type: "infra.dns", ProviderID: "example.com"}) {
1125+
t.Fatalf("read ref = %+v, want built-in DNS domain ref", driver.reads[0])
1126+
}
1127+
}
1128+
9581129
func TestApplyWithProvider_SkipsAdoptionWhenAppDriverHasNoLocator(t *testing.T) {
9591130
spec := interfaces.ResourceSpec{
9601131
Name: "site-app",

0 commit comments

Comments
 (0)