Skip to content

Commit ed387d8

Browse files
committed
Feat reattach to manual destroy run
1 parent 5d1870e commit ed387d8

File tree

3 files changed

+158
-0
lines changed

3 files changed

+158
-0
lines changed

internal/controller/workspace_controller_deletion_policy.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,30 @@ func (r *WorkspaceReconciler) deleteWorkspace(ctx context.Context, w *workspaceI
101101

102102
if _, ok := runStatusUnsuccessful[run.Status]; ok {
103103
w.log.Info("Destroy Run", "msg", fmt.Sprintf("destroy run %s is unsuccessful: %s", run.ID, run.Status))
104+
105+
workspace, err := w.tfClient.Client.Workspaces.ReadByID(ctx, w.instance.Status.WorkspaceID)
106+
if err != nil {
107+
return r.handleWorkspaceErrorNotFound(ctx, w, err)
108+
}
109+
110+
w.log.Info("Destroy Run", "msg", fmt.Sprintf("CurrentRun: %s %s %v", workspace.CurrentRun.ID, workspace.CurrentRun.Status, workspace.CurrentRun.IsDestroy))
111+
112+
if workspace.CurrentRun != nil && workspace.CurrentRun.ID != w.instance.Status.DestroyRunID {
113+
114+
run, err := w.tfClient.Client.Runs.Read(ctx, w.instance.Status.DestroyRunID)
115+
if err != nil {
116+
// ignore this run id, and let the next reconcile loop handle the error
117+
return nil
118+
}
119+
if run.IsDestroy {
120+
w.log.Info("Destroy Run", "msg", fmt.Sprintf("found more recent destroy run %s, updating DestroyRunID", workspace.CurrentRun.ID))
121+
122+
w.instance.Status.DestroyRunID = workspace.CurrentRun.ID
123+
w.updateWorkspaceStatusRun(run)
124+
return r.Status().Update(ctx, &w.instance)
125+
}
126+
}
127+
104128
return nil
105129
}
106130
w.log.Info("Destroy Run", "msg", fmt.Sprintf("destroy run %s is not finished", run.ID))

internal/controller/workspace_controller_deletion_policy_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,97 @@ var _ = Describe("Workspace controller", Ordered, func() {
195195
return err == tfc.ErrResourceNotFound
196196
}).Should(BeTrue())
197197
})
198+
It("can destroy delete a workspace when the destroy was retried manually after failing", func() {
199+
if cloudEndpoint != tfcDefaultAddress {
200+
Skip("Does not run against TFC, skip this test")
201+
}
202+
instance.Spec.AllowDestroyPlan = true
203+
instance.Spec.DeletionPolicy = appv1alpha2.DeletionPolicyDestroy
204+
createWorkspace(instance)
205+
workspaceID := instance.Status.WorkspaceID
206+
207+
cv := createAndUploadConfigurationVersion(instance.Status.WorkspaceID, "hoi")
208+
Eventually(func() bool {
209+
listOpts := tfc.ListOptions{
210+
PageNumber: 1,
211+
PageSize: maxPageSize,
212+
}
213+
for listOpts.PageNumber != 0 {
214+
runs, err := tfClient.Runs.List(ctx, workspaceID, &tfc.RunListOptions{
215+
ListOptions: listOpts,
216+
})
217+
Expect(err).To(Succeed())
218+
for _, r := range runs.Items {
219+
if r.ConfigurationVersion.ID == cv.ID {
220+
return r.Status == tfc.RunApplied
221+
}
222+
}
223+
listOpts.PageNumber = runs.NextPage
224+
}
225+
return false
226+
}).Should(BeTrue())
227+
228+
// create an errored ConfigurationVersion for the delete to fail
229+
cv = createAndUploadErroredConfigurationVersion(instance.Status.WorkspaceID, false)
230+
231+
Expect(k8sClient.Delete(ctx, instance)).To(Succeed())
232+
233+
var destroyRunID string
234+
Eventually(func() bool {
235+
ws, err := tfClient.Workspaces.ReadByID(ctx, workspaceID)
236+
Expect(err).To(Succeed())
237+
Expect(ws).ToNot(BeNil())
238+
Expect(ws.CurrentRun).ToNot(BeNil())
239+
run, err := tfClient.Runs.Read(ctx, ws.CurrentRun.ID)
240+
Expect(err).To(Succeed())
241+
Expect(run).ToNot(BeNil())
242+
destroyRunID = run.ID
243+
244+
return run.IsDestroy
245+
}).Should(BeTrue())
246+
247+
Eventually(func() bool {
248+
run, _ := tfClient.Runs.Read(ctx, destroyRunID)
249+
if run.Status == tfc.RunErrored {
250+
return true
251+
}
252+
253+
return false
254+
}).Should(BeTrue())
255+
256+
// put back a working configuration
257+
cv = createAndUploadConfigurationVersion(instance.Status.WorkspaceID, "hoi")
258+
259+
// start a new destroy run manually
260+
run, err := tfClient.Runs.Create(ctx, tfc.RunCreateOptions{
261+
IsDestroy: tfc.Bool(true),
262+
Message: tfc.String(runMessage),
263+
Workspace: &tfc.Workspace{
264+
ID: workspaceID,
265+
},
266+
})
267+
Expect(err).To(Succeed())
268+
Expect(run).ToNot(BeNil())
269+
270+
var newDestroyRunID string
271+
Eventually(func() bool {
272+
ws, err := tfClient.Workspaces.ReadByID(ctx, workspaceID)
273+
Expect(err).To(Succeed())
274+
Expect(ws).ToNot(BeNil())
275+
Expect(ws.CurrentRun).ToNot(BeNil())
276+
run, err := tfClient.Runs.Read(ctx, ws.CurrentRun.ID)
277+
Expect(err).To(Succeed())
278+
Expect(run).ToNot(BeNil())
279+
newDestroyRunID = run.ID
280+
281+
return run.IsDestroy && newDestroyRunID != destroyRunID
282+
}).Should(BeTrue())
283+
284+
Eventually(func() bool {
285+
_, err := tfClient.Workspaces.ReadByID(ctx, workspaceID)
286+
return err == tfc.ErrResourceNotFound
287+
}).Should(BeTrue())
288+
})
198289
It("can force delete a workspace", func() {
199290
instance.Spec.DeletionPolicy = appv1alpha2.DeletionPolicyForce
200291
createWorkspace(instance)

internal/controller/workspace_controller_outputs_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,46 @@ func createAndUploadConfigurationVersion(workspaceID string, outputValue string)
180180

181181
return cv
182182
}
183+
184+
func createAndUploadErroredConfigurationVersion(workspaceID string, autoQueueRuns bool) *tfc.ConfigurationVersion {
185+
GinkgoHelper()
186+
// Create a temporary dir in the current one
187+
cd, err := os.Getwd()
188+
Expect(err).Should(Succeed())
189+
td, err := os.MkdirTemp(cd, "tf-*")
190+
Expect(err).Should(Succeed())
191+
defer os.RemoveAll(td)
192+
// Create a temporary file in the temporary dir
193+
f, err := os.CreateTemp(td, "*.tf")
194+
Expect(err).Should(Succeed())
195+
defer os.Remove(f.Name())
196+
// Terraform code to upload
197+
tf := fmt.Sprint(`
198+
resource "test_non_existent_resource" "this" {}
199+
`)
200+
// Save the Terraform code to the temporary file
201+
_, err = f.WriteString(tf)
202+
Expect(err).Should(Succeed())
203+
204+
cv, err := tfClient.ConfigurationVersions.Create(ctx, workspaceID, tfc.ConfigurationVersionCreateOptions{
205+
AutoQueueRuns: tfc.Bool(autoQueueRuns),
206+
Speculative: tfc.Bool(false),
207+
})
208+
Expect(err).Should(Succeed())
209+
Expect(cv).ShouldNot(BeNil())
210+
211+
Expect(tfClient.ConfigurationVersions.Upload(ctx, cv.UploadURL, td)).Should(Succeed())
212+
213+
Eventually(func() bool {
214+
c, err := tfClient.ConfigurationVersions.Read(ctx, cv.ID)
215+
if err != nil {
216+
return false
217+
}
218+
if c.Status == tfc.ConfigurationUploaded {
219+
return true
220+
}
221+
return false
222+
}).Should(BeTrue())
223+
224+
return cv
225+
}

0 commit comments

Comments
 (0)