github.com/hazelops/ize@v1.1.12-0.20230915191306-97d7c0e48f11/internal/commands/tfenv.go (about)

     1  package commands
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  
     9  	"github.com/aws/aws-sdk-go/aws"
    10  	"github.com/aws/aws-sdk-go/aws/awserr"
    11  	"github.com/aws/aws-sdk-go/service/s3"
    12  	"github.com/aws/aws-sdk-go/service/sts"
    13  	"github.com/hazelops/ize/internal/config"
    14  	"github.com/hazelops/ize/internal/template"
    15  	"github.com/hazelops/ize/pkg/templates"
    16  	"github.com/pterm/pterm"
    17  	"github.com/sirupsen/logrus"
    18  	"github.com/spf13/cobra"
    19  )
    20  
    21  type TfenvOptions struct {
    22  	Config                   *config.Project
    23  	TerraformStateBucketName string
    24  }
    25  
    26  var tfenvLongDesc = templates.LongDesc(`
    27  	tfenv generates backend.tf and variable.tfvars files.
    28  `)
    29  
    30  var tfenvExample = templates.Examples(`
    31  	# Generate files
    32  	ize tfenv
    33  
    34  	# Generate files via config file
    35  	ize --config-file /path/to/config tfenv
    36  
    37  	# Generate files via config file installed from env
    38  	export IZE_CONFIG_FILE=/path/to/config
    39  	ize tfenv
    40  `)
    41  
    42  func NewTfenvFlags(project *config.Project) *TfenvOptions {
    43  	return &TfenvOptions{
    44  		Config: project,
    45  	}
    46  }
    47  
    48  func NewCmdTfenv(project *config.Project) *cobra.Command {
    49  	o := NewTfenvFlags(project)
    50  
    51  	cmd := &cobra.Command{
    52  		Use:     "tfenv",
    53  		Short:   "Generate terraform files",
    54  		Long:    tfenvLongDesc,
    55  		Example: tfenvExample,
    56  		Hidden:  true,
    57  		RunE: func(cmd *cobra.Command, args []string) error {
    58  			cmd.SilenceUsage = true
    59  
    60  			err := o.Run()
    61  			if err != nil {
    62  				return err
    63  			}
    64  
    65  			return nil
    66  		},
    67  	}
    68  
    69  	cmd.Flags().StringVar(&o.TerraformStateBucketName, "terraform-state-bucket-name", "", "set terraform state bucket name (default <NAMESPACE>-tf-state)")
    70  
    71  	return cmd
    72  }
    73  
    74  func (o *TfenvOptions) Run() error {
    75  	return GenerateTerraformFiles("infra", o.TerraformStateBucketName, o.Config)
    76  
    77  }
    78  
    79  func GenerateTerraformFiles(name string, terraformStateBucketName string, project *config.Project) error {
    80  	var tf config.Terraform
    81  
    82  	if project.Terraform != nil {
    83  		tf = *project.Terraform[name]
    84  	}
    85  
    86  	if len(terraformStateBucketName) != 0 {
    87  		tf.StateBucketName = terraformStateBucketName
    88  	}
    89  
    90  	if len(tf.StateBucketName) == 0 {
    91  		legacyBucketExists := checkTFStateBucket(project, fmt.Sprintf("%s-tf-state", project.Namespace))
    92  		// If we found an existing bucket that conforms with the legacy format use it.
    93  		if legacyBucketExists {
    94  			tf.StateBucketName = fmt.Sprintf("%s-tf-state", project.Namespace)
    95  		} else {
    96  			resp, err := project.AWSClient.STSClient.GetCallerIdentity(
    97  				&sts.GetCallerIdentityInput{},
    98  			)
    99  			if err != nil {
   100  				return err
   101  			}
   102  
   103  			// If we haven't found an existing legacy format state bucket use a <NAMESPACE>-<AWS_ACCOUNT>-tf-state bucket as default (unless overridden with other parameters).
   104  			tf.StateBucketName = fmt.Sprintf("%s-%s-tf-state", project.Namespace, *resp.Account)
   105  		}
   106  	}
   107  
   108  	stateKey := fmt.Sprintf("%v/%v.tfstate", project.Env, name)
   109  	if len(tf.StateName) != 0 {
   110  		stateKey = fmt.Sprintf("%v/%v.tfstate", project.Env, tf.StateName)
   111  	} else if name == "infra" {
   112  		stateKey = filepath.Join(project.Env, "terraform.tfstate")
   113  	}
   114  
   115  	if len(tf.StateBucketRegion) == 0 {
   116  		tf.StateBucketRegion = project.AwsRegion
   117  	}
   118  
   119  	backendOpts := template.BackendOpts{
   120  		ENV:                            project.Env,
   121  		LOCALSTACK_ENDPOINT:            "",
   122  		TERRAFORM_STATE_BUCKET_NAME:    tf.StateBucketName,
   123  		TERRAFORM_STATE_KEY:            stateKey,
   124  		TERRAFORM_STATE_REGION:         tf.StateBucketRegion,
   125  		TERRAFORM_STATE_PROFILE:        project.AwsProfile,
   126  		TERRAFORM_STATE_DYNAMODB_TABLE: "tf-state-lock",
   127  		TERRAFORM_AWS_PROVIDER_VERSION: "",
   128  		NAMESPACE:                      project.Namespace,
   129  	}
   130  
   131  	stackPath := filepath.Join(project.EnvDir, name)
   132  	if name == "infra" {
   133  		stackPath = project.EnvDir
   134  	}
   135  
   136  	if len(tf.TerraformConfigFile) == 0 {
   137  		tf.TerraformConfigFile = "backend.tf"
   138  	}
   139  
   140  	logrus.Debugf("backend opts: %s", backendOpts)
   141  	logrus.Debugf("state dir path: %s", stackPath)
   142  	logrus.Debugf("config file name: %s", tf.TerraformConfigFile)
   143  
   144  	err := template.GenerateBackendTf(
   145  		backendOpts,
   146  		filepath.Join(stackPath, tf.TerraformConfigFile),
   147  	)
   148  	if err != nil {
   149  		pterm.Error.Printfln("Generate terraform file for \"%s\" not completed", name)
   150  		return fmt.Errorf("can't generate backent.tf: %s", err)
   151  	}
   152  
   153  	home, _ := os.UserHomeDir()
   154  	key, err := ioutil.ReadFile(fmt.Sprintf("%s/.ssh/id_rsa.pub", home))
   155  	if err != nil {
   156  		pterm.Error.Printfln("Generate terraform file for \"%s\" not completed", name)
   157  		return fmt.Errorf("can't read public ssh key: %s", err)
   158  
   159  	}
   160  
   161  	varsOpts := template.VarsOpts{
   162  		ENV:               project.Env,
   163  		AWS_PROFILE:       project.AwsProfile,
   164  		AWS_REGION:        project.AwsRegion,
   165  		EC2_KEY_PAIR_NAME: fmt.Sprintf("%v-%v", project.Env, project.Namespace),
   166  		ROOT_DOMAIN_NAME:  tf.RootDomainName,
   167  		SSH_PUBLIC_KEY:    string(key)[:len(string(key))-1],
   168  		NAMESPACE:         project.Namespace,
   169  	}
   170  
   171  	if len(project.Ecs) != 0 {
   172  		varsOpts.TAG = project.Tag
   173  		varsOpts.DOCKER_REGISTRY = project.DockerRegistry
   174  	}
   175  
   176  	logrus.Debugf("backend opts: %s", varsOpts)
   177  	logrus.Debugf("state dir path: %s", stackPath)
   178  
   179  	err = template.GenerateVarsTf(
   180  		varsOpts,
   181  		stackPath,
   182  	)
   183  	if err != nil {
   184  		pterm.Error.Printfln("Generate terraform file for \"%s\" not completed", name)
   185  		return fmt.Errorf("can't generate tfvars: %s", err)
   186  	}
   187  
   188  	pterm.Success.Printfln("Generate terraform file for \"%s\" completed", name)
   189  
   190  	return nil
   191  }
   192  
   193  func checkTFStateBucket(project *config.Project, name string) bool {
   194  	_, err := project.AWSClient.S3Client.HeadBucket(&s3.HeadBucketInput{
   195  		Bucket: aws.String(name),
   196  	})
   197  	if err != nil {
   198  		if aerr, ok := err.(awserr.Error); ok {
   199  			switch aerr.Code() {
   200  			case s3.ErrCodeNoSuchBucket:
   201  				return false
   202  			default:
   203  				return false
   204  			}
   205  		}
   206  	}
   207  
   208  	return true
   209  }
   210  
   211  func checkTFStateKey(project *config.Project, bucket, key string) bool {
   212  	_, err := project.AWSClient.S3Client.HeadObject(&s3.HeadObjectInput{
   213  		Bucket: aws.String(bucket),
   214  		Key:    aws.String(key),
   215  	})
   216  	if err != nil {
   217  		if aerr, ok := err.(awserr.Error); ok {
   218  			switch aerr.Code() {
   219  			case s3.ErrCodeNoSuchBucket:
   220  				return false
   221  			default:
   222  				return false
   223  			}
   224  		}
   225  	}
   226  
   227  	return true
   228  }