github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cloud/amazon/permissions.go (about)

     1  package amazon
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  	"text/template"
    11  
    12  	session2 "github.com/olli-ai/jx/v2/pkg/cloud/amazon/session"
    13  
    14  	"github.com/aws/aws-sdk-go/aws"
    15  	"github.com/aws/aws-sdk-go/service/cloudformation"
    16  	"github.com/google/uuid"
    17  	"github.com/jenkins-x/jx-logging/pkg/log"
    18  	"github.com/olli-ai/jx/v2/pkg/cloud"
    19  	"github.com/olli-ai/jx/v2/pkg/config"
    20  	"github.com/olli-ai/jx/v2/pkg/helm"
    21  	"github.com/olli-ai/jx/v2/pkg/util"
    22  	"github.com/pkg/errors"
    23  	"k8s.io/helm/pkg/chartutil"
    24  )
    25  
    26  const (
    27  	// PoliciesTemplateName is the name of the custom policies CloudFormation stack that will be executed before
    28  	// calling the eksctl commands
    29  	PoliciesTemplateName = "jenkinsx-policies.yml"
    30  	// ConfigTemplatesFolder is part of the path to the configuration templates
    31  	ConfigTemplatesFolder = "templates"
    32  	// IRSATemplateName is the name of the eksctl configuration file that will be processed after creating the policies
    33  	IRSATemplateName = "irsa.tmpl.yaml"
    34  )
    35  
    36  // EnableIRSASupportInCluster Associates IAM as an OIDC provider so it can sign requests and assume roles
    37  func EnableIRSASupportInCluster(requirements *config.RequirementsConfig) error {
    38  	log.Logger().Infof("Enabling IRSA for cluster %s associating the IAM Open ID Connect provider", util.ColorInfo(requirements.Cluster.ClusterName))
    39  	args := []string{"utils", "associate-iam-oidc-provider", "--cluster", requirements.Cluster.ClusterName, "--region", requirements.Cluster.Region, "--approve"}
    40  	err := executeEksctlCommand(args)
    41  	if err != nil {
    42  		return errors.Wrap(err, "there was a porblem enabling IRSA in the cluster")
    43  	}
    44  	return nil
    45  }
    46  
    47  // CreateIRSAManagedServiceAccounts takes the KubeProviders directory and the requirements configuration and creates
    48  // new ServiceAccounts annotated with a role ARN that is generated by eksctl. The policies attached to these roles
    49  // are defined in the jenkinsx-policies.yml file within kubeProviders/eks/templates
    50  // Note: this can't yet be executed in the master pipeline of the Dev Environment because in order to recreate the
    51  // ServiceAccounts, we need to delete them and the roles first, which causes the next commands to fail
    52  func CreateIRSAManagedServiceAccounts(requirements *config.RequirementsConfig, kubeProvidersDir string) error {
    53  	templateValues, err := createPoliciesStack(requirements, kubeProvidersDir)
    54  	if err != nil {
    55  		return errors.Wrap(err, "there was a problem creating the policies stack and returning the template values")
    56  	}
    57  
    58  	processedTemplateFile, err := processIRSATemplateWithValues(requirements, kubeProvidersDir, templateValues)
    59  	if err != nil {
    60  		return errors.Wrap(err, "there was a problem processing the IRSA template with the provided values")
    61  	}
    62  	defer util.DeleteFile(processedTemplateFile.Name()) //nolint:errcheck
    63  
    64  	err = deleteIAMServiceAccount(processedTemplateFile)
    65  	if err != nil {
    66  		return errors.Wrap(err, "failure creating the IRSA managed service accounts")
    67  	}
    68  
    69  	err = executeIRSAConfigFile(processedTemplateFile)
    70  	if err != nil {
    71  		return errors.Wrap(err, "failure creating the IRSA managed service accounts")
    72  	}
    73  	return nil
    74  }
    75  
    76  // createPoliciesStack reads the jenkinsx-policies.yml CloudFormation stack template and executes it, providing a
    77  // random UUID as a parameter and extracting the outputs of the stack, removing the suffix from them and adding them to
    78  // the returned map so it can be used as parameters for the Go Template irsa.tmpl.yaml
    79  func createPoliciesStack(requirements *config.RequirementsConfig, kubeProvidersDir string) (map[string]interface{}, error) {
    80  	eksKubeProviderDir := filepath.Join(kubeProvidersDir, cloud.EKS, ConfigTemplatesFolder)
    81  	session, err := session2.NewAwsSession("", requirements.Cluster.Region)
    82  	if err != nil {
    83  		return nil, errors.Wrap(err, "error creating a new AWS Session")
    84  	}
    85  	cfn := cloudformation.New(session)
    86  	policiesTemplate, err := ioutil.ReadFile(filepath.Join(eksKubeProviderDir, PoliciesTemplateName))
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	suffix := uuid.New().String()
    91  	describeInput := &cloudformation.DescribeStacksInput{
    92  		StackName: aws.String(fmt.Sprintf("JenkinsXPolicies-%s", suffix)),
    93  	}
    94  
    95  	log.Logger().Infof("Creating CloudFormation stack %s", util.ColorInfo(*describeInput.StackName))
    96  	_, err = cfn.CreateStack(&cloudformation.CreateStackInput{
    97  		Capabilities: []*string{aws.String("CAPABILITY_NAMED_IAM")},
    98  		StackName:    describeInput.StackName,
    99  		Tags: []*cloudformation.Tag{{
   100  			Key:   aws.String("CreatedBy"),
   101  			Value: aws.String("Jenkins-x"),
   102  		}},
   103  		Parameters: []*cloudformation.Parameter{
   104  			{
   105  				ParameterKey:   aws.String("PoliciesSuffixParameter"),
   106  				ParameterValue: aws.String(suffix),
   107  			},
   108  		},
   109  		TemplateBody: aws.String(string(policiesTemplate)),
   110  	})
   111  	if err != nil {
   112  		return nil, errors.Wrapf(err, "there was a problem creating the %s CloudFormation stack", *describeInput.StackName)
   113  	}
   114  
   115  	log.Logger().Infof("Waiting until CloudFormation stack %s is created", util.ColorInfo(*describeInput.StackName))
   116  	err = cfn.WaitUntilStackCreateComplete(describeInput)
   117  	if err != nil {
   118  		return nil, errors.Wrapf(err, "there was a problem waiting for the %s CloudFormation stack to be created", *describeInput.StackName)
   119  	}
   120  
   121  	log.Logger().Infof("Describing stack %s to extract outputs", util.ColorInfo(*describeInput.StackName))
   122  	describeOutput, err := cfn.DescribeStacks(describeInput)
   123  	if err != nil {
   124  		return nil, errors.Wrapf(err, "there was a problem describing the %s CloudFormation stack to extract the outputs", *describeInput.StackName)
   125  	}
   126  
   127  	templateValues := make(map[string]interface{})
   128  	if len(describeOutput.Stacks) > 0 {
   129  		outputs := describeOutput.Stacks[0].Outputs
   130  		log.Logger().Debugf("Exported Outputs from stack %s:", util.ColorInfo(*describeInput.StackName))
   131  		for _, value := range outputs {
   132  			log.Logger().Debugf("ExportName: %s, Value: %s", util.ColorInfo(*value.ExportName), util.ColorInfo(*value.OutputValue))
   133  			exportName := strings.Replace(*value.ExportName, "-"+suffix, "", -1)
   134  			templateValues[exportName] = *value.OutputValue
   135  		}
   136  	}
   137  	return templateValues, nil
   138  }
   139  
   140  // processIRSATemplateWithValues processes the template irsa.tmpl.yaml using the Go templates API with the provided templateValues which will be added
   141  // with the IAM key so it can be referenced in the template
   142  func processIRSATemplateWithValues(requirements *config.RequirementsConfig, kubeProvidersDir string, templateValues map[string]interface{}) (*os.File, error) {
   143  	templatePath := filepath.Join(kubeProvidersDir, cloud.EKS, ConfigTemplatesFolder, IRSATemplateName)
   144  	tmpl, err := template.New(IRSATemplateName).Option("missingkey=error").Funcs(helm.NewFunctionMap()).ParseFiles(templatePath)
   145  	if err != nil {
   146  		return nil, errors.Wrapf(err, "failed to parse Secrets template: %s", templatePath)
   147  	}
   148  
   149  	requirementsMap, err := requirements.ToMap()
   150  	if err != nil {
   151  		return nil, errors.Wrapf(err, "failed turn requirements into a map: %+v", requirements)
   152  	}
   153  
   154  	templateData := map[string]interface{}{
   155  		"Requirements": chartutil.Values(requirementsMap),
   156  		"IAM":          chartutil.Values(templateValues),
   157  	}
   158  	var buf bytes.Buffer
   159  	err = tmpl.Execute(&buf, templateData)
   160  	if err != nil {
   161  		return nil, errors.Wrapf(err, "failed to execute Secrets template: %s", templatePath)
   162  	}
   163  
   164  	f, err := ioutil.TempFile("", "irsa-template-")
   165  	if err != nil {
   166  		return nil, errors.Wrap(err, "there was a problem creating a temp file for the IRSA template")
   167  	}
   168  	_, err = f.Write(buf.Bytes())
   169  	if err != nil {
   170  		return nil, errors.Wrap(err, "there was a problem writing the IRSA template to the temp file")
   171  	}
   172  
   173  	return f, nil
   174  }
   175  
   176  func executeEksctlCommand(args []string) error {
   177  	eksCtlInfo := util.ColorInfo("eksctl")
   178  	log.Logger().Debugf("executing \"%s %s\"", eksCtlInfo, util.ColorInfo(strings.Join(args, " ")))
   179  	cmd := util.Command{
   180  		Name: "eksctl",
   181  		Args: args,
   182  		Out:  os.Stdout,
   183  		Err:  os.Stderr,
   184  	}
   185  	_, err := cmd.RunWithoutRetry()
   186  	if err != nil {
   187  		return errors.Wrapf(err, "there was a problem calling eksctl with the provided args")
   188  	}
   189  	return nil
   190  }
   191  
   192  func executeIRSAConfigFile(file *os.File) error {
   193  	log.Logger().Info("Creating IRSA ServiceAccounts")
   194  	args := []string{"create", "iamserviceaccount",
   195  		"--override-existing-serviceaccounts",
   196  		"--config-file", file.Name(),
   197  		"--include=\"*\"",
   198  		"--approve"}
   199  	err := executeEksctlCommand(args)
   200  	if err != nil {
   201  		return errors.Wrap(err, "there was a problem executing the IRSA ConfigFile")
   202  	}
   203  	return nil
   204  }
   205  
   206  func deleteIAMServiceAccount(file *os.File) error {
   207  	log.Logger().Info("Deleting IRSA ServiceAccounts")
   208  	args := []string{"delete", "iamserviceaccount",
   209  		"--config-file", file.Name(),
   210  		"--include=\"*\"",
   211  		"--approve",
   212  		"--wait"}
   213  	err := executeEksctlCommand(args)
   214  	if err != nil {
   215  		return errors.Wrapf(err, "there was a problem deleting IAM ServiceAccounts")
   216  	}
   217  	return nil
   218  }