From d0538087927aa8985c5dad80ae1899b1d12bab46 Mon Sep 17 00:00:00 2001 From: cotishq Date: Tue, 5 May 2026 19:00:39 +0530 Subject: [PATCH 1/2] fix: parse import-url suffixes from right Signed-off-by: cotishq --- cmd/artifact_specifier.go | 79 ++++++++++++++++++++++++++ cmd/artifact_specifier_test.go | 101 +++++++++++++++++++++++++++++++++ cmd/context_test.go | 3 + cmd/import.go | 22 ++----- cmd/importURL.go | 31 +--------- documentation/cmd/importURL.md | 14 +++++ 6 files changed, 203 insertions(+), 47 deletions(-) create mode 100644 cmd/artifact_specifier.go create mode 100644 cmd/artifact_specifier_test.go diff --git a/cmd/artifact_specifier.go b/cmd/artifact_specifier.go new file mode 100644 index 00000000..5869fadb --- /dev/null +++ b/cmd/artifact_specifier.go @@ -0,0 +1,79 @@ +/* + * 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 ( + "strconv" + "strings" +) + +// parseImportURLSpecifier parses an import-url argument of the form: +// [:[:]] +// +// It is intentionally parsed from the right side so that normal URLs containing +// additional ':' characters (scheme, ports, etc.) are preserved unchanged unless +// they end with the supported suffixes. +func parseImportURLSpecifier(spec string) (url string, mainArtifact bool, secret string) { + mainArtifact = true + + lastColon := strings.LastIndex(spec, ":") + if lastColon == -1 { + return spec, mainArtifact, "" + } + + tail := spec[lastColon+1:] + if b, err := strconv.ParseBool(tail); err == nil { + return spec[:lastColon], b, "" + } + + // Might be :: — only treat it as such if the second-to-last + // segment parses as bool. + secretCandidate := tail + rest := spec[:lastColon] + secondColon := strings.LastIndex(rest, ":") + if secondColon == -1 { + return spec, mainArtifact, "" + } + boolCandidate := rest[secondColon+1:] + if b, err := strconv.ParseBool(boolCandidate); err == nil { + return rest[:secondColon], b, secretCandidate + } + + return spec, mainArtifact, "" +} + +// parseImportFileSpecifier parses an import argument of the form: +// [:] +// +// Like parseImportURLSpecifier, it parses from the right to avoid breaking +// paths that may contain ':' characters. +func parseImportFileSpecifier(spec string) (path string, mainArtifact bool) { + mainArtifact = true + + lastColon := strings.LastIndex(spec, ":") + if lastColon == -1 { + return spec, mainArtifact + } + + tail := spec[lastColon+1:] + if b, err := strconv.ParseBool(tail); err == nil { + return spec[:lastColon], b + } + + return spec, mainArtifact +} + diff --git a/cmd/artifact_specifier_test.go b/cmd/artifact_specifier_test.go new file mode 100644 index 00000000..cda3aea9 --- /dev/null +++ b/cmd/artifact_specifier_test.go @@ -0,0 +1,101 @@ +package cmd + +import "testing" + +func TestParseImportURLSpecifier_PreservesPortAndPathWithMainArtifactSuffix(t *testing.T) { + in := "http://localhost:8080/spec.yaml:true" + url, main, secret := parseImportURLSpecifier(in) + + if url != "http://localhost:8080/spec.yaml" { + t.Fatalf("url mismatch: got %q", url) + } + if main != true { + t.Fatalf("mainArtifact mismatch: got %v", main) + } + if secret != "" { + t.Fatalf("secret mismatch: got %q", secret) + } +} + +func TestParseImportURLSpecifier_PreservesQueryAndFragment(t *testing.T) { + in := "https://example.com:8443/spec.yaml?x=1#frag:false" + url, main, secret := parseImportURLSpecifier(in) + + if url != "https://example.com:8443/spec.yaml?x=1#frag" { + t.Fatalf("url mismatch: got %q", url) + } + if main != false { + t.Fatalf("mainArtifact mismatch: got %v", main) + } + if secret != "" { + t.Fatalf("secret mismatch: got %q", secret) + } +} + +func TestParseImportURLSpecifier_WithSecretSuffix(t *testing.T) { + in := "http://localhost:8080/spec.yaml:true:mySecret" + url, main, secret := parseImportURLSpecifier(in) + + if url != "http://localhost:8080/spec.yaml" { + t.Fatalf("url mismatch: got %q", url) + } + if main != true { + t.Fatalf("mainArtifact mismatch: got %v", main) + } + if secret != "mySecret" { + t.Fatalf("secret mismatch: got %q", secret) + } +} + +func TestParseImportURLSpecifier_NoSuffixes_Unchanged(t *testing.T) { + in := "http://localhost:8080/spec.yaml" + url, main, secret := parseImportURLSpecifier(in) + + if url != in { + t.Fatalf("url mismatch: got %q", url) + } + if main != true { + t.Fatalf("mainArtifact mismatch: got %v", main) + } + if secret != "" { + t.Fatalf("secret mismatch: got %q", secret) + } +} + +func TestParseImportURLSpecifier_PortOnly_NoSuffixes_Unchanged(t *testing.T) { + in := "http://localhost:8080/spec.yaml:1234" + url, main, secret := parseImportURLSpecifier(in) + + if url != in { + t.Fatalf("url mismatch: got %q", url) + } + if main != true { + t.Fatalf("mainArtifact mismatch: got %v", main) + } + if secret != "" { + t.Fatalf("secret mismatch: got %q", secret) + } +} + +func TestParseImportFileSpecifier_SuffixBool(t *testing.T) { + in := "./specs/openapi.yaml:false" + path, main := parseImportFileSpecifier(in) + if path != "./specs/openapi.yaml" { + t.Fatalf("path mismatch: got %q", path) + } + if main != false { + t.Fatalf("mainArtifact mismatch: got %v", main) + } +} + +func TestParseImportFileSpecifier_NoSuffix_Unchanged(t *testing.T) { + in := "./specs/openapi.yaml" + path, main := parseImportFileSpecifier(in) + if path != in { + t.Fatalf("path mismatch: got %q", path) + } + if main != true { + t.Fatalf("mainArtifact mismatch: got %v", main) + } +} + diff --git a/cmd/context_test.go b/cmd/context_test.go index 2b8a2b89..80af9715 100644 --- a/cmd/context_test.go +++ b/cmd/context_test.go @@ -19,6 +19,7 @@ package cmd import ( "encoding/json" "os" + "path/filepath" "testing" "github.com/microcks/microcks-cli/pkg/config" @@ -99,6 +100,8 @@ users: const testConfigFilePath = "./testdata/local.config" func TestDeleteContext(t *testing.T) { + require.NoError(t, os.MkdirAll(filepath.Dir(testConfigFilePath), 0o755)) + //write the test config file require.NoError(t, os.MkdirAll("./testdata", 0o750)) err := os.WriteFile(testConfigFilePath, []byte(testConfig), os.ModePerm) diff --git a/cmd/import.go b/cmd/import.go index 99bbcc6c..96a7de78 100644 --- a/cmd/import.go +++ b/cmd/import.go @@ -18,7 +18,6 @@ package cmd import ( "fmt" "os" - "strconv" "strings" "github.com/microcks/microcks-cli/pkg/config" @@ -126,21 +125,10 @@ func NewImportCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command sepSpecificationFiles := strings.Split(specificationFiles, ",") results := make([]artifactImportResult, 0, len(sepSpecificationFiles)) for _, f := range sepSpecificationFiles { - mainArtifact := true - var err error - - // Check if mainArtifact flag is provided. - if strings.Contains(f, ":") { - pathAndMainArtifact := strings.Split(f, ":") - f = pathAndMainArtifact[0] - mainArtifact, err = strconv.ParseBool(pathAndMainArtifact[1]) - if err != nil { - return errors.Wrapf(errors.KindUsage, "cannot parse %q as artifact primary flag", pathAndMainArtifact[1]) - } - } + path, mainArtifact := parseImportFileSpecifier(f) // Try uploading this artifact. - msg, err := mc.UploadArtifact(f, mainArtifact) + msg, err := mc.UploadArtifact(path, mainArtifact) if err != nil { return err } @@ -173,13 +161,11 @@ func NewImportCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command } // Normalize file path to match the watcher fsnotify events format. - if strings.HasPrefix(f, "./") { - f = strings.TrimPrefix(f, "./") - } + path = strings.TrimPrefix(path, "./") // Upsert entry. watchCfg.UpsertEntry(config.WatchEntry{ - FilePath: f, + FilePath: path, Context: []string{globalClientOpts.Context}, MainArtifact: mainArtifact, }) diff --git a/cmd/importURL.go b/cmd/importURL.go index 343145a2..08e4d0db 100644 --- a/cmd/importURL.go +++ b/cmd/importURL.go @@ -18,7 +18,6 @@ package cmd import ( "fmt" - "strconv" "strings" "github.com/microcks/microcks-cli/pkg/connectors" @@ -45,13 +44,10 @@ func NewImportURLCommand(globalClientOpts *connectors.ClientOptions) *cobra.Comm } sepSpecificationFiles := strings.Split(specificationFiles, ",") for _, f := range sepSpecificationFiles { - mainArtifact := true - secret := "" - - f, mainArtifact, secret = parseImportURLArg(f) + artifactURL, mainArtifact, secret := parseImportURLSpecifier(f) // Try downloading the artifcat - msg, err := mc.DownloadArtifact(f, mainArtifact, secret) + msg, err := mc.DownloadArtifact(artifactURL, mainArtifact, secret) if err != nil { return err } @@ -63,26 +59,3 @@ func NewImportURLCommand(globalClientOpts *connectors.ClientOptions) *cobra.Comm return importURLCmd } - -func parseImportURLArg(f string) (string, bool, string) { - mainArtifact := true - secret := "" - - // Check if URL starts with https or http - if strings.HasPrefix(f, "https://") || strings.HasPrefix(f, "http://") { - parts := strings.Split(f, ":") - n := len(parts) - - for i := n - 1; i >= 2; i-- { - if val, parseErr := strconv.ParseBool(parts[i]); parseErr == nil { - mainArtifact = val - if i+1 < n { - secret = strings.Join(parts[i+1:], ":") - } - f = strings.Join(parts[:i], ":") - break - } - } - } - return f, mainArtifact, secret -} diff --git a/documentation/cmd/importURL.md b/documentation/cmd/importURL.md index e5a60c58..1e2c6c09 100644 --- a/documentation/cmd/importURL.md +++ b/documentation/cmd/importURL.md @@ -6,6 +6,14 @@ Imports API specification files (OpenAPI, AsyncAPI, etc.) hosted at a remote URL microcks import-url , [flags] ``` +### URL suffix parsing +You can optionally append metadata suffixes to each URL: + +- `:
` where `
` is `true` or `false` +- `:
:` to additionally specify a secret name + +The CLI parses these suffixes from the **rightmost** `:` characters only, so normal URLs containing `:` (scheme, ports, etc.) are preserved. + ### Example ```bash # Import a single artifact (marked as main) @@ -14,6 +22,12 @@ microcks import-url https://example.com/openapi.yaml # Specify mainArtifact flag for each file microcks import-url https://example.com/spec1.yaml:true,https://example.com/spec2.yaml:false +# URL with port + :main suffix (port/path are preserved) +microcks import-url http://localhost:8080/spec.yaml:true + +# URL with port + :main + :secret +microcks import-url http://localhost:8080/spec.yaml:true:mySecret + # Import specification to microcks without logining to microcks microcks import-url https://example.com/openapi.yaml \ --microcksURL \ From 6ce5e355986a418e46db9c01338eb8ff8c31f8c8 Mon Sep 17 00:00:00 2001 From: cotishq Date: Fri, 21 Aug 2026 23:03:36 +0530 Subject: [PATCH 2/2] fix: use right-side parse for import file specifier Signed-off-by: cotishq --- cmd/artifact_specifier.go | 43 ++-------------- cmd/artifact_specifier_test.go | 92 ++++++---------------------------- cmd/context_test.go | 3 -- cmd/importURL.go | 32 +++++++++++- documentation/cmd/importURL.md | 14 ------ 5 files changed, 50 insertions(+), 134 deletions(-) diff --git a/cmd/artifact_specifier.go b/cmd/artifact_specifier.go index 5869fadb..085de8db 100644 --- a/cmd/artifact_specifier.go +++ b/cmd/artifact_specifier.go @@ -21,46 +21,12 @@ import ( "strings" ) -// parseImportURLSpecifier parses an import-url argument of the form: -// [:[:]] -// -// It is intentionally parsed from the right side so that normal URLs containing -// additional ':' characters (scheme, ports, etc.) are preserved unchanged unless -// they end with the supported suffixes. -func parseImportURLSpecifier(spec string) (url string, mainArtifact bool, secret string) { - mainArtifact = true - - lastColon := strings.LastIndex(spec, ":") - if lastColon == -1 { - return spec, mainArtifact, "" - } - - tail := spec[lastColon+1:] - if b, err := strconv.ParseBool(tail); err == nil { - return spec[:lastColon], b, "" - } - - // Might be :: — only treat it as such if the second-to-last - // segment parses as bool. - secretCandidate := tail - rest := spec[:lastColon] - secondColon := strings.LastIndex(rest, ":") - if secondColon == -1 { - return spec, mainArtifact, "" - } - boolCandidate := rest[secondColon+1:] - if b, err := strconv.ParseBool(boolCandidate); err == nil { - return rest[:secondColon], b, secretCandidate - } - - return spec, mainArtifact, "" -} - // parseImportFileSpecifier parses an import argument of the form: -// [:] // -// Like parseImportURLSpecifier, it parses from the right to avoid breaking -// paths that may contain ':' characters. +// [:] +// +// It parses from the right to avoid breaking paths that may contain ':' +// characters (e.g. Windows absolute paths like C:\...). func parseImportFileSpecifier(spec string) (path string, mainArtifact bool) { mainArtifact = true @@ -76,4 +42,3 @@ func parseImportFileSpecifier(spec string) (path string, mainArtifact bool) { return spec, mainArtifact } - diff --git a/cmd/artifact_specifier_test.go b/cmd/artifact_specifier_test.go index cda3aea9..f8876239 100644 --- a/cmd/artifact_specifier_test.go +++ b/cmd/artifact_specifier_test.go @@ -1,82 +1,23 @@ +/* + * 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 "testing" -func TestParseImportURLSpecifier_PreservesPortAndPathWithMainArtifactSuffix(t *testing.T) { - in := "http://localhost:8080/spec.yaml:true" - url, main, secret := parseImportURLSpecifier(in) - - if url != "http://localhost:8080/spec.yaml" { - t.Fatalf("url mismatch: got %q", url) - } - if main != true { - t.Fatalf("mainArtifact mismatch: got %v", main) - } - if secret != "" { - t.Fatalf("secret mismatch: got %q", secret) - } -} - -func TestParseImportURLSpecifier_PreservesQueryAndFragment(t *testing.T) { - in := "https://example.com:8443/spec.yaml?x=1#frag:false" - url, main, secret := parseImportURLSpecifier(in) - - if url != "https://example.com:8443/spec.yaml?x=1#frag" { - t.Fatalf("url mismatch: got %q", url) - } - if main != false { - t.Fatalf("mainArtifact mismatch: got %v", main) - } - if secret != "" { - t.Fatalf("secret mismatch: got %q", secret) - } -} - -func TestParseImportURLSpecifier_WithSecretSuffix(t *testing.T) { - in := "http://localhost:8080/spec.yaml:true:mySecret" - url, main, secret := parseImportURLSpecifier(in) - - if url != "http://localhost:8080/spec.yaml" { - t.Fatalf("url mismatch: got %q", url) - } - if main != true { - t.Fatalf("mainArtifact mismatch: got %v", main) - } - if secret != "mySecret" { - t.Fatalf("secret mismatch: got %q", secret) - } -} - -func TestParseImportURLSpecifier_NoSuffixes_Unchanged(t *testing.T) { - in := "http://localhost:8080/spec.yaml" - url, main, secret := parseImportURLSpecifier(in) - - if url != in { - t.Fatalf("url mismatch: got %q", url) - } - if main != true { - t.Fatalf("mainArtifact mismatch: got %v", main) - } - if secret != "" { - t.Fatalf("secret mismatch: got %q", secret) - } -} - -func TestParseImportURLSpecifier_PortOnly_NoSuffixes_Unchanged(t *testing.T) { - in := "http://localhost:8080/spec.yaml:1234" - url, main, secret := parseImportURLSpecifier(in) - - if url != in { - t.Fatalf("url mismatch: got %q", url) - } - if main != true { - t.Fatalf("mainArtifact mismatch: got %v", main) - } - if secret != "" { - t.Fatalf("secret mismatch: got %q", secret) - } -} - func TestParseImportFileSpecifier_SuffixBool(t *testing.T) { in := "./specs/openapi.yaml:false" path, main := parseImportFileSpecifier(in) @@ -98,4 +39,3 @@ func TestParseImportFileSpecifier_NoSuffix_Unchanged(t *testing.T) { t.Fatalf("mainArtifact mismatch: got %v", main) } } - diff --git a/cmd/context_test.go b/cmd/context_test.go index 80af9715..2b8a2b89 100644 --- a/cmd/context_test.go +++ b/cmd/context_test.go @@ -19,7 +19,6 @@ package cmd import ( "encoding/json" "os" - "path/filepath" "testing" "github.com/microcks/microcks-cli/pkg/config" @@ -100,8 +99,6 @@ users: const testConfigFilePath = "./testdata/local.config" func TestDeleteContext(t *testing.T) { - require.NoError(t, os.MkdirAll(filepath.Dir(testConfigFilePath), 0o755)) - //write the test config file require.NoError(t, os.MkdirAll("./testdata", 0o750)) err := os.WriteFile(testConfigFilePath, []byte(testConfig), os.ModePerm) diff --git a/cmd/importURL.go b/cmd/importURL.go index 08e4d0db..19de871e 100644 --- a/cmd/importURL.go +++ b/cmd/importURL.go @@ -17,7 +17,9 @@ package cmd import ( + "fmt" + "strconv" "strings" "github.com/microcks/microcks-cli/pkg/connectors" @@ -44,10 +46,13 @@ func NewImportURLCommand(globalClientOpts *connectors.ClientOptions) *cobra.Comm } sepSpecificationFiles := strings.Split(specificationFiles, ",") for _, f := range sepSpecificationFiles { - artifactURL, mainArtifact, secret := parseImportURLSpecifier(f) + mainArtifact := true + secret := "" + + f, mainArtifact, secret = parseImportURLArg(f) // Try downloading the artifcat - msg, err := mc.DownloadArtifact(artifactURL, mainArtifact, secret) + msg, err := mc.DownloadArtifact(f, mainArtifact, secret) if err != nil { return err } @@ -59,3 +64,26 @@ func NewImportURLCommand(globalClientOpts *connectors.ClientOptions) *cobra.Comm return importURLCmd } + +func parseImportURLArg(f string) (string, bool, string) { + mainArtifact := true + secret := "" + + // Check if URL starts with https or http + if strings.HasPrefix(f, "https://") || strings.HasPrefix(f, "http://") { + parts := strings.Split(f, ":") + n := len(parts) + + for i := n - 1; i >= 2; i-- { + if val, parseErr := strconv.ParseBool(parts[i]); parseErr == nil { + mainArtifact = val + if i+1 < n { + secret = strings.Join(parts[i+1:], ":") + } + f = strings.Join(parts[:i], ":") + break + } + } + } + return f, mainArtifact, secret +} diff --git a/documentation/cmd/importURL.md b/documentation/cmd/importURL.md index 1e2c6c09..e5a60c58 100644 --- a/documentation/cmd/importURL.md +++ b/documentation/cmd/importURL.md @@ -6,14 +6,6 @@ Imports API specification files (OpenAPI, AsyncAPI, etc.) hosted at a remote URL microcks import-url , [flags] ``` -### URL suffix parsing -You can optionally append metadata suffixes to each URL: - -- `:
` where `
` is `true` or `false` -- `:
:` to additionally specify a secret name - -The CLI parses these suffixes from the **rightmost** `:` characters only, so normal URLs containing `:` (scheme, ports, etc.) are preserved. - ### Example ```bash # Import a single artifact (marked as main) @@ -22,12 +14,6 @@ microcks import-url https://example.com/openapi.yaml # Specify mainArtifact flag for each file microcks import-url https://example.com/spec1.yaml:true,https://example.com/spec2.yaml:false -# URL with port + :main suffix (port/path are preserved) -microcks import-url http://localhost:8080/spec.yaml:true - -# URL with port + :main + :secret -microcks import-url http://localhost:8080/spec.yaml:true:mySecret - # Import specification to microcks without logining to microcks microcks import-url https://example.com/openapi.yaml \ --microcksURL \