feat: track resource replacements when claiming a prebuilt workspace (#17571)

Closes https://github.com/coder/internal/issues/369

We can't know whether a replacement (i.e. drift of terraform state
leading to a resource needing to be deleted/recreated) will take place
apriori; we can only detect it at `plan` time, because the provider
decides whether a resource must be replaced and it cannot be inferred
through static analysis of the template.

**This is likely to be the most common gotcha with using prebuilds,
since it requires a slight template modification to use prebuilds
effectively**, so let's head this off before it's an issue for
customers.

Drift details will now be logged in the workspace build logs:


![image](https://github.com/user-attachments/assets/da1988b6-2cbe-4a79-a3c5-ea29891f3d6f)

Plus a notification will be sent to template admins when this situation
arises:


![image](https://github.com/user-attachments/assets/39d555b1-a262-4a3e-b529-03b9f23bf66a)

A new metric - `coderd_prebuilt_workspaces_resource_replacements_total`
- will also increment each time a workspace encounters replacements.

We only track _that_ a resource replacement occurred, not how many. Just
one is enough to ruin a prebuild, but we can't know apriori which
replacement would cause this.
For example, say we have 2 replacements: a `docker_container` and a
`null_resource`; we don't know which one might
cause an issue (or indeed if either would), so we just track the
replacement.

---------

Signed-off-by: Danny Kopping <dannykopping@gmail.com>
This commit is contained in:
Danny Kopping
2025-05-14 14:52:22 +02:00
committed by GitHub
parent e75d1c1ce5
commit 6e967780c9
33 changed files with 2048 additions and 969 deletions
+2
View File
@@ -581,6 +581,7 @@ const createTemplateVersionTar = async (
externalAuthProviders: response.apply?.externalAuthProviders ?? [],
timings: response.apply?.timings ?? [],
presets: [],
resourceReplacements: [],
plan: emptyPlan,
moduleFiles: new Uint8Array(),
},
@@ -705,6 +706,7 @@ const createTemplateVersionTar = async (
timings: [],
modules: [],
presets: [],
resourceReplacements: [],
plan: emptyPlan,
moduleFiles: new Uint8Array(),
...response.plan,
+21
View File
@@ -120,6 +120,11 @@ export interface PresetParameter {
value: string;
}
export interface ResourceReplacement {
resource: string;
paths: string[];
}
/** VariableValue holds the key/value mapping of a Terraform variable. */
export interface VariableValue {
name: string;
@@ -374,6 +379,7 @@ export interface PlanComplete {
presets: Preset[];
plan: Uint8Array;
moduleFiles: Uint8Array;
resourceReplacements: ResourceReplacement[];
}
/**
@@ -573,6 +579,18 @@ export const PresetParameter = {
},
};
export const ResourceReplacement = {
encode(message: ResourceReplacement, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.resource !== "") {
writer.uint32(10).string(message.resource);
}
for (const v of message.paths) {
writer.uint32(18).string(v!);
}
return writer;
},
};
export const VariableValue = {
encode(message: VariableValue, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.name !== "") {
@@ -1172,6 +1190,9 @@ export const PlanComplete = {
if (message.moduleFiles.length !== 0) {
writer.uint32(82).bytes(message.moduleFiles);
}
for (const v of message.resourceReplacements) {
ResourceReplacement.encode(v!, writer.uint32(90).fork()).ldelim();
}
return writer;
},
};