Skip to content
Closed
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
6 changes: 4 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Piped, CI, and `NO_COLOR` output remains plain. Machine output is unchanged.
| Element | CLI can… | Commands | Details |
|---|---|---|---|
| Account / auth | sign up, log in/out | `signup`, `login`, `logout` | [authentication.md](authentication.md) |
| Access tokens | create, list, get, revoke project credentials | `cloud access-tokens …` | [access-tokens.md](access-tokens.md) |
| Access tokens | create, list, get, revoke project credentials | `projects keys access-tokens …`, `cloud access-tokens …` | [access-tokens.md](access-tokens.md) |
| Project | create, list, get, rename, delete, select, get keys and usage | `projects …`, `use` | below |
| Functions | deploy, invoke, inspect, schedule, alias | `functions …` | [functions.md](functions.md) |
| Durable functions | deploy, start, inspect executions, read logs, schedule | `durable …`, `cloud durable …` | [durable-functions.md](durable-functions.md) |
Expand All @@ -107,6 +107,7 @@ volcano projects get # details for the active project
volcano projects rename eac37d5a-5f6f-42d8-acf6-0f2ae9c7a550 new-name # rename a project
volcano projects keys anon list # anon (publishable) API keys for the browser/SDK
volcano projects keys service list # backend service-key metadata for the active project
volcano projects keys access-tokens list # project access tokens for CI
volcano projects usage # current-month and all-time usage totals
volcano projects delete my-app # delete
```
Expand All @@ -116,7 +117,8 @@ volcano projects delete my-app # delete

### Project keys

`volcano projects keys` requires an explicit key type.
`volcano projects keys` requires an explicit key type: `anon`, `service`, or
`access-tokens`. See [access-tokens.md](access-tokens.md) for token operations.
`volcano projects keys anon list [project-id]` lists publishable anon keys.
`volcano projects keys anon create <name> [project-id]` creates a publishable
auth-only key using the server default.
Expand Down
28 changes: 19 additions & 9 deletions docs/access-tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ every project you own. Only an account token can manage access tokens.
- Cannot create, inspect, or revoke tokens, and cannot run account-wide
commands: `volcano projects list`, `volcano projects create`, `volcano
projects rename`, `volcano projects delete`, selecting a project by name, or
`volcano git connect`. `volcano cloud access-tokens usage` is the exception:
a token can report its own project's consumption, and its own day-by-day
`volcano git connect`. `volcano projects keys access-tokens usage` is the
exception: a token can report its own project's consumption, and its own day-by-day
series by ID, so a CI job needs nothing but the credential it already runs
with. `get --usage` is not, because reading one token's record is itself a
token operation.
Expand All @@ -32,21 +32,31 @@ every project you own. Only an account token can manage access tokens.

| Operation | Command |
|---|---|
| Create | `volcano cloud access-tokens create <name> [--scope <scope>] [--expires-at <timestamp>] [--json]` |
| List | `volcano cloud access-tokens list [--search <text>] [--include-revoked] [--json]` |
| Get | `volcano cloud access-tokens get <name-or-id> [--usage] [--days <n>] [--json]` |
| Usage | `volcano cloud access-tokens usage [<token-id>] [--days <n>] [--json]` |
| Revoke | `volcano cloud access-tokens revoke <name-or-id> [--yes]` |
| Create | `volcano projects keys access-tokens create <name> [--scope <scope>] [--expires-at <timestamp>] [--json]` |
| List | `volcano projects keys access-tokens list [--search <text>] [--include-revoked] [--json]` |
| Get | `volcano projects keys access-tokens get <name-or-id> [--usage] [--days <n>] [--json]` |
| Usage | `volcano projects keys access-tokens usage [<token-id>] [--days <n>] [--json]` |
| Revoke | `volcano projects keys access-tokens revoke <name-or-id> [--yes]` |

`tokens` is an alias for `access-tokens`. These are cloud commands: local
development issues no credentials.
`tokens` is an alias for `access-tokens`. Both `volcano cloud access-tokens`
and `volcano projects keys access-tokens` target the current cloud project
and support the same operations and flags. Local development issues no
credentials.

## Create a token

Use either command path to create a token for the current cloud project:

```bash
volcano cloud access-tokens create ci-deploy
```

The same operation is available under project keys:

```bash
volcano projects keys access-tokens create ci-deploy
```

```text
✓ Access token 'ci-deploy' created
ID: 7f1c2e94-2a6b-4c17-9a42-1b0c8f5d3e77
Expand Down
86 changes: 86 additions & 0 deletions internal/cmd/project/access_tokens_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package project

import (
"maps"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

cliconfig "github.com/Kong/volcano-cli/internal/config"
cliruntime "github.com/Kong/volcano-cli/internal/runtime"
)

const projectAccessTokenID = "77777777-7777-4777-8777-777777777777"

