Skip to content

Commit 1093119

Browse files
authored
Add support for local catalog via command-line flags (#10)
1 parent 3e762c7 commit 1093119

4 files changed

Lines changed: 110 additions & 8 deletions

File tree

‎README.md‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ The downloaded binary is named `{tool}_{version}-{os}_{arch}`, for example:
7575
| `--username` | `-u` | Registry username |
7676
| `--password` | `-p` | Registry password |
7777
| `--verbose` | `-v` | Enable verbose logging |
78+
| `--catalog-url` | | URL of the tools catalog YAML (defaults to the catalog on this repository's `main` branch) |
79+
| `--catalog-file` | | Path to a local tools catalog YAML, instead of fetching `--catalog-url` |
7880

7981
#### `get` command
8082

@@ -88,7 +90,7 @@ The downloaded binary is named `{tool}_{version}-{os}_{arch}`, for example:
8890

8991
## Adding Tools
9092

91-
Tools are defined in [`internal/tools/tools.yaml`](internal/tools/tools.yaml) and embedded into the binary at build time. To add a new tool, append an entry and rebuild:
93+
Tools are defined in [`internal/tools/tools.yaml`](internal/tools/tools.yaml), which is fetched at runtime from the `main` branch of this repository. To add a new tool, append an entry and open a pull request:
9294

9395
```yaml
9496
my-tool:
@@ -118,6 +120,22 @@ my-tool:
118120

119121
The tag pulled from the registry is constructed as `{tag}-{os}_{arch}`, e.g. `latest-cli-linux_amd64`.
120122

123+
### Using a Custom Catalog
124+
125+
Both commands accept an alternative catalog, which is useful for testing an entry before it is merged, or for running fully offline:
126+
127+
```bash
128+
# Local file
129+
kubermatic-ee-downloader list --catalog-file internal/tools/tools.yaml
130+
kubermatic-ee-downloader get my-tool --catalog-file ./my-catalog.yaml
131+
132+
# Alternative URL (e.g. a branch or fork)
133+
kubermatic-ee-downloader list \
134+
--catalog-url https://raw.githubusercontent.com/my-org/kubermatic-ee-downloader/my-branch/internal/tools/tools.yaml
135+
```
136+
137+
`--catalog-file` and `--catalog-url` are mutually exclusive.
138+
121139
## Development
122140

123141
```bash

‎cmd/main.go‎

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ var version = "dev"
3737

3838
func main() {
3939
var (
40-
username string
41-
password string
42-
verbose bool
43-
catalogURL string
40+
username string
41+
password string
42+
verbose bool
43+
catalogURL string
44+
catalogFile string
4445
)
4546

4647
rootCmd := &cobra.Command{
@@ -50,10 +51,20 @@ func main() {
5051
SilenceUsage: true,
5152
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
5253
log := newLogger(verbose)
53-
log.WithField("url", catalogURL).Debug("Fetching tool catalog")
54-
catalog, err := tools.FetchCatalog(catalogURL, 5*time.Second)
54+
55+
var (
56+
catalog map[string]tools.Tool
57+
err error
58+
)
59+
if catalogFile != "" {
60+
log.WithField("file", catalogFile).Debug("Loading tool catalog from file")
61+
catalog, err = tools.LoadCatalogFile(catalogFile)
62+
} else {
63+
log.WithField("url", catalogURL).Debug("Fetching tool catalog")
64+
catalog, err = tools.FetchCatalog(catalogURL, 5*time.Second)
65+
}
5566
if err != nil {
56-
return fmt.Errorf("load tool catalog: %w (use --catalog-url to specify an alternative)", err)
67+
return fmt.Errorf("load tool catalog: %w (use --catalog-url or --catalog-file to specify an alternative)", err)
5768
}
5869
tools.KnownTools = catalog
5970
return nil
@@ -64,6 +75,8 @@ func main() {
6475
rootCmd.PersistentFlags().StringVarP(&password, "password", "p", "", "Registry password")
6576
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging")
6677
rootCmd.PersistentFlags().StringVar(&catalogURL, "catalog-url", tools.DefaultCatalogURL, "URL of the tools catalog YAML")
78+
rootCmd.PersistentFlags().StringVar(&catalogFile, "catalog-file", "", "Path to a local tools catalog YAML, instead of fetching --catalog-url")
79+
rootCmd.MarkFlagsMutuallyExclusive("catalog-url", "catalog-file")
6780

6881
// --- list command ---
6982
listCmd := &cobra.Command{

‎internal/tools/tools.go‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"fmt"
2020
"io"
2121
"net/http"
22+
"os"
2223
"sort"
2324
"time"
2425

@@ -69,10 +70,27 @@ func FetchCatalog(url string, timeout time.Duration) (map[string]Tool, error) {
6970
return nil, fmt.Errorf("reading catalog response: %w", err)
7071
}
7172

73+
return parseCatalog(body)
74+
}
75+
76+
// LoadCatalogFile reads and parses a tools catalog from a local file.
77+
func LoadCatalogFile(path string) (map[string]Tool, error) {
78+
body, err := os.ReadFile(path)
79+
if err != nil {
80+
return nil, fmt.Errorf("reading catalog file: %w", err)
81+
}
82+
83+
return parseCatalog(body)
84+
}
85+
86+
func parseCatalog(body []byte) (map[string]Tool, error) {
7287
var m map[string]Tool
7388
if err := yaml.Unmarshal(body, &m); err != nil {
7489
return nil, fmt.Errorf("parsing catalog YAML: %w", err)
7590
}
91+
if len(m) == 0 {
92+
return nil, fmt.Errorf("catalog contains no tools")
93+
}
7694

7795
return m, nil
7896
}

‎internal/tools/tools_test.go‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"net/http"
2121
"net/http/httptest"
2222
"os"
23+
"path/filepath"
2324
"testing"
2425
"time"
2526
)
@@ -144,6 +145,58 @@ test-tool:
144145
}
145146
}
146147

148+
func TestFetchCatalogEmpty(t *testing.T) {
149+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
150+
_, _ = w.Write([]byte("---\n"))
151+
}))
152+
defer srv.Close()
153+
154+
_, err := FetchCatalog(srv.URL, 5*time.Second)
155+
if err == nil {
156+
t.Fatal("expected error for empty catalog")
157+
}
158+
}
159+
160+
func TestLoadCatalogFileSuccess(t *testing.T) {
161+
catalog, err := LoadCatalogFile("tools.yaml")
162+
if err != nil {
163+
t.Fatalf("unexpected error: %v", err)
164+
}
165+
if _, ok := catalog["conformance-tester"]; !ok {
166+
t.Error("expected conformance-tester in catalog")
167+
}
168+
}
169+
170+
func TestLoadCatalogFileErrors(t *testing.T) {
171+
dir := t.TempDir()
172+
173+
invalid := filepath.Join(dir, "invalid.yaml")
174+
if err := os.WriteFile(invalid, []byte("not: valid: yaml: ["), 0o600); err != nil {
175+
t.Fatal(err)
176+
}
177+
empty := filepath.Join(dir, "empty.yaml")
178+
if err := os.WriteFile(empty, nil, 0o600); err != nil {
179+
t.Fatal(err)
180+
}
181+
182+
tests := []struct {
183+
name string
184+
path string
185+
}{
186+
{name: "missing file", path: filepath.Join(dir, "does-not-exist.yaml")},
187+
{name: "invalid YAML", path: invalid},
188+
{name: "empty file", path: empty},
189+
}
190+
191+
for _, tt := range tests {
192+
t.Run(tt.name, func(t *testing.T) {
193+
if _, err := LoadCatalogFile(tt.path); err == nil {
194+
t.Fatalf("expected error for %s", tt.name)
195+
}
196+
})
197+
}
198+
}
199+
147200
func TestFetchCatalogNonOKStatus(t *testing.T) {
148201
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
149202
w.WriteHeader(http.StatusNotFound)

0 commit comments

Comments
 (0)