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 }