Skip to content

Commit ff74ca4

Browse files
committed
migrate command
1 parent 9e6e6be commit ff74ca4

20 files changed

Lines changed: 132 additions & 130 deletions

.goreleaser.yaml

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -79,36 +79,6 @@ builds:
7979
- -trimpath
8080
mod_timestamp: "{{ .CommitTimestamp }}"
8181

82-
- id: arcane-migrator
83-
main: ./cmd/migrator/main.go
84-
dir: backend
85-
binary: arcane-migrator
86-
env:
87-
- CGO_ENABLED=0
88-
goos:
89-
- linux
90-
- darwin
91-
goarch:
92-
- amd64
93-
- arm64
94-
- "386"
95-
- arm
96-
goarm:
97-
- "7"
98-
goamd64:
99-
- v1
100-
go386:
101-
- sse2
102-
ldflags:
103-
- -w -s
104-
- -buildid={{ .Version }}
105-
- -X github.com/getarcaneapp/arcane/backend/internal/config.Version={{ .Version }}
106-
- -X github.com/getarcaneapp/arcane/backend/internal/config.Revision={{ .ShortCommit }}
107-
- -X github.com/getarcaneapp/arcane/backend/internal/config.BuildTime={{ .Date }}
108-
flags:
109-
- -trimpath
110-
mod_timestamp: "{{ .CommitTimestamp }}"
111-
11282
- id: arcane-cli
11383
main: .
11484
dir: cli
@@ -149,7 +119,6 @@ archives:
149119
ids:
150120
- arcane
151121
- arcane-agent
152-
- arcane-migrator
153122
formats:
154123
- binary
155124
name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}"
@@ -159,7 +128,6 @@ upx:
159128
ids:
160129
- arcane
161130
- arcane-agent
162-
- arcane-migrator
163131
- arcane-cli
164132
goos:
165133
- linux
@@ -198,7 +166,6 @@ binary_signs:
198166
ids:
199167
- arcane
200168
- arcane-agent
201-
- arcane-migrator
202169
- arcane-cli
203170
signature: '${artifact}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}.sigstore.json'
204171
stdin: "{{ .Env.COSIGN_PASSWORD }}"
@@ -318,7 +285,6 @@ dockers_v2:
318285
- id: arcane-next
319286
ids:
320287
- arcane
321-
- arcane-migrator
322288
dockerfile: docker/next-builds/Dockerfile-static
323289
images:
324290
- "ghcr.io/getarcaneapp/arcane"
@@ -346,7 +312,6 @@ dockers_v2:
346312
- id: arcane-agent-next
347313
ids:
348314
- arcane-agent
349-
- arcane-migrator
350315
dockerfile: docker/next-builds/Dockerfile-agent-static
351316
images:
352317
- "ghcr.io/getarcaneapp/arcane-headless"
@@ -374,7 +339,6 @@ dockers_v2:
374339
- id: arcane-next-distroless
375340
ids:
376341
- arcane
377-
- arcane-migrator
378342
dockerfile: docker/next-builds/Dockerfile-next-distroless
379343
images:
380344
- "ghcr.io/getarcaneapp/arcane"
@@ -402,7 +366,6 @@ dockers_v2:
402366
- id: arcane-agent-next-distroless
403367
ids:
404368
- arcane-agent
405-
- arcane-migrator
406369
dockerfile: docker/next-builds/Dockerfile-agent-distroless
407370
images:
408371
- "ghcr.io/getarcaneapp/arcane-headless"

Justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ migration-manifest version="" output="backend/resources/migration_versions.json"
447447
args+=(--include-version "{{ version }}")
448448
fi
449449
450-
go run ./backend/cmd/migrator/main.go generate-manifest "${args[@]}"
450+
go run -tags exclude_frontend ./backend/cmd migrate generate-manifest "${args[@]}"
451451
452452
# Generate the docs config schema JSON.
453453
[group('docs')]
Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package migrator
1+
package migrate
22

