Skip to content
Open
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
1 change: 1 addition & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
135 changes: 135 additions & 0 deletions cmd/services.go
Original file line number Diff line number Diff line change
@@ -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
}
40 changes: 40 additions & 0 deletions documentation/cmd/services.md
Original file line number Diff line number Diff line change
@@ -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 |
49 changes: 49 additions & 0 deletions pkg/connectors/microcks_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"`
Expand Down Expand Up @@ -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{}
Expand Down
98 changes: 98 additions & 0 deletions pkg/connectors/microcks_client_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package connectors

import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
Expand Down Expand Up @@ -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)
}
}