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

[UX-33] rpk: migrate login + profile creation flow to the Public API. #24877

Merged
merged 3 commits into from
Jan 30, 2025

Conversation

r-vasquez
Copy link
Contributor

This PR migrates the login + profile creation flow to the Public API client instead of the Cloud API. The end goal is to migrate away from the CloudAPI client.

There is no change for the end user and the commands remain the same. These are just internal changes.

Backports Required

  • none - not a bug fix
  • none - this is a backport
  • none - issue does not exist in previous branches
  • none - papercut/not impactful enough to backport
  • v24.3.x
  • v24.2.x
  • v24.1.x

Release Notes

  • none

This is done in the oauth.loadFlow of the normal
login steps.

Also renamed the publicapi.ControlPlaneClientSet
to CloudClientSet since the clients can come from
different services and not only the "controlplane"
service.
@r-vasquez r-vasquez requested review from Deflaimun and a team as code owners January 21, 2025 21:20
@r-vasquez r-vasquez requested review from malinskibeniamin and removed request for a team January 21, 2025 21:20
@r-vasquez r-vasquez changed the title [UX-32] rpk: migrate login + profile creation flow to the Public API. [UX-33] rpk: migrate login + profile creation flow to the Public API. Jan 21, 2025
@andresaristizabal
Copy link
Contributor

checking PR

// which have pagination enabled.
func Paginate[T any](
ctx context.Context,
maxPages int,
Copy link
Contributor

Choose a reason for hiding this comment

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

Is max pages necessary? why is nextTokenPage == "" not enough?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just being paranoid to avoid an infinite loop.

Basically to avoid the case of a rogue server that never stops sending the nextTokenPage with the last saved value.

src/go/rpk/pkg/publicapi/controlplane.go Outdated Show resolved Hide resolved
return Paginate(ctx, maxPages, fetchPage)
}

func (cpCl *CloudClientSet) ServerlessClusterForID(ctx context.Context, ID string) (*controlplanev1beta2.ServerlessCluster, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit]: I don't know if we have a standard in rpk, but, what do you think call it GetServerlessClusterByID?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not an rpk standard but instead of Go, we try to be as close as possible to general naming conventions.

For this case in particular: Getters.

rpk instances where we follow this (some examples):

rpk instances where we don't follow this:
to fulfill interfaces:

However, there are some other places where we have failed to follow this too in the past.


// Clusters returns all the Clusters using the pagination feature to traverse
// all pages of the list.
func (cpCl *CloudClientSet) Clusters(ctx context.Context) ([]*controlplanev1beta2.Cluster, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit]: same here "ListClusters" or "GetClusters"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as above.

src/go/rpk/pkg/publicapi/controlplane.go Outdated Show resolved Hide resolved
wg.Wait()

err := errors.Join(orgErr, rgsErr, vcErr, cErr)
return org, rgs, vcs, cs, err
Copy link
Contributor

Choose a reason for hiding this comment

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

is it safe to use var out of the go routine?

Copy link
Contributor Author

@r-vasquez r-vasquez Jan 28, 2025

Choose a reason for hiding this comment

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

In this case, it is. We are not modifying any variable concurrently, every pair of variables is modified separately in different goroutines, and we don't need any synchronization (mutex for example).

For context, this is the equivalent function of our current Cloud API helper function in: https://github.com/redpanda-data/redpanda/blob/dev/src/go/rpk/pkg/cloudapi/cloudapi.go#L66 but using the Public API

Cluster: controlplanev1beta2connect.NewClusterServiceClient(httpCl, host, opts...),
}, nil
Organization: iamv1beta2connect.NewOrganizationServiceClient(httpCl, host, opts...),
Copy link
Contributor

Choose a reason for hiding this comment

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

How about passing those clients as parameters to NewCloudClientSet? This approach allows us to use mockgen to generate mocks for these clients, making tests easier to write and clearer to understand. Additionally, we can leverage protobuf to auto-generate the necessary mocks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, having a way to inject the internal clients can be beneficial as we expand the PublicAPI usage, especially when we introduce more convoluted usage such as in creating a Cluster.

What we can do is to refactor the client creation to accept optional client configuration options, and default to our current implementation, this is what I'm thinking:

type CloudClientSet struct {
	Cluster       controlplanev1beta2connect.ClusterServiceClient
	Organization  iamv1beta2connect.OrganizationServiceClient
	ResourceGroup controlplanev1beta2connect.ResourceGroupServiceClient
	Serverless    controlplanev1beta2connect.ServerlessClusterServiceClient
}

