Skip to content

Commit

Permalink
feat: support auto installing terraform binary (#801)
Browse files Browse the repository at this point in the history
* feat: support terraform auto installation

* fix: fix a lint error
  • Loading branch information
liu-hm19 committed Feb 20, 2024
1 parent 13968cc commit ab28e79
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 3 deletions.
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,
}

0 comments on commit ab28e79

Please sign in to comment.