Skip to content

Commit

Permalink
OCM-6543 | feat: detach cmd to detach policy
Browse files Browse the repository at this point in the history
Signed-off-by: marcolan018 <llan@redhat.com>
  • Loading branch information
marcolan018 committed Apr 25, 2024
1 parent 93a653f commit 546aee3
Show file tree
Hide file tree
Showing 9 changed files with 461 additions and 41 deletions.
26 changes: 14 additions & 12 deletions cmd/attach/policy/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ var _ = Describe("rosa attach policy", func() {
roleName = "sample-role"
policyArn1 = "sample-policy-arn-1"
policyArn2 = "sample-policy-arn-2"
policyArn3 = "sample-policy-arn-3"

roleNotFoundMsg = "roleNotFoundMsg"
policyNotFoundMsg = "policyNotFoundMsg"
Expand Down Expand Up @@ -133,31 +134,32 @@ var _ = Describe("rosa attach policy", func() {
runner := AttachPolicyRunner(options)
err := runner(context.Background(), t.RosaRuntime, c, nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal("Cannot attach policies to non-ROSA roles"))
Expect(err.Error()).To(Equal("Cannot attach/detach policies to non-ROSA roles"))
})

It("Returns an error if exceeds policy quota per role", func() {
It("Returns an error if one policy does not exist", func() {
mockClient.EXPECT().GetRoleByName(roleName).Return(*role, nil)
mockClient.EXPECT().GetIAMServiceQuota(policy.QuotaCode).Return(quota, nil)
mockClient.EXPECT().GetAttachedPolicy(aws.String(roleName)).Return([]mock.PolicyDetail{}, nil)
options.policyArns = options.policyArns + ",sample-policy-3"
mockClient.EXPECT().IsPolicyExists(policyArn1).Return(nil, fmt.Errorf(policyNotFoundMsg))
runner := AttachPolicyRunner(options)
err := runner(context.Background(), t.RosaRuntime, c, nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal(fmt.Sprintf("Failed to attach policies due to quota limitations"+
" (total limit: %d, expected: %d)", 2, 3)))
Expect(err.Error()).To(Equal(fmt.Sprintf(
"Failed to find the policy %s: %s", policyArn1, policyNotFoundMsg)))
})

It("Returns an error if one policy does not exist", func() {
It("Returns an error if exceeds policy quota per role", func() {
mockClient.EXPECT().GetRoleByName(roleName).Return(*role, nil)
mockClient.EXPECT().GetIAMServiceQuota(policy.QuotaCode).Return(quota, nil)
mockClient.EXPECT().IsPolicyExists(policyArn1).Return(nil, nil)
mockClient.EXPECT().IsPolicyExists(policyArn2).Return(nil, nil)
mockClient.EXPECT().IsPolicyExists(policyArn3).Return(nil, nil)
mockClient.EXPECT().GetAttachedPolicy(aws.String(roleName)).Return([]mock.PolicyDetail{}, nil)
mockClient.EXPECT().IsPolicyExists(policyArn1).Return(nil, fmt.Errorf(policyNotFoundMsg))
mockClient.EXPECT().GetIAMServiceQuota(policy.QuotaCode).Return(quota, nil)
options.policyArns = options.policyArns + "," + policyArn3
runner := AttachPolicyRunner(options)
err := runner(context.Background(), t.RosaRuntime, c, nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal(fmt.Sprintf(
"Failed to find the policy %s: %s", policyArn1, policyNotFoundMsg)))
Expect(err.Error()).To(Equal(fmt.Sprintf("Failed to attach policies due to quota limitations"+
" (total limit: %d, expected: %d)", 2, 3)))
})

It("Attach policy to role", func() {
Expand Down
34 changes: 34 additions & 0 deletions cmd/detach/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
Copyright (c) 2024 Red Hat, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package detach

import (
"github.com/spf13/cobra"

policy "github.com/openshift/rosa/cmd/detach/policy"
)

func NewRosaDetachCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "detach",
Short: "Detach AWS resource",
Long: "Detach AWS resource",
Args: cobra.NoArgs,
}
cmd.AddCommand(policy.NewDetachPolicyCommand())
return cmd
}
133 changes: 133 additions & 0 deletions cmd/detach/policy/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
Copyright (c) 2024 Red Hat, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package policy

import (
"context"
"fmt"
"strings"

"github.com/spf13/cobra"

"github.com/openshift/rosa/pkg/interactive"
"github.com/openshift/rosa/pkg/policy"
"github.com/openshift/rosa/pkg/rosa"
)

const (
use = "policy"
short = "Detach AWS IAM Policies from an AWS IAM Role"
long = "Detach AWS IAM Policies from an AWS IAM Role in the authenticated AWS Account"
example = ` # Detach policy <policy_arn_1> and <policy_arn_2> from role <role_name>
rosa detach policy --role-arn=<role_name> --policy-arns=<policy_arn_1>,<policy_arn_2>`
)

type RosaDetachPolicyOptions struct {
policyArns string
roleName string
}

func NewRosaDetachPolicyOptions() RosaDetachPolicyOptions {
return RosaDetachPolicyOptions{}
}

func NewDetachPolicyCommand() *cobra.Command {
options := NewRosaDetachPolicyOptions()
cmd := &cobra.Command{
Use: use,
Short: short,
Long: long,
Example: example,
Args: cobra.NoArgs,
Run: rosa.DefaultRunner(rosa.RuntimeWithOCMAndAWS(), DetachPolicyRunner(&options)),
}

flags := cmd.Flags()
flags.StringVarP(
&options.policyArns,
"policy-arns",
"p",
"",
"Policy arn of the policies to be detached from the specified role."+
" Format should be a comma-separated list. (required).",
)
flags.StringVarP(
&options.roleName,
"role-name",
"r",
"",
"Role name of the role to detach the specified policy (required).",
)
cmd.MarkFlagRequired("policy-arns")
cmd.MarkFlagRequired("role-name")
interactive.AddModeFlag(cmd)
return cmd
}

func DetachPolicyRunner(userOptions *RosaDetachPolicyOptions) rosa.CommandRunner {
return func(_ context.Context, r *rosa.Runtime, cmd *cobra.Command, _ []string) error {
options := NewRosaDetachPolicyOptions()
options.BindAndValidate(*userOptions)
policySvc := policy.NewPolicyService(r.OCMClient, r.AWSClient)
policyArns := strings.Split(options.policyArns, ",")
err := policySvc.ValidateDetachOptions(options.roleName, policyArns)
if err != nil {
return err
}

mode, err := interactive.GetMode()
if err != nil {
return err
}
// Determine if interactive mode is needed
if !interactive.Enabled() && !cmd.Flags().Changed("mode") {
interactive.Enable()
}
if interactive.Enabled() {
mode, err = interactive.GetOptionMode(cmd, mode, "Detach policy mode")
if err != nil {
return fmt.Errorf("Expected a valid detach policy mode: %s", err)
}
}

orgID, _, err := r.OCMClient.GetCurrentOrganization()
if err != nil {
return fmt.Errorf("Failed to get current organization: %s", err)
}
switch mode {
case interactive.ModeAuto:
output, err := policySvc.AutoDetachArbitraryPolicy(options.roleName, policyArns,
r.Creator.AccountID, orgID)
r.Reporter.Infof(output)
if err != nil {
return err
}
case interactive.ModeManual:
r.Reporter.Infof("Run the following command to detach the policy:")
fmt.Print(policySvc.ManualDetachArbitraryPolicy(options.roleName, policyArns,
r.Creator.AccountID, orgID))
default:
return fmt.Errorf("Invalid mode. Allowed values are %s", interactive.Modes)
}
return nil
}
}

func (o *RosaDetachPolicyOptions) BindAndValidate(options RosaDetachPolicyOptions) {
o.policyArns = options.policyArns
o.roleName = options.roleName
}
151 changes: 151 additions & 0 deletions cmd/detach/policy/cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
Copyright (c) 2024 Red Hat, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package policy

import (
"context"
"fmt"
"net/http"
"testing"

"go.uber.org/mock/gomock"

"github.com/aws/aws-sdk-go-v2/aws"
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
amsv1 "github.com/openshift-online/ocm-sdk-go/accountsmgmt/v1"
. "github.com/openshift-online/ocm-sdk-go/testing"
"github.com/spf13/cobra"

mock "github.com/openshift/rosa/pkg/aws"
"github.com/openshift/rosa/pkg/aws/tags"
. "github.com/openshift/rosa/pkg/test"
)

func TestDetachPolicy(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "rosa detach policy")
}

var _ = Describe("rosa detach policy", func() {
Context("Create Command", func() {
It("Returns Command", func() {

cmd := NewDetachPolicyCommand()
Expect(cmd).NotTo(BeNil())

Expect(cmd.Use).To(Equal(use))
Expect(cmd.Example).To(Equal(example))
Expect(cmd.Short).To(Equal(short))
Expect(cmd.Long).To(Equal(long))
Expect(cmd.Args).NotTo(BeNil())
Expect(cmd.Run).NotTo(BeNil())

flag := cmd.Flags().Lookup("role-name")
Expect(flag).NotTo(BeNil())

flag = cmd.Flags().Lookup("policy-arns")
Expect(flag).NotTo(BeNil())
})
})

Context("Execute command", func() {

const (
roleName = "sample-role"
policyArn1 = "sample-policy-arn-1"
policyArn2 = "sample-policy-arn-2"

roleNotFoundMsg = "roleNotFoundMsg"
policyNotFoundMsg = "policyNotFoundMsg"
)

var (
t *TestingRuntime
c *cobra.Command
mockClient *mock.MockClient
options *RosaDetachPolicyOptions
role *iamtypes.Role
)

BeforeEach(func() {
c = NewDetachPolicyCommand()
options = &RosaDetachPolicyOptions{
roleName: roleName,
policyArns: policyArn1 + "," + policyArn2,
}
c.Flags().Set("mode", "auto")
role = &iamtypes.Role{
Tags: []iamtypes.Tag{
{
Key: aws.String(tags.RedHatManaged),
Value: aws.String("true"),
},
},
}

t = NewTestRuntime()
mockCtrl := gomock.NewController(GinkgoT())
mockClient = mock.NewMockClient(mockCtrl)
t.RosaRuntime.AWSClient = mockClient

account, _ := amsv1.NewAccount().Organization(amsv1.NewOrganization().
ID("123").ExternalID("456")).Build()
t.ApiServer.AppendHandlers(RespondWithJSON(http.StatusOK, FormatResource(account)))

})

It("Returns an error if the role does not exist", func() {
mockClient.EXPECT().GetRoleByName(roleName).Return(iamtypes.Role{}, fmt.Errorf(roleNotFoundMsg))
runner := DetachPolicyRunner(options)
err := runner(context.Background(), t.RosaRuntime, c, nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal(fmt.Sprintf(
"Failed to find the role %s: %s", roleName, roleNotFoundMsg)))
})

It("Returns an error if the role does not has tag 'red-hat-managed'", func() {
mockClient.EXPECT().GetRoleByName(roleName).Return(iamtypes.Role{}, nil)
runner := DetachPolicyRunner(options)
err := runner(context.Background(), t.RosaRuntime, c, nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal("Cannot attach/detach policies to non-ROSA roles"))
})

It("Returns an error if one policy does not exist", func() {
mockClient.EXPECT().GetRoleByName(roleName).Return(*role, nil)
mockClient.EXPECT().IsPolicyExists(policyArn1).Return(nil, fmt.Errorf(policyNotFoundMsg))
runner := DetachPolicyRunner(options)
err := runner(context.Background(), t.RosaRuntime, c, nil)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(Equal(fmt.Sprintf(
"Failed to find the policy %s: %s", policyArn1, policyNotFoundMsg)))
})

It("Detach policy from role", func() {
mockClient.EXPECT().GetRoleByName(roleName).Return(*role, nil)
mockClient.EXPECT().IsPolicyExists(policyArn1).Return(nil, nil)
mockClient.EXPECT().IsPolicyExists(policyArn2).Return(nil, nil)
mockClient.EXPECT().DetachRolePolicy(policyArn1, roleName).Return(nil)
mockClient.EXPECT().DetachRolePolicy(policyArn2, roleName).Return(nil)
runner := DetachPolicyRunner(options)
err := runner(context.Background(), t.RosaRuntime, c, nil)
Expect(err).NotTo(HaveOccurred())
})

})
})
2 changes: 2 additions & 0 deletions cmd/rosa/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/openshift/rosa/cmd/config"
"github.com/openshift/rosa/cmd/create"
"github.com/openshift/rosa/cmd/describe"
"github.com/openshift/rosa/cmd/detach"
"github.com/openshift/rosa/cmd/dlt"
"github.com/openshift/rosa/cmd/docs"
"github.com/openshift/rosa/cmd/download"
Expand Down Expand Up @@ -103,6 +104,7 @@ func init() {
root.AddCommand(token.Cmd)
root.AddCommand(config.Cmd)
root.AddCommand(attach.NewRosaAttachCommand())
root.AddCommand(detach.NewRosaDetachCommand())
}

func main() {
Expand Down
Loading

0 comments on commit 546aee3

Please sign in to comment.