-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathiac_resource_driver.go
More file actions
263 lines (246 loc) · 12.2 KB
/
iac_resource_driver.go
File metadata and controls
263 lines (246 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
package interfaces
import (
"context"
"errors"
"strings"
"time"
)
// Sentinel errors for common IaC resource operation response categories.
// Use errors.Is to identify them after wrapping.
var (
ErrResourceNotFound = errors.New("iac: resource not found") // 404/410
ErrResourceAlreadyExists = errors.New("iac: resource already exists") // 409 Conflict
ErrRateLimited = errors.New("iac: rate limited") // 429
ErrTransient = errors.New("iac: transient error") // 502/503/504
ErrUnauthorized = errors.New("iac: unauthorized") // 401
ErrForbidden = errors.New("iac: forbidden") // 403
ErrValidation = errors.New("iac: validation error") // 400/422
// ErrImageNotInRegistry indicates that an image tag or digest referenced
// by a desired ResourceSpec is not present in the registry. Drivers
// SHOULD return this (wrapped) from Diff, Create, and Update when an
// image-presence pre-flight fails. Callers can use errors.Is for typed
// identification, but gRPC-bound callers should ALSO match the message
// string "iac: image tag or digest not found in registry" for
// cross-process robustness — structpb does not preserve sentinel
// identity across the gRPC plugin boundary. The message string is
// load-bearing; do not change it.
ErrImageNotInRegistry = errors.New("iac: image tag or digest not found in registry")
// ErrProviderMethodUnimplemented indicates that a remote IaC provider
// plugin does not implement the optional method that was just dispatched.
// Used by the wfctl gRPC proxy *remoteIaCProvider to translate
// gRPC codes.Unimplemented (and equivalent string-matched plugin errors)
// into a stable sentinel that dispatch sites can errors.Is on.
//
// Why this exists
// ───────────────
// v0.27.0 added optional sub-interfaces (EnumeratorAll, Enumerator, etc.)
// to interfaces.IaCProvider. Dispatch sites (infra_audit_keys.go,
// infra_cleanup.go, infra_prune.go) iterate providers and use a
// type-assertion `p.(interfaces.X)` as the "does this provider support X?"
// gate. v0.27.1 bridged these methods on remoteIaCProvider so audit-keys
// could reach plugins that DO implement them, but as a side effect every
// gRPC-loaded provider now satisfies the optional interface — even ones
// whose plugin process does not implement the underlying method.
//
// To preserve the iterate-and-skip semantics, dispatch sites now call the
// method and check for ErrProviderMethodUnimplemented via errors.Is. A
// match is treated identically to the pre-v0.27.1 negative type-assert:
// log "skipped <provider>: does not implement <Interface>" and continue
// iterating to the next provider.
//
// Plugins SHOULD return status.Error(codes.Unimplemented, "...") from
// their InvokeMethod / InvokeMethodContext dispatcher when an optional
// method is not supported. The proxy translates this to
// ErrProviderMethodUnimplemented for callers.
ErrProviderMethodUnimplemented = errors.New("iac: provider method unimplemented")
)
// IsErrResourceNotFound reports whether err is or wraps ErrResourceNotFound,
// including the case where the sentinel was stringified across a gRPC plugin
// boundary (where errors.Is fails because structpb does not preserve sentinel
// identity). Matches the ErrImageNotInRegistry precedent above. The message
// string of ErrResourceNotFound is load-bearing for this match; do not change
// it without updating TestErrResourceNotFound_MessageStringStable.
//
// For non-gRPC callers (e.g., SQL-backed tenant lookups in module/),
// errors.Is fires and strings.Contains is never reached.
func IsErrResourceNotFound(err error) bool {
if err == nil {
return false
}
if errors.Is(err, ErrResourceNotFound) {
return true
}
return strings.Contains(err.Error(), ErrResourceNotFound.Error())
}
// ResourceDriver handles CRUD for a single resource type within a provider.
type ResourceDriver interface {
Create(ctx context.Context, spec ResourceSpec) (*ResourceOutput, error)
Read(ctx context.Context, ref ResourceRef) (*ResourceOutput, error)
Update(ctx context.Context, ref ResourceRef, spec ResourceSpec) (*ResourceOutput, error)
Delete(ctx context.Context, ref ResourceRef) error
Diff(ctx context.Context, desired ResourceSpec, current *ResourceOutput) (*DiffResult, error)
HealthCheck(ctx context.Context, ref ResourceRef) (*HealthResult, error)
Scale(ctx context.Context, ref ResourceRef, replicas int) (*ResourceOutput, error)
// SensitiveKeys returns output keys whose values should be masked in logs and plan output.
SensitiveKeys() []string
}
// ResourceAdoptionLocator is an optional interface ResourceDriver
// implementations may provide when a desired ResourceSpec can be resolved to a
// live provider resource before local state exists. wfctl infra apply uses this
// to adopt existing resources into state and then computes an update/delete
// plan from real current state instead of blindly creating duplicates.
type ResourceAdoptionLocator interface {
AdoptionRef(spec ResourceSpec) (ResourceRef, bool, error)
}
// UpsertSupporter is an optional interface implemented by ResourceDrivers
// that support locating an existing resource by name alone (empty
// ProviderID) in their Read method. wfctlhelpers.ApplyPlan uses this hook
// to recover from Create that returns ErrResourceAlreadyExists: when the
// driver opts in via SupportsUpsert()==true, ApplyPlan calls Read with a
// name-only ResourceRef to obtain the existing ProviderID, then calls
// Update to bring the resource to the desired state — net effect of an
// idempotent upsert without requiring drivers to implement upsert
// natively.
//
// Drivers that do not implement this interface (or return false) yield
// the original ErrResourceAlreadyExists unchanged — ApplyPlan does NOT
// silently swallow the conflict. Implementations should return true only
// when their Read can locate a resource by Name + Type without a
// ProviderID; returning true while requiring a non-empty ProviderID in
// Read defeats the recovery path.
type UpsertSupporter interface {
SupportsUpsert() bool
}
// ResourceReplacer is an optional interface ResourceDriver
// implementations may provide when a resource's Replace transition
// needs more than naive Delete-then-Create — typically because the
// resource owns single-attach dependents (e.g., a DO Droplet with
// Block Storage Volumes, an AWS EC2 instance with EBS Volumes, a GCP
// VM with persistent disks) that the cloud refuses to associate with
// a new parent until the old parent releases them.
//
// wfctlhelpers.doReplace probes for this interface; on opt-in, the
// driver receives the OLD ref and the NEW spec and is responsible for
// the full transition. On non-opt-in, doReplace calls
// wfctlhelpers.DefaultReplace (the existing Delete-then-Create logic,
// exported for direct dispatch).
//
// Drivers SHOULD NOT implement this interface unless the resource has
// orchestration needs the engine cannot satisfy generically — naive
// Delete-then-Create is correct for the majority of cloud resources
// and is the default for a reason (atomicity, error attribution).
//
// A driver that opts in but wants engine-default behavior for a
// particular spec calls wfctlhelpers.DefaultReplace directly. The
// engine never inspects the returned error to decide between paths,
// so there is no sentinel-error round-trip.
//
// Error attribution: drivers MUST wrap their sub-step errors with a
// recognizable prefix (e.g., "<resource-type> replace %q: detach
// volume %q: %w"). Non-conforming returns are wrapped by the engine
// with "replace: driver: " at the dispatch boundary so operator
// attribution is preserved at runtime regardless of per-plugin
// discipline.
type ResourceReplacer interface {
Replace(ctx context.Context, oldRef ResourceRef, spec ResourceSpec) (*ResourceOutput, error)
}
// ResourceOutput is the concrete output of a provisioned or read resource.
type ResourceOutput struct {
Name string `json:"name"`
Type string `json:"type"`
ProviderID string `json:"provider_id"`
Outputs map[string]any `json:"outputs"` // IPs, endpoints, connection strings
Sensitive map[string]bool `json:"sensitive,omitempty"` // keys whose values are sensitive
Status string `json:"status"`
}
// DiffResult summarises the differences between desired and actual resource state.
type DiffResult struct {
NeedsUpdate bool `json:"needs_update"`
NeedsReplace bool `json:"needs_replace"`
Changes []FieldChange `json:"changes"`
}
// FieldChange describes a single field-level difference.
type FieldChange struct {
Path string `json:"path"`
Old any `json:"old"`
New any `json:"new"`
ForceNew bool `json:"force_new"` // change requires resource replacement
}
// HealthResult is the outcome of a health check for a resource.
type HealthResult struct {
Healthy bool `json:"healthy"`
Message string `json:"message,omitempty"`
}
// Diagnostic is a single troubleshooting finding returned by a Troubleshooter.
// It describes a recent provider-side event (deployment, job run, etc.) with
// enough context to understand why a health check failed without visiting the
// provider's console.
type Diagnostic struct {
ID string `json:"id"` // provider-side identifier (e.g. deployment ID)
Phase string `json:"phase"` // terminal or current phase
Cause string `json:"cause"` // human-readable root cause or error summary
At time.Time `json:"at"` // when the event was created or last updated
Detail string `json:"detail,omitempty"` // optional verbose tail (log excerpt, stack)
}
// Troubleshooter is an optional interface that ResourceDrivers may implement.
// wfctl calls Troubleshoot automatically when a health-check poll times out or
// a deploy operation returns a generic error, surfacing provider-side context
// that would otherwise require visiting the provider's web console.
//
// Implementations should return the N most recent relevant events (deployments,
// job runs, etc.) in reverse-chronological order. Returning an error is
// non-fatal — wfctl logs it and continues with the original failure message.
type Troubleshooter interface {
Troubleshoot(ctx context.Context, ref ResourceRef, failureMsg string) ([]Diagnostic, error)
}
// ProviderIDFormat identifies the shape of provider-specific resource
// identifiers so wfctl can validate them at the driver boundary without
// knowing provider-specific semantics.
//
// The zero value IDFormatUnknown disables validation for backward
// compatibility — drivers that don't opt in get today's behavior.
type ProviderIDFormat int
const (
// IDFormatUnknown disables validation (zero value).
IDFormatUnknown ProviderIDFormat = iota
// IDFormatUUID is the canonical 36-character hyphenated UUID shape.
IDFormatUUID
// IDFormatDomainName is an RFC 1035 domain name.
IDFormatDomainName
// IDFormatARN is an AWS-style colon-separated ARN.
IDFormatARN
// IDFormatFreeform allows any non-empty string.
IDFormatFreeform
)
// String returns a stable identifier for logs and error messages.
func (f ProviderIDFormat) String() string {
switch f {
case IDFormatUUID:
return "uuid"
case IDFormatDomainName:
return "domain_name"
case IDFormatARN:
return "arn"
case IDFormatFreeform:
return "freeform"
default:
return "unknown"
}
}
// ProviderIDValidator is an optional interface ResourceDriver implementations
// may provide to declare the shape of their ProviderIDs. wfctl uses the
// declaration to validate ProviderIDs at two boundaries:
//
// - Input: before Update/Delete, probe ref.ProviderID against the declared
// format. On mismatch, wfctl logs a warning but still calls the driver so
// its own heal logic (if any) can run.
// - Output: after Apply, probe r.ProviderID before persisting to state.
// Mismatch for non-Unknown formats is a HARD failure — the driver has a
// bug and state must not be corrupted. Freeform accepts any non-empty
// ProviderID; Unknown disables output validation.
//
// Drivers that do not implement this interface receive today's behavior:
// no validation, no warning, no failure.
type ProviderIDValidator interface {
ProviderIDFormat() ProviderIDFormat
}