XDG instance layout, multi-file config editor, talosconfig renewal, node upgrade/rollback CLI, operation cancellation, app restart, and removal of now-unneeded migration code.
295 lines
6.5 KiB
Go
295 lines
6.5 KiB
Go
package strategies
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
btypes "github.com/wild-cloud/wild-central/daemon/internal/backup/types"
|
|
)
|
|
|
|
// MockPostgresDestination for testing
|
|
type MockPostgresDestination struct {
|
|
putData map[string][]byte
|
|
getData map[string][]byte
|
|
putError error
|
|
getError error
|
|
deleteKeys []string
|
|
}
|
|
|
|
func NewMockPostgresDestination() *MockPostgresDestination {
|
|
return &MockPostgresDestination{
|
|
putData: make(map[string][]byte),
|
|
getData: make(map[string][]byte),
|
|
}
|
|
}
|
|
|
|
func (m *MockPostgresDestination) Put(key string, reader io.Reader) (int64, error) {
|
|
if m.putError != nil {
|
|
return 0, m.putError
|
|
}
|
|
data, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
m.putData[key] = data
|
|
return int64(len(data)), nil
|
|
}
|
|
|
|
func (m *MockPostgresDestination) Get(key string) (io.ReadCloser, error) {
|
|
if m.getError != nil {
|
|
return nil, m.getError
|
|
}
|
|
data, exists := m.getData[key]
|
|
if !exists {
|
|
data = []byte("mock backup data")
|
|
}
|
|
return io.NopCloser(bytes.NewReader(data)), nil
|
|
}
|
|
|
|
func (m *MockPostgresDestination) Delete(key string) error {
|
|
m.deleteKeys = append(m.deleteKeys, key)
|
|
return nil
|
|
}
|
|
|
|
func (m *MockPostgresDestination) List(prefix string) ([]btypes.BackupObject, error) {
|
|
var objects []btypes.BackupObject
|
|
for key, data := range m.putData {
|
|
if strings.HasPrefix(key, prefix) {
|
|
objects = append(objects, btypes.BackupObject{
|
|
Key: key,
|
|
Size: int64(len(data)),
|
|
LastModified: time.Now(),
|
|
})
|
|
}
|
|
}
|
|
return objects, nil
|
|
}
|
|
|
|
func (m *MockPostgresDestination) GetURL(key string, expiry time.Duration) (string, error) {
|
|
return "https://mock.example.com/" + key, nil
|
|
}
|
|
|
|
func (m *MockPostgresDestination) Type() string {
|
|
return "mock"
|
|
}
|
|
|
|
func TestPostgreSQLStrategy_Name(t *testing.T) {
|
|
s := NewPostgreSQLStrategy("/tmp")
|
|
assert.Equal(t, "postgres", s.Name())
|
|
}
|
|
|
|
func TestPostgreSQLStrategy_Verify(t *testing.T) {
|
|
s := NewPostgreSQLStrategy("/tmp")
|
|
|
|
tests := []struct {
|
|
name string
|
|
plan *btypes.RecoveryPlan
|
|
destData map[string][]byte
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "successful verification with PGDMP header",
|
|
plan: &btypes.RecoveryPlan{
|
|
App: "test-app",
|
|
Instance: "test-instance",
|
|
Strategies: []btypes.StrategyEntry{
|
|
{
|
|
Name: "postgres",
|
|
Status: "backed_up",
|
|
Backup: map[string]interface{}{
|
|
"location": "test/backup.dump",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
destData: map[string][]byte{
|
|
"test/backup.dump": []byte("PGDMP\x00\x00\x00\x00"),
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "invalid dump format",
|
|
plan: &btypes.RecoveryPlan{
|
|
App: "test-app",
|
|
Instance: "test-instance",
|
|
Strategies: []btypes.StrategyEntry{
|
|
{
|
|
Name: "postgres",
|
|
Status: "backed_up",
|
|
Backup: map[string]interface{}{
|
|
"location": "test/invalid.dump",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
destData: map[string][]byte{
|
|
"test/invalid.dump": []byte("not a valid dump"),
|
|
},
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "missing backup location in plan",
|
|
plan: &btypes.RecoveryPlan{
|
|
App: "test-app",
|
|
Instance: "test-instance",
|
|
Strategies: []btypes.StrategyEntry{
|
|
{
|
|
Name: "postgres",
|
|
Status: "backed_up",
|
|
Backup: map[string]interface{}{},
|
|
},
|
|
},
|
|
},
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dest := NewMockPostgresDestination()
|
|
dest.getData = tt.destData
|
|
|
|
err := s.Verify(tt.plan, dest)
|
|
if tt.expectError {
|
|
assert.Error(t, err)
|
|
} else {
|
|
assert.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// writeAppConfig writes a per-app config file at config/apps/{appName}/config.yaml.
|
|
func writeAppConfig(t *testing.T, tmpDir, instanceName, appName, config string) {
|
|
t.Helper()
|
|
appConfigDir := filepath.Join(tmpDir, "instances", instanceName, "config", "apps", appName)
|
|
err := os.MkdirAll(appConfigDir, 0755)
|
|
assert.NoError(t, err)
|
|
err = os.WriteFile(filepath.Join(appConfigDir, "config.yaml"), []byte(config), 0644)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestPostgreSQLStrategy_GetDatabaseName(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config string
|
|
appName string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "flat dbName key",
|
|
config: "dbName: my_database\n",
|
|
appName: "myapp",
|
|
expected: "my_database",
|
|
},
|
|
{
|
|
name: "nested db.name key",
|
|
config: `namespace: e2e-test-app
|
|
db:
|
|
host: postgres
|
|
name: e2e_test_app
|
|
user: e2e_test_app
|
|
`,
|
|
appName: "e2e-test-app",
|
|
expected: "e2e_test_app",
|
|
},
|
|
{
|
|
name: "flat key takes precedence over nested",
|
|
config: `dbName: flat_name
|
|
db:
|
|
name: nested_name
|
|
`,
|
|
appName: "myapp",
|
|
expected: "flat_name",
|
|
},
|
|
{
|
|
name: "no config falls back to appName",
|
|
config: "namespace: myapp\n",
|
|
appName: "myapp",
|
|
expected: "myapp",
|
|
},
|
|
{
|
|
name: "missing app falls back to appName",
|
|
config: "",
|
|
appName: "missing-app",
|
|
expected: "missing-app",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
if tt.config != "" {
|
|
writeAppConfig(t, tmpDir, "test-instance", tt.appName, tt.config)
|
|
}
|
|
s := &PostgreSQLStrategy{dataDir: tmpDir}
|
|
result := s.getDatabaseName("test-instance", tt.appName)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPostgreSQLStrategy_GetAppUser(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config string
|
|
appName string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "flat dbUser key",
|
|
config: "dbUser: my_user\n",
|
|
appName: "myapp",
|
|
expected: "my_user",
|
|
},
|
|
{
|
|
name: "flat dbUsername key",
|
|
config: "dbUsername: my_username\n",
|
|
appName: "myapp",
|
|
expected: "my_username",
|
|
},
|
|
{
|
|
name: "nested db.user key",
|
|
config: `namespace: e2e-test-app
|
|
db:
|
|
host: postgres
|
|
name: e2e_test_app
|
|
user: e2e_test_app
|
|
`,
|
|
appName: "e2e-test-app",
|
|
expected: "e2e_test_app",
|
|
},
|
|
{
|
|
name: "flat key takes precedence over nested",
|
|
config: `dbUser: flat_user
|
|
db:
|
|
user: nested_user
|
|
`,
|
|
appName: "myapp",
|
|
expected: "flat_user",
|
|
},
|
|
{
|
|
name: "no user config falls back to appName",
|
|
config: "namespace: myapp\n",
|
|
appName: "myapp",
|
|
expected: "myapp",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
writeAppConfig(t, tmpDir, "test-instance", tt.appName, tt.config)
|
|
s := &PostgreSQLStrategy{dataDir: tmpDir}
|
|
result := s.getAppUser("test-instance", tt.appName)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|