cli/azd/pkg/tools/terraform/terraform.go (200 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package terraform
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/azure/azure-dev/cli/azd/pkg/exec"
"github.com/azure/azure-dev/cli/azd/pkg/tools"
"github.com/blang/semver/v4"
)
var _ tools.ExternalTool = (*Cli)(nil)
type Cli struct {
commandRunner exec.CommandRunner
env []string
}
func NewCli(commandRunner exec.CommandRunner) *Cli {
return &Cli{
commandRunner: commandRunner,
}
}
func (cli *Cli) Name() string {
return "Terraform CLI"
}
// Doc to be added for terraform install
func (cli *Cli) InstallUrl() string {
return "https://aka.ms/azure-dev/terraform-install"
}
func (cli *Cli) versionInfo() tools.VersionInfo {
return tools.VersionInfo{
MinimumVersion: semver.Version{
Major: 1,
Minor: 1,
Patch: 7},
UpdateCommand: "Download newer version from https://www.terraform.io/downloads",
}
}
func (cli *Cli) CheckInstalled(ctx context.Context) error {
err := tools.ToolInPath("terraform")
if err != nil {
return err
}
tfVer, err := cli.unmarshalCliVersion(ctx, "terraform_version")
if err != nil {
return fmt.Errorf("checking %s version: %w", cli.Name(), err)
}
log.Printf("terraform version: %s", tfVer)
tfSemver, err := semver.Parse(tfVer)
if err != nil {
return fmt.Errorf("converting to semver version fails: %w", err)
}
updateDetail := cli.versionInfo()
if tfSemver.LT(updateDetail.MinimumVersion) {
return &tools.ErrSemver{ToolName: cli.Name(), VersionInfo: updateDetail}
}
return nil
}
// Set environment variables to be used in all terraform commands
func (cli *Cli) SetEnv(env []string) {
cli.env = env
}
func (cli *Cli) runCommand(ctx context.Context, args ...string) (exec.RunResult, error) {
runArgs := exec.
NewRunArgs("terraform", args...).
WithEnv(cli.env)
return cli.commandRunner.Run(ctx, runArgs)
}
func (cli *Cli) runInteractive(ctx context.Context, args ...string) (exec.RunResult, error) {
runArgs := exec.
NewRunArgs("terraform", args...).
WithEnv(cli.env).
WithInteractive(true)
return cli.commandRunner.Run(ctx, runArgs)
}
func (cli *Cli) unmarshalCliVersion(ctx context.Context, component string) (string, error) {
azRes, err := tools.ExecuteCommand(ctx, cli.commandRunner, "terraform", "version", "-json")
if err != nil {
return "", err
}
var tfVerMap map[string]interface{}
err = json.Unmarshal([]byte(azRes), &tfVerMap)
if err != nil {
return "", err
}
version, ok := tfVerMap[component].(string)
if !ok {
return "", fmt.Errorf("reading %s component '%s' version failed", cli.Name(), component)
}
return version, nil
}
func (cli *Cli) Validate(ctx context.Context, modulePath string) (string, error) {
args := []string{fmt.Sprintf("-chdir=%s", modulePath), "validate"}
cmdRes, err := cli.runCommand(ctx, args...)
if err != nil {
return "", fmt.Errorf(
"failed running terraform validate: %s (%w)",
cmdRes.Stderr,
err,
)
}
return cmdRes.Stdout, nil
}
func (cli *Cli) Init(ctx context.Context, modulePath string, additionalArgs ...string) (string, error) {
args := []string{
fmt.Sprintf("-chdir=%s", modulePath),
"init",
"-upgrade",
}
args = append(args, additionalArgs...)
cmdRes, err := cli.runInteractive(ctx, args...)
if err != nil {
return "", fmt.Errorf(
"failed running terraform init: %s (%w)",
cmdRes.Stderr,
err,
)
}
return cmdRes.Stdout, nil
}
func (cli *Cli) Plan(
ctx context.Context,
modulePath string,
planFilePath string,
additionalArgs ...string,
) (string, error) {
args := []string{
fmt.Sprintf("-chdir=%s", modulePath),
"plan",
fmt.Sprintf("-out=%s", planFilePath),
"-lock=false",
}
args = append(args, additionalArgs...)
cmdRes, err := cli.runInteractive(ctx, args...)
if err != nil {
return "", fmt.Errorf(
"failed running terraform plan: %s (%w)",
cmdRes.Stderr,
err,
)
}
return cmdRes.Stdout, nil
}
func (cli *Cli) Apply(ctx context.Context, modulePath string, additionalArgs ...string) (string, error) {
args := []string{
fmt.Sprintf("-chdir=%s", modulePath),
"apply",
"-lock=false",
}
args = append(args, additionalArgs...)
cmdRes, err := cli.runInteractive(ctx, args...)
if err != nil {
return "", fmt.Errorf(
"failed running terraform apply: %s (%w)",
cmdRes.Stderr,
err,
)
}
return cmdRes.Stdout, nil
}
func (cli *Cli) Output(ctx context.Context, modulePath string, additionalArgs ...string) (string, error) {
args := []string{
fmt.Sprintf("-chdir=%s", modulePath), "output", "-json"}
args = append(args, additionalArgs...)
cmdRes, err := cli.runCommand(ctx, args...)
if err != nil {
return "", fmt.Errorf(
"failed running terraform output: %s (%w)",
cmdRes.Stderr,
err,
)
}
return cmdRes.Stdout, nil
}
func (cli *Cli) Show(ctx context.Context, modulePath string, additionalArgs ...string) (string, error) {
args := []string{
fmt.Sprintf("-chdir=%s", modulePath), "show", "-json"}
args = append(args, additionalArgs...)
cmdRes, err := cli.runCommand(ctx, args...)
if err != nil {
return "", fmt.Errorf(
"failed running terraform output: %s (%w)",
cmdRes.Stderr,
err,
)
}
return cmdRes.Stdout, nil
}
func (cli *Cli) Destroy(ctx context.Context, modulePath string, additionalArgs ...string) (string, error) {
args := []string{
fmt.Sprintf("-chdir=%s", modulePath),
"destroy",
}
args = append(args, additionalArgs...)
cmdRes, err := cli.runInteractive(ctx, args...)
if err != nil {
return "", fmt.Errorf(
"failed running terraform destroy: %s (%w)",
cmdRes.Stderr,
err,
)
}
return cmdRes.Stdout, nil
}