[WIP] v0.7.0 fixes and changes to cache and remotefetcher
This commit is contained in:
parent
8c633fd4b2
commit
086835453b
3
.changes/unreleased/Added-20250128-153524.yaml
Normal file
3
.changes/unreleased/Added-20250128-153524.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
kind: Added
|
||||||
|
body: Cache functionality - still a WIP
|
||||||
|
time: 2025-01-28T15:35:24.512485671-06:00
|
3
.changes/unreleased/Changed-20250128-154204.yaml
Normal file
3
.changes/unreleased/Changed-20250128-154204.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
kind: Changed
|
||||||
|
body: "name of `configfetcher` to `remotefetcher`"
|
||||||
|
time: 2025-01-28T15:42:04.282668058-06:00
|
3
.changes/unreleased/Fixed-20250128-153806.yaml
Normal file
3
.changes/unreleased/Fixed-20250128-153806.yaml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
kind: Fixed
|
||||||
|
body: Parsing of remote URLs when determining list config file path
|
||||||
|
time: 2025-01-28T15:38:06.957506929-06:00
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,4 +1,4 @@
|
|||||||
|
|
||||||
dist/
|
.codegpt
|
||||||
|
|
||||||
.codegpt
|
*.log
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"Cmds",
|
"Cmds",
|
||||||
"configfetcher",
|
"remotefetcher",
|
||||||
"knadh",
|
"knadh",
|
||||||
"koanf",
|
"koanf",
|
||||||
"mattn",
|
"mattn",
|
||||||
|
@ -14,9 +14,20 @@ If you leave the config path blank, the following paths will be searched in orde
|
|||||||
|
|
||||||
1. `./backy.yml`
|
1. `./backy.yml`
|
||||||
2. `./backy.yaml`
|
2. `./backy.yaml`
|
||||||
3. `~/.config/backy.yml`
|
3. The same two files above contained in a `backy` subdirectory under in what is returned by Go's `os` package function `UserConfigDir()`.
|
||||||
4. `~/.config/backy.yaml`
|
|
||||||
|
|
||||||
Create a file at `~/.config/backy.yml`.
|
{{% expand title="`UserConfigDir()` documentation:" %}}
|
||||||
|
|
||||||
See the rest of the documentation in this section to configure it.
|
Up-to date documentation for this function may be found on [GoDoc](https://pkg.go.dev/os#UserConfigDir).
|
||||||
|
|
||||||
|
>UserConfigDir returns the default root directory to use for user-specific configuration data. Users should create their own application-specific subdirectory within this one and use that.
|
||||||
|
|
||||||
|
>On Unix systems, it returns $XDG_CONFIG_HOME as specified by https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if non-empty, else $HOME/.config. On Darwin, it returns $HOME/Library/Application Support. On Windows, it returns %AppData%. On Plan 9, it returns $home/lib.
|
||||||
|
|
||||||
|
>If the location cannot be determined (for example, $HOME is not defined), then it will return an error.
|
||||||
|
|
||||||
|
{{% /expand %}}
|
||||||
|
|
||||||
|
See the rest of the documentation, titles included below, in this section to configure it.
|
||||||
|
|
||||||
|
{{% children description="true" %}}
|
@ -2,7 +2,7 @@
|
|||||||
title: "Command Lists"
|
title: "Command Lists"
|
||||||
weight: 2
|
weight: 2
|
||||||
description: >
|
description: >
|
||||||
This page tells you how to get started with Backy.
|
This page tells you how to get use command lists.
|
||||||
---
|
---
|
||||||
|
|
||||||
Command lists are for executing commands in sequence and getting notifications from them.
|
Command lists are for executing commands in sequence and getting notifications from them.
|
||||||
@ -14,7 +14,7 @@ Lists can go in a separate file. Command lists should be in a separate file if:
|
|||||||
1. key 'cmd-lists.file' is found
|
1. key 'cmd-lists.file' is found
|
||||||
2. hosts.yml or hosts.yaml is found in the same directory as the backy config file
|
2. hosts.yml or hosts.yaml is found in the same directory as the backy config file
|
||||||
|
|
||||||
```yaml
|
```yaml {lineNos="true" wrap="true" title="yaml"}
|
||||||
test2:
|
test2:
|
||||||
name: test2
|
name: test2
|
||||||
order:
|
order:
|
||||||
@ -65,10 +65,10 @@ Backy also has a cron mode, so one can run `backy cron` and start a process that
|
|||||||
Adding `cron: 0 0 1 * * *` to a `cmd-lists` object will schedule the list at 1 in the morning. See [https://crontab.guru/](https://crontab.guru/) for reference.
|
Adding `cron: 0 0 1 * * *` to a `cmd-lists` object will schedule the list at 1 in the morning. See [https://crontab.guru/](https://crontab.guru/) for reference.
|
||||||
|
|
||||||
{{% notice tip %}}
|
{{% notice tip %}}
|
||||||
Note: Backy uses the second field of cron, so add anything except * to the beginning of a regular cron expression.
|
Note: Backy uses the second field of cron, so add anything except `*` to the beginning of a regular cron expression.
|
||||||
{{% /notice %}}
|
{{% /notice %}}
|
||||||
|
|
||||||
```yaml
|
```yaml {lineNos="true" wrap="true" title="yaml"}
|
||||||
cmd-lists:
|
cmd-lists:
|
||||||
docker-container-backup: # this can be any name you want
|
docker-container-backup: # this can be any name you want
|
||||||
# all commands have to be defined
|
# all commands have to be defined
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Commands"
|
title: "Commands"
|
||||||
|
description: Commands are just that, commands
|
||||||
weight: 1
|
weight: 1
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "Packages"
|
title: "Packages"
|
||||||
weight: 2
|
weight: 2
|
||||||
|
description: This is dedicated to package commands.
|
||||||
---
|
---
|
||||||
|
|
||||||
This is dedicated to `package` commands. The command `type` field must be `package`. Package is a type that allows one to perform package operations. There are several additional options available when `type` is `package`:
|
This is dedicated to `package` commands. The command `type` field must be `package`. Package is a type that allows one to perform package operations. There are several additional options available when `type` is `package`:
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: "Vault"
|
title: "Vault"
|
||||||
weight: 4
|
weight: 4
|
||||||
|
description: Set up and configure vault.
|
||||||
---
|
---
|
||||||
|
|
||||||
[Vault](https://www.vaultproject.io/) is a tool for storing secrets and other data securely.
|
[Vault](https://www.vaultproject.io/) is a tool for storing secrets and other data securely.
|
||||||
|
@ -28,12 +28,23 @@ commands:
|
|||||||
cmd: hostname
|
cmd: hostname
|
||||||
update-docker:
|
update-docker:
|
||||||
type: package
|
type: package
|
||||||
# shell: zsh
|
shell: zsh # best to run package commands in a shell
|
||||||
packageName: docker-ce
|
packageName: docker-ce
|
||||||
Args:
|
Args:
|
||||||
- docker-ce-cli
|
- docker-ce-cli
|
||||||
packageManager: apt
|
packageManager: apt
|
||||||
packageOperation: upgrade
|
packageOperation: install
|
||||||
|
update-dockerApt:
|
||||||
|
# type: package
|
||||||
|
shell: zsh
|
||||||
|
cmd: apt
|
||||||
|
Args:
|
||||||
|
- update
|
||||||
|
- "&&"
|
||||||
|
- apt install -y docker-ce
|
||||||
|
- docker-ce-cli
|
||||||
|
packageManager: apt
|
||||||
|
packageOperation: install
|
||||||
|
|
||||||
cmd-lists:
|
cmd-lists:
|
||||||
cmds-to-run: # this can be any name you want
|
cmds-to-run: # this can be any name you want
|
||||||
|
@ -98,8 +98,14 @@ func (command *Command) RunCmd(cmdCtxLogger zerolog.Logger, opts *ConfigOpts) ([
|
|||||||
|
|
||||||
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine", command.Name)).Send()
|
cmdCtxLogger.Info().Str("Command", fmt.Sprintf("Running command %s on local machine", command.Name)).Send()
|
||||||
|
|
||||||
localCMD = exec.Command(command.Cmd, command.Args...)
|
// execute package commands in a shell
|
||||||
|
if command.Type == "package" {
|
||||||
|
cmdCtxLogger.Info().Str("package", command.PackageName).Msg("Executing package command")
|
||||||
|
ArgsStr = fmt.Sprintf("%s %s", command.Cmd, ArgsStr)
|
||||||
|
localCMD = exec.Command("/bin/sh", "-c", ArgsStr)
|
||||||
|
} else {
|
||||||
|
localCMD = exec.Command(command.Cmd, command.Args...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if command.Dir != nil {
|
if command.Dir != nil {
|
||||||
|
@ -2,15 +2,17 @@ package backy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.andrewnw.xyz/CyberShell/backy/pkg/configfetcher"
|
|
||||||
"git.andrewnw.xyz/CyberShell/backy/pkg/logging"
|
"git.andrewnw.xyz/CyberShell/backy/pkg/logging"
|
||||||
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman"
|
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman"
|
||||||
|
"git.andrewnw.xyz/CyberShell/backy/pkg/remotefetcher"
|
||||||
"git.andrewnw.xyz/CyberShell/backy/pkg/usermanager"
|
"git.andrewnw.xyz/CyberShell/backy/pkg/usermanager"
|
||||||
vault "github.com/hashicorp/vault/api"
|
vault "github.com/hashicorp/vault/api"
|
||||||
"github.com/knadh/koanf/parsers/yaml"
|
"github.com/knadh/koanf/parsers/yaml"
|
||||||
@ -20,23 +22,23 @@ import (
|
|||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
var homeDir string
|
|
||||||
var homeDirErr error
|
|
||||||
var backyHomeConfDir string
|
|
||||||
var configFiles []string
|
|
||||||
|
|
||||||
const macroStart string = "%{"
|
const macroStart string = "%{"
|
||||||
const macroEnd string = "}%"
|
const macroEnd string = "}%"
|
||||||
const envMacroStart string = "%{env:"
|
const envMacroStart string = "%{env:"
|
||||||
const vaultMacroStart string = "%{vault:"
|
const vaultMacroStart string = "%{vault:"
|
||||||
|
|
||||||
func (opts *ConfigOpts) InitConfig() {
|
func (opts *ConfigOpts) InitConfig() {
|
||||||
homeDir, err := os.UserHomeDir()
|
var err error
|
||||||
|
homeConfigDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
logging.ExitWithMSG(err.Error(), 1, nil)
|
||||||
|
}
|
||||||
|
homeCacheDir, err := os.UserCacheDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.ExitWithMSG(err.Error(), 1, nil)
|
logging.ExitWithMSG(err.Error(), 1, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
backyHomeConfDir := path.Join(homeDir, ".config/backy/")
|
backyHomeConfDir := path.Join(homeConfigDir, "backy")
|
||||||
configFiles := []string{
|
configFiles := []string{
|
||||||
"./backy.yml", "./backy.yaml",
|
"./backy.yml", "./backy.yaml",
|
||||||
path.Join(backyHomeConfDir, "backy.yml"),
|
path.Join(backyHomeConfDir, "backy.yml"),
|
||||||
@ -46,8 +48,36 @@ func (opts *ConfigOpts) InitConfig() {
|
|||||||
backyKoanf := koanf.New(".")
|
backyKoanf := koanf.New(".")
|
||||||
opts.ConfigFilePath = strings.TrimSpace(opts.ConfigFilePath)
|
opts.ConfigFilePath = strings.TrimSpace(opts.ConfigFilePath)
|
||||||
|
|
||||||
|
metadataFile := "hashMetadataSample.yml"
|
||||||
|
cacheDir := homeCacheDir
|
||||||
|
|
||||||
|
// Load metadata from file
|
||||||
|
opts.CachedData, err = remotefetcher.LoadMetadataFromFile(metadataFile)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error loading metadata:", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize cache with loaded metadata
|
||||||
|
cache, err := remotefetcher.NewCache(metadataFile, cacheDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error initializing cache:", err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate cache with loaded metadata
|
||||||
|
for _, data := range opts.CachedData {
|
||||||
|
cache.AddDataToStore(data.Hash, *data)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Cache, err = remotefetcher.NewCache(path.Join(backyHomeConfDir, "cache.yml"), backyHomeConfDir)
|
||||||
|
if err != nil {
|
||||||
|
logging.ExitWithMSG(fmt.Sprintf("error initializing cache: %v", err), 1, nil)
|
||||||
|
}
|
||||||
// Initialize the fetcher
|
// Initialize the fetcher
|
||||||
fetcher, err := configfetcher.NewConfigFetcher(opts.ConfigFilePath)
|
println("Creating new fetcher for source", opts.ConfigFilePath)
|
||||||
|
fetcher, err := remotefetcher.NewConfigFetcher(opts.ConfigFilePath, opts.Cache)
|
||||||
|
println("Created new fetcher for source", opts.ConfigFilePath)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.ExitWithMSG(fmt.Sprintf("error initializing config fetcher: %v", err), 1, nil)
|
logging.ExitWithMSG(fmt.Sprintf("error initializing config fetcher: %v", err), 1, nil)
|
||||||
@ -62,7 +92,7 @@ func (opts *ConfigOpts) InitConfig() {
|
|||||||
opts.koanf = backyKoanf
|
opts.koanf = backyKoanf
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfigFile(fetcher configfetcher.ConfigFetcher, filePath string, k *koanf.Koanf, opts *ConfigOpts) {
|
func loadConfigFile(fetcher remotefetcher.ConfigFetcher, filePath string, k *koanf.Koanf, opts *ConfigOpts) {
|
||||||
data, err := fetcher.Fetch(filePath)
|
data, err := fetcher.Fetch(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.ExitWithMSG(fmt.Sprintf("Could not fetch config file %s: %v", filePath, err), 1, nil)
|
logging.ExitWithMSG(fmt.Sprintf("Could not fetch config file %s: %v", filePath, err), 1, nil)
|
||||||
@ -73,7 +103,7 @@ func loadConfigFile(fetcher configfetcher.ConfigFetcher, filePath string, k *koa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadDefaultConfigFiles(fetcher configfetcher.ConfigFetcher, configFiles []string, k *koanf.Koanf, opts *ConfigOpts) {
|
func loadDefaultConfigFiles(fetcher remotefetcher.ConfigFetcher, configFiles []string, k *koanf.Koanf, opts *ConfigOpts) {
|
||||||
cFileFailures := 0
|
cFileFailures := 0
|
||||||
for _, c := range configFiles {
|
for _, c := range configFiles {
|
||||||
data, err := fetcher.Fetch(c)
|
data, err := fetcher.Fetch(c)
|
||||||
@ -236,13 +266,23 @@ func resolveProxyHosts(host *Host, opts *ConfigOpts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadCommandLists(opts *ConfigOpts, backyKoanf *koanf.Koanf) {
|
func loadCommandLists(opts *ConfigOpts, backyKoanf *koanf.Koanf) {
|
||||||
backyConfigFileDir := path.Dir(opts.ConfigFilePath)
|
var backyConfigFileDir string
|
||||||
listsConfig := koanf.New(".")
|
var listConfigFiles []string
|
||||||
listConfigFiles := []string{
|
var u *url.URL
|
||||||
path.Join(backyConfigFileDir, "lists.yml"),
|
// if config file is remote, use the directory of the remote file
|
||||||
path.Join(backyConfigFileDir, "lists.yaml"),
|
if isRemoteURL(opts.ConfigFilePath) {
|
||||||
|
_, u = getRemoteDir(opts.ConfigFilePath)
|
||||||
|
listConfigFiles = []string{u.JoinPath("lists.yml").String(), u.JoinPath("lists.yaml").String()}
|
||||||
|
} else {
|
||||||
|
backyConfigFileDir = path.Dir(opts.ConfigFilePath)
|
||||||
|
listConfigFiles = []string{
|
||||||
|
path.Join(backyConfigFileDir, "lists.yml"),
|
||||||
|
path.Join(backyConfigFileDir, "lists.yaml"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listsConfig := koanf.New(".")
|
||||||
|
|
||||||
for _, l := range listConfigFiles {
|
for _, l := range listConfigFiles {
|
||||||
if loadListConfigFile(l, listsConfig, opts) {
|
if loadListConfigFile(l, listsConfig, opts) {
|
||||||
break
|
break
|
||||||
@ -257,9 +297,29 @@ func loadCommandLists(opts *ConfigOpts, backyKoanf *koanf.Koanf) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadListConfigFile(filePath string, k *koanf.Koanf, opts *ConfigOpts) bool {
|
func isRemoteURL(filePath string) bool {
|
||||||
fetcher, err := configfetcher.NewConfigFetcher(filePath)
|
return strings.HasPrefix(filePath, "http://") || strings.HasPrefix(filePath, "https://") || strings.HasPrefix(filePath, "s3://")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRemoteDir(filePath string) (string, *url.URL) {
|
||||||
|
u, err := url.Parse(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
// u.Path is the path to the file, stripped of scheme and hostname
|
||||||
|
u.Path = path.Dir(u.Path)
|
||||||
|
|
||||||
|
return u.String(), u
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadListConfigFile(filePath string, k *koanf.Koanf, opts *ConfigOpts) bool {
|
||||||
|
fetcher, err := remotefetcher.NewConfigFetcher(filePath, opts.Cache, remotefetcher.IgnoreFileNotFound())
|
||||||
|
if err != nil {
|
||||||
|
// if file not found, ignore
|
||||||
|
if errors.Is(err, remotefetcher.ErrFileNotFound) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
logging.ExitWithMSG(fmt.Sprintf("error initializing config fetcher: %v", err), 1, nil)
|
logging.ExitWithMSG(fmt.Sprintf("error initializing config fetcher: %v", err), 1, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,7 +342,8 @@ func loadCmdListsFile(backyKoanf *koanf.Koanf, listsConfig *koanf.Koanf, opts *C
|
|||||||
opts.CmdListFile = path.Join(path.Dir(opts.ConfigFilePath), opts.CmdListFile)
|
opts.CmdListFile = path.Join(path.Dir(opts.ConfigFilePath), opts.CmdListFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
fetcher, err := configfetcher.NewConfigFetcher(opts.CmdListFile)
|
fetcher, err := remotefetcher.NewConfigFetcher(opts.CmdListFile, opts.Cache)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.ExitWithMSG(fmt.Sprintf("error initializing config fetcher: %v", err), 1, nil)
|
logging.ExitWithMSG(fmt.Sprintf("error initializing config fetcher: %v", err), 1, nil)
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman"
|
"git.andrewnw.xyz/CyberShell/backy/pkg/pkgman"
|
||||||
|
"git.andrewnw.xyz/CyberShell/backy/pkg/remotefetcher"
|
||||||
"git.andrewnw.xyz/CyberShell/backy/pkg/usermanager"
|
"git.andrewnw.xyz/CyberShell/backy/pkg/usermanager"
|
||||||
vaultapi "github.com/hashicorp/vault/api"
|
vaultapi "github.com/hashicorp/vault/api"
|
||||||
"github.com/kevinburke/ssh_config"
|
"github.com/kevinburke/ssh_config"
|
||||||
@ -217,6 +218,9 @@ type (
|
|||||||
koanf *koanf.Koanf
|
koanf *koanf.Koanf
|
||||||
|
|
||||||
NotificationConf *Notifications `yaml:"notifications"`
|
NotificationConf *Notifications `yaml:"notifications"`
|
||||||
|
|
||||||
|
Cache *remotefetcher.Cache
|
||||||
|
CachedData []*remotefetcher.CacheData
|
||||||
}
|
}
|
||||||
|
|
||||||
outStruct struct {
|
outStruct struct {
|
||||||
|
@ -275,7 +275,7 @@ func getCommandType(command *Command) *Command {
|
|||||||
case "modify":
|
case "modify":
|
||||||
command.Cmd, command.Args = command.userMan.ModifyUser(
|
command.Cmd, command.Args = command.userMan.ModifyUser(
|
||||||
command.Username,
|
command.Username,
|
||||||
homeDir,
|
command.UserHome,
|
||||||
command.UserShell,
|
command.UserShell,
|
||||||
command.UserGroups)
|
command.UserGroups)
|
||||||
case "checkIfExists":
|
case "checkIfExists":
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
package configfetcher
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
type ConfigFetcher interface {
|
|
||||||
// Fetch retrieves the configuration from the specified URL or source
|
|
||||||
// Returns the raw data as bytes or an error
|
|
||||||
Fetch(source string) ([]byte, error)
|
|
||||||
|
|
||||||
// Parse decodes the raw data into a Go structure (e.g., Commands, CommandLists)
|
|
||||||
// Takes the raw data as input and populates the target interface
|
|
||||||
Parse(data []byte, target interface{}) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConfigFetcher(source string, options ...Option) (ConfigFetcher, error) {
|
|
||||||
if strings.HasPrefix(source, "http") || strings.HasPrefix(source, "https") {
|
|
||||||
return NewHTTPFetcher(options...), nil
|
|
||||||
} else if strings.HasPrefix(source, "s3") {
|
|
||||||
fetcher, err := NewS3Fetcher(options...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return fetcher, nil
|
|
||||||
} else {
|
|
||||||
return &LocalFetcher{}, nil
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
package configfetcher
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LocalFetcher struct{}
|
|
||||||
|
|
||||||
// Fetch retrieves the configuration from the specified local file path
|
|
||||||
func (l *LocalFetcher) Fetch(source string) ([]byte, error) {
|
|
||||||
file, err := os.Open(source)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
return io.ReadAll(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse decodes the raw data into the provided target structure
|
|
||||||
func (l *LocalFetcher) Parse(data []byte, target interface{}) error {
|
|
||||||
return yaml.Unmarshal(data, target)
|
|
||||||
}
|
|
191
pkg/remotefetcher/cache.go
Normal file
191
pkg/remotefetcher/cache.go
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
package remotefetcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CacheData struct {
|
||||||
|
Hash string `yaml:"hash"`
|
||||||
|
Path string `yaml:"path"`
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cache struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
store map[string]CacheData
|
||||||
|
file string
|
||||||
|
dir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCache(file, dir string) (*Cache, error) {
|
||||||
|
cache := &Cache{
|
||||||
|
store: make(map[string]CacheData),
|
||||||
|
file: file,
|
||||||
|
dir: dir,
|
||||||
|
}
|
||||||
|
err := cache.loadFromFile()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cache, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) loadFromFile() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
if _, err := os.Stat(c.file); os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(c.file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheData []CacheData
|
||||||
|
err = yaml.Unmarshal(data, &cacheData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range cacheData {
|
||||||
|
c.store[item.Hash] = item
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) saveToFile() error {
|
||||||
|
// println("Saving cache to file:", c.file)
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
var cacheData []CacheData
|
||||||
|
for _, data := range c.store {
|
||||||
|
cacheData = append(cacheData, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := yaml.Marshal(cacheData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(c.file, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Get(hash string) ([]byte, CacheData, bool) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
println("Getting cache data for hash:", hash)
|
||||||
|
cacheData, exists := c.store[hash]
|
||||||
|
if !exists {
|
||||||
|
println("Cache data does not exist for hash:", hash)
|
||||||
|
return nil, CacheData{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(cacheData.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, CacheData{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, cacheData, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) AddDataToStore(hash string, cacheData CacheData) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.store[hash] = cacheData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) Set(source, hash string, data []byte, dataType string) (CacheData, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
fileName := filepath.Base(source)
|
||||||
|
|
||||||
|
path := filepath.Join(c.dir, fmt.Sprintf("%s-%s", fileName, hash))
|
||||||
|
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
os.MkdirAll(c.dir, 0700)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := os.WriteFile(path, data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return CacheData{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheData := CacheData{
|
||||||
|
Hash: hash,
|
||||||
|
Path: path,
|
||||||
|
Type: dataType,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.store[hash] = cacheData
|
||||||
|
|
||||||
|
// Unlock before calling saveToFile to avoid double-locking
|
||||||
|
c.mu.Unlock()
|
||||||
|
err = c.saveToFile()
|
||||||
|
c.mu.Lock()
|
||||||
|
if err != nil {
|
||||||
|
return CacheData{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmt.Printf("Cache data: %v", cacheData)
|
||||||
|
return cacheData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CachedFetcher struct {
|
||||||
|
data []byte
|
||||||
|
path string
|
||||||
|
dataType string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cf *CachedFetcher) Fetch(source string) ([]byte, error) {
|
||||||
|
return cf.data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cf *CachedFetcher) Parse(data []byte, target interface{}) error {
|
||||||
|
if cf.dataType == "yaml" {
|
||||||
|
return yaml.Unmarshal(data, target)
|
||||||
|
}
|
||||||
|
return errors.New("parse not supported on cached fetcher for scripts")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cf *CachedFetcher) Hash(data []byte) string {
|
||||||
|
hash := sha256.Sum256(data)
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to read and parse the hashMetadataSample.yml file
|
||||||
|
func LoadMetadataFromFile(filePath string) ([]*CacheData, error) {
|
||||||
|
// fmt.Println("Loading metadata from file:", filePath)
|
||||||
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
|
// Create the file if it does not exist
|
||||||
|
emptyData := []byte("[]")
|
||||||
|
err := os.WriteFile(filePath, emptyData, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheData []*CacheData
|
||||||
|
err = yaml.Unmarshal(data, &cacheData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cacheData, nil
|
||||||
|
}
|
75
pkg/remotefetcher/configfetcher.go
Normal file
75
pkg/remotefetcher/configfetcher.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package remotefetcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigFetcher interface {
|
||||||
|
// Fetch retrieves the configuration from the specified URL or source
|
||||||
|
// Returns the raw data as bytes or an error
|
||||||
|
Fetch(source string) ([]byte, error)
|
||||||
|
|
||||||
|
// Parse decodes the raw data into a Go structure (e.g., Commands, CommandLists)
|
||||||
|
// Takes the raw data as input and populates the target interface
|
||||||
|
Parse(data []byte, target interface{}) error
|
||||||
|
|
||||||
|
// Hash returns the hash of the configuration data
|
||||||
|
Hash(data []byte) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrFileNotFound is returned when the file is not found and should be ignored
|
||||||
|
var ErrFileNotFound = errors.New("remotefetcher: file not found")
|
||||||
|
|
||||||
|
func NewConfigFetcher(source string, cache *Cache, options ...Option) (ConfigFetcher, error) {
|
||||||
|
var fetcher ConfigFetcher
|
||||||
|
var dataType string
|
||||||
|
|
||||||
|
config := FetcherConfig{}
|
||||||
|
for _, option := range options {
|
||||||
|
option(&config)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(source, "http") || strings.HasPrefix(source, "https") {
|
||||||
|
fetcher = NewHTTPFetcher(options...)
|
||||||
|
dataType = "yaml"
|
||||||
|
} else if strings.HasPrefix(source, "s3") {
|
||||||
|
var err error
|
||||||
|
fetcher, err = NewS3Fetcher(options...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dataType = "yaml"
|
||||||
|
} else {
|
||||||
|
fetcher = &LocalFetcher{}
|
||||||
|
dataType = "yaml"
|
||||||
|
|
||||||
|
return fetcher, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: should local files be cached?
|
||||||
|
|
||||||
|
data, err := fetcher.Fetch(source)
|
||||||
|
if err != nil {
|
||||||
|
if config.IgnoreFileNotFound && isFileNotFoundError(err) {
|
||||||
|
return nil, ErrFileNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := fetcher.Hash(data)
|
||||||
|
if cachedData, cacheMeta, exists := cache.Get(hash); exists {
|
||||||
|
return &CachedFetcher{data: cachedData, path: cacheMeta.Path, dataType: cacheMeta.Type}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheData, err := cache.Set(source, hash, data, dataType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &CachedFetcher{data: data, path: cacheData.Path, dataType: cacheData.Type}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFileNotFoundError(err error) bool {
|
||||||
|
// Implement logic to check if the error is a "file not found" error
|
||||||
|
// This can be based on the error type or message
|
||||||
|
return strings.Contains(err.Error(), "file not found")
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
package configfetcher
|
package remotefetcher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -10,6 +12,7 @@ import (
|
|||||||
|
|
||||||
type HTTPFetcher struct {
|
type HTTPFetcher struct {
|
||||||
HTTPClient *http.Client
|
HTTPClient *http.Client
|
||||||
|
config FetcherConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHTTPFetcher creates a new instance of HTTPFetcher with the provided options.
|
// NewHTTPFetcher creates a new instance of HTTPFetcher with the provided options.
|
||||||
@ -24,10 +27,10 @@ func NewHTTPFetcher(options ...Option) *HTTPFetcher {
|
|||||||
cfg.HTTPClient = http.DefaultClient
|
cfg.HTTPClient = http.DefaultClient
|
||||||
}
|
}
|
||||||
|
|
||||||
return &HTTPFetcher{HTTPClient: cfg.HTTPClient}
|
return &HTTPFetcher{HTTPClient: cfg.HTTPClient, config: *cfg}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch retrieves the configuration from the specified URL
|
// Fetch retrieves the file from the specified source URL
|
||||||
func (h *HTTPFetcher) Fetch(source string) ([]byte, error) {
|
func (h *HTTPFetcher) Fetch(source string) ([]byte, error) {
|
||||||
resp, err := http.Get(source)
|
resp, err := http.Get(source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -35,6 +38,10 @@ func (h *HTTPFetcher) Fetch(source string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusNotFound && h.config.IgnoreFileNotFound {
|
||||||
|
return nil, ErrFileNotFound
|
||||||
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, errors.New("failed to fetch remote config: " + resp.Status)
|
return nil, errors.New("failed to fetch remote config: " + resp.Status)
|
||||||
}
|
}
|
||||||
@ -46,3 +53,8 @@ func (h *HTTPFetcher) Fetch(source string) ([]byte, error) {
|
|||||||
func (h *HTTPFetcher) Parse(data []byte, target interface{}) error {
|
func (h *HTTPFetcher) Parse(data []byte, target interface{}) error {
|
||||||
return yaml.Unmarshal(data, target)
|
return yaml.Unmarshal(data, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *HTTPFetcher) Hash(data []byte) string {
|
||||||
|
hash := sha256.Sum256(data)
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
42
pkg/remotefetcher/local.go
Normal file
42
pkg/remotefetcher/local.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package remotefetcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LocalFetcher struct {
|
||||||
|
config FetcherConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch retrieves the file from the specified local file path
|
||||||
|
func (l *LocalFetcher) Fetch(source string) ([]byte, error) {
|
||||||
|
// Check if the file exists
|
||||||
|
if _, err := os.Stat(source); os.IsNotExist(err) {
|
||||||
|
if l.config.IgnoreFileNotFound {
|
||||||
|
return nil, ErrFileNotFound
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
file, err := os.Open(source)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
return io.ReadAll(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse decodes the raw data into the provided target structure
|
||||||
|
func (l *LocalFetcher) Parse(data []byte, target interface{}) error {
|
||||||
|
return yaml.Unmarshal(data, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LocalFetcher) Hash(data []byte) string {
|
||||||
|
hash := sha256.Sum256(data)
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package configfetcher
|
package remotefetcher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -11,8 +11,9 @@ type Option func(*FetcherConfig)
|
|||||||
|
|
||||||
// FetcherConfig holds the configuration for a fetcher.
|
// FetcherConfig holds the configuration for a fetcher.
|
||||||
type FetcherConfig struct {
|
type FetcherConfig struct {
|
||||||
S3Client *s3.Client
|
S3Client *s3.Client
|
||||||
HTTPClient *http.Client
|
HTTPClient *http.Client
|
||||||
|
IgnoreFileNotFound bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithS3Client sets the S3 client for the fetcher.
|
// WithS3Client sets the S3 client for the fetcher.
|
||||||
@ -28,3 +29,9 @@ func WithHTTPClient(client *http.Client) Option {
|
|||||||
cfg.HTTPClient = client
|
cfg.HTTPClient = client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IgnoreFileNotFound() Option {
|
||||||
|
return func(cfg *FetcherConfig) {
|
||||||
|
cfg.IgnoreFileNotFound = true
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,22 @@
|
|||||||
package configfetcher
|
package remotefetcher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/config"
|
"github.com/aws/aws-sdk-go-v2/config"
|
||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type S3Fetcher struct {
|
type S3Fetcher struct {
|
||||||
S3Client *s3.Client
|
S3Client *s3.Client
|
||||||
|
config FetcherConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewS3Fetcher creates a new instance of S3Fetcher with the provided options.
|
// NewS3Fetcher creates a new instance of S3Fetcher with the provided options.
|
||||||
@ -31,7 +35,7 @@ func NewS3Fetcher(options ...Option) (*S3Fetcher, error) {
|
|||||||
cfg.S3Client = s3.NewFromConfig(awsCfg)
|
cfg.S3Client = s3.NewFromConfig(awsCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &S3Fetcher{S3Client: cfg.S3Client}, nil
|
return &S3Fetcher{S3Client: cfg.S3Client, config: *cfg}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch retrieves the configuration from an S3 bucket
|
// Fetch retrieves the configuration from an S3 bucket
|
||||||
@ -47,6 +51,13 @@ func (s *S3Fetcher) Fetch(source string) ([]byte, error) {
|
|||||||
Key: &key,
|
Key: &key,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err != nil {
|
||||||
|
var notFound *types.NoSuchKey
|
||||||
|
if errors.As(err, ¬Found) && s.config.IgnoreFileNotFound {
|
||||||
|
return nil, ErrFileNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
@ -73,3 +84,8 @@ func parseS3Source(source string) (bucket, key string, err error) {
|
|||||||
}
|
}
|
||||||
return parts[0], parts[1], nil
|
return parts[0], parts[1], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *S3Fetcher) Hash(data []byte) string {
|
||||||
|
hash := sha256.Sum256(data)
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user