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

     1  package commands
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"text/template"
    10  
    11  	"github.com/aws/aws-sdk-go/aws"
    12  	"github.com/aws/aws-sdk-go/aws/awserr"
    13  	"github.com/aws/aws-sdk-go/service/ssm"
    14  	"github.com/hazelops/ize/internal/config"
    15  	"github.com/hazelops/ize/pkg/templates"
    16  	"github.com/pterm/pterm"
    17  	"github.com/spf13/cobra"
    18  )
    19  
    20  type SecretsPushOptions struct {
    21  	Config      *config.Project
    22  	AppName     string
    23  	Backend     string
    24  	FilePath    string
    25  	SecretsPath string
    26  	Force       bool
    27  	Explain     bool
    28  }
    29  
    30  var explainSecretsPushTmpl = `
    31  SERVICE_SECRETS_FILE={{.EnvDir}}/secrets/{{svc}}.json
    32  SERVICE_SECRETS=$(cat $SERVICE_SECRETS_FILE | jq -e -r '. | keys[]')
    33  for item in $(echo $SERVICE_SECRETS); do 
    34      aws --profile={{.AwsProfile}} ssm put-parameter --name="/{{.Env}}/{{svc}}/${item}" --value="$(cat $SERVICE_SECRETS_FILE | jq -r .$item )" --type SecureString --overwrite && \
    35      aws --profile={{.AwsProfile}} ssm add-tags-to-resource --resource-type "Parameter" --resource-id "/{{.Env}}/{{svc}}/${item}" \
    36      --tags "Key=Application,Value={{svc}}" "Key=EnvVarName,Value=${item}"
    37  done
    38  `
    39  
    40  var secretsPushExample = templates.Examples(`
    41  	# Push secrets:
    42  
    43      # This will push secrets for "squibby" app
    44      ize secrets push squibby
    45      
    46      # This will push secrets for "squibby" app from a "example-service.json" file to the AWS SSM storage with force option (values will be overwritten if exist)
    47  	ize secrets push squibby --backend ssm --file example-service.json --force
    48  `)
    49  
    50  func NewSecretsPushFlags(project *config.Project) *SecretsPushOptions {
    51  	return &SecretsPushOptions{
    52  		Config: project,
    53  	}
    54  }
    55  
    56  func NewCmdSecretsPush(project *config.Project) *cobra.Command {
    57  	o := NewSecretsPushFlags(project)
    58  
    59  	cmd := &cobra.Command{
    60  		Use:               "push <app>",
    61  		Example:           secretsPushExample,
    62  		Short:             "Push secrets to a key-value storage (like SSM)",
    63  		Long:              "This command pushes secrets from a local file to a key-value storage (like SSM)",
    64  		Args:              cobra.MinimumNArgs(1),
    65  		ValidArgsFunction: config.GetApps,
    66  		RunE: func(cmd *cobra.Command, args []string) error {
    67  			cmd.SilenceUsage = true
    68  
    69  			err := o.Complete(cmd)
    70  			if err != nil {
    71  				return err
    72  			}
    73  
    74  			err = o.Validate()
    75  			if err != nil {
    76  				return err
    77  			}
    78  
    79  			err = o.Run()
    80  			if err != nil {
    81  				return err
    82  			}
    83  
    84  			return nil
    85  		},
    86  	}
    87  
    88  	cmd.Flags().StringVar(&o.Backend, "backend", "ssm", "backend type (default=ssm)")
    89  	cmd.Flags().StringVar(&o.FilePath, "file", "", "file with secrets")
    90  	cmd.Flags().StringVar(&o.SecretsPath, "path", "", "path where to store secrets (/<env>/<app> by default)")
    91  	cmd.Flags().BoolVar(&o.Explain, "explain", false, "bash alternative shown")
    92  	cmd.Flags().BoolVar(&o.Force, "force", false, "allow values overwrite")
    93  
    94  	return cmd
    95  }
    96  
    97  func (o *SecretsPushOptions) Complete(cmd *cobra.Command) error {
    98  	o.AppName = cmd.Flags().Args()[0]
    99  
   100  	if o.FilePath == "" {
   101  		o.FilePath = fmt.Sprintf("%s/%s/%s.json", o.Config.EnvDir, "secrets", o.AppName)
   102  	}
   103  
   104  	if o.SecretsPath == "" {
   105  		o.SecretsPath = fmt.Sprintf("/%s/%s", o.Config.Env, o.AppName)
   106  	}
   107  
   108  	return nil
   109  }
   110  
   111  func (o *SecretsPushOptions) Validate() error {
   112  	if len(o.Config.Env) == 0 {
   113  		return fmt.Errorf("env must be specified")
   114  	}
   115  
   116  	return nil
   117  }
   118  
   119  func (o *SecretsPushOptions) Run() error {
   120  	if o.Explain {
   121  		err := o.Config.Generate(explainSecretsPushTmpl, template.FuncMap{
   122  			"svc": func() string {
   123  				return o.AppName
   124  			},
   125  		})
   126  		if err != nil {
   127  			return err
   128  		}
   129  
   130  		return nil
   131  	}
   132  
   133  	s, _ := pterm.DefaultSpinner.Start(fmt.Sprintf("Pushing secrets for %s...", o.AppName))
   134  	if o.Backend == "ssm" {
   135  		err := o.push(s)
   136  		if err != nil {
   137  			return fmt.Errorf("can't push secrets: %w", err)
   138  		}
   139  	} else {
   140  		return fmt.Errorf("backend with type %s not found or not supported", o.Backend)
   141  	}
   142  
   143  	s.Success("Pushing secrets complete!")
   144  
   145  	return nil
   146  }
   147  
   148  func (o *SecretsPushOptions) push(s *pterm.SpinnerPrinter) error {
   149  	s.UpdateText("Reading secrets from file...")
   150  	values, err := getKeyValuePairs(o.FilePath)
   151  	if err != nil {
   152  		return err
   153  	}
   154  
   155  	s.UpdateText(fmt.Sprintf("Pushing secrets to %s://%s...", o.Backend, o.SecretsPath))
   156  
   157  	for key, value := range values {
   158  		name := fmt.Sprintf("%s/%s", o.SecretsPath, key)
   159  
   160  		_, err := o.Config.AWSClient.SSMClient.PutParameter(&ssm.PutParameterInput{
   161  			Name:      &name,
   162  			Value:     aws.String(value),
   163  			Type:      aws.String(ssm.ParameterTypeSecureString),
   164  			Overwrite: &o.Force,
   165  		})
   166  
   167  		if aerr, ok := err.(awserr.Error); ok {
   168  			switch aerr.Code() {
   169  			case "ParameterAlreadyExists":
   170  				return fmt.Errorf("secret already exists, you can use --force to overwrite it")
   171  			default:
   172  				return err
   173  			}
   174  		}
   175  
   176  		_, err = o.Config.AWSClient.SSMClient.AddTagsToResource(&ssm.AddTagsToResourceInput{
   177  			ResourceId:   &name,
   178  			ResourceType: aws.String("Parameter"),
   179  			Tags: []*ssm.Tag{
   180  				{
   181  					Key:   aws.String("Application"),
   182  					Value: &o.AppName,
   183  				},
   184  				{
   185  					Key:   aws.String("EnvVarName"),
   186  					Value: &key,
   187  				},
   188  			},
   189  		})
   190  
   191  		if err != nil {
   192  			return err
   193  		}
   194  	}
   195  
   196  	return nil
   197  }
   198  
   199  func getKeyValuePairs(filePath string) (map[string]string, error) {
   200  	if !filepath.IsAbs(filePath) {
   201  		var err error
   202  		wd, _ := os.Getwd()
   203  		filePath, err = filepath.Abs(wd + "/" + filePath)
   204  		if err != nil {
   205  			return nil, err
   206  		}
   207  
   208  	}
   209  
   210  	if _, err := os.Stat(filePath); err != nil {
   211  		pterm.Fatal.Sprintfln("%s does not exist", filePath)
   212  		return nil, err
   213  	}
   214  
   215  	f, err := os.Open(filePath)
   216  	if err != nil {
   217  		return nil, err
   218  	}
   219  
   220  	defer func() {
   221  		cerr := f.Close()
   222  		if err == nil {
   223  			err = cerr
   224  		}
   225  	}()
   226  
   227  	bytes, err := ioutil.ReadAll(f)
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  
   232  	var result map[string]string
   233  
   234  	err = json.Unmarshal(bytes, &result)
   235  	if err != nil {
   236  		return nil, err
   237  	}
   238  
   239  	return result, nil
   240  }