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
39 changes: 39 additions & 0 deletions internal/api/service_keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package api

import (
"context"

"github.com/google/uuid"

"github.com/Kong/volcano-cli/internal/apiclient"
)

// ListServiceKeys returns one service-key page for a project.
func (c *Client) ListServiceKeys(ctx context.Context, projectID uuid.UUID, page, limit int) (*apiclient.PaginatedServiceKeys, error) {
resp, err := c.client.ListServiceKeysWithResponse(ctx, projectID, &apiclient.ListServiceKeysParams{
Page: &page,
Limit: &limit,
})
if err != nil {
return nil, err
}
return apiResult(resp.StatusCode(), resp.Body, resp.JSON200)
}

// CreateServiceKey creates one service key in a project.
func (c *Client) CreateServiceKey(ctx context.Context, projectID uuid.UUID, name string) (*apiclient.ServiceKey, error) {
resp, err := c.client.CreateServiceKeyWithResponse(ctx, projectID, apiclient.CreateServiceKeyJSONRequestBody{Name: name})
if err != nil {
return nil, err
}
return apiResult(resp.StatusCode(), resp.Body, resp.JSON201)
}

// GetServiceKey returns one service key by ID.
func (c *Client) GetServiceKey(ctx context.Context, projectID, keyID uuid.UUID) (*apiclient.ServiceKey, error) {
resp, err := c.client.GetServiceKeyWithResponse(ctx, projectID, keyID)
if err != nil {
return nil, err
}
return apiResult(resp.StatusCode(), resp.Body, resp.JSON200)
}
6 changes: 4 additions & 2 deletions internal/cmd/cloud/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
functionscmd "github.com/Kong/volcano-cli/internal/cmd/functions"
storagecmd "github.com/Kong/volcano-cli/internal/cmd/storage"
variablescmd "github.com/Kong/volcano-cli/internal/cmd/variables"
"github.com/Kong/volcano-cli/internal/dataplane"
cliruntime "github.com/Kong/volcano-cli/internal/runtime"
)

Expand All @@ -33,12 +34,13 @@ func New(deps cliruntime.Deps) *cobra.Command {
// NewResourceCommands returns cloud resource commands.
func NewResourceCommands(deps cliruntime.Deps) []*cobra.Command {
deps.CommandPathPrefix = "volcano cloud"
dataPlaneKeys := dataplane.NewService(deps)
return []*cobra.Command{
configcmd.New(deps),
databasescmd.New(deps),
frontendscmd.New(deps),
functionscmd.New(deps),
storagecmd.New(deps),
functionscmd.NewWithOptions(deps, functionscmd.WithInvokeTokenProvider(dataPlaneKeys.ServiceKeyForProject)),
storagecmd.NewWithOptions(deps, storagecmd.WithObjectTokenProvider(dataPlaneKeys.ServiceKey)),
variablescmd.New(deps),
}
}
Expand Down
161 changes: 161 additions & 0 deletions internal/cmd/cloud/cloud_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package cloud

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/spf13/cobra"
"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 (
cloudProjectID = "33333333-3333-4333-8333-333333333333"
cloudBucketID = "44444444-4444-4444-8444-444444444444"
cloudObjectID = "66666666-6666-4666-8666-666666666666"
cloudFunctionID = "77777777-7777-4777-8777-777777777777"
)

func TestCloudStorageObjectCommandsUseCLIServiceKey(t *testing.T) {
setCloudCommandTestHome(t)
saveCloudCommandTestConfig(t)

var serviceKeyListHits int
var storageListAuth string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/projects/"+cloudProjectID+"/service-keys":
serviceKeyListHits++
assert.Equal(t, "Bearer platform-token", r.Header.Get("Authorization"))
writeCloudCommandJSON(t, w, http.StatusOK, map[string]any{
"data": []any{
cloudServiceKeyPayload("11111111-1111-4111-8111-111111111111", "sk-storage"),
},
"has_more": false,
"page": 1,
"limit": 100,
"total": 1,
})
case r.Method == http.MethodGet && r.URL.Path == "/storage/uploads":
storageListAuth = r.Header.Get("Authorization")
writeCloudCommandJSON(t, w, http.StatusOK, map[string]any{
"objects": []any{cloudObjectPayload("hello.txt")},
})
default:
http.NotFound(w, r)
}
}))
defer server.Close()

