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

feat: support auto installing terraform binary #801

Merged
merged 2 commits into from
Feb 20, 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
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ require (
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
github.com/hashicorp/hc-install v0.6.2
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.16
github.com/inconshreveable/mousetrap v1.0.1 // indirect
Expand Down Expand Up @@ -268,7 +269,7 @@ require (
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/arch v0.1.0 // indirect
golang.org/x/crypto v0.16.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/oauth2 v0.10.0 // indirect
golang.org/x/sync v0.3.0 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,8 @@ github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjG
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/hc-install v0.6.2 h1:V1k+Vraqz4olgZ9UzKiAcbman9i9scg9GgSt/U3mw/M=
github.com/hashicorp/hc-install v0.6.2/go.mod h1:2JBpd+NCFKiHiu/yYCGaPyPHhZLxXTpz8oreHa/a3Ps=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl/v2 v2.16.1 h1:BwuxEMD/tsYgbhIW7UuI3crjovf3MzuFWiVgiv57iHg=
Expand Down Expand Up @@ -755,8 +757,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down
10 changes: 10 additions & 0 deletions pkg/cmd/destroy/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"kusionstack.io/kusion/pkg/engine/backend"
"kusionstack.io/kusion/pkg/engine/operation"
opsmodels "kusionstack.io/kusion/pkg/engine/operation/models"
"kusionstack.io/kusion/pkg/engine/runtime/terraform"
"kusionstack.io/kusion/pkg/engine/states"
"kusionstack.io/kusion/pkg/log"
"kusionstack.io/kusion/pkg/project"
Expand Down Expand Up @@ -138,6 +139,15 @@ func (o *Options) preview(
) (*opsmodels.Changes, error) {
log.Info("Start compute preview changes ...")

// Check and install terraform executable binary for
// resources with the type of Terraform.
tfInstaller := terraform.CLIInstaller{
Intent: planResources,
}
if err := tfInstaller.CheckAndInstall(); err != nil {
return nil, err
}

pc := &operation.PreviewOperation{
Operation: opsmodels.Operation{
OperationType: opsmodels.DestroyPreview,
Expand Down
10 changes: 10 additions & 0 deletions pkg/cmd/preview/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"kusionstack.io/kusion/pkg/engine/backend"
"kusionstack.io/kusion/pkg/engine/operation"
opsmodels "kusionstack.io/kusion/pkg/engine/operation/models"
"kusionstack.io/kusion/pkg/engine/runtime/terraform"
"kusionstack.io/kusion/pkg/engine/states"
"kusionstack.io/kusion/pkg/log"
"kusionstack.io/kusion/pkg/project"
Expand Down Expand Up @@ -233,6 +234,15 @@ func Preview(
) (*opsmodels.Changes, error) {
log.Info("Start compute preview changes ...")

// Check and install terraform executable binary for
// resources with the type of Terraform.
tfInstaller := terraform.CLIInstaller{
Intent: planResources,
}
if err := tfInstaller.CheckAndInstall(); err != nil {
return nil, err
}

// Construct the preview operation
pc := &operation.PreviewOperation{
Operation: opsmodels.Operation{
Expand Down
142 changes: 142 additions & 0 deletions pkg/engine/runtime/terraform/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package terraform

import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"time"

"github.com/hashicorp/hc-install/product"
"github.com/hashicorp/hc-install/releases"
apiv1 "kusionstack.io/kusion/pkg/apis/core/v1"
"kusionstack.io/kusion/pkg/log"
"kusionstack.io/kusion/pkg/util/kfile"
)

var (
tfInstallSubDir = "terraform"
tfInstallTimeout = 3 * time.Minute
)

type CLIInstaller struct {
Intent *apiv1.Intent
}

// Check and install the terraform executable binary if it has not been downloaded.
func (installer *CLIInstaller) CheckAndInstall() error {
if len(installer.Intent.Resources) < 1 {
return nil
}

for i, res := range installer.Intent.Resources {
if res.Type == apiv1.Terraform {
break
}

if i == installer.Intent.Resources.Len() {
return nil
}
}

if err := checkTerraformExecutable(); err != nil {
log.Warn("Terraform executable binary is not found")

if err := installTerraform(); err != nil {
return err
}

log.Info("Successfully installed terraform and set the executable path")
}

return nil
}

// check whether the terraform executable binary has been installed.
func checkTerraformExecutable() error {
if err := exec.Command("terraform", "--version").Run(); err == nil {
return nil
}

installDir, err := getTerraformInstallDir()
if err != nil {
return err
}

execPath := filepath.Join(installDir, "terraform")
if err := exec.Command(execPath, "--version").Run(); err != nil {
return err
}

return setTerraformExecPathEnv(execPath)
}

// install and set the environment variable of executable path for terraform binary,
// currently the latest version will be downloaded by default.
func installTerraform() error {
log.Info("Installing terraform binary with the latest version...")

installDir, err := getTerraformInstallDir()
if err != nil {
return err
}

installer := &releases.LatestVersion{
Product: product.Terraform,
InstallDir: installDir,
Timeout: tfInstallTimeout,
}

execPath, err := installer.Install(context.Background())
if err != nil {
return err
}

log.Infof("Successfully located terraform binary: %s\n", execPath)

return setTerraformExecPathEnv(execPath)
}

// set the environment variable of executable path for terraform binary,
// note that this env only takes effect for the current process.
func setTerraformExecPathEnv(execPath string) error {
if execPath == "" {
return fmt.Errorf("empty executable path for terraform binary")
}

log.Info("Setting the environment variable of executable path for terraform...")
currentPath := os.Getenv("PATH")

// select the path separator according to the operating system.
var pathSeparator string
if runtime.GOOS == "windows" {
pathSeparator = ";"
} else {
pathSeparator = ":"
}

newPath := execPath + pathSeparator + currentPath

return os.Setenv("PATH", newPath)
}

// get the installation directory for terraform binary, and by default
// it is ~/.kusion/terraform.
func getTerraformInstallDir() (string, error) {
kusionDir, err := kfile.KusionDataFolder()
if err != nil {
return "", err
}

installDir := filepath.Join(kusionDir, tfInstallSubDir)

if _, err = os.Stat(installDir); os.IsNotExist(err) {
if err := os.Mkdir(installDir, 0o755); err != nil {
return "", fmt.Errorf("failed to create terraform install directory: %v", err)
}
}

return installDir, nil
}
93 changes: 93 additions & 0 deletions pkg/engine/runtime/terraform/install_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package terraform

import (
"fmt"
"testing"

"github.com/bytedance/mockey"
"github.com/stretchr/testify/assert"
v1 "kusionstack.io/kusion/pkg/apis/core/v1"
)

func TestCLIInstaller_CheckAndInstall(t *testing.T) {
mockey.PatchConvey("NoResources", t, func() {
installer := &CLIInstaller{
Intent: &v1.Intent{
Resources: v1.Resources{},
},
}
err := installer.CheckAndInstall()
assert.Nil(t, err)
})

mockey.PatchConvey("NoTerraformResources", t, func() {
installer := &CLIInstaller{
Intent: &v1.Intent{
Resources: v1.Resources{
v1.Resource{
Type: v1.Kubernetes,
},
},
},
}
err := installer.CheckAndInstall()
assert.Nil(t, err)
})

mockey.PatchConvey("ExistingTerraformExecutable", t, func() {
mockey.Mock(checkTerraformExecutable).To(func() error {
return nil
}).Build()
installer := &CLIInstaller{
Intent: &v1.Intent{
Resources: v1.Resources{
v1.Resource{
Type: v1.Terraform,
},
},
},
}
err := installer.CheckAndInstall()
assert.Nil(t, err)
})

mockey.PatchConvey("InstallTerraformTimeout", t, func() {
mockey.Mock(checkTerraformExecutable).To(func() error {
return fmt.Errorf("terraform executable not found")
}).Build()
mockey.Mock(installTerraform).To(func() error {
return fmt.Errorf("install timeout")
}).Build()
installer := &CLIInstaller{
Intent: &v1.Intent{
Resources: v1.Resources{
v1.Resource{
Type: v1.Terraform,
},
},
},
}
err := installer.CheckAndInstall()
assert.ErrorContains(t, err, "install timeout")
})

mockey.PatchConvey("SuccessfullyInstalled", t, func() {
mockey.Mock(checkTerraformExecutable).To(func() error {
return fmt.Errorf("terraform executable not found")
}).Build()
mockey.Mock(installTerraform).To(func() error {
return nil
}).Build()
installer := &CLIInstaller{
Intent: &v1.Intent{
Resources: v1.Resources{
v1.Resource{
Type: v1.Terraform,
},
},
},
}
err := installer.CheckAndInstall()
assert.Nil(t, err)
})
}
1 change: 1 addition & 0 deletions pkg/util/pretty/spinner_text.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ var SpinnerT = pterm.SpinnerPrinter{
SuccessPrinter: &SuccessT,
FailPrinter: &ErrorT,
WarningPrinter: &WarningT,
InfoPrinter: &InfoT,
}
Loading