diff --git a/cmd/ocm-backplane/login/login.go b/cmd/ocm-backplane/login/login.go index a5fd19ac..56307157 100644 --- a/cmd/ocm-backplane/login/login.go +++ b/cmd/ocm-backplane/login/login.go @@ -487,9 +487,12 @@ func GetRestConfig(bp config.BackplaneConfiguration, clusterID string) (*rest.Co return BuildRestConfig(bpAPIClusterURL, accessToken, proxyURL) } -// GetRestConfig returns a client-go *rest.Config which can be used to programmatically interact with the -// Kubernetes API of a provided clusterID +// GetRestConfigWithConn returns a client-go *rest.Config which can be used to programmatically interact with the +// Kubernetes API of a provided clusterID, using a provided OCM connection. func GetRestConfigWithConn(bp config.BackplaneConfiguration, ocmConnection *ocmsdk.Connection, clusterID string) (*rest.Config, error) { + if ocmConnection == nil { + return nil, fmt.Errorf("nil OCM connection provided to GetRestConfigWithConn()") + } cluster, err := ocm.DefaultOCMInterface.GetClusterInfoByIDWithConn(ocmConnection, clusterID) if err != nil { return nil, err @@ -500,7 +503,7 @@ func GetRestConfigWithConn(bp config.BackplaneConfiguration, ocmConnection *ocms return nil, err } - bpAPIClusterURL, err := doLogin(bp.URL, clusterID, *accessToken) + bpAPIClusterURL, err := doLoginWithConn(bp.URL, clusterID, *accessToken, ocmConnection) if err != nil { return nil, fmt.Errorf("failed to backplane login to cluster %s: %v", cluster.Name(), err) } @@ -533,11 +536,40 @@ func GetRestConfigAsUser(bp config.BackplaneConfiguration, clusterID, username s return cfg, nil } +// GetRestConfigAsUserWithConn returns a client-go *rest.Config like GetRestConfig, but supports configuring an +// impersonation username. Commonly, this is "backplane-cluster-admin" +// best practice would be to add at least one elevationReason in order to justity the impersonation +// Uses provided OCM connection for attributes +func GetRestConfigAsUserWithConn(bp config.BackplaneConfiguration, ocmConn *ocmsdk.Connection, clusterID, username string, elevationReasons ...string) (*rest.Config, error) { + cfg, err := GetRestConfigWithConn(bp, ocmConn, clusterID) + if err != nil { + return nil, err + } + + cfg.Impersonate = rest.ImpersonationConfig{ + UserName: username, + } + + if len(elevationReasons) > 0 { + cfg.Impersonate.Extra = map[string][]string{"reason": elevationReasons} + } + + 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) +} - client, err := backplaneapi.DefaultClientUtils.MakeRawBackplaneAPIClientWithAccessToken(api, accessToken) - +func doLoginWithConn(api, clusterID, accessToken string, ocmConn *ocmsdk.Connection) (string, error) { + var client BackplaneApi.ClientInterface + var err error = nil + if ocmConn != nil { + client, err = backplaneapi.DefaultClientUtils.MakeRawBackplaneAPIClientWithAccessTokenWithConn(api, accessToken, ocmConn) + } else { + client, err = backplaneapi.DefaultClientUtils.MakeRawBackplaneAPIClientWithAccessToken(api, accessToken) + } if err != nil { return "", fmt.Errorf("unable to create backplane api client") } diff --git a/pkg/backplaneapi/clientUtils.go b/pkg/backplaneapi/clientUtils.go index 70e2cf01..fa7c2597 100644 --- a/pkg/backplaneapi/clientUtils.go +++ b/pkg/backplaneapi/clientUtils.go @@ -7,18 +7,19 @@ import ( "net/http" "net/url" + ocmsdk "github.com/openshift-online/ocm-sdk-go" BackplaneApi "github.com/openshift/backplane-api/pkg/client" - logger "github.com/sirupsen/logrus" - "github.com/openshift/backplane-cli/pkg/cli/config" "github.com/openshift/backplane-cli/pkg/info" "github.com/openshift/backplane-cli/pkg/ocm" + logger "github.com/sirupsen/logrus" ) type ClientUtils interface { MakeBackplaneAPIClient(base string) (BackplaneApi.ClientWithResponsesInterface, error) MakeBackplaneAPIClientWithAccessToken(base, accessToken string) (BackplaneApi.ClientWithResponsesInterface, error) MakeRawBackplaneAPIClientWithAccessToken(base, accessToken string) (BackplaneApi.ClientInterface, error) + MakeRawBackplaneAPIClientWithAccessTokenWithConn(base, accessToken string, ocmConn *ocmsdk.Connection) (BackplaneApi.ClientInterface, error) MakeRawBackplaneAPIClient(base string) (BackplaneApi.ClientInterface, error) GetBackplaneClient(backplaneURL string, ocmToken string, proxyURL *string) (client BackplaneApi.ClientInterface, err error) SetClientProxyURL(proxyURL string) error @@ -45,9 +46,20 @@ func makeClientOptions(accessToken string) BackplaneApi.ClientOption { } func (s *DefaultClientUtilsImpl) MakeRawBackplaneAPIClientWithAccessToken(base, accessToken string) (BackplaneApi.ClientInterface, error) { + return s.MakeRawBackplaneAPIClientWithAccessTokenWithConn(base, accessToken, nil) +} + +func (s *DefaultClientUtilsImpl) MakeRawBackplaneAPIClientWithAccessTokenWithConn(base, accessToken string, ocmConn *ocmsdk.Connection) (BackplaneApi.ClientInterface, error) { + // Inject client Proxy Url from config if s.clientProxyURL == "" { - bpConfig, err := config.GetBackplaneConfiguration() + var bpConfig config.BackplaneConfiguration + var err error + if ocmConn != nil { + bpConfig, err = config.GetBackplaneConfigurationWithConn(ocmConn) + } else { + bpConfig, err = config.GetBackplaneConfiguration() + } if err != nil { return nil, err } diff --git a/pkg/backplaneapi/mocks/clientUtilsMock.go b/pkg/backplaneapi/mocks/clientUtilsMock.go index b6859fee..05ab9d4e 100644 --- a/pkg/backplaneapi/mocks/clientUtilsMock.go +++ b/pkg/backplaneapi/mocks/clientUtilsMock.go @@ -14,6 +14,7 @@ import ( Openapi "github.com/openshift/backplane-api/pkg/client" gomock "go.uber.org/mock/gomock" + ocmsdk "github.com/openshift-online/ocm-sdk-go" ) // MockClientUtils is a mock of ClientUtils interface. @@ -115,6 +116,21 @@ func (mr *MockClientUtilsMockRecorder) MakeRawBackplaneAPIClientWithAccessToken( return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MakeRawBackplaneAPIClientWithAccessToken", reflect.TypeOf((*MockClientUtils)(nil).MakeRawBackplaneAPIClientWithAccessToken), base, accessToken) } +// MakeRawBackplaneAPIClientWithAccessTokenWithConn mocks base method. +func (m *MockClientUtils) MakeRawBackplaneAPIClientWithAccessTokenWithConn(arg0, arg1 string, arg2 *ocmsdk.Connection) (Openapi.ClientInterface, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MakeRawBackplaneAPIClientWithAccessTokenWithConn", arg0, arg1) + ret0, _ := ret[0].(Openapi.ClientInterface) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// MakeRawBackplaneAPIClientWithAccessTokenWithConn indicates an expected call of MakeRawBackplaneAPIClientWithAccessTokenWithConn. +func (mr *MockClientUtilsMockRecorder) MakeRawBackplaneAPIClientWithAccessTokenWithConn(arg0, arg1 interface{}, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MakeRawBackplaneAPIClientWithAccessTokenWithConn", reflect.TypeOf((*MockClientUtils)(nil).MakeRawBackplaneAPIClientWithAccessTokenWithConn), arg0, arg1) +} + // SetClientProxyURL mocks base method. func (m *MockClientUtils) SetClientProxyURL(proxyURL string) error { m.ctrl.T.Helper() diff --git a/pkg/cli/config/config.go b/pkg/cli/config/config.go index a6f95e64..503a22f4 100644 --- a/pkg/cli/config/config.go +++ b/pkg/cli/config/config.go @@ -13,6 +13,7 @@ import ( logger "github.com/sirupsen/logrus" "github.com/spf13/viper" + ocmsdk "github.com/openshift-online/ocm-sdk-go" "github.com/openshift/backplane-cli/pkg/info" "github.com/openshift/backplane-cli/pkg/ocm" ) @@ -103,6 +104,12 @@ func GetConfigFilePath() (string, error) { // GetBackplaneConfiguration parses and returns the given backplane configuration func GetBackplaneConfiguration() (bpConfig BackplaneConfiguration, err error) { + return GetBackplaneConfigurationWithConn(nil) +} + +// GetBackplaneConfiguration parses and returns the given backplane configuration using attributes +// from provided OCM connection +func GetBackplaneConfigurationWithConn(ocmConn *ocmsdk.Connection) (bpConfig BackplaneConfiguration, err error) { viper.SetDefault(prodEnvNameKey, prodEnvNameDefaultValue) viper.SetDefault(jiraBaseURLKey, JiraBaseURLDefaultValue) viper.SetDefault(JiraConfigForAccessRequestsKey, JiraConfigForAccessRequestsDefaultValue) @@ -146,22 +153,31 @@ func GetBackplaneConfiguration() (bpConfig BackplaneConfiguration, err error) { } else { logger.Debug("This is govcloud, no proxy to use") } - // Warn user if url defined in the config file if viper.GetString("url") != "" { logger.Warn("Manual URL configuration is deprecated, please remove URL key from Backplane configuration") } - // Warn if user has explicitly defined backplane URL via env - url, ok := getBackplaneEnv(info.BackplaneURLEnvName) - if ok { - logger.Warn(fmt.Sprintf("Manual URL configuration is deprecated, please unset the environment %s", info.BackplaneURLEnvName)) - bpConfig.URL = url - } else { - // Fetch backplane URL from ocm env - if bpConfig.URL, err = bpConfig.GetBackplaneURL(); err != nil { + url, envURLok := getBackplaneEnv(info.BackplaneURLEnvName) + if envURLok { + logger.Warn(fmt.Printf("Manual URL configuration is deprecated, please unset the environment %s", info.BackplaneURLEnvName)) + } + + if ocmConn != nil { + // If an OCM connection is provided use this to fetch BP URL + // from its v1.environment info + if bpConfig.URL, err = bpConfig.GetBackplaneURLWithConn(ocmConn); err != nil { return bpConfig, err } + } else { + if envURLok { + bpConfig.URL = url + } else { + // Fetch backplane URL from ocm env + if bpConfig.URL, err = bpConfig.GetBackplaneURL(); err != nil { + return bpConfig, err + } + } } // proxyURL is required @@ -255,6 +271,10 @@ var testProxy = func(ctx context.Context, testURL string, proxyURL url.URL) erro return nil } +func (config *BackplaneConfiguration) GetFirstWorkingProxyURL(s []string) string { + return config.getFirstWorkingProxyURL(s) +} + func (config *BackplaneConfiguration) getFirstWorkingProxyURL(s []string) string { if len(s) == 0 { logger.Debug("No proxy to use") @@ -371,6 +391,20 @@ func GetConfigDirectory() (string, error) { return configDirectory, nil } +// GetBackplaneURL returns API URL +func (config *BackplaneConfiguration) GetBackplaneURLWithConn(ocmConn *ocmsdk.Connection) (string, error) { + ocmEnv, err := ocm.DefaultOCMInterface.GetOCMEnvironmentWithConn(ocmConn) + if err != nil { + return "", err + } + url, ok := ocmEnv.GetBackplaneURL() + if !ok { + return "", fmt.Errorf("the requested API endpoint is not available for the OCM environment: %v", ocmEnv.Name()) + } + logger.Infof("Backplane URL retrieved via OCM environment: %s", url) + return url, nil +} + // GetBackplaneURL returns API URL func (config *BackplaneConfiguration) GetBackplaneURL() (string, error) { diff --git a/pkg/ocm/mocks/ocmWrapperMock.go b/pkg/ocm/mocks/ocmWrapperMock.go index 730e6585..a2aed734 100644 --- a/pkg/ocm/mocks/ocmWrapperMock.go +++ b/pkg/ocm/mocks/ocmWrapperMock.go @@ -179,6 +179,21 @@ func (mr *MockOCMInterfaceMockRecorder) GetOCMEnvironment() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOCMEnvironment", reflect.TypeOf((*MockOCMInterface)(nil).GetOCMEnvironment)) } +// GetOCMEnvironmentWithConn mocks base method. +func (m *MockOCMInterface) GetOCMEnvironmentWithConn(arg0 *sdk.Connection) (*v10.Environment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOCMEnvironmentWithConn") + ret0, _ := ret[0].(*v10.Environment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOCMEnvironmentWithConn indicates an expected call of GetOCMEnvironmentWithConn. +func (mr *MockOCMInterfaceMockRecorder) GetOCMEnvironmentWithConn(arg0 *sdk.Connection) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOCMEnvironmentWithConn", reflect.TypeOf((*MockOCMInterface)(nil).GetOCMEnvironment)) +} + // GetPullSecret mocks base method. func (m *MockOCMInterface) GetPullSecret() (string, error) { m.ctrl.T.Helper() diff --git a/pkg/ocm/ocm.go b/pkg/ocm/ocm.go index 9e8db0bd..088e355e 100644 --- a/pkg/ocm/ocm.go +++ b/pkg/ocm/ocm.go @@ -36,6 +36,7 @@ type OCMInterface interface { GetOCMEnvironment() (*cmv1.Environment, error) GetOCMAccessTokenWithConn(ocmConnection *ocmsdk.Connection) (*string, error) GetClusterInfoByIDWithConn(ocmConnection *ocmsdk.Connection, clusterID string) (*cmv1.Cluster, error) + GetOCMEnvironmentWithConn(connection *ocmsdk.Connection) (*cmv1.Environment, error) IsClusterAccessProtectionEnabled(ocmConnection *ocmsdk.Connection, clusterID string) (bool, error) GetClusterActiveAccessRequest(ocmConnection *ocmsdk.Connection, clusterID string) (*acctrspv1.AccessRequest, error) CreateClusterAccessRequest(ocmConnection *ocmsdk.Connection, clusterID, reason, jiraIssueID, approvalDuration string) (*acctrspv1.AccessRequest, error) @@ -387,7 +388,7 @@ func (o *DefaultOCMInterfaceImpl) GetStsSupportJumpRoleARN(ocmConnection *ocmsdk return response.Body().RoleArn(), nil } -// GetOCMEnvironment returns the Backplane API URL based on the OCM env +// GetOCMEnvironment returns the OCM v1.environment response (containing the Backplane API URL). func (o *DefaultOCMInterfaceImpl) GetOCMEnvironment() (*cmv1.Environment, error) { // Create the client for the OCM API connection, err := o.SetupOCMConnection() @@ -395,6 +396,16 @@ func (o *DefaultOCMInterfaceImpl) GetOCMEnvironment() (*cmv1.Environment, error) return nil, fmt.Errorf("failed to create OCM connection: %v", err) } + defer connection.Close() + return o.GetOCMEnvironmentWithConn(connection) +} + +// GetOCMEnvironmentWithConn returns the v1.environment response for the provided +// OCM connection (containing the Backplane API URL) +func (o *DefaultOCMInterfaceImpl) GetOCMEnvironmentWithConn(connection *ocmsdk.Connection) (*cmv1.Environment, error) { + if connection == nil { + return nil, fmt.Errorf("err GetOCMEnvironmentWithConn() provided nil OCM connection") + } responseEnv, err := connection.ClustersMgmt().V1().Environment().Get().Send() if err != nil { // Check if the error indicates a forbidden status