Skip to content
Draft
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
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
signupcmd "github.com/launchdarkly/ldcli/cmd/signup"
sourcemapscmd "github.com/launchdarkly/ldcli/cmd/sourcemaps"
symbolscmd "github.com/launchdarkly/ldcli/cmd/symbols"
synccmd "github.com/launchdarkly/ldcli/cmd/sync"
whoamicmd "github.com/launchdarkly/ldcli/cmd/whoami"
"github.com/launchdarkly/ldcli/internal/analytics"
"github.com/launchdarkly/ldcli/internal/config"
Expand Down Expand Up @@ -299,6 +300,7 @@ func NewRootCommand(
cmd.AddCommand(devcmd.NewDevServerCmd(clients.ResourcesClient, analyticsTrackerFn, clients.DevClient))
cmd.AddCommand(sourcemapscmd.NewSourcemapsCmd(clients.ResourcesClient, analyticsTrackerFn))
cmd.AddCommand(symbolscmd.NewSymbolsCmd(clients.ResourcesClient, analyticsTrackerFn))
cmd.AddCommand(synccmd.NewSyncCmd(clients.ResourcesClient, analyticsTrackerFn))
cmd.AddCommand(whoamicmd.NewWhoAmICmd(clients.ResourcesClient))
resourcecmd.AddAllResourceCmds(cmd, clients.ResourcesClient, analyticsTrackerFn)

Expand Down
1 change: 1 addition & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ func TestNewRootCommand_RegistersTopLevelCommands(t *testing.T) {
"signup",
"sourcemaps",
"symbols",
"sync",
"whoami",
} {
assert.True(t, registered[name], "%s is not registered on the root command", name)
Expand Down
110 changes: 110 additions & 0 deletions cmd/sync/output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package sync

import (
"encoding/json"
"fmt"
"io"

"github.com/launchdarkly/ldcli/internal/output"
syncapi "github.com/launchdarkly/ldcli/internal/sync/api"
)

type planOutputResource struct {
ProjectKey string `json:"projectKey"`
ResourceKind string `json:"resourceKind"`
LookupKey string `json:"lookupKey"`
Status syncapi.ResourceStatus `json:"status"`
SyncDirection syncapi.SyncDirection `json:"syncDirection"`
Diff json.RawMessage `json:"diff,omitempty"`
Error *syncapi.ResourceError `json:"error,omitempty"`
}

type planOutputEnvelope struct {
Items []planOutputItem `json:"items"`
}

type planOutputItem struct {
Key string `json:"key"`
Name string `json:"name"`
}

func writePlanOutput(
out io.Writer,
outputKind string,
plans []syncapi.ProjectPlan,
) error {
resources := flattenPlanResources(plans)

var outputValue any = planOutputEnvelope{Items: planOutputItems(resources)}
if outputKind == "json" {
outputValue = resources
}

data, err := json.Marshal(outputValue)
if err != nil {
return fmt.Errorf("marshal plan output: %w", err)
}

formatted, err := output.CmdOutput("list", outputKind, data)
if err != nil {
return err
}
if formatted == "" {
return nil
}

if _, err := fmt.Fprintln(out, formatted); err != nil {
return fmt.Errorf("write plan output: %w", err)
}

return nil
}

func flattenPlanResources(plans []syncapi.ProjectPlan) []planOutputResource {
resources := make([]planOutputResource, 0)

for _, plan := range plans {
for _, resource := range plan.Resources {
resources = append(resources, planOutputResource{
ProjectKey: plan.ProjectKey,
ResourceKind: string(resource.ResourceKind),
LookupKey: resource.LookupKey,
Status: resource.Status,
SyncDirection: resource.SyncDirection,
Diff: resource.Diff,
Error: resource.Error,
})
}
}

return resources
}

func planOutputItems(resources []planOutputResource) []planOutputItem {
items := make([]planOutputItem, 0, len(resources))

for _, resource := range resources {
details := fmt.Sprintf(
"status=%s direction=%s",
resource.Status,
resource.SyncDirection,
)
if len(resource.Diff) > 0 {
details += " diff=" + string(resource.Diff)
}
if resource.Error != nil {
details += fmt.Sprintf(
" error=%s: %s",
resource.Error.Code,
resource.Error.Message,
)
}

items = append(items, planOutputItem{
Key: resource.ProjectKey + "/" + resource.LookupKey,
Name: details,
})
}

return items
}
75 changes: 75 additions & 0 deletions cmd/sync/prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package sync

import (
"fmt"
"os"

"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/launchdarkly/ldcli/cmd/cliflags"
resourcescmd "github.com/launchdarkly/ldcli/cmd/resources"
"github.com/launchdarkly/ldcli/cmd/validators"
"github.com/launchdarkly/ldcli/internal/config"
"github.com/launchdarkly/ldcli/internal/output"
"github.com/launchdarkly/ldcli/internal/resources"
syncapi "github.com/launchdarkly/ldcli/internal/sync/api"
synclocal "github.com/launchdarkly/ldcli/internal/sync/local"
syncsource "github.com/launchdarkly/ldcli/internal/sync/source"
)

func NewPromptCmd(client resources.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "prompt",
Short: "Preview synchronization changes for local prompts",
Long: "Read local prompt variations and preview the changes LaunchDarkly would make without creating or applying a plan.",
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.NoArgs(cmd, args); err != nil {
return err
}

return validators.Validate()(cmd, args)
},
RunE: runPrompt(client),
}

cmd.SetUsageTemplate(resourcescmd.SubcommandUsageTemplate())

return cmd
}

func runPrompt(client resources.Client) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, _ []string) error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("get working directory: %w", err)
}

workspace, err := syncsource.NewResolver(config.GetConfigFile()).Resolve(cwd)
if err != nil {
return err
}

localResources, err := synclocal.Compile(os.DirFS(workspace.Root))
if err != nil {
return err
}

plans, err := syncapi.NewClient(client).Plan(
viper.GetString(cliflags.AccessTokenFlag),
viper.GetString(cliflags.BaseURIFlag),
workspace.Source,
true,
localResources,
)
if err != nil {
return output.NewCmdOutputError(err, cliflags.GetOutputKind(cmd))
}

return writePlanOutput(
cmd.OutOrStdout(),
cliflags.GetOutputKind(cmd),
plans,
)
}
}
Loading
Loading