func TestProjectAccessTokenKeys(t *testing.T) {
setProjectCommandTestHome(t)
saveProjectCommandTestConfig(t, &cliconfig.Config{
UserToken: "token", CurrentProject: &cliconfig.ProjectConfig{ID: projectBetaID, Name: "Beta"},
})

base := "/projects/" + projectBetaID + "/access-tokens"
token := map[string]any{
"id": projectAccessTokenID, "project_id": projectBetaID, "name": "ci-deploy",
"scope": "full", "status": "active", "token_prefix": "pt-Wq9l2m4X",
"token_source": "cli", "all_time_requests": 42,
"created_at": time.Now().UTC().Format(time.RFC3339),
}
var requests []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "Bearer token", r.Header.Get("Authorization"))
requests = append(requests, r.Method+" "+r.URL.Path)
switch {
case r.Method == http.MethodPost && r.URL.Path == base:
created := maps.Clone(token)
created["token"] = "pt-secret"
writeProjectCommandJSON(t, w, http.StatusCreated, created)
case r.Method == http.MethodGet && r.URL.Path == base:
assert.Equal(t, "page=1&limit=1", r.URL.RawQuery)
writeProjectCommandJSON(t, w, http.StatusOK, map[string]any{
"data": []any{token}, "page": 1, "limit": 1, "total": 2, "has_more": true,
})
case r.Method == http.MethodGet && r.URL.Path == base+"/"+projectAccessTokenID:
writeProjectCommandJSON(t, w, http.StatusOK, token)
case r.Method == http.MethodGet && r.URL.Path == base+"/usage":
writeProjectCommandJSON(t, w, http.StatusOK, []any{})
case r.Method == http.MethodDelete && r.URL.Path == base+"/"+projectAccessTokenID:
w.WriteHeader(http.StatusNoContent)
default:
http.NotFound(w, r)
}
}))
defer server.Close()
deps := cliruntime.Deps{HTTPClient: server.Client(), APIBaseURL: server.URL}
help, err := executeProjectCommand(t, NewProjects(deps), "keys", "--help")
require.NoError(t, err)
assert.Contains(t, help, "access-tokens")
help, err = executeProjectCommand(t, NewProjects(deps), "keys", "access-tokens", "create", "--help")
require.NoError(t, err)
assert.Contains(t, help, "volcano projects keys access-tokens create ci-deploy")
for _, tc := range []struct {
args []string
want string
}{
{[]string{"create", "ci-deploy"}, "pt-secret"},
{[]string{"list", "--limit", "1"}, "Next page: volcano projects keys access-tokens list --page 2 --limit 1"},
{[]string{"get", projectAccessTokenID}, "Name: ci-deploy"},
{[]string{"usage"}, "No access tokens created"},
{[]string{"revoke", projectAccessTokenID, "--yes"}, "Access token 'ci-deploy' revoked"},
} {
out, err := executeProjectCommand(t, NewProjects(deps), append([]string{"keys", "access-tokens"}, tc.args...)...)
require.NoError(t, err, "%v", tc.args)
assert.Contains(t, out, tc.want)
if tc.args[0] != "create" {
assert.NotContains(t, out, "pt-secret")
}
}
assert.Equal(t, []string{
"POST " + base, "GET " + base, "GET " + base + "/" + projectAccessTokenID,
"GET " + base + "/usage", "GET " + base + "/" + projectAccessTokenID,
"DELETE " + base + "/" + projectAccessTokenID,
}, requests)
}
7 changes: 5 additions & 2 deletions internal/cmd/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/Kong/volcano-cli/internal/api"
"github.com/Kong/volcano-cli/internal/apiclient"
accesstokenscmd "github.com/Kong/volcano-cli/internal/cmd/accesstokens"
"github.com/Kong/volcano-cli/internal/confirm"
"github.com/Kong/volcano-cli/internal/output"
cliproject "github.com/Kong/volcano-cli/internal/project"
Expand Down Expand Up @@ -207,14 +208,16 @@ func runRename(ctx context.Context, opts renameOptions) error {
func newKeys(deps cliruntime.Deps) *cobra.Command {
cmd := &cobra.Command{
Use: "keys",
Short: "Manage project anon and service keys",
Short: "Manage project anon, service, and access-token keys",
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
return errors.New("specify a key type: anon or service")
return errors.New("specify a key type: anon, service, or access-tokens")
},
}
cmd.AddCommand(newAnonKeys(deps))
cmd.AddCommand(newServiceKeys(deps))
deps.CommandPathPrefix = "volcano projects keys"
cmd.AddCommand(accesstokenscmd.New(deps))
return cmd
}

Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/project/project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ func TestProjectsKeysRequiresKeyType(t *testing.T) {
_, err := executeProjectCommand(t, NewProjects(cliruntime.Deps{}), args...)
require.Error(t, err)
if len(args) == 1 {
assert.ErrorContains(t, err, "specify a key type: anon or service")
assert.ErrorContains(t, err, "specify a key type: anon, service, or access-tokens")
} else {
assert.ErrorContains(t, err, "unknown command")
}
Expand Down
Loading