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

tfexec: add InterruptChannel option to allow a user triggered interrupt #334

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
19 changes: 14 additions & 5 deletions tfexec/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
)

type applyConfig struct {
interruptCh <-chan struct{}

backup string
dirOrPlan string
lock bool
Expand Down Expand Up @@ -42,6 +44,10 @@ func (opt *ParallelismOption) configureApply(conf *applyConfig) {
conf.parallelism = opt.parallelism
}

func (opt *InterruptChannelOption) configureApply(conf *applyConfig) {
conf.interruptCh = opt.interrupt
}

func (opt *BackupOption) configureApply(conf *applyConfig) {
conf.backup = opt.path
}
Expand Down Expand Up @@ -92,14 +98,17 @@ func (opt *ReattachOption) configureApply(conf *applyConfig) {

// Apply represents the terraform apply subcommand.
func (tf *Terraform) Apply(ctx context.Context, opts ...ApplyOption) error {
cmd, err := tf.applyCmd(ctx, opts...)
cmd, cfg, err := tf.applyCmd(ctx, opts...)
if err != nil {
return err
}
if cfg.interruptCh != nil {
ctx = context.WithValue(ctx, interruptContext, cfg.interruptCh)
}
return tf.runTerraformCmd(ctx, cmd)
}

func (tf *Terraform) applyCmd(ctx context.Context, opts ...ApplyOption) (*exec.Cmd, error) {
func (tf *Terraform) applyCmd(ctx context.Context, opts ...ApplyOption) (*exec.Cmd, *applyConfig, error) {
c := defaultApplyOptions

for _, o := range opts {
Expand Down Expand Up @@ -134,7 +143,7 @@ func (tf *Terraform) applyCmd(ctx context.Context, opts ...ApplyOption) (*exec.C
if c.replaceAddrs != nil {
err := tf.compatible(ctx, tf0_15_2, nil)
if err != nil {
return nil, fmt.Errorf("replace option was introduced in Terraform 0.15.2: %w", err)
return nil, nil, fmt.Errorf("replace option was introduced in Terraform 0.15.2: %w", err)
}
for _, addr := range c.replaceAddrs {
args = append(args, "-replace="+addr)
Expand All @@ -160,10 +169,10 @@ func (tf *Terraform) applyCmd(ctx context.Context, opts ...ApplyOption) (*exec.C
if c.reattachInfo != nil {
reattachStr, err := c.reattachInfo.marshalString()
if err != nil {
return nil, err
return nil, nil, err
}
mergeEnv[reattachEnvVar] = reattachStr
}

return tf.buildTerraformCmd(ctx, mergeEnv, args...), nil
return tf.buildTerraformCmd(ctx, mergeEnv, args...), &c, nil
}
7 changes: 6 additions & 1 deletion tfexec/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestApplyCmd(t *testing.T) {
tf.SetEnv(map[string]string{})

t.Run("basic", func(t *testing.T) {
applyCmd, err := tf.applyCmd(context.Background(),
applyCmd, cfg, err := tf.applyCmd(context.Background(),
Backup("testbackup"),
LockTimeout("200s"),
State("teststate"),
Expand All @@ -36,6 +36,7 @@ func TestApplyCmd(t *testing.T) {
Var("var1=foo"),
Var("var2=bar"),
DirOrPlan("testfile"),
InterruptChannel(make(chan struct{})),
)
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -63,5 +64,9 @@ func TestApplyCmd(t *testing.T) {
"-var", "var2=bar",
"testfile",
}, nil, applyCmd)

if cfg.interruptCh == nil {
t.Fatal("interrupt signal is unexpectedly nil")
}
})
}
8 changes: 7 additions & 1 deletion tfexec/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import (
"github.com/hashicorp/terraform-exec/internal/version"
)

// If using the InterruptSignal option, we stuff the interrupt channel into the
// context to keep our APIs simpler (and non-changing).
//
// context.WithValue(ctx, interruptContext, interruptCh)
var interruptContext = new(struct{})

const (
checkpointDisableEnvVar = "CHECKPOINT_DISABLE"
cliArgsEnvVar = "TF_CLI_ARGS"
Expand Down Expand Up @@ -191,7 +197,7 @@ func (tf *Terraform) buildTerraformCmd(ctx context.Context, mergeEnv map[string]
}

func (tf *Terraform) runTerraformCmdJSON(ctx context.Context, cmd *exec.Cmd, v interface{}) error {
var outbuf = bytes.Buffer{}
var outbuf bytes.Buffer
cmd.Stdout = mergeWriters(cmd.Stdout, &outbuf)

err := tf.runTerraformCmd(ctx, cmd)
Expand Down
29 changes: 25 additions & 4 deletions tfexec/cmd_default_test.go → tfexec/cmd_all_platforms_test.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
//go:build !linux
// +build !linux

package tfexec

import (
"bytes"
"context"
"errors"
"log"
"strings"
"testing"
"time"
)

func Test_runTerraformCmd_default(t *testing.T) {
func Test_runTerraformCmd(t *testing.T) {
// Checks runTerraformCmd for race condition when using
// go test -race -run Test_runTerraformCmd_default ./tfexec
var buf bytes.Buffer
Expand All @@ -37,3 +35,26 @@ func Test_runTerraformCmd_default(t *testing.T) {
t.Fatal("canceling context should not lead to logging an error")
}
}

func Test_runTerraformCmdCancel(t *testing.T) {
var buf bytes.Buffer

tf := &Terraform{
logger: log.New(&buf, "", 0),
execPath: "sleep",
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

cmd := tf.buildTerraformCmd(ctx, nil, "10")
go func() {
time.Sleep(time.Second)
cancel()
}()

err := tf.runTerraformCmd(ctx, cmd)
if !errors.Is(err, context.Canceled) {
t.Fatalf("expected context.Canceled, got %T %s", err, err)
}
}
5 changes: 3 additions & 2 deletions tfexec/cmd_default.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//go:build !linux
// +build !linux
// list taken from https://github.com/golang/go/blob/91ef076562dfcf783074dbd84ad7c6db60fdd481/src/go/build/syslist.go#L38-L51
//go:build !aix && !android && !darwin && !dragonfly && !freebsd && !hurd && !illumos && !ios && !linux && !netbsd && !openbsd && !solaris
// +build !aix,!android,!darwin,!dragonfly,!freebsd,!hurd,!illumos,!ios,!linux,!netbsd,!openbsd,!solaris

package tfexec

Expand Down
36 changes: 0 additions & 36 deletions tfexec/cmd_linux_test.go

This file was deleted.

8 changes: 8 additions & 0 deletions tfexec/cmd_sys_proc_attr_default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//go:build !linux
// +build !linux

package tfexec

import "syscall"

var defaultSysProcAttr = &syscall.SysProcAttr{}
13 changes: 13 additions & 0 deletions tfexec/cmd_sys_proc_attr_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//go:build linux
// +build linux

package tfexec

import "syscall"

var defaultSysProcAttr = &syscall.SysProcAttr{
// kill children if parent is dead
Pdeathsig: syscall.SIGKILL,
// set process group ID
Setpgid: true,
}
26 changes: 19 additions & 7 deletions tfexec/cmd_linux.go → tfexec/cmd_unix.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
// list taken from https://github.com/golang/go/blob/91ef076562dfcf783074dbd84ad7c6db60fdd481/src/go/build/syslist.go#L38-L51
//go:build aix || android || darwin || dragonfly || freebsd || hurd || illumos || ios || linux || netbsd || openbsd || solaris
// +build aix android darwin dragonfly freebsd hurd illumos ios linux netbsd openbsd solaris

package tfexec

import (
"context"
"os"
"os/exec"
"strings"
"sync"
"syscall"
)

func (tf *Terraform) runTerraformCmd(ctx context.Context, cmd *exec.Cmd) error {
var errBuf strings.Builder

cmd.SysProcAttr = &syscall.SysProcAttr{
// kill children if parent is dead
Pdeathsig: syscall.SIGKILL,
// set process group ID
Setpgid: true,
}
cmd.SysProcAttr = defaultSysProcAttr

// check for early cancellation
select {
Expand Down Expand Up @@ -52,6 +51,19 @@ func (tf *Terraform) runTerraformCmd(ctx context.Context, cmd *exec.Cmd) error {
return tf.wrapExitError(ctx, err, "")
}

if interruptCh := ctx.Value(interruptContext); interruptCh != nil {
exited := make(chan struct{})
defer close(exited)
go func() {
select {
case <-interruptCh.(<-chan struct{}):
cmd.Process.Signal(os.Interrupt)
case <-exited:
case <-ctx.Done():
}
}()
}

var errStdout, errStderr error
var wg sync.WaitGroup
wg.Add(1)
Expand Down
17 changes: 13 additions & 4 deletions tfexec/destroy.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
)

type destroyConfig struct {
interruptCh <-chan struct{}

backup string
dir string
lock bool
Expand Down Expand Up @@ -46,6 +48,10 @@ func (opt *ParallelismOption) configureDestroy(conf *destroyConfig) {
conf.parallelism = opt.parallelism
}

func (opt *InterruptChannelOption) configureDestroy(conf *destroyConfig) {
conf.interruptCh = opt.interrupt
}

func (opt *BackupOption) configureDestroy(conf *destroyConfig) {
conf.backup = opt.path
}
Expand Down Expand Up @@ -88,14 +94,17 @@ func (opt *ReattachOption) configureDestroy(conf *destroyConfig) {

// Destroy represents the terraform destroy subcommand.
func (tf *Terraform) Destroy(ctx context.Context, opts ...DestroyOption) error {
cmd, err := tf.destroyCmd(ctx, opts...)
cmd, cfg, err := tf.destroyCmd(ctx, opts...)
if err != nil {
return err
}
if cfg.interruptCh != nil {
ctx = context.WithValue(ctx, interruptContext, cfg.interruptCh)
}
return tf.runTerraformCmd(ctx, cmd)
}

func (tf *Terraform) destroyCmd(ctx context.Context, opts ...DestroyOption) (*exec.Cmd, error) {
func (tf *Terraform) destroyCmd(ctx context.Context, opts ...DestroyOption) (*exec.Cmd, *destroyConfig, error) {
c := defaultDestroyOptions

for _, o := range opts {
Expand Down Expand Up @@ -147,10 +156,10 @@ func (tf *Terraform) destroyCmd(ctx context.Context, opts ...DestroyOption) (*ex
if c.reattachInfo != nil {
reattachStr, err := c.reattachInfo.marshalString()
if err != nil {
return nil, err
return nil, nil, err
}
mergeEnv[reattachEnvVar] = reattachStr
}

return tf.buildTerraformCmd(ctx, mergeEnv, args...), nil
return tf.buildTerraformCmd(ctx, mergeEnv, args...), &c, nil
}
12 changes: 10 additions & 2 deletions tfexec/destroy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestDestroyCmd(t *testing.T) {
tf.SetEnv(map[string]string{})

t.Run("defaults", func(t *testing.T) {
destroyCmd, err := tf.destroyCmd(context.Background())
destroyCmd, cfg, err := tf.destroyCmd(context.Background())
if err != nil {
t.Fatal(err)
}
Expand All @@ -34,10 +34,14 @@ func TestDestroyCmd(t *testing.T) {
"-parallelism=10",
"-refresh=true",
}, nil, destroyCmd)

if cfg.interruptCh != nil {
t.Fatal("interrupt signal is unexpectedly non-nil")
}
})

t.Run("override all defaults", func(t *testing.T) {
destroyCmd, err := tf.destroyCmd(context.Background(), Backup("testbackup"), LockTimeout("200s"), State("teststate"), StateOut("teststateout"), VarFile("testvarfile"), Lock(false), Parallelism(99), Refresh(false), Target("target1"), Target("target2"), Var("var1=foo"), Var("var2=bar"), Dir("destroydir"))
destroyCmd, cfg, err := tf.destroyCmd(context.Background(), Backup("testbackup"), LockTimeout("200s"), State("teststate"), StateOut("teststateout"), VarFile("testvarfile"), Lock(false), Parallelism(99), Refresh(false), Target("target1"), Target("target2"), Var("var1=foo"), Var("var2=bar"), Dir("destroydir"), InterruptChannel(make(chan struct{})))
if err != nil {
t.Fatal(err)
}
Expand All @@ -61,5 +65,9 @@ func TestDestroyCmd(t *testing.T) {
"-var", "var2=bar",
"destroydir",
}, nil, destroyCmd)

if cfg.interruptCh == nil {
t.Fatal("interrupt signal is unexpectedly nil")
}
})
}
Loading