Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make proxy configurable #267

Merged
merged 8 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 192 additions & 17 deletions cmd/ocm-backplane/cloud/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,198 @@ import (
"github.com/aws/aws-sdk-go-v2/aws/arn"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/sts"
ocmsdk "github.com/openshift-online/ocm-sdk-go"
cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
BackplaneApi "github.com/openshift/backplane-api/pkg/client"
"github.com/openshift/backplane-cli/pkg/awsutil"
"github.com/openshift/backplane-cli/pkg/cli/config"
bpCredentials "github.com/openshift/backplane-cli/pkg/credentials"
"github.com/openshift/backplane-cli/pkg/utils"
logger "github.com/sirupsen/logrus"
)

const OldFlowSupportRole = "role/RH-Technical-Support-Access"

var StsClientWithProxy = awsutil.StsClientWithProxy
var StsClient = awsutil.StsClient
var AssumeRoleWithJWT = awsutil.AssumeRoleWithJWT
var NewStaticCredentialsProvider = credentials.NewStaticCredentialsProvider
var AssumeRoleSequence = awsutil.AssumeRoleSequence

// Wrapper for the configuration needed for cloud requests
type QueryConfig struct {
config.BackplaneConfiguration
OcmConnection *ocmsdk.Connection
typeid marked this conversation as resolved.
Show resolved Hide resolved
Cluster *cmv1.Cluster
}

// GetAWSV2Config allows consumers to get an aws-sdk-go-v2 Config to programmatically access the AWS API
func (cfg *QueryConfig) GetAWSV2Config() (aws.Config, error) {
if cfg.Cluster.CloudProvider().ID() != "aws" {
return aws.Config{}, fmt.Errorf("only supported for the aws cloud provider, this cluster has: %s", cfg.Cluster.CloudProvider().ID())
}
creds, err := cfg.GetCloudCredentials()
if err != nil {
return aws.Config{}, err
}

awsCreds, ok := creds.(*bpCredentials.AWSCredentialsResponse)
if !ok {
return aws.Config{}, errors.New("unexpected error: failed to convert backplane creds to AWSCredentialsResponse")
}

return awsCreds.AWSV2Config()
}

// GetCloudCredentials returns Cloud Credentials Response
func (cfg *QueryConfig) GetCloudConsole() (*ConsoleResponse, error) {
ocmToken, _, err := cfg.OcmConnection.Tokens()
if err != nil {
return nil, fmt.Errorf("unable to get token for ocm connection")
}

isolatedBackplane, err := isIsolatedBackplaneAccess(cfg.Cluster, cfg.OcmConnection)
if err != nil {
logger.Infof("failed to determine if the cluster is using isolated backplane access: %v", err)
logger.Infof("for more information, try ocm get /api/clusters_mgmt/v1/clusters/%s/sts_support_jump_role", cfg.Cluster.ID())
logger.Infof("attempting to fallback to %s", OldFlowSupportRole)
}

if isolatedBackplane {
logger.Debugf("cluster is using isolated backplane")
targetCredentials, err := cfg.getIsolatedCredentials(ocmToken)
if err != nil {
// TODO: This fallback should be removed in the future
// TODO: when we are more confident in our ability to access clusters using the isolated flow
logger.Infof("failed to assume role with isolated backplane flow: %v", err)
logger.Infof("attempting to fallback to %s", OldFlowSupportRole)
return cfg.getCloudConsoleFromPublicAPI(ocmToken)
}

resp, err := awsutil.GetSigninToken(targetCredentials, cfg.Cluster.Region().ID())
if err != nil {
return nil, fmt.Errorf("failed to get signin token: %w", err)
}

signinFederationURL, err := awsutil.GetConsoleURL(resp.SigninToken, cfg.Cluster.Region().ID())
if err != nil {
return nil, fmt.Errorf("failed to generate console url: %w", err)
}
return &ConsoleResponse{ConsoleLink: signinFederationURL.String()}, nil
}

return cfg.getCloudConsoleFromPublicAPI(ocmToken)
}

// GetCloudConsole returns console response calling to public Backplane API
func (cfg *QueryConfig) getCloudConsoleFromPublicAPI(ocmToken string) (*ConsoleResponse, error) {
logger.Debugln("Getting Cloud Console")

client, err := utils.DefaultClientUtils.GetBackplaneClient(cfg.BackplaneConfiguration.URL, ocmToken, cfg.BackplaneConfiguration.ProxyURL)
if err != nil {
return nil, err
}
resp, err := client.GetCloudConsole(context.TODO(), cfg.Cluster.ID())
if err != nil {
return nil, err
}

if resp.StatusCode != http.StatusOK {
return nil, utils.TryPrintAPIError(resp, false)
}

credsResp, err := BackplaneApi.ParseGetCloudConsoleResponse(resp)
if err != nil {
return nil, fmt.Errorf("unable to parse response body from backplane:\n Status Code: %d", resp.StatusCode)
}

if len(credsResp.Body) == 0 {
return nil, fmt.Errorf("empty response from backplane")
}

cliResp := &ConsoleResponse{}
cliResp.ConsoleLink = *credsResp.JSON200.ConsoleLink

return cliResp, nil
}

