From b919acc8bcb27f40a6da0e5a3ebb00424cb3b731 Mon Sep 17 00:00:00 2001 From: puneeth_aditya_5656 Date: Wed, 6 May 2026 15:10:46 +0530 Subject: [PATCH] feat: add services list command Signed-off-by: puneeth_aditya_5656 --- cmd/cmd.go | 1 + cmd/services.go | 135 +++++++++++++++++++++++++ documentation/cmd/services.md | 40 ++++++++ pkg/connectors/microcks_client.go | 49 +++++++++ pkg/connectors/microcks_client_test.go | 98 ++++++++++++++++++ 5 files changed, 323 insertions(+) create mode 100644 cmd/services.go create mode 100644 documentation/cmd/services.md diff --git a/cmd/cmd.go b/cmd/cmd.go index 16df6b9..c0f750b 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -48,6 +48,7 @@ func NewCommad() *cobra.Command { command.AddCommand(NewContextCommand(&clientOpts)) command.AddCommand(NewLoginCommand(&clientOpts)) command.AddCommand(NewLogoutCommand(&clientOpts)) + command.AddCommand(NewServicesCommand(&clientOpts)) defaultLocalConfigPath, err := config.DefaultLocalConfigPath() errors.CheckError(err) diff --git a/cmd/services.go b/cmd/services.go new file mode 100644 index 0000000..3660beb --- /dev/null +++ b/cmd/services.go @@ -0,0 +1,135 @@ +/* + * Copyright The Microcks Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package cmd + +import ( + "fmt" + "os" + "strings" + "text/tabwriter" + + "github.com/microcks/microcks-cli/pkg/config" + "github.com/microcks/microcks-cli/pkg/connectors" + "github.com/microcks/microcks-cli/pkg/errors" + "github.com/spf13/cobra" +) + +func NewServicesCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command { + servicesCmd := &cobra.Command{ + Use: "services", + Short: "Manage Microcks services", + Long: `Manage Microcks services`, + Run: func(cmd *cobra.Command, args []string) { + cmd.HelpFunc()(cmd, args) + }, + } + + servicesCmd.AddCommand(newServicesListCommand(globalClientOpts)) + return servicesCmd +} + +func newServicesListCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command { + var ( + page int + size int + ) + + listCmd := &cobra.Command{ + Use: "list", + Short: "List services imported in Microcks", + Long: `List services imported in Microcks`, + Example: `# List services using current context +microcks services list + +# List services with pagination +microcks services list --page 1 --size 10`, + Run: func(cmd *cobra.Command, args []string) { + config.InsecureTLS = globalClientOpts.InsecureTLS + config.CaCertPaths = globalClientOpts.CaCertPaths + config.Verbose = globalClientOpts.Verbose + + var mc connectors.MicrocksClient + + if globalClientOpts.ServerAddr != "" && globalClientOpts.ClientId != "" && globalClientOpts.ClientSecret != "" { + mc = connectors.NewMicrocksClient(globalClientOpts.ServerAddr) + + keycloakURL, err := mc.GetKeycloakURL() + if err != nil { + fmt.Printf("Got error when invoking Microcks client retrieving config: %s", err) + os.Exit(1) + } + + var oauthToken string = "unauthenticated-token" + if keycloakURL != "null" { + kc := connectors.NewKeycloakClient(keycloakURL, globalClientOpts.ClientId, globalClientOpts.ClientSecret) + oauthToken, err = kc.ConnectAndGetToken() + if err != nil { + fmt.Printf("Got error when invoking Keycloak client: %s", err) + os.Exit(1) + } + } + mc.SetOAuthToken(oauthToken) + } else { + localConfig, err := config.ReadLocalConfig(globalClientOpts.ConfigPath) + if err != nil { + fmt.Println(err) + return + } + + if localConfig == nil { + fmt.Println("Please login to perform operation...") + return + } + + if globalClientOpts.Context == "" { + globalClientOpts.Context = localConfig.CurrentContext + } + + mc, err = connectors.NewClient(*globalClientOpts) + if err != nil { + fmt.Printf("error %v", err) + return + } + } + + services, err := mc.GetServices(page, size) + if err != nil { + fmt.Printf("Got error when listing services: %s", err) + os.Exit(1) + } + + if len(services) == 0 { + fmt.Println("No services found") + return + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + defer func() { _ = w.Flush() }() + columnNames := []string{"NAME", "VERSION", "TYPE"} + _, err = fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t")) + errors.CheckError(err) + + for _, svc := range services { + _, err = fmt.Fprintf(w, "%s\t%s\t%s\n", svc.Name, svc.Version, svc.Type) + errors.CheckError(err) + } + }, + } + + listCmd.Flags().IntVar(&page, "page", 0, "Page of services to retrieve (0-indexed)") + listCmd.Flags().IntVar(&size, "size", 20, "Number of services per page") + return listCmd +} diff --git a/documentation/cmd/services.md b/documentation/cmd/services.md new file mode 100644 index 0000000..a1545e7 --- /dev/null +++ b/documentation/cmd/services.md @@ -0,0 +1,40 @@ +## `microcks services list` – List Services Imported in Microcks + +Lists the services (APIs and mocks) currently imported in the connected Microcks instance. + +### Usage +```bash +microcks services list [flags] +``` + +### Flags +| Flag | Default | Description | +| -------- | ------- | -------------------------------------- | +| `--page` | `0` | Page of services to retrieve (0-indexed) | +| `--size` | `20` | Number of services per page | + +### Examples +```bash +# List services using the current context +microcks services list + +# List the second page of results with 10 services per page +microcks services list --page 1 --size 10 + +# List services against a specific Microcks instance +microcks services list --microcksURL http://localhost:8585 \ + --keycloakClientId my-client \ + --keycloakClientSecret my-secret +``` + +### Options Inherited from Parent Commands +| Flag | Description | +| ------------------------ | ------------------------------------------- | +| `--config` | Path to Microcks config file | +| `--microcks-context` | Name of the Microcks context to use | +| `--verbose` | Produce dumps of HTTP exchanges | +| `--insecure-tls` | Allow insecure HTTPS connections | +| `--caCerts` | Comma-separated paths of CA cert files | +| `--keycloakClientId` | Keycloak Realm Service Account ClientId | +| `--keycloakClientSecret` | Keycloak Realm Service Account ClientSecret | +| `--microcksURL` | Microcks API URL | diff --git a/pkg/connectors/microcks_client.go b/pkg/connectors/microcks_client.go index f76884b..80d8a6c 100644 --- a/pkg/connectors/microcks_client.go +++ b/pkg/connectors/microcks_client.go @@ -52,6 +52,7 @@ type MicrocksClient interface { GetTestResult(testResultID string) (*TestResultSummary, error) UploadArtifact(specificationFilePath string, mainArtifact bool) (string, error) DownloadArtifact(artifactURL string, mainArtifact bool, secret string) (string, error) + GetServices(page, size int) ([]ServiceSummary, error) } // TestResultSummary represents a simple view on Microcks TestResult @@ -67,6 +68,14 @@ type TestResultSummary struct { InProgress bool `json:"inProgress"` } +// ServiceSummary represents a simple view on a Microcks Service +type ServiceSummary struct { + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + Type string `json:"type"` +} + // HeaderDTO represents an operation header passed for Test type HeaderDTO struct { Name string `json:"name"` @@ -535,6 +544,46 @@ func (c *microcksClient) DownloadArtifact(artifactURL string, mainArtifact bool, return string(respBody), err } +func (c *microcksClient) GetServices(page, size int) ([]ServiceSummary, error) { + rel := &url.URL{ + Path: "services", + RawQuery: fmt.Sprintf("page=%d&size=%d", page, size), + } + u := c.APIURL.ResolveReference(rel) + + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+c.AuthToken) + + // Dump request if verbose required. + config.DumpRequestIfRequired("Microcks for listing services", req, false) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Dump response if verbose required. + config.DumpResponseIfRequired("Microcks for listing services", resp, true) + + body, err := io.ReadAll(resp.Body) + if err != nil { + panic(err.Error()) + } + + var services []ServiceSummary + if err := json.Unmarshal(body, &services); err != nil { + return nil, fmt.Errorf("failed to parse services response: %w", err) + } + + return services, nil +} + func ensureValidOperationsList(filteredOperations string) bool { // Unmarshal using a generic interface var list = []string{} diff --git a/pkg/connectors/microcks_client_test.go b/pkg/connectors/microcks_client_test.go index 9e9b6c8..df9af70 100644 --- a/pkg/connectors/microcks_client_test.go +++ b/pkg/connectors/microcks_client_test.go @@ -1,6 +1,7 @@ package connectors import ( + "encoding/json" "net/http" "net/http/httptest" "strings" @@ -41,3 +42,100 @@ func TestDownloadArtifactReturnsResponseBody(t *testing.T) { t.Fatalf("expected response body %q, got %q", expectedBody, msg) } } + +func TestGetServices(t *testing.T) { + services := []ServiceSummary{ + {ID: "1", Name: "Petstore API", Version: "1.0", Type: "REST"}, + {ID: "2", Name: "HelloService", Version: "0.9", Type: "SOAP"}, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/services" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if r.Method != http.MethodGet { + t.Fatalf("unexpected method: %s", r.Method) + } + if got := r.URL.Query().Get("page"); got != "0" { + t.Fatalf("unexpected page: %s", got) + } + if got := r.URL.Query().Get("size"); got != "20" { + t.Fatalf("unexpected size: %s", got) + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(services) + })) + defer server.Close() + + client := NewMicrocksClient(server.URL) + result, err := client.GetServices(0, 20) + if err != nil { + t.Fatalf("GetServices returned error: %v", err) + } + if len(result) != 2 { + t.Fatalf("expected 2 services, got %d", len(result)) + } + if result[0].Name != "Petstore API" { + t.Fatalf("expected first service name %q, got %q", "Petstore API", result[0].Name) + } + if result[0].Version != "1.0" { + t.Fatalf("expected first service version %q, got %q", "1.0", result[0].Version) + } + if result[0].Type != "REST" { + t.Fatalf("expected first service type %q, got %q", "REST", result[0].Type) + } +} + +func TestGetServicesEmpty(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte("[]")) + })) + defer server.Close() + + client := NewMicrocksClient(server.URL) + result, err := client.GetServices(0, 20) + if err != nil { + t.Fatalf("GetServices returned error: %v", err) + } + if len(result) != 0 { + t.Fatalf("expected empty slice, got %d services", len(result)) + } +} + +func TestGetServicesInvalidJSON(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte("not-json")) + })) + defer server.Close() + + client := NewMicrocksClient(server.URL) + _, err := client.GetServices(0, 20) + if err == nil { + t.Fatal("expected error for invalid JSON, got nil") + } + if !strings.Contains(err.Error(), "failed to parse services response") { + t.Fatalf("unexpected error message: %v", err) + } +} + +func TestGetServicesPagination(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.URL.Query().Get("page"); got != "2" { + t.Fatalf("unexpected page: %s", got) + } + if got := r.URL.Query().Get("size"); got != "5" { + t.Fatalf("unexpected size: %s", got) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte("[]")) + })) + defer server.Close() + + client := NewMicrocksClient(server.URL) + _, err := client.GetServices(2, 5) + if err != nil { + t.Fatalf("GetServices returned error: %v", err) + } +}