From 397278356200014b666e46f3890d826d395d4c56 Mon Sep 17 00:00:00 2001 From: aniket866 Date: Sat, 31 Oct 2026 01:38:57 +0530 Subject: [PATCH 1/2] fix: handle TLSExplicit bool field Signed-off-by: aniket866 --- cmd/login.go | 6 ++++-- pkg/config/localconfig.go | 1 + pkg/connectors/microcks_client.go | 23 +++++++++++++++++++++-- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/cmd/login.go b/cmd/login.go index a68a437c..86429a5b 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -127,7 +127,8 @@ microcks login http://localhost:8080 --sso --sso-launch-browser=false if keycloakUrl == "null" { localConfig.UpsertServer(config.Server{ Server: server, - InsecureTLS: true, + InsecureTLS: globalClientOpts.InsecureTLS, + TLSExplicit: true, KeycloakEnable: false, }) fmt.Print("No login required...\n") @@ -178,7 +179,8 @@ microcks login http://localhost:8080 --sso --sso-launch-browser=false localConfig.UpsertServer(config.Server{ Server: server, - InsecureTLS: true, + InsecureTLS: globalClientOpts.InsecureTLS, + TLSExplicit: true, KeycloakEnable: true, }) } diff --git a/pkg/config/localconfig.go b/pkg/config/localconfig.go index ac7dd870..4d453749 100644 --- a/pkg/config/localconfig.go +++ b/pkg/config/localconfig.go @@ -58,6 +58,7 @@ type Server struct { Name string `yaml:"name"` Server string `yaml:"server"` InsecureTLS bool `yaml:"insecureTLS"` + TLSExplicit bool `yaml:"tlsExplicit,omitempty"` KeycloakEnable bool `yaml:"keycloakEnable"` } diff --git a/pkg/connectors/microcks_client.go b/pkg/connectors/microcks_client.go index f017ff10..f8afb244 100644 --- a/pkg/connectors/microcks_client.go +++ b/pkg/connectors/microcks_client.go @@ -200,7 +200,17 @@ func NewClient(opts ClientOptions) (MicrocksClient, error) { } c.ServerAddr = configCtx.Server.Server - c.InsecureTLS = configCtx.Server.InsecureTLS + // Honor the persisted insecureTLS only when it was written by the new + // login (TLSExplicit == true). Old login always wrote insecureTLS: true + // even without --insecure-tls, so we must not blindly trust it. + if configCtx.Server.TLSExplicit { + c.InsecureTLS = configCtx.Server.InsecureTLS + } else if configCtx.Server.InsecureTLS { + // Legacy config: insecureTLS was set unconditionally by older login. + // Warn the user and keep TLS verification enabled for safety. + log.Printf("Warning: saved context %q has insecureTLS: true but was written by an older microcks-cli that always persisted that value. TLS certificate verification will be enforced. Re-run 'microcks login' with --insecure-tls if needed.\n", configCtx.Name) + c.InsecureTLS = false + } c.AuthToken = configCtx.User.AuthToken c.RefreshToken = configCtx.User.RefreshToken @@ -226,8 +236,17 @@ func NewClient(opts ClientOptions) (MicrocksClient, error) { c.Verbose = opts.Verbose } - if config.InsecureTLS || len(config.CaCertPaths) > 0 { + // Build the HTTP transport. config.InsecureTLS is set by the --insecure-tls + // CLI flag and always takes precedence. c.InsecureTLS comes from the saved + // context and is honoured only when it was written by the new login code + // (TLSExplicit == true in the stored Server entry). + effectiveInsecure := config.InsecureTLS || c.InsecureTLS + if effectiveInsecure || len(config.CaCertPaths) > 0 { + // Temporarily set the global flag so CreateTLSConfig picks it up. + prevInsecure := config.InsecureTLS + config.InsecureTLS = effectiveInsecure tlsConfig := config.CreateTLSConfig() + config.InsecureTLS = prevInsecure tr := &http.Transport{ TLSClientConfig: tlsConfig, } From 383a1092f8d7c59404b35586715acd1033f92fdb Mon Sep 17 00:00:00 2001 From: aniket866 Date: Sat, 31 Oct 2026 02:55:07 +0530 Subject: [PATCH 2/2] feat/delete-command Signed-off-by: aniket866 --- cmd/cmd.go | 7 ++ cmd/service.go | 57 +++++++++++++- cmd/service_test.go | 105 +++++++++++++++++++++++++ pkg/connectors/microcks_client.go | 67 +++++++++++++++- pkg/connectors/microcks_client_test.go | 105 +++++++++++++++++++++++++ 5 files changed, 338 insertions(+), 3 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index 422b3ff0..6819e6be 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -62,6 +62,7 @@ func NewCommand() (*cobra.Command, error) { command.AddCommand(NewContextCommand(&clientOpts)) command.AddCommand(NewLoginCommand(&clientOpts)) command.AddCommand(NewLogoutCommand(&clientOpts)) + command.AddCommand(newDeleteAliasCommand(&clientOpts)) defaultLocalConfigPath, err := config.DefaultLocalConfigPath() if err != nil { @@ -81,3 +82,9 @@ func NewCommand() (*cobra.Command, error) { return command, nil } + +func newDeleteAliasCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command { + cmd := newServiceDeleteCommand(globalClientOpts) + cmd.Hidden = true + return cmd +} diff --git a/cmd/service.go b/cmd/service.go index 8bfd9ecc..a4968cf0 100644 --- a/cmd/service.go +++ b/cmd/service.go @@ -17,7 +17,9 @@ package cmd import ( + "bufio" "fmt" + "io" "os" "strings" "text/tabwriter" @@ -26,16 +28,25 @@ import ( "github.com/microcks/microcks-cli/pkg/errors" "github.com/microcks/microcks-cli/pkg/output" "github.com/spf13/cobra" + "golang.org/x/term" +) + +var ( + isTerminal = func() bool { + return term.IsTerminal(int(os.Stdin.Fd())) + } + stdinReader io.Reader = os.Stdin ) func NewServiceCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command { serviceCmd := &cobra.Command{ Use: "service", - Short: "List and inspect Microcks services", + Short: "List, inspect, and delete Microcks services", } serviceCmd.AddCommand(newServiceListCommand(globalClientOpts)) serviceCmd.AddCommand(newServiceGetCommand(globalClientOpts)) + serviceCmd.AddCommand(newServiceDeleteCommand(globalClientOpts)) return serviceCmd } @@ -150,3 +161,47 @@ func printServiceDetail(detail *connectors.ServiceDetail) error { } return nil } + +func newServiceDeleteCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command { + var force bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a Microcks service", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + serviceRef := args[0] + + if !force { + if !isTerminal() { + return errors.Wrapf(errors.KindUsage, "--force flag is required for non-interactive deletion") + } + fmt.Printf("Are you sure you want to delete service '%s'? [y/N]: ", serviceRef) + reader := bufio.NewReader(stdinReader) + input, err := reader.ReadString('\n') + if err != nil { + return errors.Wrap(errors.KindGeneric, err) + } + input = strings.TrimSpace(strings.ToLower(input)) + if input != "y" && input != "yes" { + fmt.Println("Service deletion aborted") + return nil + } + } + + mc, _, err := newCommandClient(globalClientOpts) + if err != nil { + return err + } + + if err := mc.DeleteService(serviceRef); err != nil { + return err + } + + fmt.Printf("Service '%s' deleted successfully\n", serviceRef) + return nil + }, + } + cmd.Flags().BoolVarP(&force, "force", "f", false, "Force deletion without confirmation prompt") + return cmd +} diff --git a/cmd/service_test.go b/cmd/service_test.go index 081a5dff..3868eadc 100644 --- a/cmd/service_test.go +++ b/cmd/service_test.go @@ -110,3 +110,108 @@ func executeCLIForTest(t *testing.T, args ...string) (string, error) { } return string(out), execErr } + +func TestServiceDeleteCommandRequiresForceInNonTerminal(t *testing.T) { + oldIsTerminal := isTerminal + isTerminal = func() bool { return false } + defer func() { isTerminal = oldIsTerminal }() + + _, err := executeCLIForTest(t, "service", "delete", "svc-1", "--microcksURL", "http://localhost:8080") + if err == nil { + t.Fatal("expected error when running in non-terminal without --force, got nil") + } + if !strings.Contains(err.Error(), "--force flag is required") { + t.Fatalf("unexpected error message: %v", err) + } +} + +func TestServiceDeleteCommandWithForce(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/services/svc-1" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if r.Method != http.MethodDelete { + t.Fatalf("unexpected method: %s", r.Method) + } + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + out, err := executeCLIForTest(t, "service", "delete", "svc-1", "--force", "--microcksURL", server.URL) + if err != nil { + t.Fatalf("command returned unexpected error: %v", err) + } + if !strings.Contains(out, "Service 'svc-1' deleted successfully") { + t.Fatalf("unexpected output: %s", out) + } +} + +func TestServiceDeleteCommandWithConfirmationAccepted(t *testing.T) { + oldIsTerminal := isTerminal + oldStdinReader := stdinReader + isTerminal = func() bool { return true } + stdinReader = strings.NewReader("y\n") + defer func() { + isTerminal = oldIsTerminal + stdinReader = oldStdinReader + }() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/services/svc-1" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if r.Method != http.MethodDelete { + t.Fatalf("unexpected method: %s", r.Method) + } + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + out, err := executeCLIForTest(t, "service", "delete", "svc-1", "--microcksURL", server.URL) + if err != nil { + t.Fatalf("command returned unexpected error: %v", err) + } + if !strings.Contains(out, "Service 'svc-1' deleted successfully") { + t.Fatalf("unexpected output: %s", out) + } +} + +func TestServiceDeleteCommandWithConfirmationAborted(t *testing.T) { + oldIsTerminal := isTerminal + oldStdinReader := stdinReader + isTerminal = func() bool { return true } + stdinReader = strings.NewReader("n\n") + defer func() { + isTerminal = oldIsTerminal + stdinReader = oldStdinReader + }() + + out, err := executeCLIForTest(t, "service", "delete", "svc-1", "--microcksURL", "http://localhost:8080") + if err != nil { + t.Fatalf("command returned unexpected error: %v", err) + } + if !strings.Contains(out, "Service deletion aborted") { + t.Fatalf("unexpected output: %s", out) + } +} + +func TestRootDeleteAliasCommandWithForce(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/services/svc-1" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if r.Method != http.MethodDelete { + t.Fatalf("unexpected method: %s", r.Method) + } + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + out, err := executeCLIForTest(t, "delete", "svc-1", "--force", "--microcksURL", server.URL) + if err != nil { + t.Fatalf("command returned unexpected error: %v", err) + } + if !strings.Contains(out, "Service 'svc-1' deleted successfully") { + t.Fatalf("unexpected output: %s", out) + } +} diff --git a/pkg/connectors/microcks_client.go b/pkg/connectors/microcks_client.go index f8afb244..664d422e 100644 --- a/pkg/connectors/microcks_client.go +++ b/pkg/connectors/microcks_client.go @@ -52,6 +52,7 @@ type MicrocksClient interface { SetOAuthToken(oauthToken string) ListServices(page int, size int) ([]Service, error) GetService(ref string) (*ServiceDetail, error) + DeleteService(ref string) error ListTestResults(serviceID string, page int, size int) ([]TestResultSummary, error) CreateTestResult(serviceID string, testEndpoint string, runnerType string, secretName string, timeout int64, filteredOperations string, operationsHeaders string, oAuth2Context string) (string, error) GetTestResult(testResultID string) (*TestResultSummary, error) @@ -414,7 +415,11 @@ func (c *microcksClient) redeemRefreshToken(auth config.Auth) (string, string, e if err != nil { return "", "", err } + // Apply effective insecure setting for Keycloak client initialization. + prevInsecure := config.InsecureTLS + config.InsecureTLS = config.InsecureTLS || c.InsecureTLS kc, err := NewKeycloakClientWithContext(c.ctx, keyCloakUrl, "", "") + config.InsecureTLS = prevInsecure if err != nil { return "", "", err } @@ -472,6 +477,53 @@ func (c *microcksClient) GetService(ref string) (*ServiceDetail, error) { return &detail, nil } +// DeleteService deletes a service by ID or name:version reference. +func (c *microcksClient) DeleteService(ref string) error { + id := ref + if strings.Contains(ref, ":") { + serviceID, err := c.resolveServiceID(ref) + if err != nil { + return err + } + id = serviceID + } + + rel := &url.URL{Path: "services/" + id} + u := c.APIURL.ResolveReference(rel) + + req, err := http.NewRequestWithContext(c.ctx, "DELETE", u.String(), nil) + if err != nil { + return errors.Wrap(errors.KindGeneric, fmt.Errorf("creating delete service request: %w", err)) + } + + req.Header.Set("Authorization", "Bearer "+c.AuthToken) + + config.DumpRequestIfRequired("Microcks for deleting service", req, false) + + resp, err := c.httpClient.Do(req) + if err != nil { + return errors.Wrap(errors.KindConnection, err) + } + defer resp.Body.Close() + + config.DumpResponseIfRequired("Microcks for deleting service", resp, true) + + body, err := io.ReadAll(resp.Body) + if err != nil { + return errors.Wrap(errors.KindConnection, fmt.Errorf("reading Microcks response: %w", err)) + } + + if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { + kind := errors.KindAPI + if resp.StatusCode == http.StatusNotFound { + kind = errors.KindNotFound + } + return errors.Wrapf(kind, "Microcks returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) + } + + return nil +} + func (c *microcksClient) ListTestResults(serviceID string, page int, size int) ([]TestResultSummary, error) { values := url.Values{} values.Set("page", strconv.Itoa(page)) @@ -492,6 +544,7 @@ func (c *microcksClient) resolveServiceID(ref string) (string, error) { if !ok || name == "" || version == "" { return "", errors.Wrapf(errors.KindUsage, "service reference %q must be :", ref) } + var matches []Service for page := 0; ; page++ { services, err := c.ListServices(page, serviceLookupPageSize) if err != nil { @@ -499,14 +552,24 @@ func (c *microcksClient) resolveServiceID(ref string) (string, error) { } for _, service := range services { if service.Name == name && service.Version == version { - return service.ID, nil + matches = append(matches, service) } } if len(services) < serviceLookupPageSize { break } } - return "", errors.Wrapf(errors.KindNotFound, "service %q does not exist", ref) + if len(matches) == 0 { + return "", errors.Wrapf(errors.KindNotFound, "service %q does not exist", ref) + } + if len(matches) > 1 { + var ids []string + for _, m := range matches { + ids = append(ids, fmt.Sprintf("%s (%s)", m.ID, m.Type)) + } + return "", errors.Wrapf(errors.KindUsage, "multiple services matched %q: %s; please specify by ID", ref, strings.Join(ids, ", ")) + } + return matches[0].ID, nil } func (c *microcksClient) getJSON(path string, query url.Values, out any, dumpLabel string) error { diff --git a/pkg/connectors/microcks_client_test.go b/pkg/connectors/microcks_client_test.go index d1a24512..e70e4e11 100644 --- a/pkg/connectors/microcks_client_test.go +++ b/pkg/connectors/microcks_client_test.go @@ -467,3 +467,108 @@ func TestGetFullTestResultClassifiesNotFound(t *testing.T) { t.Fatalf("KindOf = %v, want KindNotFound", got) } } + +func TestDeleteService_Success(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/services/svc-1" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + if r.Method != http.MethodDelete { + t.Fatalf("unexpected method: %s", r.Method) + } + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + client, err := NewMicrocksClient(server.URL) + if err != nil { + t.Fatalf("NewMicrocksClient returned error: %v", err) + } + err = client.DeleteService("svc-1") + if err != nil { + t.Fatalf("DeleteService returned unexpected error: %v", err) + } +} + +func TestDeleteService_NotFound(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + t.Fatalf("unexpected method: %s", r.Method) + } + http.Error(w, "service not found", http.StatusNotFound) + })) + defer server.Close() + + client, err := NewMicrocksClient(server.URL) + if err != nil { + t.Fatalf("NewMicrocksClient returned error: %v", err) + } + err = client.DeleteService("missing") + if err == nil { + t.Fatal("expected error, got nil") + } + if got := microckserrors.KindOf(err); got != microckserrors.KindNotFound { + t.Fatalf("KindOf = %v, want KindNotFound", got) + } +} + +func TestDeleteService_ResolveNameVersion(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/services": + if err := json.NewEncoder(w).Encode([]Service{{ + ID: "svc-1", + Name: "Test API", + Version: "1.0", + Type: "REST", + }}); err != nil { + t.Fatalf("failed to encode services response: %v", err) + } + case "/api/services/svc-1": + if r.Method != http.MethodDelete { + t.Fatalf("unexpected method: %s", r.Method) + } + w.WriteHeader(http.StatusNoContent) + default: + t.Fatalf("unexpected path: %s", r.URL.Path) + } + })) + defer server.Close() + + client, err := NewMicrocksClient(server.URL) + if err != nil { + t.Fatalf("NewMicrocksClient returned error: %v", err) + } + err = client.DeleteService("Test API:1.0") + if err != nil { + t.Fatalf("DeleteService returned unexpected error: %v", err) + } +} + +func TestDeleteService_MultipleMatches(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/api/services" { + if err := json.NewEncoder(w).Encode([]Service{ + {ID: "svc-1", Name: "Dup API", Version: "1.0", Type: "REST"}, + {ID: "svc-2", Name: "Dup API", Version: "1.0", Type: "SOAP"}, + }); err != nil { + t.Fatalf("failed to encode services response: %v", err) + } + return + } + t.Fatalf("unexpected path: %s", r.URL.Path) + })) + defer server.Close() + + client, err := NewMicrocksClient(server.URL) + if err != nil { + t.Fatalf("NewMicrocksClient returned error: %v", err) + } + err = client.DeleteService("Dup API:1.0") + if err == nil { + t.Fatal("expected error for multiple matches, got nil") + } + if got := microckserrors.KindOf(err); got != microckserrors.KindUsage { + t.Fatalf("KindOf = %v, want KindUsage", got) + } +}