-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
S3 backup support
- Loading branch information
Showing
20 changed files
with
749 additions
and
152 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
package backup | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/leg100/etok/pkg/backup" | ||
"github.com/spf13/pflag" | ||
corev1 "k8s.io/api/core/v1" | ||
) | ||
|
||
// flags represents the backup provider flags | ||
type flags interface { | ||
addToFlagSet(*pflag.FlagSet) | ||
createProvider(context.Context) (backup.Provider, error) | ||
validate() error | ||
} | ||
|
||
// flagMaker is a constructor for a flags obj | ||
type flagMaker func() flags | ||
|
||
// providerMap maps name of backup provider to a flags constructor | ||
type providerMap struct { | ||
name string | ||
maker flagMaker | ||
} | ||
|
||
// providers is the collection of provider mappings | ||
type providerMaps []providerMap | ||
|
||
var ( | ||
// mappings is a singleton containing collection of provider mappings | ||
mappings = providerMaps{} | ||
|
||
// ErrInvalidConfig is wrapped within all errors from this pkg, and can be | ||
// used by downstream to identify errors | ||
ErrInvalidConfig = errors.New("invalid backup config") | ||
|
||
ErrInvalidProvider = fmt.Errorf("%w: invalid provider", ErrInvalidConfig) | ||
) | ||
|
||
func addProvider(name string, f flagMaker) { | ||
mappings = append(mappings, providerMap{name: name, maker: f}) | ||
} | ||
|
||
// Config holds the flag configuration for all providers | ||
type Config struct { | ||
providers []string | ||
providerToFlags map[string]flags | ||
|
||
flagSet *pflag.FlagSet | ||
|
||
// Name of Selected provider | ||
Selected string | ||
} | ||
|
||
func NewConfig(optionalMappings ...providerMap) *Config { | ||
cfg := &Config{ | ||
flagSet: pflag.NewFlagSet("backup", pflag.ContinueOnError), | ||
providerToFlags: make(map[string]flags), | ||
} | ||
|
||
cfgMappings := mappings | ||
if len(optionalMappings) > 0 { | ||
cfgMappings = optionalMappings | ||
} | ||
|
||
for _, m := range cfgMappings { | ||
cfg.providers = append(cfg.providers, m.name) | ||
cfg.providerToFlags[m.name] = m.maker() | ||
cfg.providerToFlags[m.name].addToFlagSet(cfg.flagSet) | ||
} | ||
|
||
cfg.flagSet.StringVar(&cfg.Selected, "backup-provider", "", fmt.Sprintf("Enable backups specifying a provider (%v)", strings.Join(cfg.providers, ","))) | ||
|
||
return cfg | ||
} | ||
|
||
// AddToFlagSet adds config's (and its providers') flagsets to fs | ||
func (c *Config) AddToFlagSet(fs *pflag.FlagSet) { | ||
fs.AddFlagSet(c.flagSet) | ||
} | ||
|
||
// GetEnvVars converts the complete flagset into a list of k8s environment | ||
// variable objects. Assumes config's flagset is present within fs, and assumes | ||
// fs has been parsed. | ||
func (c *Config) GetEnvVars(fs *pflag.FlagSet) (envvars []corev1.EnvVar) { | ||
c.flagSet.VisitAll(func(flag *pflag.Flag) { | ||
// Only fs has been parsed so we need to get populated value from there | ||
if f := fs.Lookup(flag.Name); f != nil { | ||
envvars = append(envvars, corev1.EnvVar{Name: flagToEnvVarName(f), Value: f.Value.String()}) | ||
} | ||
}) | ||
return | ||
} | ||
|
||
// flagToEnvVarName converts flag f to an etok environment variable name | ||
func flagToEnvVarName(f *pflag.Flag) string { | ||
return fmt.Sprintf("ETOK_%s", strings.Replace(strings.ToUpper(f.Name), "-", "_", -1)) | ||
} | ||
|
||
func (c *Config) CreateSelectedProvider(ctx context.Context) (backup.Provider, error) { | ||
flags, ok := c.providerToFlags[c.Selected] | ||
if !ok { | ||
return nil, nil | ||
} | ||
return flags.createProvider(ctx) | ||
} | ||
|
||
// Validate all user-specified flags | ||
func (c *Config) Validate(fs *pflag.FlagSet) error { | ||
if c.Selected == "" { | ||
return nil | ||
} | ||
flags, ok := c.providerToFlags[c.Selected] | ||
if !ok { | ||
return fmt.Errorf("%w: %s (valid providers: %s)", ErrInvalidProvider, c.Selected, strings.Join(c.providers, ",")) | ||
} | ||
|
||
// Validate selected provider's flags | ||
return flags.validate() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
package backup | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"testing" | ||
|
||
"github.com/leg100/etok/pkg/testutil" | ||
"github.com/spf13/cobra" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
corev1 "k8s.io/api/core/v1" | ||
) | ||
|
||
func TestConfig(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
args []string | ||
assertions func(t *testutil.T, cmd *cobra.Command, cfg *Config) | ||
err error | ||
}{ | ||
{ | ||
name: "no backup provider specified", | ||
assertions: func(t *testutil.T, cmd *cobra.Command, cfg *Config) { | ||
assert.Contains(t, cfg.GetEnvVars(cmd.Flags()), corev1.EnvVar{ | ||
Name: "ETOK_BACKUP_PROVIDER", | ||
Value: "", | ||
}) | ||
provider, err := cfg.CreateSelectedProvider(context.Background()) | ||
require.NoError(t, err) | ||
require.Nil(t, provider) | ||
}, | ||
}, | ||
{ | ||
name: "valid config", | ||
args: []string{"--backup-provider=fake", "--fake-bucket=backups-bucket", "--fake-region=eu-west2"}, | ||
assertions: func(t *testutil.T, cmd *cobra.Command, cfg *Config) { | ||
assert.Contains(t, cfg.GetEnvVars(cmd.Flags()), corev1.EnvVar{ | ||
Name: "ETOK_BACKUP_PROVIDER", | ||
Value: "fake", | ||
}) | ||
assert.Contains(t, cfg.GetEnvVars(cmd.Flags()), corev1.EnvVar{ | ||
Name: "ETOK_FAKE_BUCKET", | ||
Value: "backups-bucket", | ||
}) | ||
assert.Contains(t, cfg.GetEnvVars(cmd.Flags()), corev1.EnvVar{ | ||
Name: "ETOK_FAKE_REGION", | ||
Value: "eu-west2", | ||
}) | ||
provider, err := cfg.CreateSelectedProvider(context.Background()) | ||
require.NoError(t, err) | ||
require.NotNil(t, provider) | ||
}, | ||
}, | ||
{ | ||
name: "invalid config", | ||
args: []string{"--backup-provider=fake", "--fake-bucket=backups-bucket"}, | ||
err: ErrInvalidConfig, | ||
}, | ||
{ | ||
name: "invalid provider", | ||
args: []string{"--backup-provider=hpcloud"}, | ||
err: ErrInvalidProvider, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
testutil.Run(t, tt.name, func(t *testutil.T) { | ||
cfg := NewConfig(providerMap{name: "fake", maker: newFakeFlags}) | ||
assert.Equal(t, []string{"fake"}, cfg.providers) | ||
|
||
cmd := &cobra.Command{ | ||
Use: "foo", | ||
} | ||
|
||
cfg.AddToFlagSet(cmd.Flags()) | ||
|
||
cmd.SetArgs(tt.args) | ||
|
||
require.NoError(t, cmd.Execute()) | ||
|
||
err := cfg.Validate(cmd.Flags()) | ||
if !assert.True(t, errors.Is(err, tt.err)) { | ||
t.Errorf("no error in %v's chain matches %v", err, tt.err) | ||
} | ||
|
||
if tt.assertions != nil { | ||
tt.assertions(t, cmd, cfg) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package backup | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/leg100/etok/pkg/backup" | ||
"github.com/spf13/pflag" | ||
) | ||
|
||
type fakeFlags struct { | ||
bucket string | ||
region string | ||
} | ||
|
||
func newFakeFlags() flags { | ||
return &fakeFlags{} | ||
} | ||
|
||
func (f *fakeFlags) addToFlagSet(fs *pflag.FlagSet) { | ||
fs.StringVar(&f.bucket, "fake-bucket", "", "Specify fake bucket for terraform state backups") | ||
fs.StringVar(&f.region, "fake-region", "", "Specify fake region for terraform state backups") | ||
} | ||
|
||
func (f *fakeFlags) createProvider(ctx context.Context) (backup.Provider, error) { | ||
return &backup.FakeProvider{}, nil | ||
} | ||
|
||
func (f *fakeFlags) validate() error { | ||
if f.bucket == "" { | ||
return fmt.Errorf("%w: missing fake bucket name", ErrInvalidConfig) | ||
} | ||
if f.region == "" { | ||
return fmt.Errorf("%w: missing fake region name", ErrInvalidConfig) | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package backup | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/leg100/etok/pkg/backup" | ||
"github.com/spf13/pflag" | ||
) | ||
|
||
func init() { | ||
addProvider("gcs", newGcsFlags) | ||
} | ||
|
||
type gcsFlags struct { | ||
bucket string | ||
} | ||
|
||
func newGcsFlags() flags { | ||
return &gcsFlags{} | ||
} | ||
|
||
func (f *gcsFlags) addToFlagSet(fs *pflag.FlagSet) { | ||
fs.StringVar(&f.bucket, "gcs-bucket", "", "Specify gcs bucket for terraform state backups") | ||
} | ||
|
||
func (f *gcsFlags) createProvider(ctx context.Context) (backup.Provider, error) { | ||
return backup.NewGCSProvider(ctx, f.bucket, nil) | ||
} | ||
|
||
func (f *gcsFlags) validate() error { | ||
if f.bucket == "" { | ||
return fmt.Errorf("%w: missing gcs bucket name", ErrInvalidConfig) | ||
} | ||
return nil | ||
} |
Oops, something went wrong.