github.com/openshift/installer@v1.4.17/pkg/infrastructure/aws/clusterapi/iam.go (about)

     1  package clusterapi
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     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/aws/endpoints"
    12  	"github.com/aws/aws-sdk-go/service/iam"
    13  	"github.com/sirupsen/logrus"
    14  	iamv1 "sigs.k8s.io/cluster-api-provider-aws/v2/iam/api/v1beta1"
    15  
    16  	"github.com/openshift/installer/pkg/asset/installconfig"
    17  )
    18  
    19  const (
    20  	master = "master"
    21  	worker = "worker"
    22  )
    23  
    24  var (
    25  	policies = map[string]*iamv1.PolicyDocument{
    26  		master: {
    27  			Version: "2012-10-17",
    28  			Statement: []iamv1.StatementEntry{
    29  				{
    30  					Effect: "Allow",
    31  					Action: []string{
    32  						"ec2:AttachVolume",
    33  						"ec2:AuthorizeSecurityGroupIngress",
    34  						"ec2:CreateSecurityGroup",
    35  						"ec2:CreateTags",
    36  						"ec2:CreateVolume",
    37  						"ec2:DeleteSecurityGroup",
    38  						"ec2:DeleteVolume",
    39  						"ec2:Describe*",
    40  						"ec2:DetachVolume",
    41  						"ec2:ModifyInstanceAttribute",
    42  						"ec2:ModifyVolume",
    43  						"ec2:RevokeSecurityGroupIngress",
    44  						"elasticloadbalancing:AddTags",
    45  						"elasticloadbalancing:AttachLoadBalancerToSubnets",
    46  						"elasticloadbalancing:ApplySecurityGroupsToLoadBalancer",
    47  						"elasticloadbalancing:CreateListener",
    48  						"elasticloadbalancing:CreateLoadBalancer",
    49  						"elasticloadbalancing:CreateLoadBalancerPolicy",
    50  						"elasticloadbalancing:CreateLoadBalancerListeners",
    51  						"elasticloadbalancing:CreateTargetGroup",
    52  						"elasticloadbalancing:ConfigureHealthCheck",
    53  						"elasticloadbalancing:DeleteListener",
    54  						"elasticloadbalancing:DeleteLoadBalancer",
    55  						"elasticloadbalancing:DeleteLoadBalancerListeners",
    56  						"elasticloadbalancing:DeleteTargetGroup",
    57  						"elasticloadbalancing:DeregisterInstancesFromLoadBalancer",
    58  						"elasticloadbalancing:DeregisterTargets",
    59  						"elasticloadbalancing:Describe*",
    60  						"elasticloadbalancing:DetachLoadBalancerFromSubnets",
    61  						"elasticloadbalancing:ModifyListener",
    62  						"elasticloadbalancing:ModifyLoadBalancerAttributes",
    63  						"elasticloadbalancing:ModifyTargetGroup",
    64  						"elasticloadbalancing:ModifyTargetGroupAttributes",
    65  						"elasticloadbalancing:RegisterInstancesWithLoadBalancer",
    66  						"elasticloadbalancing:RegisterTargets",
    67  						"elasticloadbalancing:SetLoadBalancerPoliciesForBackendServer",
    68  						"elasticloadbalancing:SetLoadBalancerPoliciesOfListener",
    69  						"kms:DescribeKey",
    70  					},
    71  					Resource: iamv1.Resources{
    72  						"*",
    73  					},
    74  				},
    75  			},
    76  		},
    77  		worker: {
    78  			Version: "2012-10-17",
    79  			Statement: []iamv1.StatementEntry{
    80  				{
    81  					Effect: "Allow",
    82  					Action: iamv1.Actions{
    83  						"ec2:DescribeInstances",
    84  						"ec2:DescribeRegions",
    85  					},
    86  					Resource: iamv1.Resources{"*"},
    87  				},
    88  			},
    89  		},
    90  	}
    91  )
    92  
    93  // createIAMRoles creates the roles used by control-plane and compute nodes.
    94  func createIAMRoles(ctx context.Context, infraID string, ic *installconfig.InstallConfig) error {
    95  	logrus.Infoln("Reconciling IAM roles for control-plane and compute nodes")
    96  	// Create the IAM Role with the aws sdk.
    97  	// https://docs.aws.amazon.com/sdk-for-go/api/service/iam/#IAM.CreateRole
    98  	session, err := ic.AWS.Session(ctx)
    99  	if err != nil {
   100  		return fmt.Errorf("failed to load AWS session: %w", err)
   101  	}
   102  	svc := iam.New(session)
   103  
   104  	// Create the IAM Roles for master and workers.
   105  	tags := []*iam.Tag{
   106  		{
   107  			Key:   aws.String(fmt.Sprintf("kubernetes.io/cluster/%s", infraID)),
   108  			Value: aws.String("owned"),
   109  		},
   110  	}
   111  
   112  	for k, v := range ic.Config.AWS.UserTags {
   113  		tags = append(tags, &iam.Tag{
   114  			Key:   aws.String(k),
   115  			Value: aws.String(v),
   116  		})
   117  	}
   118  
   119  	assumePolicy := &iamv1.PolicyDocument{
   120  		Version: "2012-10-17",
   121  		Statement: iamv1.Statements{
   122  			{
   123  				Effect: "Allow",
   124  				Principal: iamv1.Principals{
   125  					iamv1.PrincipalService: []string{
   126  						getPartitionService(ic.AWS.Region),
   127  					},
   128  				},
   129  				Action: iamv1.Actions{
   130  					"sts:AssumeRole",
   131  				},
   132  			},
   133  		},
   134  	}
   135  	assumePolicyBytes, err := json.Marshal(assumePolicy)
   136  	if err != nil {
   137  		return fmt.Errorf("failed to marshal assume policy: %w", err)
   138  	}
   139  
   140  	var defaultProfile string
   141  	if dmp := ic.Config.AWS.DefaultMachinePlatform; dmp != nil && len(dmp.IAMProfile) > 0 {
   142  		defaultProfile = dmp.IAMProfile
   143  	}
   144  
   145  	for _, role := range []string{master, worker} {
   146  		instanceProfile := defaultProfile
   147  		switch role {
   148  		case master:
   149  			if cp := ic.Config.ControlPlane; cp != nil && cp.Platform.AWS != nil && len(cp.Platform.AWS.IAMProfile) > 0 {
   150  				instanceProfile = cp.Platform.AWS.IAMProfile
   151  			}
   152  		case worker:
   153  			if w := ic.Config.Compute; len(w) > 0 && w[0].Platform.AWS != nil && len(w[0].Platform.AWS.IAMProfile) > 0 {
   154  				instanceProfile = w[0].Platform.AWS.IAMProfile
   155  			}
   156  		}
   157  
   158  		// A user-provided instance profile already has a role attached to it, so there is nothing else for the
   159  		// Installer to do.
   160  		if len(instanceProfile) > 0 {
   161  			logrus.Debugf("Using existing %s instance profile %q", role, instanceProfile)
   162  			continue
   163  		}
   164  
   165  		roleName, err := getOrCreateIAMRole(ctx, role, infraID, string(assumePolicyBytes), *ic, tags, svc)
   166  		if err != nil {
   167  			return fmt.Errorf("failed to create IAM %s role: %w", role, err)
   168  		}
   169  
   170  		profileName := aws.String(fmt.Sprintf("%s-%s-profile", infraID, role))
   171  		if _, err := svc.GetInstanceProfileWithContext(ctx, &iam.GetInstanceProfileInput{InstanceProfileName: profileName}); err != nil {
   172  			var awsErr awserr.Error
   173  			if errors.As(err, &awsErr) && awsErr.Code() != iam.ErrCodeNoSuchEntityException {
   174  				return fmt.Errorf("failed to get %s instance profile: %w", role, err)
   175  			}
   176  			// If the profile does not exist, create it.
   177  			if _, err := svc.CreateInstanceProfileWithContext(ctx, &iam.CreateInstanceProfileInput{
   178  				InstanceProfileName: profileName,
   179  				Tags:                tags,
   180  			}); err != nil {
   181  				return fmt.Errorf("failed to create %s instance profile: %w", role, err)
   182  			}
   183  			if err := svc.WaitUntilInstanceProfileExistsWithContext(ctx, &iam.GetInstanceProfileInput{InstanceProfileName: profileName}); err != nil {
   184  				return fmt.Errorf("failed to wait for %s instance profile to exist: %w", role, err)
   185  			}
   186  
   187  			// Finally, attach the role to the profile.
   188  			if _, err := svc.AddRoleToInstanceProfileWithContext(ctx, &iam.AddRoleToInstanceProfileInput{
   189  				InstanceProfileName: profileName,
   190  				RoleName:            aws.String(roleName),
   191  			}); err != nil {
   192  				return fmt.Errorf("failed to add %s role to instance profile: %w", role, err)
   193  			}
   194  		}
   195  	}
   196  
   197  	return nil
   198  }
   199  
   200  // getOrCreateRole returns the name of the IAM role to be used,
   201  // creating it when not specified by the user in the install config.
   202  func getOrCreateIAMRole(ctx context.Context, nodeRole, infraID, assumePolicy string, ic installconfig.InstallConfig, tags []*iam.Tag, svc *iam.IAM) (string, error) {
   203  	roleName := aws.String(fmt.Sprintf("%s-%s-role", infraID, nodeRole))
   204  
   205  	var defaultRole string
   206  	if dmp := ic.Config.AWS.DefaultMachinePlatform; dmp != nil && len(dmp.IAMRole) > 0 {
   207  		defaultRole = dmp.IAMRole
   208  	}
   209  
   210  	masterRole := defaultRole
   211  	if cp := ic.Config.ControlPlane; cp != nil && cp.Platform.AWS != nil && len(cp.Platform.AWS.IAMRole) > 0 {
   212  		masterRole = cp.Platform.AWS.IAMRole
   213  	}
   214  
   215  	workerRole := defaultRole
   216  	if w := ic.Config.Compute; len(w) > 0 && w[0].Platform.AWS != nil && len(w[0].Platform.AWS.IAMRole) > 0 {
   217  		workerRole = w[0].Platform.AWS.IAMRole
   218  	}
   219  
   220  	switch {
   221  	case nodeRole == master && len(masterRole) > 0:
   222  		return masterRole, nil
   223  	case nodeRole == worker && len(workerRole) > 0:
   224  		return workerRole, nil
   225  	}
   226  
   227  	if _, err := svc.GetRoleWithContext(ctx, &iam.GetRoleInput{RoleName: roleName}); err != nil {
   228  		var awsErr awserr.Error
   229  		if errors.As(err, &awsErr) && awsErr.Code() != iam.ErrCodeNoSuchEntityException {
   230  			return "", fmt.Errorf("failed to get %s role: %w", nodeRole, err)
   231  		}
   232  		// If the role does not exist, create it.
   233  		logrus.Infof("Creating IAM role for %s", nodeRole)
   234  		createRoleInput := &iam.CreateRoleInput{
   235  			RoleName:                 roleName,
   236  			AssumeRolePolicyDocument: aws.String(assumePolicy),
   237  			Tags:                     tags,
   238  		}
   239  		if _, err := svc.CreateRoleWithContext(ctx, createRoleInput); err != nil {
   240  			return "", fmt.Errorf("failed to create %s role: %w", nodeRole, err)
   241  		}
   242  
   243  		if err := svc.WaitUntilRoleExistsWithContext(ctx, &iam.GetRoleInput{RoleName: roleName}); err != nil {
   244  			return "", fmt.Errorf("failed to wait for %s role to exist: %w", nodeRole, err)
   245  		}
   246  	}
   247  
   248  	// Put the policy inline.
   249  	policyName := aws.String(fmt.Sprintf("%s-%s-policy", infraID, nodeRole))
   250  	b, err := json.Marshal(policies[nodeRole])
   251  	if err != nil {
   252  		return "", fmt.Errorf("failed to marshal %s policy: %w", nodeRole, err)
   253  	}
   254  	if _, err := svc.PutRolePolicyWithContext(ctx, &iam.PutRolePolicyInput{
   255  		PolicyDocument: aws.String(string(b)),
   256  		PolicyName:     policyName,
   257  		RoleName:       roleName,
   258  	}); err != nil {
   259  		return "", fmt.Errorf("failed to create inline policy for role %s: %w", nodeRole, err)
   260  	}
   261  
   262  	return *roleName, nil
   263  }
   264  
   265  func getPartitionService(region string) string {
   266  	partitionDNSSuffix := "amazonaws.com"
   267  	if ps, found := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), region); found {
   268  		partitionDNSSuffix = ps.DNSSuffix()
   269  	}
   270  	return fmt.Sprintf("ec2.%s", partitionDNSSuffix)
   271  }