From a63c24e7a2bc50a9eba1141de20e4e1a17977815 Mon Sep 17 00:00:00 2001 From: Samantha Jayasinghe Date: Tue, 3 Feb 2026 12:29:41 +1300 Subject: [PATCH] SREP-3299: Add readonly flag for backplane login command --- README.md | 24 +++++++++ cmd/ocm-backplane/login/login.go | 37 +++++++++---- cmd/ocm-backplane/login/login_test.go | 76 ++++++++++++++++++++++++++- 3 files changed, 125 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c44bac32..b9a7c470 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,17 @@ In this example, we will login to a cluster with id `123456abcdef` in production ``` $ ocm backplane login --service ``` + +- To login with read-only access to the cluster + ``` + $ ocm backplane login --readonly + ``` + + The `--readonly` flag restricts access to read-only operations, preventing any modifications to the cluster. This is useful for: + - Auditing and troubleshooting without risk of making changes + - Providing temporary access with limited privileges + - Ensuring compliance with read-only access policies + ### Get cluster information after login - Login to the target cluster via backplane and add `--cluster-info` flag @@ -577,11 +588,24 @@ Login to a backplane cluster. **Parameters:** - `clusterId` (required): The cluster ID to login to +**CLI Flags:** +- `--readonly`: Login with read-only access (calls `/backplane/login/{clusterId}?readonly=true`) +- `--multi` or `-m`: Enable multi-cluster login +- `--pd `: Login using PagerDuty incident ID +- `--ohss `: Login using JIRA ID +- `--manager`: Login to the management cluster +- `--service`: Login to the service cluster +- `--cluster-info`: Print cluster information after login +- `--namespace` or `-n`: Set default namespace (default: "default") + **Example usage:** ``` AI: I'll login to cluster abc123 for you. [Uses login tool with clusterId: "abc123"] Successfully logged in to cluster 'abc123' + +# With readonly access +$ ocm backplane login abc123 --readonly ``` #### `console` diff --git a/cmd/ocm-backplane/login/login.go b/cmd/ocm-backplane/login/login.go index 56307157..97d83081 100644 --- a/cmd/ocm-backplane/login/login.go +++ b/cmd/ocm-backplane/login/login.go @@ -51,6 +51,7 @@ var ( clusterInfo bool remediation string govcloud bool + readonly bool } // loginType derive the login type based on flags and args @@ -133,6 +134,12 @@ func init() { "cluster-info", false, "Print basic cluster information after login", ) + flags.BoolVar( + &args.readonly, + "readonly", + false, + "Login with read-only access to the cluster", + ) } // 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) { logger.WithFields(logger.Fields{ "bpURL": bpURL, "clusterID": clusterID, + "readonly": args.readonly, }).Debugln("Query backplane-api for proxy url of our target cluster") // Query backplane-api for proxy url - bpAPIClusterURL, err := doLogin(bpURL, clusterID, *accessToken) + bpAPIClusterURL, err := doLoginWithConn(bpURL, clusterID, *accessToken, nil, args.readonly) if err != nil { // Declare helperMsg 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 return nil, err } - bpAPIClusterURL, err := doLogin(bp.URL, clusterID, *accessToken) + bpAPIClusterURL, err := doLoginWithConn(bp.URL, clusterID, *accessToken, nil, false) if err != nil { return nil, fmt.Errorf("failed to backplane login to cluster %s: %v", cluster.Name(), err) } @@ -503,7 +511,7 @@ func GetRestConfigWithConn(bp config.BackplaneConfiguration, ocmConnection *ocms return nil, err } - bpAPIClusterURL, err := doLoginWithConn(bp.URL, clusterID, *accessToken, ocmConnection) + bpAPIClusterURL, err := doLoginWithConn(bp.URL, clusterID, *accessToken, ocmConnection, false) if err != nil { return nil, fmt.Errorf("failed to backplane login to cluster %s: %v", cluster.Name(), err) } @@ -557,12 +565,8 @@ func GetRestConfigAsUserWithConn(bp config.BackplaneConfiguration, ocmConn *ocms return cfg, nil } -// doLogin returns the proxy url for the target cluster. -func doLogin(api, clusterID, accessToken string) (string, error) { - return doLoginWithConn(api, clusterID, accessToken, nil) -} - -func doLoginWithConn(api, clusterID, accessToken string, ocmConn *ocmsdk.Connection) (string, error) { +// doLoginWithConn returns the proxy url for the target cluster. +func doLoginWithConn(api, clusterID, accessToken string, ocmConn *ocmsdk.Connection, readonly bool) (string, error) { var client BackplaneApi.ClientInterface var err error = nil if ocmConn != nil { @@ -574,7 +578,18 @@ func doLoginWithConn(api, clusterID, accessToken string, ocmConn *ocmsdk.Connect return "", fmt.Errorf("unable to create backplane api client") } - resp, err := client.LoginCluster(context.TODO(), clusterID) + // Create request editor to add readonly query parameter if needed + var reqEditors []BackplaneApi.RequestEditorFn + if readonly { + reqEditors = append(reqEditors, func(ctx context.Context, req *http.Request) error { + q := req.URL.Query() + q.Add("readonly", "true") + req.URL.RawQuery = q.Encode() + return nil + }) + } + + resp, err := client.LoginCluster(context.TODO(), clusterID, reqEditors...) // Print the whole response if we can't parse it. Eg. 5xx error from http server. if err != nil { // trying to determine the error @@ -732,4 +747,4 @@ func getClusterIDFromExistingKubeConfig() (string, error) { clusterKey = clusterInfo.ClusterID logger.Debugf("Backplane Cluster Infromation data extracted: %+v\n", clusterInfo) return clusterKey, nil -} \ No newline at end of file +} diff --git a/cmd/ocm-backplane/login/login_test.go b/cmd/ocm-backplane/login/login_test.go index 955d8e00..64ad2f99 100644 --- a/cmd/ocm-backplane/login/login_test.go +++ b/cmd/ocm-backplane/login/login_test.go @@ -10,15 +10,16 @@ import ( "path/filepath" "strings" - "go.uber.org/mock/gomock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" "github.com/trivago/tgo/tcontainer" + "go.uber.org/mock/gomock" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" "github.com/andygrunwald/go-jira" + BackplaneApi "github.com/openshift/backplane-api/pkg/client" "github.com/openshift/backplane-cli/pkg/backplaneapi" backplaneapiMock "github.com/openshift/backplane-cli/pkg/backplaneapi/mocks" "github.com/openshift/backplane-cli/pkg/cli/config" @@ -684,4 +685,77 @@ var _ = Describe("Login command", func() { Expect(err.Error()).To(Equal("clusterID cannot be detected for JIRA issue:OHSS-1000")) }) }) + + Context("readonly flag functionality", func() { + BeforeEach(func() { + err := utils.CreateTempKubeConfig(nil) + Expect(err).To(BeNil()) + }) + + It("should add readonly=true query parameter when readonly flag is set", func() { + // Setup + args.multiCluster = false + args.readonly = true + loginType = LoginTypeClusterID + + mockOcmInterface.EXPECT().GetOCMEnvironment().Return(ocmEnv, nil).AnyTimes() + mockOcmInterface.EXPECT().GetTargetCluster(testClusterID).Return(trueClusterID, trueClusterID, nil).Times(1) + mockOcmInterface.EXPECT().GetOCMAccessToken().Return(&testToken, nil).Times(1) + mockOcmInterface.EXPECT().IsClusterHibernating(gomock.Eq(trueClusterID)).Return(false, nil).Times(1) + mockClientUtil.EXPECT().MakeRawBackplaneAPIClientWithAccessToken(backplaneAPIURI, testToken).Return(mockClient, nil) + + // Mock LoginCluster and capture the request to verify readonly query param + var capturedURL string + mockClient.EXPECT().LoginCluster(gomock.Any(), gomock.Eq(trueClusterID), gomock.Any()).DoAndReturn( + func(ctx interface{}, clusterId string, reqEditors ...interface{}) (*http.Response, error) { + // Create a mock request to test the editor + req, _ := http.NewRequest("GET", backplaneAPIURI+"/backplane/login/"+clusterId, nil) + // Apply the request editors if any + for _, editor := range reqEditors { + if fn, ok := editor.(BackplaneApi.RequestEditorFn); ok { + _ = fn(nil, req) + } + } + capturedURL = req.URL.String() + return fakeResp, nil + }, + ) + + err := runLogin(nil, []string{testClusterID}) + + Expect(err).To(BeNil()) + // Verify readonly=true query parameter is present in the URL + Expect(capturedURL).To(ContainSubstring("readonly=true")) + }) + + It("should not add readonly query parameter when readonly flag is false", func() { + // Setup + args.multiCluster = false + args.readonly = false + loginType = LoginTypeClusterID + + mockOcmInterface.EXPECT().GetOCMEnvironment().Return(ocmEnv, nil).AnyTimes() + mockOcmInterface.EXPECT().GetTargetCluster(testClusterID).Return(trueClusterID, trueClusterID, nil).Times(1) + mockOcmInterface.EXPECT().GetOCMAccessToken().Return(&testToken, nil).Times(1) + mockOcmInterface.EXPECT().IsClusterHibernating(gomock.Eq(trueClusterID)).Return(false, nil).Times(1) + mockClientUtil.EXPECT().MakeRawBackplaneAPIClientWithAccessToken(backplaneAPIURI, testToken).Return(mockClient, nil) + + // Mock LoginCluster and capture the request + var capturedURL string + mockClient.EXPECT().LoginCluster(gomock.Any(), gomock.Eq(trueClusterID)).DoAndReturn( + func(ctx interface{}, clusterId string, reqEditors ...interface{}) (*http.Response, error) { + // Create a mock request + req, _ := http.NewRequest("GET", backplaneAPIURI+"/backplane/login/"+clusterId, nil) + capturedURL = req.URL.String() + return fakeResp, nil + }, + ) + + err := runLogin(nil, []string{testClusterID}) + + Expect(err).To(BeNil()) + // Verify readonly query parameter is not present + Expect(capturedURL).NotTo(ContainSubstring("readonly")) + }) + }) })