33
import (
44
"context"
@@ -17,25 +17,40 @@ import (
1717
"github.com/getarcaneapp/arcane/backend/pkg/libarcane/startup"
1818
)
1919

20-
func NewCommand(out io.Writer) *cobra.Command {
21-
if out == nil {
22-
out = io.Discard
23-
}
20+
var MigrateCmd = &cobra.Command{
21+
Use: "migrate",
22+
Short: "Manage Arcane database schema migrations",
23+
Version: config.Version,
24+
SilenceUsage: true,
25+
SilenceErrors: true,
26+
}
2427

25-
rootCmd := &cobra.Command{
26-
Use: "arcane-migrator",
28+
func init() {
29+
configureCommand(MigrateCmd, os.Stdout)
30+
}
31+
32+
func newCommand(out io.Writer) *cobra.Command {
33+
cmd := &cobra.Command{
34+
Use: "migrate",
2735
Short: "Manage Arcane database schema migrations",
2836
Version: config.Version,
2937
SilenceUsage: true,
3038
SilenceErrors: true,
3139
}
32-
rootCmd.SetOut(out)
40+
configureCommand(cmd, out)
41+
return cmd
42+
}
43+
44+
func configureCommand(cmd *cobra.Command, out io.Writer) {
45+
if out == nil {
46+
out = io.Discard
47+
}
3348

34-
rootCmd.AddCommand(newStatusCommand(out))
35-
rootCmd.AddCommand(newUpCommand(out))
36-
rootCmd.AddCommand(newDownCommand(out))
37-
rootCmd.AddCommand(newGenerateManifestCommand(out))
38-
return rootCmd
49+
cmd.SetOut(out)
50+
cmd.AddCommand(newStatusCommand(out))
51+
cmd.AddCommand(newUpCommand(out))
52+
cmd.AddCommand(newDownCommand(out))
53+
cmd.AddCommand(newGenerateManifestCommand(out))
3954
}
4055

4156
func newStatusCommand(out io.Writer) *cobra.Command {
@@ -105,7 +120,7 @@ func newDownCommand(out io.Writer) *cobra.Command {
105120
return fmt.Errorf("database schema version %d is dirty; resolve the dirty migration state before downgrading", status.CurrentVersion)
106121
}
107122
if targetMigrationVersion > status.LatestVersion {
108-
return fmt.Errorf("target Arcane version %s requires schema version %d, but this migrator only includes migrations through %d", targetAppVersion, targetMigrationVersion, status.LatestVersion)
123+
return fmt.Errorf("target Arcane version %s requires schema version %d, but this Arcane binary only includes migrations through %d", targetAppVersion, targetMigrationVersion, status.LatestVersion)
109124
}
110125
if !status.HasVersion {
111126
return fmt.Errorf("database has no migration version; start Arcane once to initialize the schema before downgrading")
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package migrator
1+
package migrate
22

33
import (
44
"bytes"
@@ -10,7 +10,7 @@ import (
1010

1111
func TestDownRequiresTarget(t *testing.T) {
1212
var out bytes.Buffer
13-
cmd := NewCommand(&out)
13+
cmd := newCommand(&out)
1414
cmd.SetArgs([]string{"down"})
1515

1616
err := cmd.Execute()
@@ -19,7 +19,8 @@ func TestDownRequiresTarget(t *testing.T) {
1919
}
2020

2121
func TestCommandIncludesMigrationCommands(t *testing.T) {
22-
cmd := NewCommand(nil)
22+
cmd := newCommand(nil)
23+
assert.Equal(t, "migrate", cmd.Name())
2324

2425
commandNames := make(map[string]struct{})
2526
for _, child := range cmd.Commands() {

backend/cli/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/spf13/cobra"
99

1010
"github.com/getarcaneapp/arcane/backend/cli/generate"
11+
"github.com/getarcaneapp/arcane/backend/cli/migrate"
1112
"github.com/getarcaneapp/arcane/backend/cli/upgrade"
1213
"github.com/getarcaneapp/arcane/backend/internal/bootstrap"
1314
"github.com/getarcaneapp/arcane/backend/internal/config"
@@ -39,6 +40,7 @@ func Execute() {
3940

4041
func init() {
4142
rootCmd.AddCommand(upgrade.UpgradeCmd)
43+
rootCmd.AddCommand(migrate.MigrateCmd)
4244
rootCmd.AddCommand(generate.GenerateCmd)
4345
}
4446

backend/cli/upgrade/upgrade.go

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ func findArcaneContainer(ctx context.Context, dockerClient *client.Client) (cont
157157
}
158158

159159
// Legacy label (pre-migration): com.getarcaneapp.arcane.server=true
160-
// NOTE: older agent images also used this label, so we must additionally exclude AGENT_MODE=true.
160+
// NOTE: older agent images also used this label, so we must additionally exclude agent modes.
161161
if isLegacyServerLabel(labels) {
162162
slog.Info("Found Arcane container by legacy label", "id", c.ID[:12], "image", c.Image, "names", c.Names)
163163
return inspect, nil
@@ -197,9 +197,9 @@ func isAgentContainer(inspect container.InspectResponse) bool {
197197
}
198198
}
199199
}
200-
// Legacy agent detection: AGENT_MODE=true in env
200+
// Legacy agent detection: AGENT_MODE=true or EDGE_AGENT=true in env
201201
for _, env := range inspect.Config.Env {
202-
if strings.EqualFold(env, "AGENT_MODE=true") {
202+
if strings.EqualFold(env, "AGENT_MODE=true") || strings.EqualFold(env, "EDGE_AGENT=true") {
203203
return true
204204
}
205205
}
@@ -419,16 +419,12 @@ func upgradeContainer(ctx context.Context, dockerClient *client.Client, oldConta
419419
return fmt.Errorf("stop old container: %w", err)
420420
}
421421

422-
if shouldRunMigratorForContainerInternal(oldContainer) {
423-
fmt.Println("PROGRESS:73:Running database migrator")
424-
slog.Info("Running database migrator before starting new container", "image", newImage)
425-
if err := runMigratorContainerInternal(ctx, dockerClient, oldContainer, newImage, hostConfig, networkConfig, apiVersion); err != nil {
426-
_, _ = dockerClient.ContainerStart(ctx, oldContainer.ID, client.ContainerStartOptions{})
427-
_, _ = dockerClient.ContainerRename(ctx, oldContainer.ID, client.ContainerRenameOptions{NewName: originalName})
428-
return fmt.Errorf("run database migrator: %w", err)
429-
}
430-
} else {
431-
slog.Info("Skipping database migrator for non-server Arcane target", "container", originalName)
422+
fmt.Println("PROGRESS:73:Running database migrator")
423+
slog.Info("Running database migrator before starting new container", "image", newImage)
424+
if err := runMigratorContainerInternal(ctx, dockerClient, oldContainer, newImage, hostConfig, networkConfig, apiVersion); err != nil {
425+
_, _ = dockerClient.ContainerStart(ctx, oldContainer.ID, client.ContainerStartOptions{})
426+
_, _ = dockerClient.ContainerRename(ctx, oldContainer.ID, client.ContainerRenameOptions{NewName: originalName})
427+
return fmt.Errorf("run database migrator: %w", err)
432428
}
433429

434430
fmt.Println("PROGRESS:75:Creating new container")
@@ -472,14 +468,6 @@ func upgradeContainer(ctx context.Context, dockerClient *client.Client, oldConta
472468
return nil
473469
}
474470

475-
func shouldRunMigratorForContainerInternal(cont container.InspectResponse) bool {
476-
if cont.Config == nil {
477-
return false
478-
}
479-
480-
return !isAgentContainer(cont)
481-
}
482-
483471
func runMigratorContainerInternal(
484472
ctx context.Context,
485473
dockerClient *client.Client,
@@ -495,8 +483,8 @@ func runMigratorContainerInternal(
495483

496484
migratorConfig := *oldContainer.Config
497485
migratorConfig.Image = image
498-
migratorConfig.Entrypoint = []string{"/app/arcane-migrator"}
499-
migratorConfig.Cmd = []string{"up"}
486+
migratorConfig.Entrypoint = []string{migrationBinaryPathForContainerInternal(oldContainer)}
487+
migratorConfig.Cmd = []string{"migrate", "up"}
500488
migratorConfig.ExposedPorts = nil
501489
migratorConfig.Healthcheck = nil
502490

@@ -552,6 +540,14 @@ func runMigratorContainerInternal(
552540
return nil
553541
}
554542

543+
func migrationBinaryPathForContainerInternal(cont container.InspectResponse) string {
544+
if isAgentContainer(cont) {
545+
return "/app/arcane-agent"
546+
}
547+
548+
return "/app/arcane"
549+
}
550+
555551
// looksLikeContainerID checks if a string looks like a Docker container ID
556552
// (12 or 64 lowercase hex characters, which Docker auto-generates as hostnames)
557553
func looksLikeContainerID(s string) bool {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package upgrade
2+
3+
import (
4+
"testing"
5+
6+
"github.com/moby/moby/api/types/container"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestMigrationBinaryPathForManagerContainer(t *testing.T) {
11+
cont := container.InspectResponse{
12+
Config: &container.Config{
13+
Labels: map[string]string{
14+
"com.getarcaneapp.arcane": "true",
15+
},
16+
},
17+
}
18+
19+
assert.Equal(t, "/app/arcane", migrationBinaryPathForContainerInternal(cont))
20+
}
21+
22+
func TestMigrationBinaryPathForAgentContainer(t *testing.T) {
23+
tests := []struct {
24+
name string
25+
cont container.InspectResponse
26+
}{
27+
{
28+
name: "agent label",
29+
cont: container.InspectResponse{
30+
Config: &container.Config{
31+
Labels: map[string]string{
32+
"com.getarcaneapp.arcane.agent": "true",
33+
},
34+
},
35+
},
36+
},
37+
{
38+
name: "direct agent env",
39+
cont: container.InspectResponse{
40+
Config: &container.Config{
41+
Env: []string{"AGENT_MODE=true"},
42+
},
43+
},
44+
},
45+
{
46+
name: "edge agent env",
47+
cont: container.InspectResponse{
48+
Config: &container.Config{
49+
Env: []string{"EDGE_AGENT=true"},
50+
},
51+
},
52+
},
53+
}
54+
55+
for _, tt := range tests {
56+
t.Run(tt.name, func(t *testing.T) {
57+
assert.Equal(t, "/app/arcane-agent", migrationBinaryPathForContainerInternal(tt.cont))
58+
})
59+
}
60+
}

backend/cmd/migrator/main.go

Lines changed: 0 additions & 19 deletions
This file was deleted.

0 commit comments

Comments
 (0)