From 27130b290eecd920f355154161f025be64013a56 Mon Sep 17 00:00:00 2001 From: Ted Kim Date: Sat, 12 Sep 2026 16:56:53 -0400 Subject: [PATCH] feat(durable): restore CLI command group --- docs/README.md | 2 + docs/durable-functions.md | 152 + docs/functions.md | 3 + docs/project-configuration.md | 10 + internal/api/durable_functions.go | 280 ++ internal/api/durable_functions_test.go | 218 + internal/apiclient/client.gen.go | 4004 +++++++++++++---- internal/cmd/cloud/cloud.go | 2 + internal/cmd/cmdutil/jsonarg.go | 28 + internal/cmd/durable/delete.go | 80 + internal/cmd/durable/deploy.go | 240 + internal/cmd/durable/durable.go | 56 + internal/cmd/durable/durable_test.go | 511 +++ internal/cmd/durable/executions/executions.go | 23 + .../cmd/durable/executions/executions_test.go | 313 ++ internal/cmd/durable/executions/get.go | 68 + internal/cmd/durable/executions/list.go | 63 + internal/cmd/durable/executions/stop.go | 81 + internal/cmd/durable/get.go | 48 + internal/cmd/durable/helpers_test.go | 138 + internal/cmd/durable/list.go | 52 + internal/cmd/durable/schedulers/create.go | 96 + internal/cmd/durable/schedulers/delete.go | 75 + internal/cmd/durable/schedulers/disable.go | 68 + internal/cmd/durable/schedulers/enable.go | 53 + internal/cmd/durable/schedulers/list.go | 45 + internal/cmd/durable/schedulers/schedulers.go | 28 + .../cmd/durable/schedulers/schedulers_test.go | 345 ++ internal/cmd/durable/start.go | 84 + internal/cmd/functions/deploy.go | 93 +- internal/cmd/functions/deploy_durable_test.go | 169 + internal/cmd/functions/schedulers/create.go | 24 +- internal/cmd/local/local.go | 2 + internal/durable/durable.go | 291 ++ internal/function/scanner.go | 41 + internal/output/durable.go | 209 + internal/output/functions.go | 6 +- internal/projectconfig/manifest.go | 88 +- .../projectconfig/manifest_durable_test.go | 48 + openapi/openapi.yaml | 818 ++++ tests/e2e/api/durable_functions_test.go | 394 ++ tests/e2e/localmode/localmode_test.go | 58 + 42 files changed, 8532 insertions(+), 875 deletions(-) create mode 100644 docs/durable-functions.md create mode 100644 internal/api/durable_functions.go create mode 100644 internal/api/durable_functions_test.go create mode 100644 internal/cmd/cmdutil/jsonarg.go create mode 100644 internal/cmd/durable/delete.go create mode 100644 internal/cmd/durable/deploy.go create mode 100644 internal/cmd/durable/durable.go create mode 100644 internal/cmd/durable/durable_test.go create mode 100644 internal/cmd/durable/executions/executions.go create mode 100644 internal/cmd/durable/executions/executions_test.go create mode 100644 internal/cmd/durable/executions/get.go create mode 100644 internal/cmd/durable/executions/list.go create mode 100644 internal/cmd/durable/executions/stop.go create mode 100644 internal/cmd/durable/get.go create mode 100644 internal/cmd/durable/helpers_test.go create mode 100644 internal/cmd/durable/list.go create mode 100644 internal/cmd/durable/schedulers/create.go create mode 100644 internal/cmd/durable/schedulers/delete.go create mode 100644 internal/cmd/durable/schedulers/disable.go create mode 100644 internal/cmd/durable/schedulers/enable.go create mode 100644 internal/cmd/durable/schedulers/list.go create mode 100644 internal/cmd/durable/schedulers/schedulers.go create mode 100644 internal/cmd/durable/schedulers/schedulers_test.go create mode 100644 internal/cmd/durable/start.go create mode 100644 internal/cmd/functions/deploy_durable_test.go create mode 100644 internal/durable/durable.go create mode 100644 internal/output/durable.go create mode 100644 internal/projectconfig/manifest_durable_test.go create mode 100644 tests/e2e/api/durable_functions_test.go diff --git a/docs/README.md b/docs/README.md index e7efb14b..ea14720b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,6 +21,7 @@ account (auth) │ ├── connects → databases, storage │ ├── runs on → a runtime │ ├── invoked via → alias / HTTP / scheduler + ├── durable functions ..... long-running, checkpointed logic → durable-functions.md ├── databases ............. managed Postgres → databases.md │ └── schema changed by → migrations ├── storage ............... buckets → objects, policies → storage.md @@ -81,6 +82,7 @@ Piped, CI, and `NO_COLOR` output remains plain. Machine output is unchanged. | Account / auth | sign up, log in/out | `signup`, `login`, `logout` | [authentication.md](authentication.md) | | Project | create, list, get, rename, delete, select, get anon keys | `projects …`, `use` | below | | Functions | deploy, invoke, inspect, schedule, alias | `functions …` | [functions.md](functions.md) | +| Durable functions | deploy, start, inspect executions, schedule | `cloud durable …` | [durable-functions.md](durable-functions.md) | | Databases | create, inspect, delete, migrate | `databases …`, `migrations …` | [databases.md](databases.md) | | Storage | manage buckets, objects, policies | `storage …` | [storage.md](storage.md) | | Variables | deploy, list, get, delete | `variables …` | [variables.md](variables.md) | diff --git a/docs/durable-functions.md b/docs/durable-functions.md new file mode 100644 index 00000000..5f98f523 --- /dev/null +++ b/docs/durable-functions.md @@ -0,0 +1,152 @@ +--- +title: "Durable functions" +description: "A durable function checkpoints its progress and resumes from the last completed step, so one execution can run for hours instead of the seconds a normal invocation allows." +--- + +## What it is + +A durable function checkpoints its progress and resumes from the last completed +step, so one execution can run for hours instead of the seconds a normal +invocation allows. Use one for work that has to survive a restart: a multi-step +order pipeline, a long agent run, a nightly batch that calls out to a slow API. + +Durable functions are cloud-only and JavaScript/TypeScript-only, and they are a +separate collection from standard functions: + +- You **start** an execution instead of invoking a function. The start returns a + handle immediately; the result arrives later. +- Each **execution** is a resource with its own id, status, and result. +- The two collections never accept each other's names or ids. A function's kind + is fixed when it is created. + +## How it relates + +- Belongs to a **project**, and reads **variables**, **databases**, and + **storage** in that project like any function. +- Its sources live in `volcano/functions/` beside standard ones. Only + `volcano-config.yaml` says which are durable, so `volcano functions deploy + --all` skips them and `volcano cloud durable deploy --all` picks them up. +- Every region the project deploys to has to offer durable execution, or the + deploy is refused up front. +- Durable execution needs the durable authoring API, so a source whose runtime + does not offer one is refused before anything is uploaded. + +```yaml +version: 1 +project: + name: my-app +functions: + - name: hello + - name: order-pipeline + kind: durable +``` + +A durable entry takes `variable_scope` and `variables` like any other, and +`durable deploy` sends what the manifest declares. Declaring nothing leaves an +existing function's scope alone. + +## CLI operations + +| Operation | Command | +|---|---| +| Deploy all declared, or one | `volcano cloud durable deploy [--all \| -f ]` | +| List | `volcano cloud durable list` | +| Get | `volcano cloud durable get ` | +| Delete | `volcano cloud durable delete ` | +| Start an execution | `volcano cloud durable start [--input …] [--name …]` | +| List executions | `volcano cloud durable executions list [--status …]` | +| Get one execution | `volcano cloud durable executions get ` | +| Stop an execution | `volcano cloud durable executions stop ` | +| Schedule executions | `volcano cloud durable schedulers create --cron "0 * * * *" [--input …] [--regions …]` | +| List schedulers | `volcano cloud durable schedulers list ` | +| Pause or resume one | `volcano cloud durable schedulers disable\|enable ` | +| Delete one | `volcano cloud durable schedulers delete ` | + +There is no top-level `volcano durable …`: the local development environment +does not run durable executions, and the local server refuses to create a +durable function rather than pretending to. + +## Examples + +```bash +# Deploy every function volcano-config.yaml declares durable +volcano cloud durable deploy --all + +# Deploy one, and let anon keys start it +volcano cloud durable deploy -f order-pipeline --public + +# Start an execution and follow it +volcano cloud durable start order-pipeline --input '{"order_id":4417}' +volcano cloud durable executions get order-pipeline 66666666-6666-4666-8666-666666666666 + +# Only the executions still running +volcano cloud durable executions list order-pipeline --status running + +# End one where it is +volcano cloud durable executions stop order-pipeline 66666666-6666-4666-8666-666666666666 +``` + +## Scheduling executions + +A scheduler ticks on a cron expression and starts an execution instead of +invoking the function, so every tick produces an execution you can list, follow, +and stop like any other: + +```bash +# Start one execution an hour, with the same input each time +volcano cloud durable schedulers create order-pipeline --cron "0 * * * *" --input '{"scope":"hourly"}' + +volcano cloud durable schedulers list order-pipeline +volcano cloud durable executions list order-pipeline +``` + +Each tick names its execution after the run, so a tick Volcano has to retry +resolves to the execution it already started rather than beginning a second one. +Ticks draw on the same invocation allowance and concurrency cap a manual start +does; a tick that would exceed the cap fails that run rather than queueing. + +`schedulers disable` stops the ticks and leaves the scheduler in place; +executions it already started keep running. `schedulers delete` removes the +scheduler and its run history, and also leaves running executions alone — stop +those with `executions stop`. + +## Starting is asynchronous + +`start` returns as soon as the execution is accepted, with the execution's id +and name. Read the execution to get its status and, once it has finished, its +result: + +```bash +volcano cloud durable start order-pipeline --input order.json --name order-4417 +volcano cloud durable executions get order-pipeline +``` + +`--input` takes inline JSON or the path to a JSON file, and is handed to the +function verbatim. Omit it to start with no input at all, which is not the same +as starting with `{}`. + +`--name` is the execution's idempotency key. Starting again under a name that +already names an execution returns the existing one instead of beginning a +second, and is not charged again — so a retried start is safe. Omit it and +Volcano generates one. + +## Visibility + +`--public` lets a project's anon key start executions of one function; +`--private` takes that back. A public durable function is still not invocable +over HTTP the way a public standard function is — starting an execution is the +only thing the anon key can do. Polling and stopping always need a +project-scoped credential. + +Omit both flags and a redeploy keeps the visibility the function already has. A +new durable function starts private. + +## Stopping and deleting + +`executions stop` ends one execution at its next checkpoint. Steps already +completed are not undone, and work already in flight is not interrupted +mid-attempt. Stopping one that has already finished reports the state it is in +rather than failing. + +`durable delete` tears down the function and its execution history. It does not +wait for work in flight, so stop an execution you need ended first. diff --git a/docs/functions.md b/docs/functions.md index 436b1148..cacac76b 100644 --- a/docs/functions.md +++ b/docs/functions.md @@ -19,6 +19,9 @@ is invoked over HTTP, by name/alias, or on a schedule. - Its **visibility** (public/private) and **schedulers** can be declared in the [declarative config](project-configuration.md) or managed with the CLI. - Can be given an **alias** so you can invoke it by a friendly name. +- Work that has to run for hours belongs in a + [durable function](durable-functions.md) instead, which is a separate + collection with its own commands. The CLI discovers functions in the `volcano/functions` directory, detects the runtime from source file extensions, and uploads a packaged archive. diff --git a/docs/project-configuration.md b/docs/project-configuration.md index a352afdb..6d5db53b 100644 --- a/docs/project-configuration.md +++ b/docs/project-configuration.md @@ -54,6 +54,8 @@ functions: cron: "*/5 * * * *" enabled: true payload: { job: refresh } + - name: order-pipeline + kind: durable # asserted, not applied: a kind is fixed at creation ``` Cloud example — export the current cloud project's configuration to a file, @@ -70,6 +72,14 @@ volcano cloud config deploy -f volcano-config.yaml # apply Key semantics: +- `functions[].kind` declares which entries are + [durable functions](durable-functions.md). It is asserted rather than applied, + since a function's kind is fixed when it is created. It is also what tells the + deploy commands apart: `volcano functions deploy --all` skips these entries and + `volcano cloud durable deploy --all` deploys exactly them. Leave it out for a + standard function, and leave the invocation settings out of a durable one — + they describe synchronous HTTP invocation, which a durable function does not + have. - 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 diff --git a/internal/api/durable_functions.go b/internal/api/durable_functions.go new file mode 100644 index 00000000..d592a4bb --- /dev/null +++ b/internal/api/durable_functions.go @@ -0,0 +1,280 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "mime/multipart" + "strconv" + + "github.com/google/uuid" + + "github.com/Kong/volcano-cli/internal/apiclient" + "github.com/Kong/volcano-cli/internal/archive" +) + +// DurableFunctionDeployInput contains one packaged durable function source archive. +type DurableFunctionDeployInput struct { + Name string + Runtime string + Handler string + SourceArchive []byte + IsPublic *bool + // VariableScope and Variables carry the manifest's declaration, nil when it + // declared none — which leaves an existing function's scope alone. + VariableScope *string + Variables *[]string +} + +// DurableExecutionStartInput contains one durable execution start request. Name +// is the idempotency key: repeating a start with the same one returns the +// execution that already exists rather than beginning a second. +type DurableExecutionStartInput struct { + Input map[string]any + Name string +} + +// ListDurableFunctions lists one durable function page for a project. +func (c *Client) ListDurableFunctions(ctx context.Context, projectID uuid.UUID, page, limit int) (*apiclient.PaginatedDurableFunctions, error) { + resp, err := c.client.ListDurableFunctionsWithResponse(ctx, projectID, &apiclient.ListDurableFunctionsParams{ + Page: &page, + Limit: &limit, + }) + if err != nil { + return nil, err + } + return apiResult(resp.StatusCode(), resp.Body, resp.JSON200, resp.JSON400, resp.JSON404) +} + +// DeployDurableFunction deploys one durable function source archive. It creates +// the function on the first call for a name and redeploys it after that. +func (c *Client) DeployDurableFunction(ctx context.Context, projectID uuid.UUID, fn DurableFunctionDeployInput) (*apiclient.DurableFunction, error) { + body, contentType, err := buildDurableFunctionDeployMultipart(fn) + if err != nil { + return nil, err + } + resp, err := c.client.CreateDurableFunctionWithBodyWithResponse(ctx, projectID, contentType, body) + if err != nil { + return nil, err + } + if resp.JSON201 != nil { + return resp.JSON201, nil + } + return apiResult(resp.StatusCode(), resp.Body, resp.JSON200, + resp.JSON400, resp.JSON403, resp.JSON409, resp.JSON503) +} + +// GetDurableFunction returns one durable function by ID or name. +func (c *Client) GetDurableFunction(ctx context.Context, projectID uuid.UUID, function string) (*apiclient.DurableFunction, error) { + resp, err := c.client.GetDurableFunctionWithResponse(ctx, projectID, function) + if err != nil { + return nil, err + } + return apiResult(resp.StatusCode(), resp.Body, resp.JSON200, resp.JSON404) +} + +// DeleteDurableFunction starts deleting one durable function by ID or name. +func (c *Client) DeleteDurableFunction(ctx context.Context, projectID uuid.UUID, function string) error { + resp, err := c.client.DeleteDurableFunctionWithResponse(ctx, projectID, function) + if err != nil { + return err + } + return apiOK(resp.StatusCode(), resp.Body, resp.JSON404) +} + +// StartDurableExecution starts one durable execution and returns its handle. +// A durable execution outlives any request that could wait for it, so this +// never carries a result. +func (c *Client) StartDurableExecution( + ctx context.Context, projectID uuid.UUID, function string, input DurableExecutionStartInput, +) (*apiclient.DurableExecution, error) { + params := &apiclient.StartDurableExecutionParams{} + if input.Name != "" { + name := input.Name + params.XVolcanoExecutionName = &name + } + // The body is the execution's input itself rather than a wrapper, so an + // absent input has to be sent as JSON null. + var body apiclient.StartDurableExecutionJSONRequestBody + if input.Input != nil { + body = input.Input + } + + resp, err := c.client.StartDurableExecutionWithResponse(ctx, projectID, function, params, body) + if err != nil { + return nil, err + } + if resp.JSON202 != nil { + return resp.JSON202, nil + } + return nil, apiErrorFromGeneratedErrors(resp.StatusCode(), resp.Body, + resp.JSON400, resp.JSON404, resp.JSON409, resp.JSON413, resp.JSON429, resp.JSON503) +} + +// ListDurableExecutions lists one execution page for a durable function. The +// statuses each entry carries are the last the platform observed rather than +// live state; fetch a single execution for that. +func (c *Client) ListDurableExecutions( + ctx context.Context, projectID uuid.UUID, function, status string, page, limit int, +) (*apiclient.PaginatedDurableExecutions, error) { + params := &apiclient.ListDurableExecutionsParams{Page: &page, Limit: &limit} + if status != "" { + executionStatus := apiclient.DurableExecutionStatus(status) + params.Status = &executionStatus + } + + resp, err := c.client.ListDurableExecutionsWithResponse(ctx, projectID, function, params) + if err != nil { + return nil, err + } + return apiResult(resp.StatusCode(), resp.Body, resp.JSON200, resp.JSON400, resp.JSON404) +} + +// GetDurableExecution returns one durable execution, including its result once +// it has finished. +func (c *Client) GetDurableExecution( + ctx context.Context, projectID uuid.UUID, function string, executionID uuid.UUID, +) (*apiclient.DurableExecution, error) { + resp, err := c.client.GetDurableExecutionWithResponse(ctx, projectID, function, executionID) + if err != nil { + return nil, err + } + return apiResult(resp.StatusCode(), resp.Body, resp.JSON200, resp.JSON404, resp.JSON503) +} + +// StopDurableExecution stops one durable execution. Completed steps are not +// undone; the execution stops where it is. +func (c *Client) StopDurableExecution( + ctx context.Context, projectID uuid.UUID, function string, executionID uuid.UUID, +) (*apiclient.DurableExecution, error) { + resp, err := c.client.StopDurableExecutionWithResponse(ctx, projectID, function, executionID) + if err != nil { + return nil, err + } + return apiResult(resp.StatusCode(), resp.Body, resp.JSON200, resp.JSON404, resp.JSON409, resp.JSON503) +} + +// ListDurableFunctionSchedulers lists schedulers for a durable function. +func (c *Client) ListDurableFunctionSchedulers( + ctx context.Context, projectID uuid.UUID, function string, +) (*apiclient.FunctionSchedulerListResponse, error) { + resp, err := c.client.ListDurableFunctionSchedulersWithResponse(ctx, projectID, function) + if err != nil { + return nil, err + } + return apiResult(resp.StatusCode(), resp.Body, resp.JSON200, resp.JSON404) +} + +// CreateDurableFunctionScheduler creates one scheduler for a durable function. +// Each tick starts an execution rather than invoking the function. +func (c *Client) CreateDurableFunctionScheduler( + ctx context.Context, projectID uuid.UUID, function string, input FunctionSchedulerInput, +) (*apiclient.FunctionScheduler, error) { + body := apiclient.CreateDurableFunctionSchedulerJSONRequestBody{ + Name: input.Name, + Enabled: input.Enabled, + Schedule: apiclient.ScheduleRequest{ + CronExpression: input.CronExpression, + }, + } + if input.Payload != nil { + payload := input.Payload + body.Payload = &payload + } + if input.Regions != nil { + regions := input.Regions + body.Regions = ®ions + } + + resp, err := c.client.CreateDurableFunctionSchedulerWithResponse(ctx, projectID, function, body) + if err != nil { + return nil, err + } + if resp.JSON201 != nil { + return resp.JSON201, nil + } + return nil, apiErrorFromGeneratedErrors(resp.StatusCode(), resp.Body, resp.JSON400, resp.JSON404) +} + +// UpdateDurableFunctionScheduler updates one scheduler of a durable function. +func (c *Client) UpdateDurableFunctionScheduler( + ctx context.Context, projectID uuid.UUID, function string, schedulerID uuid.UUID, input FunctionSchedulerInput, +) (*apiclient.FunctionScheduler, error) { + body := apiclient.UpdateDurableFunctionSchedulerJSONRequestBody{ + Enabled: input.Enabled, + } + if input.Name != "" { + name := input.Name + body.Name = &name + } + if input.CronExpression != "" { + body.Schedule = &apiclient.ScheduleRequest{CronExpression: input.CronExpression} + } + if input.Payload != nil { + payload := input.Payload + body.Payload = &payload + } + if input.Regions != nil { + regions := input.Regions + body.Regions = ®ions + } + + resp, err := c.client.UpdateDurableFunctionSchedulerWithResponse(ctx, projectID, function, schedulerID, body) + if err != nil { + return nil, err + } + return apiResult(resp.StatusCode(), resp.Body, resp.JSON200, resp.JSON400, resp.JSON404) +} + +// DeleteDurableFunctionScheduler deletes one scheduler of a durable function. +func (c *Client) DeleteDurableFunctionScheduler( + ctx context.Context, projectID uuid.UUID, function string, schedulerID uuid.UUID, +) error { + resp, err := c.client.DeleteDurableFunctionSchedulerWithResponse(ctx, projectID, function, schedulerID) + if err != nil { + return err + } + return apiOK(resp.StatusCode(), resp.Body, resp.JSON404) +} + +func buildDurableFunctionDeployMultipart(fn DurableFunctionDeployInput) (*bytes.Buffer, string, error) { + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + if err := writer.WriteField("name", fn.Name); err != nil { + return nil, "", fmt.Errorf("failed to write name field: %w", err) + } + if err := writer.WriteField("runtime", fn.Runtime); err != nil { + return nil, "", fmt.Errorf("failed to write runtime field: %w", err) + } + if err := writer.WriteField("handler", fn.Handler); err != nil { + return nil, "", fmt.Errorf("failed to write handler field: %w", err) + } + if fn.IsPublic != nil { + if err := writer.WriteField("is_public", strconv.FormatBool(*fn.IsPublic)); err != nil { + return nil, "", fmt.Errorf("failed to write is_public field: %w", err) + } + } + if fn.VariableScope != nil { + if err := writer.WriteField("variable_scope", *fn.VariableScope); err != nil { + return nil, "", fmt.Errorf("failed to write variable_scope field: %w", err) + } + } + if fn.Variables != nil { + // A JSON-encoded string, as the standard create takes it. + encoded, err := json.Marshal(*fn.Variables) + if err != nil { + return nil, "", fmt.Errorf("failed to encode variables field: %w", err) + } + if err := writer.WriteField("variables", string(encoded)); err != nil { + return nil, "", fmt.Errorf("failed to write variables field: %w", err) + } + } + if err := archive.WriteArchivePart(writer, "code", fn.Name, fn.SourceArchive); err != nil { + return nil, "", err + } + if err := writer.Close(); err != nil { + return nil, "", fmt.Errorf("failed to finalize multipart body: %w", err) + } + return body, writer.FormDataContentType(), nil +} diff --git a/internal/api/durable_functions_test.go b/internal/api/durable_functions_test.go new file mode 100644 index 00000000..434dd570 --- /dev/null +++ b/internal/api/durable_functions_test.go @@ -0,0 +1,218 @@ +package api + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + durableTestProjectID = uuid.MustParse("22222222-2222-4222-8222-222222222222") + durableTestExecutionID = uuid.MustParse("66666666-6666-4666-8666-666666666666") +) + +// A start body is the execution's input itself rather than a wrapper, and the +// idempotency key travels as a header, so both are easy to get wrong without a +// test pinning the wire shape. +func TestStartDurableExecutionSendsInputAsTheBody(t *testing.T) { + var body any + var executionName string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, + "/projects/"+durableTestProjectID.String()+"/durable-functions/order-pipeline/executions", + r.URL.Path) + executionName = r.Header.Get("X-Volcano-Execution-Name") + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + writeAPIJSON(t, w, http.StatusAccepted, durableExecutionAPIPayload("pending")) + })) + defer server.Close() + + client, err := NewClient(server.URL, "token", WithHTTPClient(server.Client())) + require.NoError(t, err) + + execution, err := client.StartDurableExecution(context.Background(), durableTestProjectID, "order-pipeline", + DurableExecutionStartInput{Input: map[string]any{"order_id": 4417}, Name: "order-4417"}) + require.NoError(t, err) + assert.Equal(t, map[string]any{"order_id": float64(4417)}, body) + assert.Equal(t, "order-4417", executionName) + assert.Equal(t, durableTestExecutionID, execution.Id) +} + +// No input at all is a different thing from an empty object, and no name means +// the platform generates one, so neither may be sent as a made-up value. +func TestStartDurableExecutionWithoutInputOrName(t *testing.T) { + var raw []byte + var hasNameHeader bool + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, hasNameHeader = r.Header["X-Volcano-Execution-Name"] + var err error + raw, err = io.ReadAll(r.Body) + require.NoError(t, err) + writeAPIJSON(t, w, http.StatusAccepted, durableExecutionAPIPayload("pending")) + })) + defer server.Close() + + client, err := NewClient(server.URL, "token", WithHTTPClient(server.Client())) + require.NoError(t, err) + + _, err = client.StartDurableExecution(context.Background(), durableTestProjectID, "order-pipeline", + DurableExecutionStartInput{}) + require.NoError(t, err) + assert.JSONEq(t, "null", string(raw)) + assert.False(t, hasNameHeader) +} + +// A start is refused for reasons the caller has to be able to tell apart: too +// many executions in flight (429) and durable execution being unavailable +// (503) are both retryable, unlike a bad request. +func TestStartDurableExecutionSurfacesRefusalStatuses(t *testing.T) { + for _, tc := range []struct { + name string + status int + message string + }{ + {name: "concurrency", status: http.StatusTooManyRequests, message: "too many executions running"}, + {name: "unavailable", status: http.StatusServiceUnavailable, message: "durable execution is unavailable"}, + {name: "payload too large", status: http.StatusRequestEntityTooLarge, message: "input too large"}, + } { + t.Run(tc.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + writeAPIJSON(t, w, tc.status, map[string]string{"error": tc.message}) + })) + defer server.Close() + + client, err := NewClient(server.URL, "token", WithHTTPClient(server.Client())) + require.NoError(t, err) + + _, err = client.StartDurableExecution(context.Background(), durableTestProjectID, "order-pipeline", + DurableExecutionStartInput{}) + require.Error(t, err) + assert.Equal(t, tc.status, Status(err)) + assert.ErrorContains(t, err, tc.message) + }) + } +} + +// Deploying is a create-or-redeploy, so both 201 and 200 are successes. +func TestDeployDurableFunctionAcceptsCreatedAndUpdated(t *testing.T) { + for _, status := range []int{http.StatusCreated, http.StatusOK} { + t.Run(http.StatusText(status), func(t *testing.T) { + var fields map[string]string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, r.ParseMultipartForm(1024*1024)) + fields = map[string]string{ + "name": r.FormValue("name"), + "runtime": r.FormValue("runtime"), + "handler": r.FormValue("handler"), + "is_public": r.FormValue("is_public"), + } + require.Len(t, r.MultipartForm.File["code"], 1) + writeAPIJSON(t, w, status, durableFunctionAPIPayload()) + })) + defer server.Close() + + client, err := NewClient(server.URL, "token", WithHTTPClient(server.Client())) + require.NoError(t, err) + + isPublic := true + fn, err := client.DeployDurableFunction(context.Background(), durableTestProjectID, DurableFunctionDeployInput{ + Name: "order-pipeline", + Runtime: "nodejs24.x", + Handler: "handler", + SourceArchive: []byte("archive"), + IsPublic: &isPublic, + }) + require.NoError(t, err) + assert.Equal(t, "order-pipeline", fn.Name) + assert.Equal(t, map[string]string{ + "name": "order-pipeline", "runtime": "nodejs24.x", "handler": "handler", "is_public": "true", + }, fields) + }) + } +} + +// An absent is_public is what tells the server to keep the visibility the +// function already has, so it must not be sent as false. +func TestDeployDurableFunctionOmitsUnsetVisibility(t *testing.T) { + var sent []string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, r.ParseMultipartForm(1024*1024)) + sent = r.MultipartForm.Value["is_public"] + writeAPIJSON(t, w, http.StatusOK, durableFunctionAPIPayload()) + })) + defer server.Close() + + client, err := NewClient(server.URL, "token", WithHTTPClient(server.Client())) + require.NoError(t, err) + + _, err = client.DeployDurableFunction(context.Background(), durableTestProjectID, DurableFunctionDeployInput{ + Name: "order-pipeline", Runtime: "nodejs24.x", Handler: "handler", SourceArchive: []byte("archive"), + }) + require.NoError(t, err) + assert.Empty(t, sent) +} + +func TestListDurableExecutionsPassesTheStatusFilter(t *testing.T) { + var query string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + query = r.URL.RawQuery + writeAPIJSON(t, w, http.StatusOK, map[string]any{ + "data": []any{durableExecutionAPIPayload("running")}, + "has_more": false, + "page": 1, + "limit": 100, + "total": 1, + }) + })) + defer server.Close() + + client, err := NewClient(server.URL, "token", WithHTTPClient(server.Client())) + require.NoError(t, err) + + page, err := client.ListDurableExecutions(context.Background(), durableTestProjectID, "order-pipeline", + "running", 1, 100) + require.NoError(t, err) + require.Len(t, page.Data, 1) + assert.Contains(t, query, "status=running") +} + +func durableFunctionAPIPayload() map[string]any { + return map[string]any{ + "id": "55555555-5555-4555-8555-555555555555", + "project_id": durableTestProjectID.String(), + "name": "order-pipeline", + "runtime": "nodejs24.x", + "handler": "handler", + "kind": "durable", + "status": "provisioning", + "is_public": true, + "deployed_regions": []string{"aws-us-east-1"}, + "durable": map[string]any{ + "execution_timeout_seconds": 3600, + "retention_days": 30, + }, + "created_at": "2026-05-20T00:00:00Z", + "updated_at": "2026-05-20T00:00:00Z", + } +} + +func durableExecutionAPIPayload(status string) map[string]any { + return map[string]any{ + "id": durableTestExecutionID.String(), + "durable_function_id": "55555555-5555-4555-8555-555555555555", + "project_id": durableTestProjectID.String(), + "name": "order-4417", + "status": status, + "region": "aws-us-east-1", + "created_at": "2026-05-20T00:00:00Z", + "updated_at": "2026-05-20T00:00:00Z", + } +} diff --git a/internal/apiclient/client.gen.go b/internal/apiclient/client.gen.go index f54a0673..f848181e 100644 --- a/internal/apiclient/client.gen.go +++ b/internal/apiclient/client.gen.go @@ -634,6 +634,60 @@ func (e DeploymentProgressCurrentPhase) Valid() bool { } } +// Defines values for DurableExecutionStatus. +const ( + DurableExecutionStatusFailed DurableExecutionStatus = "failed" + DurableExecutionStatusPending DurableExecutionStatus = "pending" + DurableExecutionStatusRunning DurableExecutionStatus = "running" + DurableExecutionStatusStopped DurableExecutionStatus = "stopped" + DurableExecutionStatusSucceeded DurableExecutionStatus = "succeeded" + DurableExecutionStatusTimedOut DurableExecutionStatus = "timed_out" +) + +// Valid indicates whether the value is a known member of the DurableExecutionStatus enum. +func (e DurableExecutionStatus) Valid() bool { + switch e { + case DurableExecutionStatusFailed: + return true + case DurableExecutionStatusPending: + return true + case DurableExecutionStatusRunning: + return true + case DurableExecutionStatusStopped: + return true + case DurableExecutionStatusSucceeded: + return true + case DurableExecutionStatusTimedOut: + return true + default: + return false + } +} + +// Defines values for DurableFunctionStatus. +const ( + DurableFunctionStatusActive DurableFunctionStatus = "active" + DurableFunctionStatusDeleting DurableFunctionStatus = "deleting" + DurableFunctionStatusFailed DurableFunctionStatus = "failed" + DurableFunctionStatusProvisioning DurableFunctionStatus = "provisioning" +) + +// Valid indicates whether the value is a known member of the DurableFunctionStatus enum. +func (e DurableFunctionStatus) Valid() bool { + switch e { + case DurableFunctionStatusActive: + return true + case DurableFunctionStatusDeleting: + return true + case DurableFunctionStatusFailed: + return true + case DurableFunctionStatusProvisioning: + return true + default: + return false + } +} + // Defines values for EmailTemplateTemplateType. const ( EmailTemplateTemplateTypeConfirmation EmailTemplateTemplateType = "confirmation" @@ -1021,6 +1075,24 @@ func (e FunctionDeploymentStatus) Valid() bool { } } +// Defines values for FunctionKind. +const ( + Durable FunctionKind = "durable" + Standard FunctionKind = "standard" +) + +// Valid indicates whether the value is a known member of the FunctionKind enum. +func (e FunctionKind) Valid() bool { + switch e { + case Durable: + return true + case Standard: + return true + default: + return false + } +} + // Defines values for FunctionSchedulerScheduleKind. const ( FunctionSchedulerScheduleKindCron FunctionSchedulerScheduleKind = "cron" @@ -3073,6 +3145,48 @@ func (e SummarizeProjectDeploymentsParamsResourceType) Valid() bool { } } +// Defines values for CreateDurableFunctionMultipartBodyRuntime. +const ( + CreateDurableFunctionMultipartBodyRuntimeNodejs22X CreateDurableFunctionMultipartBodyRuntime = "nodejs22.x" + CreateDurableFunctionMultipartBodyRuntimeNodejs24X CreateDurableFunctionMultipartBodyRuntime = "nodejs24.x" + CreateDurableFunctionMultipartBodyRuntimePython310 CreateDurableFunctionMultipartBodyRuntime = "python3.10" + CreateDurableFunctionMultipartBodyRuntimePython311 CreateDurableFunctionMultipartBodyRuntime = "python3.11" + CreateDurableFunctionMultipartBodyRuntimePython312 CreateDurableFunctionMultipartBodyRuntime = "python3.12" + CreateDurableFunctionMultipartBodyRuntimePython313 CreateDurableFunctionMultipartBodyRuntime = "python3.13" + CreateDurableFunctionMultipartBodyRuntimePython314 CreateDurableFunctionMultipartBodyRuntime = "python3.14" + CreateDurableFunctionMultipartBodyRuntimeRuby33 CreateDurableFunctionMultipartBodyRuntime = "ruby3.3" + CreateDurableFunctionMultipartBodyRuntimeRuby34 CreateDurableFunctionMultipartBodyRuntime = "ruby3.4" + CreateDurableFunctionMultipartBodyRuntimeRuby40 CreateDurableFunctionMultipartBodyRuntime = "ruby4.0" +) + +// Valid indicates whether the value is a known member of the CreateDurableFunctionMultipartBodyRuntime enum. +func (e CreateDurableFunctionMultipartBodyRuntime) Valid() bool { + switch e { + case CreateDurableFunctionMultipartBodyRuntimeNodejs22X: + return true + case CreateDurableFunctionMultipartBodyRuntimeNodejs24X: + return true + case CreateDurableFunctionMultipartBodyRuntimePython310: + return true + case CreateDurableFunctionMultipartBodyRuntimePython311: + return true + case CreateDurableFunctionMultipartBodyRuntimePython312: + return true + case CreateDurableFunctionMultipartBodyRuntimePython313: + return true + case CreateDurableFunctionMultipartBodyRuntimePython314: + return true + case CreateDurableFunctionMultipartBodyRuntimeRuby33: + return true + case CreateDurableFunctionMultipartBodyRuntimeRuby34: + return true + case CreateDurableFunctionMultipartBodyRuntimeRuby40: + return true + default: + return false + } +} + // Defines values for DeleteEmailTemplateParamsType. const ( DeleteEmailTemplateParamsTypeConfirmation DeleteEmailTemplateParamsType = "confirmation" @@ -3162,40 +3276,40 @@ func (e CreateFrontendMultipartBodyFramework) Valid() bool { // Defines values for CreateFunctionMultipartBodyRuntime. const ( - Nodejs22X CreateFunctionMultipartBodyRuntime = "nodejs22.x" - Nodejs24X CreateFunctionMultipartBodyRuntime = "nodejs24.x" - Python310 CreateFunctionMultipartBodyRuntime = "python3.10" - Python311 CreateFunctionMultipartBodyRuntime = "python3.11" - Python312 CreateFunctionMultipartBodyRuntime = "python3.12" - Python313 CreateFunctionMultipartBodyRuntime = "python3.13" - Python314 CreateFunctionMultipartBodyRuntime = "python3.14" - Ruby33 CreateFunctionMultipartBodyRuntime = "ruby3.3" - Ruby34 CreateFunctionMultipartBodyRuntime = "ruby3.4" - Ruby40 CreateFunctionMultipartBodyRuntime = "ruby4.0" + CreateFunctionMultipartBodyRuntimeNodejs22X CreateFunctionMultipartBodyRuntime = "nodejs22.x" + CreateFunctionMultipartBodyRuntimeNodejs24X CreateFunctionMultipartBodyRuntime = "nodejs24.x" + CreateFunctionMultipartBodyRuntimePython310 CreateFunctionMultipartBodyRuntime = "python3.10" + CreateFunctionMultipartBodyRuntimePython311 CreateFunctionMultipartBodyRuntime = "python3.11" + CreateFunctionMultipartBodyRuntimePython312 CreateFunctionMultipartBodyRuntime = "python3.12" + CreateFunctionMultipartBodyRuntimePython313 CreateFunctionMultipartBodyRuntime = "python3.13" + CreateFunctionMultipartBodyRuntimePython314 CreateFunctionMultipartBodyRuntime = "python3.14" + CreateFunctionMultipartBodyRuntimeRuby33 CreateFunctionMultipartBodyRuntime = "ruby3.3" + CreateFunctionMultipartBodyRuntimeRuby34 CreateFunctionMultipartBodyRuntime = "ruby3.4" + CreateFunctionMultipartBodyRuntimeRuby40 CreateFunctionMultipartBodyRuntime = "ruby4.0" ) // Valid indicates whether the value is a known member of the CreateFunctionMultipartBodyRuntime enum. func (e CreateFunctionMultipartBodyRuntime) Valid() bool { switch e { - case Nodejs22X: + case CreateFunctionMultipartBodyRuntimeNodejs22X: return true - case Nodejs24X: + case CreateFunctionMultipartBodyRuntimeNodejs24X: return true - case Python310: + case CreateFunctionMultipartBodyRuntimePython310: return true - case Python311: + case CreateFunctionMultipartBodyRuntimePython311: return true - case Python312: + case CreateFunctionMultipartBodyRuntimePython312: return true - case Python313: + case CreateFunctionMultipartBodyRuntimePython313: return true - case Python314: + case CreateFunctionMultipartBodyRuntimePython314: return true - case Ruby33: + case CreateFunctionMultipartBodyRuntimeRuby33: return true - case Ruby34: + case CreateFunctionMultipartBodyRuntimeRuby34: return true - case Ruby40: + case CreateFunctionMultipartBodyRuntimeRuby40: return true default: return false @@ -4611,6 +4725,118 @@ type DeviceAuthorizationResponse struct { VerificationUriComplete string `json:"verification_uri_complete"` } +// DurableExecution defines model for DurableExecution. +type DurableExecution struct { + // CompletedAt Present once the execution has reached a terminal status. + CompletedAt *time.Time `json:"completed_at,omitempty"` + CreatedAt time.Time `json:"created_at"` + + // Error Why a failed or timed-out execution ended. + Error *DurableExecutionError `json:"error,omitempty"` + FunctionId openapi_types.UUID `json:"function_id"` + Id openapi_types.UUID `json:"id"` + + // Name Idempotency key for the execution. Supplied by the client through + // `X-Volcano-Execution-Name`, otherwise generated. + Name string `json:"name"` + + // Region Region the execution runs in. An execution is pinned to one region + // for its whole life because its checkpoints live there. + Region string `json:"region"` + + // Result Whatever the function returned, verbatim. Absent while the execution + // is still running, and absent once its retention period has lapsed. + Result interface{} `json:"result,omitempty"` + + // ResultExpired `true` when the execution is terminal but its result is no longer + // retained, which distinguishes a discarded result from an empty one. + ResultExpired *bool `json:"result_expired,omitempty"` + + // Status Lifecycle state of an execution. `pending` covers the window between the + // platform reserving the execution name and the function accepting the + // start, and has no counterpart once the execution is under way. + // `succeeded`, `failed`, `timed_out` and `stopped` are terminal. + Status DurableExecutionStatus `json:"status"` +} + +// DurableExecutionError Why a failed or timed-out execution ended. +type DurableExecutionError struct { + Message *string `json:"message,omitempty"` + Type *string `json:"type,omitempty"` +} + +// DurableExecutionStatus Lifecycle state of an execution. `pending` covers the window between the +// platform reserving the execution name and the function accepting the +// start, and has no counterpart once the execution is under way. +// `succeeded`, `failed`, `timed_out` and `stopped` are terminal. +type DurableExecutionStatus string + +// DurableFunction A durable function. Separate from `Function` because a durable function +// is invoked only through its own execution endpoints, so it has no +// invocation mode, HTTP auth mode, OpenAPI document or invoke URL, and it +// carries a `durable` configuration that a standard function has no field +// for. +type DurableFunction struct { + CreatedAt time.Time `json:"created_at"` + + // CurrentDeploymentId Identifier of the latest deployment operation + CurrentDeploymentId *openapi_types.UUID `json:"current_deployment_id,omitempty"` + + // DeployedRegions Regions where this function is currently deployed + DeployedRegions []string `json:"deployed_regions"` + + // Durable Execution limits the function was created with, derived from the + // project's plan. Fixed for the life of the function: changing them means + // creating a new one. + Durable DurableFunctionConfig `json:"durable"` + Handler *string `json:"handler,omitempty"` + Id openapi_types.UUID `json:"id"` + + // IsPublic Whether anon keys may start executions of this function through + // `POST /durable-functions/{functionId}/executions`. + // + // When `true`, an anon key holding `functions.invoke` can start an + // execution. When `false` (the default) only service keys and auth + // user tokens can. Reading and stopping an execution always require + // the project owner's token, whatever this is set to. + // + // Set it when the function is created. Durable functions have no + // update endpoint, so changing visibility later means redeploying. + // + // A public durable function is startable, never invocable: it is not + // reachable through `POST /functions/{functionId}/invoke` or a + // function URL, which answer `404` for either visibility. + IsPublic bool `json:"is_public"` + + // LastInvokedAt Most recent successful invocation timestamp + LastInvokedAt *time.Time `json:"last_invoked_at,omitempty"` + Name string `json:"name"` + + // PendingDeploymentId Newest queued deployment that will run after the current operation + PendingDeploymentId *openapi_types.UUID `json:"pending_deployment_id,omitempty"` + ProjectId openapi_types.UUID `json:"project_id"` + + // ProvisioningStartedAt Timestamp when the current provisioning phase started + ProvisioningStartedAt *time.Time `json:"provisioning_started_at,omitempty"` + Runtime *string `json:"runtime,omitempty"` + Status DurableFunctionStatus `json:"status"` + UpdatedAt time.Time `json:"updated_at"` +} + +// DurableFunctionStatus defines model for DurableFunction.Status. +type DurableFunctionStatus string + +// DurableFunctionConfig Execution limits the function was created with, derived from the +// project's plan. Fixed for the life of the function: changing them means +// creating a new one. +type DurableFunctionConfig struct { + // ExecutionTimeoutSeconds How long a single execution may run before it is timed out. + ExecutionTimeoutSeconds int64 `json:"execution_timeout_seconds"` + + // RetentionDays How long a finished execution's result and history are retained. + RetentionDays int64 `json:"retention_days"` +} + // EmailTemplate Custom email template type EmailTemplate struct { CreatedAt *time.Time `json:"created_at,omitempty"` @@ -4939,6 +5165,13 @@ type FunctionInvocationRequest struct { // FunctionInvocationResponse Raw function response body returned by the invoked function. type FunctionInvocationResponse map[string]interface{} +// FunctionKind Which kind of function this is. `standard` runs once per invocation. +// `durable` checkpoints its progress and resumes from the last completed +// step, and is invoked asynchronously through its own executions +// collection. A function's kind is fixed when it is created and cannot be +// changed afterwards. Omitting this field means `standard`. +type FunctionKind string + // FunctionRegion defines model for FunctionRegion. type FunctionRegion struct { // Code Region identifier accepted by function APIs. @@ -4972,6 +5205,9 @@ type FunctionRuntimeOption struct { Default bool `json:"default"` Deployment FunctionRuntimeDeployment `json:"deployment"` + // DurableCapable Whether a durable function can be authored on this runtime. Only runtimes with a durable authoring API report true, and a durable deploy naming any other runtime is rejected. + DurableCapable bool `json:"durable_capable"` + // Language Runtime language family used by the CLI to choose defaults from source files. Language string `json:"language"` @@ -4986,10 +5222,17 @@ type FunctionRuntimesResponse struct { // FunctionScheduler defines model for FunctionScheduler. type FunctionScheduler struct { - CreatedAt *time.Time `json:"created_at,omitempty"` - CronExpression *string `json:"cron_expression,omitempty"` - Enabled *bool `json:"enabled,omitempty"` - FunctionId *openapi_types.UUID `json:"function_id,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + CronExpression *string `json:"cron_expression,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + FunctionId *openapi_types.UUID `json:"function_id,omitempty"` + + // FunctionKind Which collection the scheduled function belongs to. A project-wide + // scheduler list mixes both kinds, and this is what says whether the + // function is read back from `/projects/{id}/functions` or + // `/projects/{id}/durable-functions` — and whether a tick invokes it + // or starts a durable execution. + FunctionKind *FunctionKind `json:"function_kind,omitempty"` Id *openapi_types.UUID `json:"id,omitempty"` LastCompletedAt *time.Time `json:"last_completed_at,omitempty"` LastError *string `json:"last_error,omitempty"` @@ -5515,6 +5758,40 @@ type PaginatedDatabases struct { Total int `json:"total"` } +// PaginatedDurableExecutions defines model for PaginatedDurableExecutions. +type PaginatedDurableExecutions struct { + Data []DurableExecution `json:"data"` + + // HasMore Whether there are more pages available + HasMore bool `json:"has_more"` + + // Limit Number of items per page + Limit int `json:"limit"` + + // Page Current page number (1-indexed) + Page int `json:"page"` + + // Total Total number of items across all pages + Total int `json:"total"` +} + +// PaginatedDurableFunctions defines model for PaginatedDurableFunctions. +type PaginatedDurableFunctions struct { + Data []DurableFunction `json:"data"` + + // HasMore Whether there are more pages available + HasMore bool `json:"has_more"` + + // Limit Number of items per page + Limit int `json:"limit"` + + // Page Current page number (1-indexed) + Page int `json:"page"` + + // Total Total number of items across all pages + Total int `json:"total"` +} + // PaginatedFrontendDeployments defines model for PaginatedFrontendDeployments. type PaginatedFrontendDeployments struct { Data []FrontendDeployment `json:"data"` @@ -7375,6 +7652,12 @@ type DeploymentResourceType string // DeploymentStatus defines model for DeploymentStatus. type DeploymentStatus string +// DurableExecutionId defines model for DurableExecutionId. +type DurableExecutionId = openapi_types.UUID + +// DurableFunctionId defines model for DurableFunctionId. +type DurableFunctionId = string + // EndingBefore defines model for EndingBefore. type EndingBefore = string @@ -8284,17 +8567,8 @@ type ListProjectCustomDomainsParams struct { Search *Search `form:"search,omitempty" json:"search,omitempty"` } -// DeleteEmailTemplateParamsType defines parameters for DeleteEmailTemplate. -type DeleteEmailTemplateParamsType string - -// GetEmailTemplateParamsType defines parameters for GetEmailTemplate. -type GetEmailTemplateParamsType string - -// UpdateEmailTemplateParamsType defines parameters for UpdateEmailTemplate. -type UpdateEmailTemplateParamsType string - -// ListFrontendsParams defines parameters for ListFrontends. -type ListFrontendsParams struct { +// ListDurableFunctionsParams defines parameters for ListDurableFunctions. +type ListDurableFunctionsParams struct { // Page Page number (1-indexed) for offset pagination. Declares no schema // default so the request validator does not inject one: handlers that omit // `page` see it unset (nil) and default to 1 in code, while cursor-first @@ -8305,51 +8579,35 @@ type ListFrontendsParams struct { // Limit Number of items per page (max 100) Limit *Limit `form:"limit,omitempty" json:"limit,omitempty"` - // Cursor Opaque keyset pagination cursor from a previous response's `next_cursor` - // — pages forward. Mutually exclusive with `page` and `ending_before`; - // combining them returns 400. When supplied, the request's `search` and - // `limit` must match the values bound to the cursor or the request returns 400. - Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` - - // EndingBefore Opaque keyset pagination cursor from a previous response's `prev_cursor` - // — pages backward (the page immediately preceding this cursor). Mutually - // exclusive with `page` and `cursor`; combining them returns 400. `search` - // and `limit` must match the values bound to the cursor or the request - // returns 400. - EndingBefore *EndingBefore `form:"ending_before,omitempty" json:"ending_before,omitempty"` - - // Offset Bounded row offset past the keyset anchor named by `cursor` (forward) or - // `ending_before` (backward) — the hybrid jump. Seek to the anchor, then - // skip this many rows within. Used for numbered jump-to-page: from the - // current page, seek to its next/prev cursor and offset the remaining - // pages. Only honored on the cursor pagination path; ignored otherwise. - Offset *Offset `form:"offset,omitempty" json:"offset,omitempty"` - // Search Case-insensitive substring match on the resource `name`. See the // endpoint description for supported pagination modes. Search *Search `form:"search,omitempty" json:"search,omitempty"` } -// CreateFrontendMultipartBody defines parameters for CreateFrontend. -type CreateFrontendMultipartBody struct { - // AppRoot Optional relative POSIX path from the uploaded archive root to the Next.js app to build, for example `apps/web`. - AppRoot *string `json:"app_root,omitempty"` +// CreateDurableFunctionMultipartBody defines parameters for CreateDurableFunction. +type CreateDurableFunctionMultipartBody struct { + // Code ZIP or tar.gz archive containing function source code plus dependency manifests/lockfiles. + Code openapi_types.File `json:"code"` - // Archive ZIP or tar.gz archive of the frontend project directory or monorepo workspace root. The API enforces SOURCE_ARCHIVE_SIZE_LIMIT_MB and stores a normalized tar.gz archive. - Archive openapi_types.File `json:"archive"` + // Handler The name of the function to invoke. Defaults to "handler" if not specified. + Handler *string `json:"handler,omitempty"` - // Framework Next.js frontend. Supported Next.js majors are 15.x and 16.x. - Framework *CreateFrontendMultipartBodyFramework `json:"framework,omitempty"` + // IsPublic Whether anon keys with `functions.invoke` may start an execution. + IsPublic *bool `json:"is_public,omitempty"` - // Name DNS-safe frontend name + // Name DNS-safe function name (lowercase letters, numbers, hyphens; cannot start or end with hyphen) Name string `json:"name"` + + // Runtime Runtime environment. Required. Must be a runtime that supports + // durable execution; one that does not is rejected with 400. + Runtime CreateDurableFunctionMultipartBodyRuntime `json:"runtime"` } -// CreateFrontendMultipartBodyFramework defines parameters for CreateFrontend. -type CreateFrontendMultipartBodyFramework string +// CreateDurableFunctionMultipartBodyRuntime defines parameters for CreateDurableFunction. +type CreateDurableFunctionMultipartBodyRuntime string -// ListFrontendDeploymentsParams defines parameters for ListFrontendDeployments. -type ListFrontendDeploymentsParams struct { +// ListDurableExecutionsParams defines parameters for ListDurableExecutions. +type ListDurableExecutionsParams struct { // Page Page number (1-indexed) for offset pagination. Declares no schema // default so the request validator does not inject one: handlers that omit // `page` see it unset (nil) and default to 1 in code, while cursor-first @@ -8359,16 +8617,31 @@ type ListFrontendDeploymentsParams struct { // Limit Number of items per page (max 100) Limit *Limit `form:"limit,omitempty" json:"limit,omitempty"` + + // Status Return only executions in this status. + Status *DurableExecutionStatus `form:"status,omitempty" json:"status,omitempty"` } -// GetFrontendUsageHistoryParams defines parameters for GetFrontendUsageHistory. -type GetFrontendUsageHistoryParams struct { - // Days Number of trailing days to return (1–90, default 30). - Days *int `form:"days,omitempty" json:"days,omitempty"` +// StartDurableExecutionJSONBody defines parameters for StartDurableExecution. +type StartDurableExecutionJSONBody = interface{} + +// StartDurableExecutionParams defines parameters for StartDurableExecution. +type StartDurableExecutionParams struct { + // XVolcanoExecutionName Idempotency key for this execution. Generated when omitted. + XVolcanoExecutionName *string `json:"X-Volcano-Execution-Name,omitempty"` } -// ListFunctionsParams defines parameters for ListFunctions. -type ListFunctionsParams struct { +// DeleteEmailTemplateParamsType defines parameters for DeleteEmailTemplate. +type DeleteEmailTemplateParamsType string + +// GetEmailTemplateParamsType defines parameters for GetEmailTemplate. +type GetEmailTemplateParamsType string + +// UpdateEmailTemplateParamsType defines parameters for UpdateEmailTemplate. +type UpdateEmailTemplateParamsType string + +// ListFrontendsParams defines parameters for ListFrontends. +type ListFrontendsParams struct { // Page Page number (1-indexed) for offset pagination. Declares no schema // default so the request validator does not inject one: handlers that omit // `page` see it unset (nil) and default to 1 in code, while cursor-first @@ -8404,53 +8677,26 @@ type ListFunctionsParams struct { Search *Search `form:"search,omitempty" json:"search,omitempty"` } -// CreateFunctionMultipartBody defines parameters for CreateFunction. -type CreateFunctionMultipartBody struct { - // Code ZIP or tar.gz archive containing function source code plus dependency manifests/lockfiles. The API enforces SOURCE_ARCHIVE_SIZE_LIMIT_MB and stores a normalized tar.gz source archive. - Code openapi_types.File `json:"code"` - - // Handler The name of the function to invoke. Defaults to "handler" if not specified. - // Your code must export/define a function with this name: - // - Node.js: exports.handler (in index.js) - // - Python: def handler() (in main.py) - // - Ruby: def handler() (in main.rb) - Handler *string `json:"handler,omitempty"` - - // Name DNS-safe function name (lowercase letters, numbers, hyphens; cannot start or end with hyphen) - Name string `json:"name"` +// CreateFrontendMultipartBody defines parameters for CreateFrontend. +type CreateFrontendMultipartBody struct { + // AppRoot Optional relative POSIX path from the uploaded archive root to the Next.js app to build, for example `apps/web`. + AppRoot *string `json:"app_root,omitempty"` - // Runtime Runtime environment. Required. - // - Node.js: nodejs22.x, nodejs24.x - // - Python: python3.10, python3.11, python3.12, python3.13, python3.14 - // - Ruby: ruby3.3, ruby3.4, ruby4.0 - Runtime CreateFunctionMultipartBodyRuntime `json:"runtime"` + // Archive ZIP or tar.gz archive of the frontend project directory or monorepo workspace root. The API enforces SOURCE_ARCHIVE_SIZE_LIMIT_MB and stores a normalized tar.gz archive. + Archive openapi_types.File `json:"archive"` - // VariableScope Which project variables this function receives. `all` (the default) gives it every project variable; `scoped` gives it only the variables it selects. Omitting this leaves an existing function's scope unchanged. - VariableScope *CreateFunctionMultipartBodyVariableScope `json:"variable_scope,omitempty"` + // Framework Next.js frontend. Supported Next.js majors are 15.x and 16.x. + Framework *CreateFrontendMultipartBodyFramework `json:"framework,omitempty"` - // Variables JSON-encoded array of project variable names this function requires, on top of the ones detected in its source. A declared name the project does not define is rejected with 400; a detected name it does not define is ignored. Only used when `variable_scope` is `scoped`. Omitting this leaves an existing function's declared names unchanged. - Variables *string `json:"variables,omitempty"` + // Name DNS-safe frontend name + Name string `json:"name"` } -// CreateFunctionMultipartBodyRuntime defines parameters for CreateFunction. -type CreateFunctionMultipartBodyRuntime string - -// CreateFunctionMultipartBodyVariableScope defines parameters for CreateFunction. -type CreateFunctionMultipartBodyVariableScope string - -// CreateFunctionsBatchMultipartBody defines parameters for CreateFunctionsBatch. -type CreateFunctionsBatchMultipartBody struct { - // Code0 Function ZIP or tar.gz archive referenced by the first manifest entry's `file_field`; additional code_N file fields may be included. Each archive is subject to SOURCE_ARCHIVE_SIZE_LIMIT_MB. - Code0 *openapi_types.File `json:"code_0,omitempty"` - - // Functions JSON array of functions with `name`, `runtime`, optional `handler`, and `file_field`. Each `file_field` must name a multipart file field containing that function's ZIP or tar.gz source bundle. - // - // Each entry may also declare `variable_scope` (`all` or `scoped`) and `variables` (an array of project variable names). Omitting them leaves the function's stored declaration unchanged. Volcano detects direct environment references in the uploaded source and adds the ones the project defines to the declared names; a detected name the project does not define is ignored, since such a reference is often optional. Declare a name when the function reads it through a computed key, or when it must not deploy without the variable. The request is rejected with 400 before anything is deployed if a scoped function declares a variable the project does not define, or if the resulting environment exceeds 4096 bytes. - Functions string `json:"functions"` -} +// CreateFrontendMultipartBodyFramework defines parameters for CreateFrontend. +type CreateFrontendMultipartBodyFramework string -// ListFunctionDeploymentsParams defines parameters for ListFunctionDeployments. -type ListFunctionDeploymentsParams struct { +// ListFrontendDeploymentsParams defines parameters for ListFrontendDeployments. +type ListFrontendDeploymentsParams struct { // Page Page number (1-indexed) for offset pagination. Declares no schema // default so the request validator does not inject one: handlers that omit // `page` see it unset (nil) and default to 1 in code, while cursor-first @@ -8462,50 +8708,151 @@ type ListFunctionDeploymentsParams struct { Limit *Limit `form:"limit,omitempty" json:"limit,omitempty"` } -// UploadProjectLogoMultipartBody defines parameters for UploadProjectLogo. -type UploadProjectLogoMultipartBody struct { - // Logo Logo image (PNG, JPEG, GIF, WebP, or SVG; max 2 MB) - Logo openapi_types.File `json:"logo"` -} - -// StreamProjectLogsParams defines parameters for StreamProjectLogs. -type StreamProjectLogsParams struct { - // LastEventId Opaque stream cursor fallback when setting `Last-Event-ID` is not practical. - LastEventId *string `form:"last_event_id,omitempty" json:"last_event_id,omitempty"` - - // LastEventID Opaque stream cursor from the most recent SSE `id` field. - LastEventID *string `json:"Last-Event-ID,omitempty"` -} - -// DeleteOAuthConfigParams defines parameters for DeleteOAuthConfig. -type DeleteOAuthConfigParams struct { - // ClientId Required when `provider=device` to select a specific device client. - ClientId *string `form:"client_id,omitempty" json:"client_id,omitempty"` -} - -// DeleteOAuthConfigParamsProvider defines parameters for DeleteOAuthConfig. -type DeleteOAuthConfigParamsProvider string - -// GetOAuthConfigParams defines parameters for GetOAuthConfig. -type GetOAuthConfigParams struct { - // ClientId Required when `provider=device` to select a specific device client. - ClientId *string `form:"client_id,omitempty" json:"client_id,omitempty"` -} - -// GetOAuthConfigParamsProvider defines parameters for GetOAuthConfig. -type GetOAuthConfigParamsProvider string - -// UpdateOAuthConfigParams defines parameters for UpdateOAuthConfig. -type UpdateOAuthConfigParams struct { - // ClientId Required when `provider=device` to select a specific device client. - ClientId *string `form:"client_id,omitempty" json:"client_id,omitempty"` +// GetFrontendUsageHistoryParams defines parameters for GetFrontendUsageHistory. +type GetFrontendUsageHistoryParams struct { + // Days Number of trailing days to return (1–90, default 30). + Days *int `form:"days,omitempty" json:"days,omitempty"` } -// UpdateOAuthConfigParamsProvider defines parameters for UpdateOAuthConfig. -type UpdateOAuthConfigParamsProvider string - -// ListProjectSchedulersParams defines parameters for ListProjectSchedulers. -type ListProjectSchedulersParams struct { +// ListFunctionsParams defines parameters for ListFunctions. +type ListFunctionsParams struct { + // Page Page number (1-indexed) for offset pagination. Declares no schema + // default so the request validator does not inject one: handlers that omit + // `page` see it unset (nil) and default to 1 in code, while cursor-first + // endpoints (e.g. the project deployments feed) can detect its absence to + // stay in keyset/search mode. Supplying `page` selects offset pagination. + Page *Page `form:"page,omitempty" json:"page,omitempty"` + + // Limit Number of items per page (max 100) + Limit *Limit `form:"limit,omitempty" json:"limit,omitempty"` + + // Cursor Opaque keyset pagination cursor from a previous response's `next_cursor` + // — pages forward. Mutually exclusive with `page` and `ending_before`; + // combining them returns 400. When supplied, the request's `search` and + // `limit` must match the values bound to the cursor or the request returns 400. + Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"` + + // EndingBefore Opaque keyset pagination cursor from a previous response's `prev_cursor` + // — pages backward (the page immediately preceding this cursor). Mutually + // exclusive with `page` and `cursor`; combining them returns 400. `search` + // and `limit` must match the values bound to the cursor or the request + // returns 400. + EndingBefore *EndingBefore `form:"ending_before,omitempty" json:"ending_before,omitempty"` + + // Offset Bounded row offset past the keyset anchor named by `cursor` (forward) or + // `ending_before` (backward) — the hybrid jump. Seek to the anchor, then + // skip this many rows within. Used for numbered jump-to-page: from the + // current page, seek to its next/prev cursor and offset the remaining + // pages. Only honored on the cursor pagination path; ignored otherwise. + Offset *Offset `form:"offset,omitempty" json:"offset,omitempty"` + + // Search Case-insensitive substring match on the resource `name`. See the + // endpoint description for supported pagination modes. + Search *Search `form:"search,omitempty" json:"search,omitempty"` +} + +// CreateFunctionMultipartBody defines parameters for CreateFunction. +type CreateFunctionMultipartBody struct { + // Code ZIP or tar.gz archive containing function source code plus dependency manifests/lockfiles. The API enforces SOURCE_ARCHIVE_SIZE_LIMIT_MB and stores a normalized tar.gz source archive. + Code openapi_types.File `json:"code"` + + // Handler The name of the function to invoke. Defaults to "handler" if not specified. + // Your code must export/define a function with this name: + // - Node.js: exports.handler (in index.js) + // - Python: def handler() (in main.py) + // - Ruby: def handler() (in main.rb) + Handler *string `json:"handler,omitempty"` + + // Name DNS-safe function name (lowercase letters, numbers, hyphens; cannot start or end with hyphen) + Name string `json:"name"` + + // Runtime Runtime environment. Required. + // - Node.js: nodejs22.x, nodejs24.x + // - Python: python3.10, python3.11, python3.12, python3.13, python3.14 + // - Ruby: ruby3.3, ruby3.4, ruby4.0 + Runtime CreateFunctionMultipartBodyRuntime `json:"runtime"` + + // VariableScope Which project variables this function receives. `all` (the default) gives it every project variable; `scoped` gives it only the variables it selects. Omitting this leaves an existing function's scope unchanged. + VariableScope *CreateFunctionMultipartBodyVariableScope `json:"variable_scope,omitempty"` + + // Variables JSON-encoded array of project variable names this function requires, on top of the ones detected in its source. A declared name the project does not define is rejected with 400; a detected name it does not define is ignored. Only used when `variable_scope` is `scoped`. Omitting this leaves an existing function's declared names unchanged. + Variables *string `json:"variables,omitempty"` +} + +// CreateFunctionMultipartBodyRuntime defines parameters for CreateFunction. +type CreateFunctionMultipartBodyRuntime string + +// CreateFunctionMultipartBodyVariableScope defines parameters for CreateFunction. +type CreateFunctionMultipartBodyVariableScope string + +// CreateFunctionsBatchMultipartBody defines parameters for CreateFunctionsBatch. +type CreateFunctionsBatchMultipartBody struct { + // Code0 Function ZIP or tar.gz archive referenced by the first manifest entry's `file_field`; additional code_N file fields may be included. Each archive is subject to SOURCE_ARCHIVE_SIZE_LIMIT_MB. + Code0 *openapi_types.File `json:"code_0,omitempty"` + + // Functions JSON array of functions with `name`, `runtime`, optional `handler`, and `file_field`. Each `file_field` must name a multipart file field containing that function's ZIP or tar.gz source bundle. + // + // Each entry may also declare `variable_scope` (`all` or `scoped`) and `variables` (an array of project variable names). Omitting them leaves the function's stored declaration unchanged. Volcano detects direct environment references in the uploaded source and adds the ones the project defines to the declared names; a detected name the project does not define is ignored, since such a reference is often optional. Declare a name when the function reads it through a computed key, or when it must not deploy without the variable. The request is rejected with 400 before anything is deployed if a scoped function declares a variable the project does not define, or if the resulting environment exceeds 4096 bytes. + Functions string `json:"functions"` +} + +// ListFunctionDeploymentsParams defines parameters for ListFunctionDeployments. +type ListFunctionDeploymentsParams struct { + // Page Page number (1-indexed) for offset pagination. Declares no schema + // default so the request validator does not inject one: handlers that omit + // `page` see it unset (nil) and default to 1 in code, while cursor-first + // endpoints (e.g. the project deployments feed) can detect its absence to + // stay in keyset/search mode. Supplying `page` selects offset pagination. + Page *Page `form:"page,omitempty" json:"page,omitempty"` + + // Limit Number of items per page (max 100) + Limit *Limit `form:"limit,omitempty" json:"limit,omitempty"` +} + +// UploadProjectLogoMultipartBody defines parameters for UploadProjectLogo. +type UploadProjectLogoMultipartBody struct { + // Logo Logo image (PNG, JPEG, GIF, WebP, or SVG; max 2 MB) + Logo openapi_types.File `json:"logo"` +} + +// StreamProjectLogsParams defines parameters for StreamProjectLogs. +type StreamProjectLogsParams struct { + // LastEventId Opaque stream cursor fallback when setting `Last-Event-ID` is not practical. + LastEventId *string `form:"last_event_id,omitempty" json:"last_event_id,omitempty"` + + // LastEventID Opaque stream cursor from the most recent SSE `id` field. + LastEventID *string `json:"Last-Event-ID,omitempty"` +} + +// DeleteOAuthConfigParams defines parameters for DeleteOAuthConfig. +type DeleteOAuthConfigParams struct { + // ClientId Required when `provider=device` to select a specific device client. + ClientId *string `form:"client_id,omitempty" json:"client_id,omitempty"` +} + +// DeleteOAuthConfigParamsProvider defines parameters for DeleteOAuthConfig. +type DeleteOAuthConfigParamsProvider string + +// GetOAuthConfigParams defines parameters for GetOAuthConfig. +type GetOAuthConfigParams struct { + // ClientId Required when `provider=device` to select a specific device client. + ClientId *string `form:"client_id,omitempty" json:"client_id,omitempty"` +} + +// GetOAuthConfigParamsProvider defines parameters for GetOAuthConfig. +type GetOAuthConfigParamsProvider string + +// UpdateOAuthConfigParams defines parameters for UpdateOAuthConfig. +type UpdateOAuthConfigParams struct { + // ClientId Required when `provider=device` to select a specific device client. + ClientId *string `form:"client_id,omitempty" json:"client_id,omitempty"` +} + +// UpdateOAuthConfigParamsProvider defines parameters for UpdateOAuthConfig. +type UpdateOAuthConfigParamsProvider string + +// ListProjectSchedulersParams defines parameters for ListProjectSchedulers. +type ListProjectSchedulersParams struct { // Page Page number (1-indexed) for offset pagination. Declares no schema // default so the request validator does not inject one: handlers that omit // `page` see it unset (nil) and default to 1 in code, while cursor-first @@ -8933,6 +9280,18 @@ type CreateDatabaseRestoreJSONRequestBody = CreateDatabaseRestoreRequest // UpdateDatabaseTypeJSONRequestBody defines body for UpdateDatabaseType for application/json ContentType. type UpdateDatabaseTypeJSONRequestBody = UpdateDatabaseTypeRequest +// CreateDurableFunctionMultipartRequestBody defines body for CreateDurableFunction for multipart/form-data ContentType. +type CreateDurableFunctionMultipartRequestBody CreateDurableFunctionMultipartBody + +// StartDurableExecutionJSONRequestBody defines body for StartDurableExecution for application/json ContentType. +type StartDurableExecutionJSONRequestBody = StartDurableExecutionJSONBody + +// CreateDurableFunctionSchedulerJSONRequestBody defines body for CreateDurableFunctionScheduler for application/json ContentType. +type CreateDurableFunctionSchedulerJSONRequestBody = CreateFunctionSchedulerRequest + +// UpdateDurableFunctionSchedulerJSONRequestBody defines body for UpdateDurableFunctionScheduler for application/json ContentType. +type UpdateDurableFunctionSchedulerJSONRequestBody = UpdateFunctionSchedulerRequest + // CreateEmailTemplateJSONRequestBody defines body for CreateEmailTemplate for application/json ContentType. type CreateEmailTemplateJSONRequestBody = CreateEmailTemplateRequest @@ -10306,6 +10665,51 @@ type ClientInterface interface { // ListProjectCustomDomains request ListProjectCustomDomains(ctx context.Context, id ProjectId, params *ListProjectCustomDomainsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // ListDurableFunctions request + ListDurableFunctions(ctx context.Context, id ProjectId, params *ListDurableFunctionsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // CreateDurableFunctionWithBody request with any body + CreateDurableFunctionWithBody(ctx context.Context, id ProjectId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteDurableFunction request + DeleteDurableFunction(ctx context.Context, id ProjectId, functionId DurableFunctionId, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetDurableFunction request + GetDurableFunction(ctx context.Context, id ProjectId, functionId DurableFunctionId, reqEditors ...RequestEditorFn) (*http.Response, error) + + // ListDurableExecutions request + ListDurableExecutions(ctx context.Context, id ProjectId, functionId DurableFunctionId, params *ListDurableExecutionsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // StartDurableExecutionWithBody request with any body + StartDurableExecutionWithBody(ctx context.Context, id ProjectId, functionId DurableFunctionId, params *StartDurableExecutionParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + StartDurableExecution(ctx context.Context, id ProjectId, functionId DurableFunctionId, params *StartDurableExecutionParams, body StartDurableExecutionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetDurableExecution request + GetDurableExecution(ctx context.Context, id ProjectId, functionId DurableFunctionId, executionId DurableExecutionId, reqEditors ...RequestEditorFn) (*http.Response, error) + + // StopDurableExecution request + StopDurableExecution(ctx context.Context, id ProjectId, functionId DurableFunctionId, executionId DurableExecutionId, reqEditors ...RequestEditorFn) (*http.Response, error) + + // ListDurableFunctionSchedulers request + ListDurableFunctionSchedulers(ctx context.Context, id ProjectId, functionId DurableFunctionId, reqEditors ...RequestEditorFn) (*http.Response, error) + + // CreateDurableFunctionSchedulerWithBody request with any body + CreateDurableFunctionSchedulerWithBody(ctx context.Context, id ProjectId, functionId DurableFunctionId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + CreateDurableFunctionScheduler(ctx context.Context, id ProjectId, functionId DurableFunctionId, body CreateDurableFunctionSchedulerJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteDurableFunctionScheduler request + DeleteDurableFunctionScheduler(ctx context.Context, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetDurableFunctionScheduler request + GetDurableFunctionScheduler(ctx context.Context, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, reqEditors ...RequestEditorFn) (*http.Response, error) + + // UpdateDurableFunctionSchedulerWithBody request with any body + UpdateDurableFunctionSchedulerWithBody(ctx context.Context, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + UpdateDurableFunctionScheduler(ctx context.Context, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, body UpdateDurableFunctionSchedulerJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // ListEmailTemplates request ListEmailTemplates(ctx context.Context, id ProjectId, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -12743,6 +13147,198 @@ func (c *Client) ListProjectCustomDomains(ctx context.Context, id ProjectId, par return c.Client.Do(req) } +func (c *Client) ListDurableFunctions(ctx context.Context, id ProjectId, params *ListDurableFunctionsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewListDurableFunctionsRequest(c.Server, id, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreateDurableFunctionWithBody(ctx context.Context, id ProjectId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateDurableFunctionRequestWithBody(c.Server, id, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DeleteDurableFunction(ctx context.Context, id ProjectId, functionId DurableFunctionId, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteDurableFunctionRequest(c.Server, id, functionId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetDurableFunction(ctx context.Context, id ProjectId, functionId DurableFunctionId, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetDurableFunctionRequest(c.Server, id, functionId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ListDurableExecutions(ctx context.Context, id ProjectId, functionId DurableFunctionId, params *ListDurableExecutionsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewListDurableExecutionsRequest(c.Server, id, functionId, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) StartDurableExecutionWithBody(ctx context.Context, id ProjectId, functionId DurableFunctionId, params *StartDurableExecutionParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewStartDurableExecutionRequestWithBody(c.Server, id, functionId, params, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) StartDurableExecution(ctx context.Context, id ProjectId, functionId DurableFunctionId, params *StartDurableExecutionParams, body StartDurableExecutionJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewStartDurableExecutionRequest(c.Server, id, functionId, params, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetDurableExecution(ctx context.Context, id ProjectId, functionId DurableFunctionId, executionId DurableExecutionId, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetDurableExecutionRequest(c.Server, id, functionId, executionId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) StopDurableExecution(ctx context.Context, id ProjectId, functionId DurableFunctionId, executionId DurableExecutionId, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewStopDurableExecutionRequest(c.Server, id, functionId, executionId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ListDurableFunctionSchedulers(ctx context.Context, id ProjectId, functionId DurableFunctionId, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewListDurableFunctionSchedulersRequest(c.Server, id, functionId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreateDurableFunctionSchedulerWithBody(ctx context.Context, id ProjectId, functionId DurableFunctionId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateDurableFunctionSchedulerRequestWithBody(c.Server, id, functionId, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreateDurableFunctionScheduler(ctx context.Context, id ProjectId, functionId DurableFunctionId, body CreateDurableFunctionSchedulerJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateDurableFunctionSchedulerRequest(c.Server, id, functionId, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) DeleteDurableFunctionScheduler(ctx context.Context, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteDurableFunctionSchedulerRequest(c.Server, id, functionId, schedulerId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetDurableFunctionScheduler(ctx context.Context, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetDurableFunctionSchedulerRequest(c.Server, id, functionId, schedulerId) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) UpdateDurableFunctionSchedulerWithBody(ctx context.Context, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpdateDurableFunctionSchedulerRequestWithBody(c.Server, id, functionId, schedulerId, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) UpdateDurableFunctionScheduler(ctx context.Context, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, body UpdateDurableFunctionSchedulerJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewUpdateDurableFunctionSchedulerRequest(c.Server, id, functionId, schedulerId, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) ListEmailTemplates(ctx context.Context, id ProjectId, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewListEmailTemplatesRequest(c.Server, id) if err != nil { @@ -20890,8 +21486,8 @@ func NewListProjectCustomDomainsRequest(server string, id ProjectId, params *Lis return req, nil } -// NewListEmailTemplatesRequest generates requests for ListEmailTemplates -func NewListEmailTemplatesRequest(server string, id ProjectId) (*http.Request, error) { +// NewListDurableFunctionsRequest generates requests for ListDurableFunctions +func NewListDurableFunctionsRequest(server string, id ProjectId, params *ListDurableFunctionsParams) (*http.Request, error) { var err error var pathParam0 string @@ -20906,7 +21502,7 @@ func NewListEmailTemplatesRequest(server string, id ProjectId) (*http.Request, e return nil, err } - operationPath := fmt.Sprintf("/projects/%s/email-templates", pathParam0) + operationPath := fmt.Sprintf("/projects/%s/durable-functions", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -20916,6 +21512,57 @@ func NewListEmailTemplatesRequest(server string, id ProjectId) (*http.Request, e return nil, err } + if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). + queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string + + if params.Page != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "page", *params.Page, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + } + + if params.Limit != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "limit", *params.Limit, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + } + + if params.Search != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "search", *params.Search, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + } + + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") + } + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err @@ -20924,19 +21571,8 @@ func NewListEmailTemplatesRequest(server string, id ProjectId) (*http.Request, e return req, nil } -// NewCreateEmailTemplateRequest calls the generic CreateEmailTemplate builder with application/json body -func NewCreateEmailTemplateRequest(server string, id ProjectId, body CreateEmailTemplateJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewCreateEmailTemplateRequestWithBody(server, id, "application/json", bodyReader) -} - -// NewCreateEmailTemplateRequestWithBody generates requests for CreateEmailTemplate with any type of body -func NewCreateEmailTemplateRequestWithBody(server string, id ProjectId, contentType string, body io.Reader) (*http.Request, error) { +// NewCreateDurableFunctionRequestWithBody generates requests for CreateDurableFunction with any type of body +func NewCreateDurableFunctionRequestWithBody(server string, id ProjectId, contentType string, body io.Reader) (*http.Request, error) { var err error var pathParam0 string @@ -20951,7 +21587,7 @@ func NewCreateEmailTemplateRequestWithBody(server string, id ProjectId, contentT return nil, err } - operationPath := fmt.Sprintf("/projects/%s/email-templates", pathParam0) + operationPath := fmt.Sprintf("/projects/%s/durable-functions", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -20971,8 +21607,8 @@ func NewCreateEmailTemplateRequestWithBody(server string, id ProjectId, contentT return req, nil } -// NewDeleteEmailTemplateRequest generates requests for DeleteEmailTemplate -func NewDeleteEmailTemplateRequest(server string, id ProjectId, pType DeleteEmailTemplateParamsType) (*http.Request, error) { +// NewDeleteDurableFunctionRequest generates requests for DeleteDurableFunction +func NewDeleteDurableFunctionRequest(server string, id ProjectId, functionId DurableFunctionId) (*http.Request, error) { var err error var pathParam0 string @@ -20984,7 +21620,7 @@ func NewDeleteEmailTemplateRequest(server string, id ProjectId, pType DeleteEmai var pathParam1 string - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "type", pType, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "functionId", functionId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -20994,7 +21630,7 @@ func NewDeleteEmailTemplateRequest(server string, id ProjectId, pType DeleteEmai return nil, err } - operationPath := fmt.Sprintf("/projects/%s/email-templates/%s", pathParam0, pathParam1) + operationPath := fmt.Sprintf("/projects/%s/durable-functions/%s", pathParam0, pathParam1) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -21012,8 +21648,8 @@ func NewDeleteEmailTemplateRequest(server string, id ProjectId, pType DeleteEmai return req, nil } -// NewGetEmailTemplateRequest generates requests for GetEmailTemplate -func NewGetEmailTemplateRequest(server string, id ProjectId, pType GetEmailTemplateParamsType) (*http.Request, error) { +// NewGetDurableFunctionRequest generates requests for GetDurableFunction +func NewGetDurableFunctionRequest(server string, id ProjectId, functionId DurableFunctionId) (*http.Request, error) { var err error var pathParam0 string @@ -21025,7 +21661,7 @@ func NewGetEmailTemplateRequest(server string, id ProjectId, pType GetEmailTempl var pathParam1 string - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "type", pType, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "functionId", functionId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -21035,7 +21671,7 @@ func NewGetEmailTemplateRequest(server string, id ProjectId, pType GetEmailTempl return nil, err } - operationPath := fmt.Sprintf("/projects/%s/email-templates/%s", pathParam0, pathParam1) + operationPath := fmt.Sprintf("/projects/%s/durable-functions/%s", pathParam0, pathParam1) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -21053,19 +21689,8 @@ func NewGetEmailTemplateRequest(server string, id ProjectId, pType GetEmailTempl return req, nil } -// NewUpdateEmailTemplateRequest calls the generic UpdateEmailTemplate builder with application/json body -func NewUpdateEmailTemplateRequest(server string, id ProjectId, pType UpdateEmailTemplateParamsType, body UpdateEmailTemplateJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewUpdateEmailTemplateRequestWithBody(server, id, pType, "application/json", bodyReader) -} - -// NewUpdateEmailTemplateRequestWithBody generates requests for UpdateEmailTemplate with any type of body -func NewUpdateEmailTemplateRequestWithBody(server string, id ProjectId, pType UpdateEmailTemplateParamsType, contentType string, body io.Reader) (*http.Request, error) { +// NewListDurableExecutionsRequest generates requests for ListDurableExecutions +func NewListDurableExecutionsRequest(server string, id ProjectId, functionId DurableFunctionId, params *ListDurableExecutionsParams) (*http.Request, error) { var err error var pathParam0 string @@ -21077,43 +21702,7 @@ func NewUpdateEmailTemplateRequestWithBody(server string, id ProjectId, pType Up var pathParam1 string - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "type", pType, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) - if err != nil { - return nil, err - } - - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/projects/%s/email-templates/%s", pathParam0, pathParam1) - if operationPath[0] == '/' { - operationPath = "." + operationPath - } - - queryURL, err := serverURL.Parse(operationPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) - if err != nil { - return nil, err - } - - req.Header.Add("Content-Type", contentType) - - return req, nil -} - -// NewListFrontendsRequest generates requests for ListFrontends -func NewListFrontendsRequest(server string, id ProjectId, params *ListFrontendsParams) (*http.Request, error) { - var err error - - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "functionId", functionId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -21123,7 +21712,7 @@ func NewListFrontendsRequest(server string, id ProjectId, params *ListFrontendsP return nil, err } - operationPath := fmt.Sprintf("/projects/%s/frontends", pathParam0) + operationPath := fmt.Sprintf("/projects/%s/durable-functions/%s/executions", pathParam0, pathParam1) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -21166,45 +21755,9 @@ func NewListFrontendsRequest(server string, id ProjectId, params *ListFrontendsP } - if params.Cursor != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "cursor", *params.Cursor, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.EndingBefore != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "ending_before", *params.EndingBefore, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.Offset != nil { - - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "offset", *params.Offset, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } - - } - - if params.Search != nil { + if params.Status != nil { - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "search", *params.Search, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "status", *params.Status, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { return nil, err } else { for _, qp := range strings.Split(queryFrag, "&") { @@ -21228,8 +21781,19 @@ func NewListFrontendsRequest(server string, id ProjectId, params *ListFrontendsP return req, nil } -// NewCreateFrontendRequestWithBody generates requests for CreateFrontend with any type of body -func NewCreateFrontendRequestWithBody(server string, id ProjectId, contentType string, body io.Reader) (*http.Request, error) { +// NewStartDurableExecutionRequest calls the generic StartDurableExecution builder with application/json body +func NewStartDurableExecutionRequest(server string, id ProjectId, functionId DurableFunctionId, params *StartDurableExecutionParams, body StartDurableExecutionJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewStartDurableExecutionRequestWithBody(server, id, functionId, params, "application/json", bodyReader) +} + +// NewStartDurableExecutionRequestWithBody generates requests for StartDurableExecution with any type of body +func NewStartDurableExecutionRequestWithBody(server string, id ProjectId, functionId DurableFunctionId, params *StartDurableExecutionParams, contentType string, body io.Reader) (*http.Request, error) { var err error var pathParam0 string @@ -21239,12 +21803,19 @@ func NewCreateFrontendRequestWithBody(server string, id ProjectId, contentType s return nil, err } + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "functionId", functionId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + serverURL, err := url.Parse(server) if err != nil { return nil, err } - operationPath := fmt.Sprintf("/projects/%s/frontends", pathParam0) + operationPath := fmt.Sprintf("/projects/%s/durable-functions/%s/executions", pathParam0, pathParam1) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -21261,11 +21832,26 @@ func NewCreateFrontendRequestWithBody(server string, id ProjectId, contentType s req.Header.Add("Content-Type", contentType) + if params != nil { + + if params.XVolcanoExecutionName != nil { + var headerParam0 string + + headerParam0, err = runtime.StyleParamWithOptions("simple", false, "X-Volcano-Execution-Name", *params.XVolcanoExecutionName, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationHeader, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + req.Header.Set("X-Volcano-Execution-Name", headerParam0) + } + + } + return req, nil } -// NewDeleteFrontendRequest generates requests for DeleteFrontend -func NewDeleteFrontendRequest(server string, id ProjectId, frontendId FrontendId) (*http.Request, error) { +// NewGetDurableExecutionRequest generates requests for GetDurableExecution +func NewGetDurableExecutionRequest(server string, id ProjectId, functionId DurableFunctionId, executionId DurableExecutionId) (*http.Request, error) { var err error var pathParam0 string @@ -21277,7 +21863,14 @@ func NewDeleteFrontendRequest(server string, id ProjectId, frontendId FrontendId var pathParam1 string - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "frontendId", frontendId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "functionId", functionId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam2 string + + pathParam2, err = runtime.StyleParamWithOptions("simple", false, "executionId", executionId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -21287,7 +21880,7 @@ func NewDeleteFrontendRequest(server string, id ProjectId, frontendId FrontendId return nil, err } - operationPath := fmt.Sprintf("/projects/%s/frontends/%s", pathParam0, pathParam1) + operationPath := fmt.Sprintf("/projects/%s/durable-functions/%s/executions/%s", pathParam0, pathParam1, pathParam2) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -21297,7 +21890,7 @@ func NewDeleteFrontendRequest(server string, id ProjectId, frontendId FrontendId return nil, err } - req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) if err != nil { return nil, err } @@ -21305,8 +21898,8 @@ func NewDeleteFrontendRequest(server string, id ProjectId, frontendId FrontendId return req, nil } -// NewGetFrontendRequest generates requests for GetFrontend -func NewGetFrontendRequest(server string, id ProjectId, frontendId FrontendId) (*http.Request, error) { +// NewStopDurableExecutionRequest generates requests for StopDurableExecution +func NewStopDurableExecutionRequest(server string, id ProjectId, functionId DurableFunctionId, executionId DurableExecutionId) (*http.Request, error) { var err error var pathParam0 string @@ -21318,7 +21911,14 @@ func NewGetFrontendRequest(server string, id ProjectId, frontendId FrontendId) ( var pathParam1 string - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "frontendId", frontendId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "functionId", functionId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam2 string + + pathParam2, err = runtime.StyleParamWithOptions("simple", false, "executionId", executionId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -21328,7 +21928,7 @@ func NewGetFrontendRequest(server string, id ProjectId, frontendId FrontendId) ( return nil, err } - operationPath := fmt.Sprintf("/projects/%s/frontends/%s", pathParam0, pathParam1) + operationPath := fmt.Sprintf("/projects/%s/durable-functions/%s/executions/%s/stop", pathParam0, pathParam1, pathParam2) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -21338,7 +21938,7 @@ func NewGetFrontendRequest(server string, id ProjectId, frontendId FrontendId) ( return nil, err } - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) if err != nil { return nil, err } @@ -21346,8 +21946,8 @@ func NewGetFrontendRequest(server string, id ProjectId, frontendId FrontendId) ( return req, nil } -// NewListFrontendDeploymentsRequest generates requests for ListFrontendDeployments -func NewListFrontendDeploymentsRequest(server string, id ProjectId, frontendId FrontendId, params *ListFrontendDeploymentsParams) (*http.Request, error) { +// NewListDurableFunctionSchedulersRequest generates requests for ListDurableFunctionSchedulers +func NewListDurableFunctionSchedulersRequest(server string, id ProjectId, functionId DurableFunctionId) (*http.Request, error) { var err error var pathParam0 string @@ -21359,7 +21959,7 @@ func NewListFrontendDeploymentsRequest(server string, id ProjectId, frontendId F var pathParam1 string - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "frontendId", frontendId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "functionId", functionId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -21369,7 +21969,7 @@ func NewListFrontendDeploymentsRequest(server string, id ProjectId, frontendId F return nil, err } - operationPath := fmt.Sprintf("/projects/%s/frontends/%s/deployments", pathParam0, pathParam1) + operationPath := fmt.Sprintf("/projects/%s/durable-functions/%s/schedulers", pathParam0, pathParam1) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -21379,55 +21979,70 @@ func NewListFrontendDeploymentsRequest(server string, id ProjectId, frontendId F return nil, err } - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } - if params.Page != nil { + return req, nil +} - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "page", *params.Page, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } +// NewCreateDurableFunctionSchedulerRequest calls the generic CreateDurableFunctionScheduler builder with application/json body +func NewCreateDurableFunctionSchedulerRequest(server string, id ProjectId, functionId DurableFunctionId, body CreateDurableFunctionSchedulerJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewCreateDurableFunctionSchedulerRequestWithBody(server, id, functionId, "application/json", bodyReader) +} - } +// NewCreateDurableFunctionSchedulerRequestWithBody generates requests for CreateDurableFunctionScheduler with any type of body +func NewCreateDurableFunctionSchedulerRequestWithBody(server string, id ProjectId, functionId DurableFunctionId, contentType string, body io.Reader) (*http.Request, error) { + var err error - if params.Limit != nil { + var pathParam0 string - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "limit", *params.Limit, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } - } + var pathParam1 string - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "functionId", functionId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err } - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/projects/%s/durable-functions/%s/schedulers", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) if err != nil { return nil, err } + req.Header.Add("Content-Type", contentType) + return req, nil } -// NewDeleteFrontendCustomDomainRequest generates requests for DeleteFrontendCustomDomain -func NewDeleteFrontendCustomDomainRequest(server string, id ProjectId, frontendId FrontendId) (*http.Request, error) { +// NewDeleteDurableFunctionSchedulerRequest generates requests for DeleteDurableFunctionScheduler +func NewDeleteDurableFunctionSchedulerRequest(server string, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId) (*http.Request, error) { var err error var pathParam0 string @@ -21439,7 +22054,14 @@ func NewDeleteFrontendCustomDomainRequest(server string, id ProjectId, frontendI var pathParam1 string - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "frontendId", frontendId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "functionId", functionId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam2 string + + pathParam2, err = runtime.StyleParamWithOptions("simple", false, "schedulerId", schedulerId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -21449,7 +22071,7 @@ func NewDeleteFrontendCustomDomainRequest(server string, id ProjectId, frontendI return nil, err } - operationPath := fmt.Sprintf("/projects/%s/frontends/%s/domain", pathParam0, pathParam1) + operationPath := fmt.Sprintf("/projects/%s/durable-functions/%s/schedulers/%s", pathParam0, pathParam1, pathParam2) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -21467,8 +22089,8 @@ func NewDeleteFrontendCustomDomainRequest(server string, id ProjectId, frontendI return req, nil } -// NewGetFrontendCustomDomainRequest generates requests for GetFrontendCustomDomain -func NewGetFrontendCustomDomainRequest(server string, id ProjectId, frontendId FrontendId) (*http.Request, error) { +// NewGetDurableFunctionSchedulerRequest generates requests for GetDurableFunctionScheduler +func NewGetDurableFunctionSchedulerRequest(server string, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId) (*http.Request, error) { var err error var pathParam0 string @@ -21480,7 +22102,14 @@ func NewGetFrontendCustomDomainRequest(server string, id ProjectId, frontendId F var pathParam1 string - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "frontendId", frontendId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "functionId", functionId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam2 string + + pathParam2, err = runtime.StyleParamWithOptions("simple", false, "schedulerId", schedulerId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -21490,7 +22119,7 @@ func NewGetFrontendCustomDomainRequest(server string, id ProjectId, frontendId F return nil, err } - operationPath := fmt.Sprintf("/projects/%s/frontends/%s/domain", pathParam0, pathParam1) + operationPath := fmt.Sprintf("/projects/%s/durable-functions/%s/schedulers/%s", pathParam0, pathParam1, pathParam2) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -21508,19 +22137,19 @@ func NewGetFrontendCustomDomainRequest(server string, id ProjectId, frontendId F return req, nil } -// NewCreateFrontendCustomDomainRequest calls the generic CreateFrontendCustomDomain builder with application/json body -func NewCreateFrontendCustomDomainRequest(server string, id ProjectId, frontendId FrontendId, body CreateFrontendCustomDomainJSONRequestBody) (*http.Request, error) { +// NewUpdateDurableFunctionSchedulerRequest calls the generic UpdateDurableFunctionScheduler builder with application/json body +func NewUpdateDurableFunctionSchedulerRequest(server string, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, body UpdateDurableFunctionSchedulerJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader buf, err := json.Marshal(body) if err != nil { return nil, err } bodyReader = bytes.NewReader(buf) - return NewCreateFrontendCustomDomainRequestWithBody(server, id, frontendId, "application/json", bodyReader) + return NewUpdateDurableFunctionSchedulerRequestWithBody(server, id, functionId, schedulerId, "application/json", bodyReader) } -// NewCreateFrontendCustomDomainRequestWithBody generates requests for CreateFrontendCustomDomain with any type of body -func NewCreateFrontendCustomDomainRequestWithBody(server string, id ProjectId, frontendId FrontendId, contentType string, body io.Reader) (*http.Request, error) { +// NewUpdateDurableFunctionSchedulerRequestWithBody generates requests for UpdateDurableFunctionScheduler with any type of body +func NewUpdateDurableFunctionSchedulerRequestWithBody(server string, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, contentType string, body io.Reader) (*http.Request, error) { var err error var pathParam0 string @@ -21532,7 +22161,14 @@ func NewCreateFrontendCustomDomainRequestWithBody(server string, id ProjectId, f var pathParam1 string - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "frontendId", frontendId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "functionId", functionId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + var pathParam2 string + + pathParam2, err = runtime.StyleParamWithOptions("simple", false, "schedulerId", schedulerId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) if err != nil { return nil, err } @@ -21542,7 +22178,88 @@ func NewCreateFrontendCustomDomainRequestWithBody(server string, id ProjectId, f return nil, err } - operationPath := fmt.Sprintf("/projects/%s/frontends/%s/domain", pathParam0, pathParam1) + operationPath := fmt.Sprintf("/projects/%s/durable-functions/%s/schedulers/%s", pathParam0, pathParam1, pathParam2) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPatch, queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewListEmailTemplatesRequest generates requests for ListEmailTemplates +func NewListEmailTemplatesRequest(server string, id ProjectId) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/projects/%s/email-templates", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewCreateEmailTemplateRequest calls the generic CreateEmailTemplate builder with application/json body +func NewCreateEmailTemplateRequest(server string, id ProjectId, body CreateEmailTemplateJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewCreateEmailTemplateRequestWithBody(server, id, "application/json", bodyReader) +} + +// NewCreateEmailTemplateRequestWithBody generates requests for CreateEmailTemplate with any type of body +func NewCreateEmailTemplateRequestWithBody(server string, id ProjectId, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/projects/%s/email-templates", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -21562,8 +22279,8 @@ func NewCreateFrontendCustomDomainRequestWithBody(server string, id ProjectId, f return req, nil } -// NewRedeployFrontendRequest generates requests for RedeployFrontend -func NewRedeployFrontendRequest(server string, id ProjectId, frontendId FrontendId) (*http.Request, error) { +// NewDeleteEmailTemplateRequest generates requests for DeleteEmailTemplate +func NewDeleteEmailTemplateRequest(server string, id ProjectId, pType DeleteEmailTemplateParamsType) (*http.Request, error) { var err error var pathParam0 string @@ -21575,7 +22292,7 @@ func NewRedeployFrontendRequest(server string, id ProjectId, frontendId Frontend var pathParam1 string - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "frontendId", frontendId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "type", pType, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -21585,7 +22302,7 @@ func NewRedeployFrontendRequest(server string, id ProjectId, frontendId Frontend return nil, err } - operationPath := fmt.Sprintf("/projects/%s/frontends/%s/redeploy", pathParam0, pathParam1) + operationPath := fmt.Sprintf("/projects/%s/email-templates/%s", pathParam0, pathParam1) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -21595,7 +22312,7 @@ func NewRedeployFrontendRequest(server string, id ProjectId, frontendId Frontend return nil, err } - req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) if err != nil { return nil, err } @@ -21603,8 +22320,8 @@ func NewRedeployFrontendRequest(server string, id ProjectId, frontendId Frontend return req, nil } -// NewGetFrontendUsageHistoryRequest generates requests for GetFrontendUsageHistory -func NewGetFrontendUsageHistoryRequest(server string, id ProjectId, frontendId FrontendId, params *GetFrontendUsageHistoryParams) (*http.Request, error) { +// NewGetEmailTemplateRequest generates requests for GetEmailTemplate +func NewGetEmailTemplateRequest(server string, id ProjectId, pType GetEmailTemplateParamsType) (*http.Request, error) { var err error var pathParam0 string @@ -21616,7 +22333,7 @@ func NewGetFrontendUsageHistoryRequest(server string, id ProjectId, frontendId F var pathParam1 string - pathParam1, err = runtime.StyleParamWithOptions("simple", false, "frontendId", frontendId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "type", pType, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -21626,7 +22343,7 @@ func NewGetFrontendUsageHistoryRequest(server string, id ProjectId, frontendId F return nil, err } - operationPath := fmt.Sprintf("/projects/%s/frontends/%s/usage", pathParam0, pathParam1) + operationPath := fmt.Sprintf("/projects/%s/email-templates/%s", pathParam0, pathParam1) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -21636,43 +22353,70 @@ func NewGetFrontendUsageHistoryRequest(server string, id ProjectId, frontendId F return nil, err } - if params != nil { - // queryValues collects non-styled parameters (passthrough, JSON) - // that are safe to round-trip through url.Values.Encode(). - queryValues := queryURL.Query() - // rawQueryFragments collects pre-encoded query fragments from - // styled parameters, preserving literal commas as delimiters - // per the OpenAPI spec (e.g. "color=blue,black,brown"). - var rawQueryFragments []string + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } - if params.Days != nil { + return req, nil +} - if queryFrag, err := runtime.StyleParamWithOptions("form", true, "days", *params.Days, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { - return nil, err - } else { - for _, qp := range strings.Split(queryFrag, "&") { - rawQueryFragments = append(rawQueryFragments, qp) - } - } +// NewUpdateEmailTemplateRequest calls the generic UpdateEmailTemplate builder with application/json body +func NewUpdateEmailTemplateRequest(server string, id ProjectId, pType UpdateEmailTemplateParamsType, body UpdateEmailTemplateJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewUpdateEmailTemplateRequestWithBody(server, id, pType, "application/json", bodyReader) +} - } +// NewUpdateEmailTemplateRequestWithBody generates requests for UpdateEmailTemplate with any type of body +func NewUpdateEmailTemplateRequestWithBody(server string, id ProjectId, pType UpdateEmailTemplateParamsType, contentType string, body io.Reader) (*http.Request, error) { + var err error - if encoded := queryValues.Encode(); encoded != "" { - rawQueryFragments = append(rawQueryFragments, encoded) - } - queryURL.RawQuery = strings.Join(rawQueryFragments, "&") + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err } - req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "type", pType, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/projects/%s/email-templates/%s", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPut, queryURL.String(), body) if err != nil { return nil, err } + req.Header.Add("Content-Type", contentType) + return req, nil } -// NewListFunctionsRequest generates requests for ListFunctions -func NewListFunctionsRequest(server string, id ProjectId, params *ListFunctionsParams) (*http.Request, error) { +// NewListFrontendsRequest generates requests for ListFrontends +func NewListFrontendsRequest(server string, id ProjectId, params *ListFrontendsParams) (*http.Request, error) { var err error var pathParam0 string @@ -21687,7 +22431,7 @@ func NewListFunctionsRequest(server string, id ProjectId, params *ListFunctionsP return nil, err } - operationPath := fmt.Sprintf("/projects/%s/functions", pathParam0) + operationPath := fmt.Sprintf("/projects/%s/frontends", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -21792,8 +22536,8 @@ func NewListFunctionsRequest(server string, id ProjectId, params *ListFunctionsP return req, nil } -// NewCreateFunctionRequestWithBody generates requests for CreateFunction with any type of body -func NewCreateFunctionRequestWithBody(server string, id ProjectId, contentType string, body io.Reader) (*http.Request, error) { +// NewCreateFrontendRequestWithBody generates requests for CreateFrontend with any type of body +func NewCreateFrontendRequestWithBody(server string, id ProjectId, contentType string, body io.Reader) (*http.Request, error) { var err error var pathParam0 string @@ -21808,7 +22552,571 @@ func NewCreateFunctionRequestWithBody(server string, id ProjectId, contentType s return nil, err } - operationPath := fmt.Sprintf("/projects/%s/functions", pathParam0) + operationPath := fmt.Sprintf("/projects/%s/frontends", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewDeleteFrontendRequest generates requests for DeleteFrontend +func NewDeleteFrontendRequest(server string, id ProjectId, frontendId FrontendId) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "frontendId", frontendId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/projects/%s/frontends/%s", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetFrontendRequest generates requests for GetFrontend +func NewGetFrontendRequest(server string, id ProjectId, frontendId FrontendId) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "frontendId", frontendId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/projects/%s/frontends/%s", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewListFrontendDeploymentsRequest generates requests for ListFrontendDeployments +func NewListFrontendDeploymentsRequest(server string, id ProjectId, frontendId FrontendId, params *ListFrontendDeploymentsParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "frontendId", frontendId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/projects/%s/frontends/%s/deployments", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). + queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string + + if params.Page != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "page", *params.Page, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + } + + if params.Limit != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "limit", *params.Limit, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + } + + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") + } + + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewDeleteFrontendCustomDomainRequest generates requests for DeleteFrontendCustomDomain +func NewDeleteFrontendCustomDomainRequest(server string, id ProjectId, frontendId FrontendId) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "frontendId", frontendId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/projects/%s/frontends/%s/domain", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodDelete, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetFrontendCustomDomainRequest generates requests for GetFrontendCustomDomain +func NewGetFrontendCustomDomainRequest(server string, id ProjectId, frontendId FrontendId) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "frontendId", frontendId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/projects/%s/frontends/%s/domain", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewCreateFrontendCustomDomainRequest calls the generic CreateFrontendCustomDomain builder with application/json body +func NewCreateFrontendCustomDomainRequest(server string, id ProjectId, frontendId FrontendId, body CreateFrontendCustomDomainJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewCreateFrontendCustomDomainRequestWithBody(server, id, frontendId, "application/json", bodyReader) +} + +// NewCreateFrontendCustomDomainRequestWithBody generates requests for CreateFrontendCustomDomain with any type of body +func NewCreateFrontendCustomDomainRequestWithBody(server string, id ProjectId, frontendId FrontendId, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "frontendId", frontendId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/projects/%s/frontends/%s/domain", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewRedeployFrontendRequest generates requests for RedeployFrontend +func NewRedeployFrontendRequest(server string, id ProjectId, frontendId FrontendId) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "frontendId", frontendId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/projects/%s/frontends/%s/redeploy", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodPost, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetFrontendUsageHistoryRequest generates requests for GetFrontendUsageHistory +func NewGetFrontendUsageHistoryRequest(server string, id ProjectId, frontendId FrontendId, params *GetFrontendUsageHistoryParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithOptions("simple", false, "frontendId", frontendId, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/projects/%s/frontends/%s/usage", pathParam0, pathParam1) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). + queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string + + if params.Days != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "days", *params.Days, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + } + + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") + } + + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewListFunctionsRequest generates requests for ListFunctions +func NewListFunctionsRequest(server string, id ProjectId, params *ListFunctionsParams) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/projects/%s/functions", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + // queryValues collects non-styled parameters (passthrough, JSON) + // that are safe to round-trip through url.Values.Encode(). + queryValues := queryURL.Query() + // rawQueryFragments collects pre-encoded query fragments from + // styled parameters, preserving literal commas as delimiters + // per the OpenAPI spec (e.g. "color=blue,black,brown"). + var rawQueryFragments []string + + if params.Page != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "page", *params.Page, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + } + + if params.Limit != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "limit", *params.Limit, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + } + + if params.Cursor != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "cursor", *params.Cursor, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + } + + if params.EndingBefore != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "ending_before", *params.EndingBefore, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + } + + if params.Offset != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "offset", *params.Offset, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "integer", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + } + + if params.Search != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "search", *params.Search, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "string", Format: ""}); err != nil { + return nil, err + } else { + for _, qp := range strings.Split(queryFrag, "&") { + rawQueryFragments = append(rawQueryFragments, qp) + } + } + + } + + if encoded := queryValues.Encode(); encoded != "" { + rawQueryFragments = append(rawQueryFragments, encoded) + } + queryURL.RawQuery = strings.Join(rawQueryFragments, "&") + } + + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewCreateFunctionRequestWithBody generates requests for CreateFunction with any type of body +func NewCreateFunctionRequestWithBody(server string, id ProjectId, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: "uuid"}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/projects/%s/functions", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -26189,6 +27497,51 @@ type ClientWithResponsesInterface interface { // ListProjectCustomDomainsWithResponse request ListProjectCustomDomainsWithResponse(ctx context.Context, id ProjectId, params *ListProjectCustomDomainsParams, reqEditors ...RequestEditorFn) (*ListProjectCustomDomainsClientResponse, error) + // ListDurableFunctionsWithResponse request + ListDurableFunctionsWithResponse(ctx context.Context, id ProjectId, params *ListDurableFunctionsParams, reqEditors ...RequestEditorFn) (*ListDurableFunctionsClientResponse, error) + + // CreateDurableFunctionWithBodyWithResponse request with any body + CreateDurableFunctionWithBodyWithResponse(ctx context.Context, id ProjectId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateDurableFunctionClientResponse, error) + + // DeleteDurableFunctionWithResponse request + DeleteDurableFunctionWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, reqEditors ...RequestEditorFn) (*DeleteDurableFunctionClientResponse, error) + + // GetDurableFunctionWithResponse request + GetDurableFunctionWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, reqEditors ...RequestEditorFn) (*GetDurableFunctionClientResponse, error) + + // ListDurableExecutionsWithResponse request + ListDurableExecutionsWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, params *ListDurableExecutionsParams, reqEditors ...RequestEditorFn) (*ListDurableExecutionsClientResponse, error) + + // StartDurableExecutionWithBodyWithResponse request with any body + StartDurableExecutionWithBodyWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, params *StartDurableExecutionParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StartDurableExecutionClientResponse, error) + + StartDurableExecutionWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, params *StartDurableExecutionParams, body StartDurableExecutionJSONRequestBody, reqEditors ...RequestEditorFn) (*StartDurableExecutionClientResponse, error) + + // GetDurableExecutionWithResponse request + GetDurableExecutionWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, executionId DurableExecutionId, reqEditors ...RequestEditorFn) (*GetDurableExecutionClientResponse, error) + + // StopDurableExecutionWithResponse request + StopDurableExecutionWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, executionId DurableExecutionId, reqEditors ...RequestEditorFn) (*StopDurableExecutionClientResponse, error) + + // ListDurableFunctionSchedulersWithResponse request + ListDurableFunctionSchedulersWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, reqEditors ...RequestEditorFn) (*ListDurableFunctionSchedulersClientResponse, error) + + // CreateDurableFunctionSchedulerWithBodyWithResponse request with any body + CreateDurableFunctionSchedulerWithBodyWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateDurableFunctionSchedulerClientResponse, error) + + CreateDurableFunctionSchedulerWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, body CreateDurableFunctionSchedulerJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateDurableFunctionSchedulerClientResponse, error) + + // DeleteDurableFunctionSchedulerWithResponse request + DeleteDurableFunctionSchedulerWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, reqEditors ...RequestEditorFn) (*DeleteDurableFunctionSchedulerClientResponse, error) + + // GetDurableFunctionSchedulerWithResponse request + GetDurableFunctionSchedulerWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, reqEditors ...RequestEditorFn) (*GetDurableFunctionSchedulerClientResponse, error) + + // UpdateDurableFunctionSchedulerWithBodyWithResponse request with any body + UpdateDurableFunctionSchedulerWithBodyWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateDurableFunctionSchedulerClientResponse, error) + + UpdateDurableFunctionSchedulerWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, body UpdateDurableFunctionSchedulerJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateDurableFunctionSchedulerClientResponse, error) + // ListEmailTemplatesWithResponse request ListEmailTemplatesWithResponse(ctx context.Context, id ProjectId, reqEditors ...RequestEditorFn) (*ListEmailTemplatesClientResponse, error) @@ -30774,6 +32127,423 @@ func (r ListProjectCustomDomainsClientResponse) ContentType() string { return "" } +type ListDurableFunctionsClientResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *PaginatedDurableFunctions + JSON400 *Error + JSON404 *Error +} + +// Status returns HTTPResponse.Status +func (r ListDurableFunctionsClientResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ListDurableFunctionsClientResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r ListDurableFunctionsClientResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type CreateDurableFunctionClientResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *DurableFunction + JSON201 *DurableFunction + JSON400 *Error + JSON403 *Error + JSON409 *Error + JSON503 *Error +} + +// Status returns HTTPResponse.Status +func (r CreateDurableFunctionClientResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreateDurableFunctionClientResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r CreateDurableFunctionClientResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type DeleteDurableFunctionClientResponse struct { + Body []byte + HTTPResponse *http.Response + JSON404 *Error +} + +// Status returns HTTPResponse.Status +func (r DeleteDurableFunctionClientResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteDurableFunctionClientResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r DeleteDurableFunctionClientResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type GetDurableFunctionClientResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *DurableFunction + JSON404 *Error +} + +// Status returns HTTPResponse.Status +func (r GetDurableFunctionClientResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetDurableFunctionClientResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r GetDurableFunctionClientResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type ListDurableExecutionsClientResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *PaginatedDurableExecutions + JSON400 *Error + JSON404 *Error +} + +// Status returns HTTPResponse.Status +func (r ListDurableExecutionsClientResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ListDurableExecutionsClientResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r ListDurableExecutionsClientResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type StartDurableExecutionClientResponse struct { + Body []byte + HTTPResponse *http.Response + JSON202 *DurableExecution + JSON400 *Error + JSON404 *Error + JSON409 *Error + JSON413 *Error + JSON429 *Error + JSON503 *Error +} + +// Status returns HTTPResponse.Status +func (r StartDurableExecutionClientResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r StartDurableExecutionClientResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r StartDurableExecutionClientResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type GetDurableExecutionClientResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *DurableExecution + JSON404 *Error + JSON503 *Error +} + +// Status returns HTTPResponse.Status +func (r GetDurableExecutionClientResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetDurableExecutionClientResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r GetDurableExecutionClientResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type StopDurableExecutionClientResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *DurableExecution + JSON404 *Error + JSON409 *Error + JSON503 *Error +} + +// Status returns HTTPResponse.Status +func (r StopDurableExecutionClientResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r StopDurableExecutionClientResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r StopDurableExecutionClientResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type ListDurableFunctionSchedulersClientResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *FunctionSchedulerListResponse + JSON404 *Error +} + +// Status returns HTTPResponse.Status +func (r ListDurableFunctionSchedulersClientResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ListDurableFunctionSchedulersClientResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r ListDurableFunctionSchedulersClientResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type CreateDurableFunctionSchedulerClientResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *FunctionScheduler + JSON400 *Error + JSON404 *Error +} + +// Status returns HTTPResponse.Status +func (r CreateDurableFunctionSchedulerClientResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreateDurableFunctionSchedulerClientResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r CreateDurableFunctionSchedulerClientResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type DeleteDurableFunctionSchedulerClientResponse struct { + Body []byte + HTTPResponse *http.Response + JSON404 *Error +} + +// Status returns HTTPResponse.Status +func (r DeleteDurableFunctionSchedulerClientResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteDurableFunctionSchedulerClientResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r DeleteDurableFunctionSchedulerClientResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type GetDurableFunctionSchedulerClientResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *FunctionScheduler + JSON404 *Error +} + +// Status returns HTTPResponse.Status +func (r GetDurableFunctionSchedulerClientResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetDurableFunctionSchedulerClientResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r GetDurableFunctionSchedulerClientResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + +type UpdateDurableFunctionSchedulerClientResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *FunctionScheduler + JSON400 *Error + JSON404 *Error +} + +// Status returns HTTPResponse.Status +func (r UpdateDurableFunctionSchedulerClientResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r UpdateDurableFunctionSchedulerClientResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r UpdateDurableFunctionSchedulerClientResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type ListEmailTemplatesClientResponse struct { Body []byte HTTPResponse *http.Response @@ -35216,6 +36986,147 @@ func (c *ClientWithResponses) ListProjectCustomDomainsWithResponse(ctx context.C return ParseListProjectCustomDomainsClientResponse(rsp) } +// ListDurableFunctionsWithResponse request returning *ListDurableFunctionsClientResponse +func (c *ClientWithResponses) ListDurableFunctionsWithResponse(ctx context.Context, id ProjectId, params *ListDurableFunctionsParams, reqEditors ...RequestEditorFn) (*ListDurableFunctionsClientResponse, error) { + rsp, err := c.ListDurableFunctions(ctx, id, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseListDurableFunctionsClientResponse(rsp) +} + +// CreateDurableFunctionWithBodyWithResponse request with arbitrary body returning *CreateDurableFunctionClientResponse +func (c *ClientWithResponses) CreateDurableFunctionWithBodyWithResponse(ctx context.Context, id ProjectId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateDurableFunctionClientResponse, error) { + rsp, err := c.CreateDurableFunctionWithBody(ctx, id, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateDurableFunctionClientResponse(rsp) +} + +// DeleteDurableFunctionWithResponse request returning *DeleteDurableFunctionClientResponse +func (c *ClientWithResponses) DeleteDurableFunctionWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, reqEditors ...RequestEditorFn) (*DeleteDurableFunctionClientResponse, error) { + rsp, err := c.DeleteDurableFunction(ctx, id, functionId, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteDurableFunctionClientResponse(rsp) +} + +// GetDurableFunctionWithResponse request returning *GetDurableFunctionClientResponse +func (c *ClientWithResponses) GetDurableFunctionWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, reqEditors ...RequestEditorFn) (*GetDurableFunctionClientResponse, error) { + rsp, err := c.GetDurableFunction(ctx, id, functionId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetDurableFunctionClientResponse(rsp) +} + +// ListDurableExecutionsWithResponse request returning *ListDurableExecutionsClientResponse +func (c *ClientWithResponses) ListDurableExecutionsWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, params *ListDurableExecutionsParams, reqEditors ...RequestEditorFn) (*ListDurableExecutionsClientResponse, error) { + rsp, err := c.ListDurableExecutions(ctx, id, functionId, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseListDurableExecutionsClientResponse(rsp) +} + +// StartDurableExecutionWithBodyWithResponse request with arbitrary body returning *StartDurableExecutionClientResponse +func (c *ClientWithResponses) StartDurableExecutionWithBodyWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, params *StartDurableExecutionParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StartDurableExecutionClientResponse, error) { + rsp, err := c.StartDurableExecutionWithBody(ctx, id, functionId, params, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseStartDurableExecutionClientResponse(rsp) +} + +func (c *ClientWithResponses) StartDurableExecutionWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, params *StartDurableExecutionParams, body StartDurableExecutionJSONRequestBody, reqEditors ...RequestEditorFn) (*StartDurableExecutionClientResponse, error) { + rsp, err := c.StartDurableExecution(ctx, id, functionId, params, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseStartDurableExecutionClientResponse(rsp) +} + +// GetDurableExecutionWithResponse request returning *GetDurableExecutionClientResponse +func (c *ClientWithResponses) GetDurableExecutionWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, executionId DurableExecutionId, reqEditors ...RequestEditorFn) (*GetDurableExecutionClientResponse, error) { + rsp, err := c.GetDurableExecution(ctx, id, functionId, executionId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetDurableExecutionClientResponse(rsp) +} + +// StopDurableExecutionWithResponse request returning *StopDurableExecutionClientResponse +func (c *ClientWithResponses) StopDurableExecutionWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, executionId DurableExecutionId, reqEditors ...RequestEditorFn) (*StopDurableExecutionClientResponse, error) { + rsp, err := c.StopDurableExecution(ctx, id, functionId, executionId, reqEditors...) + if err != nil { + return nil, err + } + return ParseStopDurableExecutionClientResponse(rsp) +} + +// ListDurableFunctionSchedulersWithResponse request returning *ListDurableFunctionSchedulersClientResponse +func (c *ClientWithResponses) ListDurableFunctionSchedulersWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, reqEditors ...RequestEditorFn) (*ListDurableFunctionSchedulersClientResponse, error) { + rsp, err := c.ListDurableFunctionSchedulers(ctx, id, functionId, reqEditors...) + if err != nil { + return nil, err + } + return ParseListDurableFunctionSchedulersClientResponse(rsp) +} + +// CreateDurableFunctionSchedulerWithBodyWithResponse request with arbitrary body returning *CreateDurableFunctionSchedulerClientResponse +func (c *ClientWithResponses) CreateDurableFunctionSchedulerWithBodyWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateDurableFunctionSchedulerClientResponse, error) { + rsp, err := c.CreateDurableFunctionSchedulerWithBody(ctx, id, functionId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateDurableFunctionSchedulerClientResponse(rsp) +} + +func (c *ClientWithResponses) CreateDurableFunctionSchedulerWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, body CreateDurableFunctionSchedulerJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateDurableFunctionSchedulerClientResponse, error) { + rsp, err := c.CreateDurableFunctionScheduler(ctx, id, functionId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateDurableFunctionSchedulerClientResponse(rsp) +} + +// DeleteDurableFunctionSchedulerWithResponse request returning *DeleteDurableFunctionSchedulerClientResponse +func (c *ClientWithResponses) DeleteDurableFunctionSchedulerWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, reqEditors ...RequestEditorFn) (*DeleteDurableFunctionSchedulerClientResponse, error) { + rsp, err := c.DeleteDurableFunctionScheduler(ctx, id, functionId, schedulerId, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteDurableFunctionSchedulerClientResponse(rsp) +} + +// GetDurableFunctionSchedulerWithResponse request returning *GetDurableFunctionSchedulerClientResponse +func (c *ClientWithResponses) GetDurableFunctionSchedulerWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, reqEditors ...RequestEditorFn) (*GetDurableFunctionSchedulerClientResponse, error) { + rsp, err := c.GetDurableFunctionScheduler(ctx, id, functionId, schedulerId, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetDurableFunctionSchedulerClientResponse(rsp) +} + +// UpdateDurableFunctionSchedulerWithBodyWithResponse request with arbitrary body returning *UpdateDurableFunctionSchedulerClientResponse +func (c *ClientWithResponses) UpdateDurableFunctionSchedulerWithBodyWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*UpdateDurableFunctionSchedulerClientResponse, error) { + rsp, err := c.UpdateDurableFunctionSchedulerWithBody(ctx, id, functionId, schedulerId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpdateDurableFunctionSchedulerClientResponse(rsp) +} + +func (c *ClientWithResponses) UpdateDurableFunctionSchedulerWithResponse(ctx context.Context, id ProjectId, functionId DurableFunctionId, schedulerId SchedulerId, body UpdateDurableFunctionSchedulerJSONRequestBody, reqEditors ...RequestEditorFn) (*UpdateDurableFunctionSchedulerClientResponse, error) { + rsp, err := c.UpdateDurableFunctionScheduler(ctx, id, functionId, schedulerId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseUpdateDurableFunctionSchedulerClientResponse(rsp) +} + // ListEmailTemplatesWithResponse request returning *ListEmailTemplatesClientResponse func (c *ClientWithResponses) ListEmailTemplatesWithResponse(ctx context.Context, id ProjectId, reqEditors ...RequestEditorFn) (*ListEmailTemplatesClientResponse, error) { rsp, err := c.ListEmailTemplates(ctx, id, reqEditors...) @@ -40062,30 +41973,429 @@ func ParseDeleteAuthUserClientResponse(rsp *http.Response) (*DeleteAuthUserClien return nil, err } - response := &DeleteAuthUserClientResponse{ - Body: bodyBytes, - HTTPResponse: rsp, + response := &DeleteAuthUserClientResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseGetAuthUserClientResponse parses an HTTP response from a GetAuthUserWithResponse call +func ParseGetAuthUserClientResponse(rsp *http.Response) (*GetAuthUserClientResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetAuthUserClientResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest AuthUser + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseBanAuthUserClientResponse parses an HTTP response from a BanAuthUserWithResponse call +func ParseBanAuthUserClientResponse(rsp *http.Response) (*BanAuthUserClientResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &BanAuthUserClientResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest BanUserResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + + return response, nil +} + +// ParseDeleteAllUserSessionsClientResponse parses an HTTP response from a DeleteAllUserSessionsWithResponse call +func ParseDeleteAllUserSessionsClientResponse(rsp *http.Response) (*DeleteAllUserSessionsClientResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteAllUserSessionsClientResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseListUserSessionsClientResponse parses an HTTP response from a ListUserSessionsWithResponse call +func ParseListUserSessionsClientResponse(rsp *http.Response) (*ListUserSessionsClientResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ListUserSessionsClientResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest struct { + // Data Sessions for this page (cursor pagination only) + Data *[]AuthSession `json:"data,omitempty"` + + // HasMore Whether a further page exists (cursor pagination only) + HasMore *bool `json:"has_more,omitempty"` + + // Limit Number of sessions per page + Limit *int `json:"limit,omitempty"` + + // NextCursor Opaque cursor for the next page (cursor pagination only) + NextCursor *string `json:"next_cursor,omitempty"` + + // Page Current page number + Page *int `json:"page,omitempty"` + + // PrevCursor Opaque cursor for the previous page (cursor pagination only). Send as `ending_before`. + PrevCursor *string `json:"prev_cursor,omitempty"` + Sessions *[]AuthSession `json:"sessions,omitempty"` + + // Total Total number of sessions + Total *int `json:"total,omitempty"` + + // TotalPages Total number of pages + TotalPages *int `json:"total_pages,omitempty"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + } + + return response, nil +} + +// ParseDeleteUserSessionClientResponse parses an HTTP response from a DeleteUserSessionWithResponse call +func ParseDeleteUserSessionClientResponse(rsp *http.Response) (*DeleteUserSessionClientResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteUserSessionClientResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + return response, nil +} + +// ParseUnbanAuthUserClientResponse parses an HTTP response from a UnbanAuthUserWithResponse call +func ParseUnbanAuthUserClientResponse(rsp *http.Response) (*UnbanAuthUserClientResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &UnbanAuthUserClientResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest UnbanUserResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + } + + return response, nil +} + +// ParseGetProjectConfigClientResponse parses an HTTP response from a GetProjectConfigWithResponse call +func ParseGetProjectConfigClientResponse(rsp *http.Response) (*GetProjectConfigClientResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetProjectConfigClientResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ProjectConfig + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 413: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON413 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "yaml") && rsp.StatusCode == 200: + var dest openapi_types.File + if err := yaml.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.YAML200 = &dest + + } + + return response, nil +} + +// ParseApplyProjectConfigClientResponse parses an HTTP response from a ApplyProjectConfigWithResponse call +func ParseApplyProjectConfigClientResponse(rsp *http.Response) (*ApplyProjectConfigClientResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ApplyProjectConfigClientResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ProjectConfigApplyResult + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: + var dest ProjectConfigValidationErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON422 = &dest + + } + + return response, nil +} + +// ParseListDatabasesClientResponse parses an HTTP response from a ListDatabasesWithResponse call +func ParseListDatabasesClientResponse(rsp *http.Response) (*ListDatabasesClientResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ListDatabasesClientResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest PaginatedDatabases + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + +// ParseCreateDatabaseClientResponse parses an HTTP response from a CreateDatabaseWithResponse call +func ParseCreateDatabaseClientResponse(rsp *http.Response) (*CreateDatabaseClientResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &CreateDatabaseClientResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest Database + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + } + + return response, nil +} + +// ParseDeleteDatabaseClientResponse parses an HTTP response from a DeleteDatabaseWithResponse call +func ParseDeleteDatabaseClientResponse(rsp *http.Response) (*DeleteDatabaseClientResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteDatabaseClientResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: + var dest struct { + Message *string `json:"message,omitempty"` + Status *string `json:"status,omitempty"` + } + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON202 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + } return response, nil } -// ParseGetAuthUserClientResponse parses an HTTP response from a GetAuthUserWithResponse call -func ParseGetAuthUserClientResponse(rsp *http.Response) (*GetAuthUserClientResponse, error) { +// ParseGetDatabaseClientResponse parses an HTTP response from a GetDatabaseWithResponse call +func ParseGetDatabaseClientResponse(rsp *http.Response) (*GetDatabaseClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &GetAuthUserClientResponse{ + response := &GetDatabaseClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest AuthUser + var dest Database if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } @@ -40096,27 +42406,34 @@ func ParseGetAuthUserClientResponse(rsp *http.Response) (*GetAuthUserClientRespo return response, nil } -// ParseBanAuthUserClientResponse parses an HTTP response from a BanAuthUserWithResponse call -func ParseBanAuthUserClientResponse(rsp *http.Response) (*BanAuthUserClientResponse, error) { +// ParseGetDatabaseBackupScheduleClientResponse parses an HTTP response from a GetDatabaseBackupScheduleWithResponse call +func ParseGetDatabaseBackupScheduleClientResponse(rsp *http.Response) (*GetDatabaseBackupScheduleClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &BanAuthUserClientResponse{ + response := &GetDatabaseBackupScheduleClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest BanUserResponse + var dest DatabaseBackupSchedule if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -40124,121 +42441,174 @@ func ParseBanAuthUserClientResponse(rsp *http.Response) (*BanAuthUserClientRespo } response.JSON404 = &dest - } - - return response, nil -} + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest -// ParseDeleteAllUserSessionsClientResponse parses an HTTP response from a DeleteAllUserSessionsWithResponse call -func ParseDeleteAllUserSessionsClientResponse(rsp *http.Response) (*DeleteAllUserSessionsClientResponse, error) { - bodyBytes, err := io.ReadAll(rsp.Body) - defer func() { _ = rsp.Body.Close() }() - if err != nil { - return nil, err - } + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON503 = &dest - response := &DeleteAllUserSessionsClientResponse{ - Body: bodyBytes, - HTTPResponse: rsp, } return response, nil } -// ParseListUserSessionsClientResponse parses an HTTP response from a ListUserSessionsWithResponse call -func ParseListUserSessionsClientResponse(rsp *http.Response) (*ListUserSessionsClientResponse, error) { +// ParseUpdateDatabaseBackupScheduleClientResponse parses an HTTP response from a UpdateDatabaseBackupScheduleWithResponse call +func ParseUpdateDatabaseBackupScheduleClientResponse(rsp *http.Response) (*UpdateDatabaseBackupScheduleClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ListUserSessionsClientResponse{ + response := &UpdateDatabaseBackupScheduleClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest struct { - // Data Sessions for this page (cursor pagination only) - Data *[]AuthSession `json:"data,omitempty"` - - // HasMore Whether a further page exists (cursor pagination only) - HasMore *bool `json:"has_more,omitempty"` - - // Limit Number of sessions per page - Limit *int `json:"limit,omitempty"` - - // NextCursor Opaque cursor for the next page (cursor pagination only) - NextCursor *string `json:"next_cursor,omitempty"` - - // Page Current page number - Page *int `json:"page,omitempty"` + var dest DatabaseBackupSchedule + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest - // PrevCursor Opaque cursor for the previous page (cursor pagination only). Send as `ending_before`. - PrevCursor *string `json:"prev_cursor,omitempty"` - Sessions *[]AuthSession `json:"sessions,omitempty"` + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest - // Total Total number of sessions - Total *int `json:"total,omitempty"` + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest - // TotalPages Total number of pages - TotalPages *int `json:"total_pages,omitempty"` + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON200 = &dest + response.JSON409 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON400 = &dest + response.JSON503 = &dest } return response, nil } -// ParseDeleteUserSessionClientResponse parses an HTTP response from a DeleteUserSessionWithResponse call -func ParseDeleteUserSessionClientResponse(rsp *http.Response) (*DeleteUserSessionClientResponse, error) { +// ParseListDatabaseBackupsClientResponse parses an HTTP response from a ListDatabaseBackupsWithResponse call +func ParseListDatabaseBackupsClientResponse(rsp *http.Response) (*ListDatabaseBackupsClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &DeleteUserSessionClientResponse{ + response := &ListDatabaseBackupsClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest DatabaseBackupList + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON503 = &dest + + } + return response, nil } -// ParseUnbanAuthUserClientResponse parses an HTTP response from a UnbanAuthUserWithResponse call -func ParseUnbanAuthUserClientResponse(rsp *http.Response) (*UnbanAuthUserClientResponse, error) { +// ParseCreateDatabaseBackupClientResponse parses an HTTP response from a CreateDatabaseBackupWithResponse call +func ParseCreateDatabaseBackupClientResponse(rsp *http.Response) (*CreateDatabaseBackupClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &UnbanAuthUserClientResponse{ + response := &CreateDatabaseBackupClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest UnbanUserResponse + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest DatabaseBackup if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON200 = &dest + response.JSON201 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error @@ -40247,38 +42617,55 @@ func ParseUnbanAuthUserClientResponse(rsp *http.Response) (*UnbanAuthUserClientR } response.JSON404 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON503 = &dest + } return response, nil } -// ParseGetProjectConfigClientResponse parses an HTTP response from a GetProjectConfigWithResponse call -func ParseGetProjectConfigClientResponse(rsp *http.Response) (*GetProjectConfigClientResponse, error) { +// ParseDeleteDatabaseBackupClientResponse parses an HTTP response from a DeleteDatabaseBackupWithResponse call +func ParseDeleteDatabaseBackupClientResponse(rsp *http.Response) (*DeleteDatabaseBackupClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &GetProjectConfigClientResponse{ + response := &DeleteDatabaseBackupClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ProjectConfig + var dest struct { + Message string `json:"message"` + Status string `json:"status"` + } if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON401 = &dest + response.JSON403 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error @@ -40287,59 +42674,52 @@ func ParseGetProjectConfigClientResponse(rsp *http.Response) (*GetProjectConfigC } response.JSON404 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 413: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON413 = &dest + response.JSON409 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "yaml") && rsp.StatusCode == 200: - var dest openapi_types.File - if err := yaml.Unmarshal(bodyBytes, &dest); err != nil { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.YAML200 = &dest + response.JSON503 = &dest } return response, nil } -// ParseApplyProjectConfigClientResponse parses an HTTP response from a ApplyProjectConfigWithResponse call -func ParseApplyProjectConfigClientResponse(rsp *http.Response) (*ApplyProjectConfigClientResponse, error) { +// ParseGetDatabaseBackupClientResponse parses an HTTP response from a GetDatabaseBackupWithResponse call +func ParseGetDatabaseBackupClientResponse(rsp *http.Response) (*GetDatabaseBackupClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ApplyProjectConfigClientResponse{ + response := &GetDatabaseBackupClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ProjectConfigApplyResult + var dest DatabaseBackup if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON400 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON401 = &dest + response.JSON403 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error @@ -40355,64 +42735,85 @@ func ParseApplyProjectConfigClientResponse(rsp *http.Response) (*ApplyProjectCon } response.JSON409 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 422: - var dest ProjectConfigValidationErrorResponse + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON422 = &dest + response.JSON503 = &dest } return response, nil } -// ParseListDatabasesClientResponse parses an HTTP response from a ListDatabasesWithResponse call -func ParseListDatabasesClientResponse(rsp *http.Response) (*ListDatabasesClientResponse, error) { +// ParseListDatabaseBranchesClientResponse parses an HTTP response from a ListDatabaseBranchesWithResponse call +func ParseListDatabaseBranchesClientResponse(rsp *http.Response) (*ListDatabaseBranchesClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ListDatabasesClientResponse{ + response := &ListDatabaseBranchesClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest PaginatedDatabases + var dest DatabaseBranchList if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON503 = &dest + } return response, nil } -// ParseCreateDatabaseClientResponse parses an HTTP response from a CreateDatabaseWithResponse call -func ParseCreateDatabaseClientResponse(rsp *http.Response) (*CreateDatabaseClientResponse, error) { +// ParseCreateDatabaseBranchClientResponse parses an HTTP response from a CreateDatabaseBranchWithResponse call +func ParseCreateDatabaseBranchClientResponse(rsp *http.Response) (*CreateDatabaseBranchClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &CreateDatabaseClientResponse{ + response := &CreateDatabaseBranchClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest Database + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: + var dest DatabaseBranch if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON201 = &dest + response.JSON202 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: var dest Error @@ -40421,20 +42822,41 @@ func ParseCreateDatabaseClientResponse(rsp *http.Response) (*CreateDatabaseClien } response.JSON403 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON503 = &dest + } return response, nil } -// ParseDeleteDatabaseClientResponse parses an HTTP response from a DeleteDatabaseWithResponse call -func ParseDeleteDatabaseClientResponse(rsp *http.Response) (*DeleteDatabaseClientResponse, error) { +// ParseDeleteDatabaseBranchClientResponse parses an HTTP response from a DeleteDatabaseBranchWithResponse call +func ParseDeleteDatabaseBranchClientResponse(rsp *http.Response) (*DeleteDatabaseBranchClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &DeleteDatabaseClientResponse{ + response := &DeleteDatabaseBranchClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -40442,8 +42864,8 @@ func ParseDeleteDatabaseClientResponse(rsp *http.Response) (*DeleteDatabaseClien switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: var dest struct { - Message *string `json:"message,omitempty"` - Status *string `json:"status,omitempty"` + Message string `json:"message"` + Status string `json:"status"` } if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err @@ -40457,71 +42879,85 @@ func ParseDeleteDatabaseClientResponse(rsp *http.Response) (*DeleteDatabaseClien } response.JSON404 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON409 = &dest + response.JSON503 = &dest } return response, nil } -// ParseGetDatabaseClientResponse parses an HTTP response from a GetDatabaseWithResponse call -func ParseGetDatabaseClientResponse(rsp *http.Response) (*GetDatabaseClientResponse, error) { +// ParseGetDatabaseBranchClientResponse parses an HTTP response from a GetDatabaseBranchWithResponse call +func ParseGetDatabaseBranchClientResponse(rsp *http.Response) (*GetDatabaseBranchClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &GetDatabaseClientResponse{ + response := &GetDatabaseBranchClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest Database + var dest DatabaseBranch if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON503 = &dest + } return response, nil } -// ParseGetDatabaseBackupScheduleClientResponse parses an HTTP response from a GetDatabaseBackupScheduleWithResponse call -func ParseGetDatabaseBackupScheduleClientResponse(rsp *http.Response) (*GetDatabaseBackupScheduleClientResponse, error) { +// ParseUpdateDatabaseBranchClientResponse parses an HTTP response from a UpdateDatabaseBranchWithResponse call +func ParseUpdateDatabaseBranchClientResponse(rsp *http.Response) (*UpdateDatabaseBranchClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &GetDatabaseBackupScheduleClientResponse{ + response := &UpdateDatabaseBranchClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DatabaseBackupSchedule + var dest DatabaseBranch if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON403 = &dest + response.JSON400 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error @@ -40549,40 +42985,26 @@ func ParseGetDatabaseBackupScheduleClientResponse(rsp *http.Response) (*GetDatab return response, nil } -// ParseUpdateDatabaseBackupScheduleClientResponse parses an HTTP response from a UpdateDatabaseBackupScheduleWithResponse call -func ParseUpdateDatabaseBackupScheduleClientResponse(rsp *http.Response) (*UpdateDatabaseBackupScheduleClientResponse, error) { +// ParseResetDatabaseBranchClientResponse parses an HTTP response from a ResetDatabaseBranchWithResponse call +func ParseResetDatabaseBranchClientResponse(rsp *http.Response) (*ResetDatabaseBranchClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &UpdateDatabaseBackupScheduleClientResponse{ + response := &ResetDatabaseBranchClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DatabaseBackupSchedule - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON400 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: - var dest Error + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: + var dest DatabaseBranch if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON403 = &dest + response.JSON202 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error @@ -40610,34 +43032,27 @@ func ParseUpdateDatabaseBackupScheduleClientResponse(rsp *http.Response) (*Updat return response, nil } -// ParseListDatabaseBackupsClientResponse parses an HTTP response from a ListDatabaseBackupsWithResponse call -func ParseListDatabaseBackupsClientResponse(rsp *http.Response) (*ListDatabaseBackupsClientResponse, error) { +// ParseResetDatabaseBranchPasswordClientResponse parses an HTTP response from a ResetDatabaseBranchPasswordWithResponse call +func ParseResetDatabaseBranchPasswordClientResponse(rsp *http.Response) (*ResetDatabaseBranchPasswordClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ListDatabaseBackupsClientResponse{ + response := &ResetDatabaseBranchPasswordClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DatabaseBackupList + var dest DatabaseBranch if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON403 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -40664,26 +43079,26 @@ func ParseListDatabaseBackupsClientResponse(rsp *http.Response) (*ListDatabaseBa return response, nil } -// ParseCreateDatabaseBackupClientResponse parses an HTTP response from a CreateDatabaseBackupWithResponse call -func ParseCreateDatabaseBackupClientResponse(rsp *http.Response) (*CreateDatabaseBackupClientResponse, error) { +// ParseGetProjectDatabaseQueriesClientResponse parses an HTTP response from a GetProjectDatabaseQueriesWithResponse call +func ParseGetProjectDatabaseQueriesClientResponse(rsp *http.Response) (*GetProjectDatabaseQueriesClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &CreateDatabaseBackupClientResponse{ + response := &GetProjectDatabaseQueriesClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest DatabaseBackup + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest DatabaseQueryPerformanceResponse if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON201 = &dest + response.JSON200 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest Error @@ -40692,48 +43107,48 @@ func ParseCreateDatabaseBackupClientResponse(rsp *http.Response) (*CreateDatabas } response.JSON400 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON403 = &dest + response.JSON401 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON404 = &dest + response.JSON403 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON409 = &dest + response.JSON404 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON503 = &dest + response.JSON500 = &dest } return response, nil } -// ParseDeleteDatabaseBackupClientResponse parses an HTTP response from a DeleteDatabaseBackupWithResponse call -func ParseDeleteDatabaseBackupClientResponse(rsp *http.Response) (*DeleteDatabaseBackupClientResponse, error) { +// ParseResetDatabasePasswordClientResponse parses an HTTP response from a ResetDatabasePasswordWithResponse call +func ParseResetDatabasePasswordClientResponse(rsp *http.Response) (*ResetDatabasePasswordClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &DeleteDatabaseBackupClientResponse{ + response := &ResetDatabasePasswordClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -40741,20 +43156,27 @@ func ParseDeleteDatabaseBackupClientResponse(rsp *http.Response) (*DeleteDatabas switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: var dest struct { - Message string `json:"message"` - Status string `json:"status"` + // ConnectionString Updated pgproxy connection string using Volcano-managed credentials. + ConnectionString *string `json:"connection_string,omitempty"` + Message *string `json:"message,omitempty"` + + // NewPassword New Volcano-managed client password. Always starts with `vpg_`. + NewPassword *string `json:"new_password,omitempty"` + + // RoleName Volcano-managed per-database client login (also the pgproxy routing username) + RoleName *string `json:"role_name,omitempty"` } if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON403 = &dest + response.JSON400 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error @@ -40770,6 +43192,46 @@ func ParseDeleteDatabaseBackupClientResponse(rsp *http.Response) (*DeleteDatabas } response.JSON409 = &dest + } + + return response, nil +} + +// ParseListDatabaseRestoresClientResponse parses an HTTP response from a ListDatabaseRestoresWithResponse call +func ParseListDatabaseRestoresClientResponse(rsp *http.Response) (*ListDatabaseRestoresClientResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ListDatabaseRestoresClientResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest DatabaseRestoreList + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -40782,26 +43244,33 @@ func ParseDeleteDatabaseBackupClientResponse(rsp *http.Response) (*DeleteDatabas return response, nil } -// ParseGetDatabaseBackupClientResponse parses an HTTP response from a GetDatabaseBackupWithResponse call -func ParseGetDatabaseBackupClientResponse(rsp *http.Response) (*GetDatabaseBackupClientResponse, error) { +// ParseCreateDatabaseRestoreClientResponse parses an HTTP response from a CreateDatabaseRestoreWithResponse call +func ParseCreateDatabaseRestoreClientResponse(rsp *http.Response) (*CreateDatabaseRestoreClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &GetDatabaseBackupClientResponse{ + response := &CreateDatabaseRestoreClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DatabaseBackup + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: + var dest DatabaseRestore if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON200 = &dest + response.JSON202 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: var dest Error @@ -40836,27 +43305,34 @@ func ParseGetDatabaseBackupClientResponse(rsp *http.Response) (*GetDatabaseBacku return response, nil } -// ParseListDatabaseBranchesClientResponse parses an HTTP response from a ListDatabaseBranchesWithResponse call -func ParseListDatabaseBranchesClientResponse(rsp *http.Response) (*ListDatabaseBranchesClientResponse, error) { +// ParseGetDatabaseRestoreClientResponse parses an HTTP response from a GetDatabaseRestoreWithResponse call +func ParseGetDatabaseRestoreClientResponse(rsp *http.Response) (*GetDatabaseRestoreClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ListDatabaseBranchesClientResponse{ + response := &GetDatabaseRestoreClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DatabaseBranchList + var dest DatabaseRestore if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -40876,26 +43352,26 @@ func ParseListDatabaseBranchesClientResponse(rsp *http.Response) (*ListDatabaseB return response, nil } -// ParseCreateDatabaseBranchClientResponse parses an HTTP response from a CreateDatabaseBranchWithResponse call -func ParseCreateDatabaseBranchClientResponse(rsp *http.Response) (*CreateDatabaseBranchClientResponse, error) { +// ParseGetDatabaseStatsClientResponse parses an HTTP response from a GetDatabaseStatsWithResponse call +func ParseGetDatabaseStatsClientResponse(rsp *http.Response) (*GetDatabaseStatsClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &CreateDatabaseBranchClientResponse{ + response := &GetDatabaseStatsClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: - var dest DatabaseBranch + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest DatabaseStats if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON202 = &dest + response.JSON200 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest Error @@ -40904,13 +43380,6 @@ func ParseCreateDatabaseBranchClientResponse(rsp *http.Response) (*CreateDatabas } response.JSON400 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON403 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -40918,13 +43387,6 @@ func ParseCreateDatabaseBranchClientResponse(rsp *http.Response) (*CreateDatabas } response.JSON404 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON409 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -40937,105 +43399,109 @@ func ParseCreateDatabaseBranchClientResponse(rsp *http.Response) (*CreateDatabas return response, nil } -// ParseDeleteDatabaseBranchClientResponse parses an HTTP response from a DeleteDatabaseBranchWithResponse call -func ParseDeleteDatabaseBranchClientResponse(rsp *http.Response) (*DeleteDatabaseBranchClientResponse, error) { +// ParseUpdateDatabaseTypeClientResponse parses an HTTP response from a UpdateDatabaseTypeWithResponse call +func ParseUpdateDatabaseTypeClientResponse(rsp *http.Response) (*UpdateDatabaseTypeClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &DeleteDatabaseBranchClientResponse{ + response := &UpdateDatabaseTypeClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: - var dest struct { - Message string `json:"message"` - Status string `json:"status"` - } - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON202 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest Error + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Database if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON404 = &dest + response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON503 = &dest + response.JSON409 = &dest } return response, nil } -// ParseGetDatabaseBranchClientResponse parses an HTTP response from a GetDatabaseBranchWithResponse call -func ParseGetDatabaseBranchClientResponse(rsp *http.Response) (*GetDatabaseBranchClientResponse, error) { +// ParseListProjectDeploymentsClientResponse parses an HTTP response from a ListProjectDeploymentsWithResponse call +func ParseListProjectDeploymentsClientResponse(rsp *http.Response) (*ListProjectDeploymentsClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &GetDatabaseBranchClientResponse{ + response := &ListProjectDeploymentsClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DatabaseBranch + var dest PaginatedProjectDeployments if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON404 = &dest + response.JSON400 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON503 = &dest + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest } return response, nil } -// ParseUpdateDatabaseBranchClientResponse parses an HTTP response from a UpdateDatabaseBranchWithResponse call -func ParseUpdateDatabaseBranchClientResponse(rsp *http.Response) (*UpdateDatabaseBranchClientResponse, error) { +// ParseSummarizeProjectDeploymentsClientResponse parses an HTTP response from a SummarizeProjectDeploymentsWithResponse call +func ParseSummarizeProjectDeploymentsClientResponse(rsp *http.Response) (*SummarizeProjectDeploymentsClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &UpdateDatabaseBranchClientResponse{ + response := &SummarizeProjectDeploymentsClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DatabaseBranch + var dest ProjectDeploymentSummary if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } @@ -41048,160 +43514,160 @@ func ParseUpdateDatabaseBranchClientResponse(rsp *http.Response) (*UpdateDatabas } response.JSON400 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON404 = &dest + response.JSON401 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON409 = &dest + response.JSON403 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON503 = &dest + response.JSON500 = &dest } return response, nil } -// ParseResetDatabaseBranchClientResponse parses an HTTP response from a ResetDatabaseBranchWithResponse call -func ParseResetDatabaseBranchClientResponse(rsp *http.Response) (*ResetDatabaseBranchClientResponse, error) { +// ParseListProjectCustomDomainsClientResponse parses an HTTP response from a ListProjectCustomDomainsWithResponse call +func ParseListProjectCustomDomainsClientResponse(rsp *http.Response) (*ListProjectCustomDomainsClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ResetDatabaseBranchClientResponse{ + response := &ListProjectCustomDomainsClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: - var dest DatabaseBranch + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest PaginatedProjectCustomDomains if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON202 = &dest + response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON404 = &dest + response.JSON400 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON409 = &dest + response.JSON401 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON503 = &dest + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest } return response, nil } -// ParseResetDatabaseBranchPasswordClientResponse parses an HTTP response from a ResetDatabaseBranchPasswordWithResponse call -func ParseResetDatabaseBranchPasswordClientResponse(rsp *http.Response) (*ResetDatabaseBranchPasswordClientResponse, error) { +// ParseListDurableFunctionsClientResponse parses an HTTP response from a ListDurableFunctionsWithResponse call +func ParseListDurableFunctionsClientResponse(rsp *http.Response) (*ListDurableFunctionsClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ResetDatabaseBranchPasswordClientResponse{ + response := &ListDurableFunctionsClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DatabaseBranch + var dest PaginatedDurableFunctions if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON404 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON409 = &dest + response.JSON400 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON503 = &dest + response.JSON404 = &dest } return response, nil } -// ParseGetProjectDatabaseQueriesClientResponse parses an HTTP response from a GetProjectDatabaseQueriesWithResponse call -func ParseGetProjectDatabaseQueriesClientResponse(rsp *http.Response) (*GetProjectDatabaseQueriesClientResponse, error) { +// ParseCreateDurableFunctionClientResponse parses an HTTP response from a CreateDurableFunctionWithResponse call +func ParseCreateDurableFunctionClientResponse(rsp *http.Response) (*CreateDurableFunctionClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &GetProjectDatabaseQueriesClientResponse{ + response := &CreateDurableFunctionClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DatabaseQueryPerformanceResponse + var dest DurableFunction if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest Error + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest DurableFunction if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON400 = &dest + response.JSON201 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON401 = &dest + response.JSON400 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: var dest Error @@ -41210,62 +43676,71 @@ func ParseGetProjectDatabaseQueriesClientResponse(rsp *http.Response) (*GetProje } response.JSON403 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON404 = &dest + response.JSON409 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON500 = &dest + response.JSON503 = &dest } return response, nil } -// ParseResetDatabasePasswordClientResponse parses an HTTP response from a ResetDatabasePasswordWithResponse call -func ParseResetDatabasePasswordClientResponse(rsp *http.Response) (*ResetDatabasePasswordClientResponse, error) { +// ParseDeleteDurableFunctionClientResponse parses an HTTP response from a DeleteDurableFunctionWithResponse call +func ParseDeleteDurableFunctionClientResponse(rsp *http.Response) (*DeleteDurableFunctionClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ResetDatabasePasswordClientResponse{ + response := &DeleteDurableFunctionClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest struct { - // ConnectionString Updated pgproxy connection string using Volcano-managed credentials. - ConnectionString *string `json:"connection_string,omitempty"` - Message *string `json:"message,omitempty"` - - // NewPassword New Volcano-managed client password. Always starts with `vpg_`. - NewPassword *string `json:"new_password,omitempty"` - - // RoleName Volcano-managed per-database client login (also the pgproxy routing username) - RoleName *string `json:"role_name,omitempty"` - } + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON200 = &dest + response.JSON404 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest Error + } + + return response, nil +} + +// ParseGetDurableFunctionClientResponse parses an HTTP response from a GetDurableFunctionWithResponse call +func ParseGetDurableFunctionClientResponse(rsp *http.Response) (*GetDurableFunctionClientResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetDurableFunctionClientResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest DurableFunction if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON400 = &dest + response.JSON200 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error @@ -41274,45 +43749,38 @@ func ParseResetDatabasePasswordClientResponse(rsp *http.Response) (*ResetDatabas } response.JSON404 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON409 = &dest - } return response, nil } -// ParseListDatabaseRestoresClientResponse parses an HTTP response from a ListDatabaseRestoresWithResponse call -func ParseListDatabaseRestoresClientResponse(rsp *http.Response) (*ListDatabaseRestoresClientResponse, error) { +// ParseListDurableExecutionsClientResponse parses an HTTP response from a ListDurableExecutionsWithResponse call +func ParseListDurableExecutionsClientResponse(rsp *http.Response) (*ListDurableExecutionsClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ListDatabaseRestoresClientResponse{ + response := &ListDurableExecutionsClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DatabaseRestoreList + var dest PaginatedDurableExecutions if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON403 = &dest + response.JSON400 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error @@ -41321,34 +43789,27 @@ func ParseListDatabaseRestoresClientResponse(rsp *http.Response) (*ListDatabaseR } response.JSON404 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON503 = &dest - } return response, nil } -// ParseCreateDatabaseRestoreClientResponse parses an HTTP response from a CreateDatabaseRestoreWithResponse call -func ParseCreateDatabaseRestoreClientResponse(rsp *http.Response) (*CreateDatabaseRestoreClientResponse, error) { +// ParseStartDurableExecutionClientResponse parses an HTTP response from a StartDurableExecutionWithResponse call +func ParseStartDurableExecutionClientResponse(rsp *http.Response) (*StartDurableExecutionClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &CreateDatabaseRestoreClientResponse{ + response := &StartDurableExecutionClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 202: - var dest DatabaseRestore + var dest DurableExecution if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } @@ -41361,26 +43822,33 @@ func ParseCreateDatabaseRestoreClientResponse(rsp *http.Response) (*CreateDataba } response.JSON400 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON403 = &dest + response.JSON404 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON404 = &dest + response.JSON409 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 413: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON409 = &dest + response.JSON413 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 429: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON429 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: var dest Error @@ -41394,34 +43862,27 @@ func ParseCreateDatabaseRestoreClientResponse(rsp *http.Response) (*CreateDataba return response, nil } -// ParseGetDatabaseRestoreClientResponse parses an HTTP response from a GetDatabaseRestoreWithResponse call -func ParseGetDatabaseRestoreClientResponse(rsp *http.Response) (*GetDatabaseRestoreClientResponse, error) { +// ParseGetDurableExecutionClientResponse parses an HTTP response from a GetDurableExecutionWithResponse call +func ParseGetDurableExecutionClientResponse(rsp *http.Response) (*GetDurableExecutionClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &GetDatabaseRestoreClientResponse{ + response := &GetDurableExecutionClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DatabaseRestore + var dest DurableExecution if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON403 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -41441,40 +43902,40 @@ func ParseGetDatabaseRestoreClientResponse(rsp *http.Response) (*GetDatabaseRest return response, nil } -// ParseGetDatabaseStatsClientResponse parses an HTTP response from a GetDatabaseStatsWithResponse call -func ParseGetDatabaseStatsClientResponse(rsp *http.Response) (*GetDatabaseStatsClientResponse, error) { +// ParseStopDurableExecutionClientResponse parses an HTTP response from a StopDurableExecutionWithResponse call +func ParseStopDurableExecutionClientResponse(rsp *http.Response) (*StopDurableExecutionClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &GetDatabaseStatsClientResponse{ + response := &StopDurableExecutionClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DatabaseStats + var dest DurableExecution if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON400 = &dest + response.JSON404 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON404 = &dest + response.JSON409 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 503: var dest Error @@ -41488,59 +43949,59 @@ func ParseGetDatabaseStatsClientResponse(rsp *http.Response) (*GetDatabaseStatsC return response, nil } -// ParseUpdateDatabaseTypeClientResponse parses an HTTP response from a UpdateDatabaseTypeWithResponse call -func ParseUpdateDatabaseTypeClientResponse(rsp *http.Response) (*UpdateDatabaseTypeClientResponse, error) { +// ParseListDurableFunctionSchedulersClientResponse parses an HTTP response from a ListDurableFunctionSchedulersWithResponse call +func ParseListDurableFunctionSchedulersClientResponse(rsp *http.Response) (*ListDurableFunctionSchedulersClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &UpdateDatabaseTypeClientResponse{ + response := &ListDurableFunctionSchedulersClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest Database + var dest FunctionSchedulerListResponse if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON409 = &dest + response.JSON404 = &dest } return response, nil } -// ParseListProjectDeploymentsClientResponse parses an HTTP response from a ListProjectDeploymentsWithResponse call -func ParseListProjectDeploymentsClientResponse(rsp *http.Response) (*ListProjectDeploymentsClientResponse, error) { +// ParseCreateDurableFunctionSchedulerClientResponse parses an HTTP response from a CreateDurableFunctionSchedulerWithResponse call +func ParseCreateDurableFunctionSchedulerClientResponse(rsp *http.Response) (*CreateDurableFunctionSchedulerClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ListProjectDeploymentsClientResponse{ + response := &CreateDurableFunctionSchedulerClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest PaginatedProjectDeployments + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest FunctionScheduler if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON200 = &dest + response.JSON201 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest Error @@ -41549,102 +44010,93 @@ func ParseListProjectDeploymentsClientResponse(rsp *http.Response) (*ListProject } response.JSON400 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON401 = &dest + response.JSON404 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON403 = &dest + } - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + return response, nil +} + +// ParseDeleteDurableFunctionSchedulerClientResponse parses an HTTP response from a DeleteDurableFunctionSchedulerWithResponse call +func ParseDeleteDurableFunctionSchedulerClientResponse(rsp *http.Response) (*DeleteDurableFunctionSchedulerClientResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteDurableFunctionSchedulerClientResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON500 = &dest + response.JSON404 = &dest } return response, nil } -// ParseSummarizeProjectDeploymentsClientResponse parses an HTTP response from a SummarizeProjectDeploymentsWithResponse call -func ParseSummarizeProjectDeploymentsClientResponse(rsp *http.Response) (*SummarizeProjectDeploymentsClientResponse, error) { +// ParseGetDurableFunctionSchedulerClientResponse parses an HTTP response from a GetDurableFunctionSchedulerWithResponse call +func ParseGetDurableFunctionSchedulerClientResponse(rsp *http.Response) (*GetDurableFunctionSchedulerClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &SummarizeProjectDeploymentsClientResponse{ + response := &GetDurableFunctionSchedulerClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ProjectDeploymentSummary + var dest FunctionScheduler if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON400 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON401 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON403 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON500 = &dest + response.JSON404 = &dest } return response, nil } -// ParseListProjectCustomDomainsClientResponse parses an HTTP response from a ListProjectCustomDomainsWithResponse call -func ParseListProjectCustomDomainsClientResponse(rsp *http.Response) (*ListProjectCustomDomainsClientResponse, error) { +// ParseUpdateDurableFunctionSchedulerClientResponse parses an HTTP response from a UpdateDurableFunctionSchedulerWithResponse call +func ParseUpdateDurableFunctionSchedulerClientResponse(rsp *http.Response) (*UpdateDurableFunctionSchedulerClientResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ListProjectCustomDomainsClientResponse{ + response := &UpdateDurableFunctionSchedulerClientResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest PaginatedProjectCustomDomains + var dest FunctionScheduler if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } @@ -41657,26 +44109,12 @@ func ParseListProjectCustomDomainsClientResponse(rsp *http.Response) (*ListProje } response.JSON400 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON401 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: - var dest Error - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON403 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: var dest Error if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON500 = &dest + response.JSON404 = &dest } diff --git a/internal/cmd/cloud/cloud.go b/internal/cmd/cloud/cloud.go index 24558a83..f2a6862a 100644 --- a/internal/cmd/cloud/cloud.go +++ b/internal/cmd/cloud/cloud.go @@ -7,6 +7,7 @@ import ( "github.com/Kong/volcano-cli/internal/cmd/cmdutil" configcmd "github.com/Kong/volcano-cli/internal/cmd/config" databasescmd "github.com/Kong/volcano-cli/internal/cmd/databases" + durablecmd "github.com/Kong/volcano-cli/internal/cmd/durable" frontendscmd "github.com/Kong/volcano-cli/internal/cmd/frontends" functionscmd "github.com/Kong/volcano-cli/internal/cmd/functions" storagecmd "github.com/Kong/volcano-cli/internal/cmd/storage" @@ -38,6 +39,7 @@ func NewResourceCommands(deps cliruntime.Deps) []*cobra.Command { return []*cobra.Command{ configcmd.New(deps), databasescmd.New(deps), + durablecmd.New(deps), frontendscmd.New(deps), functionscmd.NewWithOptions(deps, functionscmd.WithInvokeTokenProvider(dataPlaneKeys.ServiceKeyForProject)), storagecmd.NewWithOptions(deps, storagecmd.WithObjectTokenProvider(dataPlaneKeys.ServiceKey)), diff --git a/internal/cmd/cmdutil/jsonarg.go b/internal/cmd/cmdutil/jsonarg.go new file mode 100644 index 00000000..19f60f6a --- /dev/null +++ b/internal/cmd/cmdutil/jsonarg.go @@ -0,0 +1,28 @@ +package cmdutil + +import ( + "encoding/json" + "fmt" + "os" +) + +// ParseJSONObject reads a flag value that carries a JSON object, either inline +// or as the path to a file holding it. subject names the flag in errors, so +// they read as "payload must be a JSON object" or "input must be a JSON +// object". +func ParseJSONObject(subject, value string) (map[string]any, error) { + data := []byte(value) + if info, err := os.Stat(value); err == nil && !info.IsDir() { + fileBytes, readErr := os.ReadFile(value) + if readErr != nil { + return nil, fmt.Errorf("failed to read %s file %q: %w", subject, value, readErr) + } + data = fileBytes + } + + var object map[string]any + if err := json.Unmarshal(data, &object); err != nil { + return nil, fmt.Errorf("%s must be a JSON object: %w", subject, err) + } + return object, nil +} diff --git a/internal/cmd/durable/delete.go b/internal/cmd/durable/delete.go new file mode 100644 index 00000000..94e4f9d8 --- /dev/null +++ b/internal/cmd/durable/delete.go @@ -0,0 +1,80 @@ +package durable + +import ( + "context" + "fmt" + "io" + "strings" + + "github.com/spf13/cobra" + + "github.com/Kong/volcano-cli/internal/confirm" + clidurable "github.com/Kong/volcano-cli/internal/durable" + "github.com/Kong/volcano-cli/internal/output" + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +type deleteOptions struct { + deps cliruntime.Deps + identifier string + yes bool + in io.Reader + out io.Writer +} + +func newDelete(deps cliruntime.Deps) *cobra.Command { + var yes bool + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a durable function", + Long: `Delete a durable function from the current project. + +Teardown continues after the command returns, and it takes the function's +execution history with it. Stop an execution you need to end first, since +deleting the function does not wait for work already in flight. + +By default this command prompts for confirmation. +Use --yes to skip the prompt.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runDelete(cmd.Context(), deleteOptions{ + deps: deps, + identifier: strings.TrimSpace(args[0]), + yes: yes, + in: cmd.InOrStdin(), + out: cmd.OutOrStdout(), + }) + }, + } + cmd.Flags().BoolVarP(&yes, "yes", "y", false, "Skip confirmation prompt") + return cmd +} + +func runDelete(ctx context.Context, opts deleteOptions) error { + service := clidurable.NewService(opts.deps) + // Read first so the prompt names the function rather than whatever the user + // typed, and so a name that belongs to a standard function is refused before + // anything is torn down. + function, err := service.Get(ctx, opts.identifier) + if err != nil { + return err + } + + if !opts.yes { + confirmed, err := confirm.Delete(opts.in, opts.out, "durable function", function.Name) + if err != nil { + return err + } + if !confirmed { + return nil + } + } + + if err := service.Delete(ctx, function.Id.String()); err != nil { + return err + } + + output.Success(opts.out, "Durable function '%s' deletion started", function.Name) + fmt.Fprintln(opts.out, `Status will be "deleting" until cleanup finishes; afterwards the function and its executions will no longer appear in list/get responses.`) + return nil +} diff --git a/internal/cmd/durable/deploy.go b/internal/cmd/durable/deploy.go new file mode 100644 index 00000000..edc91ed0 --- /dev/null +++ b/internal/cmd/durable/deploy.go @@ -0,0 +1,240 @@ +package durable + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/spf13/cobra" + + "github.com/Kong/volcano-cli/internal/apiclient" + "github.com/Kong/volcano-cli/internal/archive" + clidurable "github.com/Kong/volcano-cli/internal/durable" + clifunction "github.com/Kong/volcano-cli/internal/function" + "github.com/Kong/volcano-cli/internal/output" + "github.com/Kong/volcano-cli/internal/projectconfig" + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +type deployOptions struct { + deps cliruntime.Deps + file string + all bool + public bool + private bool + out io.Writer +} + +func newDeploy(deps cliruntime.Deps) *cobra.Command { + opts := deployOptions{} + cmd := &cobra.Command{ + Use: "deploy", + Short: "Deploy durable functions", + Long: fmt.Sprintf(`Deploy durable functions from the volcano/functions directory. + +Durable function sources live alongside standard ones; what makes a function +durable is its kind, which volcano-config.yaml declares and which cannot be +changed once the function exists: + + functions: + - name: order-pipeline + kind: durable + +%s deploys every function the manifest declares durable. %s deploys +one by name or path, whether or not the manifest mentions it. + +Deploying over an existing durable function redeploys it. Executions already +running continue on the version they started on.`, + cliruntime.CommandPath(deps, "durable deploy --all"), + cliruntime.CommandPath(deps, "durable deploy -f order-pipeline")), + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + opts.deps = deps + opts.file = strings.TrimSpace(opts.file) + opts.out = cmd.OutOrStdout() + return runDeploy(cmd.Context(), opts) + }, + } + cmd.Flags().StringVarP(&opts.file, "file", "f", "", "Deploy a specific durable function by name or path") + cmd.Flags().BoolVarP(&opts.all, "all", "a", false, "Deploy every function volcano-config.yaml declares durable") + cmd.Flags().BoolVar(&opts.public, "public", false, "Let anon keys start executions of this function") + cmd.Flags().BoolVar(&opts.private, "private", false, "Stop anon keys from starting executions of this function") + return cmd +} + +func runDeploy(ctx context.Context, opts deployOptions) error { + if opts.public && opts.private { + return errors.New("cannot use --public and --private together") + } + visibility := deployVisibility(opts) + targets, err := deployTargets(opts) + if err != nil { + return err + } + // The same manifest read the standard deploy does. Without it a durable + // function the manifest scopes is created with every project variable, since + // the API reads a declaration only from the deploy that sends one. + manifest, err := projectconfig.ReadFunctionDeployManifest("") + if err != nil { + return err + } + + service := clidurable.NewService(opts.deps) + sources, baseDir, err := durableSources(ctx, opts, targets) + if err != nil { + return err + } + + for i, source := range sources { + fmt.Fprintf(opts.out, "\n[%d/%d] Deploying %s...\n", i+1, len(sources), source.Name) + if err := deployOne( + ctx, opts.out, service, baseDir, source, visibility, manifest.Declarations, + ); err != nil { + return err + } + } + fmt.Fprintln(opts.out) + output.Success(opts.out, "%d/%d durable function(s) deployment started", len(sources), len(sources)) + fmt.Fprintf(opts.out, "Follow the rollout with %s\n", cliruntime.CommandPath(opts.deps, "durable get "+sources[0].Name)) + return nil +} + +func deployOne( + ctx context.Context, + out io.Writer, + service clidurable.Service, + baseDir string, + source clifunction.SourceInfo, + visibility *bool, + declarations map[string]projectconfig.FunctionVariableDeclaration, +) error { + fmt.Fprintf(out, " Runtime: %s\n", source.Runtime.Name) + fmt.Fprintf(out, " Function code: %s\n", source.Path) + pkg, err := clifunction.PackageSource(source, baseDir) + if err != nil { + return fmt.Errorf("failed to package durable function %s: %w", source.Name, err) + } + if declaration, ok := declarations[pkg.Name]; ok { + pkg.VariableScope = declaration.VariableScope + pkg.Variables = declaration.Variables + } + fmt.Fprintf(out, " Archive size: %s\n", archive.FormatSize(pkg.Size)) + + deployed, err := service.Deploy(ctx, *pkg, visibility) + if err != nil { + return err + } + fmt.Fprintf(out, " Deployed %s (%s)\n", deployed.Name, string(deployed.Status)) + return nil +} + +// deployVisibility turns the two flags into the field the API takes, where an +// absent value keeps the deployed function's current visibility. A durable +// function has no update endpoint, so redeploying is the only way to change it. +func deployVisibility(opts deployOptions) *bool { + switch { + case opts.public: + public := true + return &public + case opts.private: + private := false + return &private + default: + return nil + } +} + +// deployTargets resolves which functions to deploy: the one named by --file, or +// every function the local manifest declares durable. +func deployTargets(opts deployOptions) ([]string, error) { + if opts.all && opts.file != "" { + return nil, errors.New("cannot use --all and --file together") + } + if !opts.all && opts.file == "" { + return nil, errors.New("specify either --all to deploy every durable function declared in volcano-config.yaml, or --file/-f to deploy a specific one") + } + if opts.file != "" { + return []string{opts.file}, nil + } + + manifestPath, err := projectconfig.ResolveManifestPath("") + if err != nil { + return nil, err + } + manifest, _, err := projectconfig.Load(manifestPath) + if err != nil { + return nil, err + } + names := manifest.DurableFunctionNames() + if len(names) == 0 { + return nil, errors.New("volcano-config.yaml declares no durable functions; add 'kind: durable' to a function entry, or deploy one with --file/-f") + } + return names, nil +} + +// durableSources packages the scanned sources for the requested targets, +// keeping the target order so the progress lines match what the user asked for. +func durableSources( + ctx context.Context, + opts deployOptions, + targets []string, +) ([]clifunction.SourceInfo, string, error) { + baseDir, err := os.Getwd() + if err != nil { + return nil, "", fmt.Errorf("failed to get current directory: %w", err) + } + + runtimes, err := clifunction.NewService(opts.deps).ListRuntimes(ctx) + if err != nil { + return nil, "", err + } + fmt.Fprintln(opts.out, "\nScanning volcano/functions/...") + scanned, err := clifunction.ScanSources(baseDir, clifunction.RuntimeCatalogFromOptions(runtimes)) + if err != nil { + return nil, "", fmt.Errorf("failed to scan functions: %w", err) + } + + sources := make([]clifunction.SourceInfo, 0, len(targets)) + for _, target := range targets { + source, found := matchSource(scanned, target, baseDir) + if !found { + return nil, "", fmt.Errorf("function %q not found in volcano/functions/\navailable functions: %s", + target, clifunction.FormatSourceNames(scanned)) + } + if err := checkDurableRuntime(source, runtimes); err != nil { + return nil, "", err + } + sources = append(sources, source) + } + return sources, baseDir, nil +} + +// checkDurableRuntime refuses a source whose runtime has no durable authoring +// API before anything is packaged or uploaded. The API rejects it too, but only +// after the archive has been built and sent, and the message there cannot name +// the file the runtime was inferred from. +func checkDurableRuntime(source clifunction.SourceInfo, runtimes []apiclient.FunctionRuntimeOption) error { + if source.Runtime.DurableCapable { + return nil + } + var capable []string + for _, runtime := range runtimes { + if runtime.DurableCapable { + capable = append(capable, runtime.Name) + } + } + return fmt.Errorf("durable functions cannot run on %s, which is the runtime for %s\ndurable runtimes: %s", + source.Runtime.Name, source.Path, strings.Join(capable, ", ")) +} + +func matchSource(sources []clifunction.SourceInfo, target, baseDir string) (clifunction.SourceInfo, bool) { + for _, source := range sources { + if clifunction.MatchesTarget(source, target, baseDir) { + return source, true + } + } + return clifunction.SourceInfo{}, false +} diff --git a/internal/cmd/durable/durable.go b/internal/cmd/durable/durable.go new file mode 100644 index 00000000..7cb8df98 --- /dev/null +++ b/internal/cmd/durable/durable.go @@ -0,0 +1,56 @@ +// Package durable wires the volcano durable subcommands. +package durable + +import ( + "errors" + + "github.com/spf13/cobra" + + executionscmd "github.com/Kong/volcano-cli/internal/cmd/durable/executions" + schedulerscmd "github.com/Kong/volcano-cli/internal/cmd/durable/schedulers" + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +// New returns the durable functions command. It belongs to the cloud tree +// only; NewCloudOnly stands in for it locally. +func New(deps cliruntime.Deps) *cobra.Command { + cmd := &cobra.Command{ + Use: "durable", + Short: "Manage durable functions", + Long: `Durable functions checkpoint their progress and resume where they left off, +so one execution can run far longer than a single invocation is allowed. + +They are a separate collection from standard functions: a durable function is +started rather than invoked, its executions are tracked resources, and the two +collections never accept each other's names or ids.`, + } + cmd.AddCommand(newDeploy(deps)) + cmd.AddCommand(newList(deps)) + cmd.AddCommand(newGet(deps)) + cmd.AddCommand(newStart(deps)) + cmd.AddCommand(newDelete(deps)) + cmd.AddCommand(executionscmd.New(deps)) + cmd.AddCommand(schedulerscmd.New(deps)) + return cmd +} + +// NewCloudOnly stands in for the durable command in the local tree. Durable +// execution is checkpoint-and-replay run by the platform's regional engine, and +// the local server refuses to create a durable function rather than pretending +// to run one, so there is nothing to point the command at. +// +// Without the stub, cobra answers an unknown subcommand by printing the root's +// help and exiting 0, which reads as if the command had run. Hidden so local +// help still lists only what local mode can do, and flag parsing disabled so +// `durable deploy --all` reaches it rather than failing on an unknown flag. +func NewCloudOnly() *cobra.Command { + return &cobra.Command{ + Use: "durable", + Hidden: true, + DisableFlagParsing: true, + RunE: func(_ *cobra.Command, _ []string) error { + return errors.New(`"durable" is a cloud command: local development does not run durable ` + + `executions, so run 'volcano cloud durable ...' against a cloud project`) + }, + } +} diff --git a/internal/cmd/durable/durable_test.go b/internal/cmd/durable/durable_test.go new file mode 100644 index 00000000..372f5952 --- /dev/null +++ b/internal/cmd/durable/durable_test.go @@ -0,0 +1,511 @@ +package durable + +import ( + "bytes" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +func TestDurableListPopulatedAndEmpty(t *testing.T) { + for _, tc := range []struct { + name string + body map[string]any + want []string + }{ + { + name: "populated", + body: map[string]any{ + "data": []any{durableFunctionPayload("order-pipeline", false)}, + "has_more": true, + "page": 1, + "limit": 100, + "total": 2, + }, + want: []string{ + "order-pipeline", + "nodejs24.x", + "active", + "Showing 1 of 2 durable function(s) (page 1, limit 100)", + "Next page: volcano cloud durable list --page 2 --limit 100", + }, + }, + { + name: "empty", + body: map[string]any{ + "data": []any{}, + "has_more": false, + "page": 1, + "limit": 100, + "total": 0, + }, + want: []string{ + "No durable functions deployed", + "Showing 0 of 0 durable function(s) (page 1, limit 100)", + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + setDurableCommandTestHome(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "Bearer token", r.Header.Get("Authorization")) + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "/projects/"+durableProjectID+"/durable-functions", r.URL.Path) + assert.Equal(t, "page=1&limit=100", r.URL.RawQuery) + writeDurableCommandJSON(t, w, http.StatusOK, tc.body) + })) + defer server.Close() + + out, err := executeDurableCommand(t, newCloudDurableCommand(server), "list") + require.NoError(t, err) + for _, want := range tc.want { + assert.Contains(t, out, want) + } + }) + } +} + +// A durable function's visibility governs whether an anon key may start an +// execution, and nothing else: it is never invocable over HTTP the way a public +// standard function is, so the field is labeled for what it does. +func TestDurableGetRendersAnonKeyStart(t *testing.T) { + for _, tc := range []struct { + name string + isPublic bool + want string + }{ + {name: "public", isPublic: true, want: "Anon Key Start: allowed"}, + {name: "private", isPublic: false, want: "Anon Key Start: denied"}, + } { + t.Run(tc.name, func(t *testing.T) { + setDurableCommandTestHome(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/projects/"+durableProjectID+"/durable-functions/order-pipeline", r.URL.Path) + writeDurableCommandJSON(t, w, http.StatusOK, durableFunctionPayload("order-pipeline", tc.isPublic)) + })) + defer server.Close() + + out, err := executeDurableCommand(t, newCloudDurableCommand(server), "get", "order-pipeline") + require.NoError(t, err) + assert.Contains(t, out, tc.want) + assert.Contains(t, out, "Execution Timeout: 1h0m0s") + assert.Contains(t, out, "Retention: 30 day(s)") + }) + } +} + +// The API answers 404 for a standard function's name here, because the two +// collections never accept each other's names. The message has to say which +// collection was searched, or a user who mixed the two reads it as a name typo. +func TestDurableGetNamesTheCollectionOnNotFound(t *testing.T) { + setDurableCommandTestHome(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + writeDurableCommandJSON(t, w, http.StatusNotFound, map[string]any{"error": "durable function not found"}) + })) + defer server.Close() + + _, err := executeDurableCommand(t, newCloudDurableCommand(server), "get", "hello") + require.ErrorContains(t, err, `durable function "hello" not found`) +} + +func TestDurableStartSendsInputAndIdempotencyName(t *testing.T) { + setDurableCommandTestHome(t) + var body map[string]any + var executionName string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, "/projects/"+durableProjectID+"/durable-functions/order-pipeline/executions", r.URL.Path) + executionName = r.Header.Get("X-Volcano-Execution-Name") + require.NoError(t, decodeJSONBody(r, &body)) + writeDurableCommandJSON(t, w, http.StatusAccepted, + durableExecutionPayload(durableExecutionA, "order-4417", "pending")) + })) + defer server.Close() + + out, err := executeDurableCommand(t, newCloudDurableCommand(server), + "start", "order-pipeline", "--input", `{"order_id":4417}`, "--name", "order-4417") + require.NoError(t, err) + assert.Equal(t, map[string]any{"order_id": float64(4417)}, body) + assert.Equal(t, "order-4417", executionName) + assert.Contains(t, out, "Execution order-4417 started") + assert.Contains(t, out, "volcano cloud durable executions get order-pipeline "+durableExecutionA) +} + +func TestDurableStartReadsInputFromFile(t *testing.T) { + setDurableCommandTestHome(t) + t.Chdir(t.TempDir()) + writeDurableProjectFile(t, "order.json", `{"order_id": 99}`) + + var body map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, decodeJSONBody(r, &body)) + writeDurableCommandJSON(t, w, http.StatusAccepted, + durableExecutionPayload(durableExecutionA, "generated", "pending")) + })) + defer server.Close() + + _, err := executeDurableCommand(t, newCloudDurableCommand(server), + "start", "order-pipeline", "--input", "order.json") + require.NoError(t, err) + assert.Equal(t, map[string]any{"order_id": float64(99)}, body) +} + +func TestDurableStartRejectsInputThatIsNotAJSONObject(t *testing.T) { + setDurableCommandTestHome(t) + _, err := executeDurableCommand(t, newCloudDurableCommand(nil), + "start", "order-pipeline", "--input", "not-json") + require.ErrorContains(t, err, "input must be a JSON object") +} + +// Deleting reads the function first, so the prompt names what is about to go +// and a name belonging to a standard function is refused before anything is +// torn down. +func TestDurableDeleteConfirmsAgainstTheResolvedFunction(t *testing.T) { + setDurableCommandTestHome(t) + deleted := false + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + path := "/projects/" + durableProjectID + "/durable-functions/" + switch { + case r.Method == http.MethodGet && r.URL.Path == path+"order-pipeline": + writeDurableCommandJSON(t, w, http.StatusOK, durableFunctionPayload("order-pipeline", false)) + case r.Method == http.MethodDelete && r.URL.Path == path+durableFunctionID: + deleted = true + w.WriteHeader(http.StatusNoContent) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + out, err := executeDurableCommand(t, newCloudDurableCommand(server), "delete", "order-pipeline") + require.NoError(t, err) + assert.True(t, deleted) + assert.Contains(t, out, "Delete durable function 'order-pipeline'?") + assert.Contains(t, out, "Durable function 'order-pipeline' deletion started") +} + +func TestDurableDeployRefusesConflictingVisibilityFlags(t *testing.T) { + setDurableCommandTestHome(t) + _, err := executeDurableCommand(t, newCloudDurableCommand(nil), + "deploy", "-f", "order-pipeline", "--public", "--private") + require.ErrorContains(t, err, "cannot use --public and --private together") +} + +func TestDurableDeployRequiresATarget(t *testing.T) { + setDurableCommandTestHome(t) + _, err := executeDurableCommand(t, newCloudDurableCommand(nil), "deploy") + require.ErrorContains(t, err, "specify either --all") + + _, err = executeDurableCommand(t, newCloudDurableCommand(nil), "deploy", "--all", "-f", "order-pipeline") + require.ErrorContains(t, err, "cannot use --all and --file together") +} + +func TestDurableDeployUploadsSourceAndVisibility(t *testing.T) { + setDurableCommandTestHome(t) + t.Chdir(t.TempDir()) + writeDurableProjectFile(t, "volcano/functions/order-pipeline.js", + `exports.handler = async () => ({ ok: true });`) + + var fields map[string]string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case writeDurableRuntimesResponse(t, w, r): + return + case r.Method == http.MethodPost && r.URL.Path == "/projects/"+durableProjectID+"/durable-functions": + require.NoError(t, r.ParseMultipartForm(4*1024*1024)) + fields = map[string]string{ + "name": r.FormValue("name"), + "runtime": r.FormValue("runtime"), + "handler": r.FormValue("handler"), + "is_public": r.FormValue("is_public"), + } + require.Len(t, r.MultipartForm.File["code"], 1) + writeDurableCommandJSON(t, w, http.StatusCreated, durableFunctionPayload("order-pipeline", true)) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + out, err := executeDurableCommand(t, newCloudDurableCommand(server), + "deploy", "-f", "volcano/functions/order-pipeline.js", "--public") + require.NoError(t, err) + assert.Equal(t, map[string]string{ + "name": "order-pipeline", "runtime": "nodejs24.x", "handler": "handler", "is_public": "true", + }, fields) + assert.Contains(t, out, "Deploying order-pipeline") + assert.Contains(t, out, "1/1 durable function(s) deployment started") +} + +// Durable execution needs the durable authoring API, so only some runtimes +// qualify. Refusing before the archive is built keeps the message able to name +// the file the runtime was inferred from, which the API's rejection cannot. +func TestDurableDeployRefusesARuntimeWithoutDurableSupport(t *testing.T) { + setDurableCommandTestHome(t) + t.Chdir(t.TempDir()) + writeDurableProjectFile(t, "volcano/functions/order-pipeline.py", "def handler(event, context):\n return {}\n") + + var deployed bool + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case writeDurableRuntimesResponse(t, w, r): + return + case r.Method == http.MethodPost: + deployed = true + writeDurableCommandJSON(t, w, http.StatusCreated, durableFunctionPayload("order-pipeline", false)) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + _, err := executeDurableCommand(t, newCloudDurableCommand(server), "deploy", "-f", "order-pipeline") + require.Error(t, err) + assert.Contains(t, err.Error(), "durable functions cannot run on python3.12") + assert.Contains(t, err.Error(), "order-pipeline.py") + assert.Contains(t, err.Error(), "durable runtimes: nodejs24.x") + assert.False(t, deployed, "nothing should be uploaded once the runtime is refused") +} + +// --private is the way back from public, and a durable function has no update +// endpoint, so a redeploy that sends nothing instead of false would make the +// flag unusable. +func TestDurableDeploySendsPrivateVisibility(t *testing.T) { + setDurableCommandTestHome(t) + t.Chdir(t.TempDir()) + writeDurableProjectFile(t, "volcano/functions/order-pipeline.js", + `exports.handler = async () => ({ ok: true });`) + + var sentIsPublic string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case writeDurableRuntimesResponse(t, w, r): + return + case r.Method == http.MethodPost && r.URL.Path == "/projects/"+durableProjectID+"/durable-functions": + require.NoError(t, r.ParseMultipartForm(4*1024*1024)) + sentIsPublic = r.FormValue("is_public") + writeDurableCommandJSON(t, w, http.StatusOK, durableFunctionPayload("order-pipeline", false)) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + out, err := executeDurableCommand(t, newCloudDurableCommand(server), + "deploy", "-f", "order-pipeline", "--private") + require.NoError(t, err) + assert.Equal(t, "false", sentIsPublic) + assert.Contains(t, out, "1/1 durable function(s) deployment started") +} + +// Omitting both visibility flags sends no is_public field, which the API reads +// as "keep what the function has". A redeploy must not silently make a public +// function private. +func TestDurableDeployLeavesVisibilityAloneByDefault(t *testing.T) { + setDurableCommandTestHome(t) + t.Chdir(t.TempDir()) + writeDurableProjectFile(t, "volcano/functions/order-pipeline.js", + `exports.handler = async () => ({ ok: true });`) + + var sentIsPublic []string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case writeDurableRuntimesResponse(t, w, r): + return + case r.Method == http.MethodPost && r.URL.Path == "/projects/"+durableProjectID+"/durable-functions": + require.NoError(t, r.ParseMultipartForm(4*1024*1024)) + sentIsPublic = r.MultipartForm.Value["is_public"] + writeDurableCommandJSON(t, w, http.StatusOK, durableFunctionPayload("order-pipeline", true)) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + _, err := executeDurableCommand(t, newCloudDurableCommand(server), + "deploy", "-f", "order-pipeline") + require.NoError(t, err) + assert.Empty(t, sentIsPublic) +} + +// --all takes its targets from the manifest rather than from the scan, because +// the source tree does not say which functions are durable. +func TestDurableDeployAllTakesTargetsFromTheManifest(t *testing.T) { + setDurableCommandTestHome(t) + t.Chdir(t.TempDir()) + writeDurableProjectFile(t, "volcano/functions/order-pipeline.js", `exports.handler = async () => ({});`) + writeDurableProjectFile(t, "volcano/functions/hello.js", `exports.handler = async () => ({});`) + writeDurableProjectFile(t, "volcano-config.yaml", `version: 1 +project: + name: beta +functions: + - name: hello + - name: order-pipeline + kind: durable +`) + + var deployed []string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case writeDurableRuntimesResponse(t, w, r): + return + case r.Method == http.MethodPost && r.URL.Path == "/projects/"+durableProjectID+"/durable-functions": + require.NoError(t, r.ParseMultipartForm(4*1024*1024)) + deployed = append(deployed, r.FormValue("name")) + writeDurableCommandJSON(t, w, http.StatusCreated, durableFunctionPayload("order-pipeline", false)) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + out, err := executeDurableCommand(t, newCloudDurableCommand(server), "deploy", "--all") + require.NoError(t, err) + assert.Equal(t, []string{"order-pipeline"}, deployed, "a standard function must not be deployed as durable") + assert.Contains(t, out, "1/1 durable function(s) deployment started") +} + +// The manifest is the only place a durable function's variable scope can be +// declared, and the API reads a declaration only from the deploy that sends +// one. Left out, a durable function the manifest scopes is created with every +// project variable in its environment — the widest possible reading of an entry +// that asked for the narrowest. +func TestDurableDeploySendsTheManifestVariableScope(t *testing.T) { + setDurableCommandTestHome(t) + t.Chdir(t.TempDir()) + writeDurableProjectFile(t, "volcano/functions/order-pipeline.js", `exports.handler = async () => ({});`) + writeDurableProjectFile(t, "volcano-config.yaml", `version: 1 +project: + name: beta +functions: + - name: order-pipeline + kind: durable + variable_scope: scoped + variables: + - STRIPE_SECRET +`) + + var fields map[string]string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case writeDurableRuntimesResponse(t, w, r): + return + case r.Method == http.MethodPost && r.URL.Path == "/projects/"+durableProjectID+"/durable-functions": + require.NoError(t, r.ParseMultipartForm(4*1024*1024)) + fields = map[string]string{ + "variable_scope": r.FormValue("variable_scope"), + "variables": r.FormValue("variables"), + } + writeDurableCommandJSON(t, w, http.StatusCreated, durableFunctionPayload("order-pipeline", false)) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + _, err := executeDurableCommand(t, newCloudDurableCommand(server), "deploy", "--all") + require.NoError(t, err) + assert.Equal(t, map[string]string{ + "variable_scope": "scoped", + "variables": `["STRIPE_SECRET"]`, + }, fields) +} + +// And a manifest that declares no scope sends neither field, which is what +// leaves an existing function's scope alone on redeploy. +func TestDurableDeployWithoutADeclarationSendsNoScope(t *testing.T) { + setDurableCommandTestHome(t) + t.Chdir(t.TempDir()) + writeDurableProjectFile(t, "volcano/functions/order-pipeline.js", `exports.handler = async () => ({});`) + writeDurableProjectFile(t, "volcano-config.yaml", `version: 1 +project: + name: beta +functions: + - name: order-pipeline + kind: durable +`) + + var form map[string][]string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case writeDurableRuntimesResponse(t, w, r): + return + case r.Method == http.MethodPost && r.URL.Path == "/projects/"+durableProjectID+"/durable-functions": + require.NoError(t, r.ParseMultipartForm(4*1024*1024)) + form = r.MultipartForm.Value + writeDurableCommandJSON(t, w, http.StatusCreated, durableFunctionPayload("order-pipeline", false)) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + _, err := executeDurableCommand(t, newCloudDurableCommand(server), "deploy", "--all") + require.NoError(t, err) + assert.NotContains(t, form, "variable_scope") + assert.NotContains(t, form, "variables") +} + +func TestDurableDeployAllWithoutADurableDeclaration(t *testing.T) { + setDurableCommandTestHome(t) + t.Chdir(t.TempDir()) + writeDurableProjectFile(t, "volcano-config.yaml", `version: 1 +project: + name: beta +functions: + - name: hello +`) + + _, err := executeDurableCommand(t, newCloudDurableCommand(nil), "deploy", "--all") + require.ErrorContains(t, err, "declares no durable functions") +} + +func newCloudDurableCommand(server *httptest.Server) *cobra.Command { + deps := cliruntime.Deps{CommandPathPrefix: "volcano cloud"} + if server != nil { + deps.HTTPClient = server.Client() + deps.APIBaseURL = server.URL + } + return New(deps) +} + +// Local mode has no durable engine behind the command. Cobra answers an unknown +// subcommand by printing help and exiting 0, so the stub is what makes the +// refusal legible — and it stays hidden, or local help would advertise a +// capability local mode does not have. +func TestDurableIsRefusedInTheLocalTree(t *testing.T) { + for _, args := range [][]string{ + {"durable", "deploy", "--all"}, + {"durable", "list"}, + {"durable", "start", "order-pipeline", "--input", "{}"}, + {"durable", "executions", "list", "order-pipeline"}, + } { + t.Run(strings.Join(args, " "), func(t *testing.T) { + root := &cobra.Command{Use: "volcano"} + root.AddCommand(NewCloudOnly()) + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs(args) + + err := root.Execute() + require.ErrorContains(t, err, `"durable" is a cloud command`) + require.ErrorContains(t, err, "volcano cloud durable") + }) + } + + root := &cobra.Command{Use: "volcano"} + root.AddCommand(NewCloudOnly()) + var help bytes.Buffer + root.SetOut(&help) + root.SetArgs([]string{"--help"}) + require.NoError(t, root.Execute()) + assert.NotContains(t, help.String(), "durable") +} diff --git a/internal/cmd/durable/executions/executions.go b/internal/cmd/durable/executions/executions.go new file mode 100644 index 00000000..19253529 --- /dev/null +++ b/internal/cmd/durable/executions/executions.go @@ -0,0 +1,23 @@ +// Package executions wires the volcano durable executions subcommands. +package executions + +import ( + "github.com/spf13/cobra" + + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +// New returns the durable executions command. +func New(deps cliruntime.Deps) *cobra.Command { + cmd := &cobra.Command{ + Use: "executions", + Short: "Inspect durable executions", + Long: `An execution is one run of a durable function, and a resource of its own: +it has a name that makes a repeated start idempotent, a region it is pinned to, +and a result that is kept for as long as the function's retention allows.`, + } + cmd.AddCommand(newList(deps)) + cmd.AddCommand(newGet(deps)) + cmd.AddCommand(newStop(deps)) + return cmd +} diff --git a/internal/cmd/durable/executions/executions_test.go b/internal/cmd/durable/executions/executions_test.go new file mode 100644 index 00000000..8f465175 --- /dev/null +++ b/internal/cmd/durable/executions/executions_test.go @@ -0,0 +1,313 @@ +package executions + +import ( + "bytes" + "encoding/json" + "maps" + "net/http" + "net/http/httptest" + "strings" + "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 ( + projectID = "22222222-2222-4222-8222-222222222222" + executionID = "66666666-6666-4666-8666-666666666666" +) + +func TestExecutionsListPopulatedAndEmpty(t *testing.T) { + for _, tc := range []struct { + name string + body map[string]any + query string + args []string + want []string + }{ + { + name: "populated", + body: map[string]any{ + "data": []any{ + executionPayload(executionID, "order-4417", "running"), + }, + "has_more": true, + "page": 1, + "limit": 100, + "total": 2, + }, + query: "page=1&limit=100", + args: []string{"list", "order-pipeline"}, + want: []string{ + executionID, + "order-4417", + "running", + "Showing 1 of 2 execution(s) (page 1, limit 100)", + "Next page: volcano cloud durable executions list order-pipeline --page 2 --limit 100", + }, + }, + { + name: "filtered and empty", + body: map[string]any{ + "data": []any{}, + "has_more": false, + "page": 1, + "limit": 100, + "total": 0, + }, + query: "page=1&limit=100&status=succeeded", + args: []string{"list", "order-pipeline", "--status", "succeeded"}, + want: []string{ + `No executions started for durable function "order-pipeline"`, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + setExecutionsTestHome(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "/projects/"+projectID+"/durable-functions/order-pipeline/executions", r.URL.Path) + assert.ElementsMatch(t, splitQuery(tc.query), splitQuery(r.URL.RawQuery)) + writeJSON(t, w, http.StatusOK, tc.body) + })) + defer server.Close() + + out, err := executeCommand(t, newExecutionsCommand(server), tc.args...) + require.NoError(t, err) + for _, want := range tc.want { + assert.Contains(t, out, want) + } + }) + } +} + +// Reading an execution is what refreshes its status, and a result that is past +// retention has to read as gone rather than as an execution that produced +// nothing. +func TestExecutionsGetRendersOutcome(t *testing.T) { + for _, tc := range []struct { + name string + payload map[string]any + want []string + wantMissing []string + }{ + { + name: "succeeded with a result", + payload: withFields(executionPayload(executionID, "order-4417", "succeeded"), map[string]any{ + "completed_at": "2026-05-20T00:05:00Z", + "result": map[string]any{"shipped": true}, + }), + want: []string{"Status: succeeded", "Duration: 5m0s", `"shipped": true`}, + }, + { + name: "result no longer retained", + payload: withFields(executionPayload(executionID, "order-4417", "succeeded"), map[string]any{ + "completed_at": "2026-05-20T00:05:00Z", + "result_expired": true, + }), + want: []string{"Result: no longer retained"}, + }, + { + name: "failed", + payload: withFields(executionPayload(executionID, "order-4417", "failed"), map[string]any{ + "completed_at": "2026-05-20T00:05:00Z", + "error": map[string]any{"type": "Error", "message": "charge declined"}, + }), + want: []string{"Error Type: Error", "Error: charge declined"}, + wantMissing: []string{"Result:"}, + }, + { + name: "still running", + payload: executionPayload(executionID, "order-4417", "running"), + want: []string{"Status: running"}, + wantMissing: []string{"Result:", "Completed:"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + setExecutionsTestHome(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, + "/projects/"+projectID+"/durable-functions/order-pipeline/executions/"+executionID, + r.URL.Path) + writeJSON(t, w, http.StatusOK, tc.payload) + })) + defer server.Close() + + out, err := executeCommand(t, newExecutionsCommand(server), "get", "order-pipeline", executionID) + require.NoError(t, err) + for _, want := range tc.want { + assert.Contains(t, out, want) + } + for _, missing := range tc.wantMissing { + assert.NotContains(t, out, missing) + } + }) + } +} + +// An execution is addressed by id. Its name is only an idempotency key and is +// not unique over time, so a name has to be refused here rather than sent to a +// route that would read it as an id. +func TestExecutionsRefuseANameWhereAnIDIsRequired(t *testing.T) { + for _, args := range [][]string{ + {"get", "order-pipeline", "order-4417"}, + {"stop", "order-pipeline", "order-4417", "--yes"}, + } { + t.Run(args[0], func(t *testing.T) { + setExecutionsTestHome(t) + _, err := executeCommand(t, newExecutionsCommand(nil), args...) + require.ErrorContains(t, err, `invalid execution ID "order-4417"`) + }) + } +} + +// An execution route answers 404 for an unknown function as readily as for an +// unknown execution. Those are different mistakes, so the message has to name +// both the execution asked for and the function it was asked of, and carry the +// API's own answer about which one was missing. +func TestExecutionsNameBothSubjectsOnNotFound(t *testing.T) { + for _, tc := range []struct { + name string + message string + args []string + want string + }{ + { + name: "unknown function", + message: "durable function not found", + args: []string{"get", "order-pipeline", executionID}, + want: "durable function not found", + }, + { + name: "unknown execution", + message: "durable execution not found", + args: []string{"stop", "order-pipeline", executionID, "--yes"}, + want: "durable execution not found", + }, + } { + t.Run(tc.name, func(t *testing.T) { + setExecutionsTestHome(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + writeJSON(t, w, http.StatusNotFound, map[string]any{"error": tc.message}) + })) + defer server.Close() + + _, err := executeCommand(t, newExecutionsCommand(server), tc.args...) + require.Error(t, err) + assert.Contains(t, err.Error(), `durable execution "`+executionID+`"`) + assert.Contains(t, err.Error(), `durable function "order-pipeline"`) + assert.Contains(t, err.Error(), tc.want) + }) + } +} + +func TestExecutionsStopConfirms(t *testing.T) { + setExecutionsTestHome(t) + stopped := false + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + assert.Equal(t, + "/projects/"+projectID+"/durable-functions/order-pipeline/executions/"+executionID+"/stop", + r.URL.Path) + stopped = true + writeJSON(t, w, http.StatusOK, executionPayload(executionID, "order-4417", "stopped")) + })) + defer server.Close() + + out, err := executeCommand(t, newExecutionsCommand(server), "stop", "order-pipeline", executionID) + require.NoError(t, err) + assert.True(t, stopped) + assert.Contains(t, out, "Stop execution "+executionID+"?") + assert.Contains(t, out, "Status: stopped") + assert.Contains(t, out, "Stop requested for execution "+executionID) +} + +func TestExecutionsStopDeclinedLeavesTheExecutionAlone(t *testing.T) { + setExecutionsTestHome(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + t.Error("stop must not be called when the prompt is declined") + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + cmd := newExecutionsCommand(server) + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&out) + cmd.SetIn(bytes.NewBufferString("n\n")) + cmd.SetArgs([]string{"stop", "order-pipeline", executionID}) + require.NoError(t, cmd.Execute()) + assert.Contains(t, out.String(), "Cancelled.") +} + +func newExecutionsCommand(server *httptest.Server) *cobra.Command { + deps := cliruntime.Deps{CommandPathPrefix: "volcano cloud"} + if server != nil { + deps.HTTPClient = server.Client() + deps.APIBaseURL = server.URL + } + return New(deps) +} + +func executeCommand(t *testing.T, cmd *cobra.Command, args ...string) (string, error) { + t.Helper() + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&out) + cmd.SetIn(bytes.NewBufferString("y\n")) + cmd.SetArgs(args) + err := cmd.Execute() + return out.String(), err +} + +func setExecutionsTestHome(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", "") + + cfg := &cliconfig.Config{ + UserToken: "token", + CurrentProject: &cliconfig.ProjectConfig{ID: projectID, Name: "Beta"}, + } + require.NoError(t, cfg.Save()) +} + +func writeJSON(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 executionPayload(id, name, status string) map[string]any { + return map[string]any{ + "id": id, + "durable_function_id": "55555555-5555-4555-8555-555555555555", + "project_id": projectID, + "name": name, + "status": status, + "region": "aws-us-east-1", + "created_at": "2026-05-20T00:00:00Z", + "updated_at": "2026-05-20T00:00:00Z", + } +} + +func withFields(payload, fields map[string]any) map[string]any { + maps.Copy(payload, fields) + return payload +} + +func splitQuery(query string) []string { + if query == "" { + return nil + } + return strings.Split(query, "&") +} diff --git a/internal/cmd/durable/executions/get.go b/internal/cmd/durable/executions/get.go new file mode 100644 index 00000000..49418371 --- /dev/null +++ b/internal/cmd/durable/executions/get.go @@ -0,0 +1,68 @@ +package executions + +import ( + "context" + "fmt" + "io" + "strings" + + "github.com/google/uuid" + "github.com/spf13/cobra" + + clidurable "github.com/Kong/volcano-cli/internal/durable" + "github.com/Kong/volcano-cli/internal/output" + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +type getOptions struct { + deps cliruntime.Deps + function string + executionID uuid.UUID + out io.Writer +} + +func newGet(deps cliruntime.Deps) *cobra.Command { + return &cobra.Command{ + Use: "get ", + Short: "Get one durable execution", + Long: `Show one execution, including its result once it has finished. + +This is the command to poll: reading an execution is what refreshes its status, +and a result is read straight through from the execution rather than stored, so +it is available for as long as the function's retention covers it.`, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + executionID, err := parseExecutionID(args[1]) + if err != nil { + return err + } + return runGet(cmd.Context(), getOptions{ + deps: deps, + function: strings.TrimSpace(args[0]), + executionID: executionID, + out: cmd.OutOrStdout(), + }) + }, + } +} + +func runGet(ctx context.Context, opts getOptions) error { + execution, err := clidurable.NewService(opts.deps).GetExecution(ctx, opts.function, opts.executionID) + if err != nil { + return err + } + + output.DurableExecution(opts.out, execution) + return nil +} + +// parseExecutionID rejects a name here rather than sending it to a route that +// takes an id: an execution is addressed by id, while its name is only an +// idempotency key and is not unique over time. +func parseExecutionID(value string) (uuid.UUID, error) { + executionID, err := uuid.Parse(strings.TrimSpace(value)) + if err != nil { + return uuid.Nil, fmt.Errorf("invalid execution ID %q: %w", value, err) + } + return executionID, nil +} diff --git a/internal/cmd/durable/executions/list.go b/internal/cmd/durable/executions/list.go new file mode 100644 index 00000000..a61cf5a4 --- /dev/null +++ b/internal/cmd/durable/executions/list.go @@ -0,0 +1,63 @@ +package executions + +import ( + "context" + "fmt" + "io" + "strings" + + "github.com/spf13/cobra" + + "github.com/Kong/volcano-cli/internal/api" + clidurable "github.com/Kong/volcano-cli/internal/durable" + "github.com/Kong/volcano-cli/internal/output" + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +type listOptions struct { + deps cliruntime.Deps + function string + status string + page int + limit int + out io.Writer +} + +func newList(deps cliruntime.Deps) *cobra.Command { + opts := listOptions{} + cmd := &cobra.Command{ + Use: "list ", + Short: "List executions of a durable function", + Long: `List the executions of one durable function, newest first. + +The status each entry carries is the last one Volcano observed. Fetch a single +execution to have its state refreshed.`, + Example: fmt.Sprintf(` %s + %s`, + cliruntime.CommandPath(deps, "durable executions list order-pipeline"), + cliruntime.CommandPath(deps, "durable executions list order-pipeline --status running")), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.deps = deps + opts.function = strings.TrimSpace(args[0]) + opts.out = cmd.OutOrStdout() + return runList(cmd.Context(), opts) + }, + } + cmd.Flags().StringVar(&opts.status, "status", "", + "Only executions in this status (pending, running, succeeded, failed, timed_out, stopped)") + cmd.Flags().IntVar(&opts.page, "page", api.DefaultPage, "Page number to fetch") + cmd.Flags().IntVar(&opts.limit, "limit", api.DefaultLimit, "Number of executions per page") + return cmd +} + +func runList(ctx context.Context, opts listOptions) error { + executions, err := clidurable.NewService(opts.deps).ListExecutions( + ctx, opts.function, strings.TrimSpace(opts.status), opts.page, opts.limit) + if err != nil { + return err + } + + output.DurableExecutions(opts.out, opts.function, executions, cliruntime.CommandPath(opts.deps, "")) + return nil +} diff --git a/internal/cmd/durable/executions/stop.go b/internal/cmd/durable/executions/stop.go new file mode 100644 index 00000000..daa9897d --- /dev/null +++ b/internal/cmd/durable/executions/stop.go @@ -0,0 +1,81 @@ +package executions + +import ( + "context" + "io" + "strings" + + "github.com/google/uuid" + "github.com/spf13/cobra" + + "github.com/Kong/volcano-cli/internal/confirm" + clidurable "github.com/Kong/volcano-cli/internal/durable" + "github.com/Kong/volcano-cli/internal/output" + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +type stopOptions struct { + deps cliruntime.Deps + function string + executionID uuid.UUID + yes bool + in io.Reader + out io.Writer +} + +func newStop(deps cliruntime.Deps) *cobra.Command { + var yes bool + cmd := &cobra.Command{ + Use: "stop ", + Short: "Stop a running durable execution", + Long: `Stop one execution of a durable function. + +Steps already completed are not undone, and work already in flight is not +interrupted mid-attempt: the execution stops at its next checkpoint. Stopping +one that has already finished reports the state it is in rather than failing, +so a retried stop is safe. + +By default this command prompts for confirmation. +Use --yes to skip the prompt.`, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + executionID, err := parseExecutionID(args[1]) + if err != nil { + return err + } + return runStop(cmd.Context(), stopOptions{ + deps: deps, + function: strings.TrimSpace(args[0]), + executionID: executionID, + yes: yes, + in: cmd.InOrStdin(), + out: cmd.OutOrStdout(), + }) + }, + } + cmd.Flags().BoolVarP(&yes, "yes", "y", false, "Skip confirmation prompt") + return cmd +} + +func runStop(ctx context.Context, opts stopOptions) error { + if !opts.yes { + confirmed, err := confirm.Action(opts.in, opts.out, + "Stopping an execution ends it where it is; steps already completed are not undone.", + "Stop execution "+opts.executionID.String()+"?") + if err != nil { + return err + } + if !confirmed { + return nil + } + } + + execution, err := clidurable.NewService(opts.deps).StopExecution(ctx, opts.function, opts.executionID) + if err != nil { + return err + } + + output.DurableExecution(opts.out, execution) + output.Success(opts.out, "Stop requested for execution %s", execution.Id.String()) + return nil +} diff --git a/internal/cmd/durable/get.go b/internal/cmd/durable/get.go new file mode 100644 index 00000000..1cca51d5 --- /dev/null +++ b/internal/cmd/durable/get.go @@ -0,0 +1,48 @@ +package durable + +import ( + "context" + "io" + "strings" + + "github.com/spf13/cobra" + + clidurable "github.com/Kong/volcano-cli/internal/durable" + "github.com/Kong/volcano-cli/internal/output" + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +type getOptions struct { + deps cliruntime.Deps + identifier string + out io.Writer +} + +func newGet(deps cliruntime.Deps) *cobra.Command { + return &cobra.Command{ + Use: "get ", + Short: "Get a durable function", + Long: `Show one durable function, including the execution limits it was created with. + +Those limits come from the project's plan and are fixed for the life of the +function; changing them means creating a new one.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runGet(cmd.Context(), getOptions{ + deps: deps, + identifier: strings.TrimSpace(args[0]), + out: cmd.OutOrStdout(), + }) + }, + } +} + +func runGet(ctx context.Context, opts getOptions) error { + function, err := clidurable.NewService(opts.deps).Get(ctx, opts.identifier) + if err != nil { + return err + } + + output.DurableFunction(opts.out, function) + return nil +} diff --git a/internal/cmd/durable/helpers_test.go b/internal/cmd/durable/helpers_test.go new file mode 100644 index 00000000..2595ad69 --- /dev/null +++ b/internal/cmd/durable/helpers_test.go @@ -0,0 +1,138 @@ +package durable + +import ( + "bytes" + "encoding/json" + "net/http" + "os" + "path/filepath" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + + cliconfig "github.com/Kong/volcano-cli/internal/config" +) + +const ( + durableProjectID = "22222222-2222-4222-8222-222222222222" + durableFunctionID = "55555555-5555-4555-8555-555555555555" + durableExecutionA = "66666666-6666-4666-8666-666666666666" +) + +func executeDurableCommand(t *testing.T, cmd *cobra.Command, args ...string) (string, error) { + t.Helper() + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&out) + cmd.SetIn(bytes.NewBufferString("y\n")) + cmd.SetArgs(args) + err := cmd.Execute() + return out.String(), err +} + +func setDurableCommandTestHome(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", "") + + cfg := &cliconfig.Config{ + UserToken: "token", + CurrentProject: &cliconfig.ProjectConfig{ + ID: durableProjectID, + Name: "Beta", + }, + } + require.NoError(t, cfg.Save()) +} + +func writeDurableCommandJSON(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 durableFunctionPayload(name string, isPublic bool) map[string]any { + return map[string]any{ + "id": durableFunctionID, + "project_id": durableProjectID, + "name": name, + "runtime": "nodejs24.x", + "handler": "handler", + "kind": "durable", + "status": "active", + "is_public": isPublic, + "deployed_regions": []string{"aws-us-east-1"}, + "durable": map[string]any{ + "execution_timeout_seconds": 3600, + "retention_days": 30, + }, + "created_at": "2026-05-20T00:00:00Z", + "updated_at": "2026-05-20T00:00:00Z", + } +} + +func durableExecutionPayload(id, name, status string) map[string]any { + return map[string]any{ + "id": id, + "durable_function_id": durableFunctionID, + "project_id": durableProjectID, + "name": name, + "status": status, + "region": "aws-us-east-1", + "created_at": "2026-05-20T00:00:00Z", + "updated_at": "2026-05-20T00:00:00Z", + } +} + +// writeDurableRuntimesResponse answers the runtime catalog the deploy path +// fetches before it scans sources. +func writeDurableRuntimesResponse(t *testing.T, w http.ResponseWriter, r *http.Request) bool { + t.Helper() + if r.Method != http.MethodGet || r.URL.Path != "/functions/runtimes" { + return false + } + writeDurableCommandJSON(t, w, http.StatusOK, map[string]any{ + "runtimes": []any{ + map[string]any{ + "name": "nodejs24.x", + "language": "javascript", + "default": true, + "durable_capable": true, + "deployment": map[string]any{ + "file_extensions": []string{".js", ".mjs"}, + "entrypoint": "index.js", + "handler": "handler", + "dependency_manifests": []string{"package.json"}, + }, + }, + map[string]any{ + "name": "python3.12", + "language": "python", + "default": true, + "durable_capable": false, + "deployment": map[string]any{ + "file_extensions": []string{".py"}, + "entrypoint": "main.py", + "handler": "handler", + "dependency_manifests": []string{"requirements.txt"}, + }, + }, + }, + }) + return true +} + +func writeDurableProjectFile(t *testing.T, path, contents string) { + t.Helper() + require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755)) + require.NoError(t, os.WriteFile(path, []byte(contents), 0o600)) +} + +func decodeJSONBody(r *http.Request, into any) error { + return json.NewDecoder(r.Body).Decode(into) +} diff --git a/internal/cmd/durable/list.go b/internal/cmd/durable/list.go new file mode 100644 index 00000000..7be3a74e --- /dev/null +++ b/internal/cmd/durable/list.go @@ -0,0 +1,52 @@ +package durable + +import ( + "context" + "io" + + "github.com/spf13/cobra" + + "github.com/Kong/volcano-cli/internal/api" + clidurable "github.com/Kong/volcano-cli/internal/durable" + "github.com/Kong/volcano-cli/internal/output" + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +type listOptions struct { + deps cliruntime.Deps + page int + limit int + out io.Writer +} + +func newList(deps cliruntime.Deps) *cobra.Command { + var page int + var limit int + cmd := &cobra.Command{ + Use: "list", + Short: "List durable functions", + Long: "List durable functions for the current project. Standard functions never appear here.", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + return runList(cmd.Context(), listOptions{ + deps: deps, + page: page, + limit: limit, + out: cmd.OutOrStdout(), + }) + }, + } + cmd.Flags().IntVar(&page, "page", api.DefaultPage, "Page number to fetch") + cmd.Flags().IntVar(&limit, "limit", api.DefaultLimit, "Number of durable functions per page") + return cmd +} + +func runList(ctx context.Context, opts listOptions) error { + functions, err := clidurable.NewService(opts.deps).ListPage(ctx, opts.page, opts.limit) + if err != nil { + return err + } + + output.DurableFunctions(opts.out, functions, cliruntime.CommandPath(opts.deps, "")) + return nil +} diff --git a/internal/cmd/durable/schedulers/create.go b/internal/cmd/durable/schedulers/create.go new file mode 100644 index 00000000..22220360 --- /dev/null +++ b/internal/cmd/durable/schedulers/create.go @@ -0,0 +1,96 @@ +package schedulers + +import ( + "context" + "fmt" + "io" + "strings" + + "github.com/spf13/cobra" + + "github.com/Kong/volcano-cli/internal/api" + "github.com/Kong/volcano-cli/internal/cmd/cmdutil" + clidurable "github.com/Kong/volcano-cli/internal/durable" + "github.com/Kong/volcano-cli/internal/output" + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +type createOptions struct { + deps cliruntime.Deps + function string + name string + cron string + input string + regions string + out io.Writer +} + +func newCreate(deps cliruntime.Deps) *cobra.Command { + opts := createOptions{} + cmd := &cobra.Command{ + Use: "create ", + Short: "Create a durable function scheduler", + Long: `Create a scheduler that starts an execution of a durable function on a cron +schedule. + +Each tick starts an execution under a name derived from that run, so a tick +Volcano has to retry resolves to the execution it already started rather than +beginning a second one. + +The --cron flag accepts standard 5-field cron expressions such as "*/5 * * * *". +If --regions is omitted, Volcano chooses one deployed region and keeps using it +until geofencing removes that region. If --regions is provided, it must be a +single region where the function is deployed. + +The --input flag accepts either inline JSON or a path to a JSON file, and is the +input every execution the scheduler starts receives.`, + Example: fmt.Sprintf(` %s + %s + %s`, + cliruntime.CommandPath(deps, `durable schedulers create order-pipeline --cron "0 * * * *"`), + cliruntime.CommandPath(deps, `durable schedulers create order-pipeline --name nightly-sweep --cron "0 2 * * *" --input sweep.json`), + cliruntime.CommandPath(deps, `durable schedulers create order-pipeline --cron "0 9 * * 1-5" --regions aws-us-east-1`)), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.deps = deps + opts.function = strings.TrimSpace(args[0]) + opts.out = cmd.OutOrStdout() + return runCreate(cmd.Context(), opts) + }, + } + cmd.Flags().StringVar(&opts.cron, "cron", "", "5-field cron expression, for example '0 * * * *' (required)") + cmd.Flags().StringVar(&opts.name, "name", "", "Scheduler name (defaults to ' scheduler')") + cmd.Flags().StringVar(&opts.input, "input", "", "Inline JSON object or path to a JSON file passed as each execution's input") + cmd.Flags().StringVar(&opts.regions, "regions", "", "Single scheduler region; defaults to one deployed region") + _ = cmd.MarkFlagRequired("cron") + return cmd +} + +func runCreate(ctx context.Context, opts createOptions) error { + input := api.FunctionSchedulerInput{ + Name: strings.TrimSpace(opts.name), + CronExpression: strings.TrimSpace(opts.cron), + } + if input.Name == "" { + input.Name = opts.function + " scheduler" + } + if value := strings.TrimSpace(opts.input); value != "" { + payload, err := cmdutil.ParseJSONObject("input", value) + if err != nil { + return err + } + input.Payload = payload + } + if regions := strings.TrimSpace(opts.regions); regions != "" { + input.Regions = []string{regions} + } + + scheduler, err := clidurable.NewService(opts.deps).CreateScheduler(ctx, opts.function, input) + if err != nil { + return err + } + + output.Scheduler(opts.out, scheduler) + output.Success(opts.out, "Created scheduler for durable function %q", opts.function) + return nil +} diff --git a/internal/cmd/durable/schedulers/delete.go b/internal/cmd/durable/schedulers/delete.go new file mode 100644 index 00000000..d4a9d051 --- /dev/null +++ b/internal/cmd/durable/schedulers/delete.go @@ -0,0 +1,75 @@ +package schedulers + +import ( + "context" + "io" + "strings" + + "github.com/google/uuid" + "github.com/spf13/cobra" + + "github.com/Kong/volcano-cli/internal/confirm" + clidurable "github.com/Kong/volcano-cli/internal/durable" + "github.com/Kong/volcano-cli/internal/output" + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +type deleteOptions struct { + deps cliruntime.Deps + function string + schedulerID uuid.UUID + yes bool + in io.Reader + out io.Writer +} + +func newDelete(deps cliruntime.Deps) *cobra.Command { + var yes bool + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a durable function scheduler", + Long: `Delete a scheduler and its run history. + +Executions the scheduler already started keep running, and stay readable for as +long as the function's retention allows. + +By default this command prompts for confirmation. +Use --yes to skip the prompt.`, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + schedulerID, err := parseSchedulerID(args[1]) + if err != nil { + return err + } + return runDelete(cmd.Context(), deleteOptions{ + deps: deps, + function: strings.TrimSpace(args[0]), + schedulerID: schedulerID, + yes: yes, + in: cmd.InOrStdin(), + out: cmd.OutOrStdout(), + }) + }, + } + cmd.Flags().BoolVarP(&yes, "yes", "y", false, "Skip confirmation prompt") + return cmd +} + +func runDelete(ctx context.Context, opts deleteOptions) error { + if !opts.yes { + confirmed, err := confirm.Delete(opts.in, opts.out, "durable function scheduler", opts.schedulerID.String()) + if err != nil { + return err + } + if !confirmed { + return nil + } + } + + if err := clidurable.NewService(opts.deps).DeleteScheduler(ctx, opts.function, opts.schedulerID); err != nil { + return err + } + + output.Success(opts.out, "Deleted scheduler %s", opts.schedulerID.String()) + return nil +} diff --git a/internal/cmd/durable/schedulers/disable.go b/internal/cmd/durable/schedulers/disable.go new file mode 100644 index 00000000..0d9a8719 --- /dev/null +++ b/internal/cmd/durable/schedulers/disable.go @@ -0,0 +1,68 @@ +package schedulers + +import ( + "context" + "fmt" + "io" + "strings" + + "github.com/google/uuid" + "github.com/spf13/cobra" + + clidurable "github.com/Kong/volcano-cli/internal/durable" + "github.com/Kong/volcano-cli/internal/output" + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +type disableOptions struct { + deps cliruntime.Deps + function string + schedulerID uuid.UUID + out io.Writer +} + +func newDisable(deps cliruntime.Deps) *cobra.Command { + return &cobra.Command{ + Use: "disable ", + Short: "Disable a durable function scheduler", + Long: `Stop a scheduler's ticks without deleting it. + +Executions the scheduler already started keep running; stop them with +'durable executions stop' if you need them ended.`, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + schedulerID, err := parseSchedulerID(args[1]) + if err != nil { + return err + } + return runDisable(cmd.Context(), disableOptions{ + deps: deps, + function: strings.TrimSpace(args[0]), + schedulerID: schedulerID, + out: cmd.OutOrStdout(), + }) + }, + } +} + +func runDisable(ctx context.Context, opts disableOptions) error { + scheduler, err := clidurable.NewService(opts.deps).DisableScheduler(ctx, opts.function, opts.schedulerID) + if err != nil { + return err + } + + output.Scheduler(opts.out, scheduler) + output.Success(opts.out, "Disabled scheduler %s", opts.schedulerID.String()) + return nil +} + +// parseSchedulerID keeps the id check in the command, so a typo fails before +// anything is sent. +func parseSchedulerID(value string) (uuid.UUID, error) { + trimmed := strings.TrimSpace(value) + schedulerID, err := uuid.Parse(trimmed) + if err != nil { + return uuid.Nil, fmt.Errorf("invalid scheduler id %q: %w", trimmed, err) + } + return schedulerID, nil +} diff --git a/internal/cmd/durable/schedulers/enable.go b/internal/cmd/durable/schedulers/enable.go new file mode 100644 index 00000000..9e84462a --- /dev/null +++ b/internal/cmd/durable/schedulers/enable.go @@ -0,0 +1,53 @@ +package schedulers + +import ( + "context" + "io" + "strings" + + "github.com/google/uuid" + "github.com/spf13/cobra" + + clidurable "github.com/Kong/volcano-cli/internal/durable" + "github.com/Kong/volcano-cli/internal/output" + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +type enableOptions struct { + deps cliruntime.Deps + function string + schedulerID uuid.UUID + out io.Writer +} + +func newEnable(deps cliruntime.Deps) *cobra.Command { + return &cobra.Command{ + Use: "enable ", + Short: "Enable a durable function scheduler", + Long: "Resume a scheduler's ticks. The next tick starts an execution on schedule.", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + schedulerID, err := parseSchedulerID(args[1]) + if err != nil { + return err + } + return runEnable(cmd.Context(), enableOptions{ + deps: deps, + function: strings.TrimSpace(args[0]), + schedulerID: schedulerID, + out: cmd.OutOrStdout(), + }) + }, + } +} + +func runEnable(ctx context.Context, opts enableOptions) error { + scheduler, err := clidurable.NewService(opts.deps).EnableScheduler(ctx, opts.function, opts.schedulerID) + if err != nil { + return err + } + + output.Scheduler(opts.out, scheduler) + output.Success(opts.out, "Enabled scheduler %s", opts.schedulerID.String()) + return nil +} diff --git a/internal/cmd/durable/schedulers/list.go b/internal/cmd/durable/schedulers/list.go new file mode 100644 index 00000000..de98f122 --- /dev/null +++ b/internal/cmd/durable/schedulers/list.go @@ -0,0 +1,45 @@ +package schedulers + +import ( + "context" + "io" + "strings" + + "github.com/spf13/cobra" + + clidurable "github.com/Kong/volcano-cli/internal/durable" + "github.com/Kong/volcano-cli/internal/output" + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +type listOptions struct { + deps cliruntime.Deps + function string + out io.Writer +} + +func newList(deps cliruntime.Deps) *cobra.Command { + return &cobra.Command{ + Use: "list ", + Short: "List schedulers for a durable function", + Long: "List the schedulers that start executions of one durable function.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runList(cmd.Context(), listOptions{ + deps: deps, + function: strings.TrimSpace(args[0]), + out: cmd.OutOrStdout(), + }) + }, + } +} + +func runList(ctx context.Context, opts listOptions) error { + schedulers, err := clidurable.NewService(opts.deps).ListSchedulers(ctx, opts.function) + if err != nil { + return err + } + + output.DurableSchedulers(opts.out, opts.function, schedulers) + return nil +} diff --git a/internal/cmd/durable/schedulers/schedulers.go b/internal/cmd/durable/schedulers/schedulers.go new file mode 100644 index 00000000..3759d26b --- /dev/null +++ b/internal/cmd/durable/schedulers/schedulers.go @@ -0,0 +1,28 @@ +// Package schedulers wires the volcano durable schedulers subcommands. +package schedulers + +import ( + "github.com/spf13/cobra" + + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +// New returns the durable schedulers command. +func New(deps cliruntime.Deps) *cobra.Command { + cmd := &cobra.Command{ + Use: "schedulers", + Short: "Manage scheduled durable executions", + Long: `A durable scheduler starts an execution on a cron schedule instead of invoking +the function, so each tick produces an execution you can list, follow, and stop +like any other. + +A tick draws on the same invocation allowance and concurrency cap a manual start +does, so a tick that would exceed the cap fails that run rather than queueing.`, + } + cmd.AddCommand(newList(deps)) + cmd.AddCommand(newCreate(deps)) + cmd.AddCommand(newEnable(deps)) + cmd.AddCommand(newDisable(deps)) + cmd.AddCommand(newDelete(deps)) + return cmd +} diff --git a/internal/cmd/durable/schedulers/schedulers_test.go b/internal/cmd/durable/schedulers/schedulers_test.go new file mode 100644 index 00000000..3e31c925 --- /dev/null +++ b/internal/cmd/durable/schedulers/schedulers_test.go @@ -0,0 +1,345 @@ +package schedulers + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "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 ( + projectID = "22222222-2222-4222-8222-222222222222" + functionID = "55555555-5555-4555-8555-555555555555" + schedulerID = "77777777-7777-4777-8777-777777777777" +) + +func TestSchedulersListPopulatedAndEmpty(t *testing.T) { + for _, tc := range []struct { + name string + body map[string]any + want []string + }{ + { + name: "populated", + body: map[string]any{"data": []any{schedulerPayload("order-pipeline scheduler", true)}}, + want: []string{schedulerID, "order-pipeline scheduler", "enabled", "0 * * * *", "aws-us-east-1"}, + }, + { + name: "none configured", + body: map[string]any{"data": []any{}}, + want: []string{`No schedulers configured for durable function "order-pipeline"`}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + setSchedulersTestHome(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "/projects/"+projectID+"/durable-functions/order-pipeline/schedulers", r.URL.Path) + writeJSON(t, w, http.StatusOK, tc.body) + })) + defer server.Close() + + out, err := executeCommand(t, newSchedulersCommand(server), "list", "order-pipeline") + require.NoError(t, err) + for _, want := range tc.want { + assert.Contains(t, out, want) + } + }) + } +} + +// The durable collection is addressed by name or id, and the two collections +// never accept each other's, so a create must go to the durable path rather +// than resolving the name against /functions first. +func TestSchedulersCreatePostsToTheDurableCollection(t *testing.T) { + setSchedulersTestHome(t) + var body map[string]any + var paths []string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + paths = append(paths, r.URL.Path) + switch { + case r.Method == http.MethodPost && + r.URL.Path == "/projects/"+projectID+"/durable-functions/order-pipeline/schedulers": + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + writeJSON(t, w, http.StatusCreated, schedulerPayload("order-pipeline scheduler", true)) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + out, err := executeCommand(t, newSchedulersCommand(server), + "create", "order-pipeline", "--cron", "0 * * * *") + require.NoError(t, err) + assert.Equal(t, "order-pipeline scheduler", body["name"], "default name should be ' scheduler'") + assert.Equal(t, map[string]any{"cron_expression": "0 * * * *"}, body["schedule"]) + assert.NotContains(t, body, "payload", "no --input should send no payload at all") + assert.Contains(t, out, `Created scheduler for durable function "order-pipeline"`) + assert.NotContains(t, paths, "/projects/"+projectID+"/functions") +} + +func TestSchedulersCreateSendsNameRegionsAndInput(t *testing.T) { + setSchedulersTestHome(t) + dir := t.TempDir() + t.Chdir(dir) + inputPath := filepath.Join(dir, "sweep.json") + require.NoError(t, os.WriteFile(inputPath, []byte(`{"scope":"nightly"}`), 0o600)) + + var body map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + writeJSON(t, w, http.StatusCreated, schedulerPayload("nightly-sweep", true)) + })) + defer server.Close() + + _, err := executeCommand(t, newSchedulersCommand(server), + "create", "order-pipeline", + "--name", "nightly-sweep", + "--cron", "0 2 * * *", + "--input", "sweep.json", + "--regions", "aws-us-east-1", + ) + require.NoError(t, err) + assert.Equal(t, "nightly-sweep", body["name"]) + assert.Equal(t, map[string]any{"scope": "nightly"}, body["payload"]) + assert.Equal(t, []any{"aws-us-east-1"}, body["regions"]) +} + +func TestSchedulersCreateRefusesMalformedInput(t *testing.T) { + setSchedulersTestHome(t) + + _, err := executeCommand(t, newSchedulersCommand(nil), + "create", "order-pipeline", "--cron", "0 * * * *", "--input", "not-json") + require.ErrorContains(t, err, "input must be a JSON object") +} + +func TestSchedulersCreateRequiresACron(t *testing.T) { + setSchedulersTestHome(t) + + _, err := executeCommand(t, newSchedulersCommand(nil), "create", "order-pipeline") + require.ErrorContains(t, err, `required flag(s) "cron" not set`) +} + +func TestSchedulersEnableAndDisable(t *testing.T) { + for _, tc := range []struct { + name string + args []string + wantEnabled bool + want string + }{ + { + name: "enable", + args: []string{"enable", "order-pipeline", schedulerID}, + wantEnabled: true, + want: "Enabled scheduler " + schedulerID, + }, + { + name: "disable", + args: []string{"disable", "order-pipeline", schedulerID}, + wantEnabled: false, + want: "Disabled scheduler " + schedulerID, + }, + } { + t.Run(tc.name, func(t *testing.T) { + setSchedulersTestHome(t) + var body map[string]any + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPatch, r.Method) + assert.Equal(t, + "/projects/"+projectID+"/durable-functions/order-pipeline/schedulers/"+schedulerID, + r.URL.Path) + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + writeJSON(t, w, http.StatusOK, schedulerPayload("order-pipeline scheduler", tc.wantEnabled)) + })) + defer server.Close() + + out, err := executeCommand(t, newSchedulersCommand(server), tc.args...) + require.NoError(t, err) + assert.Equal(t, tc.wantEnabled, body["enabled"]) + assert.Len(t, body, 1, "only the enabled flag should be sent") + assert.Contains(t, out, tc.want) + }) + } +} + +func TestSchedulersDelete(t *testing.T) { + setSchedulersTestHome(t) + var deleted string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodDelete { + deleted = r.URL.Path + w.WriteHeader(http.StatusNoContent) + return + } + http.NotFound(w, r) + })) + defer server.Close() + + out, err := executeCommand(t, newSchedulersCommand(server), + "delete", "order-pipeline", schedulerID, "--yes") + require.NoError(t, err) + assert.Equal(t, + "/projects/"+projectID+"/durable-functions/order-pipeline/schedulers/"+schedulerID, deleted) + assert.Contains(t, out, "Deleted scheduler "+schedulerID) +} + +func TestSchedulersDeletePromptsAndCancels(t *testing.T) { + setSchedulersTestHome(t) + var sawDelete bool + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodDelete { + sawDelete = true + w.WriteHeader(http.StatusNoContent) + return + } + http.NotFound(w, r) + })) + defer server.Close() + + cmd := newSchedulersCommand(server) + cmd.SetIn(strings.NewReader("no\n")) + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&out) + cmd.SetArgs([]string{"delete", "order-pipeline", schedulerID}) + require.NoError(t, cmd.Execute()) + + assert.False(t, sawDelete) + assert.Contains(t, out.String(), "Delete durable function scheduler '"+schedulerID+"'?") + assert.Contains(t, out.String(), "Delete cancelled.") +} + +// A scheduler id is checked before anything is sent, so a typo cannot be +// mistaken for a scheduler that is missing. +func TestSchedulersRefuseAMalformedSchedulerID(t *testing.T) { + for _, args := range [][]string{ + {"enable", "order-pipeline", "not-a-uuid"}, + {"disable", "order-pipeline", "not-a-uuid"}, + {"delete", "order-pipeline", "not-a-uuid", "--yes"}, + } { + t.Run(args[0], func(t *testing.T) { + setSchedulersTestHome(t) + + _, err := executeCommand(t, newSchedulersCommand(nil), args...) + require.ErrorContains(t, err, `invalid scheduler id "not-a-uuid"`) + }) + } +} + +// A scheduler route answers 404 for an unknown durable function as readily as +// for an unknown scheduler, so the message names both subjects and carries the +// API's own answer about which was missing. +func TestSchedulersNameBothSubjectsOnNotFound(t *testing.T) { + for _, tc := range []struct { + name string + message string + args []string + }{ + { + name: "unknown function", + message: "durable function not found", + args: []string{"disable", "order-pipeline", schedulerID}, + }, + { + name: "unknown scheduler", + message: "scheduler not found", + args: []string{"delete", "order-pipeline", schedulerID, "--yes"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + setSchedulersTestHome(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + writeJSON(t, w, http.StatusNotFound, map[string]any{"error": tc.message}) + })) + defer server.Close() + + _, err := executeCommand(t, newSchedulersCommand(server), tc.args...) + require.Error(t, err) + assert.Contains(t, err.Error(), `scheduler "`+schedulerID+`"`) + assert.Contains(t, err.Error(), `durable function "order-pipeline"`) + assert.Contains(t, err.Error(), tc.message) + }) + } +} + +// A durable function's name is not accepted by the standard collection, so a +// list that lands there is a bug the message has to survive. +func TestSchedulersListReportsAMissingFunction(t *testing.T) { + setSchedulersTestHome(t) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + writeJSON(t, w, http.StatusNotFound, map[string]any{"error": "durable function not found"}) + })) + defer server.Close() + + _, err := executeCommand(t, newSchedulersCommand(server), "list", "hello") + require.ErrorContains(t, err, `durable function "hello" not found`) +} + +func newSchedulersCommand(server *httptest.Server) *cobra.Command { + deps := cliruntime.Deps{CommandPathPrefix: "volcano cloud"} + if server != nil { + deps.HTTPClient = server.Client() + deps.APIBaseURL = server.URL + } + return New(deps) +} + +func executeCommand(t *testing.T, cmd *cobra.Command, args ...string) (string, error) { + t.Helper() + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&out) + cmd.SetIn(bytes.NewBufferString("y\n")) + cmd.SetArgs(args) + err := cmd.Execute() + return out.String(), err +} + +func setSchedulersTestHome(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", "") + + cfg := &cliconfig.Config{ + UserToken: "token", + CurrentProject: &cliconfig.ProjectConfig{ID: projectID, Name: "Beta"}, + } + require.NoError(t, cfg.Save()) +} + +func writeJSON(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 schedulerPayload(name string, enabled bool) map[string]any { + return map[string]any{ + "id": schedulerID, + "project_id": projectID, + "function_id": functionID, + "function_kind": "durable", + "name": name, + "enabled": enabled, + "schedule_kind": "cron", + "cron_expression": "0 * * * *", + "regions": []string{"aws-us-east-1"}, + "created_at": "2026-05-20T00:00:00Z", + "updated_at": "2026-05-20T00:00:00Z", + } +} diff --git a/internal/cmd/durable/start.go b/internal/cmd/durable/start.go new file mode 100644 index 00000000..aaee0625 --- /dev/null +++ b/internal/cmd/durable/start.go @@ -0,0 +1,84 @@ +package durable + +import ( + "context" + "fmt" + "io" + "strings" + + "github.com/spf13/cobra" + + "github.com/Kong/volcano-cli/internal/api" + "github.com/Kong/volcano-cli/internal/cmd/cmdutil" + clidurable "github.com/Kong/volcano-cli/internal/durable" + "github.com/Kong/volcano-cli/internal/output" + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +type startOptions struct { + deps cliruntime.Deps + function string + input string + name string + out io.Writer +} + +func newStart(deps cliruntime.Deps) *cobra.Command { + opts := startOptions{} + cmd := &cobra.Command{ + Use: "start ", + Short: "Start a durable execution", + Long: `Start one execution of a durable function. + +Starting is asynchronous by construction: an execution can run for hours, so +this returns a handle rather than a result. Read the execution to follow it. + +--name is the execution's idempotency key. Repeating a start under a name that +already names an execution returns that one instead of beginning a second, and +is not charged again, which is what makes retrying a start safe. Omit it and +Volcano generates one. + +--input accepts inline JSON or a path to a JSON file, and is the function's +input verbatim.`, + Example: fmt.Sprintf(` %s + %s + %s`, + cliruntime.CommandPath(deps, "durable start order-pipeline"), + cliruntime.CommandPath(deps, `durable start order-pipeline --input '{"order_id":4417}'`), + cliruntime.CommandPath(deps, "durable start order-pipeline --input order.json --name order-4417")), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.deps = deps + opts.function = strings.TrimSpace(args[0]) + opts.out = cmd.OutOrStdout() + return runStart(cmd.Context(), opts) + }, + } + cmd.Flags().StringVar(&opts.input, "input", "", "Inline JSON object or path to a JSON file passed as the execution's input") + cmd.Flags().StringVar(&opts.name, "name", "", "Execution name, used as the idempotency key") + return cmd +} + +func runStart(ctx context.Context, opts startOptions) error { + start := api.DurableExecutionStartInput{Name: strings.TrimSpace(opts.name)} + // An omitted --input starts the execution with no input at all, which is not + // the same thing as starting it with an empty object. + if value := strings.TrimSpace(opts.input); value != "" { + input, err := cmdutil.ParseJSONObject("input", value) + if err != nil { + return err + } + start.Input = input + } + + execution, err := clidurable.NewService(opts.deps).StartExecution(ctx, opts.function, start) + if err != nil { + return err + } + + output.DurableExecution(opts.out, execution) + output.Success(opts.out, "Execution %s started", execution.Name) + fmt.Fprintf(opts.out, "Follow it with %s\n", + cliruntime.CommandPath(opts.deps, "durable executions get "+opts.function+" "+execution.Id.String())) + return nil +} diff --git a/internal/cmd/functions/deploy.go b/internal/cmd/functions/deploy.go index e4154fa5..aa8202e4 100644 --- a/internal/cmd/functions/deploy.go +++ b/internal/cmd/functions/deploy.go @@ -99,35 +99,65 @@ func runDeploy(ctx context.Context, opts deployOptions) error { return nil } - sources := allSources + // Read before anything is packaged or uploaded. An existing but unreadable + // manifest aborts here rather than deploying with its scope declarations + // silently dropped, or its durable functions mistaken for standard ones. + manifest, err := projectconfig.ReadFunctionDeployManifest("") + if err != nil { + return err + } + + var sources []clifunction.SourceInfo if !opts.all { - sources = nil for _, source := range allSources { - if sourceMatchesTarget(source, opts.file, baseDir) { + if clifunction.MatchesTarget(source, opts.file, baseDir) { sources = append(sources, source) break } } if len(sources) == 0 { - return fmt.Errorf("function %q not found in volcano/functions/\navailable functions: %s", opts.file, formatSourceNames(allSources)) + return fmt.Errorf("function %q not found in volcano/functions/\navailable functions: %s", opts.file, clifunction.FormatSourceNames(allSources)) + } + if manifest.DurableNames[sources[0].Name] { + return fmt.Errorf("%q is declared durable in volcano-config.yaml; deploy it with %q", + sources[0].Name, "volcano cloud durable deploy -f "+sources[0].Name) } fmt.Fprintf(opts.out, "Deploying function: %s\n", sources[0].Name) } else { + sources = excludeDurable(allSources, manifest.DurableNames, opts.out) + if len(sources) == 0 { + fmt.Fprintln(opts.out, "No standard functions found in volcano/functions/") + return nil + } fmt.Fprintf(opts.out, "Found %d function(s)\n", len(sources)) } - // Read the manifest before anything is uploaded. An existing but unreadable - // manifest aborts here rather than deploying with its scope declarations - // silently dropped. - declarations, err := projectconfig.FunctionVariableDeclarations("") - if err != nil { - return err + if opts.all { + return runDeployAll(ctx, opts.out, service, baseDir, sources, opts.batchAll, manifest.Declarations) + } + return runDeployOne(ctx, opts.out, service, baseDir, sources[0], manifest.Declarations) +} + +func excludeDurable(sources []clifunction.SourceInfo, durableNames map[string]bool, out io.Writer) []clifunction.SourceInfo { + if len(durableNames) == 0 { + return sources } - if opts.all { - return runDeployAll(ctx, opts.out, service, baseDir, sources, opts.batchAll, declarations) + kept := make([]clifunction.SourceInfo, 0, len(sources)) + var skipped []string + for _, source := range sources { + if durableNames[source.Name] { + skipped = append(skipped, source.Name) + continue + } + kept = append(kept, source) } - return runDeployOne(ctx, opts.out, service, baseDir, sources[0], declarations) + if len(skipped) > 0 { + fmt.Fprintf(out, "Skipping %d durable function(s) declared in volcano-config.yaml: %s\n", + len(skipped), strings.Join(skipped, ", ")) + fmt.Fprintln(out, "Deploy them with \"volcano cloud durable deploy --all\"") + } + return kept } // applyVariableDeclaration attaches the manifest declaration for pkg, if the @@ -244,43 +274,6 @@ func printSourceSummary(out io.Writer, source clifunction.SourceInfo) { fmt.Fprintf(out, " Function code: %s\n", source.Path) } -func normalizeSourceTarget(target string) string { - name := filepath.Base(strings.TrimSpace(target)) - if ext := filepath.Ext(name); ext != "" { - name = strings.TrimSuffix(name, ext) - } - return name -} - -func sourceMatchesTarget(source clifunction.SourceInfo, target, baseDir string) bool { - target = strings.TrimSpace(target) - if source.Name == normalizeSourceTarget(target) { - return true - } - - sourcePath, err := filepath.Abs(source.Path) - if err != nil { - return false - } - targetPath := target - if !filepath.IsAbs(targetPath) { - targetPath = filepath.Join(baseDir, targetPath) - } - targetPath, err = filepath.Abs(targetPath) - if err != nil { - return false - } - return filepath.Clean(sourcePath) == filepath.Clean(targetPath) -} - -func formatSourceNames(sources []clifunction.SourceInfo) string { - names := make([]string, len(sources)) - for i, source := range sources { - names[i] = source.Name - } - return strings.Join(names, ", ") -} - func batchFailures(resp *apiclient.BatchFunctionDeployResponse) []apiclient.BatchFunctionDeployFailure { if resp == nil || resp.Failed == nil { return nil diff --git a/internal/cmd/functions/deploy_durable_test.go b/internal/cmd/functions/deploy_durable_test.go new file mode 100644 index 00000000..0a504191 --- /dev/null +++ b/internal/cmd/functions/deploy_durable_test.go @@ -0,0 +1,169 @@ +package functions + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + cliruntime "github.com/Kong/volcano-cli/internal/runtime" +) + +// A durable function's source sits in volcano/functions alongside the standard +// ones, and only volcano-config.yaml says which is which. A function's kind is +// fixed at creation, and the server refuses the cross-kind name with a 409 that +// would fail the whole batch, so deploy-all has to leave durable names out. +func TestFunctionsDeployAllSkipsDurableDeclaredFunctions(t *testing.T) { + setFunctionCommandTestHome(t) + saveFunctionCommandTestConfig(t) + t.Chdir(t.TempDir()) + require.NoError(t, writeProjectFile(filepath.Join("volcano", "functions", "hello.js"), `exports.handler = async () => ({});`)) + require.NoError(t, writeProjectFile(filepath.Join("volcano", "functions", "order-pipeline.js"), `exports.handler = async () => ({});`)) + require.NoError(t, writeProjectFile("volcano-config.yaml", `version: 1 +project: + name: beta +functions: + - name: hello + - name: order-pipeline + kind: durable +`)) + + var deployed []string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case writeFunctionRuntimesCommandResponse(w, r): + return + case r.Method == http.MethodPost && r.URL.Path == "/projects/"+functionProjectID+"/functions/batch": + require.NoError(t, r.ParseMultipartForm(4*1024*1024)) + var manifest []struct { + Name string `json:"name"` + } + require.NoError(t, json.Unmarshal([]byte(r.FormValue("functions")), &manifest)) + data := make([]any, 0, len(manifest)) + for _, item := range manifest { + deployed = append(deployed, item.Name) + data = append(data, functionCommandPayload(functionID, item.Name)) + } + writeFunctionCommandJSON(t, w, http.StatusAccepted, map[string]any{ + "batch_id": "77777777-7777-4777-8777-777777777777", + "data": data, + }) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + out, err := executeFunctionsCommand(t, + New(cliruntime.Deps{HTTPClient: server.Client(), APIBaseURL: server.URL}), "deploy", "--all") + require.NoError(t, err) + assert.Equal(t, []string{"hello"}, deployed) + assert.Contains(t, out, "Skipping 1 durable function(s) declared in volcano-config.yaml: order-pipeline") + assert.Contains(t, out, `Deploy them with "volcano cloud durable deploy --all"`) +} + +// Naming a durable function here is a mistake worth catching locally: sent on, +// it would be refused by the server with a message about kinds rather than one +// about which command to use. +func TestFunctionsDeploySingleRefusesADurableDeclaredFunction(t *testing.T) { + setFunctionCommandTestHome(t) + saveFunctionCommandTestConfig(t) + t.Chdir(t.TempDir()) + require.NoError(t, writeProjectFile(filepath.Join("volcano", "functions", "order-pipeline.js"), `exports.handler = async () => ({});`)) + require.NoError(t, writeProjectFile("volcano-config.yaml", `version: 1 +project: + name: beta +functions: + - name: order-pipeline + kind: durable +`)) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if writeFunctionRuntimesCommandResponse(w, r) { + return + } + t.Errorf("unexpected request to %s %s", r.Method, r.URL.Path) + http.NotFound(w, r) + })) + defer server.Close() + + _, err := executeFunctionsCommand(t, + New(cliruntime.Deps{HTTPClient: server.Client(), APIBaseURL: server.URL}), "deploy", "-f", "order-pipeline") + require.ErrorContains(t, err, `"order-pipeline" is declared durable in volcano-config.yaml`) + require.ErrorContains(t, err, "volcano cloud durable deploy -f order-pipeline") +} + +// A standard function deploy in a project that also holds durable ones must not +// mention them: nothing was skipped. +func TestFunctionsDeploySingleSaysNothingAboutDurableSiblings(t *testing.T) { + setFunctionCommandTestHome(t) + saveFunctionCommandTestConfig(t) + t.Chdir(t.TempDir()) + require.NoError(t, writeProjectFile(filepath.Join("volcano", "functions", "hello.js"), `exports.handler = async () => ({});`)) + require.NoError(t, writeProjectFile(filepath.Join("volcano", "functions", "order-pipeline.js"), `exports.handler = async () => ({});`)) + require.NoError(t, writeProjectFile("volcano-config.yaml", `version: 1 +project: + name: beta +functions: + - name: order-pipeline + kind: durable +`)) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case writeFunctionRuntimesCommandResponse(w, r): + return + case r.Method == http.MethodPost && r.URL.Path == "/projects/"+functionProjectID+"/functions": + require.NoError(t, r.ParseMultipartForm(4*1024*1024)) + assert.Equal(t, "hello", r.FormValue("name")) + writeFunctionCommandJSON(t, w, http.StatusCreated, functionCommandPayload(functionID, "hello")) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + out, err := executeFunctionsCommand(t, + New(cliruntime.Deps{HTTPClient: server.Client(), APIBaseURL: server.URL}), "deploy", "-f", "hello") + require.NoError(t, err) + assert.NotContains(t, out, "Skipping") + assert.Contains(t, out, "Function 'hello' deployment started") +} + +// A manifest that cannot be parsed leaves the CLI unable to tell the kinds +// apart, and unable to read the variable declarations. Deploy refuses rather +// than uploading: creating a durable function's source as a standard function +// is not reversible, and a dropped `variable_scope` hands it every project +// variable. +func TestFunctionsDeployAllRefusesAnUnreadableManifest(t *testing.T) { + setFunctionCommandTestHome(t) + saveFunctionCommandTestConfig(t) + t.Chdir(t.TempDir()) + require.NoError(t, writeProjectFile(filepath.Join("volcano", "functions", "hello.js"), `exports.handler = async () => ({});`)) + require.NoError(t, writeProjectFile("volcano-config.yaml", "version: 1\nproject:\n name: [unterminated\n")) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case writeFunctionRuntimesCommandResponse(w, r): + return + case r.Method == http.MethodPost && r.URL.Path == "/projects/"+functionProjectID+"/functions/batch": + writeFunctionCommandJSON(t, w, http.StatusAccepted, map[string]any{ + "batch_id": "77777777-7777-4777-8777-777777777777", + "data": []any{functionCommandPayload(functionID, "hello")}, + }) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + out, err := executeFunctionsCommand(t, + New(cliruntime.Deps{HTTPClient: server.Client(), APIBaseURL: server.URL}), "deploy", "--all") + require.Error(t, err) + assert.Contains(t, err.Error(), "volcano-config.yaml") + assert.NotContains(t, out, "deployment started") +} diff --git a/internal/cmd/functions/schedulers/create.go b/internal/cmd/functions/schedulers/create.go index af3baa95..190d2585 100644 --- a/internal/cmd/functions/schedulers/create.go +++ b/internal/cmd/functions/schedulers/create.go @@ -3,15 +3,14 @@ package schedulers import ( "context" - "encoding/json" "fmt" "io" - "os" "strings" "github.com/spf13/cobra" "github.com/Kong/volcano-cli/internal/api" + "github.com/Kong/volcano-cli/internal/cmd/cmdutil" clifunction "github.com/Kong/volcano-cli/internal/function" "github.com/Kong/volcano-cli/internal/output" cliruntime "github.com/Kong/volcano-cli/internal/runtime" @@ -65,7 +64,7 @@ The --payload flag accepts either inline JSON or a path to a JSON file.`, func runCreate(ctx context.Context, opts createOptions) error { var payload map[string]any if rawPayload := strings.TrimSpace(opts.payload); rawPayload != "" { - parsed, err := loadSchedulerPayload(rawPayload) + parsed, err := cmdutil.ParseJSONObject("payload", rawPayload) if err != nil { return err } @@ -101,22 +100,3 @@ func runCreate(ctx context.Context, opts createOptions) error { output.Success(opts.out, "Created scheduler for function %q", fn.Name) return nil } - -// loadSchedulerPayload parses the --payload value as inline JSON or as a path -// to a JSON file. The caller is responsible for not invoking it on empty input. -func loadSchedulerPayload(value string) (map[string]any, error) { - data := []byte(value) - if info, statErr := os.Stat(value); statErr == nil && !info.IsDir() { - fileBytes, err := os.ReadFile(value) - if err != nil { - return nil, fmt.Errorf("failed to read payload file %q: %w", value, err) - } - data = fileBytes - } - - var payload map[string]any - if err := json.Unmarshal(data, &payload); err != nil { - return nil, fmt.Errorf("payload must be a JSON object: %w", err) - } - return payload, nil -} diff --git a/internal/cmd/local/local.go b/internal/cmd/local/local.go index a6bb6fca..4bda7fdc 100644 --- a/internal/cmd/local/local.go +++ b/internal/cmd/local/local.go @@ -12,6 +12,7 @@ import ( configcmd "github.com/Kong/volcano-cli/internal/cmd/config" databasescmd "github.com/Kong/volcano-cli/internal/cmd/databases" migrationcmd "github.com/Kong/volcano-cli/internal/cmd/databases/migration" + durablecmd "github.com/Kong/volcano-cli/internal/cmd/durable" 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" @@ -42,6 +43,7 @@ func NewResourceCommands(deps cliruntime.Deps) []*cobra.Command { storagecmd.New(localDeps), configcmd.New(localDeps), functionscmd.NewLocal(localDeps), + durablecmd.NewCloudOnly(), variablescmd.New(localDeps), newReset(deps), } diff --git a/internal/durable/durable.go b/internal/durable/durable.go new file mode 100644 index 00000000..550ae2ae --- /dev/null +++ b/internal/durable/durable.go @@ -0,0 +1,291 @@ +// Package durable performs authenticated Volcano durable function workflows. +// +// Durable functions are their own collection: they are started rather than +// invoked, an execution outlives the request that began it, and the API +// resolves a function by name or id server-side. That last part is why this +// package is thinner than internal/function, which pages a list to turn a name +// into an id. +package durable + +import ( + "context" + "fmt" + "net/http" + + "github.com/google/uuid" + + "github.com/Kong/volcano-cli/internal/api" + "github.com/Kong/volcano-cli/internal/apiclient" + clifunction "github.com/Kong/volcano-cli/internal/function" + cliruntime "github.com/Kong/volcano-cli/internal/runtime" + clisession "github.com/Kong/volcano-cli/internal/session" +) + +// Service performs durable function workflows against the current project. +type Service struct { + sessions clisession.Factory +} + +// NewService returns a durable function service. +func NewService(deps cliruntime.Deps) Service { + return Service{sessions: clisession.NewFactory(deps)} +} + +// ListPage returns one durable function page in the current project. +func (s Service) ListPage(ctx context.Context, page, limit int) (*apiclient.PaginatedDurableFunctions, error) { + authenticated, err := s.sessions.CurrentProject() + if err != nil { + return nil, err + } + + functions, err := authenticated.API.ListDurableFunctions(ctx, authenticated.ProjectID, page, limit) + if err != nil { + return nil, fmt.Errorf("failed to list durable functions: %w", err) + } + return functions, nil +} + +// Deploy uploads one packaged source archive as a durable function. It creates +// the function on the first call for a name and redeploys it after that. +// +// isPublic is left nil to keep the deployed function's current visibility, +// which is what the collection does with an absent field. A new function +// starts private. +func (s Service) Deploy(ctx context.Context, pkg clifunction.Package, isPublic *bool) (*apiclient.DurableFunction, error) { + authenticated, err := s.sessions.CurrentProject() + if err != nil { + return nil, err + } + + fn, err := authenticated.API.DeployDurableFunction(ctx, authenticated.ProjectID, api.DurableFunctionDeployInput{ + Name: pkg.Name, + Runtime: pkg.Runtime, + Handler: pkg.Handler, + SourceArchive: pkg.ArchiveData, + IsPublic: isPublic, + VariableScope: pkg.VariableScope, + Variables: pkg.Variables, + }) + if err != nil { + return nil, fmt.Errorf("failed to deploy durable function %q: %w", pkg.Name, err) + } + return fn, nil +} + +// Get returns one durable function by name or id. +func (s Service) Get(ctx context.Context, identifier string) (*apiclient.DurableFunction, error) { + authenticated, err := s.sessions.CurrentProject() + if err != nil { + return nil, err + } + + fn, err := authenticated.API.GetDurableFunction(ctx, authenticated.ProjectID, identifier) + if err != nil { + return nil, durableFunctionError("get", identifier, err) + } + return fn, nil +} + +// Delete starts deleting one durable function by name or id. +func (s Service) Delete(ctx context.Context, identifier string) error { + authenticated, err := s.sessions.CurrentProject() + if err != nil { + return err + } + + if err := authenticated.API.DeleteDurableFunction(ctx, authenticated.ProjectID, identifier); err != nil { + return durableFunctionError("delete", identifier, err) + } + return nil +} + +// StartExecution starts one execution and returns its handle. Starting is +// asynchronous by construction, so this never carries a result. +func (s Service) StartExecution( + ctx context.Context, + identifier string, + input api.DurableExecutionStartInput, +) (*apiclient.DurableExecution, error) { + authenticated, err := s.sessions.CurrentProject() + if err != nil { + return nil, err + } + + execution, err := authenticated.API.StartDurableExecution(ctx, authenticated.ProjectID, identifier, input) + if err != nil { + return nil, durableFunctionError("start an execution of", identifier, err) + } + return execution, nil +} + +// ListExecutions returns one execution page for a durable function. An empty +// status lists every execution. +func (s Service) ListExecutions( + ctx context.Context, + identifier, status string, + page, limit int, +) (*apiclient.PaginatedDurableExecutions, error) { + authenticated, err := s.sessions.CurrentProject() + if err != nil { + return nil, err + } + + executions, err := authenticated.API.ListDurableExecutions( + ctx, authenticated.ProjectID, identifier, status, page, limit) + if err != nil { + return nil, durableFunctionError("list executions of", identifier, err) + } + return executions, nil +} + +// GetExecution returns one execution, including its result once it has +// finished. +func (s Service) GetExecution( + ctx context.Context, + identifier string, + executionID uuid.UUID, +) (*apiclient.DurableExecution, error) { + authenticated, err := s.sessions.CurrentProject() + if err != nil { + return nil, err + } + + execution, err := authenticated.API.GetDurableExecution(ctx, authenticated.ProjectID, identifier, executionID) + if err != nil { + return nil, durableExecutionError("get", identifier, executionID, err) + } + return execution, nil +} + +// StopExecution stops one execution. Steps already completed are not undone. +func (s Service) StopExecution( + ctx context.Context, + identifier string, + executionID uuid.UUID, +) (*apiclient.DurableExecution, error) { + authenticated, err := s.sessions.CurrentProject() + if err != nil { + return nil, err + } + + execution, err := authenticated.API.StopDurableExecution(ctx, authenticated.ProjectID, identifier, executionID) + if err != nil { + return nil, durableExecutionError("stop", identifier, executionID, err) + } + return execution, nil +} + +// ListSchedulers returns the schedulers of one durable function. +func (s Service) ListSchedulers( + ctx context.Context, + identifier string, +) (*apiclient.FunctionSchedulerListResponse, error) { + authenticated, err := s.sessions.CurrentProject() + if err != nil { + return nil, err + } + + schedulers, err := authenticated.API.ListDurableFunctionSchedulers(ctx, authenticated.ProjectID, identifier) + if err != nil { + return nil, durableFunctionError("list schedulers of", identifier, err) + } + return schedulers, nil +} + +// CreateScheduler adds a scheduler to one durable function. Each tick starts an +// execution rather than invoking the function. +func (s Service) CreateScheduler( + ctx context.Context, + identifier string, + input api.FunctionSchedulerInput, +) (*apiclient.FunctionScheduler, error) { + authenticated, err := s.sessions.CurrentProject() + if err != nil { + return nil, err + } + + scheduler, err := authenticated.API.CreateDurableFunctionScheduler( + ctx, authenticated.ProjectID, identifier, input) + if err != nil { + return nil, durableFunctionError("create a scheduler for", identifier, err) + } + return scheduler, nil +} + +// EnableScheduler resumes a scheduler's ticks. +func (s Service) EnableScheduler( + ctx context.Context, identifier string, schedulerID uuid.UUID, +) (*apiclient.FunctionScheduler, error) { + return s.setSchedulerEnabled(ctx, identifier, schedulerID, true) +} + +// DisableScheduler stops a scheduler's ticks without deleting it. Executions +// already started keep running. +func (s Service) DisableScheduler( + ctx context.Context, identifier string, schedulerID uuid.UUID, +) (*apiclient.FunctionScheduler, error) { + return s.setSchedulerEnabled(ctx, identifier, schedulerID, false) +} + +func (s Service) setSchedulerEnabled( + ctx context.Context, identifier string, schedulerID uuid.UUID, enabled bool, +) (*apiclient.FunctionScheduler, error) { + authenticated, err := s.sessions.CurrentProject() + if err != nil { + return nil, err + } + + flag := enabled + scheduler, err := authenticated.API.UpdateDurableFunctionScheduler( + ctx, authenticated.ProjectID, identifier, schedulerID, api.FunctionSchedulerInput{Enabled: &flag}) + if err != nil { + action := "disable" + if enabled { + action = "enable" + } + return nil, durableSchedulerError(action, identifier, schedulerID, err) + } + return scheduler, nil +} + +// DeleteScheduler removes a scheduler and its run history. Executions it +// already started keep running. +func (s Service) DeleteScheduler(ctx context.Context, identifier string, schedulerID uuid.UUID) error { + authenticated, err := s.sessions.CurrentProject() + if err != nil { + return err + } + + if err := authenticated.API.DeleteDurableFunctionScheduler( + ctx, authenticated.ProjectID, identifier, schedulerID); err != nil { + return durableSchedulerError("delete", identifier, schedulerID, err) + } + return nil +} + +// durableFunctionError names the function the user asked for. The API answers +// 404 for a standard function's name as well as for an unknown one, since the +// two collections are separate, so the message says which one was searched. +func durableFunctionError(action, identifier string, err error) error { + if api.Status(err) == http.StatusNotFound { + return fmt.Errorf("durable function %q not found", identifier) + } + return fmt.Errorf("failed to %s durable function %q: %w", action, identifier, err) +} + +// durableExecutionError names both the execution and the function it was asked +// for. An execution route answers 404 for an unknown function as readily as for +// an unknown execution, and those are different mistakes; the API says which, +// so naming both leaves its answer meaning something. +func durableExecutionError(action, identifier string, executionID uuid.UUID, err error) error { + return fmt.Errorf("failed to %s durable execution %q of durable function %q: %w", + action, executionID.String(), identifier, err) +} + +// durableSchedulerError names both subjects for the same reason +// durableExecutionError does: a scheduler route answers 404 for an unknown +// durable function as readily as for an unknown scheduler. +func durableSchedulerError(action, identifier string, schedulerID uuid.UUID, err error) error { + return fmt.Errorf("failed to %s scheduler %q of durable function %q: %w", + action, schedulerID.String(), identifier, err) +} diff --git a/internal/function/scanner.go b/internal/function/scanner.go index a6933664..4b2e2825 100644 --- a/internal/function/scanner.go +++ b/internal/function/scanner.go @@ -101,6 +101,47 @@ func ScanSources(baseDir string, catalog RuntimeCatalog) ([]SourceInfo, error) { return functions, nil } +// MatchesTarget reports whether a scanned source is the one a --file value +// names, by function name or by path to its source. +func MatchesTarget(source SourceInfo, target, baseDir string) bool { + target = strings.TrimSpace(target) + if source.Name == normalizeTarget(target) { + return true + } + + sourcePath, err := filepath.Abs(source.Path) + if err != nil { + return false + } + targetPath := target + if !filepath.IsAbs(targetPath) { + targetPath = filepath.Join(baseDir, targetPath) + } + targetPath, err = filepath.Abs(targetPath) + if err != nil { + return false + } + return filepath.Clean(sourcePath) == filepath.Clean(targetPath) +} + +// FormatSourceNames lists scanned source names for an error that has to say +// what was available. +func FormatSourceNames(sources []SourceInfo) string { + names := make([]string, len(sources)) + for i, source := range sources { + names[i] = source.Name + } + return strings.Join(names, ", ") +} + +func normalizeTarget(target string) string { + name := filepath.Base(strings.TrimSpace(target)) + if extension := filepath.Ext(name); extension != "" { + name = strings.TrimSuffix(name, extension) + } + return name +} + func detectDirectoryRuntime(dirPath string, catalog RuntimeCatalog) (apiclient.FunctionRuntimeOption, string, bool) { entries, err := os.ReadDir(dirPath) if err != nil { diff --git a/internal/output/durable.go b/internal/output/durable.go new file mode 100644 index 00000000..6e6ba364 --- /dev/null +++ b/internal/output/durable.go @@ -0,0 +1,209 @@ +package output + +import ( + "encoding/json" + "fmt" + "io" + "strings" + "time" + + "github.com/Kong/volcano-cli/internal/apiclient" + "github.com/Kong/volcano-cli/internal/theme" +) + +// DurableFunctions renders one durable function list page. +func DurableFunctions(w io.Writer, page *apiclient.PaginatedDurableFunctions, commandPrefix ...string) { + if page == nil { + page = &apiclient.PaginatedDurableFunctions{} + } + + on := theme.On(w) + if len(page.Data) == 0 { + if page.Total == 0 { + fmt.Fprintln(w, "No durable functions deployed") + } else { + fmt.Fprintf(w, "No durable functions found on page %d\n", page.Page) + } + printDurableFunctionPageSummary(w, on, page) + return + } + + tableHead(w, on, true, 88, "%-20s %-15s %-12s %-15s %-15s", "Name", "Runtime", "Status", "Created", "Updated") + for _, fn := range page.Data { + fmt.Fprintf(w, "%-20s %-15s %s %-15s %-15s\n", + Truncate(fn.Name, 20), + blankString(stringPtrValue(fn.Runtime)), + statusCell(durableFunctionStatus(fn), 12, on), + FormatTimeAgo(fn.CreatedAt), + FormatTimeAgo(fn.UpdatedAt), + ) + } + printDurableFunctionPageSummary(w, on, page) + if page.HasMore { + nextPage(w, on, fmt.Sprintf("%s durable list --page %d --limit %d", + commandPathPrefix(commandPrefix), page.Page+1, page.Limit)) + } +} + +func printDurableFunctionPageSummary(w io.Writer, on bool, page *apiclient.PaginatedDurableFunctions) { + summary(w, on, "Showing %d of %d durable function(s) (page %d, limit %d)", + len(page.Data), page.Total, page.Page, page.Limit) +} + +// DurableFunction renders one durable function. +func DurableFunction(w io.Writer, fn *apiclient.DurableFunction) { + if fn == nil { + return + } + on := theme.On(w) + kv(w, on, "ID", "%s", fn.Id.String()) + kv(w, on, "Name", "%s", fn.Name) + if runtime := stringPtrValue(fn.Runtime); runtime != "" { + kv(w, on, "Runtime", "%s", runtime) + } + if handler := stringPtrValue(fn.Handler); handler != "" { + kv(w, on, "Handler", "%s", handler) + } + kv(w, on, "Status", "%s", theme.Status(durableFunctionStatus(*fn), on)) + if len(fn.DeployedRegions) > 0 { + kv(w, on, "Regions", "%s", strings.Join(fn.DeployedRegions, ", ")) + } + // Named "Anon key start" rather than "Visibility": a public durable function + // is startable with an anon key, never invocable over HTTP the way a public + // standard function is. + kv(w, on, "Anon Key Start", "%s", theme.Status(durableVisibility(*fn), on)) + kv(w, on, "Execution Timeout", "%s", formatDurableSeconds(fn.Durable.ExecutionTimeoutSeconds)) + kv(w, on, "Retention", "%d day(s)", fn.Durable.RetentionDays) + if fn.PendingDeploymentId != nil { + kv(w, on, "Pending Deployment", "%s", fn.PendingDeploymentId.String()) + } + kv(w, on, "Created", "%s", FormatTimestamp(fn.CreatedAt)) + kv(w, on, "Updated", "%s", FormatTimestamp(fn.UpdatedAt)) +} + +// DurableExecutions renders one execution list page for a durable function. +func DurableExecutions(w io.Writer, functionName string, page *apiclient.PaginatedDurableExecutions, commandPrefix ...string) { + if page == nil { + page = &apiclient.PaginatedDurableExecutions{} + } + + on := theme.On(w) + if len(page.Data) == 0 { + if page.Total == 0 { + fmt.Fprintf(w, "No executions started for durable function %q\n", functionName) + } else { + fmt.Fprintf(w, "No executions found on page %d\n", page.Page) + } + printDurableExecutionPageSummary(w, on, page) + return + } + + tableHead(w, on, true, 118, "%-36s %-24s %-10s %-16s %-15s", "ID", "Name", "Status", "Region", "Started") + for _, execution := range page.Data { + fmt.Fprintf(w, "%-36s %-24s %s %-16s %-15s\n", + execution.Id.String(), + Truncate(execution.Name, 24), + statusCell(string(execution.Status), 10, on), + Truncate(execution.Region, 16), + FormatTimeAgo(execution.CreatedAt), + ) + } + printDurableExecutionPageSummary(w, on, page) + if page.HasMore { + nextPage(w, on, fmt.Sprintf("%s durable executions list %s --page %d --limit %d", + commandPathPrefix(commandPrefix), functionName, page.Page+1, page.Limit)) + } +} + +func printDurableExecutionPageSummary(w io.Writer, on bool, page *apiclient.PaginatedDurableExecutions) { + summary(w, on, "Showing %d of %d execution(s) (page %d, limit %d)", + len(page.Data), page.Total, page.Page, page.Limit) +} + +// DurableExecution renders one execution, including its outcome once it has +// one. +func DurableExecution(w io.Writer, execution *apiclient.DurableExecution) { + if execution == nil { + return + } + on := theme.On(w) + kv(w, on, "ID", "%s", execution.Id.String()) + kv(w, on, "Name", "%s", execution.Name) + kv(w, on, "Status", "%s", theme.Status(string(execution.Status), on)) + kv(w, on, "Region", "%s", execution.Region) + kv(w, on, "Started", "%s", FormatTimestamp(execution.CreatedAt)) + if execution.CompletedAt != nil { + kv(w, on, "Completed", "%s", FormatTimestamp(*execution.CompletedAt)) + kv(w, on, "Duration", "%s", formatDurableDuration(execution.CreatedAt, *execution.CompletedAt)) + } + if execution.Error != nil { + if errorType := stringPtrValue(execution.Error.Type); errorType != "" { + kv(w, on, "Error Type", "%s", errorType) + } + if message := stringPtrValue(execution.Error.Message); message != "" { + kv(w, on, "Error", "%s", message) + } + } + printDurableExecutionResult(w, on, execution) +} + +// printDurableExecutionResult distinguishes the three things an absent result +// can mean, because the difference is what a caller polling for one needs: +// still running, finished with nothing, or finished too long ago to still hold +// the result. +func printDurableExecutionResult(w io.Writer, on bool, execution *apiclient.DurableExecution) { + if execution.ResultExpired != nil && *execution.ResultExpired { + kv(w, on, "Result", "%s", theme.Dim("no longer retained", on)) + return + } + if execution.Result == nil { + return + } + encoded, err := json.MarshalIndent(execution.Result, "", " ") + if err != nil { + kv(w, on, "Result", "%v", execution.Result) + return + } + kv(w, on, "Result", "%s", string(encoded)) +} + +// DurableSchedulers renders the schedulers of one durable function. A tick +// starts an execution, so the run columns say when the scheduler last started +// one rather than when the function last ran. +func DurableSchedulers(w io.Writer, functionName string, resp *apiclient.FunctionSchedulerListResponse) { + if resp == nil { + resp = &apiclient.FunctionSchedulerListResponse{} + } + if len(resp.Data) == 0 { + fmt.Fprintf(w, "No schedulers configured for durable function %q\n", functionName) + return + } + schedulerTable(w, theme.On(w), resp.Data) +} + +func durableFunctionStatus(fn apiclient.DurableFunction) string { + status := strings.TrimSpace(string(fn.Status)) + if status == "" { + return "-" + } + return status +} + +func durableVisibility(fn apiclient.DurableFunction) string { + if fn.IsPublic { + return "allowed" + } + return "denied" +} + +func formatDurableSeconds(seconds int64) string { + return (time.Duration(seconds) * time.Second).String() +} + +func formatDurableDuration(from, to time.Time) string { + elapsed := to.Sub(from) + if elapsed < 0 { + return "-" + } + return elapsed.Round(time.Second).String() +} diff --git a/internal/output/functions.go b/internal/output/functions.go index 28b1b69c..6c5ce33a 100644 --- a/internal/output/functions.go +++ b/internal/output/functions.go @@ -172,7 +172,6 @@ func Schedulers(w io.Writer, fn *apiclient.Function, resp *apiclient.FunctionSch if resp == nil { resp = &apiclient.FunctionSchedulerListResponse{} } - on := theme.On(w) if len(resp.Data) == 0 { if fn != nil { fmt.Fprintf(w, "No schedulers configured for function %q\n", fn.Name) @@ -181,9 +180,12 @@ func Schedulers(w io.Writer, fn *apiclient.Function, resp *apiclient.FunctionSch } return } + schedulerTable(w, theme.On(w), resp.Data) +} +func schedulerTable(w io.Writer, on bool, schedulers []apiclient.FunctionScheduler) { tableHead(w, on, true, 130, "%-36s %-24s %-9s %-15s %-20s %-15s", "ID", "Name", "State", "Cron", "Next Run", "Last Run") - for _, scheduler := range resp.Data { + for _, scheduler := range schedulers { fmt.Fprintf(w, "%-36s %-24s %s %-15s %-20s %-15s\n", schedulerID(scheduler), Truncate(blankString(stringPtrValue(scheduler.Name)), 24), diff --git a/internal/projectconfig/manifest.go b/internal/projectconfig/manifest.go index 7e359a14..1c0f516a 100644 --- a/internal/projectconfig/manifest.go +++ b/internal/projectconfig/manifest.go @@ -21,6 +21,10 @@ const ( // ManifestVersion is the only currently supported manifest schema version. ManifestVersion = 1 + // FunctionKindDurable is the manifest value that declares a durable + // function. + FunctionKindDurable = "durable" + manifestDir = "volcano" nestedManifestPath = "volcano/volcano-config.yaml" rootManifestPath = "volcano-config.yaml" @@ -264,16 +268,43 @@ type HostedPageManifest struct { // FunctionManifest declares configuration for one deployed function. type FunctionManifest struct { - Name string `yaml:"name" json:"name"` - Public *bool `yaml:"public,omitempty" json:"public,omitempty"` - VariableScope *string `yaml:"variable_scope,omitempty" json:"variable_scope,omitempty"` - Variables *[]string `yaml:"variables,omitempty" json:"variables,omitempty"` + Name string `yaml:"name" json:"name"` + // Kind is asserted rather than applied: a function's kind is fixed when it + // is created, so the server compares this against the deployed function. + // Empty means standard, which is what every manifest written before durable + // functions existed declares. + Kind *string `yaml:"kind,omitempty" json:"kind,omitempty"` + Public *bool `yaml:"public,omitempty" json:"public,omitempty"` + VariableScope *string `yaml:"variable_scope,omitempty" json:"variable_scope,omitempty"` + Variables *[]string `yaml:"variables,omitempty" json:"variables,omitempty"` + // The invocation settings describe synchronous HTTP invocation, which a + // durable function does not have; the server rejects them on one. InvocationMode *string `yaml:"invocation_mode,omitempty" json:"invocation_mode,omitempty"` HTTPAuthMode *string `yaml:"http_auth_mode,omitempty" json:"http_auth_mode,omitempty"` OpenAPISpec any `yaml:"openapi_spec,omitempty" json:"openapi_spec,omitempty"` Schedulers *[]SchedulerManifest `yaml:"schedulers,omitempty" json:"schedulers,omitempty"` } +// DurableFunctionNames returns the functions the manifest declares durable, in +// declaration order. +// +// A kind cannot be changed once a function exists, so deploying one of these +// through the standard collection would create the wrong kind of function +// permanently. The deploy commands use this to keep each name with its own +// collection. +func (m *Manifest) DurableFunctionNames() []string { + if m == nil || m.Functions == nil { + return nil + } + var names []string + for _, fn := range *m.Functions { + if fn.Kind != nil && strings.TrimSpace(*fn.Kind) == FunctionKindDurable { + names = append(names, fn.Name) + } + } + return names +} + // SchedulerManifest declares one scheduler attached to a function. Regions is // parsed only to reject manifests using the removed field with a clear error. type SchedulerManifest struct { @@ -497,33 +528,54 @@ type FunctionVariableDeclaration struct { // `value: ${API_KEY}` elsewhere in the manifest is unset in the deploying // shell — and hand the function every project variable instead. func FunctionVariableDeclarations(fileArg string) (map[string]FunctionVariableDeclaration, error) { + read, err := ReadFunctionDeployManifest(fileArg) + if err != nil { + return nil, err + } + return read.Declarations, nil +} + +// FunctionDeployManifest is what a function deploy needs from the manifest: +// each function's variable declaration, and the names declared durable so the +// standard collection leaves them to `volcano cloud durable deploy`. +type FunctionDeployManifest struct { + Declarations map[string]FunctionVariableDeclaration + DurableNames map[string]bool +} + +// ReadFunctionDeployManifest reads both of a deploy's manifest inputs in one +// pass, with the absence-versus-invalidity rule described on +// FunctionVariableDeclarations. +func ReadFunctionDeployManifest(fileArg string) (FunctionDeployManifest, error) { + read := FunctionDeployManifest{ + Declarations: map[string]FunctionVariableDeclaration{}, + DurableNames: map[string]bool{}, + } path, err := ResolveManifestPath(fileArg) if errors.Is(err, ErrManifestNotFound) { - return map[string]FunctionVariableDeclaration{}, nil + return read, nil } if err != nil { - return nil, err + return FunctionDeployManifest{}, err } manifest, _, err := Load(path) if err != nil { - return nil, fmt.Errorf("failed to read function variable scope from volcano-config.yaml: %w\nfix the manifest (or export the environment variables it references) and deploy again", err) + return FunctionDeployManifest{}, fmt.Errorf("failed to read function variable scope from volcano-config.yaml: %w\nfix the manifest (or export the environment variables it references) and deploy again", err) } if manifest.Functions == nil { - return map[string]FunctionVariableDeclaration{}, nil + return read, nil } - declarations := make(map[string]FunctionVariableDeclaration) for _, function := range *manifest.Functions { - if function.VariableScope == nil && function.Variables == nil { - continue - } - declarations[function.Name] = FunctionVariableDeclaration{ - VariableScope: function.VariableScope, - Variables: function.Variables, + if function.VariableScope != nil || function.Variables != nil { + read.Declarations[function.Name] = FunctionVariableDeclaration{ + VariableScope: function.VariableScope, + Variables: function.Variables, + } } } - if len(declarations) == 0 { - return declarations, nil + for _, name := range manifest.DurableFunctionNames() { + read.DurableNames[name] = true } - return declarations, nil + return read, nil } diff --git a/internal/projectconfig/manifest_durable_test.go b/internal/projectconfig/manifest_durable_test.go new file mode 100644 index 00000000..fc1e904e --- /dev/null +++ b/internal/projectconfig/manifest_durable_test.go @@ -0,0 +1,48 @@ +package projectconfig + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// The manifest decodes strictly, so a declared kind has to be a known field +// here or every project holding a durable function fails to parse. It uploads +// unchanged: the server asserts it against the deployed function rather than +// applying it. +func TestManifestParsesAndUploadsFunctionKind(t *testing.T) { + manifest, err := Parse([]byte(`version: 1 +functions: + - name: hello + - name: order-pipeline + kind: durable +`), noEnv) + require.NoError(t, err) + + assert.Equal(t, []string{"order-pipeline"}, manifest.DurableFunctionNames()) + + encoded, err := manifest.uploadBody() + require.NoError(t, err) + var body map[string]any + require.NoError(t, json.Unmarshal(encoded, &body)) + + functions, ok := body["functions"].([]any) + require.True(t, ok) + standard, ok := functions[0].(map[string]any) + require.True(t, ok) + durable, ok := functions[1].(map[string]any) + require.True(t, ok) + assert.NotContains(t, standard, "kind") + assert.Equal(t, "durable", durable["kind"]) +} + +func TestDurableFunctionNamesWithoutFunctions(t *testing.T) { + manifest, err := Parse([]byte("version: 1\n"), noEnv) + require.NoError(t, err) + assert.Empty(t, manifest.DurableFunctionNames()) + + var missing *Manifest + assert.Empty(t, missing.DurableFunctionNames()) +} diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index bd473528..78fe6ad5 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -3240,6 +3240,559 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /projects/{id}/durable-functions/{functionId}/executions/{executionId}/stop: + post: + tags: + - Durable Functions + summary: Stop a durable execution + description: | + Cancels a running execution. Its completed steps are not undone; the + execution stops where it is and becomes `stopped`. + operationId: stopDurableExecution + security: + - UserToken: [] + parameters: + - $ref: '#/components/parameters/ProjectId' + - $ref: '#/components/parameters/DurableFunctionId' + - $ref: '#/components/parameters/DurableExecutionId' + responses: + '200': + description: Execution stopped + content: + application/json: + schema: + $ref: '#/components/schemas/DurableExecution' + '404': + description: Durable function or execution not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '409': + description: Execution has not started yet + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '503': + description: | + Durable execution is not available in this environment. Returned by + a deployment that has no durable execution engine, such as a local + one; the request is not retryable there. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /projects/{id}/durable-functions/{functionId}/executions/{executionId}: + get: + tags: + - Durable Functions + summary: Get a durable execution + description: | + Returns the execution's current state, including its `result` once it has + succeeded. Poll this to wait for an execution to finish. + operationId: getDurableExecution + security: + - UserToken: [] + parameters: + - $ref: '#/components/parameters/ProjectId' + - $ref: '#/components/parameters/DurableFunctionId' + - $ref: '#/components/parameters/DurableExecutionId' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/DurableExecution' + '404': + description: Durable function or execution not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '503': + description: | + Durable execution is not available in this environment. Returned by + a deployment that has no durable execution engine, such as a local + one; the request is not retryable there. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /projects/{id}/durable-functions/{functionId}/executions: + post: + tags: + - Durable Functions + summary: Start a durable execution + description: | + Starts an execution and returns its handle. Never returns a result: an + execution can outlive any request a client could hold open, so the + result is read back from + `GET /projects/{id}/durable-functions/{functionId}/executions/{executionId}`. + + The request body is the execution's input and must be valid JSON if + present. An empty body starts the execution with no input. + + Send `X-Volcano-Execution-Name` to make the start idempotent: repeating a + start with the same name returns the existing execution instead of + beginning a second one. + + Each execution counts against the project's function invocation + allowance, and the number of executions in flight at once is capped by + the plan. + operationId: startDurableExecution + security: + - UserToken: [] + parameters: + - $ref: '#/components/parameters/ProjectId' + - $ref: '#/components/parameters/DurableFunctionId' + - name: X-Volcano-Execution-Name + in: header + required: false + description: | + Idempotency key for this execution. Generated when omitted. + schema: + type: string + requestBody: + required: false + content: + application/json: + schema: + description: Input passed to the function, up to 256 KiB. + responses: + '202': + description: Execution accepted and started + content: + application/json: + schema: + $ref: '#/components/schemas/DurableExecution' + '400': + description: Payload is not valid JSON, or the execution name is invalid + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Durable function not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '409': + description: Function is not deployed yet, or has no deployed region + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '413': + description: Payload exceeds the maximum execution input size + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '429': + description: Too many executions already in flight for this project + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '503': + description: | + Durable execution is not available in this environment. Returned by + a deployment that has no durable execution engine, such as a local + one; the request is not retryable there. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + get: + tags: + - Durable Functions + summary: List a durable function's executions + description: | + Returns the platform's last observed status for each execution; listing + does not poll each one. Fetch a single execution for its live state. + operationId: listDurableExecutions + security: + - UserToken: [] + parameters: + - $ref: '#/components/parameters/ProjectId' + - $ref: '#/components/parameters/DurableFunctionId' + - $ref: '#/components/parameters/Page' + - $ref: '#/components/parameters/Limit' + - name: status + in: query + required: false + description: Return only executions in this status. + schema: + $ref: '#/components/schemas/DurableExecutionStatus' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedDurableExecutions' + '400': + description: Unsupported status filter + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Durable function not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /projects/{id}/durable-functions/{functionId}/schedulers: + get: + tags: + - Durable Functions + summary: List schedulers for a durable function + description: | + The durable collection's counterpart to + `/projects/{id}/functions/{functionId}/schedulers`. A standard + function's id is not accepted here, and a durable function's id is not + accepted there. + operationId: listDurableFunctionSchedulers + security: + - UserToken: [] + parameters: + - $ref: '#/components/parameters/ProjectId' + - $ref: '#/components/parameters/DurableFunctionId' + responses: + '200': + description: Durable function schedulers + content: + application/json: + schema: + $ref: '#/components/schemas/FunctionSchedulerListResponse' + '404': + description: Durable function not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + tags: + - Durable Functions + summary: Create a scheduler for a durable function + description: | + Each tick starts an execution rather than invoking the function, under + an execution name derived from the run, so a retried tick resolves to + the execution it already started. Requested regions must be a subset of + the function's deployed regions. + + A tick draws on the same invocation allowance and concurrency cap a + manual start does, and a tick that would exceed the cap fails that run. + operationId: createDurableFunctionScheduler + security: + - UserToken: [] + parameters: + - $ref: '#/components/parameters/ProjectId' + - $ref: '#/components/parameters/DurableFunctionId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateFunctionSchedulerRequest' + responses: + '201': + description: Scheduler created + content: + application/json: + schema: + $ref: '#/components/schemas/FunctionScheduler' + '400': + description: Invalid schedule or geofenced region + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Durable function not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /projects/{id}/durable-functions/{functionId}/schedulers/{schedulerId}: + get: + tags: + - Durable Functions + summary: Get a durable function scheduler + operationId: getDurableFunctionScheduler + security: + - UserToken: [] + parameters: + - $ref: '#/components/parameters/ProjectId' + - $ref: '#/components/parameters/DurableFunctionId' + - $ref: '#/components/parameters/SchedulerId' + responses: + '200': + description: Durable function scheduler + content: + application/json: + schema: + $ref: '#/components/schemas/FunctionScheduler' + '404': + description: Durable function or scheduler not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + patch: + tags: + - Durable Functions + summary: Update a durable function scheduler + operationId: updateDurableFunctionScheduler + security: + - UserToken: [] + parameters: + - $ref: '#/components/parameters/ProjectId' + - $ref: '#/components/parameters/DurableFunctionId' + - $ref: '#/components/parameters/SchedulerId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateFunctionSchedulerRequest' + responses: + '200': + description: Scheduler updated + content: + application/json: + schema: + $ref: '#/components/schemas/FunctionScheduler' + '400': + description: Invalid schedule or geofenced region + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Durable function or scheduler not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + tags: + - Durable Functions + summary: Delete a durable function scheduler + operationId: deleteDurableFunctionScheduler + security: + - UserToken: [] + parameters: + - $ref: '#/components/parameters/ProjectId' + - $ref: '#/components/parameters/DurableFunctionId' + - $ref: '#/components/parameters/SchedulerId' + responses: + '204': + description: Scheduler deleted + '404': + description: Durable function or scheduler not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /projects/{id}/durable-functions/{functionId}: + get: + tags: + - Durable Functions + summary: Get durable function by ID or name + operationId: getDurableFunction + security: + - UserToken: [] + parameters: + - $ref: '#/components/parameters/ProjectId' + - $ref: '#/components/parameters/DurableFunctionId' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/DurableFunction' + '404': + description: Durable function not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + tags: + - Durable Functions + summary: Delete a durable function + description: | + Accepted for asynchronous teardown; the work continues after the + response. Executions still running do not survive the function. History + already retained is governed by the function's `retention_days`, which + this does not shorten. + operationId: deleteDurableFunction + security: + - UserToken: [] + parameters: + - $ref: '#/components/parameters/ProjectId' + - $ref: '#/components/parameters/DurableFunctionId' + responses: + '202': + description: Deletion accepted and teardown started + '404': + description: | + Durable function not found. Also returned for an id that names a + durable function in another project, so the response cannot be used + to tell the two apart. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /projects/{id}/durable-functions: + get: + tags: + - Durable Functions + summary: List all durable functions in a project + description: | + Standard functions never appear here, and durable functions never appear + under `/projects/{id}/functions`. The two are separate collections. + operationId: listDurableFunctions + security: + - UserToken: [] + parameters: + - $ref: '#/components/parameters/ProjectId' + - $ref: '#/components/parameters/Page' + - $ref: '#/components/parameters/Limit' + - $ref: '#/components/parameters/Search' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedDurableFunctions' + '400': + description: Bad request - invalid pagination parameters + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Project not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + tags: + - Durable Functions + summary: Create or update a durable function + description: | + Upload a durable function source bundle. Creates the function on the + first call for a name and redeploys it on every call after that, the + same create-or-update contract `POST /projects/{id}/functions` has. + + Volcano builds and deploys asynchronously. A deployment that starts + immediately returns `status: provisioning`, then transitions to `active` + or `failed`; a deployment that has to wait for a running one is exposed + through `pending_deployment_id`. Existing executions keep running + against the runtime they started on. + + The `durable` configuration is derived from the project's plan rather + than supplied here, and is fixed once the function exists. A name + already held by a standard function is rejected with 409: a function + cannot change kind. + operationId: createDurableFunction + security: + - UserToken: [] + parameters: + - $ref: '#/components/parameters/ProjectId' + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - name + - code + - runtime + properties: + name: + type: string + description: DNS-safe function name (lowercase letters, numbers, hyphens; cannot start or end with hyphen) + maxLength: 63 + pattern: ^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$ + example: order-pipeline + code: + type: string + format: binary + description: ZIP or tar.gz archive containing function source code plus dependency manifests/lockfiles. + runtime: + type: string + enum: + - nodejs22.x + - nodejs24.x + - python3.10 + - python3.11 + - python3.12 + - python3.13 + - python3.14 + - ruby3.3 + - ruby3.4 + - ruby4.0 + description: | + Runtime environment. Required. Must be a runtime that supports + durable execution; one that does not is rejected with 400. + example: nodejs24.x + handler: + type: string + description: The name of the function to invoke. Defaults to "handler" if not specified. + default: handler + example: handler + is_public: + type: boolean + default: false + description: Whether anon keys with `functions.invoke` may start an execution. + responses: + '200': + description: Existing durable function updated; its deployment was started or queued + content: + application/json: + schema: + $ref: '#/components/schemas/DurableFunction' + '201': + description: Durable function created and deployment workflow started + content: + application/json: + schema: + $ref: '#/components/schemas/DurableFunction' + '400': + description: Bad request (invalid archive, unsupported runtime, invalid name) + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Durable function limit exceeded for the project + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '409': + description: Name is held by a standard function, or a deletion is queued or running + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '503': + description: Durable functions are unavailable for this project or region + content: + application/json: + schema: + $ref: '#/components/schemas/Error' /projects/{id}/frontends: get: tags: @@ -10623,6 +11176,21 @@ components: type: string format: uuid description: Function ID + DurableFunctionId: + name: functionId + in: path + required: true + schema: + type: string + description: Durable function ID, or its name within the project + DurableExecutionId: + name: executionId + in: path + required: true + schema: + type: string + format: uuid + description: Durable execution ID Limit: name: limit in: query @@ -13773,6 +14341,178 @@ components: - deployed_regions - created_at - updated_at + DurableFunctionConfig: + type: object + description: | + Execution limits the function was created with, derived from the + project's plan. Fixed for the life of the function: changing them means + creating a new one. + properties: + execution_timeout_seconds: + type: integer + format: int64 + description: How long a single execution may run before it is timed out. + retention_days: + type: integer + format: int64 + description: How long a finished execution's result and history are retained. + required: + - execution_timeout_seconds + - retention_days + DurableFunction: + type: object + description: | + A durable function. Separate from `Function` because a durable function + is invoked only through its own execution endpoints, so it has no + invocation mode, HTTP auth mode, OpenAPI document or invoke URL, and it + carries a `durable` configuration that a standard function has no field + for. + properties: + id: + type: string + format: uuid + project_id: + type: string + format: uuid + name: + type: string + maxLength: 63 + pattern: ^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$ + status: + type: string + enum: + - provisioning + - active + - failed + - deleting + provisioning_started_at: + type: string + format: date-time + description: Timestamp when the current provisioning phase started + is_public: + type: boolean + description: | + Whether anon keys may start executions of this function through + `POST /durable-functions/{functionId}/executions`. + + When `true`, an anon key holding `functions.invoke` can start an + execution. When `false` (the default) only service keys and auth + user tokens can. Reading and stopping an execution always require + the project owner's token, whatever this is set to. + + Set it when the function is created. Durable functions have no + update endpoint, so changing visibility later means redeploying. + + A public durable function is startable, never invocable: it is not + reachable through `POST /functions/{functionId}/invoke` or a + function URL, which answer `404` for either visibility. + durable: + $ref: '#/components/schemas/DurableFunctionConfig' + deployed_regions: + type: array + items: + type: string + description: Regions where this function is currently deployed + runtime: + type: string + handler: + type: string + current_deployment_id: + type: string + format: uuid + description: Identifier of the latest deployment operation + pending_deployment_id: + type: string + format: uuid + description: Newest queued deployment that will run after the current operation + last_invoked_at: + type: string + format: date-time + description: Most recent successful invocation timestamp + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + required: + - id + - project_id + - name + - status + - is_public + - durable + - deployed_regions + - created_at + - updated_at + DurableExecutionStatus: + type: string + enum: + - pending + - running + - succeeded + - failed + - timed_out + - stopped + description: | + Lifecycle state of an execution. `pending` covers the window between the + platform reserving the execution name and the function accepting the + start, and has no counterpart once the execution is under way. + `succeeded`, `failed`, `timed_out` and `stopped` are terminal. + DurableExecutionError: + type: object + description: Why a failed or timed-out execution ended. + properties: + type: + type: string + message: + type: string + DurableExecution: + type: object + properties: + id: + type: string + format: uuid + function_id: + type: string + format: uuid + name: + type: string + description: | + Idempotency key for the execution. Supplied by the client through + `X-Volcano-Execution-Name`, otherwise generated. + status: + $ref: '#/components/schemas/DurableExecutionStatus' + region: + type: string + description: | + Region the execution runs in. An execution is pinned to one region + for its whole life because its checkpoints live there. + result: + description: | + Whatever the function returned, verbatim. Absent while the execution + is still running, and absent once its retention period has lapsed. + result_expired: + type: boolean + description: | + `true` when the execution is terminal but its result is no longer + retained, which distinguishes a discarded result from an empty one. + error: + $ref: '#/components/schemas/DurableExecutionError' + created_at: + type: string + format: date-time + completed_at: + type: string + format: date-time + description: Present once the execution has reached a terminal status. + required: + - id + - function_id + - name + - status + - region + - created_at FunctionDeployment: type: object properties: @@ -14071,12 +14811,25 @@ components: type: string example: 🇺🇸 description: Country flag emoji associated with the region's geography. + FunctionKind: + type: string + enum: + - standard + - durable + default: standard + description: | + Which kind of function this is. `standard` runs once per invocation. + `durable` checkpoints its progress and resumes from the last completed + step, and is invoked asynchronously through its own executions + collection. A function's kind is fixed when it is created and cannot be + changed afterwards. Omitting this field means `standard`. FunctionRuntimeOption: type: object required: - name - language - default + - durable_capable - deployment properties: name: @@ -14090,6 +14843,12 @@ components: default: type: boolean description: Whether this runtime is the CLI default for its language. + durable_capable: + type: boolean + description: >- + Whether a durable function can be authored on this runtime. Only + runtimes with a durable authoring API report true, and a durable + deploy naming any other runtime is rejected. deployment: $ref: '#/components/schemas/FunctionRuntimeDeployment' FunctionRuntimesResponse: @@ -14113,6 +14872,15 @@ components: function_id: type: string format: uuid + function_kind: + allOf: + - $ref: '#/components/schemas/FunctionKind' + description: | + Which collection the scheduled function belongs to. A project-wide + scheduler list mixes both kinds, and this is what says whether the + function is read back from `/projects/{id}/functions` or + `/projects/{id}/durable-functions` — and whether a tick invokes it + or starts a durable execution. name: type: string enabled: @@ -14541,6 +15309,56 @@ components: - limit - total - has_more + PaginatedDurableFunctions: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/DurableFunction' + page: + type: integer + description: Current page number (1-indexed) + limit: + type: integer + description: Number of items per page + total: + type: integer + description: Total number of items across all pages + has_more: + type: boolean + description: Whether there are more pages available + required: + - data + - page + - limit + - total + - has_more + PaginatedDurableExecutions: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/DurableExecution' + page: + type: integer + description: Current page number (1-indexed) + limit: + type: integer + description: Number of items per page + total: + type: integer + description: Total number of items across all pages + has_more: + type: boolean + description: Whether there are more pages available + required: + - data + - page + - limit + - total + - has_more PaginatedProjectCustomDomains: type: object properties: diff --git a/tests/e2e/api/durable_functions_test.go b/tests/e2e/api/durable_functions_test.go new file mode 100644 index 00000000..ec506326 --- /dev/null +++ b/tests/e2e/api/durable_functions_test.go @@ -0,0 +1,394 @@ +package api + +import ( + "path/filepath" + "strings" + "testing" + "time" + + "github.com/google/uuid" +) + +const ( + apiE2EDurableExecutionTimeout = 5 * time.Minute + apiE2EDurableStopTimeout = 5 * time.Minute + // A minute for the next tick, plus room for the run to be claimed and the + // execution to appear. + apiE2EDurableSchedulerTickTimeout = 5 * time.Minute +) + +// A durable invocation speaks an envelope protocol rather than taking the +// caller's payload as the event, so the fixture is written against the raw +// protocol and depends on nothing but the source it ships. Authoring one with +// the SDK is covered where the SDK is: this suite is about the commands. +const apiE2EDurableSource = ` +exports.handler = async (envelope) => ({ + Status: "SUCCEEDED", + Result: JSON.stringify({ echoed: durableInput(envelope), ran: true }), +}); + +function durableInput(envelope) { + const operationId = String(envelope.DurableExecutionArn || "").split("/").pop(); + const state = envelope.InitialExecutionState || {}; + const operations = state.Operations || []; + const execution = operations.find((op) => op.Id === operationId) || operations[0] || {}; + return (execution.ExecutionDetails || {}).InputPayload || null; +} +` + +// apiE2EDurableSuspendedSource never finishes. `PENDING` means "I suspended, +// resume me later", and what resumes an execution is whatever the handler +// checkpointed before returning; this one checkpoints nothing, so it sits in +// `running` until something stops it. +// +// That is the only state `executions stop` can be observed working in: a +// handler that returns `SUCCEEDED` has already finished before a stop lands. +const apiE2EDurableSuspendedSource = `exports.handler = async () => ({ Status: "PENDING" });` + +// TestAPIE2ECloudDurableFunctions walks the durable group against real +// infrastructure: declare, deploy, start, poll, page, stop, redeploy, delete. +// +// Every command in the group and every flag on it runs here, in one project, +// because each durable deploy is a real build — two functions cover the whole +// surface where a test per command would pay for a build per command. The +// control-plane refusals (unknown names, bad ids, flag conflicts) are covered +// by the hosting repo's `TestCLIE2ESmokeDurableFunctions`, which needs no +// provisioning. +func TestAPIE2ECloudDurableFunctions(t *testing.T) { + env := setupAPIE2E(t, "cloud-durable") + writeAPIE2EDurableProject(t, env.projectDir) + + env.loginAndUse(t) + env.runCloudCLI(t, "durable", "list").requireSuccess(t, "No durable functions") + + // --all takes its targets from the manifest, so both durable entries deploy + // and the standard one does not. + env.runCloudCLI(t, "durable", "deploy", "--all").requireSuccess(t, + "[1/2] Deploying order-pipeline", "[2/2] Deploying sleeper", + "2/2 durable function(s) deployment started") + t.Cleanup(func() { + env.runCloudCLI(t, "durable", "delete", "order-pipeline", "--yes") + env.runCloudCLI(t, "durable", "delete", "sleeper", "--yes") + }) + + env.waitForCloudCLIContains(t, apiE2EFunctionDeploymentTimeout, "Status: active", + "durable", "get", "order-pipeline") + env.runCloudCLI(t, "durable", "get", "order-pipeline"). + requireSuccess(t, "Anon Key Start: denied", "Execution Timeout:", "Retention:") + env.runCloudCLI(t, "durable", "list").requireSuccess(t, "order-pipeline", "sleeper", "active") + + // A durable function is in its own collection: the standard commands must + // not find it, and it must not be deployable as a standard function. + env.runCloudCLI(t, "functions", "list").requireNotContains(t, "order-pipeline") + env.runCloudCLI(t, "functions", "get", "order-pipeline").requireFailure(t) + env.runCloudCLI(t, "functions", "deploy", "-f", "order-pipeline"). + requireFailure(t, "declared durable in volcano-config.yaml") + + requireAPIE2EDurableListPages(t, env) + requireAPIE2EDurableStarts(t, env) + requireAPIE2EDurableExecutionPages(t, env) + requireAPIE2EDurableSchedulers(t, env) + requireAPIE2EDurableStop(t, env) + requireAPIE2EDurableVisibility(t, env) + + env.runCloudCLI(t, "durable", "delete", "order-pipeline", "--yes"). + requireSuccess(t, "Durable function 'order-pipeline' deletion started") + env.runCloudCLI(t, "durable", "delete", "sleeper", "--yes"). + requireSuccess(t, "Durable function 'sleeper' deletion started") + env.waitForCloudCLIContains(t, apiE2EResourceDeleteTimeout, "No durable functions", + "durable", "list") +} + +// requireAPIE2EDurableListPages covers --page and --limit on the function +// listing. Paging is where a listing command is most often wrong in a way the +// default page hides: a limit that never reaches the API still returns +// everything. +func requireAPIE2EDurableListPages(t *testing.T, env *apiE2E) { + t.Helper() + + first := env.runCloudCLI(t, "durable", "list", "--limit", "1") + first.requireSuccess(t, "Showing 1 of 2 durable function(s) (page 1, limit 1)", + "durable list --page 2 --limit 1") + + second := env.runCloudCLI(t, "durable", "list", "--page", "2", "--limit", "1") + second.requireSuccess(t, "Showing 1 of 2 durable function(s) (page 2, limit 1)") + + if apiE2EDurableListedName(t, first.output) == apiE2EDurableListedName(t, second.output) { + t.Fatalf("page 1 and page 2 listed the same durable function:\n%s\n%s", + first.output, second.output) + } + + env.runCloudCLI(t, "durable", "list", "--page", "9"). + requireSuccess(t, "No durable functions found on page 9") +} + +// requireAPIE2EDurableStarts covers `durable start` in all three input forms — +// inline JSON, a file, and none — and the idempotency key that makes a retried +// start safe. +func requireAPIE2EDurableStarts(t *testing.T, env *apiE2E) { + t.Helper() + + // Starting is asynchronous by construction: the command returns a handle, + // and a result exists only once the execution has run. + start := env.runCloudCLI(t, "durable", "start", "order-pipeline", + "--input", `{"order_id":4417}`, "--name", "cli-e2e-order") + start.requireSuccess(t, "Execution cli-e2e-order started") + executionID := apiE2EDurableExecutionID(t, start.output) + + env.waitForCloudCLIContains(t, apiE2EDurableExecutionTimeout, "Status: succeeded", + "durable", "executions", "get", "order-pipeline", executionID) + env.runCloudCLI(t, "durable", "executions", "get", "order-pipeline", executionID). + requireSuccess(t, "4417", "Duration:") + env.runCloudCLI(t, "durable", "executions", "list", "order-pipeline", "--status", "succeeded"). + requireSuccess(t, executionID, "cli-e2e-order") + + // The name is the idempotency key, so repeating the start returns the + // execution that already exists rather than beginning a second. + env.runCloudCLI(t, "durable", "start", "order-pipeline", + "--input", `{"order_id":4417}`, "--name", "cli-e2e-order"). + requireSuccess(t, executionID) + env.runCloudCLI(t, "durable", "executions", "list", "order-pipeline"). + requireSuccess(t, "Showing 1 of 1 execution(s)") + + // --input takes a path as readily as inline JSON, which is how a payload too + // unwieldy for a shell argument is passed. The file's contents have to reach + // the function, not merely be accepted: the fixture echoes its input back. + inputPath := filepath.Join(env.projectDir, "order.json") + writeAPIE2EFile(t, inputPath, `{"order_id":8231,"source":"file"}`) + fromFile := env.runCloudCLI(t, "durable", "start", "order-pipeline", + "--input", inputPath, "--name", "cli-e2e-file") + fromFile.requireSuccess(t, "Execution cli-e2e-file started") + fileExecutionID := apiE2EDurableExecutionID(t, fromFile.output) + env.waitForCloudCLIContains(t, apiE2EDurableExecutionTimeout, "Status: succeeded", + "durable", "executions", "get", "order-pipeline", fileExecutionID) + env.runCloudCLI(t, "durable", "executions", "get", "order-pipeline", fileExecutionID). + requireSuccess(t, "8231", "file") + + // The shortest start there is: no input, and a name Volcano generates. No + // input reaches the function as no input, which the fixture reports as a + // null echo rather than an empty object. + bare := env.runCloudCLI(t, "durable", "start", "order-pipeline") + bare.requireSuccess(t, "started") + bareExecutionID := apiE2EDurableExecutionID(t, bare.output) + env.waitForCloudCLIContains(t, apiE2EDurableExecutionTimeout, "Status: succeeded", + "durable", "executions", "get", "order-pipeline", bareExecutionID) + env.runCloudCLI(t, "durable", "executions", "get", "order-pipeline", bareExecutionID). + requireSuccess(t, `"echoed": null`) +} + +// requireAPIE2EDurableExecutionPages covers --page and --limit on the execution +// listing, which by now has three executions to page through. +func requireAPIE2EDurableExecutionPages(t *testing.T, env *apiE2E) { + t.Helper() + + first := env.runCloudCLI(t, "durable", "executions", "list", "order-pipeline", "--limit", "1") + first.requireSuccess(t, "Showing 1 of 3 execution(s) (page 1, limit 1)", + "durable executions list order-pipeline --page 2 --limit 1") + + second := env.runCloudCLI(t, "durable", "executions", "list", "order-pipeline", + "--page", "2", "--limit", "1") + second.requireSuccess(t, "Showing 1 of 3 execution(s) (page 2, limit 1)") + + if apiE2EDurableListedExecutionID(t, first.output) == + apiE2EDurableListedExecutionID(t, second.output) { + t.Fatalf("page 1 and page 2 listed the same execution:\n%s\n%s", first.output, second.output) + } + + // A status none of this function's executions is in filters server-side + // rather than being dropped on the way. + env.runCloudCLI(t, "durable", "executions", "list", "order-pipeline", "--status", "stopped"). + requireSuccess(t, `No executions started for durable function "order-pipeline"`) +} + +// requireAPIE2EDurableSchedulers covers the schedulers group end to end, +// including the part only real infrastructure can show: a tick starting an +// execution. A scheduler the API accepted but whose ticks never reach the +// durable collection would pass every assertion short of that one. +func requireAPIE2EDurableSchedulers(t *testing.T, env *apiE2E) { + t.Helper() + + env.runCloudCLI(t, "durable", "schedulers", "list", "order-pipeline"). + requireSuccess(t, `No schedulers configured for durable function "order-pipeline"`) + + // Every minute, so a tick lands inside the wait below. + created := env.runCloudCLI(t, "durable", "schedulers", "create", "order-pipeline", + "--name", "cli-e2e-tick", "--cron", "* * * * *", "--input", `{"order_id":9001}`) + created.requireSuccess(t, `Created scheduler for durable function "order-pipeline"`, + "cli-e2e-tick", "enabled", "* * * * *") + schedulerID := apiE2EDurableSchedulerID(t, created.output) + t.Cleanup(func() { + env.runCloudCLI(t, "durable", "schedulers", "delete", "order-pipeline", schedulerID, "--yes") + }) + + env.runCloudCLI(t, "durable", "schedulers", "list", "order-pipeline"). + requireSuccess(t, schedulerID, "cli-e2e-tick", "enabled") + + // A tick starts an execution rather than invoking the function, so the proof + // is an execution in the durable collection named after the scheduler run, + // not a counter on the scheduler. + env.waitForCloudCLIContains(t, apiE2EDurableSchedulerTickTimeout, "sched-", + "durable", "executions", "list", "order-pipeline") + + // Disabling stops the ticks and leaves the scheduler in place. Nothing here + // asserts that no further tick lands — that would be waiting to observe an + // absence — only that the state the next tick reads is off. + env.runCloudCLI(t, "durable", "schedulers", "disable", "order-pipeline", schedulerID). + requireSuccess(t, "Disabled scheduler "+schedulerID, "disabled") + env.runCloudCLI(t, "durable", "schedulers", "list", "order-pipeline"). + requireSuccess(t, "disabled") + + env.runCloudCLI(t, "durable", "schedulers", "enable", "order-pipeline", schedulerID). + requireSuccess(t, "Enabled scheduler "+schedulerID, "enabled") + env.runCloudCLI(t, "durable", "schedulers", "disable", "order-pipeline", schedulerID). + requireSuccess(t, "disabled") + + // Deleting the scheduler leaves the executions it started, which are what + // the rest of this test pages through. + env.runCloudCLI(t, "durable", "schedulers", "delete", "order-pipeline", schedulerID, "--yes"). + requireSuccess(t, "Deleted scheduler "+schedulerID) + env.runCloudCLI(t, "durable", "schedulers", "list", "order-pipeline"). + requireSuccess(t, `No schedulers configured for durable function "order-pipeline"`) + env.runCloudCLI(t, "durable", "schedulers", "delete", "order-pipeline", schedulerID, "--yes"). + requireFailure(t, schedulerID) +} + +// requireAPIE2EDurableStop covers the one command that changes a running +// execution. Stop answers with the execution rather than a status of its own, +// so a stop that reported success while the execution kept running would look +// identical at the call site — the assertion has to be the state afterwards. +func requireAPIE2EDurableStop(t *testing.T, env *apiE2E) { + t.Helper() + + env.waitForCloudCLIContains(t, apiE2EFunctionDeploymentTimeout, "Status: active", + "durable", "get", "sleeper") + + start := env.runCloudCLI(t, "durable", "start", "sleeper", "--name", "cli-e2e-stop") + start.requireSuccess(t, "Execution cli-e2e-stop started") + executionID := apiE2EDurableExecutionID(t, start.output) + + // A suspended durable execution reports `running` while no invocation of it + // is live, which is the state stopping one is meaningful in. + env.waitForCloudCLIContains(t, apiE2EDurableStopTimeout, "Status: running", + "durable", "executions", "get", "sleeper", executionID) + + env.runCloudCLI(t, "durable", "executions", "stop", "sleeper", executionID, "--yes"). + requireSuccess(t, "Stop requested for execution "+executionID) + env.waitForCloudCLIContains(t, apiE2EDurableStopTimeout, "Status: stopped", + "durable", "executions", "get", "sleeper", executionID) + + // Stopping a terminal execution reports where it is rather than failing, so + // a retried stop is safe. + env.runCloudCLI(t, "durable", "executions", "stop", "sleeper", executionID, "--yes"). + requireSuccess(t, "Status: stopped") + env.runCloudCLI(t, "durable", "executions", "list", "sleeper", "--status", "stopped"). + requireSuccess(t, executionID) +} + +// requireAPIE2EDurableVisibility covers `deploy -f` and the two visibility +// flags. Visibility travels with a deploy because a durable function has no +// update endpoint, so getting this wrong means a function that cannot be made +// public at all — or worse, one that quietly becomes public on a redeploy. +func requireAPIE2EDurableVisibility(t *testing.T, env *apiE2E) { + t.Helper() + + // -f deploys by name whether or not the manifest mentions the function, and + // visibility is applied when the deploy is accepted rather than when the + // build finishes. + env.runCloudCLI(t, "durable", "deploy", "-f", "order-pipeline", "--public"). + requireSuccess(t, "Deploying order-pipeline", "1/1 durable function(s) deployment started") + env.runCloudCLI(t, "durable", "get", "order-pipeline"). + requireSuccess(t, "Anon Key Start: allowed") + + // A redeploy has to be able to take it back. Each of these is a real build, + // so waiting for the rollout in between is the price of not racing it. + env.waitForCloudCLIContains(t, apiE2EFunctionDeploymentTimeout, "Status: active", + "durable", "get", "order-pipeline") + env.runCloudCLI(t, "durable", "deploy", "-f", + filepath.Join("volcano", "functions", "order-pipeline.js"), "--private"). + requireSuccess(t, "1/1 durable function(s) deployment started") + env.runCloudCLI(t, "durable", "get", "order-pipeline"). + requireSuccess(t, "Anon Key Start: denied") + + // Leave no build in flight, or the delete this test ends with races the + // rollout. + env.waitForCloudCLIContains(t, apiE2EFunctionDeploymentTimeout, "Status: active", + "durable", "get", "order-pipeline") +} + +// apiE2EDurableExecutionID reads the id out of what `durable start` printed, +// which is the only place the caller learns it. +func apiE2EDurableExecutionID(t *testing.T, output string) string { + t.Helper() + for line := range strings.SplitSeq(output, "\n") { + if id, found := strings.CutPrefix(strings.TrimSpace(line), "ID:"); found { + return strings.TrimSpace(id) + } + } + t.Fatalf("durable start printed no execution ID:\n%s", output) + return "" +} + +// apiE2EDurableSchedulerID reads the id out of what `schedulers create` +// printed. Every later scheduler command takes it, and it is the only place the +// caller learns it. +func apiE2EDurableSchedulerID(t *testing.T, output string) string { + t.Helper() + for line := range strings.SplitSeq(output, "\n") { + id, found := strings.CutPrefix(strings.TrimSpace(line), "ID:") + if !found { + continue + } + if _, err := uuid.Parse(strings.TrimSpace(id)); err == nil { + return strings.TrimSpace(id) + } + } + t.Fatalf("durable schedulers create printed no scheduler ID:\n%s", output) + return "" +} + +// apiE2EDurableListedName reads the one function name out of a single-row +// listing, which is how the paging assertions tell two pages apart. +func apiE2EDurableListedName(t *testing.T, output string) string { + t.Helper() + for line := range strings.SplitSeq(output, "\n") { + name, _, _ := strings.Cut(strings.TrimSpace(line), " ") + if name == "order-pipeline" || name == "sleeper" { + return name + } + } + t.Fatalf("no durable function row in listing:\n%s", output) + return "" +} + +// apiE2EDurableListedExecutionID reads the one execution id out of a single-row +// listing. The id is the first column, and it is the only uuid-shaped field a +// row starts with. +func apiE2EDurableListedExecutionID(t *testing.T, output string) string { + t.Helper() + for line := range strings.SplitSeq(output, "\n") { + field, _, _ := strings.Cut(strings.TrimSpace(line), " ") + if _, err := uuid.Parse(field); err == nil { + return field + } + } + t.Fatalf("no execution row in listing:\n%s", output) + return "" +} + +func writeAPIE2EDurableProject(t *testing.T, projectDir string) { + t.Helper() + writeAPIE2EBaseProject(t, projectDir) + writeAPIE2EFile(t, filepath.Join(projectDir, "volcano", "functions", "order-pipeline.js"), + apiE2EDurableSource) + writeAPIE2EFile(t, filepath.Join(projectDir, "volcano", "functions", "sleeper.js"), + apiE2EDurableSuspendedSource) + writeAPIE2EFile(t, filepath.Join(projectDir, "volcano-config.yaml"), ` +version: 1 +functions: + - name: hello + - name: order-pipeline + kind: durable + - name: sleeper + kind: durable +`) +} diff --git a/tests/e2e/localmode/localmode_test.go b/tests/e2e/localmode/localmode_test.go index 6f25ba52..f93847d3 100644 --- a/tests/e2e/localmode/localmode_test.go +++ b/tests/e2e/localmode/localmode_test.go @@ -49,6 +49,7 @@ func TestLocalModeE2ESmoke(t *testing.T) { requireContains(t, databasesAfterCreate, "app") requireLocalModeOmitsProviderOnlyDatabaseCommands(t, volcanoBin, env, projectDir) + requireLocalModeRefusesDurableFunctions(t, volcanoBin, env, projectDir) migrationOutput := runVolcanoLocalModeE2E(t, volcanoBin, env, projectDir, "migrations", "deploy", "--all", "-d", "app") requireContains(t, migrationOutput, "Applying 001_create_cli_contract.sql... ok") @@ -302,6 +303,63 @@ func requireLocalModeOmitsProviderOnlyDatabaseCommands(t *testing.T, binary stri } } +// Local development runs no durable engine: the local server refuses to create +// a durable function rather than pretending to run one. The command has to say +// that rather than print help and exit 0, which is what cobra does with an +// unknown subcommand and what a developer would read as success. +func requireLocalModeRefusesDurableFunctions(t *testing.T, binary string, env []string, dir string) { + t.Helper() + help := runVolcanoLocalModeE2E(t, binary, env, dir, "--help") + requireNotContains(t, help, "durable") + + for _, args := range [][]string{ + {"durable", "deploy", "--all"}, + {"durable", "list"}, + {"durable", "start", "order-pipeline", "--input", "{}"}, + {"durable", "executions", "list", "order-pipeline"}, + {"durable", "schedulers", "list", "order-pipeline"}, + {"durable", "schedulers", "create", "order-pipeline", "--cron", "0 * * * *"}, + } { + output, err := runVolcanoLocalModeE2EAllowFailure(t, binary, env, dir, args...) + if err == nil { + t.Fatalf("expected volcano %s to fail in local mode\n%s", strings.Join(args, " "), output) + } + requireContains(t, output, `"durable" is a cloud command`) + requireContains(t, output, "volcano cloud durable") + } + + // A durable function declared in the manifest must not be deployed as a + // standard one by the local deploy-all, which is the mistake that would be + // permanent: a function's kind is fixed when it is created. + writeLocalModeE2EFile(t, dir, "volcano-config.yaml", ` +version: 1 +functions: + - name: hello + - name: order-pipeline + kind: durable +`) + writeLocalModeE2EFile(t, dir, filepath.Join("volcano", "functions", "order-pipeline.js"), ` +exports.handler = async () => ({ statusCode: 200, body: "{}" }); +`) + + skipOutput := runVolcanoLocalModeE2E(t, binary, env, dir, "functions", "deploy", "--all") + requireContains(t, skipOutput, "Skipping 1 durable function(s) declared in volcano-config.yaml: order-pipeline") + requireNotContains(t, runVolcanoLocalModeE2E(t, binary, env, dir, "functions", "list"), "order-pipeline") + + // Removed here rather than in a cleanup: the config smoke later in this test + // writes volcano/volcano-config.yaml, and two manifests in one project is + // ambiguous by design. + removeLocalModeE2EFile(t, dir, "volcano-config.yaml") + removeLocalModeE2EFile(t, dir, filepath.Join("volcano", "functions", "order-pipeline.js")) +} + +func removeLocalModeE2EFile(t *testing.T, projectDir, relativePath string) { + t.Helper() + if err := os.Remove(filepath.Join(projectDir, relativePath)); err != nil { + t.Fatalf("failed to remove %s: %v", relativePath, err) + } +} + func runVolcanoLocalModeE2E(t *testing.T, binary string, env []string, dir string, args ...string) string { t.Helper() output, err := runVolcanoLocalModeE2EAllowFailure(t, binary, env, dir, args...)