-
Notifications
You must be signed in to change notification settings - Fork 22
Add confluent endpoint list command #3268
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package endpoint | ||
|
|
||
| import ( | ||
| "github.com/spf13/cobra" | ||
|
|
||
| pcmd "github.com/confluentinc/cli/v4/pkg/cmd" | ||
| ) | ||
|
|
||
| type out struct { | ||
| Id string `human:"ID" serialized:"id"` | ||
| Cloud string `human:"Cloud" serialized:"cloud"` | ||
| Region string `human:"Region" serialized:"region"` | ||
| Service string `human:"Service" serialized:"service"` | ||
| IsPrivate bool `human:"Is Private" serialized:"is_private"` | ||
| ConnectionType string `human:"Connection Type,omitempty" serialized:"connection_type,omitempty"` | ||
| Endpoint string `human:"Endpoint URL" serialized:"endpoint"` | ||
| EndpointType string `human:"Endpoint Type,omitempty" serialized:"endpoint_type,omitempty"` | ||
| Environment string `human:"Environment" serialized:"environment"` | ||
| Resource string `human:"Resource,omitempty" serialized:"resource,omitempty"` | ||
| Gateway string `human:"Gateway,omitempty" serialized:"gateway,omitempty"` | ||
| AccessPoint string `human:"Access Point,omitempty" serialized:"access_point,omitempty"` | ||
| } | ||
|
|
||
| type command struct { | ||
| *pcmd.AuthenticatedCLICommand | ||
| } | ||
|
|
||
| func New(prerunner pcmd.PreRunner) *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "endpoint", | ||
| Short: "Manage Confluent Cloud endpoints.", | ||
| Annotations: map[string]string{pcmd.RunRequirement: pcmd.RequireCloudLogin}, | ||
| } | ||
|
|
||
| c := &command{pcmd.NewAuthenticatedCLICommand(cmd, prerunner)} | ||
|
|
||
| cmd.AddCommand(c.newListCommand()) | ||
|
|
||
| return cmd | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| package endpoint | ||
|
|
||
| import ( | ||
| "sort" | ||
| "strings" | ||
|
|
||
| "github.com/spf13/cobra" | ||
|
|
||
| pcmd "github.com/confluentinc/cli/v4/pkg/cmd" | ||
| "github.com/confluentinc/cli/v4/pkg/output" | ||
| ) | ||
|
|
||
| func (c *command) newListCommand() *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "list", | ||
| Short: "List endpoints.", | ||
| Args: cobra.NoArgs, | ||
| RunE: c.list, | ||
| } | ||
|
|
||
| // Required flags | ||
| cmd.Flags().String("service", "", "Filter by service type (KAFKA, KSQL, SCHEMA_REGISTRY, etc.).") | ||
| cobra.CheckErr(cmd.MarkFlagRequired("service")) | ||
| pcmd.AddEnvironmentFlag(cmd, c.AuthenticatedCLICommand) | ||
|
|
||
ejagade marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Optional filter flags | ||
| cmd.Flags().String("cloud", "", "Filter by cloud provider (AWS, GCP, AZURE).") | ||
| cmd.Flags().String("region", "", "Filter by region.") | ||
| cmd.Flags().Bool("is-private", false, "Filter by privacy (true for private, false for public).") | ||
| cmd.Flags().String("resource", "", "Filter by resource ID.") | ||
|
Comment on lines
+26
to
+30
|
||
|
|
||
| // Standard flags | ||
| pcmd.AddContextFlag(cmd, c.CLICommand) | ||
| pcmd.AddOutputFlag(cmd) | ||
|
|
||
| return cmd | ||
| } | ||
|
|
||
| func (c *command) list(cmd *cobra.Command, _ []string) error { | ||
| // Get environment ID | ||
| environmentId, err := c.Context.EnvironmentId() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Get required service flag | ||
| service, err := cmd.Flags().GetString("service") | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Get optional filter flags | ||
| cloud, err := cmd.Flags().GetString("cloud") | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| region, err := cmd.Flags().GetString("region") | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| resource, err := cmd.Flags().GetString("resource") | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Handle is-private flag (optional boolean) | ||
| // nil means no filter, &true means filter for private, &false means filter for public | ||
| var isPrivate *bool | ||
| if cmd.Flags().Changed("is-private") { | ||
| val, err := cmd.Flags().GetBool("is-private") | ||
| if err != nil { | ||
| return err | ||
| } | ||
| isPrivate = &val | ||
| } | ||
|
|
||
| // Convert to uppercase for API consistency | ||
| service = strings.ToUpper(service) | ||
| if cloud != "" { | ||
| cloud = strings.ToUpper(cloud) | ||
| } | ||
|
|
||
| // Call API via client wrapper | ||
| endpoints, err := c.V2Client.ListEndpoints(environmentId, cloud, region, service, isPrivate, resource) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Sort by Cloud, then Region, then ID for deterministic output | ||
| sort.Slice(endpoints, func(i, j int) bool { | ||
| if endpoints[i].GetCloud() != endpoints[j].GetCloud() { | ||
| return endpoints[i].GetCloud() < endpoints[j].GetCloud() | ||
| } | ||
| if endpoints[i].GetRegion() != endpoints[j].GetRegion() { | ||
| return endpoints[i].GetRegion() < endpoints[j].GetRegion() | ||
| } | ||
| return endpoints[i].GetId() < endpoints[j].GetId() | ||
| }) | ||
|
|
||
| // Build output | ||
| list := output.NewList(cmd) | ||
| for _, endpoint := range endpoints { | ||
| out := &out{ | ||
| Id: endpoint.GetId(), | ||
| Cloud: endpoint.GetCloud(), | ||
| Region: endpoint.GetRegion(), | ||
| Service: endpoint.GetService(), | ||
| IsPrivate: endpoint.GetIsPrivate(), | ||
| Endpoint: endpoint.GetEndpoint(), | ||
| } | ||
|
|
||
| // Add environment if present | ||
| if endpoint.Environment != nil { | ||
| out.Environment = endpoint.Environment.GetId() | ||
| } | ||
|
|
||
| // Add optional fields if present | ||
| if endpoint.ConnectionType != nil { | ||
| out.ConnectionType = endpoint.GetConnectionType() | ||
| } | ||
| if endpoint.EndpointType != nil { | ||
| out.EndpointType = endpoint.GetEndpointType() | ||
| } | ||
| if endpoint.Resource != nil { | ||
| out.Resource = endpoint.Resource.Id | ||
| } | ||
| if endpoint.Gateway != nil { | ||
| out.Gateway = endpoint.Gateway.Id | ||
| } | ||
| if endpoint.AccessPoint != nil { | ||
| out.AccessPoint = endpoint.AccessPoint.Id | ||
| } | ||
|
|
||
| list.Add(out) | ||
| } | ||
|
|
||
| // Sort and filter fields for display | ||
| list.Sort(false) | ||
| list.Filter([]string{"Id", "Cloud", "Region", "Service", "IsPrivate", "ConnectionType", "Endpoint", "EndpointType", "Environment", "Resource", "Gateway", "AccessPoint"}) | ||
|
|
||
| return list.Print() | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| package ccloudv2 | ||
|
|
||
| import ( | ||
| "context" | ||
| "net/http" | ||
|
|
||
| endpointv1 "github.com/confluentinc/ccloud-sdk-go-v2/endpoint/v1" | ||
|
|
||
| "github.com/confluentinc/cli/v4/pkg/errors" | ||
| ) | ||
|
|
||
| func newEndpointClient(httpClient *http.Client, url, userAgent string, unsafeTrace bool) *endpointv1.APIClient { | ||
| cfg := endpointv1.NewConfiguration() | ||
| cfg.Debug = unsafeTrace | ||
| cfg.HTTPClient = httpClient | ||
| cfg.Servers = endpointv1.ServerConfigurations{{URL: url}} | ||
| cfg.UserAgent = userAgent | ||
|
|
||
| return endpointv1.NewAPIClient(cfg) | ||
| } | ||
|
|
||
| func (c *Client) endpointApiContext() context.Context { | ||
| return context.WithValue(context.Background(), endpointv1.ContextAccessToken, c.cfg.Context().GetAuthToken()) | ||
| } | ||
|
|
||
| func (c *Client) ListEndpoints(environment, cloud, region, service string, isPrivate *bool, resource string) ([]endpointv1.EndpointV1Endpoint, error) { | ||
| var list []endpointv1.EndpointV1Endpoint | ||
|
|
||
| done := false | ||
| pageToken := "" | ||
| for !done { | ||
| page, err := c.executeListEndpoints(environment, pageToken, cloud, region, service, isPrivate, resource) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| list = append(list, page.GetData()...) | ||
|
|
||
| pageToken, done, err = extractNextPageToken(page.GetMetadata().Next) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| } | ||
| return list, nil | ||
| } | ||
|
|
||
| func (c *Client) executeListEndpoints(environment, pageToken, cloud, region, service string, isPrivate *bool, resource string) (endpointv1.EndpointV1EndpointList, error) { | ||
| req := c.EndpointClient.EndpointsEndpointV1Api.ListEndpointV1Endpoints(c.endpointApiContext()). | ||
| Environment(environment). | ||
| PageSize(ccloudV2ListPageSize) | ||
|
|
||
| // Add optional filters | ||
| if cloud != "" { | ||
| req = req.Cloud(cloud) | ||
| } | ||
| if region != "" { | ||
| req = req.Region(region) | ||
| } | ||
| if service != "" { | ||
| req = req.Service(service) | ||
| } | ||
| if isPrivate != nil { | ||
| req = req.IsPrivate(*isPrivate) | ||
| } | ||
| if resource != "" { | ||
| req = req.Resource(resource) | ||
| } | ||
| if pageToken != "" { | ||
| req = req.PageToken(pageToken) | ||
| } | ||
|
|
||
| resp, httpResp, err := req.Execute() | ||
| return resp, errors.CatchCCloudV2Error(err, httpResp) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The top-level endpoint command is gated by RequireCloudLogin, which allows API-key based cloud contexts. This command uses V2Client/ListEndpoints which authenticates with an OAuth access token from the context; API-key contexts generally won't have that token and the request will fail. Use RequireNonAPIKeyCloudLogin (as done by other V2 Cloud resource commands) or otherwise implement API-key auth for the Endpoint API client.