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
134 changes: 134 additions & 0 deletions cmd/sync/output.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package sync

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

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

type planOutputResource struct {
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 projectPlanOutput struct {
ProjectKey string `json:"projectKey"`
PlanID string `json:"planId,omitempty"`
ExpiresAt string `json:"expiresAt,omitempty"`
Resources []planOutputResource `json:"resources"`
}

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 {
outputPlans := newProjectPlanOutputs(plans)

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

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 newProjectPlanOutputs(plans []syncapi.ProjectPlan) []projectPlanOutput {
outputPlans := make([]projectPlanOutput, 0, len(plans))
for _, plan := range plans {
outputPlan := projectPlanOutput{
ProjectKey: plan.ProjectKey,
PlanID: plan.PlanID,
ExpiresAt: plan.ExpiresAt,
Resources: make([]planOutputResource, 0, len(plan.Resources)),
}
for _, resource := range plan.Resources {
outputPlan.Resources = append(outputPlan.Resources, planOutputResource{
ResourceKind: string(resource.ResourceKind),
LookupKey: resource.LookupKey,
Status: resource.Status,
SyncDirection: resource.SyncDirection,
Diff: resource.Diff,
Error: resource.Error,
})
}
outputPlans = append(outputPlans, outputPlan)
}

return outputPlans
}

func planOutputItems(plans []projectPlanOutput) []planOutputItem {
var items []planOutputItem

for _, plan := range plans {
if plan.PlanID != "" {
items = append(items, planOutputItem{
Key: plan.ProjectKey,
Name: fmt.Sprintf(
"planId=%s expiresAt=%s",
plan.PlanID,
plan.ExpiresAt,
),
})
}

for _, resource := range plan.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: plan.ProjectKey + "/" + resource.LookupKey,
Name: details,
})
}
}

return items
}
83 changes: 83 additions & 0 deletions cmd/sync/prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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"
)

const dryRunFlag = "dry-run"

func NewPromptCmd(client resources.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "prompt",
Short: "Synchronize local prompt variations with LaunchDarkly",
Long: "Plan synchronization changes for local prompt variations. Use --dry-run to preview changes without creating 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.Flags().Bool(
dryRunFlag,
false,
"Preview synchronization changes without creating a plan",
)
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
}

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

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