// GetCloudCredentials returns Cloud Credentials Response
func (cfg *QueryConfig) GetCloudCredentials() (bpCredentials.Response, error) {
ocmToken, _, err := cfg.OcmConnection.Tokens()
if err != nil {
return nil, fmt.Errorf("unable to get token for ocm connection")
}

isolatedBackplane, err := isIsolatedBackplaneAccess(cfg.Cluster, cfg.OcmConnection)
if err != nil {
logger.Infof("failed to determine if the cluster is using isolated backplane access: %v", err)
logger.Infof("for more information, try ocm get /api/clusters_mgmt/v1/clusters/%s/sts_support_jump_role", cfg.Cluster.ID())
logger.Infof("attempting to fallback to %s", OldFlowSupportRole)
}

if isolatedBackplane {
logger.Debugf("cluster is using isolated backplane")
targetCredentials, err := cfg.getIsolatedCredentials(ocmToken)
if err != nil {
// TODO: This fallback should be removed in the future
// TODO: when we are more confident in our ability to access clusters using the isolated flow
logger.Infof("failed to assume role with isolated backplane flow: %v", err)
logger.Infof("attempting to fallback to %s", OldFlowSupportRole)
return cfg.getCloudCredentialsFromBackplaneAPI(ocmToken)
}

return &bpCredentials.AWSCredentialsResponse{
AccessKeyID: targetCredentials.AccessKeyID,
SecretAccessKey: targetCredentials.SecretAccessKey,
SessionToken: targetCredentials.SessionToken,
Expiration: targetCredentials.Expires.String(),
Region: cfg.Cluster.Region().ID(),
}, nil
}

return cfg.getCloudCredentialsFromBackplaneAPI(ocmToken)
}

func (cfg *QueryConfig) getCloudCredentialsFromBackplaneAPI(ocmToken string) (bpCredentials.Response, error) {
client, err := utils.DefaultClientUtils.GetBackplaneClient(cfg.BackplaneConfiguration.URL, ocmToken, cfg.BackplaneConfiguration.ProxyURL)
if err != nil {
return nil, err
}

resp, err := client.GetCloudCredentials(context.TODO(), cfg.Cluster.ID())
if err != nil {
return nil, err
}

if resp.StatusCode != http.StatusOK {
return nil, utils.TryPrintAPIError(resp, false)
}

logger.Debugln("Parsing response")

credsResp, err := BackplaneApi.ParseGetCloudCredentialsResponse(resp)
if err != nil {
return nil, fmt.Errorf("unable to parse response body from backplane:\n Status Code: %d : err: %v", resp.StatusCode, err)
}

switch cfg.Cluster.CloudProvider().ID() {
case "aws":
cliResp := &bpCredentials.AWSCredentialsResponse{}
if err := json.Unmarshal([]byte(*credsResp.JSON200.Credentials), cliResp); err != nil {
return nil, fmt.Errorf("unable to unmarshal AWS credentials response from backplane %s: %w", *credsResp.JSON200.Credentials, err)
}
cliResp.Region = cfg.Cluster.Region().ID()
return cliResp, nil
case "gcp":
cliResp := &bpCredentials.GCPCredentialsResponse{}
if err := json.Unmarshal([]byte(*credsResp.JSON200.Credentials), cliResp); err != nil {
return nil, fmt.Errorf("unable to unmarshal GCP credentials response from backplane %s: %w", *credsResp.JSON200.Credentials, err)
}
return cliResp, nil
default:
return nil, fmt.Errorf("unsupported cloud provider: %s", cfg.Cluster.CloudProvider().ID())
}
}

type assumeChainResponse struct {
AssumptionSequence []namedRoleArn `json:"assumptionSequence"`
}
Expand All @@ -33,41 +213,36 @@ type namedRoleArn struct {
Arn string `json:"arn"`
}

