Skip to content

Commit

Permalink
OCM-12362 | feat: Add HCP shared-vpc support to accountroles
Browse files Browse the repository at this point in the history
  • Loading branch information
hunterkepley committed Nov 19, 2024
1 parent 7c02b05 commit c4fad7a
Show file tree
Hide file tree
Showing 18 changed files with 372 additions and 154 deletions.
35 changes: 34 additions & 1 deletion cmd/create/accountroles/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ import (
"github.com/openshift/rosa/pkg/rosa"
)

const (
route53RoleArnFlag = "route53-role-arn"
vpcEndpointRoleArnFlag = "vpc-endpoint-role-arn"
)

var args struct {
prefix string
permissionsBoundary string
Expand All @@ -42,6 +47,8 @@ var args struct {
forcePolicyCreation bool
hostedCP bool
classic bool
route53RoleArn string
vpcEndpointRoleArn string
}

var Cmd = &cobra.Command{
Expand Down Expand Up @@ -133,6 +140,22 @@ func init() {
"Create only classic Rosa account roles",
)

flags.StringVar(
&args.route53RoleArn,
route53RoleArnFlag,
"",
"Role ARN associated with the private hosted zone used for Hosted Control Plane cluster shared VPC, this "+
"role contains policies to be used with Route 53",
)

flags.StringVar(
&args.vpcEndpointRoleArn,
vpcEndpointRoleArnFlag,
"",
"Role ARN associated with the shared VPC used for Hosted Control Plane clusters, this role contains "+
"policies to be used with the VPC endpoint",
)

interactive.AddModeFlag(Cmd)

confirm.AddFlag(flags)
Expand All @@ -148,6 +171,16 @@ func run(cmd *cobra.Command, argv []string) {
os.Exit(1)
}

if !cmd.Flag("hosted-cp").Changed {
rosa.HostedClusterOnlyFlag(r, cmd, route53RoleArnFlag)
rosa.HostedClusterOnlyFlag(r, cmd, vpcEndpointRoleArnFlag)
}
isHcpSharedVpc, err := validateSharedVpcInputs(args.hostedCP, args.vpcEndpointRoleArn, args.route53RoleArn)
if err != nil {
r.Reporter.Errorf("%s", err)
os.Exit(1)
}

// If necessary, call `login` as part of `init`. We do this before
// other validations to get the prompt out of the way before performing
// longer checks.
Expand Down Expand Up @@ -383,7 +416,7 @@ func run(cmd *cobra.Command, argv []string) {
}

input := buildRolesCreationInput(prefix, permissionsBoundary, r.Creator.AccountID, env, policies,
policyVersion, path)
policyVersion, path, isHcpSharedVpc)

switch mode {
case interactive.ModeAuto:
Expand Down
47 changes: 46 additions & 1 deletion cmd/create/accountroles/creators.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,12 @@ type accountRolesCreationInput struct {
policies map[string]*cmv1.AWSSTSPolicy
defaultPolicyVersion string
path string
isSharedVpc bool
}

func buildRolesCreationInput(prefix, permissionsBoundary, accountID, env string,
policies map[string]*cmv1.AWSSTSPolicy, defaultPolicyVersion string,
path string) *accountRolesCreationInput {
path string, isSharedVpc bool) *accountRolesCreationInput {
return &accountRolesCreationInput{
prefix: prefix,
permissionsBoundary: permissionsBoundary,
Expand All @@ -73,6 +74,7 @@ func buildRolesCreationInput(prefix, permissionsBoundary, accountID, env string,
policies: policies,
defaultPolicyVersion: defaultPolicyVersion,
path: path,
isSharedVpc: isSharedVpc,
}
}

Expand Down Expand Up @@ -322,6 +324,21 @@ func (hcp *hcpManagedPoliciesCreator) createRoles(r *rosa.Runtime, input *accoun
}
r.Reporter.Infof("Created role '%s' with ARN '%s'", accRoleName, roleARN)

if role == aws.HCPAccountRoles[aws.HCPInstallerRole] {
if input.isSharedVpc {
err := attachHcpSharedVpcPolicy(r, args.route53RoleArn, accRoleName,
input.path, input.defaultPolicyVersion)
if err != nil {
return err
}
err = attachHcpSharedVpcPolicy(r, args.vpcEndpointRoleArn, accRoleName,
input.path, input.defaultPolicyVersion)
if err != nil {
return err
}
}
}

policyKeys := aws.GetHcpAccountRolePolicyKeys(file)
for _, policyKey := range policyKeys {
policyARN, err := aws.GetManagedPolicyARN(input.policies, policyKey)
Expand Down Expand Up @@ -434,3 +451,31 @@ func buildAttachRolePolicyCommand(accRoleName string, policyARN string) string {
AddParam(awscb.PolicyArn, policyARN).
Build()
}

func attachHcpSharedVpcPolicy(r *rosa.Runtime, sharedVpcRoleArn string, roleName string,
path string, defaultPolicyVersion string) error {
policyDetails := aws.InterpolatePolicyDocument(r.Creator.Partition, aws.SharedVpcDefaultPolicy, map[string]string{
"shared_vpc_role_arn": sharedVpcRoleArn,
})

policyTags := map[string]string{
tags.RedHatManaged: aws.TrueString,
}

userProvidedRoleName, err := aws.GetResourceIdFromARN(sharedVpcRoleArn)
if err != nil {
return err
}
policyName := fmt.Sprintf(aws.AssumeRolePolicyPrefix, userProvidedRoleName)
policyArn := aws.GetPolicyARN(r.Creator.Partition, r.Creator.AccountID, roleName, path)
policyArn, err = r.AWSClient.EnsurePolicyWithName(policyArn, policyDetails,
defaultPolicyVersion, policyTags, path, policyName)
if err != nil {
return err
}
err = r.AWSClient.AttachRolePolicy(r.Reporter, roleName, policyArn)
if err != nil {
return err
}
return nil
}
37 changes: 34 additions & 3 deletions cmd/create/accountroles/creators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ var _ = Describe("Accountroles", Ordered, func() {
r.AWSClient = mockClient
r.Creator = &mock.Creator{ARN: "arn-123"}
policies := map[string]*cmv1.AWSSTSPolicy{}
accountRolesCreationInput := buildRolesCreationInput("test", "", "account-123", "stage", policies, "", "")
accountRolesCreationInput := buildRolesCreationInput("test", "", "account-123", "stage", policies, "", "", false)
err := (&hcpManagedPoliciesCreator{}).createRoles(r, accountRolesCreationInput)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("failed to find policy ARN for"))
Expand All @@ -49,7 +49,7 @@ var _ = Describe("Accountroles", Ordered, func() {
"sts_hcp_support_permission_policy": supportPolicy,
}

accountRolesCreationInput := buildRolesCreationInput("test", "", "account-123", "stage", policies, "", "")
accountRolesCreationInput := buildRolesCreationInput("test", "", "account-123", "stage", policies, "", "", false)
err := (&hcpManagedPoliciesCreator{}).createRoles(r, accountRolesCreationInput)
Expect(err).ToNot(HaveOccurred())
})
Expand All @@ -75,7 +75,38 @@ var _ = Describe("Accountroles", Ordered, func() {
"sts_hcp_ec2_registry_permission_policy": ec2Policy,
}

accountRolesCreationInput := buildRolesCreationInput("test", "", "account-123", "stage", policies, "", "")
accountRolesCreationInput := buildRolesCreationInput("test", "", "account-123", "stage", policies, "", "", false)
err := (&hcpManagedPoliciesCreator{}).createRoles(r, accountRolesCreationInput)
Expect(err).ToNot(HaveOccurred())
})
It("createRole succeeds with hosted-cp and shared-vpc roles", func() {
mockCtrl := gomock.NewController(GinkgoT())
mockClient := mock.NewMockClient(mockCtrl)
mockClient.EXPECT().AttachRolePolicy(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(6)
mockClient.EXPECT().EnsureRole(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any()).Return("arn::role:role-123", nil).AnyTimes()
mockClient.EXPECT().EnsurePolicyWithName(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(),
gomock.Any(), gomock.Any()).Return("arn::policy:123", nil).Times(2)

r := rosa.NewRuntime()
r.AWSClient = mockClient
r.Creator = &mock.Creator{ARN: "arn-123"}
installerPolicy, _ := (&cmv1.AWSSTSPolicyBuilder{}).ARN("arn::installer").Build()
workerPolicy, _ := (&cmv1.AWSSTSPolicyBuilder{}).ARN("arn::worker").Build()
supportPolicy, _ := (&cmv1.AWSSTSPolicyBuilder{}).ARN("arn::support").Build()
ec2Policy, _ := (&cmv1.AWSSTSPolicyBuilder{}).ARN("arn::ec2").Build()

policies := map[string]*cmv1.AWSSTSPolicy{
"sts_hcp_installer_permission_policy": installerPolicy,
"sts_hcp_instance_worker_permission_policy": workerPolicy,
"sts_hcp_support_permission_policy": supportPolicy,
"sts_hcp_ec2_registry_permission_policy": ec2Policy,
}

accountRolesCreationInput := buildRolesCreationInput("test", "123", "account-123", "stage", policies,
"123", "123", true)
args.route53RoleArn = "arn:aws:iam::123:role/route53"
args.vpcEndpointRoleArn = "arn:aws:iam::123:role/vpce"
err := (&hcpManagedPoliciesCreator{}).createRoles(r, accountRolesCreationInput)
Expect(err).ToNot(HaveOccurred())
})
Expand Down
30 changes: 30 additions & 0 deletions cmd/create/accountroles/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package accountroles

import (
errors "github.com/zgalor/weberr"

"github.com/openshift/rosa/pkg/arguments"
)

func validateSharedVpcInputs(hostedCp bool, vpcEndpointRoleArn string,
route53RoleArn string) (bool, error) {
if hostedCp {
if vpcEndpointRoleArn != "" && route53RoleArn == "" {
return false, errors.UserErrorf(
arguments.MustUseBothFlagsErrorMessage,
route53RoleArnFlag,
vpcEndpointRoleArnFlag,
)
}

if route53RoleArn != "" && vpcEndpointRoleArn == "" {
return false, errors.UserErrorf(
arguments.MustUseBothFlagsErrorMessage,
vpcEndpointRoleArnFlag,
route53RoleArnFlag,
)
}
}

return hostedCp && vpcEndpointRoleArn != "" && route53RoleArn != "", nil
}
59 changes: 59 additions & 0 deletions cmd/create/accountroles/validation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package accountroles

import (
"go.uber.org/mock/gomock"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/openshift/rosa/pkg/arguments"
)

var _ = Describe("Validate Shared VPC Inputs", func() {
var ctrl *gomock.Controller

BeforeEach(func() {
ctrl = gomock.NewController(GinkgoT())
})
AfterEach(func() {
ctrl.Finish()
})

Context("validateSharedVpcInputs", func() {
When("Validate flags properly for shared VPC for HCP op roles", func() {
It("OK: Should pass with no error, for classic (return false)", func() {
usingSharedVpc, err := validateSharedVpcInputs(false, "", "")
Expect(usingSharedVpc).To(BeFalse())
Expect(err).ToNot(HaveOccurred())
})
It("OK: Should pass with no error, for HCP (return false)", func() {
usingSharedVpc, err := validateSharedVpcInputs(true, "", "")
Expect(usingSharedVpc).To(BeFalse())
Expect(err).ToNot(HaveOccurred())
})
It("OK: Should pass with no error, for HCP (return true)", func() {
usingSharedVpc, err := validateSharedVpcInputs(true, "123", "123")
Expect(usingSharedVpc).To(BeTrue())
Expect(err).ToNot(HaveOccurred())
})
It("KO: Should error when using HCP and the first flag but not the second (return false)", func() {
usingSharedVpc, err := validateSharedVpcInputs(true, "123", "")
Expect(usingSharedVpc).To(BeFalse())
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring(arguments.MustUseBothFlagsErrorMessage,
route53RoleArnFlag,
vpcEndpointRoleArnFlag,
))
})
It("KO: Should error when using HCP and the second flag but not the first (return false)", func() {
usingSharedVpc, err := validateSharedVpcInputs(true, "", "123")
Expect(usingSharedVpc).To(BeFalse())
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring(arguments.MustUseBothFlagsErrorMessage,
vpcEndpointRoleArnFlag,
route53RoleArnFlag,
))
})
})
})
})
2 changes: 2 additions & 0 deletions cmd/create/operatorroles/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ func run(cmd *cobra.Command, argv []string) {
r := rosa.NewRuntime().WithAWS().WithOCM()
defer r.Cleanup()

rosa.HostedClusterOnlyFlag(r, cmd, vpcEndpointRoleArnFlag)

// Allow the command to be called programmatically
isProgmaticallyCalled := false
if len(argv) == 3 && !cmd.Flag("cluster").Changed {
Expand Down
17 changes: 8 additions & 9 deletions cmd/create/operatorroles/validation.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
package operatorroles

import errors "github.com/zgalor/weberr"
import (
errors "github.com/zgalor/weberr"

"github.com/openshift/rosa/pkg/arguments"
)

func validateSharedVpcInputs(hostedCp bool, vpcEndpointRoleArn string,
route53RoleArn string) (bool, error) {

if !hostedCp {
if vpcEndpointRoleArn != "" {
return false, errors.UserErrorf("Can only use '%s' flag for Hosted Control Plane operator roles",
vpcEndpointRoleArnFlag)
}
} else {
if hostedCp {
if vpcEndpointRoleArn != "" && route53RoleArn == "" {
return false, errors.UserErrorf(
"Must supply '%s' flag when using the '%s' flag",
arguments.MustUseBothFlagsErrorMessage,
hostedZoneRoleArnFlag,
vpcEndpointRoleArnFlag,
)
}

if route53RoleArn != "" && vpcEndpointRoleArn == "" {
return false, errors.UserErrorf(
"Must supply '%s' flag when using the '%s' flag",
arguments.MustUseBothFlagsErrorMessage,
vpcEndpointRoleArnFlag,
hostedZoneRoleArnFlag,
)
Expand Down
33 changes: 8 additions & 25 deletions cmd/create/operatorroles/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/openshift/rosa/pkg/arguments"
)

var _ = Describe("Create dns domain", func() {
Expand All @@ -29,35 +31,16 @@ var _ = Describe("Create dns domain", func() {
Expect(usingSharedVpc).To(BeFalse())
Expect(err).ToNot(HaveOccurred())
})
It("KO: Should error when using classic and the first flag (return false)", func() {
usingSharedVpc, err := validateSharedVpcInputs(false, "123", "")
Expect(usingSharedVpc).To(BeFalse())
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Can only use '%s' flag for Hosted Control Plane operator "+
"roles", vpcEndpointRoleArnFlag,
))
})
It("KO: Should error when using classic and the vpc endpoint flag (return false)", func() {
usingSharedVpc, err := validateSharedVpcInputs(false, "123", "")
Expect(usingSharedVpc).To(BeFalse())
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Can only use '%s' flag for Hosted Control Plane operator "+
"roles", vpcEndpointRoleArnFlag,
))
})
It("KO: Should error when using classic and both flags (return false)", func() {
usingSharedVpc, err := validateSharedVpcInputs(false, "123", "123")
Expect(usingSharedVpc).To(BeFalse())
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Can only use '%s' flag for Hosted Control Plane operator "+
"roles", vpcEndpointRoleArnFlag,
))
It("OK: Should pass with no error, for HCP (return true)", func() {
usingSharedVpc, err := validateSharedVpcInputs(true, "123", "123")
Expect(usingSharedVpc).To(BeTrue())
Expect(err).ToNot(HaveOccurred())
})
It("KO: Should error when using HCP and the first flag but not the second (return false)", func() {
usingSharedVpc, err := validateSharedVpcInputs(true, "123", "")
Expect(usingSharedVpc).To(BeFalse())
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Must supply '%s' flag when using the '%s' flag",
Expect(err.Error()).To(ContainSubstring(arguments.MustUseBothFlagsErrorMessage,
hostedZoneRoleArnFlag,
vpcEndpointRoleArnFlag,
))
Expand All @@ -66,7 +49,7 @@ var _ = Describe("Create dns domain", func() {
usingSharedVpc, err := validateSharedVpcInputs(true, "", "123")
Expect(usingSharedVpc).To(BeFalse())
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Must supply '%s' flag when using the '%s' flag",
Expect(err.Error()).To(ContainSubstring(arguments.MustUseBothFlagsErrorMessage,
vpcEndpointRoleArnFlag,
hostedZoneRoleArnFlag,
))
Expand Down
Loading

0 comments on commit c4fad7a

Please sign in to comment.