Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions cmd/cloud/apps/bind_manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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 BindManifest struct {
AppID string `json:"appId"`
ManifestID string `json:"manifestId"`
}

type BindManifestCtrl struct {
store *BindManifest
}

var _ fctl.Controller[*BindManifest] = (*BindManifestCtrl)(nil)

func NewBindManifestCtrl() *BindManifestCtrl {
return &BindManifestCtrl{store: &BindManifest{}}
}

func NewBindManifest() *cobra.Command {
return fctl.NewCommand("bind-manifest",
fctl.WithShortDescription("Bind a manifest to an app"),
fctl.WithStringFlag("app-id", "", "App ID"),
fctl.WithStringFlag("manifest-id", "", "Manifest ID to bind"),
fctl.WithController(NewBindManifestCtrl()),
)
}

func (c *BindManifestCtrl) GetStore() *BindManifest { return c.store }

func (c *BindManifestCtrl) 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
}

appID := fctl.GetString(cmd, "app-id")
if appID == "" {
return nil, fmt.Errorf("app-id is required")
}
manifestID := fctl.GetString(cmd, "manifest-id")
if manifestID == "" {
return nil, fmt.Errorf("manifest-id is required")
}

if _, err := apiClient.AttachAppManifest(cmd.Context(), appID, components.AttachManifestRequest{
ManifestID: manifestID,
}); err != nil {
return nil, err
}

c.store.AppID = appID
c.store.ManifestID = manifestID
return c, nil
}

func (c *BindManifestCtrl) Render(_ *cobra.Command, _ []string) error {
pterm.Success.Printfln("Manifest %s bound to app %s", c.store.ManifestID, c.store.AppID)
return nil
}
49 changes: 49 additions & 0 deletions cmd/cloud/apps/bind_manifest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package apps

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"

fctl "github.com/formancehq/fctl/v3/pkg"
)

func TestBindManifest_JSONOutput_LowercaseKeys(t *testing.T) {
store := &BindManifest{
AppID: "app-abc",
ManifestID: "mnf-xyz",
}

out, err := json.Marshal(fctl.ExportedData{Data: store})
require.NoError(t, err)

var raw map[string]any
require.NoError(t, json.Unmarshal(out, &raw))

data, ok := raw["data"].(map[string]any)
require.True(t, ok)
require.Equal(t, "app-abc", data["appId"])
require.Equal(t, "mnf-xyz", data["manifestId"])

for _, capitalized := range []string{"AppID", "ManifestID"} {
_, present := data[capitalized]
require.False(t, present, "must not surface Go-cased key %q", capitalized)
}
}

func TestUnbindManifest_JSONOutput_LowercaseKeys(t *testing.T) {
store := &UnbindManifest{AppID: "app-abc"}

out, err := json.Marshal(fctl.ExportedData{Data: store})
require.NoError(t, err)

var raw map[string]any
require.NoError(t, json.Unmarshal(out, &raw))

data, ok := raw["data"].(map[string]any)
require.True(t, ok)
require.Equal(t, "app-abc", data["appId"])
_, capitalized := data["AppID"]
require.False(t, capitalized)
}
23 changes: 19 additions & 4 deletions cmd/cloud/apps/create.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package apps

import (
"fmt"

"github.com/pterm/pterm"
"github.com/spf13/cobra"

Expand Down Expand Up @@ -34,6 +36,8 @@ func NewCreateCtrl() *CreateCtrl {
func NewCreate() *cobra.Command {
return fctl.NewCommand("create",
fctl.WithShortDescription("Create an app"),
fctl.WithStringFlag("name", "", "App name"),
fctl.WithStringFlag("stack-id", "", "Optional existing stack ID to claim"),
fctl.WithController(NewCreateCtrl()),
)
}
Expand All @@ -49,7 +53,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(),
Expand All @@ -59,9 +63,20 @@ func (c *CreateCtrl) Run(cmd *cobra.Command, _ []string) (fctl.Renderable, error
if err != nil {
return nil, err
}
apps, err := apiClient.CreateApp(cmd.Context(), components.CreateAppRequest{
OrganizationID: organizationID,
})

name := fctl.GetString(cmd, "name")
if name == "" {
return nil, fmt.Errorf("name is required")
}

req := components.CreateAppRequest{
Name: name,
}
if stackID := fctl.GetString(cmd, "stack-id"); stackID != "" {
req.StackID = &stackID
}

apps, err := apiClient.CreateApp(cmd.Context(), req)
if err != nil {
return nil, err
}
Expand Down
28 changes: 24 additions & 4 deletions cmd/cloud/apps/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import (
)

type Delete struct {
ID string
ID string `json:"id"`
DestroyDeploymentID *string `json:"destroyDeploymentId,omitempty"`
Waited bool `json:"waited"`
}

type DeleteCtrl struct {
Expand All @@ -31,8 +33,9 @@ func NewDeleteCtrl() *DeleteCtrl {

func NewDelete() *cobra.Command {
return fctl.NewCommand("delete",
fctl.WithShortDescription("Delete an app"),
fctl.WithShortDescription("Soft-delete an app and enqueue a destroy deployment"),
fctl.WithStringFlag("id", "", "App ID"),
fctl.WithBoolFlag("wait", false, "Block until the destroy deployment reaches a terminal status"),
fctl.WithController(NewDeleteCtrl()),
)
}
Expand Down Expand Up @@ -61,17 +64,34 @@ func (c *DeleteCtrl) Run(cmd *cobra.Command, args []string) (fctl.Renderable, er
if id == "" {
return nil, fmt.Errorf("id is required")
}
_, err = apiClient.DeleteApp(cmd.Context(), id)

wait := fctl.GetBool(cmd, "wait")
resp, err := apiClient.DeleteApp(cmd.Context(), id, &wait)
if err != nil {
return nil, err
}

c.store.ID = id
c.store.Waited = wait
if resp.DeleteAppResponse != nil {
c.store.DestroyDeploymentID = resp.DeleteAppResponse.DestroyDeploymentID
}

return c, nil
}

func (c *DeleteCtrl) Render(cmd *cobra.Command, args []string) error {
pterm.Success.Println("App deleted", c.store.ID)
switch {
case c.store.DestroyDeploymentID != nil && *c.store.DestroyDeploymentID != "":
if c.store.Waited {
pterm.Success.Printfln("App %s deleted (destroy deployment %s reached terminal status)",
c.store.ID, *c.store.DestroyDeploymentID)
} else {
pterm.Success.Printfln("App %s soft-deleted; destroy deployment %s enqueued (poll with `fctl cloud app deployments show --id %s`)",
c.store.ID, *c.store.DestroyDeploymentID, *c.store.DestroyDeploymentID)
}
default:
pterm.Success.Printfln("App %s deleted (no destroy deployment was needed)", c.store.ID)
}
return nil
}
Loading
Loading