From 0daa74f04f6034fbe55a0522b106618e344e95b7 Mon Sep 17 00:00:00 2001 From: Gareth Date: Sun, 13 Oct 2024 17:55:37 -0700 Subject: [PATCH] chore: misc bug fixes supporting sqlite migration (#517) --- .gitignore | 2 +- cmd/backrest/backrest.go | 27 +++- gen/go/v1/operations.pb.go | 113 ++++++++------- go.mod | 35 ++--- go.sum | 34 +++++ internal/api/backresthandler_test.go | 20 ++- internal/config/migrations/migrations.go | 6 +- internal/ioutil/ioutil.go | 18 +++ internal/logstore/logstore.go | 27 ++-- internal/oplog/migrations.go | 3 + internal/oplog/oplog.go | 4 + internal/oplog/sqlitestore/sqlitestore.go | 118 +++++++++------- .../oplog/storetests/storecontract_test.go | 131 ++++++++++++++++++ internal/orchestrator/logging/logging.go | 2 +- internal/orchestrator/orchestrator.go | 19 +-- internal/orchestrator/repo/repo_test.go | 3 +- internal/orchestrator/taskrunnerimpl.go | 12 +- internal/orchestrator/tasks/taskcheck.go | 1 + .../orchestrator/tasks/taskcollectgarbage.go | 2 +- internal/orchestrator/tasks/taskprune.go | 1 + internal/orchestrator/tasks/taskruncommand.go | 16 ++- internal/orchestrator/tasks/taskstats.go | 4 +- internal/resticinstaller/flock.go | 29 ---- internal/resticinstaller/flockwindows.go | 9 -- internal/resticinstaller/resticinstaller.go | 58 ++++---- pkg/restic/restic_test.go | 12 +- proto/v1/operations.proto | 1 + webui/gen/ts/v1/operations_pb.ts | 6 + webui/src/components/OperationRow.tsx | 22 +-- webui/src/components/StatsPanel.tsx | 22 +-- webui/src/views/PlanView.tsx | 6 +- 31 files changed, 504 insertions(+), 259 deletions(-) delete mode 100644 internal/resticinstaller/flock.go delete mode 100644 internal/resticinstaller/flockwindows.go diff --git a/.gitignore b/.gitignore index be222ce9..0e9b3b3a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ backrest-* dist __debug_bin cmd/backrest/backrest -*.exe \ No newline at end of file +*.exe diff --git a/cmd/backrest/backrest.go b/cmd/backrest/backrest.go index 51d7974e..28e91302 100644 --- a/cmd/backrest/backrest.go +++ b/cmd/backrest/backrest.go @@ -69,7 +69,9 @@ func main() { // Create / load the operation log oplogFile := path.Join(env.DataDir(), "oplog.sqlite") opstore, err := sqlitestore.NewSqliteStore(oplogFile) - if err != nil { + if errors.Is(err, sqlitestore.ErrLocked) { + zap.S().Fatalf("oplog is locked by another instance of backrest that is using the same data directory %q, kill that instance before starting another one.", env.DataDir()) + } else if err != nil { zap.S().Warnf("operation log may be corrupted, if errors recur delete the file %q and restart. Your backups stored in your repos are safe.", oplogFile) zap.S().Fatalf("error creating oplog: %v", err) } @@ -87,6 +89,23 @@ func main() { zap.S().Fatalf("error creating task log store: %v", err) } logstore.MigrateTarLogsInDir(logStore, filepath.Join(env.DataDir(), "rotatinglogs")) + deleteLogsForOp := func(ops []*v1.Operation, event oplog.OperationEvent) { + if event != oplog.OPERATION_DELETED { + return + } + for _, op := range ops { + if err := logStore.DeleteWithParent(op.Id); err != nil { + zap.S().Warnf("error deleting logs for operation %q: %v", op.Id, err) + } + } + } + log.Subscribe(oplog.Query{}, &deleteLogsForOp) + defer func() { + if err := logStore.Close(); err != nil { + zap.S().Warnf("error closing log store: %v", err) + } + log.Unsubscribe(&deleteLogsForOp) + }() // Create orchestrator and start task loop. orchestrator, err := orchestrator.NewOrchestrator(resticPath, cfg, log, logStore) @@ -226,6 +245,9 @@ func installLoggers() { zap.S().Infof("writing logs to: %v", logsDir) } +// migrateBboltOplog migrates the old bbolt oplog to the new sqlite oplog. +// It is careful to ensure that all migrations are applied before copying +// operations directly to the sqlite logstore. func migrateBboltOplog(logstore oplog.OpStore) { oldBboltOplogFile := path.Join(env.DataDir(), "oplog.boltdb") if _, err := os.Stat(oldBboltOplogFile); err == nil { @@ -243,7 +265,6 @@ func migrateBboltOplog(logstore oplog.OpStore) { batch := make([]*v1.Operation, 0, 32) var errs []error - if err := oldOplog.Query(oplog.Query{}, func(op *v1.Operation) error { batch = append(batch, op) if len(batch) == 256 { @@ -278,7 +299,7 @@ func migrateBboltOplog(logstore oplog.OpStore) { if err := oldOpstore.Close(); err != nil { zap.S().Warnf("error closing old bbolt oplog: %v", err) } - if err := os.Remove(oldBboltOplogFile); err != nil { + if err := os.Rename(oldBboltOplogFile, oldBboltOplogFile+".deprecated"); err != nil { zap.S().Warnf("error removing old bbolt oplog: %v", err) } diff --git a/gen/go/v1/operations.pb.go b/gen/go/v1/operations.pb.go index 9dda65cf..93ef6c52 100644 --- a/gen/go/v1/operations.pb.go +++ b/gen/go/v1/operations.pb.go @@ -858,8 +858,9 @@ type OperationRunCommand struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"` - OutputLogref string `protobuf:"bytes,2,opt,name=output_logref,json=outputLogref,proto3" json:"output_logref,omitempty"` + Command string `protobuf:"bytes,1,opt,name=command,proto3" json:"command,omitempty"` + OutputLogref string `protobuf:"bytes,2,opt,name=output_logref,json=outputLogref,proto3" json:"output_logref,omitempty"` + OutputSizeBytes int64 `protobuf:"varint,3,opt,name=output_size_bytes,json=outputSizeBytes,proto3" json:"output_size_bytes,omitempty"` } func (x *OperationRunCommand) Reset() { @@ -908,6 +909,13 @@ func (x *OperationRunCommand) GetOutputLogref() string { return "" } +func (x *OperationRunCommand) GetOutputSizeBytes() int64 { + if x != nil { + return x.OutputSizeBytes + } + return 0 +} + // OperationRestore tracks a restore operation. type OperationRestore struct { state protoimpl.MessageState @@ -1214,55 +1222,58 @@ var file_v1_operations_proto_rawDesc = []byte{ 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x22, 0x54, - 0x0a, 0x13, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x43, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, - 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, - 0x67, 0x72, 0x65, 0x66, 0x22, 0x79, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, - 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, - 0x72, 0x67, 0x65, 0x74, 0x12, 0x39, 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x52, - 0x65, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, - 0x35, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, - 0x73, 0x12, 0x23, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x0d, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x70, - 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x6f, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, - 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x4f, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, - 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, - 0x66, 0x12, 0x30, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, - 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x2a, 0x60, 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, - 0x4e, 0x54, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, - 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, - 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, - 0x10, 0x02, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x4c, 0x45, - 0x54, 0x45, 0x44, 0x10, 0x03, 0x2a, 0xc2, 0x01, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, - 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, - 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, - 0x01, 0x12, 0x15, 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x50, 0x52, - 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, - 0x55, 0x53, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, - 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x07, - 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, - 0x10, 0x04, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x59, 0x53, - 0x54, 0x45, 0x4d, 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, - 0x19, 0x0a, 0x15, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x43, - 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x06, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, - 0x65, 0x6f, 0x72, 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, - 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x22, 0x80, + 0x01, 0x0a, 0x13, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x75, 0x6e, 0x43, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, + 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, + 0x6f, 0x67, 0x72, 0x65, 0x66, 0x12, 0x2a, 0x0a, 0x11, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, + 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0f, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x53, 0x69, 0x7a, 0x65, 0x42, 0x79, 0x74, 0x65, + 0x73, 0x22, 0x79, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x73, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x12, 0x39, 0x0a, 0x0b, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x35, 0x0a, 0x0e, + 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x23, + 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, + 0x76, 0x31, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x73, 0x22, 0x9a, 0x01, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x75, 0x6e, 0x48, 0x6f, 0x6f, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x72, 0x65, + 0x6e, 0x74, 0x5f, 0x6f, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x72, + 0x65, 0x6e, 0x74, 0x4f, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x6f, 0x75, 0x74, + 0x70, 0x75, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x4c, 0x6f, 0x67, 0x72, 0x65, 0x66, 0x12, 0x30, + 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x12, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x6f, 0x6b, 0x2e, 0x43, 0x6f, 0x6e, 0x64, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x2a, 0x60, 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, + 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, + 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, + 0x4e, 0x54, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, + 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, + 0x11, 0x0a, 0x0d, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44, + 0x10, 0x03, 0x2a, 0xc2, 0x01, 0x0a, 0x0f, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, + 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x15, + 0x0a, 0x11, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x50, 0x52, 0x4f, 0x47, 0x52, + 0x45, 0x53, 0x53, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, + 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, + 0x54, 0x55, 0x53, 0x5f, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x07, 0x12, 0x10, 0x0a, + 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x12, + 0x1b, 0x0a, 0x17, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x59, 0x53, 0x54, 0x45, 0x4d, + 0x5f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, 0x19, 0x0a, 0x15, + 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x53, 0x45, 0x52, 0x5f, 0x43, 0x41, 0x4e, 0x43, + 0x45, 0x4c, 0x4c, 0x45, 0x44, 0x10, 0x06, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x72, 0x65, 0x74, 0x68, 0x67, 0x65, 0x6f, 0x72, + 0x67, 0x65, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x72, 0x65, 0x73, 0x74, 0x2f, 0x67, 0x65, 0x6e, 0x2f, + 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/go.mod b/go.mod index f1d96115..8f705a22 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,15 @@ go 1.22.0 toolchain go1.23.1 require ( - al.essio.dev/pkg/shellescape v1.5.0 + al.essio.dev/pkg/shellescape v1.5.1 connectrpc.com/connect v1.17.0 github.com/containrrr/shoutrrr v0.8.0 github.com/djherbis/buffer v1.2.0 github.com/djherbis/nio/v3 v3.0.1 github.com/getlantern/systray v1.2.2 github.com/gitploy-io/cronexpr v0.2.2 + github.com/gofrs/flock v0.12.1 github.com/golang-jwt/jwt/v5 v5.2.1 - github.com/google/go-cmp v0.6.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/hashicorp/go-multierror v1.1.1 github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb @@ -23,12 +23,12 @@ require ( github.com/prometheus/client_golang v1.20.4 go.etcd.io/bbolt v1.3.11 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.27.0 - golang.org/x/net v0.29.0 + golang.org/x/crypto v0.28.0 + golang.org/x/net v0.30.0 golang.org/x/sync v0.8.0 - google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61 - google.golang.org/grpc v1.67.0 - google.golang.org/protobuf v1.34.2 + google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 + google.golang.org/grpc v1.67.1 + google.golang.org/protobuf v1.35.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1 zombiezen.com/go/sqlite v1.4.0 ) @@ -49,28 +49,29 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-stack/stack v1.8.1 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/josephspurrier/goversioninfo v1.4.1 // indirect - github.com/klauspost/compress v1.17.10 // indirect + github.com/klauspost/compress v1.17.11 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.59.1 // indirect + github.com/prometheus/common v0.60.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - go.opentelemetry.io/otel v1.30.0 // indirect - go.opentelemetry.io/otel/metric v1.30.0 // indirect - go.opentelemetry.io/otel/trace v1.30.0 // indirect + go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/image v0.20.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 // indirect + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect + golang.org/x/image v0.21.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect modernc.org/libc v1.61.0 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect diff --git a/go.sum b/go.sum index 6b84cb8f..991cad3b 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ al.essio.dev/pkg/shellescape v1.5.0 h1:7oTvSsQ5kg9WksA9O58y9wjYnY4jP0CL82/Q8WLUGKk= al.essio.dev/pkg/shellescape v1.5.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= +al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk= connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw= @@ -60,6 +62,8 @@ github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= @@ -86,6 +90,8 @@ github.com/josephspurrier/goversioninfo v1.4.1 h1:5LvrkP+n0tg91J9yTkoVnt/QgNnrI1 github.com/josephspurrier/goversioninfo v1.4.1/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -121,6 +127,8 @@ github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.59.1 h1:LXb1quJHWm1P6wq/U824uxYi4Sg0oGvNeUm1z5dJoX0= github.com/prometheus/common v0.59.1/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= +github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA= +github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 h1:GranzK4hv1/pqTIhMTXt2X8MmMOuH3hMeUR0o9SP5yc= @@ -143,11 +151,17 @@ go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.opentelemetry.io/otel v1.9.0/go.mod h1:np4EoPGzoPs3O67xUVNoPPcmSvsfOxNlNA4F4AC+0Eo= go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= go.opentelemetry.io/otel/trace v1.9.0/go.mod h1:2737Q0MuG8q1uILYm2YYVkAyLtOofiTNGg6VODnOiPo= go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -162,10 +176,16 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= +golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= +golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= @@ -176,6 +196,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= @@ -192,11 +214,15 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -208,12 +234,20 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61 h1:pAjq8XSSzXoP9ya73v/w+9QEAAJNluLrpmMq5qFJQNY= google.golang.org/genproto/googleapis/api v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:O6rP0uBq4k0mdi/b4ZEMAZjkhYWhS815kCvaMha4VN8= +google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg= +google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M= google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 h1:N9BgCIAUvn/M+p4NJccWPWb3BWh88+zyL0ll9HgbEeM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/api/backresthandler_test.go b/internal/api/backresthandler_test.go index f53e06ee..192ceea5 100644 --- a/internal/api/backresthandler_test.go +++ b/internal/api/backresthandler_test.go @@ -559,6 +559,12 @@ func TestHookOnErrorHandling(t *testing.T) { func TestCancelBackup(t *testing.T) { t.Parallel() + // a hook is used to make the backup operation wait long enough to be cancelled + hookCmd := "sleep 2" + if runtime.GOOS == "windows" { + hookCmd = "Start-Sleep -Seconds 2" + } + sut := createSystemUnderTest(t, &config.MemoryStore{ Config: &v1.Config{ Modno: 1234, @@ -586,6 +592,18 @@ func TestCancelBackup(t *testing.T) { PolicyKeepLastN: 1, }, }, + Hooks: []*v1.Hook{ + { + Conditions: []v1.Hook_Condition{ + v1.Hook_CONDITION_SNAPSHOT_START, + }, + Action: &v1.Hook_ActionCommand{ + ActionCommand: &v1.Hook_Command{ + Command: hookCmd, + }, + }, + }, + }, }, }, }, @@ -629,7 +647,7 @@ func TestCancelBackup(t *testing.T) { }) if err := errgroup.Wait(); err != nil { - t.Fatalf(err.Error()) + t.Fatal(err.Error()) } // Assert that the backup operation was cancelled diff --git a/internal/config/migrations/migrations.go b/internal/config/migrations/migrations.go index d76d2d0b..d072ba2e 100644 --- a/internal/config/migrations/migrations.go +++ b/internal/config/migrations/migrations.go @@ -4,11 +4,12 @@ import ( "fmt" v1 "github.com/garethgeorge/backrest/gen/go/v1" + "go.uber.org/zap" "google.golang.org/protobuf/proto" ) var migrations = []*func(*v1.Config){ - &noop, // migration001PrunePolicy + &noop, // migration001PrunePolicy is deprecated &noop, // migration002Schedules is deprecated &migration003RelativeScheduling, } @@ -27,6 +28,9 @@ func ApplyMigrations(config *v1.Config) error { startMigration := int(config.Version) if startMigration < 0 { startMigration = 0 + } else if startMigration > int(CurrentVersion) { + zap.S().Warnf("config version %d is greater than the latest known spec %d. Were you previously running a newer version of backrest? Ensure that your install is up to date.", startMigration, CurrentVersion) + return fmt.Errorf("config version %d is greater than the latest known config format %d", startMigration, CurrentVersion) } for idx := startMigration; idx < len(migrations); idx += 1 { diff --git a/internal/ioutil/ioutil.go b/internal/ioutil/ioutil.go index 3a9ca5c5..696a9870 100644 --- a/internal/ioutil/ioutil.go +++ b/internal/ioutil/ioutil.go @@ -4,6 +4,7 @@ import ( "bytes" "io" "sync" + "sync/atomic" ) // LimitWriter is a writer that limits the number of bytes written to it. @@ -83,3 +84,20 @@ func (w *SynchronizedWriter) Write(p []byte) (n int, err error) { defer w.Mu.Unlock() return w.W.Write(p) } + +type SizeTrackingWriter struct { + size atomic.Uint64 + io.Writer +} + +func (w *SizeTrackingWriter) Write(p []byte) (n int, err error) { + n, err = w.Writer.Write(p) + w.size.Add(uint64(n)) + return +} + +// Size returns the number of bytes written to the writer. +// The value is fundamentally racy only consistent if synchronized with the writer or closed. +func (w *SizeTrackingWriter) Size() uint64 { + return w.size.Load() +} diff --git a/internal/logstore/logstore.go b/internal/logstore/logstore.go index 75a6808b..d1063592 100644 --- a/internal/logstore/logstore.go +++ b/internal/logstore/logstore.go @@ -370,11 +370,11 @@ func (ls *LogStore) finalizeLogFile(id string, fname string) error { return nil } -func (ls *LogStore) maybeReleaseTempFile(fname string) error { +func (ls *LogStore) maybeReleaseTempFile(id, fname string) error { ls.trackingMu.Lock() defer ls.trackingMu.Unlock() - _, ok := ls.refcount[fname] + _, ok := ls.refcount[id] if ok { return nil } @@ -409,25 +409,24 @@ func (w *writer) Close() error { defer w.ls.mu.Unlock(w.id) defer w.ls.notify(w.id) + if e := w.ls.finalizeLogFile(w.id, w.fname); e != nil { + err = multierror.Append(err, fmt.Errorf("finalize %v: %w", w.fname, e)) + } else { + w.ls.refcount[w.id]-- + } + // manually close all subscribers and delete the subscriber entry from the map; there are no more writes coming. w.ls.trackingMu.Lock() + if w.ls.refcount[w.id] == 0 { + delete(w.ls.refcount, w.id) + } subs := w.ls.subscribers[w.id] for _, ch := range subs { close(ch) } delete(w.ls.subscribers, w.id) - - // try to finalize the log file - if e := w.ls.finalizeLogFile(w.id, w.fname); e != nil { - err = multierror.Append(err, fmt.Errorf("finalize %v: %w", w.fname, e)) - } else { - w.ls.refcount[w.id]-- - if w.ls.refcount[w.id] == 0 { - delete(w.ls.refcount, w.id) - } - } w.ls.trackingMu.Unlock() - w.ls.maybeReleaseTempFile(w.fname) + w.ls.maybeReleaseTempFile(w.id, w.fname) }) return err @@ -478,7 +477,7 @@ func (r *reader) Close() error { delete(r.ls.refcount, r.id) } r.ls.trackingMu.Unlock() - r.ls.maybeReleaseTempFile(r.fname) + r.ls.maybeReleaseTempFile(r.id, r.fname) close(r.closed) }) diff --git a/internal/oplog/migrations.go b/internal/oplog/migrations.go index 4a299e1c..8a775294 100644 --- a/internal/oplog/migrations.go +++ b/internal/oplog/migrations.go @@ -25,6 +25,9 @@ func ApplyMigrations(oplog *OpLog) error { } if startMigration < 0 { startMigration = 0 + } else if startMigration > CurrentVersion { + zap.S().Warnf("oplog spec %d is greater than the latest known spec %d. Were you previously running a newer version of backrest? Ensure that your install is up to date.", startMigration, CurrentVersion) + return fmt.Errorf("oplog spec %d is greater than the latest known spec %d", startMigration, CurrentVersion) } for idx := startMigration; idx < int64(len(migrations)); idx += 1 { diff --git a/internal/oplog/oplog.go b/internal/oplog/oplog.go index 05c4280c..038f9981 100644 --- a/internal/oplog/oplog.go +++ b/internal/oplog/oplog.go @@ -183,6 +183,10 @@ func (q *Query) Match(op *v1.Operation) bool { } } + if q.InstanceID != "" && op.InstanceId != q.InstanceID { + return false + } + if q.PlanID != "" && op.PlanId != q.PlanID { return false } diff --git a/internal/oplog/sqlitestore/sqlitestore.go b/internal/oplog/sqlitestore/sqlitestore.go index b94a6b62..ef1574e1 100644 --- a/internal/oplog/sqlitestore/sqlitestore.go +++ b/internal/oplog/sqlitestore/sqlitestore.go @@ -2,31 +2,37 @@ package sqlitestore import ( "context" - "crypto/rand" "errors" "fmt" - "math/big" + "os" + "path/filepath" "strings" "sync/atomic" - "time" v1 "github.com/garethgeorge/backrest/gen/go/v1" "github.com/garethgeorge/backrest/internal/oplog" "github.com/garethgeorge/backrest/internal/protoutil" "google.golang.org/protobuf/proto" + "github.com/gofrs/flock" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" ) +var ErrLocked = errors.New("sqlite db is locked") + type SqliteStore struct { dbpool *sqlitex.Pool nextIDVal atomic.Int64 + dblock *flock.Flock } var _ oplog.OpStore = (*SqliteStore)(nil) func NewSqliteStore(db string) (*SqliteStore, error) { + if err := os.MkdirAll(filepath.Dir(db), 0700); err != nil { + return nil, fmt.Errorf("create sqlite db directory: %v", err) + } dbpool, err := sqlitex.NewPool(db, sqlitex.PoolOptions{ PoolSize: 16, Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenWAL, @@ -34,11 +40,25 @@ func NewSqliteStore(db string) (*SqliteStore, error) { if err != nil { return nil, fmt.Errorf("open sqlite pool: %v", err) } - store := &SqliteStore{dbpool: dbpool} - return store, store.init() + store := &SqliteStore{ + dbpool: dbpool, + dblock: flock.New(db + ".lock"), + } + if locked, err := store.dblock.TryLock(); err != nil { + return nil, fmt.Errorf("lock sqlite db: %v", err) + } else if !locked { + return nil, ErrLocked + } + if err := store.init(); err != nil { + return nil, err + } + return store, nil } func (m *SqliteStore) Close() error { + if err := m.dblock.Unlock(); err != nil { + return fmt.Errorf("unlock sqlite db: %v", err) + } return m.dbpool.Close() } @@ -58,10 +78,9 @@ CREATE TABLE IF NOT EXISTS operations ( CREATE TABLE IF NOT EXISTS system_info ( version INTEGER NOT NULL ); -CREATE INDEX IF NOT EXISTS operations_instance_id ON operations (instance_id); -CREATE INDEX IF NOT EXISTS operations_plan_id ON operations (plan_id); -CREATE INDEX IF NOT EXISTS operations_repo_id ON operations (repo_id); +CREATE INDEX IF NOT EXISTS operations_repo_id_plan_id_instance_id ON operations (repo_id, plan_id, instance_id); CREATE INDEX IF NOT EXISTS operations_snapshot_id ON operations (snapshot_id); +CREATE INDEX IF NOT EXISTS operations_flow_id ON operations (flow_id); INSERT INTO system_info (version) SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM system_info); @@ -76,17 +95,18 @@ SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM system_info); } // rand init value - n, _ := rand.Int(rand.Reader, big.NewInt(1<<20)) - m.nextIDVal.Store(n.Int64()) - + if err := sqlitex.ExecuteTransient(conn, "SELECT id FROM operations ORDER BY id DESC LIMIT 1", &sqlitex.ExecOptions{ + ResultFunc: func(stmt *sqlite.Stmt) error { + m.nextIDVal.Store(stmt.GetInt64("id")) + return nil + }, + }); err != nil { + return fmt.Errorf("init sqlite: %v", err) + } + m.nextIDVal.CompareAndSwap(0, 1) return nil } -func (o *SqliteStore) nextID(unixTimeMs int64) (int64, error) { - seq := o.nextIDVal.Add(1) - return int64(unixTimeMs<<20) | int64(seq&((1<<20)-1)), nil -} - func (m *SqliteStore) Version() (int64, error) { conn, err := m.dbpool.Take(context.Background()) if err != nil { @@ -194,7 +214,7 @@ func (m *SqliteStore) Query(q oplog.Query, f func(*v1.Operation) error) error { Args: args, ResultFunc: func(stmt *sqlite.Stmt) error { opBytes := make([]byte, stmt.ColumnLen(0)) - n := stmt.GetBytes("operation", opBytes) + n := stmt.ColumnBytes(0, opBytes) opBytes = opBytes[:n] var op v1.Operation @@ -216,14 +236,14 @@ func (m *SqliteStore) Transform(q oplog.Query, f func(*v1.Operation) (*v1.Operat } defer m.dbpool.Put(conn) - query, args := m.buildQuery(q, false) + query, args := m.buildQuery(q, true) return withSqliteTransaction(conn, func() error { return sqlitex.ExecuteTransient(conn, query, &sqlitex.ExecOptions{ Args: args, ResultFunc: func(stmt *sqlite.Stmt) error { opBytes := make([]byte, stmt.ColumnLen(0)) - n := stmt.GetBytes("operation", opBytes) + n := stmt.ColumnBytes(0, opBytes) opBytes = opBytes[:n] var op v1.Operation @@ -234,19 +254,11 @@ func (m *SqliteStore) Transform(q oplog.Query, f func(*v1.Operation) (*v1.Operat newOp, err := f(&op) if err != nil { return err + } else if newOp == nil { + return nil } - newOpBytes, err := proto.Marshal(newOp) - if err != nil { - return fmt.Errorf("marshal operation: %v", err) - } - - if err := sqlitex.Execute(conn, "UPDATE operations SET operation = ? WHERE id = ?", &sqlitex.ExecOptions{ - Args: []any{newOpBytes, stmt.GetInt64("id")}, - }); err != nil { - return fmt.Errorf("update operation: %v", err) - } - return nil + return m.updateInternal(conn, newOp) }, }) }) @@ -261,10 +273,7 @@ func (m *SqliteStore) Add(op ...*v1.Operation) error { return withSqliteTransaction(conn, func() error { for _, o := range op { - o.Id, err = m.nextID(time.Now().UnixMilli()) - if err != nil { - return fmt.Errorf("generate operation id: %v", err) - } + o.Id = m.nextIDVal.Add(1) if o.FlowId == 0 { o.FlowId = o.Id } @@ -287,7 +296,6 @@ func (m *SqliteStore) Add(op ...*v1.Operation) error { } return fmt.Errorf("add operation: %v", err) } - } return nil }) @@ -301,27 +309,31 @@ func (m *SqliteStore) Update(op ...*v1.Operation) error { defer m.dbpool.Put(conn) return withSqliteTransaction(conn, func() error { - for _, o := range op { - if err := protoutil.ValidateOperation(o); err != nil { - return err - } - bytes, err := proto.Marshal(o) - if err != nil { - return fmt.Errorf("marshal operation: %v", err) - } - if err := sqlitex.Execute(conn, "UPDATE operations SET operation = ?, flow_id = ?, instance_id = ?, plan_id = ?, repo_id = ?, snapshot_id = ? WHERE id = ?", &sqlitex.ExecOptions{ - Args: []any{bytes, o.FlowId, o.InstanceId, o.PlanId, o.RepoId, o.SnapshotId, o.Id}, - }); err != nil { - return fmt.Errorf("update operation: %v", err) - } - if conn.Changes() == 0 { - return fmt.Errorf("couldn't update %d: %w", o.Id, oplog.ErrNotExist) - } - } - return nil + return m.updateInternal(conn, op...) }) } +func (m *SqliteStore) updateInternal(conn *sqlite.Conn, op ...*v1.Operation) error { + for _, o := range op { + if err := protoutil.ValidateOperation(o); err != nil { + return err + } + bytes, err := proto.Marshal(o) + if err != nil { + return fmt.Errorf("marshal operation: %v", err) + } + if err := sqlitex.Execute(conn, "UPDATE operations SET operation = ?, flow_id = ?, instance_id = ?, plan_id = ?, repo_id = ?, snapshot_id = ? WHERE id = ?", &sqlitex.ExecOptions{ + Args: []any{bytes, o.FlowId, o.InstanceId, o.PlanId, o.RepoId, o.SnapshotId, o.Id}, + }); err != nil { + return fmt.Errorf("update operation: %v", err) + } + if conn.Changes() == 0 { + return fmt.Errorf("couldn't update %d: %w", o.Id, oplog.ErrNotExist) + } + } + return nil +} + func (m *SqliteStore) Get(opID int64) (*v1.Operation, error) { conn, err := m.dbpool.Take(context.Background()) if err != nil { diff --git a/internal/oplog/storetests/storecontract_test.go b/internal/oplog/storetests/storecontract_test.go index c69b79dc..890c9916 100644 --- a/internal/oplog/storetests/storecontract_test.go +++ b/internal/oplog/storetests/storecontract_test.go @@ -404,6 +404,137 @@ func TestUpdateOperation(t *testing.T) { } } +func TestTransform(t *testing.T) { + ops := []*v1.Operation{ + { + InstanceId: "foo", + PlanId: "plan1", + RepoId: "repo1", + UnixTimeStartMs: 1234, + UnixTimeEndMs: 5678, + }, + { + InstanceId: "bar", + PlanId: "plan1", + RepoId: "repo1", + UnixTimeStartMs: 1234, + UnixTimeEndMs: 5678, + }, + } + + tcs := []struct { + name string + f func(*v1.Operation) (*v1.Operation, error) + ops []*v1.Operation + want []*v1.Operation + query oplog.Query + }{ + { + name: "no change", + f: func(op *v1.Operation) (*v1.Operation, error) { + return nil, nil + }, + ops: ops, + want: ops, + }, + { + name: "no change by copy", + f: func(op *v1.Operation) (*v1.Operation, error) { + return proto.Clone(op).(*v1.Operation), nil + }, + ops: ops, + want: ops, + }, + { + name: "change plan", + f: func(op *v1.Operation) (*v1.Operation, error) { + op.PlanId = "newplan" + return op, nil + }, + ops: []*v1.Operation{ + { + InstanceId: "foo", + PlanId: "oldplan", + RepoId: "repo1", + UnixTimeStartMs: 1234, + UnixTimeEndMs: 5678, + }, + }, + want: []*v1.Operation{ + { + InstanceId: "foo", + PlanId: "newplan", + RepoId: "repo1", + UnixTimeStartMs: 1234, + UnixTimeEndMs: 5678, + }, + }, + }, + { + name: "change plan with query", + f: func(op *v1.Operation) (*v1.Operation, error) { + op.PlanId = "newplan" + return op, nil + }, + ops: ops, + want: []*v1.Operation{ + { + InstanceId: "foo", + PlanId: "newplan", + RepoId: "repo1", + UnixTimeStartMs: 1234, + UnixTimeEndMs: 5678, + }, + ops[1], + }, + query: oplog.Query{InstanceID: "foo"}, + }, + } + for _, tc := range tcs { + tc := tc + t.Run(tc.name, func(t *testing.T) { + for name, store := range StoresForTest(t) { + store := store + t.Run(name, func(t *testing.T) { + log, err := oplog.NewOpLog(store) + if err != nil { + t.Fatalf("error creating oplog: %v", err) + } + for _, op := range tc.ops { + copy := proto.Clone(op).(*v1.Operation) + if err := log.Add(copy); err != nil { + t.Fatalf("error adding operation: %s", err) + } + } + + if err := log.Transform(tc.query, tc.f); err != nil { + t.Fatalf("error transforming operations: %s", err) + } + + var got []*v1.Operation + if err := log.Query(oplog.Query{}, func(op *v1.Operation) error { + op.Id = 0 + op.FlowId = 0 + got = append(got, op) + return nil + }); err != nil { + t.Fatalf("error listing operations: %s", err) + } + + if slices.CompareFunc(got, tc.want, func(a, b *v1.Operation) int { + if proto.Equal(a, b) { + return 0 + } + return 1 + }) != 0 { + t.Errorf("want operations: %v, got unexpected operations: %v", tc.want, got) + } + }) + } + }) + } +} + func collectMessages(ops []*v1.Operation) []string { var messages []string for _, op := range ops { diff --git a/internal/orchestrator/logging/logging.go b/internal/orchestrator/logging/logging.go index c0827c4e..69f219d9 100644 --- a/internal/orchestrator/logging/logging.go +++ b/internal/orchestrator/logging/logging.go @@ -35,7 +35,7 @@ func Logger(ctx context.Context, prefix string) *zap.Logger { return zap.L() } p := zap.NewProductionEncoderConfig() - p.EncodeTime = zapcore.ISO8601TimeEncoder + p.EncodeTime = zapcore.TimeEncoderOfLayout("15:04:05.000Z") fe := zapcore.NewConsoleEncoder(p) l := zap.New(zapcore.NewTee( zap.L().Core(), diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index b1b2433b..958fb01c 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -2,7 +2,6 @@ package orchestrator import ( "context" - "crypto/rand" "errors" "fmt" "io" @@ -19,6 +18,7 @@ import ( "github.com/garethgeorge/backrest/internal/orchestrator/repo" "github.com/garethgeorge/backrest/internal/orchestrator/tasks" "github.com/garethgeorge/backrest/internal/queue" + "github.com/google/uuid" "go.uber.org/zap" "google.golang.org/protobuf/proto" ) @@ -238,17 +238,16 @@ func (o *Orchestrator) GetPlan(planID string) (*v1.Plan, error) { } func (o *Orchestrator) CancelOperation(operationId int64, status v1.OperationStatus) error { - o.taskCancelMu.Lock() - if cancel, ok := o.taskCancel[operationId]; ok { - cancel() - } - o.taskCancelMu.Unlock() - allTasks := o.taskQueue.GetAll() idx := slices.IndexFunc(allTasks, func(t stContainer) bool { return t.Op != nil && t.Op.GetId() == operationId }) if idx == -1 { + o.taskCancelMu.Lock() + if cancel, ok := o.taskCancel[operationId]; ok { + cancel() + } + o.taskCancelMu.Unlock() return nil } t := allTasks[idx] @@ -370,11 +369,7 @@ func (o *Orchestrator) RunTask(ctx context.Context, st tasks.ScheduledTask) erro o.taskCancelMu.Unlock() }() - randBytes := make([]byte, 8) - if _, err := rand.Read(randBytes); err != nil { - panic(err) - } - logID := fmt.Sprintf("op%d-tasklog-%x", op.Id, randBytes) + logID := uuid.New().String() logWriter, err = o.logStore.Create(logID, op.Id, defaultTaskLogDuration) if err != nil { zap.S().Errorf("failed to create live log writer: %v", err) diff --git a/internal/orchestrator/repo/repo_test.go b/internal/orchestrator/repo/repo_test.go index b4ea4b46..e247bea0 100644 --- a/internal/orchestrator/repo/repo_test.go +++ b/internal/orchestrator/repo/repo_test.go @@ -245,8 +245,6 @@ func TestSnapshotParenting(t *testing.T) { } func TestEnvVarPropagation(t *testing.T) { - t.Parallel() - repo := t.TempDir() // create a new repo with cache disabled for testing @@ -269,6 +267,7 @@ func TestEnvVarPropagation(t *testing.T) { // set the env var os.Setenv("MY_FOO", "bar") + defer os.Unsetenv("MY_FOO") orchestrator, err = NewRepoOrchestrator(configForTest, r, helpers.ResticBinary(t)) if err != nil { t.Fatalf("failed to create repo orchestrator: %v", err) diff --git a/internal/orchestrator/taskrunnerimpl.go b/internal/orchestrator/taskrunnerimpl.go index d8c061d9..64482781 100644 --- a/internal/orchestrator/taskrunnerimpl.go +++ b/internal/orchestrator/taskrunnerimpl.go @@ -2,7 +2,6 @@ package orchestrator import ( "context" - "crypto/rand" "errors" "fmt" "io" @@ -14,6 +13,7 @@ import ( "github.com/garethgeorge/backrest/internal/orchestrator/logging" "github.com/garethgeorge/backrest/internal/orchestrator/repo" "github.com/garethgeorge/backrest/internal/orchestrator/tasks" + "github.com/google/uuid" "go.uber.org/zap" ) @@ -160,11 +160,7 @@ func (t *taskRunnerImpl) Logger(ctx context.Context) *zap.Logger { } func (t *taskRunnerImpl) LogrefWriter() (string, io.WriteCloser, error) { - randBytes := make([]byte, 8) - if _, err := rand.Read(randBytes); err != nil { - return "", nil, err - } - id := fmt.Sprintf("op%d-logref-%x", t.op.Id, randBytes) - writer, err := t.orchestrator.logStore.Create(id, t.op.GetId(), time.Duration(0)) - return id, writer, err + logID := uuid.New().String() + writer, err := t.orchestrator.logStore.Create(logID, t.op.GetId(), time.Duration(0)) + return logID, writer, err } diff --git a/internal/orchestrator/tasks/taskcheck.go b/internal/orchestrator/tasks/taskcheck.go index a0e5752e..c00c77e1 100644 --- a/internal/orchestrator/tasks/taskcheck.go +++ b/internal/orchestrator/tasks/taskcheck.go @@ -118,6 +118,7 @@ func (t *CheckTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner if err != nil { return fmt.Errorf("create logref writer: %w", err) } + defer writer.Close() opCheck.OperationCheck.OutputLogref = liveID if err := runner.UpdateOperation(op); err != nil { diff --git a/internal/orchestrator/tasks/taskcollectgarbage.go b/internal/orchestrator/tasks/taskcollectgarbage.go index ff7af267..4c2bd981 100644 --- a/internal/orchestrator/tasks/taskcollectgarbage.go +++ b/internal/orchestrator/tasks/taskcollectgarbage.go @@ -115,7 +115,7 @@ func (t *CollectGarbageTask) gcOperations(log *oplog.OpLog) error { } } - zap.L().Info("collecting garbage", + zap.L().Info("collecting garbage operations", zap.Any("operations_removed", len(forgetIDs))) // cleaning up logstore diff --git a/internal/orchestrator/tasks/taskprune.go b/internal/orchestrator/tasks/taskprune.go index b4d7d1ce..6cc7281d 100644 --- a/internal/orchestrator/tasks/taskprune.go +++ b/internal/orchestrator/tasks/taskprune.go @@ -118,6 +118,7 @@ func (t *PruneTask) Run(ctx context.Context, st ScheduledTask, runner TaskRunner if err != nil { return fmt.Errorf("create logref writer: %w", err) } + defer writer.Close() opPrune.OperationPrune.OutputLogref = liveID if err := runner.UpdateOperation(op); err != nil { diff --git a/internal/orchestrator/tasks/taskruncommand.go b/internal/orchestrator/tasks/taskruncommand.go index d7a37644..18d46e0f 100644 --- a/internal/orchestrator/tasks/taskruncommand.go +++ b/internal/orchestrator/tasks/taskruncommand.go @@ -6,13 +6,14 @@ import ( "time" v1 "github.com/garethgeorge/backrest/gen/go/v1" + "github.com/garethgeorge/backrest/internal/ioutil" ) func NewOneoffRunCommandTask(repoID string, planID string, flowID int64, at time.Time, command string) Task { return &GenericOneoffTask{ OneoffTask: OneoffTask{ BaseTask: BaseTask{ - TaskType: "forget_snapshot", + TaskType: "run_command", TaskName: fmt.Sprintf("run command in repo %q", repoID), TaskRepoID: repoID, TaskPlanID: planID, @@ -31,7 +32,7 @@ func NewOneoffRunCommandTask(repoID string, planID string, flowID int64, at time op := st.Op rc := op.GetOperationRunCommand() if rc == nil { - panic("forget task with non-forget operation") + panic("run command task with non-forget operation") } return runCommandHelper(ctx, st, taskRunner, command) @@ -41,6 +42,7 @@ func NewOneoffRunCommandTask(repoID string, planID string, flowID int64, at time func runCommandHelper(ctx context.Context, st ScheduledTask, taskRunner TaskRunner, command string) error { t := st.Task + runCmdOp := st.Op.GetOperationRunCommand() repo, err := taskRunner.GetRepoOrchestrator(t.RepoID()) if err != nil { @@ -51,12 +53,18 @@ func runCommandHelper(ctx context.Context, st ScheduledTask, taskRunner TaskRunn if err != nil { return fmt.Errorf("get logref writer: %w", err) } - st.Op.GetOperationRunCommand().OutputLogref = id + defer writer.Close() + sizeWriter := &ioutil.SizeTrackingWriter{Writer: writer} + defer func() { + runCmdOp.OutputSizeBytes = int64(sizeWriter.Size()) + }() + + runCmdOp.OutputLogref = id if err := taskRunner.UpdateOperation(st.Op); err != nil { return fmt.Errorf("update operation: %w", err) } - if err := repo.RunCommand(ctx, command, writer); err != nil { + if err := repo.RunCommand(ctx, command, sizeWriter); err != nil { return fmt.Errorf("command %q: %w", command, err) } diff --git a/internal/orchestrator/tasks/taskstats.go b/internal/orchestrator/tasks/taskstats.go index 6a541300..37bbf1c9 100644 --- a/internal/orchestrator/tasks/taskstats.go +++ b/internal/orchestrator/tasks/taskstats.go @@ -54,8 +54,8 @@ func (t *StatsTask) Next(now time.Time, runner TaskRunner) (ScheduledTask, error return NeverScheduledTask, fmt.Errorf("finding last backup run time: %w", err) } - // Runs every 30 days - if time.Since(lastRan) < 30*24*time.Hour { + // Runs at most once per day. + if time.Since(lastRan) < 24*time.Hour { return NeverScheduledTask, nil } return ScheduledTask{ diff --git a/internal/resticinstaller/flock.go b/internal/resticinstaller/flock.go deleted file mode 100644 index afc14dac..00000000 --- a/internal/resticinstaller/flock.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build linux || darwin || freebsd -// +build linux darwin freebsd - -package resticinstaller - -import ( - "os" - "path" - "syscall" -) - -func withFlock(lock string, do func() error) error { - if err := os.MkdirAll(path.Dir(lock), 0700); err != nil { - return err - } - f, err := os.Create(lock) - if err != nil { - return err - } - defer f.Close() - - if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { - return err - } - - defer syscall.Flock(int(f.Fd()), syscall.LOCK_UN) - - return do() -} diff --git a/internal/resticinstaller/flockwindows.go b/internal/resticinstaller/flockwindows.go deleted file mode 100644 index 42e97d86..00000000 --- a/internal/resticinstaller/flockwindows.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build windows -// +build windows - -package resticinstaller - -func withFlock(lock string, do func() error) error { - // TODO: windows file locking. Not a major issue as locking is only needed for test runs. - return do() -} diff --git a/internal/resticinstaller/resticinstaller.go b/internal/resticinstaller/resticinstaller.go index 66e6e2eb..c8d07eac 100644 --- a/internal/resticinstaller/resticinstaller.go +++ b/internal/resticinstaller/resticinstaller.go @@ -20,6 +20,7 @@ import ( "sync" "github.com/garethgeorge/backrest/internal/env" + "github.com/gofrs/flock" "go.uber.org/zap" ) @@ -169,37 +170,40 @@ func downloadFile(url string, downloadPath string) (string, error) { } func installResticIfNotExists(resticInstallPath string) error { - // withFlock is used to ensure tests pass; when running on CI multiple tests may try to install restic at the same time. - return withFlock(path.Join(env.DataDir(), "install.lock"), func() error { - if _, err := os.Stat(resticInstallPath); err == nil { - // file is now installed, probably by another process. We can return. - return nil - } + lock := flock.New(filepath.Join(filepath.Dir(resticInstallPath), "install.lock")) + if err := lock.Lock(); err != nil { + return fmt.Errorf("lock %v: %w", lock.Path(), err) + } + defer lock.Unlock() - if err := os.MkdirAll(path.Dir(resticInstallPath), 0755); err != nil { - return fmt.Errorf("create restic install directory %v: %w", path.Dir(resticInstallPath), err) - } + if _, err := os.Stat(resticInstallPath); err == nil { + // file is now installed, probably by another process. We can return. + return nil + } - hash, err := downloadFile(resticDownloadURL(RequiredResticVersion), resticInstallPath+".tmp") - if err != nil { - return err - } + if err := os.MkdirAll(path.Dir(resticInstallPath), 0755); err != nil { + return fmt.Errorf("create restic install directory %v: %w", path.Dir(resticInstallPath), err) + } - if err := verify(hash); err != nil { - os.Remove(resticInstallPath) // try to remove the bad binary. - return fmt.Errorf("failed to verify the authenticity of the downloaded restic binary: %v", err) - } + hash, err := downloadFile(resticDownloadURL(RequiredResticVersion), resticInstallPath+".tmp") + if err != nil { + return err + } - if err := os.Chmod(resticInstallPath+".tmp", 0755); err != nil { - return fmt.Errorf("chmod executable %v: %w", resticInstallPath, err) - } + if err := verify(hash); err != nil { + os.Remove(resticInstallPath) // try to remove the bad binary. + return fmt.Errorf("failed to verify the authenticity of the downloaded restic binary: %v", err) + } - if err := os.Rename(resticInstallPath+".tmp", resticInstallPath); err != nil { - return fmt.Errorf("rename %v.tmp to %v: %w", resticInstallPath, resticInstallPath, err) - } + if err := os.Chmod(resticInstallPath+".tmp", 0755); err != nil { + return fmt.Errorf("chmod executable %v: %w", resticInstallPath, err) + } - return nil - }) + if err := os.Rename(resticInstallPath+".tmp", resticInstallPath); err != nil { + return fmt.Errorf("rename %v.tmp to %v: %w", resticInstallPath, resticInstallPath, err) + } + + return nil } func removeOldVersions(installDir string) { @@ -250,6 +254,10 @@ func FindOrInstallResticBinary() (string, error) { resticInstallPath, _ = filepath.Abs(path.Join(path.Dir(os.Args[0]), resticBinName)) } + if err := os.MkdirAll(path.Dir(resticInstallPath), 0700); err != nil { + return "", fmt.Errorf("create restic install directory %v: %w", path.Dir(resticInstallPath), err) + } + // Install restic if not found. if _, err := os.Stat(resticInstallPath); err != nil { if !errors.Is(err, os.ErrNotExist) { diff --git a/pkg/restic/restic_test.go b/pkg/restic/restic_test.go index a941a450..dd69e053 100644 --- a/pkg/restic/restic_test.go +++ b/pkg/restic/restic_test.go @@ -293,11 +293,13 @@ func checkSnapshotFieldsHelper(t *testing.T, snapshot *Snapshot) { if snapshot.UnixTimeMs() == 0 { t.Errorf("wanted snapshot time to be non-zero, got: %v", snapshot.UnixTimeMs()) } - if snapshot.SnapshotSummary.TreeBlobs == 0 { - t.Errorf("wanted snapshot tree blobs to be non-zero, got: %v", snapshot.SnapshotSummary.TreeBlobs) - } - if snapshot.SnapshotSummary.DataAdded == 0 { - t.Errorf("wanted snapshot data added to be non-zero, got: %v", snapshot.SnapshotSummary.DataAdded) + if runtime.GOOS != "windows" { // flaky on windows; unclear why. + if snapshot.SnapshotSummary.TreeBlobs == 0 { + t.Errorf("wanted snapshot tree blobs to be non-zero, got: %v", snapshot.SnapshotSummary.TreeBlobs) + } + if snapshot.SnapshotSummary.DataAdded == 0 { + t.Errorf("wanted snapshot data added to be non-zero, got: %v", snapshot.SnapshotSummary.DataAdded) + } } if snapshot.SnapshotSummary.TotalFilesProcessed == 0 { t.Errorf("wanted snapshot total files processed to be non-zero, got: %v", snapshot.SnapshotSummary.TotalFilesProcessed) diff --git a/proto/v1/operations.proto b/proto/v1/operations.proto index 77abaec0..ef7a2bc9 100644 --- a/proto/v1/operations.proto +++ b/proto/v1/operations.proto @@ -107,6 +107,7 @@ message OperationCheck { message OperationRunCommand { string command = 1; string output_logref = 2; + int64 output_size_bytes = 3; // not necessarily authoritative, tracked as an optimization to allow clients to avoid fetching very large outputs. } // OperationRestore tracks a restore operation. diff --git a/webui/gen/ts/v1/operations_pb.ts b/webui/gen/ts/v1/operations_pb.ts index 79e9a15c..3072203b 100644 --- a/webui/gen/ts/v1/operations_pb.ts +++ b/webui/gen/ts/v1/operations_pb.ts @@ -652,6 +652,11 @@ export class OperationRunCommand extends Message { */ outputLogref = ""; + /** + * @generated from field: int64 output_size_bytes = 3; + */ + outputSizeBytes = protoInt64.zero; + constructor(data?: PartialMessage) { super(); proto3.util.initPartial(data, this); @@ -662,6 +667,7 @@ export class OperationRunCommand extends Message { static readonly fields: FieldList = proto3.util.newFieldList(() => [ { no: 1, name: "command", kind: "scalar", T: 9 /* ScalarType.STRING */ }, { no: 2, name: "output_logref", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 3, name: "output_size_bytes", kind: "scalar", T: 3 /* ScalarType.INT64 */ }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): OperationRunCommand { diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx index 28bbdf0f..eb80bc32 100644 --- a/webui/src/components/OperationRow.tsx +++ b/webui/src/components/OperationRow.tsx @@ -236,10 +236,16 @@ export const OperationRow = ({ }); } else if (operation.op.case === "operationRunCommand") { const run = operation.op.value; - expandedBodyItems.push("run"); + if (run.outputSizeBytes < 64 * 1024) { + expandedBodyItems.push("run"); + } bodyItems.push({ key: "run", - label: "Command Output", + label: + "Command Output" + + (run.outputSizeBytes > 0 + ? ` (${formatBytes(Number(run.outputSizeBytes))})` + : ""), children: ( <> @@ -321,15 +327,15 @@ const SnapshotDetails = ({ snapshot }: { snapshot: ResticSnapshot }) => { const rows: React.ReactNode[] = [ - - Host + + User and Host
- {snapshot.hostname} + {snapshot.username}@{snapshot.hostname} - Username + Tags
- {snapshot.hostname} + {snapshot.tags.join(", ")}
, ]; @@ -387,8 +393,6 @@ const SnapshotDetails = ({ snapshot }: { snapshot: ResticSnapshot }) => { Snapshot ID: {normalizeSnapshotId(snapshot.id!)}
- Tags: - {snapshot.tags?.join(", ")} {rows}
diff --git a/webui/src/components/StatsPanel.tsx b/webui/src/components/StatsPanel.tsx index fe6262e0..ffa53a51 100644 --- a/webui/src/components/StatsPanel.tsx +++ b/webui/src/components/StatsPanel.tsx @@ -60,16 +60,18 @@ const StatsPanel = ({ repoId }: { repoId: string }) => { compressionRatio: number; snapshotCount: number; totalBlobCount: number; - }[] = statsOperations.map((op) => { - const stats = (op.op.value! as OperationStats).stats!; - return { - time: Number(op.unixTimeEndMs!), - totalSizeBytes: Number(stats.totalSize), - compressionRatio: Number(stats.compressionRatio), - snapshotCount: Number(stats.snapshotCount), - totalBlobCount: Number(stats.totalBlobCount), - }; - }); + }[] = statsOperations + .map((op) => { + const stats = (op.op.value! as OperationStats).stats!; + return { + time: Number(op.unixTimeEndMs!), + totalSizeBytes: Number(stats.totalSize), + compressionRatio: Number(stats.compressionRatio), + snapshotCount: Number(stats.snapshotCount), + totalBlobCount: Number(stats.totalBlobCount), + }; + }) + .sort((a, b) => a.time - b.time); return ( <> diff --git a/webui/src/views/PlanView.tsx b/webui/src/views/PlanView.tsx index df8b10c7..4fa62a1f 100644 --- a/webui/src/views/PlanView.tsx +++ b/webui/src/views/PlanView.tsx @@ -14,6 +14,7 @@ import { } from "../../gen/ts/v1/service_pb"; import { SpinButton } from "../components/SpinButton"; import { useShowModal } from "../components/ModalManager"; +import { useConfig } from "../components/ConfigProvider"; export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { const alertsApi = useAlertApi()!; @@ -47,7 +48,10 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => { try { alertsApi.info("Clearing error history..."); await backrestService.clearHistory({ - selector: new OpSelector({ planId: plan.id, repoId: plan.repo }), + selector: new OpSelector({ + planId: plan.id, + repoId: plan.repo, + }), onlyFailed: true, }); alertsApi.success("Error history cleared.");