mirror of
https://github.com/ipfs/kubo.git
synced 2026-02-21 10:27:46 +08:00
Some checks failed
CodeQL / codeql (push) Has been cancelled
Docker Build / docker-build (push) Has been cancelled
Gateway Conformance / gateway-conformance (push) Has been cancelled
Gateway Conformance / gateway-conformance-libp2p-experiment (push) Has been cancelled
Go Build / go-build (push) Has been cancelled
Go Check / go-check (push) Has been cancelled
Go Lint / go-lint (push) Has been cancelled
Go Test / go-test (push) Has been cancelled
Interop / interop-prep (push) Has been cancelled
Sharness / sharness-test (push) Has been cancelled
Spell Check / spellcheck (push) Has been cancelled
Interop / helia-interop (push) Has been cancelled
Interop / ipfs-webui (push) Has been cancelled
https://github.com/ipfs/kubo/pull/10883 https://github.com/ipshipyard/config.ipfs-mainnet.org/issues/3 --------- Co-authored-by: gammazero <gammazero@users.noreply.github.com>
685 lines
25 KiB
Go
685 lines
25 KiB
Go
package migrations
|
|
|
|
// NOTE: These migration tests require the local Kubo binary (built with 'make build') to be in PATH.
|
|
// The tests migrate from repo version 16 to 17, which requires Kubo version 0.37.0+ (expects repo v17).
|
|
// If using system ipfs binary v0.36.0 or older (expects repo v16), no migration will be triggered.
|
|
//
|
|
// To run these tests successfully:
|
|
// export PATH="$(pwd)/cmd/ipfs:$PATH"
|
|
// go test ./test/cli/migrations/
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/ipfs/kubo/test/cli/harness"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestMigration16To17(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Primary tests using 'ipfs daemon --migrate' command (default in Docker)
|
|
t.Run("daemon migrate: forward migration with auto values", testDaemonMigrationWithAuto)
|
|
t.Run("daemon migrate: forward migration without auto values", testDaemonMigrationWithoutAuto)
|
|
t.Run("daemon migrate: corrupted config handling", testDaemonCorruptedConfigHandling)
|
|
t.Run("daemon migrate: missing fields handling", testDaemonMissingFieldsHandling)
|
|
|
|
// Comparison tests using 'ipfs repo migrate' command
|
|
t.Run("repo migrate: forward migration with auto values", testRepoMigrationWithAuto)
|
|
t.Run("repo migrate: backward migration", testRepoBackwardMigration)
|
|
}
|
|
|
|
// =============================================================================
|
|
// PRIMARY TESTS: 'ipfs daemon --migrate' command (default in Docker)
|
|
//
|
|
// These tests exercise the primary migration path used in production Docker
|
|
// containers where --migrate is enabled by default. This covers:
|
|
// - Normal forward migration scenarios
|
|
// - Error handling with corrupted configs
|
|
// - Migration with minimal/missing config fields
|
|
// =============================================================================
|
|
|
|
func testDaemonMigrationWithAuto(t *testing.T) {
|
|
// TEST: Forward migration using 'ipfs daemon --migrate' command (PRIMARY)
|
|
// Use static v16 repo fixture from real Kubo 0.36 `ipfs init`
|
|
// NOTE: This test may need to be revised/updated once repo version 18 is released,
|
|
// at that point only keep tests that use 'ipfs repo migrate'
|
|
node := setupStaticV16Repo(t)
|
|
|
|
configPath := filepath.Join(node.Dir, "config")
|
|
versionPath := filepath.Join(node.Dir, "version")
|
|
|
|
// Static fixture already uses port 0 for random port assignment - no config update needed
|
|
|
|
// Run migration using daemon --migrate (automatic during daemon startup)
|
|
// This is the primary method used in Docker containers
|
|
// Monitor output until daemon is ready, then shut it down gracefully
|
|
stdoutOutput, migrationSuccess := runDaemonMigrationWithMonitoring(t, node)
|
|
|
|
// Debug: Print the actual output
|
|
t.Logf("Daemon output:\n%s", stdoutOutput)
|
|
|
|
// Verify migration was successful based on monitoring
|
|
require.True(t, migrationSuccess, "Migration should have been successful")
|
|
require.Contains(t, stdoutOutput, "applying 16-to-17 repo migration", "Migration should have been triggered")
|
|
require.Contains(t, stdoutOutput, "Migration 16 to 17 succeeded", "Migration should have completed successfully")
|
|
|
|
// Verify version was updated to 17
|
|
versionData, err := os.ReadFile(versionPath)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "17", strings.TrimSpace(string(versionData)), "Version should be updated to 17")
|
|
|
|
// Verify migration results using DRY helper
|
|
helper := NewMigrationTestHelper(t, configPath)
|
|
helper.RequireAutoConfDefaults().
|
|
RequireArrayContains("Bootstrap", "auto").
|
|
RequireArrayLength("Bootstrap", 1). // Should only contain "auto" when all peers were defaults
|
|
RequireArrayContains("Routing.DelegatedRouters", "auto").
|
|
RequireArrayContains("Ipns.DelegatedPublishers", "auto")
|
|
|
|
// DNS resolver in static fixture should be empty, so "." should be set to "auto"
|
|
helper.RequireFieldEquals("DNS.Resolvers[.]", "auto")
|
|
}
|
|
|
|
func testDaemonMigrationWithoutAuto(t *testing.T) {
|
|
// TEST: Forward migration using 'ipfs daemon --migrate' command (PRIMARY)
|
|
// Test migration of a config that already has some custom values
|
|
// NOTE: This test may need to be revised/updated once repo version 18 is released,
|
|
// at that point only keep tests that use 'ipfs repo migrate'
|
|
// Should preserve existing settings and only add missing ones
|
|
node := setupStaticV16Repo(t)
|
|
|
|
// Modify the static fixture to add some custom values for testing mixed scenarios
|
|
configPath := filepath.Join(node.Dir, "config")
|
|
|
|
// Read existing config from static fixture
|
|
var v16Config map[string]interface{}
|
|
configData, err := os.ReadFile(configPath)
|
|
require.NoError(t, err)
|
|
require.NoError(t, json.Unmarshal(configData, &v16Config))
|
|
|
|
// Add custom DNS resolver that should be preserved
|
|
if v16Config["DNS"] == nil {
|
|
v16Config["DNS"] = map[string]interface{}{}
|
|
}
|
|
dnsSection := v16Config["DNS"].(map[string]interface{})
|
|
dnsSection["Resolvers"] = map[string]string{
|
|
".": "https://custom-dns.example.com/dns-query",
|
|
"eth.": "https://dns.eth.limo/dns-query", // This is a default that will be replaced with "auto"
|
|
}
|
|
|
|
// Write modified config back
|
|
modifiedConfigData, err := json.MarshalIndent(v16Config, "", " ")
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(configPath, modifiedConfigData, 0644))
|
|
|
|
// Static fixture already uses port 0 for random port assignment - no config update needed
|
|
|
|
// Run migration using daemon --migrate command (this is a daemon test)
|
|
// Monitor output until daemon is ready, then shut it down gracefully
|
|
stdoutOutput, migrationSuccess := runDaemonMigrationWithMonitoring(t, node)
|
|
|
|
// Verify migration was successful based on monitoring
|
|
require.True(t, migrationSuccess, "Migration should have been successful")
|
|
require.Contains(t, stdoutOutput, "applying 16-to-17 repo migration", "Migration should have been triggered")
|
|
require.Contains(t, stdoutOutput, "Migration 16 to 17 succeeded", "Migration should have completed successfully")
|
|
|
|
// Verify migration results: custom values preserved alongside "auto"
|
|
helper := NewMigrationTestHelper(t, configPath)
|
|
helper.RequireAutoConfDefaults().
|
|
RequireArrayContains("Bootstrap", "auto").
|
|
RequireFieldEquals("DNS.Resolvers[.]", "https://custom-dns.example.com/dns-query")
|
|
|
|
// Check that eth. resolver was replaced with "auto" since it uses a default URL
|
|
helper.RequireFieldEquals("DNS.Resolvers[eth.]", "auto").
|
|
RequireFieldEquals("DNS.Resolvers[.]", "https://custom-dns.example.com/dns-query")
|
|
}
|
|
|
|
// =============================================================================
|
|
// Tests using 'ipfs daemon --migrate' command
|
|
// =============================================================================
|
|
|
|
// Test helper structs and functions for cleaner, more DRY tests
|
|
|
|
type ConfigField struct {
|
|
Path string
|
|
Expected interface{}
|
|
Message string
|
|
}
|
|
|
|
type MigrationTestHelper struct {
|
|
t *testing.T
|
|
config map[string]interface{}
|
|
}
|
|
|
|
func NewMigrationTestHelper(t *testing.T, configPath string) *MigrationTestHelper {
|
|
var config map[string]interface{}
|
|
configData, err := os.ReadFile(configPath)
|
|
require.NoError(t, err)
|
|
require.NoError(t, json.Unmarshal(configData, &config))
|
|
|
|
return &MigrationTestHelper{t: t, config: config}
|
|
}
|
|
|
|
func (h *MigrationTestHelper) RequireFieldExists(path string) *MigrationTestHelper {
|
|
value := h.getNestedValue(path)
|
|
require.NotNil(h.t, value, "Field %s should exist", path)
|
|
return h
|
|
}
|
|
|
|
func (h *MigrationTestHelper) RequireFieldEquals(path string, expected interface{}) *MigrationTestHelper {
|
|
value := h.getNestedValue(path)
|
|
require.Equal(h.t, expected, value, "Field %s should equal %v", path, expected)
|
|
return h
|
|
}
|
|
|
|
func (h *MigrationTestHelper) RequireArrayContains(path string, expected interface{}) *MigrationTestHelper {
|
|
value := h.getNestedValue(path)
|
|
require.IsType(h.t, []interface{}{}, value, "Field %s should be an array", path)
|
|
array := value.([]interface{})
|
|
require.Contains(h.t, array, expected, "Array %s should contain %v", path, expected)
|
|
return h
|
|
}
|
|
|
|
func (h *MigrationTestHelper) RequireArrayLength(path string, expectedLen int) *MigrationTestHelper {
|
|
value := h.getNestedValue(path)
|
|
require.IsType(h.t, []interface{}{}, value, "Field %s should be an array", path)
|
|
array := value.([]interface{})
|
|
require.Len(h.t, array, expectedLen, "Array %s should have length %d", path, expectedLen)
|
|
return h
|
|
}
|
|
|
|
func (h *MigrationTestHelper) RequireArrayDoesNotContain(path string, notExpected interface{}) *MigrationTestHelper {
|
|
value := h.getNestedValue(path)
|
|
require.IsType(h.t, []interface{}{}, value, "Field %s should be an array", path)
|
|
array := value.([]interface{})
|
|
require.NotContains(h.t, array, notExpected, "Array %s should not contain %v", path, notExpected)
|
|
return h
|
|
}
|
|
|
|
func (h *MigrationTestHelper) RequireFieldAbsent(path string) *MigrationTestHelper {
|
|
value := h.getNestedValue(path)
|
|
require.Nil(h.t, value, "Field %s should not exist", path)
|
|
return h
|
|
}
|
|
|
|
func (h *MigrationTestHelper) RequireAutoConfDefaults() *MigrationTestHelper {
|
|
// AutoConf section should exist but be empty (using implicit defaults)
|
|
return h.RequireFieldExists("AutoConf").
|
|
RequireFieldAbsent("AutoConf.Enabled"). // Should use implicit default (true)
|
|
RequireFieldAbsent("AutoConf.URL"). // Should use implicit default (mainnet URL)
|
|
RequireFieldAbsent("AutoConf.RefreshInterval"). // Should use implicit default (24h)
|
|
RequireFieldAbsent("AutoConf.TLSInsecureSkipVerify") // Should use implicit default (false)
|
|
}
|
|
|
|
func (h *MigrationTestHelper) RequireAutoFieldsSetToAuto() *MigrationTestHelper {
|
|
return h.RequireArrayContains("Bootstrap", "auto").
|
|
RequireFieldEquals("DNS.Resolvers[.]", "auto").
|
|
RequireArrayContains("Routing.DelegatedRouters", "auto").
|
|
RequireArrayContains("Ipns.DelegatedPublishers", "auto")
|
|
}
|
|
|
|
func (h *MigrationTestHelper) RequireNoAutoValues() *MigrationTestHelper {
|
|
// Check Bootstrap if it exists
|
|
if h.getNestedValue("Bootstrap") != nil {
|
|
h.RequireArrayDoesNotContain("Bootstrap", "auto")
|
|
}
|
|
|
|
// Check DNS.Resolvers if it exists
|
|
if h.getNestedValue("DNS.Resolvers") != nil {
|
|
h.RequireMapDoesNotContainValue("DNS.Resolvers", "auto")
|
|
}
|
|
|
|
// Check Routing.DelegatedRouters if it exists
|
|
if h.getNestedValue("Routing.DelegatedRouters") != nil {
|
|
h.RequireArrayDoesNotContain("Routing.DelegatedRouters", "auto")
|
|
}
|
|
|
|
// Check Ipns.DelegatedPublishers if it exists
|
|
if h.getNestedValue("Ipns.DelegatedPublishers") != nil {
|
|
h.RequireArrayDoesNotContain("Ipns.DelegatedPublishers", "auto")
|
|
}
|
|
|
|
return h
|
|
}
|
|
|
|
func (h *MigrationTestHelper) RequireMapDoesNotContainValue(path string, notExpected interface{}) *MigrationTestHelper {
|
|
value := h.getNestedValue(path)
|
|
require.IsType(h.t, map[string]interface{}{}, value, "Field %s should be a map", path)
|
|
mapValue := value.(map[string]interface{})
|
|
for k, v := range mapValue {
|
|
require.NotEqual(h.t, notExpected, v, "Map %s[%s] should not equal %v", path, k, notExpected)
|
|
}
|
|
return h
|
|
}
|
|
|
|
func (h *MigrationTestHelper) getNestedValue(path string) interface{} {
|
|
segments := h.parseKuboConfigPath(path)
|
|
current := interface{}(h.config)
|
|
|
|
for _, segment := range segments {
|
|
switch segment.Type {
|
|
case "field":
|
|
switch v := current.(type) {
|
|
case map[string]interface{}:
|
|
current = v[segment.Key]
|
|
default:
|
|
return nil
|
|
}
|
|
case "mapKey":
|
|
switch v := current.(type) {
|
|
case map[string]interface{}:
|
|
current = v[segment.Key]
|
|
default:
|
|
return nil
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
if current == nil {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return current
|
|
}
|
|
|
|
type PathSegment struct {
|
|
Type string // "field" or "mapKey"
|
|
Key string
|
|
}
|
|
|
|
func (h *MigrationTestHelper) parseKuboConfigPath(path string) []PathSegment {
|
|
var segments []PathSegment
|
|
|
|
// Split path into parts, respecting bracket boundaries
|
|
parts := h.splitKuboConfigPath(path)
|
|
|
|
for _, part := range parts {
|
|
if strings.Contains(part, "[") && strings.HasSuffix(part, "]") {
|
|
// Handle field[key] notation
|
|
bracketStart := strings.Index(part, "[")
|
|
fieldName := part[:bracketStart]
|
|
mapKey := part[bracketStart+1 : len(part)-1] // Remove [ and ]
|
|
|
|
// Add field segment if present
|
|
if fieldName != "" {
|
|
segments = append(segments, PathSegment{Type: "field", Key: fieldName})
|
|
}
|
|
// Add map key segment
|
|
segments = append(segments, PathSegment{Type: "mapKey", Key: mapKey})
|
|
} else {
|
|
// Regular field access
|
|
if part != "" {
|
|
segments = append(segments, PathSegment{Type: "field", Key: part})
|
|
}
|
|
}
|
|
}
|
|
|
|
return segments
|
|
}
|
|
|
|
// splitKuboConfigPath splits a path on dots, but preserves bracket sections intact
|
|
func (h *MigrationTestHelper) splitKuboConfigPath(path string) []string {
|
|
var parts []string
|
|
var current strings.Builder
|
|
inBrackets := false
|
|
|
|
for _, r := range path {
|
|
switch r {
|
|
case '[':
|
|
inBrackets = true
|
|
current.WriteRune(r)
|
|
case ']':
|
|
inBrackets = false
|
|
current.WriteRune(r)
|
|
case '.':
|
|
if inBrackets {
|
|
// Inside brackets, preserve the dot
|
|
current.WriteRune(r)
|
|
} else {
|
|
// Outside brackets, split here
|
|
if current.Len() > 0 {
|
|
parts = append(parts, current.String())
|
|
current.Reset()
|
|
}
|
|
}
|
|
default:
|
|
current.WriteRune(r)
|
|
}
|
|
}
|
|
|
|
// Add final part if any
|
|
if current.Len() > 0 {
|
|
parts = append(parts, current.String())
|
|
}
|
|
|
|
return parts
|
|
}
|
|
|
|
// setupStaticV16Repo creates a test node using static v16 repo fixture from real Kubo 0.36 `ipfs init`
|
|
// This ensures tests remain stable regardless of future changes to the IPFS binary
|
|
// Each test gets its own copy in a temporary directory to allow modifications
|
|
func setupStaticV16Repo(t *testing.T) *harness.Node {
|
|
// Get absolute path to static v16 repo fixture
|
|
v16FixturePath := "testdata/v16-repo"
|
|
|
|
// Create a temporary test directory - each test gets its own copy
|
|
// Use ./tmp.DELETEME/ as requested by user instead of /tmp/
|
|
tmpDir := filepath.Join("tmp.DELETEME", "migration-test-"+t.Name())
|
|
require.NoError(t, os.MkdirAll(tmpDir, 0755))
|
|
t.Cleanup(func() { os.RemoveAll(tmpDir) })
|
|
|
|
// Convert to absolute path for harness
|
|
absTmpDir, err := filepath.Abs(tmpDir)
|
|
require.NoError(t, err)
|
|
|
|
// Use the built binary (should be in PATH)
|
|
node := harness.BuildNode("ipfs", absTmpDir, 0)
|
|
|
|
// Replace IPFS_PATH with static fixture files to test directory (creates independent copy per test)
|
|
cloneStaticRepoFixture(t, v16FixturePath, node.Dir)
|
|
|
|
return node
|
|
}
|
|
|
|
// cloneStaticRepoFixture recursively copies the v16 repo fixture to the target directory
|
|
// It completely removes the target directory contents before copying to ensure no extra files remain
|
|
func cloneStaticRepoFixture(t *testing.T, srcPath, dstPath string) {
|
|
srcInfo, err := os.Stat(srcPath)
|
|
require.NoError(t, err)
|
|
|
|
if srcInfo.IsDir() {
|
|
// Completely remove destination directory and all contents
|
|
require.NoError(t, os.RemoveAll(dstPath))
|
|
// Create fresh destination directory
|
|
require.NoError(t, os.MkdirAll(dstPath, srcInfo.Mode()))
|
|
|
|
// Read source directory
|
|
entries, err := os.ReadDir(srcPath)
|
|
require.NoError(t, err)
|
|
|
|
// Copy each entry recursively
|
|
for _, entry := range entries {
|
|
srcEntryPath := filepath.Join(srcPath, entry.Name())
|
|
dstEntryPath := filepath.Join(dstPath, entry.Name())
|
|
cloneStaticRepoFixture(t, srcEntryPath, dstEntryPath)
|
|
}
|
|
} else {
|
|
// Copy file (destination directory should already be clean from parent call)
|
|
srcFile, err := os.Open(srcPath)
|
|
require.NoError(t, err)
|
|
defer srcFile.Close()
|
|
|
|
dstFile, err := os.Create(dstPath)
|
|
require.NoError(t, err)
|
|
defer dstFile.Close()
|
|
|
|
_, err = io.Copy(dstFile, srcFile)
|
|
require.NoError(t, err)
|
|
|
|
// Copy file permissions
|
|
require.NoError(t, dstFile.Chmod(srcInfo.Mode()))
|
|
}
|
|
}
|
|
|
|
// Placeholder stubs for new test functions - to be implemented
|
|
func testDaemonCorruptedConfigHandling(t *testing.T) {
|
|
// TEST: Error handling using 'ipfs daemon --migrate' command with corrupted config (PRIMARY)
|
|
// Test what happens when config file is corrupted during migration
|
|
// NOTE: This test may need to be revised/updated once repo version 18 is released,
|
|
// at that point only keep tests that use 'ipfs repo migrate'
|
|
node := setupStaticV16Repo(t)
|
|
|
|
// Create corrupted config
|
|
configPath := filepath.Join(node.Dir, "config")
|
|
corruptedJson := `{"Bootstrap": [invalid json}`
|
|
require.NoError(t, os.WriteFile(configPath, []byte(corruptedJson), 0644))
|
|
|
|
// Write version file indicating v16
|
|
versionPath := filepath.Join(node.Dir, "version")
|
|
require.NoError(t, os.WriteFile(versionPath, []byte("16"), 0644))
|
|
|
|
// Run daemon with --migrate flag - this should fail gracefully
|
|
result := node.RunIPFS("daemon", "--migrate")
|
|
|
|
// Verify graceful failure handling
|
|
// The daemon should fail but migration error should be clear
|
|
errorOutput := result.Stderr.String() + result.Stdout.String()
|
|
require.True(t, strings.Contains(errorOutput, "json") || strings.Contains(errorOutput, "invalid character"), "Error should mention JSON parsing issue")
|
|
|
|
// Verify atomic failure: version and config should remain unchanged
|
|
versionData, err := os.ReadFile(versionPath)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "16", strings.TrimSpace(string(versionData)), "Version should remain unchanged after failed migration")
|
|
|
|
originalContent, err := os.ReadFile(configPath)
|
|
require.NoError(t, err)
|
|
require.Equal(t, corruptedJson, string(originalContent), "Original config should be unchanged after failed migration")
|
|
}
|
|
|
|
func testDaemonMissingFieldsHandling(t *testing.T) {
|
|
// TEST: Migration using 'ipfs daemon --migrate' command with minimal config (PRIMARY)
|
|
// Test migration when config is missing expected fields
|
|
// NOTE: This test may need to be revised/updated once repo version 18 is released,
|
|
// at that point only keep tests that use 'ipfs repo migrate'
|
|
node := setupStaticV16Repo(t)
|
|
|
|
// The static fixture already has all required fields, use it as-is
|
|
configPath := filepath.Join(node.Dir, "config")
|
|
versionPath := filepath.Join(node.Dir, "version")
|
|
|
|
// Static fixture already uses port 0 for random port assignment - no config update needed
|
|
|
|
// Run daemon migration
|
|
stdoutOutput, migrationSuccess := runDaemonMigrationWithMonitoring(t, node)
|
|
|
|
// Verify migration was successful
|
|
require.True(t, migrationSuccess, "Migration should have been successful")
|
|
require.Contains(t, stdoutOutput, "applying 16-to-17 repo migration", "Migration should have been triggered")
|
|
require.Contains(t, stdoutOutput, "Migration 16 to 17 succeeded", "Migration should have completed successfully")
|
|
|
|
// Verify version was updated
|
|
versionData, err := os.ReadFile(versionPath)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "17", strings.TrimSpace(string(versionData)), "Version should be updated to 17")
|
|
|
|
// Verify migration adds all required fields to minimal config
|
|
NewMigrationTestHelper(t, configPath).
|
|
RequireAutoConfDefaults().
|
|
RequireAutoFieldsSetToAuto().
|
|
RequireFieldExists("Identity.PeerID") // Original identity preserved from static fixture
|
|
}
|
|
|
|
// =============================================================================
|
|
// COMPARISON TESTS: 'ipfs repo migrate' command
|
|
//
|
|
// These tests verify that repo migrate produces equivalent results to
|
|
// daemon migrate, and test scenarios specific to repo migrate like
|
|
// backward migration (which daemon doesn't support).
|
|
// =============================================================================
|
|
|
|
func testRepoMigrationWithAuto(t *testing.T) {
|
|
// TEST: Forward migration using 'ipfs repo migrate' command (COMPARISON)
|
|
// Simple comparison test to verify repo migrate produces same results as daemon migrate
|
|
node := setupStaticV16Repo(t)
|
|
|
|
// Use static fixture as-is
|
|
configPath := filepath.Join(node.Dir, "config")
|
|
|
|
// Run migration using 'ipfs repo migrate' command
|
|
result := node.RunIPFS("repo", "migrate")
|
|
require.Empty(t, result.Stderr.String(), "Migration should succeed without errors")
|
|
|
|
// Verify same results as daemon migrate
|
|
helper := NewMigrationTestHelper(t, configPath)
|
|
helper.RequireAutoConfDefaults().
|
|
RequireArrayContains("Bootstrap", "auto").
|
|
RequireArrayContains("Routing.DelegatedRouters", "auto").
|
|
RequireArrayContains("Ipns.DelegatedPublishers", "auto").
|
|
RequireFieldEquals("DNS.Resolvers[.]", "auto")
|
|
}
|
|
|
|
func testRepoBackwardMigration(t *testing.T) {
|
|
// TEST: Backward migration using 'ipfs repo migrate --to=16 --allow-downgrade' command
|
|
// This is kept as repo migrate since daemon doesn't support backward migration
|
|
node := setupStaticV16Repo(t)
|
|
|
|
// Use static fixture as-is
|
|
configPath := filepath.Join(node.Dir, "config")
|
|
versionPath := filepath.Join(node.Dir, "version")
|
|
|
|
// First run forward migration to get to v17
|
|
result := node.RunIPFS("repo", "migrate")
|
|
require.Empty(t, result.Stderr.String(), "Forward migration should succeed")
|
|
|
|
// Verify we're at v17
|
|
versionData, err := os.ReadFile(versionPath)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "17", strings.TrimSpace(string(versionData)), "Should be at version 17 after forward migration")
|
|
|
|
// Now run reverse migration back to v16
|
|
result = node.RunIPFS("repo", "migrate", "--to=16", "--allow-downgrade")
|
|
require.Empty(t, result.Stderr.String(), "Reverse migration should succeed")
|
|
|
|
// Verify version was downgraded to 16
|
|
versionData, err = os.ReadFile(versionPath)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "16", strings.TrimSpace(string(versionData)), "Version should be downgraded to 16")
|
|
|
|
// Verify backward migration results: AutoConf removed and no "auto" values remain
|
|
NewMigrationTestHelper(t, configPath).
|
|
RequireFieldAbsent("AutoConf").
|
|
RequireNoAutoValues()
|
|
}
|
|
|
|
// runDaemonMigrationWithMonitoring starts daemon --migrate, monitors output until "Daemon is ready",
|
|
// then gracefully shuts down the daemon and returns the captured output and success status.
|
|
// This is a generic helper that can monitor for any migration patterns.
|
|
func runDaemonMigrationWithMonitoring(t *testing.T, node *harness.Node) (string, bool) {
|
|
// Use specific patterns for 16-to-17 migration
|
|
return runDaemonWithMigrationMonitoring(t, node, "applying 16-to-17 repo migration", "Migration 16 to 17 succeeded")
|
|
}
|
|
|
|
// runDaemonWithMigrationMonitoring is a generic helper for running daemon --migrate and monitoring output.
|
|
// It waits for the daemon to be ready, then shuts it down gracefully.
|
|
// migrationPattern: pattern to detect migration started (e.g., "applying X-to-Y repo migration")
|
|
// successPattern: pattern to detect migration succeeded (e.g., "Migration X to Y succeeded")
|
|
// Returns the stdout output and whether both patterns were detected.
|
|
func runDaemonWithMigrationMonitoring(t *testing.T, node *harness.Node, migrationPattern, successPattern string) (string, bool) {
|
|
// Create context with timeout as safety net
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
// Set up daemon command with output monitoring
|
|
cmd := exec.CommandContext(ctx, node.IPFSBin, "daemon", "--migrate")
|
|
cmd.Dir = node.Dir
|
|
|
|
// Set environment (especially IPFS_PATH)
|
|
for k, v := range node.Runner.Env {
|
|
cmd.Env = append(cmd.Env, k+"="+v)
|
|
}
|
|
|
|
// Set up pipes for output monitoring
|
|
stdout, err := cmd.StdoutPipe()
|
|
require.NoError(t, err)
|
|
stderr, err := cmd.StderrPipe()
|
|
require.NoError(t, err)
|
|
|
|
// Start the daemon
|
|
err = cmd.Start()
|
|
require.NoError(t, err)
|
|
|
|
var allOutput strings.Builder
|
|
var migrationDetected, migrationSucceeded, daemonReady bool
|
|
|
|
// Monitor stdout for completion signals
|
|
scanner := bufio.NewScanner(stdout)
|
|
go func() {
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
allOutput.WriteString(line + "\n")
|
|
|
|
// Check for migration messages
|
|
if migrationPattern != "" && strings.Contains(line, migrationPattern) {
|
|
migrationDetected = true
|
|
}
|
|
if successPattern != "" && strings.Contains(line, successPattern) {
|
|
migrationSucceeded = true
|
|
}
|
|
if strings.Contains(line, "Daemon is ready") {
|
|
daemonReady = true
|
|
break // Exit monitoring loop
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Also monitor stderr (but don't use it for completion detection)
|
|
go func() {
|
|
stderrScanner := bufio.NewScanner(stderr)
|
|
for stderrScanner.Scan() {
|
|
line := stderrScanner.Text()
|
|
allOutput.WriteString("STDERR: " + line + "\n")
|
|
}
|
|
}()
|
|
|
|
// Wait for daemon ready signal or timeout
|
|
ticker := time.NewTicker(100 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
// Timeout - kill the process
|
|
if cmd.Process != nil {
|
|
_ = cmd.Process.Kill()
|
|
}
|
|
t.Logf("Daemon migration timed out after 60 seconds")
|
|
return allOutput.String(), false
|
|
|
|
case <-ticker.C:
|
|
if daemonReady {
|
|
// Daemon is ready - shut it down gracefully
|
|
shutdownCmd := exec.Command(node.IPFSBin, "shutdown")
|
|
shutdownCmd.Dir = node.Dir
|
|
for k, v := range node.Runner.Env {
|
|
shutdownCmd.Env = append(shutdownCmd.Env, k+"="+v)
|
|
}
|
|
|
|
if err := shutdownCmd.Run(); err != nil {
|
|
t.Logf("Warning: ipfs shutdown failed: %v", err)
|
|
// Force kill if graceful shutdown fails
|
|
if cmd.Process != nil {
|
|
_ = cmd.Process.Kill()
|
|
}
|
|
}
|
|
|
|
// Wait for process to exit
|
|
_ = cmd.Wait()
|
|
|
|
// Return success if we detected migration
|
|
success := migrationDetected && migrationSucceeded
|
|
return allOutput.String(), success
|
|
}
|
|
|
|
// Check if process has exited (e.g., due to startup failure after migration)
|
|
if cmd.ProcessState != nil && cmd.ProcessState.Exited() {
|
|
// Process exited - migration may have completed but daemon failed to start
|
|
// This is expected for corrupted config tests
|
|
success := migrationDetected && migrationSucceeded
|
|
return allOutput.String(), success
|
|
}
|
|
}
|
|
}
|
|
}
|