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
7 changes: 6 additions & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ type BackupProfile = engine.BackupProfile
type DiscoveredSource = engine.DiscoveredSource
type WorkstationSetupPlan = engine.WorkstationSetupPlan
type WorkstationSetupOption = engine.WorkstationSetupOption
type WorkstationApplyResult = engine.WorkstationApplyResult
type WorkstationProfileDraft = engine.WorkstationProfileDraft
type WorkstationFolderCandidate = engine.WorkstationFolderCandidate
type WorkstationCoverageSummary = engine.WorkstationCoverageSummary
Expand Down Expand Up @@ -349,10 +350,14 @@ func (c *Client) DiscoverSources(ctx context.Context) ([]DiscoveredSource, error
return engine.DiscoverSources(ctx)
}

func (c *Client) PlanWorkstationSetup(ctx context.Context, opts ...WorkstationSetupOption) (*WorkstationSetupPlan, error) {
func PlanWorkstationSetup(ctx context.Context, opts ...WorkstationSetupOption) (*WorkstationSetupPlan, error) {
return engine.PlanWorkstationSetup(ctx, opts...)
}

func ApplyWorkstationSetupPlan(cfg *ProfilesConfig, plan *WorkstationSetupPlan) (*WorkstationApplyResult, error) {
return engine.ApplyWorkstationSetupPlan(cfg, plan)
}

// LoadProfilesFile parses a backup profiles YAML file.
func LoadProfilesFile(path string) (*ProfilesConfig, error) {
return engine.LoadProfilesFile(path)
Expand Down
18 changes: 16 additions & 2 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,12 +224,26 @@ func TestClientDiscoverSources(t *testing.T) {
}

func TestClientPlanWorkstationSetup(t *testing.T) {
c := &Client{}
if _, err := c.PlanWorkstationSetup(context.Background()); err != nil {
if _, err := PlanWorkstationSetup(context.Background()); err != nil {
t.Fatalf("PlanWorkstationSetup: %v", err)
}
}

func TestApplyWorkstationSetupPlan(t *testing.T) {
cfg := &ProfilesConfig{}
result, err := ApplyWorkstationSetupPlan(cfg, &WorkstationSetupPlan{
Profiles: []WorkstationProfileDraft{
{Name: "documents", SourceURI: "local:/Users/test/Documents", StoreRef: "primary", Tags: []string{"workstation"}, Selected: true},
},
})
if err != nil {
t.Fatalf("ApplyWorkstationSetupPlan: %v", err)
}
if result.ProfilesCreated != 1 || cfg.Profiles["documents"].Store != "primary" {
t.Fatalf("unexpected apply result: %#v %#v", result, cfg)
}
}

// ---------------------------------------------------------------------------
// Cat
// ---------------------------------------------------------------------------
Expand Down
1 change: 0 additions & 1 deletion cmd/cloudstic/client_iface.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
type cloudsticClient interface {
Backup(ctx context.Context, src source.Source, opts ...cloudstic.BackupOption) (*cloudstic.BackupResult, error)
DiscoverSources(ctx context.Context) ([]cloudstic.DiscoveredSource, error)
PlanWorkstationSetup(ctx context.Context, opts ...cloudstic.WorkstationSetupOption) (*cloudstic.WorkstationSetupPlan, error)
Restore(ctx context.Context, w io.Writer, snapshotRef string, opts ...cloudstic.RestoreOption) (*cloudstic.RestoreResult, error)
RestoreToDir(ctx context.Context, outputDir, snapshotRef string, opts ...cloudstic.RestoreOption) (*cloudstic.RestoreResult, error)
List(ctx context.Context, opts ...cloudstic.ListOption) (*cloudstic.ListResult, error)
Expand Down
9 changes: 4 additions & 5 deletions cmd/cloudstic/cmd_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"github.com/jedib0t/go-pretty/v6/table"
)

var planWorkstationSetup = cloudstic.PlanWorkstationSetup

func defaultProfilesPathNoCreate() string {
if path := os.Getenv("CLOUDSTIC_PROFILES_FILE"); path != "" {
return path
Expand Down Expand Up @@ -75,9 +77,6 @@ func (r *runner) runSetupWorkstation(ctx context.Context) int {
return r.fail("Failed to load profiles: %v", err)
}
ensureProfilesMaps(cfg)
if r.client == nil {
r.client = &cloudstic.Client{}
}

if !args.dryRun && args.storeRef == "" {
if len(cfg.Stores) == 0 {
Expand Down Expand Up @@ -118,7 +117,7 @@ func (r *runner) runSetupWorkstation(ctx context.Context) int {
if args.storeRef != "" {
opts = append(opts, cloudstic.WithWorkstationStoreRef(args.storeRef))
}
plan, err := r.client.PlanWorkstationSetup(ctx, opts...)
plan, err := planWorkstationSetup(ctx, opts...)
if err != nil {
return r.fail("Failed to plan workstation setup: %v", err)
}
Expand Down Expand Up @@ -169,7 +168,7 @@ func (r *runner) runSetupWorkstation(ctx context.Context) int {
}
}

result, err := engine.ApplyWorkstationSetupPlan(cfg, (*engine.WorkstationSetupPlan)(plan))
result, err := cloudstic.ApplyWorkstationSetupPlan(cfg, plan)
if err != nil {
return r.fail("Failed to apply workstation setup plan: %v", err)
}
Expand Down
65 changes: 35 additions & 30 deletions cmd/cloudstic/cmd_setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,37 @@ import (
"github.com/cloudstic/cli/internal/engine"
)

func stubSetupWorkstationPlan(t *testing.T, plan *cloudstic.WorkstationSetupPlan, err error) {
t.Helper()
old := planWorkstationSetup
planWorkstationSetup = func(context.Context, ...cloudstic.WorkstationSetupOption) (*cloudstic.WorkstationSetupPlan, error) {
return plan, err
}
t.Cleanup(func() { planWorkstationSetup = old })
}

func TestRunSetupWorkstation_DryRun(t *testing.T) {
client := &stubClient{
setupPlan: &cloudstic.WorkstationSetupPlan{
Hostname: "testbox",
StoreRef: "primary",
StoreAction: "use-existing",
Profiles: []cloudstic.WorkstationProfileDraft{
{Name: "documents", SourceURI: "local:/Users/test/Documents", StoreRef: "primary", Tags: []string{"workstation"}, Action: "create", Selected: true},
},
Coverage: cloudstic.WorkstationCoverageSummary{
ProtectedNow: []string{"Documents (/Users/test/Documents)"},
SkippedIntentionally: []string{"Downloads (/Users/test/Downloads)"},
},
t.Setenv("CLOUDSTIC_CONFIG_DIR", t.TempDir())
stubSetupWorkstationPlan(t, &cloudstic.WorkstationSetupPlan{
Hostname: "testbox",
StoreRef: "primary",
StoreAction: "use-existing",
Profiles: []cloudstic.WorkstationProfileDraft{
{Name: "documents", SourceURI: "local:/Users/test/Documents", StoreRef: "primary", Tags: []string{"workstation"}, Action: "create", Selected: true},
},
}
Coverage: cloudstic.WorkstationCoverageSummary{
ProtectedNow: []string{"Documents (/Users/test/Documents)"},
SkippedIntentionally: []string{"Downloads (/Users/test/Downloads)"},
},
}, nil)

osArgs := os.Args
t.Cleanup(func() { os.Args = osArgs })
os.Args = []string{"cloudstic", "setup", "workstation", "-dry-run"}

var out strings.Builder
var errOut strings.Builder
r := &runner{out: &out, errOut: &errOut, client: client}
r := &runner{out: &out, errOut: &errOut, client: &stubClient{}}
if code := r.runSetup(context.Background()); code != 0 {
t.Fatalf("code=%d err=%s", code, errOut.String())
}
Expand All @@ -44,19 +52,18 @@ func TestRunSetupWorkstation_DryRun(t *testing.T) {
}

func TestRunSetupWorkstation_JSON(t *testing.T) {
client := &stubClient{
setupPlan: &cloudstic.WorkstationSetupPlan{
Hostname: "testbox",
},
}
t.Setenv("CLOUDSTIC_CONFIG_DIR", t.TempDir())
stubSetupWorkstationPlan(t, &cloudstic.WorkstationSetupPlan{
Hostname: "testbox",
}, nil)

osArgs := os.Args
t.Cleanup(func() { os.Args = osArgs })
os.Args = []string{"cloudstic", "setup", "workstation", "-dry-run", "-json"}

var out strings.Builder
var errOut strings.Builder
r := &runner{out: &out, errOut: &errOut, client: client}
r := &runner{out: &out, errOut: &errOut, client: &stubClient{}}
if code := r.runSetup(context.Background()); code != 0 {
t.Fatalf("code=%d err=%s", code, errOut.String())
}
Expand All @@ -66,16 +73,14 @@ func TestRunSetupWorkstation_JSON(t *testing.T) {
}

func TestRunSetupWorkstation_ApplyYes(t *testing.T) {
client := &stubClient{
setupPlan: &cloudstic.WorkstationSetupPlan{
Hostname: "testbox",
StoreRef: "primary",
StoreAction: "use-existing",
Profiles: []cloudstic.WorkstationProfileDraft{
{Name: "documents", SourceURI: "local:/Users/test/Documents", StoreRef: "primary", Tags: []string{"workstation"}, Action: "create", Selected: true},
},
stubSetupWorkstationPlan(t, &cloudstic.WorkstationSetupPlan{
Hostname: "testbox",
StoreRef: "primary",
StoreAction: "use-existing",
Profiles: []cloudstic.WorkstationProfileDraft{
{Name: "documents", SourceURI: "local:/Users/test/Documents", StoreRef: "primary", Tags: []string{"workstation"}, Action: "create", Selected: true},
},
}
}, nil)
profilesPath := filepath.Join(t.TempDir(), "profiles.yaml")
if err := cloudstic.SaveProfilesFile(profilesPath, &cloudstic.ProfilesConfig{
Version: 1,
Expand All @@ -90,7 +95,7 @@ func TestRunSetupWorkstation_ApplyYes(t *testing.T) {

var out strings.Builder
var errOut strings.Builder
r := &runner{out: &out, errOut: &errOut, client: client, noPrompt: true}
r := &runner{out: &out, errOut: &errOut, client: &stubClient{}, noPrompt: true}
if code := r.runSetup(context.Background()); code != 0 {
t.Fatalf("code=%d err=%s", code, errOut.String())
}
Expand Down
34 changes: 34 additions & 0 deletions internal/engine/workstation_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type WorkstationSetupOption func(*workstationSetupOptions)
type workstationSetupOptions struct {
profiles *ProfilesConfig
storeRef string
dryRun bool
}

func WithWorkstationProfiles(cfg *ProfilesConfig) WorkstationSetupOption {
Expand All @@ -37,6 +38,10 @@ func WithWorkstationStoreRef(name string) WorkstationSetupOption {
return func(o *workstationSetupOptions) { o.storeRef = strings.TrimSpace(name) }
}

func WithWorkstationDryRun() WorkstationSetupOption {
return func(o *workstationSetupOptions) { o.dryRun = true }
}

type WorkstationFolderCandidate struct {
Key string `json:"key"`
Label string `json:"label"`
Expand Down Expand Up @@ -68,6 +73,11 @@ type WorkstationApplyResult struct {
ProfileNames []string `json:"profile_names,omitempty"`
}

type WorkstationSetupResult struct {
Plan *WorkstationSetupPlan `json:"plan,omitempty"`
Applied *WorkstationApplyResult `json:"applied,omitempty"`
}

type WorkstationSetupPlan struct {
Hostname string `json:"hostname"`
StoreRef string `json:"store_ref,omitempty"`
Expand Down Expand Up @@ -169,6 +179,30 @@ func PlanWorkstationSetup(ctx context.Context, opts ...WorkstationSetupOption) (
return plan, nil
}

func SetupWorkstation(ctx context.Context, opts ...WorkstationSetupOption) (*WorkstationSetupResult, error) {
options := workstationSetupOptions{}
for _, opt := range opts {
opt(&options)
}

plan, err := PlanWorkstationSetup(ctx, opts...)
if err != nil {
return nil, err
}

result := &WorkstationSetupResult{Plan: plan}
if options.dryRun {
return result, nil
}

applied, err := ApplyWorkstationSetupPlan(options.profiles, plan)
if err != nil {
return nil, err
}
result.Applied = applied
return result, nil
}

func ApplyWorkstationSetupPlan(cfg *ProfilesConfig, plan *WorkstationSetupPlan) (*WorkstationApplyResult, error) {
if plan == nil {
return nil, fmt.Errorf("workstation setup plan is required")
Expand Down
48 changes: 48 additions & 0 deletions internal/engine/workstation_setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,54 @@ func TestApplyWorkstationSetupPlan_Errors(t *testing.T) {
}
}

func TestSetupWorkstation_DryRun(t *testing.T) {
reset := stubWorkstationSetupEnv(t)
defer reset()

workstationHostnameFunc = func() (string, error) { return "host", nil }
workstationUserHomeDirFunc = func() (string, error) { return "/home/test", nil }
workstationPathExistsFunc = func(path string) bool { return path == "/home/test/Documents" }
workstationDiscoverSourcesFunc = func(context.Context) ([]DiscoveredSource, error) { return nil, nil }

cfg := &ProfilesConfig{}
result, err := SetupWorkstation(context.Background(), WithWorkstationProfiles(cfg), WithWorkstationDryRun())
if err != nil {
t.Fatalf("SetupWorkstation: %v", err)
}
if result.Plan == nil || result.Applied != nil {
t.Fatalf("unexpected setup result: %#v", result)
}
if len(cfg.Profiles) != 0 {
t.Fatalf("dry-run should not mutate profiles: %#v", cfg.Profiles)
}
}

func TestSetupWorkstation_Apply(t *testing.T) {
reset := stubWorkstationSetupEnv(t)
defer reset()

workstationHostnameFunc = func() (string, error) { return "host", nil }
workstationUserHomeDirFunc = func() (string, error) { return "/home/test", nil }
workstationPathExistsFunc = func(path string) bool { return path == "/home/test/Documents" }
workstationDiscoverSourcesFunc = func(context.Context) ([]DiscoveredSource, error) { return nil, nil }

cfg := &ProfilesConfig{
Stores: map[string]ProfileStore{
"primary": {URI: "local:/repo"},
},
}
result, err := SetupWorkstation(context.Background(), WithWorkstationProfiles(cfg), WithWorkstationStoreRef("primary"))
if err != nil {
t.Fatalf("SetupWorkstation: %v", err)
}
if result.Applied == nil || result.Applied.ProfilesCreated != 1 {
t.Fatalf("unexpected apply result: %#v", result)
}
if got := cfg.Profiles["documents"].Store; got != "primary" {
t.Fatalf("documents store = %q, want primary", got)
}
}

func stubWorkstationSetupEnv(t *testing.T) func() {
t.Helper()
oldDiscover := workstationDiscoverSourcesFunc
Expand Down
2 changes: 1 addition & 1 deletion pkg/source/gdrive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ func TestBuildDriveService_PrebuiltServiceTakesPriority(t *testing.T) {
injected := &drive.Service{}
cfg := gDriveOptions{
service: injected,
httpClient: &http.Client{}, // should be ignored
httpClient: &http.Client{}, // should be ignored
credsJSON: []byte(`{"type":"service_account"}`), // should be ignored
}
srv, err := buildDriveService(context.Background(), cfg)
Expand Down
Loading