Skip to content
Draft
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
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,17 @@ This opens your browser for authentication. Alternatively, provide a token file:
unikraft login --token /path/to/token
```

Or, if you need to directly specify a metro endpoint, you can manually create a
profile:
Or, you can manually create a profile and configure metros using the CLI:

```sh
# Create a profile with a token and organization
unikraft profile create --name my-profile --token /path/to/token --organization my-org

# Add a metro to the profile
unikraft metro create --name fra --endpoint https://api.fra.unikraft.cloud --country de
```

You can also directly edit the config file if you prefer:

```yaml
# Linux: ~/.config/unikraft/config.yaml
Expand Down Expand Up @@ -290,6 +299,12 @@ Manage multiple accounts or configurations with profiles:
# List profiles
unikraft profile list

# Create a new profile
unikraft profile create --name staging --token /path/to/token --organization my-org

# Delete a profile
unikraft profile delete old-profile

# Switch profile
unikraft profile use staging

Expand Down
62 changes: 59 additions & 3 deletions cmd/unikraft/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ func authTests(t *testing.T, r *testRunner) {
{args: []string{unikraftCmd, "profile", "get", "--help"}},
{args: []string{unikraftCmd, "profile", "list", "--help"}},
{args: []string{unikraftCmd, "profile", "use", "--help"}},
{args: []string{unikraftCmd, "metro", "--help"}},
{args: []string{unikraftCmd, "metro", "get", "--help"}},
{args: []string{unikraftCmd, "metro", "list", "--help"}},
{args: []string{unikraftCmd, "profile", "create", "--help"}},
{args: []string{unikraftCmd, "profile", "delete", "--help"}},
})
})
t.Run("flow", func(t *testing.T) {
Expand All @@ -33,4 +32,61 @@ func authTests(t *testing.T, r *testRunner) {
{args: []string{unikraftCmd, "metro", "list"}, allowErr: true},
})
})