func getIsolatedCredentials(clusterID string, ocmToken *string) (aws.Credentials, error) {
if clusterID == "" {
func (cfg *QueryConfig) getIsolatedCredentials(ocmToken string) (aws.Credentials, error) {
if cfg.Cluster.ID() == "" {
return aws.Credentials{}, errors.New("must provide non-empty cluster ID")
}

email, err := utils.GetStringFieldFromJWT(*ocmToken, "email")
email, err := utils.GetStringFieldFromJWT(ocmToken, "email")
if err != nil {
return aws.Credentials{}, fmt.Errorf("unable to extract email from given token: %w", err)
}

bpConfig, err := GetBackplaneConfiguration()
if err != nil {
return aws.Credentials{}, fmt.Errorf("error retrieving backplane configuration: %w", err)
}

if bpConfig.AssumeInitialArn == "" {
if cfg.BackplaneConfiguration.AssumeInitialArn == "" {
return aws.Credentials{}, errors.New("backplane config is missing required `assume-initial-arn` property")
}

initialClient, err := StsClientWithProxy(bpConfig.ProxyURL)
initialClient, err := StsClient(cfg.BackplaneConfiguration.ProxyURL)
if err != nil {
return aws.Credentials{}, fmt.Errorf("failed to create sts client: %w", err)
}

seedCredentials, err := AssumeRoleWithJWT(*ocmToken, bpConfig.AssumeInitialArn, initialClient)
seedCredentials, err := AssumeRoleWithJWT(ocmToken, cfg.BackplaneConfiguration.AssumeInitialArn, initialClient)
if err != nil {
return aws.Credentials{}, fmt.Errorf("failed to assume role using JWT: %w", err)
}

backplaneClient, err := utils.DefaultClientUtils.MakeRawBackplaneAPIClientWithAccessToken(bpConfig.URL, *ocmToken)
backplaneClient, err := utils.DefaultClientUtils.GetBackplaneClient(cfg.BackplaneConfiguration.URL, ocmToken, cfg.BackplaneConfiguration.ProxyURL)
if err != nil {
return aws.Credentials{}, fmt.Errorf("failed to create backplane client with access token: %w", err)
}

response, err := backplaneClient.GetAssumeRoleSequence(context.TODO(), clusterID)
response, err := backplaneClient.GetAssumeRoleSequence(context.TODO(), cfg.Cluster.ID())
if err != nil {
return aws.Credentials{}, fmt.Errorf("failed to fetch arn sequence: %w", err)
}
Expand Down Expand Up @@ -96,16 +271,16 @@ func getIsolatedCredentials(clusterID string, ocmToken *string) (aws.Credentials
Credentials: NewStaticCredentialsProvider(seedCredentials.AccessKeyID, seedCredentials.SecretAccessKey, seedCredentials.SessionToken),
})

targetCredentials, err := AssumeRoleSequence(email, seedClient, roleAssumeSequence, bpConfig.ProxyURL, awsutil.DefaultSTSClientProviderFunc)
targetCredentials, err := AssumeRoleSequence(email, seedClient, roleAssumeSequence, cfg.BackplaneConfiguration.ProxyURL, awsutil.DefaultSTSClientProviderFunc)
if err != nil {
return aws.Credentials{}, fmt.Errorf("failed to assume role sequence: %w", err)
}
return targetCredentials, nil
}

func isIsolatedBackplaneAccess(cluster *cmv1.Cluster) (bool, error) {
func isIsolatedBackplaneAccess(cluster *cmv1.Cluster, ocmConnection *ocmsdk.Connection) (bool, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func isIsolatedBackplaneAccess(cluster *cmv1.Cluster, ocmConnection *ocmsdk.Connection) (bool, error) {
func (cfg *QueryConfig) isIsolatedBackplaneAccess(cluster *cmv1.Cluster) (bool, error) {

Any reason to not make this (and kind of all the other functions) methods off of QueryConfig? To me, that's one of the main benefits of defining a QueryConfig in the first place - you don't have to worry about passing the right arguments?

I could be convinced otherwise though, currently I'm thinking as long as the public/external helper functions are methods that's good enough for me (e.g. skip this one and refactor GetAWSV2Config to be a method).

Copy link
Member Author

@typeid typeid Nov 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that it makes sense, I was being a bit too careful with refactoring I suppose.

The following are now members of QueryConfig (and have been moved to common.go:

  • getCloudCredentials -> public
  • getCloudCredentialsFromBackplaneAPI
  • getCloudConsole -> public

I also refactored GetCloudConsole the same way GetCloudCredentials has been refactored to enable calling it from console.go, and moved some logic into the new getCloudConsoleFromBackplaneAPI to make it less confusing.

After these changes I tested the following:
Old flow:

  • ocm backplane cloud credentials
  • ocm backplane cloud console

Non STS clusters:

  • ocm backplane cloud credentials
  • ocm backplane cloud console

New flow:

  • ocm backplane cloud credentials
  • ocm backplane cloud console

if cluster.AWS().STS().Enabled() {
stsSupportJumpRole, err := utils.DefaultOCMInterface.GetStsSupportJumpRoleARN(cluster.ID())
stsSupportJumpRole, err := utils.DefaultOCMInterface.GetStsSupportJumpRoleARN(ocmConnection, cluster.ID())
if err != nil {
return false, fmt.Errorf("failed to get sts support jump role ARN for cluster %v: %w", cluster.ID(), err)
}
Expand Down
Loading