Skip to content

Commit 5e1e949

Browse files
simple client/server model
1 parent f9039fa commit 5e1e949

33 files changed

+1117
-64
lines changed

commands.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,24 @@ func getOwnCommands() []ownCommand {
168168
needConfiguration: false,
169169
hide: true,
170170
},
171+
{
172+
name: "send",
173+
description: "send a configuration profile to a remote client and execute a command",
174+
action: sendProfileCommand,
175+
needConfiguration: true,
176+
noProfile: true,
177+
hide: true,
178+
experimental: true,
179+
},
180+
{
181+
name: "serve",
182+
description: "serve configuration profiles to remote clients",
183+
action: serveCommand,
184+
needConfiguration: true,
185+
noProfile: true,
186+
hide: true,
187+
experimental: true,
188+
},
171189
}
172190
}
173191

complete.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/creativeprojects/clog"
1111
"github.com/creativeprojects/resticprofile/config"
1212
"github.com/creativeprojects/resticprofile/filesearch"
13+
"github.com/spf13/afero"
1314
"github.com/spf13/pflag"
1415
)
1516

@@ -135,7 +136,7 @@ func (c *Completer) listProfileNames() (list []string) {
135136
}
136137

137138
if file, err := filesearch.NewFinder().FindConfigurationFile(filename); err == nil {
138-
if conf, err := config.LoadFile(file, format); err == nil {
139+
if conf, err := config.LoadFile(afero.NewOsFs(), file, format); err == nil {
139140
list = append(list, conf.GetProfileNames()...)
140141
for name := range conf.GetProfileGroups() {
141142
list = append(list, name)

config/checkdoc/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/creativeprojects/clog"
1313
"github.com/creativeprojects/resticprofile/config"
14+
"github.com/spf13/afero"
1415
"github.com/spf13/pflag"
1516
)
1617

@@ -174,7 +175,7 @@ func saveConfiguration(content []byte, configType string) (string, error) {
174175

175176
// checkConfiguration returns true when the configuration is valid
176177
func checkConfiguration(filename, configType string, lineNum int) bool {
177-
cfg, err := config.LoadFile(filename, configType)
178+
cfg, err := config.LoadFile(afero.NewOsFs(), filename, configType)
178179
if err != nil {
179180
clog.Errorf(" %q on line %d: %s", configType, lineNum, err)
180181
return false

config/config.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"fmt"
77
"io"
88
"maps"
9-
"os"
109
"path/filepath"
1110
"slices"
1211
"sort"
@@ -21,6 +20,7 @@ import (
2120
"github.com/creativeprojects/resticprofile/util/maybe"
2221
"github.com/creativeprojects/resticprofile/util/templates"
2322
"github.com/mitchellh/mapstructure"
23+
"github.com/spf13/afero"
2424
"github.com/spf13/viper"
2525
)
2626

@@ -74,7 +74,7 @@ func formatFromExtension(configFile string) string {
7474

7575
// LoadFile loads configuration from file
7676
// Leave format blank for auto-detection from the file extension
77-
func LoadFile(configFile, format string) (config *Config, err error) {
77+
func LoadFile(fs afero.Fs, configFile, format string) (config *Config, err error) {
7878
if format == "" {
7979
format = formatFromExtension(configFile)
8080
}
@@ -84,7 +84,7 @@ func LoadFile(configFile, format string) (config *Config, err error) {
8484

8585
readAndAdd := func(configFile string, replace bool) error {
8686
clog.Debugf("loading: %s", configFile)
87-
file, fileErr := os.Open(configFile)
87+
file, fileErr := fs.Open(configFile)
8888
if fileErr != nil {
8989
return fmt.Errorf("cannot open configuration file for reading: %w", fileErr)
9090
}
@@ -716,6 +716,22 @@ func (c *Config) getProfilePath(key string) string {
716716
return c.flatKey(constants.SectionConfigurationProfiles, key)
717717
}
718718

719+
// HasRemote returns true if the remote exists in the configuration
720+
func (c *Config) HasRemote(remoteName string) bool {
721+
return c.IsSet(c.flatKey(constants.SectionConfigurationRemotes, remoteName))
722+
}
723+
724+
func (c *Config) GetRemote(remoteName string) (*Remote, error) {
725+
// we don't need to check the file version: the remotes can be in a separate configuration file
726+
727+
remote := NewRemote(c, remoteName)
728+
err := c.unmarshalKey(c.flatKey(constants.SectionConfigurationRemotes, remoteName), remote)
729+
730+
rootPath := filepath.Dir(c.GetConfigFile())
731+
remote.SetRootPath(rootPath)
732+
return remote, err
733+
}
734+
719735
// unmarshalConfig returns the decoder config options depending on the configuration version and format
720736
func (c *Config) unmarshalConfig() viper.DecoderConfigOption {
721737
if c.GetVersion() == Version01 {

config/config_test.go

Lines changed: 42 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"time"
1212

1313
"github.com/creativeprojects/resticprofile/util/maybe"
14+
"github.com/spf13/afero"
1415
"github.com/stretchr/testify/assert"
1516
"github.com/stretchr/testify/require"
1617
)
@@ -366,67 +367,57 @@ x=0
366367
}
367368

368369
func TestIncludes(t *testing.T) {
369-
files := []string{}
370-
cleanFiles := func() {
371-
for _, file := range files {
372-
os.Remove(file)
373-
}
374-
files = files[:0]
375-
}
376-
defer cleanFiles()
377-
378-
createFile := func(t *testing.T, suffix, content string) string {
370+
createFile := func(t *testing.T, fs afero.Fs, suffix, content string) string {
379371
t.Helper()
380372
name := ""
381-
file, err := os.CreateTemp("", "*-"+suffix)
373+
file, err := afero.TempFile(fs, "", "*-"+suffix)
382374
if err == nil {
383375
defer file.Close()
384376
_, err = file.WriteString(content)
385377
name = file.Name()
386-
files = append(files, name)
387378
}
388379
require.NoError(t, err)
389380
return name
390381
}
391382

392-
mustLoadConfig := func(t *testing.T, configFile string) *Config {
383+
mustLoadConfig := func(t *testing.T, fs afero.Fs, configFile string) *Config {
393384
t.Helper()
394-
config, err := LoadFile(configFile, "")
385+
config, err := LoadFile(fs, configFile, "")
395386
require.NoError(t, err)
396387
return config
397388
}
398389

399390
testID := fmt.Sprintf("%d", time.Now().Unix())
400391

401392
t.Run("multiple-includes", func(t *testing.T) {
402-
defer cleanFiles()
393+
fs := afero.NewMemMapFs()
403394
content := fmt.Sprintf(`includes=['*%[1]s.inc.toml','*%[1]s.inc.yaml','*%[1]s.inc.json']`, testID)
404395

405-
configFile := createFile(t, "profiles.conf", content)
406-
createFile(t, "d-"+testID+".inc.toml", "[one]\nk='v'")
407-
createFile(t, "o-"+testID+".inc.yaml", `two: { k: v }`)
408-
createFile(t, "j-"+testID+".inc.json", `{"three":{ "k": "v" }}`)
396+
configFile := createFile(t, fs, "profiles.conf", content)
397+
createFile(t, fs, "d-"+testID+".inc.toml", "[one]\nk='v'")
398+
createFile(t, fs, "o-"+testID+".inc.yaml", `two: { k: v }`)
399+
createFile(t, fs, "j-"+testID+".inc.json", `{"three":{ "k": "v" }}`)
409400

410-
config := mustLoadConfig(t, configFile)
401+
config := mustLoadConfig(t, fs, configFile)
411402
assert.True(t, config.IsSet("includes"))
412403
assert.True(t, config.HasProfile("one"))
413404
assert.True(t, config.HasProfile("two"))
414405
assert.True(t, config.HasProfile("three"))
415406
})
416407

417408
t.Run("overrides", func(t *testing.T) {
418-
defer cleanFiles()
409+
fs := afero.NewMemMapFs()
419410

420-
configFile := createFile(t, "profiles.conf", `
411+
configFile := createFile(t, fs, "profiles.conf", `
421412
includes = "*`+testID+`.inc.toml"
422413
[default]
423414
repository = "default-repo"`)
424415

425-
createFile(t, "override-"+testID+".inc.toml", `
416+
createFile(t, fs, "override-"+testID+".inc.toml", `
426417
[default]
427418
repository = "overridden-repo"`)
428419

429-
config := mustLoadConfig(t, configFile)
420+
config := mustLoadConfig(t, fs, configFile)
430421
assert.True(t, config.HasProfile("default"))
431422

432423
profile, err := config.GetProfile("default")
@@ -435,26 +426,26 @@ repository = "overridden-repo"`)
435426
})
436427

437428
t.Run("mixins", func(t *testing.T) {
438-
defer cleanFiles()
429+
fs := afero.NewMemMapFs()
439430

440-
configFile := createFile(t, "profiles.conf", `
431+
configFile := createFile(t, fs, "profiles.conf", `
441432
version = 2
442433
includes = "*`+testID+`.inc.toml"
443434
[profiles.default]
444435
use = "another-run-before"
445436
run-before = "default-before"`)
446437

447-
createFile(t, "mixin-"+testID+".inc.toml", `
438+
createFile(t, fs, "mixin-"+testID+".inc.toml", `
448439
[mixins.another-run-before]
449440
"run-before..." = "another-run-before"
450441
[mixins.another-run-before2]
451442
"run-before..." = "another-run-before2"`)
452443

453-
createFile(t, "mixin-use-"+testID+".inc.toml", `
444+
createFile(t, fs, "mixin-use-"+testID+".inc.toml", `
454445
[profiles.default]
455446
use = "another-run-before2"`)
456447

457-
config := mustLoadConfig(t, configFile)
448+
config := mustLoadConfig(t, fs, configFile)
458449
assert.True(t, config.HasProfile("default"))
459450

460451
profile, err := config.GetProfile("default")
@@ -463,56 +454,56 @@ use = "another-run-before2"`)
463454
})
464455

465456
t.Run("hcl-includes-only-hcl", func(t *testing.T) {
466-
defer cleanFiles()
457+
fs := afero.NewMemMapFs()
467458

468-
configFile := createFile(t, "profiles.hcl", `includes = "*`+testID+`.inc.*"`)
469-
createFile(t, "pass-"+testID+".inc.hcl", `one { }`)
459+
configFile := createFile(t, fs, "profiles.hcl", `includes = "*`+testID+`.inc.*"`)
460+
createFile(t, fs, "pass-"+testID+".inc.hcl", `one { }`)
470461

471-
config := mustLoadConfig(t, configFile)
462+
config := mustLoadConfig(t, fs, configFile)
472463
assert.True(t, config.HasProfile("one"))
473464

474-
createFile(t, "fail-"+testID+".inc.toml", `[two]`)
475-
_, err := LoadFile(configFile, "")
465+
createFile(t, fs, "fail-"+testID+".inc.toml", `[two]`)
466+
_, err := LoadFile(fs, configFile, "")
476467
assert.Error(t, err)
477468
assert.Regexp(t, ".+ is in hcl format, includes must use the same format", err.Error())
478469
})
479470

480471
t.Run("non-hcl-include-no-hcl", func(t *testing.T) {
481-
defer cleanFiles()
472+
fs := afero.NewMemMapFs()
482473

483-
configFile := createFile(t, "profiles.toml", `includes = "*`+testID+`.inc.*"`)
484-
createFile(t, "pass-"+testID+".inc.toml", "[one]\nk='v'")
474+
configFile := createFile(t, fs, "profiles.toml", `includes = "*`+testID+`.inc.*"`)
475+
createFile(t, fs, "pass-"+testID+".inc.toml", "[one]\nk='v'")
485476

486-
config := mustLoadConfig(t, configFile)
477+
config := mustLoadConfig(t, fs, configFile)
487478
assert.True(t, config.HasProfile("one"))
488479

489-
createFile(t, "fail-"+testID+".inc.hcl", `one { }`)
490-
_, err := LoadFile(configFile, "")
480+
createFile(t, fs, "fail-"+testID+".inc.hcl", `one { }`)
481+
_, err := LoadFile(fs, configFile, "")
491482
assert.Error(t, err)
492483
assert.Regexp(t, "hcl format .+ cannot be used in includes from toml", err.Error())
493484
})
494485

495486
t.Run("cannot-load-different-versions", func(t *testing.T) {
496-
defer cleanFiles()
487+
fs := afero.NewMemMapFs()
497488
content := fmt.Sprintf(`includes=['*%s.inc.json']`, testID)
498489

499-
configFile := createFile(t, "profiles.conf", content)
500-
createFile(t, "a-"+testID+".inc.json", `{"version": 2, "profiles": {"one":{}}}`)
501-
createFile(t, "b-"+testID+".inc.json", `{"two":{}}`)
490+
configFile := createFile(t, fs, "profiles.conf", content)
491+
createFile(t, fs, "a-"+testID+".inc.json", `{"version": 2, "profiles": {"one":{}}}`)
492+
createFile(t, fs, "b-"+testID+".inc.json", `{"two":{}}`)
502493

503-
_, err := LoadFile(configFile, "")
494+
_, err := LoadFile(fs, configFile, "")
504495
assert.ErrorContains(t, err, "cannot include different versions of the configuration file")
505496
})
506497

507498
t.Run("cannot-load-different-versions", func(t *testing.T) {
508-
defer cleanFiles()
499+
fs := afero.NewMemMapFs()
509500
content := fmt.Sprintf(`{"version": 2, "includes":["*%s.inc.json"]}`, testID)
510501

511-
configFile := createFile(t, "profiles.json", content)
512-
createFile(t, "c-"+testID+".inc.json", `{"version": 1, "two":{}}`)
513-
createFile(t, "d-"+testID+".inc.json", `{"profiles": {"one":{}}}`)
502+
configFile := createFile(t, fs, "profiles.json", content)
503+
createFile(t, fs, "c-"+testID+".inc.json", `{"version": 1, "two":{}}`)
504+
createFile(t, fs, "d-"+testID+".inc.json", `{"profiles": {"one":{}}}`)
514505

515-
_, err := LoadFile(configFile, "")
506+
_, err := LoadFile(fs, configFile, "")
516507
assert.ErrorContains(t, err, "cannot include different versions of the configuration file")
517508
})
518509
}

config/error.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ package config
33
import "errors"
44

55
var (
6-
ErrNotFound = errors.New("not found")
6+
ErrNotFound = errors.New("not found")
7+
ErrNotSupportedInVersion1 = errors.New("not supported in configuration version 1")
78
)

config/remote.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package config
2+
3+
type Remote struct {
4+
name string
5+
config *Config
6+
Connection string `mapstructure:"connection" default:"ssh" description:"Connection type to use to connect to the remote client"`
7+
Host string `mapstructure:"host" description:"Address of the remote client. Format: <host>:<port>"`
8+
Username string `mapstructure:"username" description:"User to connect to the remote client"`
9+
PrivateKeyPath string `mapstructure:"private-key" description:"Path to the private key to use for authentication"`
10+
KnownHostsPath string `mapstructure:"known-hosts" description:"Path to the known hosts file"`
11+
BinaryPath string `mapstructure:"binary-path" description:"Path to the resticprofile binary to use on the remote client"`
12+
ConfigurationFile string `mapstructure:"configuration-file" description:"Path to the configuration file to transfer to the remote client"`
13+
ProfileName string `mapstructure:"profile-name" description:"Name of the profile to use on the remote client"`
14+
SendFiles []string `mapstructure:"send-files" description:"Other configuration files to transfer to the remote client"`
15+
}
16+
17+
func NewRemote(config *Config, name string) *Remote {
18+
remote := &Remote{
19+
name: name,
20+
config: config,
21+
}
22+
return remote
23+
}
24+
25+
// SetRootPath changes the path of all the relative paths and files in the configuration
26+
func (r *Remote) SetRootPath(rootPath string) {
27+
r.PrivateKeyPath = fixPath(r.PrivateKeyPath, expandEnv, absolutePrefix(rootPath))
28+
r.KnownHostsPath = fixPath(r.KnownHostsPath, expandEnv, absolutePrefix(rootPath))
29+
r.ConfigurationFile = fixPath(r.ConfigurationFile, expandEnv, absolutePrefix(rootPath))
30+
31+
for i := range r.SendFiles {
32+
r.SendFiles[i] = fixPath(r.SendFiles[i], expandEnv, absolutePrefix(rootPath))
33+
}
34+
}

constants/exit_code.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package constants
2+
3+
const (
4+
ExitSuccess = iota
5+
ExitGeneralError
6+
ExitErrorInvalidFlags
7+
ExitRunningOnBattery
8+
ExitCannotSetupRemoteConfiguration
9+
ExitErrorChildHasNoParentPort = 10
10+
)

constants/other.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ package constants
33
const (
44
TemporaryDirMarker = "temp:"
55
JSONSchema = "$schema"
6+
ManifestFilename = ".manifest.json"
67
)

constants/section.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const (
1313
SectionConfigurationMixins = "mixins"
1414
SectionConfigurationMixinUse = "use"
1515
SectionConfigurationSchedule = "schedule"
16+
SectionConfigurationRemotes = "remotes"
1617

1718
SectionDefinitionCommon = "common"
1819
SectionDefinitionForget = "forget"

0 commit comments

Comments
 (0)