diff --git a/cmd/artifact_specifier.go b/cmd/artifact_specifier.go new file mode 100644 index 0000000..5869fad --- /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 0000000..cda3aea --- /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 8de7935..67844e4 100644 --- a/cmd/context_test.go +++ b/cmd/context_test.go @@ -2,6 +2,7 @@ package cmd import ( "os" + "path/filepath" "testing" "github.com/microcks/microcks-cli/pkg/config" @@ -39,6 +40,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 err := os.WriteFile(testConfigFilePath, []byte(testConfig), os.ModePerm) require.NoError(t, err) diff --git a/cmd/import.go b/cmd/import.go index c1a74c4..5f65db6 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" @@ -118,21 +117,10 @@ func NewImportCommand(globalClientOpts *connectors.ClientOptions) *cobra.Command // Handle multiple specification files separated by comma. sepSpecificationFiles := strings.Split(specificationFiles, ",") 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 { - fmt.Printf("Cannot parse '%s' as Bool, default to true\n", 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 { fmt.Printf("Got error when invoking Microcks client importing Artifact: %s", err) os.Exit(1) @@ -155,13 +143,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 69c32e8..8f43353 100644 --- a/cmd/importURL.go +++ b/cmd/importURL.go @@ -19,7 +19,6 @@ package cmd import ( "fmt" "os" - "strconv" "strings" "github.com/microcks/microcks-cli/pkg/config" @@ -97,28 +96,10 @@ func NewImportURLCommand(globalClientOpts *connectors.ClientOptions) *cobra.Comm } sepSpecificationFiles := strings.Split(specificationFiles, ",") for _, f := range sepSpecificationFiles { - mainArtifact := true - secret := "" - - // Check if URL starts with https or http - if strings.HasPrefix(f, "https://") || strings.HasPrefix(f, "http://") { - urlAndMainAtrifactAndSecretName := strings.Split(f, ":") - n := len(urlAndMainAtrifactAndSecretName) - f = urlAndMainAtrifactAndSecretName[0] + ":" + urlAndMainAtrifactAndSecretName[1] - if n > 2 { - val, err := strconv.ParseBool(urlAndMainAtrifactAndSecretName[2]) - if err != nil { - fmt.Println(err) - } - mainArtifact = val - } - if n > 3 { - secret = urlAndMainAtrifactAndSecretName[3] - } - } + 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 { fmt.Printf("Got error when invoking Microcks client importing Artifact: %s", err) os.Exit(1) diff --git a/documentation/cmd/importURL.md b/documentation/cmd/importURL.md index e5a60c5..1e2c6c0 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 \