Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ linters:
excludes:
- G104 # duplicates errcheck
- G304 # CLI legitimately reads user-supplied file paths
misspell:
ignore-rules:
# API field name (auth enable_anonymous_signins); same exception as
# the volcano-hosting lint config.
- signins
testifylint:
disable:
# require inside httptest handlers exits the handler goroutine but
Expand Down
58 changes: 37 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,22 @@ just like `volcano login` — so you normally don't need anything else. Set

## Project configuration (`volcano-config.yaml`)

`volcano config deploy` reconciles declarative project configuration
(`volcano/volcano-config.yaml` or `./volcano-config.yaml`) against the active
target — the same manifest applies to local mode and cloud.

Functions may declare scheduled invocations. `name` and `cron` are required;
`enabled` (default `true`), `payload`, and `regions` are optional. A function
entry is valid if it sets `public` **or** declares at least one scheduler.
`volcano config deploy` uploads a declarative manifest
(`volcano/volcano-config.yaml` or `./volcano-config.yaml`) to the server, which
validates and reconciles the full project configuration — project settings,
database assertions, variables, buckets and policies, realtime, the complete
auth configuration (providers, email, templates, managed pages), function
visibility and schedulers, and frontend custom domains. The same manifest
applies to local mode and cloud. `volcano config pull` downloads the current
configuration as a canonical manifest rendered by the server.

```yaml
version: 1
variables:
- name: STRIPE_SECRET_KEY
value: ${STRIPE_SECRET_KEY} # interpolated from the CLI environment
realtime:
enabled: true
functions:
- name: hello
public: false
Expand All @@ -101,22 +107,32 @@ functions:
cron: "*/5 * * * *"
enabled: true
payload: { job: refresh }
regions: [us-east-1] # omit to let the server pick one deployed region
```

Reconciliation follows one rule that mirrors the server: **fields you declare
are enforced; fields you omit are left server-managed.** An omitted `enabled`,
`payload`, or `regions` keeps whatever the scheduler already has on the server
(on first create the server applies its defaults — `enabled: true`, an empty
payload, and one chosen region). In particular, `config deploy` will not
re-enable a scheduler you disabled out of band unless the manifest sets
`enabled: true`. `cron` is always required and enforced.

Reconciliation is also **non-destructive**: it creates and updates the
schedulers a function declares (matched by `name`, preserving the scheduler ID)
but never deletes or disables one. A scheduler the manifest no longer declares
is left running; to remove or disable one, use the imperative commands
(`volcano functions schedulers delete` / `disable`).
Key semantics (see the hosted manifest reference for the full schema):

- **Declared config sections are the source of truth.** Variables, bucket
policies, OAuth providers, email templates, and function schedulers are
fully synced when declared: entries absent from the manifest are deleted.
Omitted sections and fields keep their server values.
- **Functions, frontends, databases, and buckets are never created or deleted
through the manifest** — only their configuration is updated. A manifest
entry for a resource that does not exist is skipped with a warning; a
deployed resource missing from a declared section is reported too.
- `${ENV_VAR}` references are interpolated before upload; a reference to an
unset variable is an error, and `$$` produces a literal `$`.
- `volcano config deploy --dry-run` prints the projected actions without
changing anything. Validation failures (including plan-gate violations)
exit non-zero with the server's error list, and nothing is applied.
- Write-only secrets (SMTP password, OAuth client secrets, TLS material) are
omitted from `config pull` exports; keep them in your environment and set
them via `${ENV_VAR}` interpolation.

