Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ build-prod osmanage:
build-dev:
go build -o osmanage ./cmd/osmanage

gen-proto:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
export PATH="$$PATH:$$(go env GOPATH)/bin"; \
protoc --go_out=. --go_opt=module=github.com/OpenSlides/openslides-cli --go-grpc_out=. --go-grpc_opt=module=github.com/OpenSlides/openslides-cli proto/osmanage.proto

# Tests / Linting

Expand Down
14 changes: 1 addition & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,7 @@ Commands for interacting with the OpenSlides backend API.
Manage OpenSlides database migrations.

**Subcommands:**
- `migrate`: Run migrations on auxiliary tables
- `finalize`: Apply migrations to live tables
- `migrate`: Run migrations on live tables
- `reset`: Reset unapplied migrations
- `stats`: Show migration statistics
- `progress`: Check running migration progress
Expand All @@ -235,17 +234,6 @@ osmanage migrations stats \
osmanage migrations migrate \
--address localhost:9002 \
--password-file ./secrets/internal_auth_password

# Apply migrations
osmanage migrations finalize \
--address localhost:9002 \
--password-file ./secrets/internal_auth_password

# Apply without progress output
osmanage migrations finalize \
--address localhost:9002 \
--password-file ./secrets/internal_auth_password \
--interval 0
```

**Migration Stats Output:**
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/OpenSlides/openslides-cli
go 1.26.0

require (
github.com/OpenSlides/openslides-go v0.0.0-20260914113122-b8fc5dcb5833
github.com/OpenSlides/openslides-go v0.0.0-20261019143238-985a3acd9af1
github.com/schollz/progressbar/v3 v3.19.1
github.com/shopspring/decimal v1.4.0
github.com/spf13/cobra v1.10.2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/OpenSlides/openslides-go v0.0.0-20260914113122-b8fc5dcb5833 h1:e/jwi8XU9tUyX3hul2YHzydAb2bFqbzxsAGtWrTRd38=
github.com/OpenSlides/openslides-go v0.0.0-20260914113122-b8fc5dcb5833/go.mod h1:ngfwMkOksw2fydfKOfJyFM0wiyilScNIOM092pHwc0c=
github.com/OpenSlides/openslides-go v0.0.0-20261019143238-985a3acd9af1 h1:fKCAsEw5knz82tw6lPcgCDHG7kkwQyZJVJQh06yWzIw=
github.com/OpenSlides/openslides-go v0.0.0-20261019143238-985a3acd9af1/go.mod h1:ngfwMkOksw2fydfKOfJyFM0wiyilScNIOM092pHwc0c=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
Expand Down
8 changes: 4 additions & 4 deletions internal/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,11 @@ const (
// MigrationStatusFailed indicates a migration process has failed
MigrationStatusFailed string = "migration_failed"

// FinalizationStatusRunning indicates a migration finalization is currently in progress
FinalizationStatusRunning string = "finalization_running"
// MigrationStatusFinished indicates a migration modules process has finished successfully
MigrationStatusFinished string = "migration_finished"

// FinalizationStatusFailed indicates a migration finalization process has failed
FinalizationStatusFailed string = "finalization_failed"
// MigrationStatusFinalized indicates a migration has successfully finished and tables have been persisted to outside transactions
MigrationStatusFinalized string = "finalized"

// MigrationMaxRetries is the maximum number of retry attempts for failed migration requests
MigrationMaxRetries int = 8
Expand Down
11 changes: 1 addition & 10 deletions internal/grpc/server/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,6 @@ func (s *OsmanageServiceServer) MigrationsMigrate(
)
}

func (s *OsmanageServiceServer) MigrationsFinalize(
req *pb.MigrationsRequest,
stream pb.OsmanageService_MigrationsFinalizeServer,
) error {
return executeMigrationStream(req, stream, "finalize",
func(r *pb.MigrationsResponse) bool { return !migrations.Running(r) && !migrations.Finalizing(r) },
)
}

func (s *OsmanageServiceServer) MigrationsReset(
ctx context.Context,
req *pb.MigrationsRequest,
Expand Down Expand Up @@ -57,7 +48,7 @@ func (s *OsmanageServiceServer) MigrationsProgress(
return executeMigrationUnary(req, "progress")
}

// executeMigrationStream handles streaming migration commands (migrate, finalize)
// executeMigrationStream handles streaming migration commands (migrate)
func executeMigrationStream(
req *pb.MigrationsRequest,
stream interface {
Expand Down
2 changes: 1 addition & 1 deletion internal/grpc/server/update_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (s *OsmanageServiceServer) UpdateInstance(
})
}

err = actions.UpdateInstance(ctx, k8sClient, req.InstanceDir, req.SkipReadyCheck, timeout, streamCallback, inactiveCallback)
err = actions.UpdateInstance(ctx, k8sClient, req.InstanceDir, req.SkipReadyCheck, timeout, streamCallback, inactiveCallback, req.Prune)
if err != nil {
return stream.Send(&pb.UpdateInstanceResponse{
Complete: true,
Expand Down
11 changes: 8 additions & 3 deletions internal/k8s/actions/update_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func UpdateInstanceCmd() *cobra.Command {

kubeconfig := cmd.Flags().String("kubeconfig", "", "Path to kubeconfig file")
skipReadyCheck := cmd.Flags().Bool("skip-ready-check", false, "Skip waiting for instance to become ready")
prune := cmd.Flags().Bool("pruen", false, "Prune Kubernetes instance resources that are no longer in use.")
timeout := cmd.Flags().Duration("timeout", constants.DefaultInstanceTimeout, "Timeout for instance health check")

cmd.RunE = func(cmd *cobra.Command, args []string) error {
Expand All @@ -46,7 +47,7 @@ func UpdateInstanceCmd() *cobra.Command {
return fmt.Errorf("creating k8s client: %w", err)
}

if err := UpdateInstance(context.Background(), k8sClient, instanceDir, *skipReadyCheck, *timeout, nil, nil); err != nil {
if err := UpdateInstance(context.Background(), k8sClient, instanceDir, *skipReadyCheck, *timeout, nil, nil, *prune); err != nil {
return err
}

Expand All @@ -67,6 +68,7 @@ func UpdateInstance(
timeout time.Duration,
callback func(*HealthStatus) error,
inactiveCallback func() error,
prune bool,
) error {
namespace := utils.ExtractNamespace(instanceDir)
logger.Info("Namespace: %s", namespace)
Expand All @@ -92,12 +94,15 @@ func UpdateInstance(

stackDir := filepath.Join(instanceDir, constants.StackDirName)
applied, err := applyDirectory(ctx, k8sClient, stackDir, nil)

if err != nil {
return fmt.Errorf("applying stack: %w", err)
}

if err := pruneOrphans(ctx, k8sClient, namespace, applied); err != nil {
logger.Warn("Failed to prune orphaned resources: %v", err)
if prune {
if err := pruneOrphans(ctx, k8sClient, namespace, applied); err != nil {
logger.Warn("Failed to prune orphaned resources: %v", err)
}
}

if skipReadyCheck {
Expand Down
54 changes: 13 additions & 41 deletions internal/manage/actions/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,11 @@ Examples:
--address <myBackendManageIP>:9002 \
--password-file my.instance.dir/secrets/internal_auth_password

# Run migrations on auxiliary tables
# Run migrations on live tables
osmanage migrations migrate \
--address <myBackendManageIP>:9002 \
--password-file my.instance.dir/secrets/internal_auth_password

# Apply migrations to live tables
osmanage migrations finalize \
--address <myBackendManageIP>:9002 \
--password-file my.instance.dir/secrets/internal_auth_password

# Reset unapplied migrations
osmanage migrations reset \
--address <myBackendManageIP>:9002 \
Expand All @@ -46,15 +41,8 @@ Examples:
--address <myBackendManageIP>:9002 \
--password-file my.instance.dir/secrets/internal_auth_password

# Custom progress interval
osmanage migrations finalize \
--address <myBackendManageIP>:9002 \
--password-file my.instance.dir/secrets/internal_auth_password \
--interval 2s

Available commands:
migrate Run migrations on auxiliary tables
finalize Apply migrations to live tables
migrate Run migrations on live tables
reset Reset unapplied migrations
stats Show migration statistics
progress Check running migration progress`
Expand All @@ -69,9 +57,7 @@ func Cmd() *cobra.Command {

cmd.AddCommand(
migrateCmd(),
finalizeCmd(),
resetCmd(),
clearCollectionfieldTablesCmd(),
statsCmd(),
progressCmd(),
)
Expand All @@ -80,22 +66,13 @@ func Cmd() *cobra.Command {
}

func migrateCmd() *cobra.Command {
return createMigrationCmd("migrate", "Prepare migrations but do not apply them to the live tables", true)
}

func finalizeCmd() *cobra.Command {
return createMigrationCmd("finalize", "Prepare migrations and apply them to the live tables", true)
return createMigrationCmd("migrate", "Prepare migrations and apply them to the live tables", true)
}

func resetCmd() *cobra.Command {
return createMigrationCmd("reset", "Reset unapplied migrations", false)
}

// TODO: unavailable after 4.3.0, delete when backward compatibility no longer needed
func clearCollectionfieldTablesCmd() *cobra.Command {
return createMigrationCmd("clear-collectionfield-tables", "Clear all data from auxiliary tables (only when OpenSlides is offline)", false)
}

func statsCmd() *cobra.Command {
return createMigrationCmd("stats", "Print statistics about the current migration state", false)
}
Expand Down Expand Up @@ -145,13 +122,9 @@ func createMigrationCmd(name, description string, withProgressTracking bool) *co
}
fmt.Print(output)