out, err := executeCloudCommand(t, New(cliruntime.Deps{HTTPClient: server.Client(), APIBaseURL: server.URL}), "storage", "object", "list", "uploads")
require.NoError(t, err)
assert.Contains(t, out, "hello.txt")
assert.Equal(t, 1, serviceKeyListHits)
assert.Equal(t, "Bearer sk-storage", storageListAuth)
}

func TestCloudFunctionInvokeUsesCLIServiceKey(t *testing.T) {
setCloudCommandTestHome(t)
saveCloudCommandTestConfig(t)

var serviceKeyListHits int
var invokeAuth string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == http.MethodGet && r.URL.Path == "/projects/"+cloudProjectID+"/service-keys":
serviceKeyListHits++
assert.Equal(t, "Bearer platform-token", r.Header.Get("Authorization"))
writeCloudCommandJSON(t, w, http.StatusOK, map[string]any{
"data": []any{
cloudServiceKeyPayload("22222222-2222-4222-8222-222222222222", "sk-functions"),
},
"has_more": false,
"page": 1,
"limit": 100,
"total": 1,
})
case r.Method == http.MethodPost && r.URL.Path == "/functions/"+cloudFunctionID+"/invoke":
invokeAuth = r.Header.Get("Authorization")
writeCloudCommandJSON(t, w, http.StatusOK, map[string]any{"ok": true})
default:
http.NotFound(w, r)
}
}))
defer server.Close()

out, err := executeCloudCommand(t, New(cliruntime.Deps{HTTPClient: server.Client(), APIBaseURL: server.URL}), "functions", "invoke", "--id", cloudFunctionID, "--json")
require.NoError(t, err)
assert.JSONEq(t, `{"ok":true}`, out)
assert.Equal(t, 1, serviceKeyListHits)
assert.Equal(t, "Bearer sk-functions", invokeAuth)
}

func executeCloudCommand(t *testing.T, cmd *cobra.Command, args ...string) (string, error) {
t.Helper()
var out bytes.Buffer
cmd.SetOut(&out)
cmd.SetErr(&out)
cmd.SetArgs(args)
err := cmd.Execute()
return out.String(), err
}

func setCloudCommandTestHome(t *testing.T) {
t.Helper()
t.Setenv("HOME", t.TempDir())
t.Setenv("VOLCANO_TOKEN", "")
t.Setenv("VOLCANO_PROJECT_ID", "")
t.Setenv("VOLCANO_API_URL", "")
t.Setenv("VOLCANO_FIRST_PARTY_DEVICE_CLIENT_ID", "")
}

func saveCloudCommandTestConfig(t *testing.T) {
t.Helper()
cfg := &cliconfig.Config{
UserToken: "platform-token",
CurrentProject: &cliconfig.ProjectConfig{
ID: cloudProjectID,
Name: "Gamma",
},
}
require.NoError(t, cfg.Save())
}

func writeCloudCommandJSON(t *testing.T, w http.ResponseWriter, status int, value any) {
t.Helper()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
require.NoError(t, json.NewEncoder(w).Encode(value))
}

func cloudServiceKeyPayload(id, keyValue string) map[string]any {
return map[string]any{
"id": id,
"project_id": cloudProjectID,
"name": "volcano-cli-data-plane",
"key_prefix": keyValue[:min(len(keyValue), 12)],
"key_value": keyValue,
"permissions": []string{"*"},
"created_at": "2026-05-20T00:00:00Z",
"updated_at": "2026-05-20T00:00:00Z",
}
}

