Skip to content

Commit a63c24e

Browse files
SREP-3299: Add readonly flag for backplane login command
1 parent 62a772a commit a63c24e

3 files changed

Lines changed: 125 additions & 12 deletions

File tree

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,17 @@ In this example, we will login to a cluster with id `123456abcdef` in production
115115
```
116116
$ ocm backplane login <cluster> --service
117117
```
118+
119+
- To login with read-only access to the cluster
120+
```
121+
$ ocm backplane login <cluster> --readonly
122+
```
123+
124+
The `--readonly` flag restricts access to read-only operations, preventing any modifications to the cluster. This is useful for:
125+
- Auditing and troubleshooting without risk of making changes
126+
- Providing temporary access with limited privileges
127+
- Ensuring compliance with read-only access policies
128+
118129
### Get cluster information after login
119130

120131
- Login to the target cluster via backplane and add `--cluster-info` flag
@@ -577,11 +588,24 @@ Login to a backplane cluster.
577588
**Parameters:**
578589
- `clusterId` (required): The cluster ID to login to
579590

591+
**CLI Flags:**
592+
- `--readonly`: Login with read-only access (calls `/backplane/login/{clusterId}?readonly=true`)
593+
- `--multi` or `-m`: Enable multi-cluster login
594+
- `--pd <incident-id>`: Login using PagerDuty incident ID
595+
- `--ohss <jira-id>`: Login using JIRA ID
596+
- `--manager`: Login to the management cluster
597+
- `--service`: Login to the service cluster
598+
- `--cluster-info`: Print cluster information after login
599+
- `--namespace` or `-n`: Set default namespace (default: "default")
600+
580601
**Example usage:**
581602
```
582603
AI: I'll login to cluster abc123 for you.
583604
[Uses login tool with clusterId: "abc123"]
584605
Successfully logged in to cluster 'abc123'
606+
607+
# With readonly access
608+
$ ocm backplane login abc123 --readonly
585609
```
586610
587611
#### `console`

cmd/ocm-backplane/login/login.go

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ var (
5151
clusterInfo bool
5252
remediation string
5353
govcloud bool
54+
readonly bool
5455
}
5556

5657
// loginType derive the login type based on flags and args
@@ -133,6 +134,12 @@ func init() {
133134
"cluster-info",
134135
false, "Print basic cluster information after login",
135136
)
137+
flags.BoolVar(
138+
&args.readonly,
139+
"readonly",
140+
false,
141+
"Login with read-only access to the cluster",
142+
)
136143
}
137144