Behavior changes from older CLI releases: buckets are no longer auto-created,
an omitted `policies` key now leaves a bucket's policies untouched (previously
it deleted them all), schedulers are now deleted by omission within a declared
`schedulers` list, and the scheduler `regions` field is no longer supported
(placement is managed by the server).

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion internal/api/frontends.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func (c *Client) StreamFrontendDeploymentLogs(ctx context.Context, projectID, fr
func (c *Client) CreateFrontendCustomDomain(ctx context.Context, projectID, frontendID uuid.UUID, input FrontendCustomDomainInput) (*apiclient.FrontendCustomDomainResponse, error) {
body := apiclient.CreateFrontendCustomDomainJSONRequestBody{
Domain: strings.TrimSpace(input.Domain),
Tls: apiclient.FrontendCustomDomainTLSConfig{
Tls: apicommon.FrontendCustomDomainTLSConfig{
CertificatePem: strings.TrimSpace(input.CertificatePEM),
Mode: apicommon.FrontendCustomDomainTLSConfigModeByoc,
PrivateKeyPem: strings.TrimSpace(input.PrivateKeyPEM),
Expand Down
7 changes: 4 additions & 3 deletions internal/api/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/google/uuid"

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

Expand Down Expand Up @@ -130,7 +131,7 @@ func (c *Client) InvokeFunction(ctx context.Context, functionID uuid.UUID, input
}

// ListFunctionRuntimes returns the function runtime catalog.
func (c *Client) ListFunctionRuntimes(ctx context.Context) ([]apiclient.FunctionRuntimeOption, error) {
func (c *Client) ListFunctionRuntimes(ctx context.Context) ([]apicommon.FunctionRuntimeOption, error) {
resp, err := c.client.ListFunctionRuntimesWithResponse(ctx)
if err != nil {
return nil, err
Expand Down Expand Up @@ -249,7 +250,7 @@ func (c *Client) CreateFunctionScheduler(ctx context.Context, projectID, functio
body := apiclient.CreateFunctionSchedulerJSONRequestBody{
Name: input.Name,
Enabled: input.Enabled,
Schedule: apiclient.ScheduleRequest{
Schedule: apicommon.ScheduleRequest{
CronExpression: input.CronExpression,
},
}
Expand Down Expand Up @@ -281,7 +282,7 @@ func (c *Client) UpdateFunctionScheduler(ctx context.Context, projectID, functio
body.Name = &name
}
if input.CronExpression != "" {
body.Schedule = &apiclient.ScheduleRequest{CronExpression: input.CronExpression}
body.Schedule = &apicommon.ScheduleRequest{CronExpression: input.CronExpression}
}
if input.Payload != nil {
payload := input.Payload
Expand Down
6 changes: 3 additions & 3 deletions internal/api/log_stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

"github.com/google/uuid"

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

const logStreamScannerMaxTokenSize = 1024 * 1024
Expand All @@ -29,7 +29,7 @@ type ProjectLogStreamEvent struct {
ID string
Type string
Data string
Log *apiclient.LogSearchEvent
Log *apicommon.LogSearchEvent
Warning string
}

Expand Down Expand Up @@ -108,7 +108,7 @@ func (s *ProjectLogStream) nextRaw() (*ProjectLogStreamEvent, error) {
func parseProjectLogStreamEvent(event *ProjectLogStreamEvent) (*ProjectLogStreamEvent, error) {
switch event.Type {
case "log":
var logEvent apiclient.LogSearchEvent
var logEvent apicommon.LogSearchEvent
if err := json.Unmarshal([]byte(event.Data), &logEvent); err != nil {
return nil, fmt.Errorf("failed to decode log stream event: %w", err)
}
Expand Down
61 changes: 61 additions & 0 deletions internal/api/project_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package api

import (
"bytes"
"context"
"fmt"
"io"
"net/http"

"github.com/google/uuid"

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

// ProjectConfigValidationError is returned when the server rejects a config
// manifest with 422. Nothing was applied; Errors carries the full list.
type ProjectConfigValidationError struct {
Errors []apicommon.ProjectConfigValidationError
}

func (e *ProjectConfigValidationError) Error() string {
return fmt.Sprintf("configuration validation failed with %d error(s); nothing was applied", len(e.Errors))
}

// ApplyProjectConfig uploads a full configuration manifest (pre-encoded JSON)
// and returns the server's per-resource apply report.
func (c *Client) ApplyProjectConfig(ctx context.Context, projectID uuid.UUID, manifestJSON []byte, dryRun bool) (*apicommon.ProjectConfigApplyResult, error) {
params := &apiclient.ApplyProjectConfigParams{}
if dryRun {
params.DryRun = &dryRun
}
resp, err := c.client.ApplyProjectConfigWithBodyWithResponse(ctx, projectID, params, "application/json", bytes.NewReader(manifestJSON))
if err != nil {
return nil, err
}
if resp.JSON422 != nil {
return nil, &ProjectConfigValidationError{Errors: resp.JSON422.Errors}
}
return apiResult(resp.StatusCode(), resp.Body, resp.JSON200, resp.JSON400, resp.JSON401, resp.JSON404, resp.JSON409)
}

// GetProjectConfigYAML downloads the project configuration as the canonical
// YAML manifest rendered by the server, returned verbatim.
func (c *Client) GetProjectConfigYAML(ctx context.Context, projectID uuid.UUID) ([]byte, error) {
format := apiclient.Yaml
resp, err := c.client.GetProjectConfig(ctx, projectID, &apiclient.GetProjectConfigParams{Format: &format})
if err != nil {
return nil, err
}
defer func() { _ = resp.Body.Close() }()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, apiError(resp.StatusCode, body)
}
return body, nil
}
Loading
Loading