func cloudObjectPayload(name string) map[string]any {
return map[string]any{
"id": cloudObjectID,
"bucket_id": cloudBucketID,
"name": name,
"size": 12,
"mime_type": "text/plain",
"is_public": false,
"created_at": "2026-05-20T00:00:00Z",
"updated_at": "2026-05-20T00:00:00Z",
}
}
27 changes: 24 additions & 3 deletions internal/cmd/functions/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,32 @@ import (
"github.com/spf13/cobra"

schedulerscmd "github.com/Kong/volcano-cli/internal/cmd/functions/schedulers"
clifunction "github.com/Kong/volcano-cli/internal/function"
cliruntime "github.com/Kong/volcano-cli/internal/runtime"
)

// Option configures function command behavior.
type Option func(*commandOptions)

// WithInvokeTokenProvider configures the bearer token source for function invoke routes.
func WithInvokeTokenProvider(provider clifunction.InvokeTokenProvider) Option {
return func(opts *commandOptions) {
opts.functionOptions = append(opts.functionOptions, clifunction.WithInvokeTokenProvider(provider))
}
}

// New returns the functions command.
func New(deps cliruntime.Deps) *cobra.Command {
return newWithOptions(deps, commandOptions{batchDeployAll: true})
return NewWithOptions(deps)
}

// NewWithOptions returns the functions command with custom function behavior.
func NewWithOptions(deps cliruntime.Deps, options ...Option) *cobra.Command {
opts := commandOptions{batchDeployAll: true}
for _, option := range options {
option(&opts)
}
return newWithOptions(deps, opts)
}

// NewLocal returns the functions command for local-mode projects.
Expand All @@ -18,7 +38,8 @@ func NewLocal(deps cliruntime.Deps) *cobra.Command {
}

type commandOptions struct {
batchDeployAll bool
batchDeployAll bool
functionOptions []clifunction.Option
}

func newWithOptions(deps cliruntime.Deps, opts commandOptions) *cobra.Command {
Expand All @@ -30,7 +51,7 @@ func newWithOptions(deps cliruntime.Deps, opts commandOptions) *cobra.Command {
cmd.AddCommand(newDeploy(deps, opts.batchDeployAll))
cmd.AddCommand(newList(deps))
cmd.AddCommand(newGet(deps))
cmd.AddCommand(newInvoke(deps))
cmd.AddCommand(newInvoke(deps, opts.functionOptions...))
cmd.AddCommand(newAlias(deps))
cmd.AddCommand(newDelete(deps))
cmd.AddCommand(newUpdate(deps))
Expand Down
20 changes: 11 additions & 9 deletions internal/cmd/functions/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@ import (
)

type invokeOptions struct {
deps cliruntime.Deps
identifier string
functionID string
payload string
hasPayload bool
jsonOutput bool
out io.Writer
deps cliruntime.Deps
functionOptions []clifunction.Option
identifier string
functionID string
payload string
hasPayload bool
jsonOutput bool
out io.Writer
}

func newInvoke(deps cliruntime.Deps) *cobra.Command {
func newInvoke(deps cliruntime.Deps, functionOptions ...clifunction.Option) *cobra.Command {
opts := invokeOptions{}
cmd := &cobra.Command{
Use: "invoke [name]",
Expand All @@ -40,6 +41,7 @@ func newInvoke(deps cliruntime.Deps) *cobra.Command {
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.deps = deps
opts.functionOptions = functionOptions
if len(args) == 1 {
opts.identifier = strings.TrimSpace(args[0])
}
Expand Down Expand Up @@ -73,7 +75,7 @@ func runInvoke(ctx context.Context, opts invokeOptions) error {
}
}

service := clifunction.NewService(opts.deps)
service := clifunction.NewService(opts.deps, opts.functionOptions...)
var resp any
if hasID {
var functionID uuid.UUID
Expand Down
Loading
Loading