if withProgressTracking && progressInterval != nil && *progressInterval > 0 && (Running(response) || Finalizing(response)) {
var stopCondition func(*pb.MigrationsResponse) bool
if name == "finalize" {
stopCondition = func(r *pb.MigrationsResponse) bool { return !Running(r) && !Finalizing(r) }
} else {
stopCondition = func(r *pb.MigrationsResponse) bool { return !Running(r) }
}
if withProgressTracking && progressInterval != nil && *progressInterval > 0 && Running(response) {
// check whether migration has been completed successfully (not running and finished)
stopCondition := func(r *pb.MigrationsResponse) bool { return !Running(r) && Finalized(r) }

printCallback := func(update *pb.MigrationsProgressResponse) error {
fmt.Print(update.Output)
Expand Down Expand Up @@ -236,8 +209,8 @@ func ExecuteMigrationCommand(cl *client.Client, command string) (*pb.MigrationsR
Stats: string(httpResp.Stats),
}

logger.Debug("Migration response - Success: %v, Status: %s, Running: %v, Finalizing: %v",
migrationResp.Success, migrationResp.Status, Running(migrationResp), Finalizing(migrationResp))
logger.Debug("Migration response - Success: %v, Status: %s, Running: %v",
migrationResp.Success, migrationResp.Status, Running(migrationResp))

return migrationResp, nil
}
Expand All @@ -264,7 +237,7 @@ func TrackMigrationProgress(

update := &pb.MigrationsProgressResponse{
Output: response.Output,
Running: Running(response) || Finalizing(response),
Running: Running(response),
Success: response.Success,
Exception: response.Exception,
}
Expand Down Expand Up @@ -328,18 +301,17 @@ func formatAll(mr *pb.MigrationsResponse) (string, error) {
// Faulty returns true if the migration failed
func Faulty(mr *pb.MigrationsResponse) bool {
return !mr.Success || mr.Exception != "" ||
mr.Status == constants.MigrationStatusFailed ||
mr.Status == constants.FinalizationStatusFailed
mr.Status == constants.MigrationStatusFailed
}

// Running returns true if the migration is currently in progress
func Running(mr *pb.MigrationsResponse) bool {
return mr.Status == constants.MigrationStatusRunning
}

// Finalizing returns true if the migration finalization is currently in progress
func Finalizing(mr *pb.MigrationsResponse) bool {
return mr.Status == constants.FinalizationStatusRunning
// Finalized returns true if the migration has finalized successfully
func Finalized(mr *pb.MigrationsResponse) bool {
return mr.Status == constants.MigrationStatusFinalized
}

// isRetryableError determines if an error should trigger a retry
Expand Down
21 changes: 21 additions & 0 deletions internal/manage/actions/migrations/migrations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,27 @@ func TestRunning(t *testing.T) {
}
}

func TestFinalized(t *testing.T) {
tests := []struct {
name string
status string
finalized bool
}{
{"finalized", constants.MigrationStatusFinalized, true},
{"completed", "completed", false},
{"failed", "failed", false},
{"empty", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp := &pb.MigrationsResponse{Status: tt.status}
if got := Finalized(resp); got != tt.finalized {
t.Errorf("Finalized() = %v, want %v", got, tt.finalized)
}
})
}
}

func TestGetOutput(t *testing.T) {
t.Run("normal output", func(t *testing.T) {
resp := &pb.MigrationsResponse{
Expand Down
5 changes: 2 additions & 3 deletions proto/osmanage.proto
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,10 @@ service OsmanageService {

// server side streaming
rpc MigrationsMigrate(MigrationsRequest) returns (stream MigrationsProgressResponse);
rpc MigrationsFinalize(MigrationsRequest) returns (stream MigrationsProgressResponse);
rpc StreamLogs(LogStreamRequest) returns (stream LogEntry);

// unary gRPC calls
rpc MigrationsReset(MigrationsRequest) returns (MigrationsResponse);
rpc MigrationsClearCollectionfieldTables(MigrationsRequest) returns (MigrationsResponse);
rpc MigrationsStats(MigrationsRequest) returns (MigrationsResponse);
rpc MigrationsProgress(MigrationsRequest) returns (MigrationsResponse);

Expand Down Expand Up @@ -189,6 +187,7 @@ message UpdateInstanceRequest {
string kubeconfig = 2;
bool skip_ready_check = 3;
int32 timeout_seconds = 4;
bool prune = 5;
}

message UpdateInstanceResponse {
Expand Down Expand Up @@ -312,4 +311,4 @@ message SendManageActionResponse {
bool success = 1;
string error = 2;
bytes body = 3;
}
}
Loading
Loading