Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: locked release file caused by interruption #1219

Merged
merged 2 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 64 additions & 7 deletions pkg/cmd/destroy/destroy.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package destroy

import (
"errors"
"fmt"
"os"
"strings"
Expand All @@ -36,6 +37,7 @@ import (
"kusionstack.io/kusion/pkg/engine/runtime/terraform"
"kusionstack.io/kusion/pkg/log"
"kusionstack.io/kusion/pkg/util/pretty"
"kusionstack.io/kusion/pkg/util/signal"
"kusionstack.io/kusion/pkg/util/terminal"
)

Expand Down Expand Up @@ -188,12 +190,66 @@ func (o *DestroyOptions) Run() (err error) {
}
releaseCreated = true

errCh := make(chan error, 1)
defer close(errCh)

// wait for the SIGTERM or SIGINT
go func() {
stopCh := signal.SetupSignalHandler()
<-stopCh
errCh <- errors.New("receive SIGTERM or SIGINT, exit cmd")
}()

// run destroy command
go func() {
errCh <- o.run(rel, storage)
}()

if err = <-errCh; err != nil {
rel.Phase = apiv1.ReleasePhaseFailed
release.UpdateDestroyRelease(storage, rel)
} else {
rel.Phase = apiv1.ReleasePhaseSucceeded
release.UpdateDestroyRelease(storage, rel)
}

return err
}

// run executes the delete command after release is created.
func (o *DestroyOptions) run(rel *apiv1.Release, storage release.Storage) (err error) {
// update release to succeeded or failed
defer func() {
if err != nil {
rel.Phase = apiv1.ReleasePhaseFailed
release.UpdateDestroyRelease(storage, rel)
} else {
rel.Phase = apiv1.ReleasePhaseSucceeded
err = release.UpdateDestroyRelease(storage, rel)
}
}()

// set no style
if o.NoStyle {
pterm.DisableStyling()
}

sp := o.UI.SpinnerPrinter
sp, _ = sp.Start(fmt.Sprintf("Computing destroy changes in the Stack %s...", o.RefStack.Name))

// compute changes for preview
changes, err := o.preview(rel.Spec, rel.State, o.RefProject, o.RefStack, storage)
if err != nil {
if sp != nil {
sp.Fail()
}
return
}

if sp != nil {
sp.Success()
}

// preview
changes.Summary(os.Stdout, o.NoStyle)

Expand All @@ -203,16 +259,11 @@ func (o *DestroyOptions) Run() (err error) {
return nil
}

// set no style
if o.NoStyle {
pterm.DisableStyling()
}

// prompt
if !o.Yes {
for {
var input string
input, err = prompt(o.UI)
input, err = prompt(o.UI, rel, storage)
if err != nil {
return
}
Expand Down Expand Up @@ -395,13 +446,19 @@ func (o *DestroyOptions) destroy(rel *apiv1.Release, changes *models.Changes, st
return updatedRel, nil
}

func prompt(ui *terminal.UI) (string, error) {
func prompt(ui *terminal.UI, rel *apiv1.Release, storage release.Storage) (string, error) {
options := []string{"yes", "details", "no"}
input, err := ui.InteractiveSelectPrinter.
WithFilter(false).
WithDefaultText(`Do you want to destroy these diffs?`).
WithOptions(options).
WithDefaultOption("details").
// To gracefully exit if interrupted by SIGINT or SIGTERM.
WithOnInterruptFunc(func() {
rel.Phase = apiv1.ReleasePhaseFailed
release.UpdateDestroyRelease(storage, rel)
os.Exit(1)
}).
Show()
if err != nil {
fmt.Printf("Prompt failed: %v\n", err)
Expand Down
4 changes: 2 additions & 2 deletions pkg/cmd/destroy/destroy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,13 +324,13 @@ func mockOperationDestroy(res models.OpResult) {
func TestPrompt(t *testing.T) {
mockey.PatchConvey("prompt error", t, func() {
mockey.Mock((*pterm.InteractiveSelectPrinter).Show).Return("", errors.New("mock error")).Build()
_, err := prompt(terminal.DefaultUI())
_, err := prompt(terminal.DefaultUI(), &apiv1.Release{}, &releasestorages.LocalStorage{})
assert.NotNil(t, err)
})

mockey.PatchConvey("prompt yes", t, func() {
mockPromptOutput("yes")
_, err := prompt(terminal.DefaultUI())
_, err := prompt(terminal.DefaultUI(), &apiv1.Release{}, &releasestorages.LocalStorage{})
assert.Nil(t, err)
})
}
8 changes: 8 additions & 0 deletions pkg/engine/operation/models/change.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io"
"os"
"strings"

"github.com/liu-hm19/pterm"
Expand Down Expand Up @@ -262,6 +263,13 @@ func (o *ChangeOrder) PromptDetails(ui *terminal.UI) (string, error) {
WithDefaultText(`Which diff detail do you want to see?`).
WithOptions(options).
WithDefaultOption("all").
// Fixme: interruption during 'apply' or 'destroy' may result in a locked release file.
WithOnInterruptFunc(func() {
hint := `Interruption during 'apply' or 'destroy' may result in a locked release file.
Please use 'kusion release unlock' before executing the next operation.`
fmt.Printf("\n" + hint + "\n")
os.Exit(1)
}).
Show()
if err != nil {
fmt.Printf("Prompt failed: %v\n", err)
Expand Down
Loading