From f720c010e4bf31216f21cfad2f1909c6420aa728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Sala=C3=BCn?= Date: Fri, 3 Apr 2026 12:36:49 +0200 Subject: [PATCH] feat: promote apps to top-level command and update for refactored deploy API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move `fctl cloud apps` → `fctl apps` (remove --experimental gate) - Add `init` command: create app + default deployment in one step - Rewrite `deploy` command: push manifest + deploy to deployment by app name - Add `manifest` subcommand group (push, list versions) - Add `deployments` subcommand group (create, list, show, delete, deploy) - Extract shared WaitRunCompletion helper - Update SDK: remove organizationID from ListApps, add Name to CreateApp, add PushManifest, ListManifestVersions, and deployment CRUD methods - Update OpenAPI spec from terraform-hcp-proxy Co-Authored-By: Claude Opus 4.6 (1M context) --- cmd/{cloud => }/apps/create.go | 13 +- cmd/{cloud => }/apps/delete.go | 0 cmd/apps/deploy.go | 147 ++++ cmd/apps/deployments/create.go | 107 +++ cmd/apps/deployments/delete.go | 86 ++ cmd/apps/deployments/deploy.go | 116 +++ cmd/apps/deployments/list.go | 102 +++ cmd/apps/deployments/root.go | 20 + cmd/apps/deployments/show.go | 101 +++ cmd/apps/init.go | 114 +++ cmd/{cloud => }/apps/list.go | 3 +- cmd/apps/manifest/list.go | 102 +++ cmd/apps/manifest/push.go | 107 +++ cmd/apps/manifest/root.go | 17 + cmd/{cloud => }/apps/printer/logs.go | 0 cmd/apps/root.go | 35 + cmd/{cloud => }/apps/runs/list.go | 0 cmd/{cloud => }/apps/runs/logs.go | 2 +- cmd/{cloud => }/apps/runs/root.go | 0 cmd/{cloud => }/apps/runs/show.go | 0 cmd/apps/shared/wait.go | 70 ++ cmd/{cloud => }/apps/show.go | 0 cmd/{cloud => }/apps/variables/create.go | 0 cmd/{cloud => }/apps/variables/delete.go | 0 cmd/{cloud => }/apps/variables/list.go | 0 cmd/{cloud => }/apps/variables/root.go | 0 cmd/{cloud => }/apps/versions/archive.go | 0 cmd/{cloud => }/apps/versions/list.go | 0 cmd/{cloud => }/apps/versions/manifest.go | 0 cmd/{cloud => }/apps/versions/root.go | 0 cmd/{cloud => }/apps/versions/show.go | 0 cmd/cloud/apps/deploy.go | 239 ------ cmd/cloud/apps/root.go | 45 - cmd/cloud/root.go | 2 - cmd/root.go | 2 + internal/deployserverclient/deployserver.go | 7 +- .../deployserver_extensions.go | 748 +++++++++++++++++ .../models/components/createapprequest.go | 9 +- .../components/createdeploymentrequest.go | 22 + .../models/components/deployment.go | 40 + .../models/components/deploymentresponse.go | 12 + .../components/listdeploymentsresponse.go | 12 + .../components/listmanifestsresponse.go | 12 + .../models/components/manifestversion.go | 42 + .../components/manifestversionresponse.go | 12 + .../models/operations/createdeployment.go | 53 ++ .../models/operations/deletedeployment.go | 44 + .../models/operations/deploytodeployment.go | 61 ++ .../models/operations/listapps.go | 13 +- .../models/operations/listdeployments.go | 45 + .../models/operations/listmanifestversions.go | 45 + .../models/operations/pushmanifest.go | 54 ++ .../models/operations/readdeployment.go | 53 ++ openapi/deployserver.yaml | 767 ++++++++---------- 54 files changed, 2727 insertions(+), 754 deletions(-) rename cmd/{cloud => }/apps/create.go (86%) rename cmd/{cloud => }/apps/delete.go (100%) create mode 100644 cmd/apps/deploy.go create mode 100644 cmd/apps/deployments/create.go create mode 100644 cmd/apps/deployments/delete.go create mode 100644 cmd/apps/deployments/deploy.go create mode 100644 cmd/apps/deployments/list.go create mode 100644 cmd/apps/deployments/root.go create mode 100644 cmd/apps/deployments/show.go create mode 100644 cmd/apps/init.go rename cmd/{cloud => }/apps/list.go (96%) create mode 100644 cmd/apps/manifest/list.go create mode 100644 cmd/apps/manifest/push.go create mode 100644 cmd/apps/manifest/root.go rename cmd/{cloud => }/apps/printer/logs.go (100%) create mode 100644 cmd/apps/root.go rename cmd/{cloud => }/apps/runs/list.go (100%) rename cmd/{cloud => }/apps/runs/logs.go (96%) rename cmd/{cloud => }/apps/runs/root.go (100%) rename cmd/{cloud => }/apps/runs/show.go (100%) create mode 100644 cmd/apps/shared/wait.go rename cmd/{cloud => }/apps/show.go (100%) rename cmd/{cloud => }/apps/variables/create.go (100%) rename cmd/{cloud => }/apps/variables/delete.go (100%) rename cmd/{cloud => }/apps/variables/list.go (100%) rename cmd/{cloud => }/apps/variables/root.go (100%) rename cmd/{cloud => }/apps/versions/archive.go (100%) rename cmd/{cloud => }/apps/versions/list.go (100%) rename cmd/{cloud => }/apps/versions/manifest.go (100%) rename cmd/{cloud => }/apps/versions/root.go (100%) rename cmd/{cloud => }/apps/versions/show.go (100%) delete mode 100644 cmd/cloud/apps/deploy.go delete mode 100644 cmd/cloud/apps/root.go create mode 100644 internal/deployserverclient/deployserver_extensions.go create mode 100644 internal/deployserverclient/models/components/createdeploymentrequest.go create mode 100644 internal/deployserverclient/models/components/deployment.go create mode 100644 internal/deployserverclient/models/components/deploymentresponse.go create mode 100644 internal/deployserverclient/models/components/listdeploymentsresponse.go create mode 100644 internal/deployserverclient/models/components/listmanifestsresponse.go create mode 100644 internal/deployserverclient/models/components/manifestversion.go create mode 100644 internal/deployserverclient/models/components/manifestversionresponse.go create mode 100644 internal/deployserverclient/models/operations/createdeployment.go create mode 100644 internal/deployserverclient/models/operations/deletedeployment.go create mode 100644 internal/deployserverclient/models/operations/deploytodeployment.go create mode 100644 internal/deployserverclient/models/operations/listdeployments.go create mode 100644 internal/deployserverclient/models/operations/listmanifestversions.go create mode 100644 internal/deployserverclient/models/operations/pushmanifest.go create mode 100644 internal/deployserverclient/models/operations/readdeployment.go diff --git a/cmd/cloud/apps/create.go b/cmd/apps/create.go similarity index 86% rename from cmd/cloud/apps/create.go rename to cmd/apps/create.go index 718d7407..a81c75a3 100644 --- a/cmd/cloud/apps/create.go +++ b/cmd/apps/create.go @@ -1,6 +1,8 @@ package apps import ( + "fmt" + "github.com/pterm/pterm" "github.com/spf13/cobra" @@ -34,6 +36,7 @@ func NewCreateCtrl() *CreateCtrl { func NewCreate() *cobra.Command { return fctl.NewCommand("create", fctl.WithShortDescription("Create apps"), + fctl.WithStringFlag("name", "", "App name (required)"), fctl.WithController(NewCreateCtrl()), ) } @@ -49,7 +52,7 @@ func (c *CreateCtrl) Run(cmd *cobra.Command, _ []string) (fctl.Renderable, error return nil, err } - organizationID, apiClient, err := fctl.NewAppDeployClientFromFlags( + _, apiClient, err := fctl.NewAppDeployClientFromFlags( cmd, relyingParty, fctl.NewPTermDialog(), @@ -59,8 +62,14 @@ func (c *CreateCtrl) Run(cmd *cobra.Command, _ []string) (fctl.Renderable, error if err != nil { return nil, err } + + name := fctl.GetString(cmd, "name") + if name == "" { + return nil, fmt.Errorf("--name is required") + } + apps, err := apiClient.CreateApp(cmd.Context(), components.CreateAppRequest{ - OrganizationID: organizationID, + Name: name, }) if err != nil { return nil, err diff --git a/cmd/cloud/apps/delete.go b/cmd/apps/delete.go similarity index 100% rename from cmd/cloud/apps/delete.go rename to cmd/apps/delete.go diff --git a/cmd/apps/deploy.go b/cmd/apps/deploy.go new file mode 100644 index 00000000..053d2577 --- /dev/null +++ b/cmd/apps/deploy.go @@ -0,0 +1,147 @@ +package apps + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" + + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" + + "github.com/formancehq/fctl/v3/cmd/apps/printer" + "github.com/formancehq/fctl/v3/cmd/apps/shared" + fctl "github.com/formancehq/fctl/v3/pkg" +) + +type Deploy struct { + *components.Run + logs []components.Log +} + +type DeployCtrl struct { + store *Deploy +} + +var _ fctl.Controller[*Deploy] = (*DeployCtrl)(nil) + +func newDeployStore() *Deploy { + return &Deploy{} +} + +func NewDeployCtrl() *DeployCtrl { + return &DeployCtrl{ + store: newDeployStore(), + } +} + +func NewDeploy() *cobra.Command { + return fctl.NewCommand("deploy", + fctl.WithShortDescription("Push manifest and deploy to a deployment"), + fctl.WithStringFlag("name", "", "App name (required)"), + fctl.WithStringFlag("path", "", "Path to the manifest file (required)"), + fctl.WithStringFlag("deployment", "default", "Deployment name"), + fctl.WithIntFlag("version", 0, "Manifest version to deploy (0 = latest)"), + fctl.WithBoolFlag("wait", true, "Wait for the deployment to complete"), + fctl.WithController(NewDeployCtrl()), + ) +} + +func (c *DeployCtrl) GetStore() *Deploy { + return c.store +} + +func (c *DeployCtrl) Run(cmd *cobra.Command, args []string) (fctl.Renderable, error) { + _, profile, profileName, relyingParty, err := fctl.LoadAndAuthenticateCurrentProfile(cmd) + if err != nil { + return nil, err + } + + _, apiClient, err := fctl.NewAppDeployClientFromFlags( + cmd, + relyingParty, + fctl.NewPTermDialog(), + profileName, + *profile, + ) + if err != nil { + return nil, err + } + + appName := fctl.GetString(cmd, "name") + if appName == "" { + return nil, fmt.Errorf("--name is required") + } + path := fctl.GetString(cmd, "path") + if path == "" { + return nil, fmt.Errorf("--path is required") + } + deploymentName := fctl.GetString(cmd, "deployment") + + cmd.SilenceUsage = true + + // 1. Look up app by name + appsRes, err := apiClient.ListApps(cmd.Context(), nil, nil) + if err != nil { + return nil, err + } + var appID string + for _, app := range appsRes.ListAppsResponse.Data.Items { + if app.Name == appName { + appID = app.ID + break + } + } + if appID == "" { + return nil, fmt.Errorf("app %q not found", appName) + } + + // 2. Read and push manifest + data, err := os.ReadFile(filepath.Clean(path)) + if err != nil { + return nil, err + } + _, err = apiClient.PushManifest(cmd.Context(), appID, data) + if err != nil { + return nil, fmt.Errorf("failed to push manifest: %w", err) + } + + // 3. Deploy to deployment + var versionPtr *int64 + if v := fctl.GetInt(cmd, "version"); v > 0 { + v64 := int64(v) + versionPtr = &v64 + } + deployRes, err := apiClient.DeployToDeployment(cmd.Context(), appID, deploymentName, versionPtr) + if err != nil { + return nil, fmt.Errorf("failed to deploy: %w", err) + } + c.store.Run = &deployRes.RunResponse.Data + + // 4. Wait if requested + if fctl.GetBool(cmd, "wait") { + run, logs, err := shared.WaitRunCompletion(cmd, apiClient, c.store.Run.ID) + if err != nil { + return nil, err + } + c.store.Run = run + c.store.logs = logs + } + + return c, nil +} + +func (c *DeployCtrl) Render(cmd *cobra.Command, args []string) error { + if c.store.Run.Status == "errored" { + if len(c.store.logs) > 0 { + if err := printer.RenderLogs(cmd.ErrOrStderr(), c.store.logs); err != nil { + return err + } + } + return fmt.Errorf("deployment failed: %s", c.store.Run.ID) + } + + pterm.Success.Printfln("Deployment %s: %s", c.store.Run.Status, c.store.Run.ID) + return nil +} diff --git a/cmd/apps/deployments/create.go b/cmd/apps/deployments/create.go new file mode 100644 index 00000000..2ce7ce53 --- /dev/null +++ b/cmd/apps/deployments/create.go @@ -0,0 +1,107 @@ +package deployments + +import ( + "fmt" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" + + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" + + fctl "github.com/formancehq/fctl/v3/pkg" +) + +type CreateStore struct { + components.Deployment +} + +type CreateCtrl struct { + store *CreateStore +} + +var _ fctl.Controller[*CreateStore] = (*CreateCtrl)(nil) + +func newCreateStore() *CreateStore { + return &CreateStore{} +} + +func NewCreateCtrl() *CreateCtrl { + return &CreateCtrl{ + store: newCreateStore(), + } +} + +func NewCreate() *cobra.Command { + return fctl.NewCommand("create", + fctl.WithShortDescription("Create a deployment for an app"), + fctl.WithStringFlag("id", "", "App ID (required)"), + fctl.WithStringFlag("name", "", "Deployment name (required)"), + fctl.WithStringFlag("stack-id", "", "Stack ID (required)"), + fctl.WithController(NewCreateCtrl()), + ) +} + +func (c *CreateCtrl) GetStore() *CreateStore { + return c.store +} + +func (c *CreateCtrl) Run(cmd *cobra.Command, _ []string) (fctl.Renderable, error) { + _, profile, profileName, relyingParty, err := fctl.LoadAndAuthenticateCurrentProfile(cmd) + if err != nil { + return nil, err + } + + _, apiClient, err := fctl.NewAppDeployClientFromFlags( + cmd, + relyingParty, + fctl.NewPTermDialog(), + profileName, + *profile, + ) + if err != nil { + return nil, err + } + + id := fctl.GetString(cmd, "id") + if id == "" { + return nil, fmt.Errorf("--id is required") + } + name := fctl.GetString(cmd, "name") + if name == "" { + return nil, fmt.Errorf("--name is required") + } + stackID := fctl.GetString(cmd, "stack-id") + if stackID == "" { + return nil, fmt.Errorf("--stack-id is required") + } + + cmd.SilenceUsage = true + + res, err := apiClient.CreateDeployment(cmd.Context(), id, components.CreateDeploymentRequest{ + Name: name, + StackID: stackID, + }) + if err != nil { + return nil, err + } + + c.store.Deployment = res.DeploymentResponse.Data + + return c, nil +} + +func (c *CreateCtrl) Render(cmd *cobra.Command, args []string) error { + pterm.Success.Printfln("Deployment created successfully") + if err := pterm. + DefaultTable. + WithHasHeader(). + WithData([][]string{ + {"Name", "App ID", "Stack ID", "Workspace ID"}, + {c.store.Deployment.Name, c.store.Deployment.AppID, c.store.Deployment.StackID, c.store.Deployment.WorkspaceID}, + }). + WithWriter(cmd.OutOrStdout()). + Render(); err != nil { + return err + } + return nil +} diff --git a/cmd/apps/deployments/delete.go b/cmd/apps/deployments/delete.go new file mode 100644 index 00000000..9edda8a6 --- /dev/null +++ b/cmd/apps/deployments/delete.go @@ -0,0 +1,86 @@ +package deployments + +import ( + "fmt" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" + + fctl "github.com/formancehq/fctl/v3/pkg" +) + +type DeleteStore struct { + Name string +} + +type DeleteCtrl struct { + store *DeleteStore +} + +var _ fctl.Controller[*DeleteStore] = (*DeleteCtrl)(nil) + +func newDeleteStore() *DeleteStore { + return &DeleteStore{} +} + +func NewDeleteCtrl() *DeleteCtrl { + return &DeleteCtrl{ + store: newDeleteStore(), + } +} + +func NewDelete() *cobra.Command { + return fctl.NewCommand("delete", + fctl.WithShortDescription("Delete a deployment"), + fctl.WithStringFlag("id", "", "App ID (required)"), + fctl.WithStringFlag("name", "", "Deployment name (required)"), + fctl.WithController(NewDeleteCtrl()), + ) +} + +func (c *DeleteCtrl) GetStore() *DeleteStore { + return c.store +} + +func (c *DeleteCtrl) Run(cmd *cobra.Command, _ []string) (fctl.Renderable, error) { + _, profile, profileName, relyingParty, err := fctl.LoadAndAuthenticateCurrentProfile(cmd) + if err != nil { + return nil, err + } + + _, apiClient, err := fctl.NewAppDeployClientFromFlags( + cmd, + relyingParty, + fctl.NewPTermDialog(), + profileName, + *profile, + ) + if err != nil { + return nil, err + } + + id := fctl.GetString(cmd, "id") + if id == "" { + return nil, fmt.Errorf("--id is required") + } + name := fctl.GetString(cmd, "name") + if name == "" { + return nil, fmt.Errorf("--name is required") + } + + cmd.SilenceUsage = true + + _, err = apiClient.DeleteDeployment(cmd.Context(), id, name) + if err != nil { + return nil, err + } + + c.store.Name = name + + return c, nil +} + +func (c *DeleteCtrl) Render(cmd *cobra.Command, args []string) error { + pterm.Success.Println("Deployment deleted", c.store.Name) + return nil +} diff --git a/cmd/apps/deployments/deploy.go b/cmd/apps/deployments/deploy.go new file mode 100644 index 00000000..7d6a9e59 --- /dev/null +++ b/cmd/apps/deployments/deploy.go @@ -0,0 +1,116 @@ +package deployments + +import ( + "fmt" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" + + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" + + "github.com/formancehq/fctl/v3/cmd/apps/printer" + "github.com/formancehq/fctl/v3/cmd/apps/shared" + fctl "github.com/formancehq/fctl/v3/pkg" +) + +type DeployStore struct { + *components.Run + logs []components.Log +} + +type DeployCtrl struct { + store *DeployStore +} + +var _ fctl.Controller[*DeployStore] = (*DeployCtrl)(nil) + +func newDeployStore() *DeployStore { + return &DeployStore{} +} + +func NewDeployCtrl() *DeployCtrl { + return &DeployCtrl{ + store: newDeployStore(), + } +} + +func NewDeploy() *cobra.Command { + return fctl.NewCommand("deploy", + fctl.WithShortDescription("Deploy a manifest version to a deployment"), + fctl.WithStringFlag("id", "", "App ID (required)"), + fctl.WithStringFlag("name", "", "Deployment name (required)"), + fctl.WithIntFlag("version", 0, "Manifest version to deploy (0 = latest)"), + fctl.WithBoolFlag("wait", true, "Wait for the deployment to complete"), + fctl.WithController(NewDeployCtrl()), + ) +} + +func (c *DeployCtrl) GetStore() *DeployStore { + return c.store +} + +func (c *DeployCtrl) Run(cmd *cobra.Command, _ []string) (fctl.Renderable, error) { + _, profile, profileName, relyingParty, err := fctl.LoadAndAuthenticateCurrentProfile(cmd) + if err != nil { + return nil, err + } + + _, apiClient, err := fctl.NewAppDeployClientFromFlags( + cmd, + relyingParty, + fctl.NewPTermDialog(), + profileName, + *profile, + ) + if err != nil { + return nil, err + } + + id := fctl.GetString(cmd, "id") + if id == "" { + return nil, fmt.Errorf("--id is required") + } + name := fctl.GetString(cmd, "name") + if name == "" { + return nil, fmt.Errorf("--name is required") + } + + var versionPtr *int64 + if v := fctl.GetInt(cmd, "version"); v > 0 { + v64 := int64(v) + versionPtr = &v64 + } + + cmd.SilenceUsage = true + + res, err := apiClient.DeployToDeployment(cmd.Context(), id, name, versionPtr) + if err != nil { + return nil, err + } + c.store.Run = &res.RunResponse.Data + + if fctl.GetBool(cmd, "wait") { + run, logs, err := shared.WaitRunCompletion(cmd, apiClient, c.store.Run.ID) + if err != nil { + return nil, err + } + c.store.Run = run + c.store.logs = logs + } + + return c, nil +} + +func (c *DeployCtrl) Render(cmd *cobra.Command, args []string) error { + if c.store.Run.Status == "errored" { + if len(c.store.logs) > 0 { + if err := printer.RenderLogs(cmd.ErrOrStderr(), c.store.logs); err != nil { + return err + } + } + return fmt.Errorf("deployment failed: %s", c.store.Run.ID) + } + + pterm.Success.Printfln("Deployment %s: %s", c.store.Run.Status, c.store.Run.ID) + return nil +} diff --git a/cmd/apps/deployments/list.go b/cmd/apps/deployments/list.go new file mode 100644 index 00000000..98b383c6 --- /dev/null +++ b/cmd/apps/deployments/list.go @@ -0,0 +1,102 @@ +package deployments + +import ( + "fmt" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" + + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" + + fctl "github.com/formancehq/fctl/v3/pkg" +) + +type ListStore struct { + Deployments []components.Deployment +} + +type ListCtrl struct { + store *ListStore +} + +var _ fctl.Controller[*ListStore] = (*ListCtrl)(nil) + +func newListStore() *ListStore { + return &ListStore{} +} + +func NewListCtrl() *ListCtrl { + return &ListCtrl{ + store: newListStore(), + } +} + +func NewList() *cobra.Command { + return fctl.NewCommand("list", + fctl.WithAliases("ls"), + fctl.WithShortDescription("List deployments for an app"), + fctl.WithStringFlag("id", "", "App ID (required)"), + fctl.WithController(NewListCtrl()), + ) +} + +func (c *ListCtrl) GetStore() *ListStore { + return c.store +} + +func (c *ListCtrl) Run(cmd *cobra.Command, _ []string) (fctl.Renderable, error) { + _, profile, profileName, relyingParty, err := fctl.LoadAndAuthenticateCurrentProfile(cmd) + if err != nil { + return nil, err + } + + _, apiClient, err := fctl.NewAppDeployClientFromFlags( + cmd, + relyingParty, + fctl.NewPTermDialog(), + profileName, + *profile, + ) + if err != nil { + return nil, err + } + + id := fctl.GetString(cmd, "id") + if id == "" { + return nil, fmt.Errorf("--id is required") + } + + res, err := apiClient.ListDeployments(cmd.Context(), id) + if err != nil { + return nil, err + } + + c.store.Deployments = res.ListDeploymentsResponse.Data + + return c, nil +} + +func (c *ListCtrl) Render(cmd *cobra.Command, _ []string) error { + data := [][]string{ + {"Name", "App ID", "Stack ID", "Workspace ID"}, + } + + for _, d := range c.store.Deployments { + data = append(data, []string{ + d.Name, + d.AppID, + d.StackID, + d.WorkspaceID, + }) + } + + if err := pterm. + DefaultTable. + WithHasHeader(). + WithWriter(cmd.OutOrStdout()). + WithData(data). + Render(); err != nil { + return err + } + return nil +} diff --git a/cmd/apps/deployments/root.go b/cmd/apps/deployments/root.go new file mode 100644 index 00000000..a524a51d --- /dev/null +++ b/cmd/apps/deployments/root.go @@ -0,0 +1,20 @@ +package deployments + +import ( + "github.com/spf13/cobra" + + fctl "github.com/formancehq/fctl/v3/pkg" +) + +func NewCommand() *cobra.Command { + return fctl.NewCommand("deployments", + fctl.WithShortDescription("Manage app deployments"), + fctl.WithChildCommands( + NewCreate(), + NewList(), + NewShow(), + NewDelete(), + NewDeploy(), + ), + ) +} diff --git a/cmd/apps/deployments/show.go b/cmd/apps/deployments/show.go new file mode 100644 index 00000000..3dba1e70 --- /dev/null +++ b/cmd/apps/deployments/show.go @@ -0,0 +1,101 @@ +package deployments + +import ( + "fmt" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" + + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" + + fctl "github.com/formancehq/fctl/v3/pkg" +) + +type ShowStore struct { + components.Deployment +} + +type ShowCtrl struct { + store *ShowStore +} + +var _ fctl.Controller[*ShowStore] = (*ShowCtrl)(nil) + +func newShowStore() *ShowStore { + return &ShowStore{} +} + +func NewShowCtrl() *ShowCtrl { + return &ShowCtrl{ + store: newShowStore(), + } +} + +func NewShow() *cobra.Command { + return fctl.NewCommand("show", + fctl.WithShortDescription("Show a deployment"), + fctl.WithStringFlag("id", "", "App ID (required)"), + fctl.WithStringFlag("name", "", "Deployment name (required)"), + fctl.WithController(NewShowCtrl()), + ) +} + +func (c *ShowCtrl) GetStore() *ShowStore { + return c.store +} + +func (c *ShowCtrl) Run(cmd *cobra.Command, _ []string) (fctl.Renderable, error) { + _, profile, profileName, relyingParty, err := fctl.LoadAndAuthenticateCurrentProfile(cmd) + if err != nil { + return nil, err + } + + _, apiClient, err := fctl.NewAppDeployClientFromFlags( + cmd, + relyingParty, + fctl.NewPTermDialog(), + profileName, + *profile, + ) + if err != nil { + return nil, err + } + + id := fctl.GetString(cmd, "id") + if id == "" { + return nil, fmt.Errorf("--id is required") + } + name := fctl.GetString(cmd, "name") + if name == "" { + return nil, fmt.Errorf("--name is required") + } + + res, err := apiClient.ReadDeployment(cmd.Context(), id, name) + if err != nil { + return nil, err + } + + c.store.Deployment = res.DeploymentResponse.Data + + return c, nil +} + +func (c *ShowCtrl) Render(cmd *cobra.Command, args []string) error { + pterm.DefaultSection.Println("Deployment") + + items := []pterm.BulletListItem{ + {Level: 0, Text: fmt.Sprintf("Name: %s", c.store.Deployment.Name)}, + {Level: 0, Text: fmt.Sprintf("App ID: %s", c.store.Deployment.AppID)}, + {Level: 0, Text: fmt.Sprintf("Stack ID: %s", c.store.Deployment.StackID)}, + {Level: 0, Text: fmt.Sprintf("Workspace ID: %s", c.store.Deployment.WorkspaceID)}, + } + + if err := pterm. + DefaultBulletList. + WithItems(items). + WithWriter(cmd.OutOrStdout()). + Render(); err != nil { + return err + } + return nil +} diff --git a/cmd/apps/init.go b/cmd/apps/init.go new file mode 100644 index 00000000..f538554b --- /dev/null +++ b/cmd/apps/init.go @@ -0,0 +1,114 @@ +package apps + +import ( + "fmt" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" + + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" + + fctl "github.com/formancehq/fctl/v3/pkg" +) + +type Init struct { + App components.App + Deployment components.Deployment +} + +type InitCtrl struct { + store *Init +} + +var _ fctl.Controller[*Init] = (*InitCtrl)(nil) + +func newInitStore() *Init { + return &Init{} +} + +func NewInitCtrl() *InitCtrl { + return &InitCtrl{ + store: newInitStore(), + } +} + +func NewInit() *cobra.Command { + return fctl.NewCommand("init", + fctl.WithShortDescription("Create an app and its default deployment"), + fctl.WithStringFlag("name", "", "App name (required)"), + fctl.WithStringFlag("stack-id", "", "Stack ID for the deployment (required)"), + fctl.WithStringFlag("deployment", "default", "Deployment name"), + fctl.WithController(NewInitCtrl()), + ) +} + +func (c *InitCtrl) GetStore() *Init { + return c.store +} + +func (c *InitCtrl) Run(cmd *cobra.Command, _ []string) (fctl.Renderable, error) { + _, profile, profileName, relyingParty, err := fctl.LoadAndAuthenticateCurrentProfile(cmd) + if err != nil { + return nil, err + } + + _, apiClient, err := fctl.NewAppDeployClientFromFlags( + cmd, + relyingParty, + fctl.NewPTermDialog(), + profileName, + *profile, + ) + if err != nil { + return nil, err + } + + name := fctl.GetString(cmd, "name") + if name == "" { + return nil, fmt.Errorf("--name is required") + } + stackID := fctl.GetString(cmd, "stack-id") + if stackID == "" { + return nil, fmt.Errorf("--stack-id is required") + } + deploymentName := fctl.GetString(cmd, "deployment") + + cmd.SilenceUsage = true + + // 1. Create the app + appRes, err := apiClient.CreateApp(cmd.Context(), components.CreateAppRequest{ + Name: name, + }) + if err != nil { + return nil, fmt.Errorf("failed to create app: %w", err) + } + c.store.App = appRes.AppResponse.Data + + // 2. Create the deployment + depRes, err := apiClient.CreateDeployment(cmd.Context(), c.store.App.ID, components.CreateDeploymentRequest{ + Name: deploymentName, + StackID: stackID, + }) + if err != nil { + return nil, fmt.Errorf("failed to create deployment: %w", err) + } + c.store.Deployment = depRes.DeploymentResponse.Data + + return c, nil +} + +func (c *InitCtrl) Render(cmd *cobra.Command, args []string) error { + pterm.Success.Printfln("App initialized successfully") + if err := pterm. + DefaultTable. + WithHasHeader(). + WithData([][]string{ + {"App ID", "App Name", "Deployment", "Stack ID"}, + {c.store.App.ID, c.store.App.Name, c.store.Deployment.Name, c.store.Deployment.StackID}, + }). + WithWriter(cmd.OutOrStdout()). + Render(); err != nil { + return err + } + return nil +} diff --git a/cmd/cloud/apps/list.go b/cmd/apps/list.go similarity index 96% rename from cmd/cloud/apps/list.go rename to cmd/apps/list.go index 119478fe..c0774c40 100644 --- a/cmd/cloud/apps/list.go +++ b/cmd/apps/list.go @@ -56,7 +56,7 @@ func (c *ListCtrl) Run(cmd *cobra.Command, _ []string) (fctl.Renderable, error) pageSize := fctl.GetInt(cmd, "page-size") page := fctl.GetInt(cmd, "page") - organizationID, apiClient, err := fctl.NewAppDeployClientFromFlags( + _, apiClient, err := fctl.NewAppDeployClientFromFlags( cmd, relyingParty, fctl.NewPTermDialog(), @@ -68,7 +68,6 @@ func (c *ListCtrl) Run(cmd *cobra.Command, _ []string) (fctl.Renderable, error) } apps, err := apiClient.ListApps( cmd.Context(), - organizationID, pointer.For(int64(page)), pointer.For(int64(pageSize)), ) diff --git a/cmd/apps/manifest/list.go b/cmd/apps/manifest/list.go new file mode 100644 index 00000000..16e83518 --- /dev/null +++ b/cmd/apps/manifest/list.go @@ -0,0 +1,102 @@ +package manifest + +import ( + "fmt" + "strconv" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" + + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" + + fctl "github.com/formancehq/fctl/v3/pkg" +) + +type ListStore struct { + Versions []components.ManifestVersion +} + +type ListCtrl struct { + store *ListStore +} + +var _ fctl.Controller[*ListStore] = (*ListCtrl)(nil) + +func newListStore() *ListStore { + return &ListStore{} +} + +func NewListCtrl() *ListCtrl { + return &ListCtrl{ + store: newListStore(), + } +} + +func NewList() *cobra.Command { + return fctl.NewCommand("list", + fctl.WithAliases("ls"), + fctl.WithShortDescription("List manifest versions"), + fctl.WithStringFlag("id", "", "App ID (required)"), + fctl.WithController(NewListCtrl()), + ) +} + +func (c *ListCtrl) GetStore() *ListStore { + return c.store +} + +func (c *ListCtrl) Run(cmd *cobra.Command, _ []string) (fctl.Renderable, error) { + _, profile, profileName, relyingParty, err := fctl.LoadAndAuthenticateCurrentProfile(cmd) + if err != nil { + return nil, err + } + + _, apiClient, err := fctl.NewAppDeployClientFromFlags( + cmd, + relyingParty, + fctl.NewPTermDialog(), + profileName, + *profile, + ) + if err != nil { + return nil, err + } + + id := fctl.GetString(cmd, "id") + if id == "" { + return nil, fmt.Errorf("--id is required") + } + + res, err := apiClient.ListManifestVersions(cmd.Context(), id) + if err != nil { + return nil, err + } + + c.store.Versions = res.ListManifestsResponse.Data + + return c, nil +} + +func (c *ListCtrl) Render(cmd *cobra.Command, _ []string) error { + data := [][]string{ + {"Version", "App ID", "Created At"}, + } + + for _, v := range c.store.Versions { + data = append(data, []string{ + strconv.Itoa(v.Version), + v.AppID, + v.CreatedAt.String(), + }) + } + + if err := pterm. + DefaultTable. + WithHasHeader(). + WithWriter(cmd.OutOrStdout()). + WithData(data). + Render(); err != nil { + return err + } + return nil +} diff --git a/cmd/apps/manifest/push.go b/cmd/apps/manifest/push.go new file mode 100644 index 00000000..7f834a27 --- /dev/null +++ b/cmd/apps/manifest/push.go @@ -0,0 +1,107 @@ +package manifest + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" + + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" + + fctl "github.com/formancehq/fctl/v3/pkg" +) + +type Push struct { + components.ManifestVersion +} + +type PushCtrl struct { + store *Push +} + +var _ fctl.Controller[*Push] = (*PushCtrl)(nil) + +func newPushStore() *Push { + return &Push{} +} + +func NewPushCtrl() *PushCtrl { + return &PushCtrl{ + store: newPushStore(), + } +} + +func NewPush() *cobra.Command { + return fctl.NewCommand("push", + fctl.WithShortDescription("Push a new manifest version"), + fctl.WithStringFlag("id", "", "App ID (required)"), + fctl.WithStringFlag("path", "", "Path to the manifest YAML file (required)"), + fctl.WithController(NewPushCtrl()), + ) +} + +func (c *PushCtrl) GetStore() *Push { + return c.store +} + +func (c *PushCtrl) Run(cmd *cobra.Command, _ []string) (fctl.Renderable, error) { + _, profile, profileName, relyingParty, err := fctl.LoadAndAuthenticateCurrentProfile(cmd) + if err != nil { + return nil, err + } + + _, apiClient, err := fctl.NewAppDeployClientFromFlags( + cmd, + relyingParty, + fctl.NewPTermDialog(), + profileName, + *profile, + ) + if err != nil { + return nil, err + } + + id := fctl.GetString(cmd, "id") + if id == "" { + return nil, fmt.Errorf("--id is required") + } + path := fctl.GetString(cmd, "path") + if path == "" { + return nil, fmt.Errorf("--path is required") + } + + data, err := os.ReadFile(filepath.Clean(path)) + if err != nil { + return nil, err + } + + cmd.SilenceUsage = true + + res, err := apiClient.PushManifest(cmd.Context(), id, data) + if err != nil { + return nil, err + } + + c.store.ManifestVersion = res.ManifestVersionResponse.Data + + return c, nil +} + +func (c *PushCtrl) Render(cmd *cobra.Command, args []string) error { + pterm.Success.Printfln("Manifest pushed successfully") + if err := pterm. + DefaultTable. + WithHasHeader(). + WithData([][]string{ + {"Version", "App ID", "Created At"}, + {strconv.Itoa(c.store.ManifestVersion.Version), c.store.ManifestVersion.AppID, c.store.ManifestVersion.CreatedAt.String()}, + }). + WithWriter(cmd.OutOrStdout()). + Render(); err != nil { + return err + } + return nil +} diff --git a/cmd/apps/manifest/root.go b/cmd/apps/manifest/root.go new file mode 100644 index 00000000..5acc1f4c --- /dev/null +++ b/cmd/apps/manifest/root.go @@ -0,0 +1,17 @@ +package manifest + +import ( + "github.com/spf13/cobra" + + fctl "github.com/formancehq/fctl/v3/pkg" +) + +func NewCommand() *cobra.Command { + return fctl.NewCommand("manifest", + fctl.WithShortDescription("Manage app manifests"), + fctl.WithChildCommands( + NewPush(), + NewList(), + ), + ) +} diff --git a/cmd/cloud/apps/printer/logs.go b/cmd/apps/printer/logs.go similarity index 100% rename from cmd/cloud/apps/printer/logs.go rename to cmd/apps/printer/logs.go diff --git a/cmd/apps/root.go b/cmd/apps/root.go new file mode 100644 index 00000000..7a6a15c6 --- /dev/null +++ b/cmd/apps/root.go @@ -0,0 +1,35 @@ +package apps + +import ( + "github.com/spf13/cobra" + + "github.com/formancehq/fctl/v3/cmd/apps/deployments" + "github.com/formancehq/fctl/v3/cmd/apps/manifest" + "github.com/formancehq/fctl/v3/cmd/apps/runs" + "github.com/formancehq/fctl/v3/cmd/apps/variables" + "github.com/formancehq/fctl/v3/cmd/apps/versions" + fctl "github.com/formancehq/fctl/v3/pkg" +) + +func NewCommand() *cobra.Command { + cmd := fctl.NewMembershipCommand("apps", + fctl.WithShortDescription("Deploy Formance applications from manifest files"), + fctl.WithPersistentStringFlag(fctl.FrameworkURIFlag, "https://deploy.formance.cloud", "Framework URI"), + fctl.WithAliases("app"), + fctl.WithChildCommands( + NewInit(), + NewList(), + NewCreate(), + NewDelete(), + NewShow(), + NewDeploy(), + manifest.NewCommand(), + deployments.NewCommand(), + runs.NewCommand(), + versions.NewCommand(), + variables.NewCommand(), + ), + ) + + return cmd +} diff --git a/cmd/cloud/apps/runs/list.go b/cmd/apps/runs/list.go similarity index 100% rename from cmd/cloud/apps/runs/list.go rename to cmd/apps/runs/list.go diff --git a/cmd/cloud/apps/runs/logs.go b/cmd/apps/runs/logs.go similarity index 96% rename from cmd/cloud/apps/runs/logs.go rename to cmd/apps/runs/logs.go index a8469f0b..616abbd3 100644 --- a/cmd/cloud/apps/runs/logs.go +++ b/cmd/apps/runs/logs.go @@ -7,7 +7,7 @@ import ( "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" - "github.com/formancehq/fctl/v3/cmd/cloud/apps/printer" + "github.com/formancehq/fctl/v3/cmd/apps/printer" fctl "github.com/formancehq/fctl/v3/pkg" ) diff --git a/cmd/cloud/apps/runs/root.go b/cmd/apps/runs/root.go similarity index 100% rename from cmd/cloud/apps/runs/root.go rename to cmd/apps/runs/root.go diff --git a/cmd/cloud/apps/runs/show.go b/cmd/apps/runs/show.go similarity index 100% rename from cmd/cloud/apps/runs/show.go rename to cmd/apps/runs/show.go diff --git a/cmd/apps/shared/wait.go b/cmd/apps/shared/wait.go new file mode 100644 index 00000000..28a71d90 --- /dev/null +++ b/cmd/apps/shared/wait.go @@ -0,0 +1,70 @@ +package shared + +import ( + "fmt" + "io" + "time" + + "github.com/pterm/pterm" + "github.com/spf13/cobra" + + deployserverclient "github.com/formancehq/fctl/internal/deployserverclient/v3" + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" + fctl "github.com/formancehq/fctl/v3/pkg" +) + +// WaitRunCompletion polls for a run to reach a terminal state and returns the final run and any error logs. +func WaitRunCompletion(cmd *cobra.Command, apiClient *deployserverclient.DeployServer, runID string) (*components.Run, []components.Log, error) { + spinner := &pterm.DefaultSpinner + + if s := fctl.GetString(cmd, "output"); s == "plain" { + var err error + spinner, err = spinner.Start("Waiting for deployment to complete...") + if err != nil { + return nil, nil, err + } + defer func() { + if err := spinner.Stop(); err != nil { + pterm.Error.Println(err) + } + }() + } else { + spinner.SetWriter(io.Discard) + } + defer func() { + _ = spinner.Stop() + }() + + waitFor := 0 * time.Second + for { + select { + case <-cmd.Context().Done(): + return nil, nil, cmd.Context().Err() + case <-time.After(waitFor): + waitFor = 2 * time.Second + r, err := apiClient.ReadRun(cmd.Context(), runID) + if err != nil { + return nil, nil, err + } + run := r.RunResponse.Data + + spinner.UpdateText(fmt.Sprintf("Deployment status: %s", run.Status)) + switch run.Status { + case "applied": + spinner.UpdateText("Deployment completed successfully") + return &run, nil, nil + case "planned_and_finished": + spinner.UpdateText("Deployment completed successfully, no changes to apply") + return &run, nil, nil + case "errored": + l, err := apiClient.ReadRunLogs(cmd.Context(), runID) + if err != nil { + return &run, nil, err + } + return &run, l.ReadLogsResponse.Data, nil + default: + continue + } + } + } +} diff --git a/cmd/cloud/apps/show.go b/cmd/apps/show.go similarity index 100% rename from cmd/cloud/apps/show.go rename to cmd/apps/show.go diff --git a/cmd/cloud/apps/variables/create.go b/cmd/apps/variables/create.go similarity index 100% rename from cmd/cloud/apps/variables/create.go rename to cmd/apps/variables/create.go diff --git a/cmd/cloud/apps/variables/delete.go b/cmd/apps/variables/delete.go similarity index 100% rename from cmd/cloud/apps/variables/delete.go rename to cmd/apps/variables/delete.go diff --git a/cmd/cloud/apps/variables/list.go b/cmd/apps/variables/list.go similarity index 100% rename from cmd/cloud/apps/variables/list.go rename to cmd/apps/variables/list.go diff --git a/cmd/cloud/apps/variables/root.go b/cmd/apps/variables/root.go similarity index 100% rename from cmd/cloud/apps/variables/root.go rename to cmd/apps/variables/root.go diff --git a/cmd/cloud/apps/versions/archive.go b/cmd/apps/versions/archive.go similarity index 100% rename from cmd/cloud/apps/versions/archive.go rename to cmd/apps/versions/archive.go diff --git a/cmd/cloud/apps/versions/list.go b/cmd/apps/versions/list.go similarity index 100% rename from cmd/cloud/apps/versions/list.go rename to cmd/apps/versions/list.go diff --git a/cmd/cloud/apps/versions/manifest.go b/cmd/apps/versions/manifest.go similarity index 100% rename from cmd/cloud/apps/versions/manifest.go rename to cmd/apps/versions/manifest.go diff --git a/cmd/cloud/apps/versions/root.go b/cmd/apps/versions/root.go similarity index 100% rename from cmd/cloud/apps/versions/root.go rename to cmd/apps/versions/root.go diff --git a/cmd/cloud/apps/versions/show.go b/cmd/apps/versions/show.go similarity index 100% rename from cmd/cloud/apps/versions/show.go rename to cmd/apps/versions/show.go diff --git a/cmd/cloud/apps/deploy.go b/cmd/cloud/apps/deploy.go deleted file mode 100644 index 35c89519..00000000 --- a/cmd/cloud/apps/deploy.go +++ /dev/null @@ -1,239 +0,0 @@ -package apps - -import ( - "fmt" - "io" - "os" - "path/filepath" - "time" - - "github.com/pterm/pterm" - "github.com/spf13/cobra" - - "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" - - "github.com/formancehq/fctl/v3/cmd/cloud/apps/printer" - fctl "github.com/formancehq/fctl/v3/pkg" -) - -type Deploy struct { - *components.Run - logs []components.Log -} - -type DeployCtrl struct { - store *Deploy -} - -var _ fctl.Controller[*Deploy] = (*DeployCtrl)(nil) - -func newDeployStore() *Deploy { - return &Deploy{} -} - -func NewDeployCtrl() *DeployCtrl { - return &DeployCtrl{ - store: newDeployStore(), - } -} - -func NewDeploy() *cobra.Command { - return fctl.NewCommand("deploy", - fctl.WithShortDescription("Deploy apps"), - fctl.WithStringFlag("id", "", "App ID"), - fctl.WithStringFlag("path", "", "Path to the manifest file"), - fctl.WithBoolFlag("wait", true, "Wait for the deployment to complete"), - fctl.WithController(NewDeployCtrl()), - ) -} - -func (c *DeployCtrl) GetStore() *Deploy { - return c.store -} - -func (c *DeployCtrl) Run(cmd *cobra.Command, args []string) (fctl.Renderable, error) { - _, profile, profileName, relyingParty, err := fctl.LoadAndAuthenticateCurrentProfile(cmd) - if err != nil { - return nil, err - } - - _, apiClient, err := fctl.NewAppDeployClientFromFlags( - cmd, - relyingParty, - fctl.NewPTermDialog(), - profileName, - *profile, - ) - if err != nil { - return nil, err - } - id := fctl.GetString(cmd, "id") - if id == "" { - return nil, fmt.Errorf("id is required") - } - path := fctl.GetString(cmd, "path") - if path == "" { - return nil, fmt.Errorf("path is required") - } - data, err := os.ReadFile(filepath.Clean(path)) - if err != nil { - return nil, err - } - - cmd.SilenceUsage = true - deployment, err := apiClient.DeployAppConfigurationRaw(cmd.Context(), id, data) - if err != nil { - return nil, err - } - c.store.Run = &deployment.RunResponse.Data - - if fctl.GetBool(cmd, "wait") { - if err := c.waitRunCompletion(cmd); err != nil { - return nil, err - } - } - - return c, nil -} - -func (c *DeployCtrl) waitRunCompletion(cmd *cobra.Command) error { - - _, profile, profileName, relyingParty, err := fctl.LoadAndAuthenticateCurrentProfile(cmd) - if err != nil { - return err - } - - _, apiClient, err := fctl.NewAppDeployClientFromFlags( - cmd, - relyingParty, - fctl.NewPTermDialog(), - profileName, - *profile, - ) - if err != nil { - return err - } - spinner := &pterm.DefaultSpinner - - if s := fctl.GetString(cmd, "output"); s == "plain" { - var err error - spinner, err = spinner.Start("Waiting for deployment to complete...") - if err != nil { - return err - } - defer func() { - if err := spinner.Stop(); err != nil { - pterm.Error.Println(err) - } - }() - } else { - spinner.SetWriter(io.Discard) - } - defer func() { - _ = spinner.Stop() - }() - - waitFor := 0 * time.Second - for { - select { - case <-cmd.Context().Done(): - return cmd.Context().Err() - case <-time.After(waitFor): - waitFor = 2 * time.Second - r, err := apiClient.ReadRun(cmd.Context(), c.store.ID) - if err != nil { - return err - } - c.store.Run = &r.RunResponse.Data - - spinner.UpdateText(fmt.Sprintf("Deployment status: %s", r.RunResponse.Data.Status)) - switch r.RunResponse.Data.Status { - case "applied": - spinner.UpdateText("Deployment completed successfully") - return nil - case "planned_and_finished": - spinner.UpdateText("Deployment completed successfully, no changes to apply") - return nil - case "errored": - l, err := apiClient.ReadRunLogs(cmd.Context(), c.store.ID) - if err != nil { - return err - } - - c.store.logs = l.ReadLogsResponse.Data - - return nil - default: - continue - } - } - } -} - -func (c *DeployCtrl) Render(cmd *cobra.Command, args []string) error { - if c.store.Run.Status == "errored" { - if len(c.store.logs) > 0 { - if err := printer.RenderLogs(cmd.ErrOrStderr(), c.store.logs); err != nil { - return err - } - } - return fmt.Errorf("deployment failed: %s", c.store.ID) - } - - pterm.Info.Println("App Deployment accepted", c.store.ID) - wait := fctl.GetBool(cmd, "wait") - if !wait { - return nil - } - if err := c.waitRunCompletion(cmd); err != nil { - return err - } - - cfg, err := fctl.LoadConfig(cmd) - if err != nil { - return err - } - - profile, profileName, err := fctl.LoadCurrentProfile(cmd, *cfg) - if err != nil { - return err - } - - relyingParty, err := fctl.GetAuthRelyingParty(cmd.Context(), fctl.GetHttpClient(cmd), profile.MembershipURI) - if err != nil { - return err - } - - organizationID, apiClient, err := fctl.NewAppDeployClientFromFlags( - cmd, - relyingParty, - fctl.NewPTermDialog(), - profileName, - *profile, - ) - if err != nil { - return err - } - id := fctl.GetString(cmd, "id") - currentStateRes, err := apiClient.ReadAppCurrentStateVersion(cmd.Context(), id) - if err != nil { - return err - } - if state := currentStateRes.GetReadStateResponse().Data.Stack; state != nil { - - apiClient, err := fctl.NewMembershipClientForOrganization(cmd, relyingParty, fctl.NewPTermDialog(), profileName, *profile, organizationID) - if err != nil { - return err - } - - info, err := apiClient.GetServerInfo(cmd.Context()) - if err != nil { - return err - } - - if info.ServerInfo.ConsoleURL != nil { - pterm.Success.Printfln("View stack in console: %s/%s/%s?region=%s", *info.ServerInfo.ConsoleURL, organizationID, state["id"], state["region_id"]) - } - } - return nil -} diff --git a/cmd/cloud/apps/root.go b/cmd/cloud/apps/root.go deleted file mode 100644 index eadf32b7..00000000 --- a/cmd/cloud/apps/root.go +++ /dev/null @@ -1,45 +0,0 @@ -package apps - -import ( - "fmt" - - "github.com/spf13/cobra" - - "github.com/formancehq/fctl/v3/cmd/cloud/apps/runs" - "github.com/formancehq/fctl/v3/cmd/cloud/apps/variables" - "github.com/formancehq/fctl/v3/cmd/cloud/apps/versions" - fctl "github.com/formancehq/fctl/v3/pkg" -) - -func NewCommand() *cobra.Command { - cmd := fctl.NewMembershipCommand("apps", - fctl.WithShortDescription("* New * Apps manifests management"), - fctl.WithPersistentBoolFlag("experimental", false, "Enable experimental commands"), - fctl.WithPersistentStringFlag(fctl.FrameworkURIFlag, "https://deploy.formance.cloud", "Framework URI"), - fctl.WithPersistentPreRunE(func(cmd *cobra.Command, args []string) error { - ok, err := cmd.Flags().GetBool("experimental") - if err != nil { - return err - } - - if !ok { - return fmt.Errorf("the apps command is experimental, please use the --experimental flag to enable it") - } - - return nil - }), - fctl.WithAliases("app"), - fctl.WithChildCommands( - NewList(), - NewCreate(), - NewDelete(), - NewShow(), - NewDeploy(), - runs.NewCommand(), - versions.NewCommand(), - variables.NewCommand(), - ), - ) - - return cmd -} diff --git a/cmd/cloud/root.go b/cmd/cloud/root.go index 5021bfc8..48da7b97 100644 --- a/cmd/cloud/root.go +++ b/cmd/cloud/root.go @@ -3,7 +3,6 @@ package cloud import ( "github.com/spf13/cobra" - "github.com/formancehq/fctl/v3/cmd/cloud/apps" "github.com/formancehq/fctl/v3/cmd/cloud/me" "github.com/formancehq/fctl/v3/cmd/cloud/organizations" "github.com/formancehq/fctl/v3/cmd/cloud/regions" @@ -19,7 +18,6 @@ func NewCommand() *cobra.Command { me.NewCommand(), regions.NewCommand(), NewGeneratePersonalTokenCommand(), - apps.NewCommand(), ), ) } diff --git a/cmd/root.go b/cmd/root.go index 1108224d..13029392 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,6 +17,7 @@ import ( "github.com/formancehq/go-libs/v3/api" "github.com/formancehq/go-libs/v3/logging" + "github.com/formancehq/fctl/v3/cmd/apps" "github.com/formancehq/fctl/v3/cmd/auth" "github.com/formancehq/fctl/v3/cmd/cloud" "github.com/formancehq/fctl/v3/cmd/ledger" @@ -52,6 +53,7 @@ func NewRootCommand() *cobra.Command { version.NewCommand(), login.NewCommand(), NewPromptCommand(), + apps.NewCommand(), ledger.NewCommand(), payments.NewCommand(), reconciliation.NewCommand(), diff --git a/internal/deployserverclient/deployserver.go b/internal/deployserverclient/deployserver.go index fcb5fda0..1940c5cf 100644 --- a/internal/deployserverclient/deployserver.go +++ b/internal/deployserverclient/deployserver.go @@ -140,11 +140,10 @@ func New(opts ...SDKOption) *DeployServer { } // ListApps - List organization apps -func (s *DeployServer) ListApps(ctx context.Context, organizationID string, pageNumber *int64, pageSize *int64, opts ...operations.Option) (*operations.ListAppsResponse, error) { +func (s *DeployServer) ListApps(ctx context.Context, pageNumber *int64, pageSize *int64, opts ...operations.Option) (*operations.ListAppsResponse, error) { request := operations.ListAppsRequest{ - OrganizationID: organizationID, - PageNumber: pageNumber, - PageSize: pageSize, + PageNumber: pageNumber, + PageSize: pageSize, } o := operations.Options{} diff --git a/internal/deployserverclient/deployserver_extensions.go b/internal/deployserverclient/deployserver_extensions.go new file mode 100644 index 00000000..5b972623 --- /dev/null +++ b/internal/deployserverclient/deployserver_extensions.go @@ -0,0 +1,748 @@ +package deployserverclient + +import ( + "bytes" + "context" + "fmt" + "net/http" + + "github.com/formancehq/fctl/internal/deployserverclient/v3/internal/hooks" + "github.com/formancehq/fctl/internal/deployserverclient/v3/internal/utils" + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/apierrors" + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/operations" + "github.com/formancehq/fctl/internal/deployserverclient/v3/retry" +) + +// doRequest is a shared helper that handles the full request lifecycle (hooks, retries, error responses). +func (s *DeployServer) doRequest(ctx context.Context, hookCtx hooks.HookContext, req *http.Request, o operations.Options) (*http.Response, error) { + globalRetryConfig := s.sdkConfiguration.RetryConfig + retryConfig := o.Retries + if retryConfig == nil { + if globalRetryConfig != nil { + retryConfig = globalRetryConfig + } + } + + var httpRes *http.Response + var err error + + if retryConfig != nil { + httpRes, err = utils.Retry(ctx, utils.Retries{ + Config: retryConfig, + StatusCodes: []string{ + "429", "500", "502", "503", "504", + }, + }, func() (*http.Response, error) { + if req.Body != nil && req.Body != http.NoBody && req.GetBody != nil { + copyBody, err := req.GetBody() + if err != nil { + return nil, err + } + req.Body = copyBody + } + + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + if retry.IsPermanentError(err) || retry.IsTemporaryError(err) { + return nil, err + } + return nil, retry.Permanent(err) + } + + httpRes, err := s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + } + return httpRes, err + }) + + if err != nil { + return nil, err + } + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } else { + req, err = s.hooks.BeforeRequest(hooks.BeforeRequestContext{HookContext: hookCtx}, req) + if err != nil { + return nil, err + } + + httpRes, err = s.sdkConfiguration.Client.Do(req) + if err != nil || httpRes == nil { + if err != nil { + err = fmt.Errorf("error sending request: %w", err) + } else { + err = fmt.Errorf("error sending request: no response") + } + _, err = s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, nil, err) + return nil, err + } else if utils.MatchStatusCodes([]string{"4XX", "5XX"}, httpRes.StatusCode) { + _httpRes, err := s.hooks.AfterError(hooks.AfterErrorContext{HookContext: hookCtx}, httpRes, nil) + if err != nil { + return nil, err + } else if _httpRes != nil { + httpRes = _httpRes + } + } else { + httpRes, err = s.hooks.AfterSuccess(hooks.AfterSuccessContext{HookContext: hookCtx}, httpRes) + if err != nil { + return nil, err + } + } + } + return httpRes, nil +} + +func (s *DeployServer) prepareOptions(opts []operations.Option) (operations.Options, error) { + o := operations.Options{} + supportedOptions := []string{ + operations.SupportedOptionRetries, + operations.SupportedOptionTimeout, + } + for _, opt := range opts { + if err := opt(&o, supportedOptions...); err != nil { + return o, fmt.Errorf("error applying option: %w", err) + } + } + return o, nil +} + +func (s *DeployServer) baseURL(o operations.Options) string { + if o.ServerURL == nil { + return utils.ReplaceParameters(s.sdkConfiguration.GetServerDetails()) + } + return *o.ServerURL +} + +func (s *DeployServer) newHookCtx(ctx context.Context, baseURL, operationID string) hooks.HookContext { + return hooks.HookContext{ + SDK: s, + SDKConfiguration: s.sdkConfiguration, + BaseURL: baseURL, + Context: ctx, + OperationID: operationID, + OAuth2Scopes: nil, + SecuritySource: nil, + } +} + +func handleErrorResponse(httpRes *http.Response) error { + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return err + } + return apierrors.NewAPIError("API error occurred", httpRes.StatusCode, string(rawBody), httpRes) +} + +// PushManifest - Push a new manifest version +func (s *DeployServer) PushManifest(ctx context.Context, id string, requestBody any, opts ...operations.Option) (*operations.PushManifestResponse, error) { + request := operations.PushManifestRequest{ + ID: id, + RequestBody: requestBody, + } + + o, err := s.prepareOptions(opts) + if err != nil { + return nil, err + } + + baseURL := s.baseURL(o) + opURL, err := utils.GenerateURL(ctx, baseURL, "/apps/{id}/manifest", request, nil) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := s.newHookCtx(ctx, baseURL, "pushManifest") + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, false, "RequestBody", "raw", `request:"mediaType=application/yaml"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + httpRes, err := s.doRequest(ctx, hookCtx, req, o) + if err != nil { + return nil, err + } + + res := &operations.PushManifestResponse{ + HTTPMeta: components.HTTPMetadata{ + Request: req, + Response: httpRes, + }, + } + + switch { + case httpRes.StatusCode == 201: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + var out components.ManifestVersionResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + res.ManifestVersionResponse = &out + default: + return nil, handleErrorResponse(httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 600: + return nil, handleErrorResponse(httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + var out components.Error + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + res.Error = &out + default: + return nil, handleErrorResponse(httpRes) + } + } + + return res, nil +} + +// ListManifestVersions - List manifest versions +func (s *DeployServer) ListManifestVersions(ctx context.Context, id string, opts ...operations.Option) (*operations.ListManifestVersionsResponse, error) { + request := operations.ListManifestVersionsRequest{ID: id} + + o, err := s.prepareOptions(opts) + if err != nil { + return nil, err + } + + baseURL := s.baseURL(o) + opURL, err := utils.GenerateURL(ctx, baseURL, "/apps/{id}/manifest/versions", request, nil) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := s.newHookCtx(ctx, baseURL, "listManifestVersions") + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + httpRes, err := s.doRequest(ctx, hookCtx, req, o) + if err != nil { + return nil, err + } + + res := &operations.ListManifestVersionsResponse{ + HTTPMeta: components.HTTPMetadata{Request: req, Response: httpRes}, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + var out components.ListManifestsResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + res.ListManifestsResponse = &out + default: + return nil, handleErrorResponse(httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 600: + return nil, handleErrorResponse(httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + var out components.Error + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + res.Error = &out + default: + return nil, handleErrorResponse(httpRes) + } + } + + return res, nil +} + +// CreateDeployment - Create a deployment for an app +func (s *DeployServer) CreateDeployment(ctx context.Context, id string, createDeploymentRequest components.CreateDeploymentRequest, opts ...operations.Option) (*operations.CreateDeploymentResponse, error) { + request := operations.CreateDeploymentRequest{ + ID: id, + CreateDeploymentRequest: createDeploymentRequest, + } + + o, err := s.prepareOptions(opts) + if err != nil { + return nil, err + } + + baseURL := s.baseURL(o) + opURL, err := utils.GenerateURL(ctx, baseURL, "/apps/{id}/deployments", request, nil) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := s.newHookCtx(ctx, baseURL, "createDeployment") + bodyReader, reqContentType, err := utils.SerializeRequestBody(ctx, request, false, false, "CreateDeploymentRequest", "json", `request:"mediaType=application/json"`) + if err != nil { + return nil, err + } + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, bodyReader) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if reqContentType != "" { + req.Header.Set("Content-Type", reqContentType) + } + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + httpRes, err := s.doRequest(ctx, hookCtx, req, o) + if err != nil { + return nil, err + } + + res := &operations.CreateDeploymentResponse{ + HTTPMeta: components.HTTPMetadata{Request: req, Response: httpRes}, + } + + switch { + case httpRes.StatusCode == 201: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + var out components.DeploymentResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + res.DeploymentResponse = &out + default: + return nil, handleErrorResponse(httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 600: + return nil, handleErrorResponse(httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + var out components.Error + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + res.Error = &out + default: + return nil, handleErrorResponse(httpRes) + } + } + + return res, nil +} + +// ListDeployments - List deployments for an app +func (s *DeployServer) ListDeployments(ctx context.Context, id string, opts ...operations.Option) (*operations.ListDeploymentsResponse, error) { + request := operations.ListDeploymentsRequest{ID: id} + + o, err := s.prepareOptions(opts) + if err != nil { + return nil, err + } + + baseURL := s.baseURL(o) + opURL, err := utils.GenerateURL(ctx, baseURL, "/apps/{id}/deployments", request, nil) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := s.newHookCtx(ctx, baseURL, "listDeployments") + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + httpRes, err := s.doRequest(ctx, hookCtx, req, o) + if err != nil { + return nil, err + } + + res := &operations.ListDeploymentsResponse{ + HTTPMeta: components.HTTPMetadata{Request: req, Response: httpRes}, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + var out components.ListDeploymentsResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + res.ListDeploymentsResponse = &out + default: + return nil, handleErrorResponse(httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 600: + return nil, handleErrorResponse(httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + var out components.Error + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + res.Error = &out + default: + return nil, handleErrorResponse(httpRes) + } + } + + return res, nil +} + +// ReadDeployment - Read a deployment +func (s *DeployServer) ReadDeployment(ctx context.Context, id string, name string, opts ...operations.Option) (*operations.ReadDeploymentResponse, error) { + request := operations.ReadDeploymentRequest{ID: id, Name: name} + + o, err := s.prepareOptions(opts) + if err != nil { + return nil, err + } + + baseURL := s.baseURL(o) + opURL, err := utils.GenerateURL(ctx, baseURL, "/apps/{id}/deployments/{name}", request, nil) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := s.newHookCtx(ctx, baseURL, "readDeployment") + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "GET", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + httpRes, err := s.doRequest(ctx, hookCtx, req, o) + if err != nil { + return nil, err + } + + res := &operations.ReadDeploymentResponse{ + HTTPMeta: components.HTTPMetadata{Request: req, Response: httpRes}, + } + + switch { + case httpRes.StatusCode == 200: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + var out components.DeploymentResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + res.DeploymentResponse = &out + default: + return nil, handleErrorResponse(httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 600: + return nil, handleErrorResponse(httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + var out components.Error + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + res.Error = &out + default: + return nil, handleErrorResponse(httpRes) + } + } + + return res, nil +} + +// DeleteDeployment - Delete a deployment +func (s *DeployServer) DeleteDeployment(ctx context.Context, id string, name string, opts ...operations.Option) (*operations.DeleteDeploymentResponse, error) { + request := operations.DeleteDeploymentRequest{ID: id, Name: name} + + o, err := s.prepareOptions(opts) + if err != nil { + return nil, err + } + + baseURL := s.baseURL(o) + opURL, err := utils.GenerateURL(ctx, baseURL, "/apps/{id}/deployments/{name}", request, nil) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := s.newHookCtx(ctx, baseURL, "deleteDeployment") + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "DELETE", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + httpRes, err := s.doRequest(ctx, hookCtx, req, o) + if err != nil { + return nil, err + } + + res := &operations.DeleteDeploymentResponse{ + HTTPMeta: components.HTTPMetadata{Request: req, Response: httpRes}, + } + + switch { + case httpRes.StatusCode == 204: + utils.DrainBody(httpRes) + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 600: + return nil, handleErrorResponse(httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + var out components.Error + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + res.Error = &out + default: + return nil, handleErrorResponse(httpRes) + } + } + + return res, nil +} + +// DeployToDeployment - Deploy a manifest to a deployment +func (s *DeployServer) DeployToDeployment(ctx context.Context, id string, name string, version *int64, opts ...operations.Option) (*operations.DeployToDeploymentResponse, error) { + request := operations.DeployToDeploymentRequest{ + ID: id, + Name: name, + Version: version, + } + + o, err := s.prepareOptions(opts) + if err != nil { + return nil, err + } + + baseURL := s.baseURL(o) + opURL, err := utils.GenerateURL(ctx, baseURL, "/apps/{id}/deployments/{name}/deploy", request, nil) + if err != nil { + return nil, fmt.Errorf("error generating URL: %w", err) + } + + hookCtx := s.newHookCtx(ctx, baseURL, "deployToDeployment") + + timeout := o.Timeout + if timeout == nil { + timeout = s.sdkConfiguration.Timeout + } + if timeout != nil { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *timeout) + defer cancel() + } + + req, err := http.NewRequestWithContext(ctx, "POST", opURL, nil) + if err != nil { + return nil, fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", s.sdkConfiguration.UserAgent) + if err := utils.PopulateQueryParams(ctx, req, request, nil, nil); err != nil { + return nil, fmt.Errorf("error populating query params: %w", err) + } + for k, v := range o.SetHeaders { + req.Header.Set(k, v) + } + + httpRes, err := s.doRequest(ctx, hookCtx, req, o) + if err != nil { + return nil, err + } + + res := &operations.DeployToDeploymentResponse{ + HTTPMeta: components.HTTPMetadata{Request: req, Response: httpRes}, + } + + switch { + case httpRes.StatusCode == 202: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + var out components.RunResponse + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + res.RunResponse = &out + default: + return nil, handleErrorResponse(httpRes) + } + case httpRes.StatusCode >= 400 && httpRes.StatusCode < 600: + return nil, handleErrorResponse(httpRes) + default: + switch { + case utils.MatchContentType(httpRes.Header.Get("Content-Type"), `application/json`): + rawBody, err := utils.ConsumeRawBody(httpRes) + if err != nil { + return nil, err + } + var out components.Error + if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out, ""); err != nil { + return nil, err + } + res.Error = &out + default: + return nil, handleErrorResponse(httpRes) + } + } + + return res, nil +} diff --git a/internal/deployserverclient/models/components/createapprequest.go b/internal/deployserverclient/models/components/createapprequest.go index b79eeb0c..454c8cb8 100644 --- a/internal/deployserverclient/models/components/createapprequest.go +++ b/internal/deployserverclient/models/components/createapprequest.go @@ -1,16 +1,15 @@ // Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. -// @generated-id: 348979031a84 package components type CreateAppRequest struct { - // ID of the organization to which the app belongs - OrganizationID string `json:"organizationId"` + // User-provided name for the app + Name string `json:"name"` } -func (c *CreateAppRequest) GetOrganizationID() string { +func (c *CreateAppRequest) GetName() string { if c == nil { return "" } - return c.OrganizationID + return c.Name } diff --git a/internal/deployserverclient/models/components/createdeploymentrequest.go b/internal/deployserverclient/models/components/createdeploymentrequest.go new file mode 100644 index 00000000..9d461210 --- /dev/null +++ b/internal/deployserverclient/models/components/createdeploymentrequest.go @@ -0,0 +1,22 @@ +package components + +type CreateDeploymentRequest struct { + // Name for the deployment + Name string `json:"name"` + // ID of the Formance Cloud stack to deploy to + StackID string `json:"stackId"` +} + +func (c *CreateDeploymentRequest) GetName() string { + if c == nil { + return "" + } + return c.Name +} + +func (c *CreateDeploymentRequest) GetStackID() string { + if c == nil { + return "" + } + return c.StackID +} diff --git a/internal/deployserverclient/models/components/deployment.go b/internal/deployserverclient/models/components/deployment.go new file mode 100644 index 00000000..1959a6ab --- /dev/null +++ b/internal/deployserverclient/models/components/deployment.go @@ -0,0 +1,40 @@ +package components + +type Deployment struct { + // Name of the deployment + Name string `json:"name"` + // ID of the parent app + AppID string `json:"appId"` + // ID of the Formance Cloud stack + StackID string `json:"stackId"` + // ID of the TFC workspace + WorkspaceID string `json:"workspaceId"` +} + +func (d *Deployment) GetName() string { + if d == nil { + return "" + } + return d.Name +} + +func (d *Deployment) GetAppID() string { + if d == nil { + return "" + } + return d.AppID +} + +func (d *Deployment) GetStackID() string { + if d == nil { + return "" + } + return d.StackID +} + +func (d *Deployment) GetWorkspaceID() string { + if d == nil { + return "" + } + return d.WorkspaceID +} diff --git a/internal/deployserverclient/models/components/deploymentresponse.go b/internal/deployserverclient/models/components/deploymentresponse.go new file mode 100644 index 00000000..bc387cbe --- /dev/null +++ b/internal/deployserverclient/models/components/deploymentresponse.go @@ -0,0 +1,12 @@ +package components + +type DeploymentResponse struct { + Data Deployment `json:"data"` +} + +func (d *DeploymentResponse) GetData() Deployment { + if d == nil { + return Deployment{} + } + return d.Data +} diff --git a/internal/deployserverclient/models/components/listdeploymentsresponse.go b/internal/deployserverclient/models/components/listdeploymentsresponse.go new file mode 100644 index 00000000..bd540899 --- /dev/null +++ b/internal/deployserverclient/models/components/listdeploymentsresponse.go @@ -0,0 +1,12 @@ +package components + +type ListDeploymentsResponse struct { + Data []Deployment `json:"data"` +} + +func (l *ListDeploymentsResponse) GetData() []Deployment { + if l == nil { + return nil + } + return l.Data +} diff --git a/internal/deployserverclient/models/components/listmanifestsresponse.go b/internal/deployserverclient/models/components/listmanifestsresponse.go new file mode 100644 index 00000000..5f05f065 --- /dev/null +++ b/internal/deployserverclient/models/components/listmanifestsresponse.go @@ -0,0 +1,12 @@ +package components + +type ListManifestsResponse struct { + Data []ManifestVersion `json:"data"` +} + +func (l *ListManifestsResponse) GetData() []ManifestVersion { + if l == nil { + return nil + } + return l.Data +} diff --git a/internal/deployserverclient/models/components/manifestversion.go b/internal/deployserverclient/models/components/manifestversion.go new file mode 100644 index 00000000..736ae204 --- /dev/null +++ b/internal/deployserverclient/models/components/manifestversion.go @@ -0,0 +1,42 @@ +package components + +import "time" + +type ManifestVersion struct { + // ID of the parent app + AppID string `json:"appId"` + // Manifest version number + Version int `json:"version"` + // Raw manifest content + Content string `json:"content"` + // When this version was pushed + CreatedAt time.Time `json:"createdAt"` +} + +func (m *ManifestVersion) GetAppID() string { + if m == nil { + return "" + } + return m.AppID +} + +func (m *ManifestVersion) GetVersion() int { + if m == nil { + return 0 + } + return m.Version +} + +func (m *ManifestVersion) GetContent() string { + if m == nil { + return "" + } + return m.Content +} + +func (m *ManifestVersion) GetCreatedAt() time.Time { + if m == nil { + return time.Time{} + } + return m.CreatedAt +} diff --git a/internal/deployserverclient/models/components/manifestversionresponse.go b/internal/deployserverclient/models/components/manifestversionresponse.go new file mode 100644 index 00000000..60d02a71 --- /dev/null +++ b/internal/deployserverclient/models/components/manifestversionresponse.go @@ -0,0 +1,12 @@ +package components + +type ManifestVersionResponse struct { + Data ManifestVersion `json:"data"` +} + +func (m *ManifestVersionResponse) GetData() ManifestVersion { + if m == nil { + return ManifestVersion{} + } + return m.Data +} diff --git a/internal/deployserverclient/models/operations/createdeployment.go b/internal/deployserverclient/models/operations/createdeployment.go new file mode 100644 index 00000000..33cb5586 --- /dev/null +++ b/internal/deployserverclient/models/operations/createdeployment.go @@ -0,0 +1,53 @@ +package operations + +import ( + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" +) + +type CreateDeploymentRequest struct { + ID string `pathParam:"style=simple,explode=false,name=id"` + CreateDeploymentRequest components.CreateDeploymentRequest `request:"mediaType=application/json"` +} + +func (c *CreateDeploymentRequest) GetID() string { + if c == nil { + return "" + } + return c.ID +} + +func (c *CreateDeploymentRequest) GetCreateDeploymentRequest() components.CreateDeploymentRequest { + if c == nil { + return components.CreateDeploymentRequest{} + } + return c.CreateDeploymentRequest +} + +type CreateDeploymentResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Deployment created successfully + DeploymentResponse *components.DeploymentResponse + // Error + Error *components.Error +} + +func (c *CreateDeploymentResponse) GetHTTPMeta() components.HTTPMetadata { + if c == nil { + return components.HTTPMetadata{} + } + return c.HTTPMeta +} + +func (c *CreateDeploymentResponse) GetDeploymentResponse() *components.DeploymentResponse { + if c == nil { + return nil + } + return c.DeploymentResponse +} + +func (c *CreateDeploymentResponse) GetError() *components.Error { + if c == nil { + return nil + } + return c.Error +} diff --git a/internal/deployserverclient/models/operations/deletedeployment.go b/internal/deployserverclient/models/operations/deletedeployment.go new file mode 100644 index 00000000..57a7c76f --- /dev/null +++ b/internal/deployserverclient/models/operations/deletedeployment.go @@ -0,0 +1,44 @@ +package operations + +import ( + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" +) + +type DeleteDeploymentRequest struct { + ID string `pathParam:"style=simple,explode=false,name=id"` + Name string `pathParam:"style=simple,explode=false,name=name"` +} + +func (d *DeleteDeploymentRequest) GetID() string { + if d == nil { + return "" + } + return d.ID +} + +func (d *DeleteDeploymentRequest) GetName() string { + if d == nil { + return "" + } + return d.Name +} + +type DeleteDeploymentResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Error + Error *components.Error +} + +func (d *DeleteDeploymentResponse) GetHTTPMeta() components.HTTPMetadata { + if d == nil { + return components.HTTPMetadata{} + } + return d.HTTPMeta +} + +func (d *DeleteDeploymentResponse) GetError() *components.Error { + if d == nil { + return nil + } + return d.Error +} diff --git a/internal/deployserverclient/models/operations/deploytodeployment.go b/internal/deployserverclient/models/operations/deploytodeployment.go new file mode 100644 index 00000000..2f9af59b --- /dev/null +++ b/internal/deployserverclient/models/operations/deploytodeployment.go @@ -0,0 +1,61 @@ +package operations + +import ( + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" +) + +type DeployToDeploymentRequest struct { + ID string `pathParam:"style=simple,explode=false,name=id"` + Name string `pathParam:"style=simple,explode=false,name=name"` + Version *int64 `queryParam:"style=form,explode=true,name=version"` +} + +func (d *DeployToDeploymentRequest) GetID() string { + if d == nil { + return "" + } + return d.ID +} + +func (d *DeployToDeploymentRequest) GetName() string { + if d == nil { + return "" + } + return d.Name +} + +func (d *DeployToDeploymentRequest) GetVersion() *int64 { + if d == nil { + return nil + } + return d.Version +} + +type DeployToDeploymentResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Manifest deployed successfully + RunResponse *components.RunResponse + // Error + Error *components.Error +} + +func (d *DeployToDeploymentResponse) GetHTTPMeta() components.HTTPMetadata { + if d == nil { + return components.HTTPMetadata{} + } + return d.HTTPMeta +} + +func (d *DeployToDeploymentResponse) GetRunResponse() *components.RunResponse { + if d == nil { + return nil + } + return d.RunResponse +} + +func (d *DeployToDeploymentResponse) GetError() *components.Error { + if d == nil { + return nil + } + return d.Error +} diff --git a/internal/deployserverclient/models/operations/listapps.go b/internal/deployserverclient/models/operations/listapps.go index cffd13ec..e1695b2a 100644 --- a/internal/deployserverclient/models/operations/listapps.go +++ b/internal/deployserverclient/models/operations/listapps.go @@ -1,5 +1,4 @@ // Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. -// @generated-id: 5836489f2f6f package operations @@ -8,16 +7,8 @@ import ( ) type ListAppsRequest struct { - OrganizationID string `queryParam:"style=form,explode=true,name=organizationId"` - PageNumber *int64 `queryParam:"style=form,explode=true,name=pageNumber"` - PageSize *int64 `queryParam:"style=form,explode=true,name=pageSize"` -} - -func (l *ListAppsRequest) GetOrganizationID() string { - if l == nil { - return "" - } - return l.OrganizationID + PageNumber *int64 `queryParam:"style=form,explode=true,name=pageNumber"` + PageSize *int64 `queryParam:"style=form,explode=true,name=pageSize"` } func (l *ListAppsRequest) GetPageNumber() *int64 { diff --git a/internal/deployserverclient/models/operations/listdeployments.go b/internal/deployserverclient/models/operations/listdeployments.go new file mode 100644 index 00000000..bdda746d --- /dev/null +++ b/internal/deployserverclient/models/operations/listdeployments.go @@ -0,0 +1,45 @@ +package operations + +import ( + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" +) + +type ListDeploymentsRequest struct { + ID string `pathParam:"style=simple,explode=false,name=id"` +} + +func (l *ListDeploymentsRequest) GetID() string { + if l == nil { + return "" + } + return l.ID +} + +type ListDeploymentsResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Deployments retrieved successfully + ListDeploymentsResponse *components.ListDeploymentsResponse + // Error + Error *components.Error +} + +func (l *ListDeploymentsResponse) GetHTTPMeta() components.HTTPMetadata { + if l == nil { + return components.HTTPMetadata{} + } + return l.HTTPMeta +} + +func (l *ListDeploymentsResponse) GetListDeploymentsResponse() *components.ListDeploymentsResponse { + if l == nil { + return nil + } + return l.ListDeploymentsResponse +} + +func (l *ListDeploymentsResponse) GetError() *components.Error { + if l == nil { + return nil + } + return l.Error +} diff --git a/internal/deployserverclient/models/operations/listmanifestversions.go b/internal/deployserverclient/models/operations/listmanifestversions.go new file mode 100644 index 00000000..79d43958 --- /dev/null +++ b/internal/deployserverclient/models/operations/listmanifestversions.go @@ -0,0 +1,45 @@ +package operations + +import ( + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" +) + +type ListManifestVersionsRequest struct { + ID string `pathParam:"style=simple,explode=false,name=id"` +} + +func (l *ListManifestVersionsRequest) GetID() string { + if l == nil { + return "" + } + return l.ID +} + +type ListManifestVersionsResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Manifest versions retrieved successfully + ListManifestsResponse *components.ListManifestsResponse + // Error + Error *components.Error +} + +func (l *ListManifestVersionsResponse) GetHTTPMeta() components.HTTPMetadata { + if l == nil { + return components.HTTPMetadata{} + } + return l.HTTPMeta +} + +func (l *ListManifestVersionsResponse) GetListManifestsResponse() *components.ListManifestsResponse { + if l == nil { + return nil + } + return l.ListManifestsResponse +} + +func (l *ListManifestVersionsResponse) GetError() *components.Error { + if l == nil { + return nil + } + return l.Error +} diff --git a/internal/deployserverclient/models/operations/pushmanifest.go b/internal/deployserverclient/models/operations/pushmanifest.go new file mode 100644 index 00000000..65a72616 --- /dev/null +++ b/internal/deployserverclient/models/operations/pushmanifest.go @@ -0,0 +1,54 @@ +package operations + +import ( + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" +) + +type PushManifestRequest struct { + ID string `pathParam:"style=simple,explode=false,name=id"` + // This field accepts []byte data or io.Reader implementations, such as *os.File. + RequestBody any `request:"mediaType=application/yaml"` +} + +func (p *PushManifestRequest) GetID() string { + if p == nil { + return "" + } + return p.ID +} + +func (p *PushManifestRequest) GetRequestBody() any { + if p == nil { + return nil + } + return p.RequestBody +} + +type PushManifestResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Manifest pushed successfully + ManifestVersionResponse *components.ManifestVersionResponse + // Error + Error *components.Error +} + +func (p *PushManifestResponse) GetHTTPMeta() components.HTTPMetadata { + if p == nil { + return components.HTTPMetadata{} + } + return p.HTTPMeta +} + +func (p *PushManifestResponse) GetManifestVersionResponse() *components.ManifestVersionResponse { + if p == nil { + return nil + } + return p.ManifestVersionResponse +} + +func (p *PushManifestResponse) GetError() *components.Error { + if p == nil { + return nil + } + return p.Error +} diff --git a/internal/deployserverclient/models/operations/readdeployment.go b/internal/deployserverclient/models/operations/readdeployment.go new file mode 100644 index 00000000..29065927 --- /dev/null +++ b/internal/deployserverclient/models/operations/readdeployment.go @@ -0,0 +1,53 @@ +package operations + +import ( + "github.com/formancehq/fctl/internal/deployserverclient/v3/models/components" +) + +type ReadDeploymentRequest struct { + ID string `pathParam:"style=simple,explode=false,name=id"` + Name string `pathParam:"style=simple,explode=false,name=name"` +} + +func (r *ReadDeploymentRequest) GetID() string { + if r == nil { + return "" + } + return r.ID +} + +func (r *ReadDeploymentRequest) GetName() string { + if r == nil { + return "" + } + return r.Name +} + +type ReadDeploymentResponse struct { + HTTPMeta components.HTTPMetadata `json:"-"` + // Deployment retrieved successfully + DeploymentResponse *components.DeploymentResponse + // Error + Error *components.Error +} + +func (r *ReadDeploymentResponse) GetHTTPMeta() components.HTTPMetadata { + if r == nil { + return components.HTTPMetadata{} + } + return r.HTTPMeta +} + +func (r *ReadDeploymentResponse) GetDeploymentResponse() *components.DeploymentResponse { + if r == nil { + return nil + } + return r.DeploymentResponse +} + +func (r *ReadDeploymentResponse) GetError() *components.Error { + if r == nil { + return nil + } + return r.Error +} diff --git a/openapi/deployserver.yaml b/openapi/deployserver.yaml index 6bcb0d43..9f2d4fc7 100644 --- a/openapi/deployserver.yaml +++ b/openapi/deployserver.yaml @@ -2,7 +2,7 @@ openapi: 3.1.0 info: title: Terraform HCP Proxy API contact: {} - version: 0.1.0 + version: "0.1.0" servers: - url: https://deploy.formance.cloud description: Production server @@ -16,11 +16,6 @@ paths: summary: List organization apps operationId: listApps parameters: - - name: organizationId - in: query - required: true - schema: - type: string - name: pageNumber in: query schema: @@ -30,18 +25,18 @@ paths: schema: type: integer responses: - '200': + "200": description: Apps retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/ListAppsResponse' + $ref: "#/components/schemas/ListAppsResponse" default: description: Error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" post: summary: Create a new app operationId: createApp @@ -50,20 +45,20 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/CreateAppRequest' + $ref: "#/components/schemas/CreateAppRequest" responses: - '201': + "201": description: App created successfully content: application/json: schema: - $ref: '#/components/schemas/AppResponse' + $ref: "#/components/schemas/AppResponse" default: description: Error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /apps/{id}: put: summary: Update an app @@ -79,16 +74,16 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/UpdateAppRequest' + $ref: "#/components/schemas/UpdateAppRequest" responses: - '204': + "204": description: App updated successfully default: description: Error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" get: summary: read app details operationId: readApp @@ -99,18 +94,18 @@ paths: schema: type: string responses: - '200': + "200": description: App details retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/AppResponse' + $ref: "#/components/schemas/AppResponse" default: description: Error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" delete: summary: Delete an app operationId: deleteApp @@ -121,14 +116,14 @@ paths: schema: type: string responses: - '204': + "204": description: App details retrieved successfully default: description: Error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /apps/{id}/current-state-version: get: summary: Get the current state version of an app @@ -140,18 +135,18 @@ paths: schema: type: string responses: - '200': + "200": description: Current state version retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/ReadStateResponse' + $ref: "#/components/schemas/ReadStateResponse" default: description: Error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /apps/{id}/variables: get: summary: Get all variables of an app @@ -171,18 +166,18 @@ paths: schema: type: integer responses: - '200': + "200": description: App variables retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/ReadVariablesResponse' + $ref: "#/components/schemas/ReadVariablesResponse" default: description: Error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" post: summary: Create variable for an app operationId: createAppVariable @@ -197,20 +192,20 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/CreateVariableRequest' + $ref: "#/components/schemas/CreateVariableRequest" responses: - '201': + "201": description: Variable created successfully content: application/json: schema: - $ref: '#/components/schemas/CreateVariableResponse' + $ref: "#/components/schemas/CreateVariableResponse" default: description: Error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /apps/{id}/variables/{variableId}: delete: summary: Delete a variable from an app @@ -227,14 +222,14 @@ paths: schema: type: string responses: - '204': + "204": description: Variable deleted successfully default: description: Error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /apps/{id}/runs: get: summary: Get runs of an app @@ -254,18 +249,18 @@ paths: schema: type: integer responses: - '200': + "200": description: App runs retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/ListRunsResponse' + $ref: "#/components/schemas/ListRunsResponse" default: description: Error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /apps/{id}/versions: get: summary: Get versions of an app @@ -285,22 +280,22 @@ paths: schema: type: integer responses: - '200': + "200": description: App versions retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/ListVersionsResponse' + $ref: "#/components/schemas/ListVersionsResponse" default: description: Error content: application/json: schema: - $ref: '#/components/schemas/Error' - /apps/{id}/deploy: + $ref: "#/components/schemas/Error" + /apps/{id}/manifest: post: - summary: Deploy a new configuration for an app - operationId: deployAppConfiguration + summary: Push a new manifest version + operationId: pushManifest parameters: - name: id in: path @@ -314,23 +309,46 @@ paths: schema: type: string format: binary - description: YAML manifest content + description: YAML manifest content with version field application/json: schema: - $ref: '#/components/schemas/application' + $ref: "#/components/schemas/Manifest" + responses: + "201": + description: Manifest pushed successfully + content: + application/json: + schema: + $ref: "#/components/schemas/ManifestVersionResponse" + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /apps/{id}/manifest/versions: + get: + summary: List manifest versions + operationId: listManifestVersions + parameters: + - name: id + in: path + required: true + schema: + type: string responses: - '202': - description: App configuration deployed successfully + "200": + description: Manifest versions retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/RunResponse' + $ref: "#/components/schemas/ListManifestsResponse" default: description: Error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /apps/{id}/run: get: summary: Get the current run of an app @@ -342,18 +360,18 @@ paths: schema: type: string responses: - '200': + "200": description: Current run retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/RunResponse' + $ref: "#/components/schemas/RunResponse" default: description: Error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /versions/{id}: get: summary: Get a specific version @@ -379,12 +397,12 @@ paths: - application/gzip - application/yaml responses: - '200': + "200": description: version retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/AppVersionResponse' + $ref: "#/components/schemas/AppVersionResponse" application/gzip: schema: type: string @@ -400,7 +418,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /runs/{id}: get: summary: Get the run of a version @@ -412,18 +430,18 @@ paths: schema: type: string responses: - '200': + "200": description: Run retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/RunResponse' + $ref: "#/components/schemas/RunResponse" default: description: Error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /runs/{id}/logs: get: summary: Get logs of a run by its ID @@ -435,18 +453,18 @@ paths: schema: type: string responses: - '200': + "200": description: Run logs retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/ReadLogsResponse' + $ref: "#/components/schemas/ReadLogsResponse" default: description: Error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /apps/{id}/run/logs: get: summary: Get logs of the current run of an app @@ -458,18 +476,18 @@ paths: schema: type: string responses: - '200': + "200": description: Current run logs retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/ReadLogsResponse' + $ref: "#/components/schemas/ReadLogsResponse" default: description: Error content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /apps/{id}/version: get: summary: Get the current version of an app @@ -502,12 +520,12 @@ paths: enum: - state responses: - '200': + "200": description: Current app version retrieved successfully content: application/json: schema: - $ref: '#/components/schemas/AppVersionResponse' + $ref: "#/components/schemas/AppVersionResponse" application/gzip: schema: type: string @@ -523,7 +541,143 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" + /apps/{id}/deployments: + get: + summary: List deployments for an app + operationId: listDeployments + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: Deployments retrieved successfully + content: + application/json: + schema: + $ref: "#/components/schemas/ListDeploymentsResponse" + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a deployment for an app + operationId: createDeployment + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateDeploymentRequest" + responses: + "201": + description: Deployment created successfully + content: + application/json: + schema: + $ref: "#/components/schemas/DeploymentResponse" + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /apps/{id}/deployments/{name}: + get: + summary: Read a deployment + operationId: readDeployment + parameters: + - name: id + in: path + required: true + schema: + type: string + - name: name + in: path + required: true + schema: + type: string + responses: + "200": + description: Deployment retrieved successfully + content: + application/json: + schema: + $ref: "#/components/schemas/DeploymentResponse" + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + delete: + summary: Delete a deployment + operationId: deleteDeployment + parameters: + - name: id + in: path + required: true + schema: + type: string + - name: name + in: path + required: true + schema: + type: string + responses: + "204": + description: Deployment deleted successfully + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /apps/{id}/deployments/{name}/deploy: + post: + summary: Deploy a manifest to a deployment + operationId: deployToDeployment + parameters: + - name: id + in: path + required: true + schema: + type: string + - name: name + in: path + required: true + schema: + type: string + - name: version + in: query + required: false + description: Manifest version to deploy. If omitted, deploys the latest version. + schema: + type: integer + responses: + "202": + description: Manifest deployed successfully + content: + application/json: + schema: + $ref: "#/components/schemas/RunResponse" + default: + description: Error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" components: schemas: ReadStateResponse: @@ -532,7 +686,7 @@ components: - data properties: data: - $ref: '#/components/schemas/State' + $ref: "#/components/schemas/State" State: type: object required: @@ -557,7 +711,7 @@ components: data: type: array items: - $ref: '#/components/schemas/Log' + $ref: "#/components/schemas/Log" Log: type: object required: @@ -609,14 +763,6 @@ components: totalCount: type: integer description: Total number of items - CreateAppRequest: - type: object - required: - - organizationId - properties: - organizationId: - type: string - description: ID of the organization to which the app belongs ListAppsResponse: type: object required: @@ -624,7 +770,7 @@ components: properties: data: allOf: - - $ref: '#/components/schemas/PaginationResponse' + - $ref: "#/components/schemas/PaginationResponse" - type: object required: - items @@ -632,14 +778,14 @@ components: items: type: array items: - $ref: '#/components/schemas/App' + $ref: "#/components/schemas/App" AppResponse: type: object required: - data properties: data: - $ref: '#/components/schemas/App' + $ref: "#/components/schemas/App" ListRunsResponse: type: object required: @@ -647,7 +793,7 @@ components: properties: data: allOf: - - $ref: '#/components/schemas/PaginationResponse' + - $ref: "#/components/schemas/PaginationResponse" - type: object required: - items @@ -655,7 +801,7 @@ components: items: type: array items: - $ref: '#/components/schemas/Run' + $ref: "#/components/schemas/Run" ListVersionsResponse: type: object required: @@ -663,7 +809,7 @@ components: properties: data: allOf: - - $ref: '#/components/schemas/PaginationResponse' + - $ref: "#/components/schemas/PaginationResponse" - type: object required: - items @@ -671,14 +817,14 @@ components: items: type: array items: - $ref: '#/components/schemas/ConfigurationVersion' + $ref: "#/components/schemas/ConfigurationVersion" AppVersionResponse: type: object required: - data properties: data: - $ref: '#/components/schemas/ConfigurationVersion' + $ref: "#/components/schemas/ConfigurationVersion" App: type: object required: @@ -692,16 +838,16 @@ components: type: string description: Name of the app currentConfigurationVersion: - $ref: '#/components/schemas/ConfigurationVersion' + $ref: "#/components/schemas/ConfigurationVersion" currentRun: - $ref: '#/components/schemas/Run' + $ref: "#/components/schemas/Run" RunResponse: type: object required: - data properties: data: - $ref: '#/components/schemas/Run' + $ref: "#/components/schemas/Run" Run: type: object required: @@ -752,7 +898,8 @@ components: type: string description: Status of the run configurationVersion: - $ref: '#/components/schemas/ConfigurationVersion' + $ref: "#/components/schemas/ConfigurationVersion" + ConfigurationVersion: type: object required: @@ -782,7 +929,15 @@ components: - pending - fetching - uploaded - - '' + - "" + CreateAppRequest: + type: object + required: + - name + properties: + name: + type: string + description: User-provided name for the app UpdateAppRequest: type: object ReadVariablesResponse: @@ -797,22 +952,22 @@ components: items: type: array items: - $ref: '#/components/schemas/Variable' - - $ref: '#/components/schemas/PaginationResponse' + $ref: "#/components/schemas/Variable" + - $ref: "#/components/schemas/PaginationResponse" CreateVariableRequest: type: object required: - variable properties: variable: - $ref: '#/components/schemas/VariableData' + $ref: "#/components/schemas/VariableData" CreateVariableResponse: type: object required: - data properties: data: - $ref: '#/components/schemas/Variable' + $ref: "#/components/schemas/Variable" VariableData: type: object required: @@ -834,7 +989,7 @@ components: description: Whether the variable is sensitive Variable: allOf: - - $ref: '#/components/schemas/VariableData' + - $ref: "#/components/schemas/VariableData" - type: object required: - id @@ -851,388 +1006,114 @@ components: type: string required: - errorCode - V2ChartAccountMetadata: - properties: - default: - type: - - 'null' - - string - type: object - V2ChartAccountRules: - type: object - DotSelf: - type: object - V2ChartSegment: - additionalProperties: true - properties: - .metadata: - additionalProperties: - $ref: '#/components/schemas/V2ChartAccountMetadata' - type: object - .pattern: - type: - - 'null' - - string - .rules: - $ref: '#/components/schemas/V2ChartAccountRules' - .self: - $ref: '#/components/schemas/DotSelf' + Deployment: type: object - V2TransactionTemplate: - properties: - description: - type: - - 'null' - - string - runtime: - type: - - 'null' - - string - script: - type: - - 'null' - - string - type: object - V2SchemaData: - properties: - chart: - additionalProperties: - $ref: '#/components/schemas/V2ChartSegment' - type: - - object - - 'null' - transactions: - additionalProperties: - $ref: '#/components/schemas/V2TransactionTemplate' - type: object - type: object - Ledger: required: - name + - appId + - stackId + - workspaceId properties: name: type: string - schema: - additionalProperties: - $ref: '#/components/schemas/V2SchemaData' - type: - - object - - 'null' + description: Name of the deployment + appId: + type: string + description: ID of the parent app + stackId: + type: string + description: ID of the Formance Cloud stack + workspaceId: + type: string + description: ID of the TFC workspace + DeploymentResponse: type: object - Pool: + required: + - data properties: - accountIds: - items: - type: string - type: array - query: - additionalProperties: {} - type: object + data: + $ref: "#/components/schemas/Deployment" + ListDeploymentsResponse: type: object - ReconciliationLedger: required: - - name - - query + - data properties: - name: - type: string - query: - additionalProperties: {} - type: - - object - - 'null' + data: + type: array + items: + $ref: "#/components/schemas/Deployment" + CreateDeploymentRequest: type: object - ReconciliationPolicy: required: - name - - ledger - - pool + - stackId properties: - ledger: - $ref: '#/components/schemas/ReconciliationLedger' name: type: string - pool: + description: Name for the deployment + stackId: type: string + description: ID of the Formance Cloud stack to deploy to + Manifest: type: object - RegionSelector: - properties: - id: - type: string - name: - type: string - type: object - Webhook: + description: Application manifest with version field required: - - name - - endpoint + - version properties: - endpoint: - type: string - events: - items: - type: string + version: + type: integer + description: Manifest version number (monotonically increasing) + ledgers: type: array - name: - type: string - secret: - type: string - type: object - Ledgers: - items: - $ref: '#/components/schemas/Ledger' - type: - - 'null' - - array - Payments: - properties: - connectors: items: - required: - - name - - provider - properties: - configuration: - additionalProperties: {} - type: object - credentials: - additionalProperties: - type: string - type: object - name: - type: string - provider: - type: string type: object - type: array - pools: - additionalProperties: - $ref: '#/components/schemas/Pool' + additionalProperties: true + payments: type: object - type: object - Reconciliation: - properties: - policies: - items: - $ref: '#/components/schemas/ReconciliationPolicy' + additionalProperties: true + reconciliation: + type: object + additionalProperties: true + webhooks: type: array + items: + type: object + additionalProperties: true + ManifestVersion: type: object - Stack: required: - - name - - region + - appId + - version + - content + - createdAt properties: - name: + appId: type: string - region: - $ref: '#/components/schemas/RegionSelector' + description: ID of the parent app version: + type: integer + description: Manifest version number + content: type: string + format: binary + description: Raw manifest content + createdAt: + type: string + format: date-time + description: When this version was pushed + ManifestVersionResponse: type: object - Webhooks: - items: - $ref: '#/components/schemas/Webhook' - type: - - 'null' - - array - application: - $schema: http://json-schema.org/draft-04/schema required: - - stack - definitions: - DotSelf: - type: object - Ledger: - required: - - name - properties: - name: - type: string - schema: - additionalProperties: - $ref: '#/components/schemas/V2SchemaData' - type: - - object - - 'null' - type: object - Ledgers: - items: - $ref: '#/components/schemas/Ledger' - type: - - 'null' - - array - Payments: - properties: - connectors: - items: - required: - - name - - provider - properties: - configuration: - additionalProperties: {} - type: object - credentials: - additionalProperties: - type: string - type: object - name: - type: string - provider: - type: string - type: object - type: array - pools: - additionalProperties: - $ref: '#/components/schemas/Pool' - type: object - type: object - Pool: - properties: - accountIds: - items: - type: string - type: array - query: - additionalProperties: {} - type: object - type: object - Reconciliation: - properties: - policies: - items: - $ref: '#/components/schemas/ReconciliationPolicy' - type: array - type: object - ReconciliationLedger: - required: - - name - - query - properties: - name: - type: string - query: - additionalProperties: {} - type: - - object - - 'null' - type: object - ReconciliationPolicy: - required: - - name - - ledger - - pool - properties: - ledger: - $ref: '#/components/schemas/ReconciliationLedger' - name: - type: string - pool: - type: string - type: object - RegionSelector: - properties: - id: - type: string - name: - type: string - type: object - Stack: - required: - - name - - region - properties: - name: - type: string - region: - $ref: '#/components/schemas/RegionSelector' - version: - type: string - type: object - V2ChartAccountMetadata: - properties: - default: - type: - - 'null' - - string - type: object - V2ChartAccountRules: - type: object - V2ChartSegment: - additionalProperties: true - properties: - .metadata: - additionalProperties: - $ref: '#/components/schemas/V2ChartAccountMetadata' - type: object - .pattern: - type: - - 'null' - - string - .rules: - $ref: '#/components/schemas/V2ChartAccountRules' - .self: - $ref: '#/components/schemas/DotSelf' - type: object - V2SchemaData: - properties: - chart: - additionalProperties: - $ref: '#/components/schemas/V2ChartSegment' - type: - - object - - 'null' - transactions: - additionalProperties: - $ref: '#/components/schemas/V2TransactionTemplate' - type: object - type: object - V2TransactionTemplate: - properties: - description: - type: - - 'null' - - string - runtime: - type: - - 'null' - - string - script: - type: - - 'null' - - string - type: object - Webhook: - required: - - name - - endpoint - properties: - endpoint: - type: string - events: - items: - type: string - type: array - name: - type: string - secret: - type: string - type: object - Webhooks: - items: - $ref: '#/components/schemas/Webhook' - type: - - 'null' - - array + - data properties: - ledgers: - $ref: '#/components/schemas/Ledgers' - payments: - $ref: '#/components/schemas/Payments' - reconciliation: - $ref: '#/components/schemas/Reconciliation' - stack: - $ref: '#/components/schemas/Stack' - webhooks: - $ref: '#/components/schemas/Webhooks' + data: + $ref: "#/components/schemas/ManifestVersion" + ListManifestsResponse: type: object + required: + - data + properties: + data: + type: array + items: + $ref: "#/components/schemas/ManifestVersion"