138145
// TODO there is something about the proxy config in relation to overriding with --url
@@ -336,9 +343,10 @@ func runLogin(cmd *cobra.Command, argv []string) (err error) {
336343
logger.WithFields(logger.Fields{
337344
"bpURL": bpURL,
338345
"clusterID": clusterID,
346+
"readonly": args.readonly,
339347
}).Debugln("Query backplane-api for proxy url of our target cluster")
340348
// Query backplane-api for proxy url
341-
bpAPIClusterURL, err := doLogin(bpURL, clusterID, *accessToken)
349+
bpAPIClusterURL, err := doLoginWithConn(bpURL, clusterID, *accessToken, nil, args.readonly)
342350
if err != nil {
343351
// Declare helperMsg
344352
helperMsg := "\n\033[1mNOTE: To troubleshoot the connectivity issues, please run `ocm-backplane health-check`\033[0m\n\n"
@@ -474,7 +482,7 @@ func GetRestConfig(bp config.BackplaneConfiguration, clusterID string) (*rest.Co
474482
return nil, err
475483
}
476484

477-
bpAPIClusterURL, err := doLogin(bp.URL, clusterID, *accessToken)
485+
bpAPIClusterURL, err := doLoginWithConn(bp.URL, clusterID, *accessToken, nil, false)
478486
if err != nil {
479487
return nil, fmt.Errorf("failed to backplane login to cluster %s: %v", cluster.Name(), err)
480488
}
@@ -503,7 +511,7 @@ func GetRestConfigWithConn(bp config.BackplaneConfiguration, ocmConnection *ocms
503511
return nil, err
504512
}
505513

506-
bpAPIClusterURL, err := doLoginWithConn(bp.URL, clusterID, *accessToken, ocmConnection)
514+
bpAPIClusterURL, err := doLoginWithConn(bp.URL, clusterID, *accessToken, ocmConnection, false)
507515
if err != nil {
508516
return nil, fmt.Errorf("failed to backplane login to cluster %s: %v", cluster.Name(), err)
509517
}
@@ -557,12 +565,8 @@ func GetRestConfigAsUserWithConn(bp config.BackplaneConfiguration, ocmConn *ocms
557565
return cfg, nil
558566
}
559567

560-
// doLogin returns the proxy url for the target cluster.
561-
func doLogin(api, clusterID, accessToken string) (string, error) {
562-
return doLoginWithConn(api, clusterID, accessToken, nil)
563-
}
564-
565-
func doLoginWithConn(api, clusterID, accessToken string, ocmConn *ocmsdk.Connection) (string, error) {
568+
// doLoginWithConn returns the proxy url for the target cluster.
569+
func doLoginWithConn(api, clusterID, accessToken string, ocmConn *ocmsdk.Connection, readonly bool) (string, error) {
566570
var client BackplaneApi.ClientInterface
567571
var err error = nil
568572
if ocmConn != nil {
@@ -574,7 +578,18 @@ func doLoginWithConn(api, clusterID, accessToken string, ocmConn *ocmsdk.Connect
574578
return "", fmt.Errorf("unable to create backplane api client")
575579
}
576580

577-
resp, err := client.LoginCluster(context.TODO(), clusterID)
581+
// Create request editor to add readonly query parameter if needed
582+
var reqEditors []BackplaneApi.RequestEditorFn
583+
if readonly {
584+
reqEditors = append(reqEditors, func(ctx context.Context, req *http.Request) error {
585+
q := req.URL.Query()
586+
q.Add("readonly", "true")
587+
req.URL.RawQuery = q.Encode()
588+
return nil
589+
})
590+
}
591+
592+
resp, err := client.LoginCluster(context.TODO(), clusterID, reqEditors...)
578593
// Print the whole response if we can't parse it. Eg. 5xx error from http server.
579594
if err != nil {
580595
// trying to determine the error
@@ -732,4 +747,4 @@ func getClusterIDFromExistingKubeConfig() (string, error) {
732747
clusterKey = clusterInfo.ClusterID
733748
logger.Debugf("Backplane Cluster Infromation data extracted: %+v\n", clusterInfo)
734749
return clusterKey, nil
735-
}
750+
}

cmd/ocm-backplane/login/login_test.go

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@ import (
1010
"path/filepath"
1111
"strings"
1212

13-
"go.uber.org/mock/gomock"
1413
. "github.com/onsi/ginkgo/v2"
1514
. "github.com/onsi/gomega"
1615
cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
1716
"github.com/trivago/tgo/tcontainer"
17+
"go.uber.org/mock/gomock"
1818
"k8s.io/client-go/tools/clientcmd"
1919
"k8s.io/client-go/tools/clientcmd/api"
2020

2121
"github.com/andygrunwald/go-jira"
22+
BackplaneApi "github.com/openshift/backplane-api/pkg/client"
2223
"github.com/openshift/backplane-cli/pkg/backplaneapi"
2324
backplaneapiMock "github.com/openshift/backplane-cli/pkg/backplaneapi/mocks"
2425
"github.com/openshift/backplane-cli/pkg/cli/config"
@@ -684,4 +685,77 @@ var _ = Describe("Login command", func() {
684685
Expect(err.Error()).To(Equal("clusterID cannot be detected for JIRA issue:OHSS-1000"))
685686
})
686687
})
688+
689+
Context("readonly flag functionality", func() {
690+
BeforeEach(func() {
691+
err := utils.CreateTempKubeConfig(nil)
692+
Expect(err).To(BeNil())
693+
})
694+
695+
It("should add readonly=true query parameter when readonly flag is set", func() {
696+
// Setup
697+
args.multiCluster = false
698+
args.readonly = true
699+
loginType = LoginTypeClusterID
700+
701+
mockOcmInterface.EXPECT().GetOCMEnvironment().Return(ocmEnv, nil).AnyTimes()
702+
mockOcmInterface.EXPECT().GetTargetCluster(testClusterID).Return(trueClusterID, trueClusterID, nil).Times(1)
703+
mockOcmInterface.EXPECT().GetOCMAccessToken().Return(&testToken, nil).Times(1)
704+
mockOcmInterface.EXPECT().IsClusterHibernating(gomock.Eq(trueClusterID)).Return(false, nil).Times(1)
705+
mockClientUtil.EXPECT().MakeRawBackplaneAPIClientWithAccessToken(backplaneAPIURI, testToken).Return(mockClient, nil)
706+
707+
// Mock LoginCluster and capture the request to verify readonly query param
708+
var capturedURL string
709+
mockClient.EXPECT().LoginCluster(gomock.Any(), gomock.Eq(trueClusterID), gomock.Any()).DoAndReturn(
710+
func(ctx interface{}, clusterId string, reqEditors ...interface{}) (*http.Response, error) {
711+
// Create a mock request to test the editor
712+
req, _ := http.NewRequest("GET", backplaneAPIURI+"/backplane/login/"+clusterId, nil)
713+
// Apply the request editors if any
714+
for _, editor := range reqEditors {
715+
if fn, ok := editor.(BackplaneApi.RequestEditorFn); ok {
716+
_ = fn(nil, req)
717+
}
718+
}
719+
capturedURL = req.URL.String()
720+
return fakeResp, nil
721+
},
722+
)
723+
724+
err := runLogin(nil, []string{testClusterID})
725+
726+
Expect(err).To(BeNil())
727+
// Verify readonly=true query parameter is present in the URL
728+
Expect(capturedURL).To(ContainSubstring("readonly=true"))
729+
})
730+
731+
It("should not add readonly query parameter when readonly flag is false", func() {
732+
// Setup
733+
args.multiCluster = false
734+
args.readonly = false
735+
loginType = LoginTypeClusterID
736+
737+
mockOcmInterface.EXPECT().GetOCMEnvironment().Return(ocmEnv, nil).AnyTimes()
738+
mockOcmInterface.EXPECT().GetTargetCluster(testClusterID).Return(trueClusterID, trueClusterID, nil).Times(1)
739+
mockOcmInterface.EXPECT().GetOCMAccessToken().Return(&testToken, nil).Times(1)
740+
mockOcmInterface.EXPECT().IsClusterHibernating(gomock.Eq(trueClusterID)).Return(false, nil).Times(1)
741+
mockClientUtil.EXPECT().MakeRawBackplaneAPIClientWithAccessToken(backplaneAPIURI, testToken).Return(mockClient, nil)
742+
743+
// Mock LoginCluster and capture the request
744+
var capturedURL string
745+
mockClient.EXPECT().LoginCluster(gomock.Any(), gomock.Eq(trueClusterID)).DoAndReturn(
746+
func(ctx interface{}, clusterId string, reqEditors ...interface{}) (*http.Response, error) {
747+
// Create a mock request
748+
req, _ := http.NewRequest("GET", backplaneAPIURI+"/backplane/login/"+clusterId, nil)
749+
capturedURL = req.URL.String()
750+
return fakeResp, nil
751+
},
752+
)
753+
754+
err := runLogin(nil, []string{testClusterID})
755+
756+
Expect(err).To(BeNil())
757+
// Verify readonly query parameter is not present
758+
Expect(capturedURL).NotTo(ContainSubstring("readonly"))
759+
})
760+
})
687761
})

0 commit comments

Comments
 (0)