type CloudClientSetOptions struct {
	connectOptions []connect.ClientOption
	httpClient     *http.Client

	Cluster       controlplanev1beta2connect.ClusterServiceClient
	Organization  iamv1beta2connect.OrganizationServiceClient
	ResourceGroup controlplanev1beta2connect.ResourceGroupServiceClient
	Serverless    controlplanev1beta2connect.ServerlessClusterServiceClient
}

// Opt configures the CloudClientSet options.
type Opt func(*CloudClientSetOptions)

func NewCloudClientSet(host, authToken string, opts ...Opt) *CloudClientSet {
	if host == "" {
		host = ControlPlaneProdURL
	}

        // Default values
	clOpts := &CloudClientSetOptions{
		connectOptions: []connect.ClientOption{
			connect.WithInterceptors(
				newAuthInterceptor(authToken), // Add the Bearer token.
				newLoggerInterceptor(),        // Add logs to every request.
			),
		},
		httpClient: &http.Client{Timeout: 30 * time.Second},
	}
	// Apply custom options
	for _, opt := range opts {
		opt(clOpts)
	}

	// Initialize the CloudClientSet with default clients
	cl := &CloudClientSet{
		Cluster:       controlplanev1beta2connect.NewClusterServiceClient(clOpts.httpClient, host, clOpts.connectOptions...),
		Organization:  iamv1beta2connect.NewOrganizationServiceClient(clOpts.httpClient, host, clOpts.connectOptions...),
		ResourceGroup: controlplanev1beta2connect.NewResourceGroupServiceClient(clOpts.httpClient, host, clOpts.connectOptions...),
		Serverless:    controlplanev1beta2connect.NewServerlessClusterServiceClient(clOpts.httpClient, host, clOpts.connectOptions...),
	}

	// Override with custom clients from options if provided
	if clOpts.Cluster != nil {
		cl.Cluster = clOpts.Cluster
	}
	if clOpts.Organization != nil {
		cl.Organization = clOpts.Organization
	}
	if clOpts.ResourceGroup != nil {
		cl.ResourceGroup = clOpts.ResourceGroup
	}
	if clOpts.Serverless != nil {
		cl.Serverless = clOpts.Serverless
	}
	return cl
}

// Options setters:

func WithClusterServiceClient(cluster controlplanev1beta2connect.ClusterServiceClient) Opt {
	return func(cl *CloudClientSetOptions) { cl.Cluster = cluster }
}

func WithOrganizationClient(organization iamv1beta2connect.OrganizationServiceClient) Opt {
	return func(cl *CloudClientSetOptions) { cl.Organization = organization }
}

func WithResourceGroupClient(resourceGroup controlplanev1beta2connect.ResourceGroupServiceClient) Opt {
	return func(cl *CloudClientSetOptions) { cl.ResourceGroup = resourceGroup }
}

func WithServerlessClient(serverless controlplanev1beta2connect.ServerlessClusterServiceClient) Opt {
	return func(cl *CloudClientSetOptions) { cl.Serverless = serverless }
}

func WithConnectClientOptions(opts ...connect.ClientOption) Opt {
	return func(cl *CloudClientSetOptions) { cl.connectOptions = append(cl.connectOptions, opts...) }
}

func WithHTTPClient(client *http.Client) Opt {
	return func(cl *CloudClientSetOptions) { cl.httpClient = client }
}

However, such a refactor does not currently benefit us, as we don't currently need to mock any of our tests. So, I'll leave this as a reference for any future refactor for this client.

I've added a fake response to one of our tests as you suggested earlier: b142aa2

This is an ongoing migration to the new Cloud API.
There is no difference for the end user.
Instead of setting the inTest boolean on the test
and having testing code in the implementation.
@vbotbuildovich
Copy link
Collaborator

CI test results

test results on build#61325
test_id test_kind job_url test_status passed
rptest.tests.compaction_recovery_test.CompactionRecoveryTest.test_index_recovery ducktape https://buildkite.com/redpanda/redpanda/builds/61325#0194b0a8-f6ca-4cba-ae0c-5c725fe9a641 FLAKY 1/2
rptest.tests.timequery_test.TimeQueryKafkaTest.test_timequery_below_start_offset ducktape https://buildkite.com/redpanda/redpanda/builds/61325#0194b0a8-f6cc-48ca-b061-3b4b3a9acef4 FLAKY 1/2

@@ -93,6 +94,25 @@ func TestLoadFlow(t *testing.T) {
"cloud.client_secret=" + tt.clientSecret,
},
}
handler := func() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "GetCurrentOrganization") {
Copy link
Contributor

Choose a reason for hiding this comment

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

NICE!

@r-vasquez r-vasquez merged commit c0f6536 into redpanda-data:dev Jan 30, 2025
23 checks passed
@r-vasquez
Copy link
Contributor Author

/backport v24.3.x

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants