Skip to content

Commit f38f45b

Browse files
Squashed commit of the following:
commit b7c7296 Author: Fred <[email protected]> Date: Mon Oct 28 18:06:31 2024 +0000 simple client/server model
1 parent 87cfd5d commit f38f45b

34 files changed

+1146
-121
lines changed

commands.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,24 @@ func getOwnCommands() []ownCommand {
157157
needConfiguration: false,
158158
hide: true,
159159
},
160+
{
161+
name: "send",
162+
description: "send a configuration profile to a remote client and execute a command",
163+
action: sendProfileCommand,
164+
needConfiguration: true,
165+
noProfile: true,
166+
hide: true,
167+
experimental: true,
168+
},
169+
{
170+
name: "serve",
171+
description: "serve configuration profiles to remote clients",
172+
action: serveCommand,
173+
needConfiguration: true,
174+
noProfile: true,
175+
hide: true,
176+
experimental: true,
177+
},
160178
}
161179
}
162180

commands_display.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/creativeprojects/resticprofile/util/collect"
2020
"github.com/fatih/color"
2121
colorable "github.com/mattn/go-colorable"
22+
"github.com/spf13/afero"
2223
)
2324

2425
var (
@@ -165,7 +166,7 @@ func displayResticHelp(output io.Writer, configuration *config.Config, flags com
165166
}
166167
}
167168

168-
if restic, err := filesearch.FindResticBinary(resticBinary); err == nil {
169+
if restic, err := filesearch.FindResticBinary(afero.NewOsFs(), resticBinary); err == nil {
169170
buf := bytes.Buffer{}
170171
cmd := shell.NewCommand(restic, []string{"help", command})
171172
cmd.Stdout = &buf

complete.go

Lines changed: 3 additions & 2 deletions
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

@@ -134,8 +135,8 @@ func (c *Completer) listProfileNames() (list []string) {
134135
format = formatFlag.Value.String()
135136
}
136137

137-
if file, err := filesearch.FindConfigurationFile(filename); err == nil {
138-
if conf, err := config.LoadFile(file, format); err == nil {
138+
if file, err := filesearch.FindConfigurationFile(afero.NewOsFs(), filename); 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: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"errors"
66
"fmt"
77
"io"
8-
"os"
98
"path/filepath"
109
"slices"
1110
"sort"
@@ -20,6 +19,7 @@ import (
2019
"github.com/creativeprojects/resticprofile/util/maybe"
2120
"github.com/creativeprojects/resticprofile/util/templates"
2221
"github.com/mitchellh/mapstructure"
22+
"github.com/spf13/afero"
2323
"github.com/spf13/viper"
2424
"golang.org/x/exp/maps"
2525
)
@@ -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
}
@@ -101,7 +101,7 @@ func LoadFile(configFile, format string) (config *Config, err error) {
101101

102102
// Load includes (if any).
103103
var includes []string
104-
if includes, err = filesearch.FindConfigurationIncludes(configFile, config.getIncludes()); err == nil {
104+
if includes, err = filesearch.FindConfigurationIncludes(fs, configFile, config.getIncludes()); err == nil {
105105
for _, include := range includes {
106106
format := formatFromExtension(include)
107107

@@ -717,6 +717,22 @@ func (c *Config) getProfilePath(key string) string {
717717
return c.flatKey(constants.SectionConfigurationProfiles, key)
718718
}
719719

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

0 commit comments

Comments
 (0)