t.Run("profile-create", func(t *testing.T) {
r.
online().
run(t, []command{
{args: []string{unikraftCmd, "profile", "list"}},
{args: []string{
unikraftCmd, "profile", "create",
"--name", "test-profile",
"--token", "test-token",
"--organization", "test-org",
}},
{args: []string{unikraftCmd, "profile", "list"}},
{args: []string{unikraftCmd, "profile", "get", "test-profile"}},
})
})
t.Run("profile-create-duplicate", func(t *testing.T) {
r.
online().
run(t, []command{
{args: []string{
unikraftCmd, "profile", "create",
"--name", "dup-profile",
"--token", "test-token",
}},
{args: []string{
unikraftCmd, "profile", "create",
"--name", "dup-profile",
"--token", "test-token-2",
}, allowErr: true},
})
})
t.Run("profile-delete", func(t *testing.T) {
r.
online().
run(t, []command{
{args: []string{
unikraftCmd, "profile", "create",
"--name", "to-delete",
"--token", "test-token",
}},
{args: []string{unikraftCmd, "profile", "list"}},
{args: []string{unikraftCmd, "profile", "delete", "to-delete"}},
{args: []string{unikraftCmd, "profile", "list"}},
})
})
t.Run("profile-delete-active", func(t *testing.T) {
r.
online().
run(t, []command{
// The default profile from the online config is active;
// deleting it should fail.
{args: []string{unikraftCmd, "profile", "list"}},
{args: []string{unikraftCmd, "profile", "delete", "default"}, allowErr: true},
{args: []string{unikraftCmd, "profile", "list"}},
})
})
}
7 changes: 7 additions & 0 deletions cmd/unikraft/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func TestGolden(t *testing.T) {
}{
{"help", helpTests},
{"auth", authTests},
{"metro", metroTests},
{"instances", instancesTests},
{"instance-templates", instanceTemplatesTests},
{"volumes", volumesTests},
Expand Down Expand Up @@ -461,6 +462,12 @@ var cleaners = []cleaner{
pattern: regexp.MustCompile(`/tmp/TestGolden[^/]+/`),
repl: "/tmp/TestGolden/",
},
{
// quota field paths like "quotas.instances.active.used" vary between runs
// because concurrent resolution picks a non-deterministic field.
pattern: regexp.MustCompile(`\bresolving quotas\.[a-z.]+:`),
repl: "resolving quotas.FIELD:",
},
{
// auto-generated domain names like "foo.ukp-stable.apw.unikraft.internal"
pattern: regexp.MustCompile(`\.[a-z0-9.\-]+\.unikraft\.(app|internal)\b`),
Expand Down
135 changes: 135 additions & 0 deletions cmd/unikraft/metro_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2025, Unikraft GmbH and The Unikraft CLI Authors.
// Licensed under the BSD-3-Clause License (the "License").
// You may not use this file except in compliance with the License.

package main

import "testing"

func metroTests(t *testing.T, r *testRunner) {
t.Run("help", func(t *testing.T) {
r.run(t, []command{
{args: []string{unikraftCmd, "metro", "--help"}},
{args: []string{unikraftCmd, "metro", "get", "--help"}},
{args: []string{unikraftCmd, "metro", "list", "--help"}},
{args: []string{unikraftCmd, "metro", "create", "--help"}},
{args: []string{unikraftCmd, "metro", "edit", "--help"}},
{args: []string{unikraftCmd, "metro", "delete", "--help"}},
})
})

t.Run("create", func(t *testing.T) {
r.
online().
run(t, []command{
{args: []string{unikraftCmd, "metro", "list"}},
// The create command resolves quotas/status against the
// endpoint. For a fake metro this fails, but the metro is
// still persisted.
{args: []string{
unikraftCmd, "metro", "create",
"--set", "name=example",
"--set", "endpoint=https://api.example.unikraft.cloud",
"--set", "country=se",
}, allowErr: true},
{args: []string{unikraftCmd, "metro", "list"}},
{args: []string{unikraftCmd, "metro", "get", "example", "-f", "name,country,endpoint"}},
})
})
t.Run("create-shortcut", func(t *testing.T) {
r.
online().
run(t, []command{
{args: []string{unikraftCmd, "metro", "list"}},
{args: []string{
unikraftCmd, "metro", "create",
"--name", "example",
"--endpoint", "https://api.example.unikraft.cloud",
"--country", "se",
}, allowErr: true},
{args: []string{unikraftCmd, "metro", "list"}},
{args: []string{unikraftCmd, "metro", "get", "example", "-f", "name,country,endpoint"}},
})
})
t.Run("create-duplicate", func(t *testing.T) {
r.
online().
run(t, []command{
{args: []string{
unikraftCmd, "metro", "create",
"--name", "example",
"--endpoint", "https://api.example.unikraft.cloud",
"--country", "se",
}, allowErr: true},
{args: []string{
unikraftCmd, "metro", "create",
"--name", "example",
"--endpoint", "https://api.example2.unikraft.cloud",
"--country", "se",
}, allowErr: true},
{args: []string{unikraftCmd, "metro", "list"}},
})
})

t.Run("edit", func(t *testing.T) {
r.
online().
run(t, []command{
{args: []string{
unikraftCmd, "metro", "create",
"--set", "name=example",
"--set", "endpoint=https://api.example.unikraft.cloud",
"--set", "country=se",
}, allowErr: true},
{args: []string{unikraftCmd, "metro", "get", "example", "-f", "name,country,endpoint"}},
{args: []string{
unikraftCmd, "metro", "edit", "example",
"--set", "endpoint=https://api.example2.unikraft.cloud",
}, allowErr: true},
{args: []string{unikraftCmd, "metro", "get", "example", "-f", "name,country,endpoint"}},
})
})
t.Run("edit-shortcut", func(t *testing.T) {
r.
online().
run(t, []command{
{args: []string{
unikraftCmd, "metro", "create",
"--name", "example",
"--endpoint", "https://api.example.unikraft.cloud",
"--country", "se",
}, allowErr: true},
{args: []string{unikraftCmd, "metro", "get", "example", "-f", "name,country,endpoint"}},
{args: []string{
unikraftCmd, "metro", "edit", "example",
"--endpoint", "https://api.example2.unikraft.cloud",
"--country", "SE",
}, allowErr: true},
{args: []string{unikraftCmd, "metro", "get", "example", "-f", "name,country,endpoint"}},
})
})

t.Run("delete", func(t *testing.T) {
r.
online().
run(t, []command{
{args: []string{
unikraftCmd, "metro", "create",
"--set", "name=example",
"--set", "endpoint=https://api.example.unikraft.cloud",
"--set", "country=se",
}, allowErr: true},
{args: []string{unikraftCmd, "metro", "list"}},
{args: []string{unikraftCmd, "metro", "delete", "example"}},
{args: []string{unikraftCmd, "metro", "list"}},
})
})
t.Run("delete-nonexistent", func(t *testing.T) {
r.
online().
run(t, []command{
{args: []string{unikraftCmd, "metro", "delete", "nonexistent"}, allowErr: true},
})
})
}
6 changes: 3 additions & 3 deletions cmd/unikraft/testdata/TestGolden/auth/flow

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading