diff --git a/.docker/entrypoint.sh b/.docker/entrypoint.sh index fb5e265..88b964a 100755 --- a/.docker/entrypoint.sh +++ b/.docker/entrypoint.sh @@ -1,9 +1,25 @@ #!/usr/bin/env bash set -e -SD=".scroll" +SD="./" input=$@ +# Migrate legacy .scroll layout: +# Before: .scroll/ + +# After: + data/ +if [ -d "${SD}.scroll" ]; then + echo "Migrating legacy .scroll layout..." + mkdir -p "${SD}data" + for item in "${SD}"* "${SD}".[!.]*; do + [ -e "$item" ] || continue + name=$(basename "$item") + [ "$name" != "data" ] && [ "$name" != ".scroll" ] && mv "$item" "${SD}data/" + done + mv "${SD}.scroll"/* "${SD}" + rm -rf "${SD}.scroll" + echo "Legacy migration complete" +fi + echo "Druid Version: $(druid version)" if [ "${ENSURE_NIX}" = "true" ]; diff --git a/.vscode/launch.json b/.vscode/launch.json index 6b28b24..6a2c10b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,36 +2,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Debug Daemon backup", - "type": "go", - "request": "launch", - "mode": "debug", - "console": "integratedTerminal", - "program": "${workspaceFolder}/main.go", - "args": [ - "backup", - "--cwd", - "${workspaceFolder}/examples/minecraft", - "testtest.tgz" - ], - "envFile": "${workspaceFolder}/.env", - }, - { - "name": "Debug Daemon restore", - "type": "go", - "request": "launch", - "mode": "debug", - "console": "integratedTerminal", - "program": "${workspaceFolder}/main.go", - "args": [ - "restore", - "--cwd", - "${workspaceFolder}/examples/restore-test", - "https://fsn1.your-objectstorage.com/druid-deployment-assets/lgsm/ark-snapshot-latest.tar.gz" - ] - }, - { - "name": "Debug Daemon serve (restore example)", + "name": "Serve (scroll-cwd)", "type": "go", "request": "launch", "mode": "debug", @@ -39,247 +10,14 @@ "program": "${workspaceFolder}/main.go", "args": [ "serve", - "--cwd", - "${workspaceFolder}/examples/ark-restore", - "--init-snapshot-url", "https://fsn1.your-objectstorage.com/druid-deployment-assets/lgsm/ark-snapshot-latest.tar.gz", - "-p", - "9190", - "artifacts.druid.gg/druid-team/scroll-lgsm:arkserver", - "--coldstarter=true", - "--watch-ports=false", - "--allow-plugin-errors" - ], - "env": { - "DRUID_PORT_QUERY": "2556", - "DRUID_PORT_MAIN": "7771" - } - }, - { - "name": "Debug Daemon serve (restore example no snap)", - "type": "go", - "request": "launch", - "mode": "debug", - "console": "integratedTerminal", - "program": "${workspaceFolder}/main.go", - "args": [ - "serve", - "--cwd", - "${workspaceFolder}/examples/ark-restore-t", - "-p", - "9190", - "artifacts.druid.gg/druid-team/scroll-minecraft-spigot:1.21.1", - "--coldstarter", - "--init-snapshot-url", "nil" - ], - "env": { - "DRUID_PORT_QUERY": "2556", - "DRUID_PORT_MAIN": "7771" - } - }, - { - "name": "Debug Daemon serve (htop example)", - "type": "go", - "request": "launch", - "mode": "debug", - "console": "integratedTerminal", - "program": "${workspaceFolder}/main.go", - "args": [ - "serve", - "--cwd", - "${workspaceFolder}/examples/htop", - "-p", - "9190" - ], - }, - { - "name": "Debug Daemon serve (minecraft example)", - "type": "go", - "request": "launch", - "mode": "debug", - "console": "integratedTerminal", - "program": "${workspaceFolder}/main.go", - "args": [ - "serve", - "--cwd", - "${workspaceFolder}/examples/minecraft", - "--watch-ports", - "-p", - "9190", - "--pprof", - ":6060" - ], - }, - { - "name": "Debug Daemon serve --idle (minecraft example)", - "type": "go", - "request": "launch", - "mode": "debug", - "console": "integratedTerminal", - "program": "${workspaceFolder}/main.go", - "args": [ - "serve", - "--cwd", - "${workspaceFolder}/examples/minecraft", - "--watch-ports", - "-p", - "9190", - "--idle" - ], - }, - { - "name": "Debug Daemon serve --coldstarter (minecraft example)", - "type": "go", - "request": "launch", - "mode": "debug", - "console": "integratedTerminal", - "program": "${workspaceFolder}/main.go", - "args": [ - "serve", - "--cwd", - "${workspaceFolder}/examples/minecraft", - "--coldstarter", - "-p", - "9190", - ], - "env": { - "DRUID_PORT_QUERY": "2556", - "DRUID_PORT_MAIN": "7771", - "LOG_LEVEL": "debug" - } - }, - { - "name": "Debug Daemon serve --coldstarter (gmod example)", - "type": "go", - "request": "launch", - "mode": "debug", - "console": "integratedTerminal", - "program": "${workspaceFolder}/main.go", - "args": [ - "serve", - "artifacts.druid.gg/druid-team/scroll-lgsm:pzserver", - "--cwd", - "${workspaceFolder}/examples/gmod", - "--coldstarter", - "-p", - "9190", - ], - "env": { - "DRUID_PORT_QUERY": "2556", - "DRUID_PORT_MAIN": "7771", - "LOG_LEVEL": "debug" - } - }, - { - "name": "Debug Daemon coldstart (minecraft example)", - "type": "go", - "request": "launch", - "mode": "debug", - "console": "integratedTerminal", - "program": "${workspaceFolder}/main.go", - "args": [ - "coldstarter", - "--cwd", - "${workspaceFolder}/examples/minecraft", - "--pprof", - ":6060" - ], - "env": { - "DRUID_PORT_QUERY": "2556", - "DRUID_PORT_MAIN": "7771" - } - }, - { - "name": "Debug Daemon coldstart (minecraft2 example)", - "type": "go", - "request": "launch", - "mode": "debug", - "console": "integratedTerminal", - "program": "${workspaceFolder}/main.go", - "args": [ - "coldstarter", - "--cwd", - "${workspaceFolder}/examples/minecraft2", - ], - }, - { - "name": "Debug Daemon serve (nginx example)", - "type": "go", - "request": "launch", - "mode": "debug", - "console": "integratedTerminal", - "program": "${workspaceFolder}/main.go", - "args": [ - "serve", - "--cwd", - "${workspaceFolder}/examples/nginx", - "-p", - "9190" - ], - }, - { - "name": "Debug Daemon serve (sleep install example)", - "type": "go", - "request": "launch", - "mode": "debug", - "console": "integratedTerminal", - "program": "${workspaceFolder}/main.go", - "args": [ - "serve", - "--cwd", - "${workspaceFolder}/examples/long-install", - "-p", - "9190" - ], - }, - { - "name": "Debug Daemon run install ark", - "type": "go", - "request": "launch", - "mode": "debug", - "console": "integratedTerminal", - "program": "${workspaceFolder}/main.go", - "args": [ - "run", - "install", - "--cwd", - "${workspaceFolder}/examples/ark" - ], - }, - { - "name": "Debug Daemon serve (emtpy dir)", - "type": "go", - "request": "launch", - "mode": "debug", - "console": "integratedTerminal", - "program": "${workspaceFolder}/main.go", - "args": [ - "serve", - "--additional-endpoints", - "annotations", - "--cwd", - "${workspaceFolder}/examples/scroll-cwd", - "--coldstarter", - "-p", - "9190", - "artifacts.druid.gg/druid-team/scroll-minecraft-spigot:1.19.1" - ], - }, - { - "name": "Debug Daemon run (minecraft example)", - "type": "go", - "request": "launch", - "mode": "debug", - "console": "integratedTerminal", - "program": "${workspaceFolder}/main.go", - "args": [ - "run", - "--cwd", - "${workspaceFolder}/examples/minecraft", - "start" - ], + "--cwd", "${workspaceFolder}/examples/scroll-cwd", + "--additional-endpoints", "annotations", + "--allow-plugin-errors", + "-p", "9190" + ] }, { - "name": "Debug Daemon run (touch example)", + "name": "Run (scroll-cwd)", "type": "go", "request": "launch", "mode": "debug", @@ -287,72 +25,49 @@ "program": "${workspaceFolder}/main.go", "args": [ "run", - "--cwd", - "${workspaceFolder}/examples/touch", + "--cwd", "${workspaceFolder}/examples/scroll-cwd", "start" - ], - }, - { - "name": "Debug Daemon run registry push", - "type": "go", - "request": "launch", - "mode": "debug", - "program": "${workspaceFolder}/main.go", - "args": [ - "registry", - "push", - "--cwd", - "${workspaceFolder}/examples/minecraft/", ".scroll", - "-c", "1", - "-r", "1G", - "-d", "1G", - "-i", "highcard/lol", - "-p", "ftp", - "-p", "http=80", - "-p", "dns=53/udp", - ], + ] }, { - "name": "Debug Daemon run registry pull", + "name": "Registry Push (scroll-cwd)", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceFolder}/main.go", "args": [ - "registry", - "pull", - "--cwd", - "${workspaceFolder}/examples/scroll-cwd-pull/", "artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.20.1", - ], + "registry", "push", + "--cwd", "${workspaceFolder}/examples/scroll-cwd" + ] }, { - "name": "Debug Daemon run registry push meta", + "name": "Registry Pull (scroll-cwd-pull)", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceFolder}/main.go", "args": [ - "registry", - "push", - "meta", - "registry-1.docker.io/highcard/scroll-minecraft-forge1", "meta", - "--cwd", - "${workspaceFolder}/examples/", - ], + "registry", "pull", + "--full", + "--cwd", "${workspaceFolder}/examples/scroll-cwd-pull", + "artifacts.druid.gg/druid-team/scroll-minecraft-forge:1.20.1-test" + ] }, { - "name": "Debug Daemon port", + "name": "Registry Login", "type": "go", "request": "launch", "mode": "debug", "program": "${workspaceFolder}/main.go", "args": [ - "port", - "3000", "9090" - ], + "registry", "login", + "--host", "artifacts.druid.gg", + "-u", "$REGISTRY_USER", + "-p", "$REGISTRY_PASS" + ] }, { - "name": "Remote Debug Daemon serve", + "name": "Remote Attach", "type": "go", "request": "attach", "mode": "remote", @@ -360,9 +75,7 @@ "cwd": "${workspaceFolder}", "port": 2345, "host": "127.0.0.1", - "trace": "verbose", "debugAdapter": "legacy" - }, { "name": "Test Current File", @@ -370,31 +83,8 @@ "request": "launch", "mode": "test", "program": "${file}", - "env": {}, - "args": [], - "showLog": true - }, - { - "name": "Attach", - "type": "go", - "request": "attach", - "mode": "remote", - "remotePath": "", - "port":2345, - "host":"127.0.0.1", - "showLog": true, - "trace": "log", - "logOutput": "rpc" - }, - { - "name": "Debug current file", - "type": "go", - "request": "launch", - "mode": "debug", - "program": "${file}", - "env": {}, "args": [], "showLog": true } ] -} \ No newline at end of file +} diff --git a/cmd/backup.go b/cmd/backup.go deleted file mode 100644 index 4941164..0000000 --- a/cmd/backup.go +++ /dev/null @@ -1,73 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - - "github.com/highcard-dev/daemon/internal/core/ports" - "github.com/highcard-dev/daemon/internal/core/services" - snapshotServices "github.com/highcard-dev/daemon/internal/core/services/snapshot" - - "github.com/spf13/cobra" -) - -var compressionLevel int - -var s3AccessKey string -var s3SecretKey string -var s3Bucket string -var s3Endpoint string -var backupInsecure bool - -var BackupCmd = &cobra.Command{ - Use: "backup", - Short: "Backup the current scroll", - Long: "Backup the current scroll, by specifying the s3 bucket to backup to or a pre-signed url", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - - destination := args[0] - - scrollService, err := services.NewScrollService(cwd) - if scrollService == nil { - return fmt.Errorf("failed to load scroll - %w", err) - } - - snapshotService := snapshotServices.NewSnapshotService() - - var s3Destination ports.S3Destination - - if s3AccessKey != "" && s3SecretKey != "" && s3Bucket != "" { - s3Destination = ports.S3Destination{ - AccessKey: s3AccessKey, - SecretKey: s3SecretKey, - Bucket: s3Bucket, - Endpoint: s3Endpoint, - Insecure: backupInsecure, - } - } - - options := ports.SnapshotOptions{ - CompressionLevel: compressionLevel, - S3Destination: &s3Destination, - } - - return snapshotService.Snapshot(scrollService.GetCwd(), destination, options) - }, -} - -func init() { - - accessKeyEnv := os.Getenv("DRUID_S3_ACCESS_KEY") - secretKeyEnv := os.Getenv("DRUID_S3_SECRET_KEY") - bucketEnv := os.Getenv("DRUID_S3_BUCKET") - endpointEnv := os.Getenv("DRUID_S3_ENDPOINT") - - BackupCmd.Flags().IntVarP(&compressionLevel, "compression-level", "", 1, "Compression level for the backup") - BackupCmd.Flags().StringVarP(&s3AccessKey, "s3-access-key", "", accessKeyEnv, "S3 access key. You can also set the DRUID_S3_ACCESS_KEY environment variable") - BackupCmd.Flags().StringVarP(&s3SecretKey, "s3-secret-key", "", secretKeyEnv, "S3 secret key. You can also set the DRUID_S3_SECRET_KEY environment variable") - BackupCmd.Flags().StringVarP(&s3Bucket, "s3-bucket", "", bucketEnv, "S3 bucket. You can also set the DRUID_S3_BUCKET environment variable") - BackupCmd.Flags().StringVarP(&s3Endpoint, "s3-endpoint", "", endpointEnv, "S3 endpoint. You can also set the DRUID_S3_ENDPOINT environment variable") - BackupCmd.Flags().BoolVarP(&backupInsecure, "insecure", "", false, "Use http instead of https for the S3 endpoint. You can also set the DRUID_S3_DISABLE_SECURE environment variable to true to disable secure connections") - -} diff --git a/cmd/coldstarter.go b/cmd/coldstarter.go index 18ea693..f26960d 100644 --- a/cmd/coldstarter.go +++ b/cmd/coldstarter.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/highcard-dev/daemon/internal/core/services" - progressService "github.com/highcard-dev/daemon/internal/core/services/snapshot" "github.com/highcard-dev/daemon/internal/utils/logger" "github.com/spf13/cobra" "go.uber.org/zap" @@ -32,9 +31,7 @@ var ColdstarterCmd = &cobra.Command{ portService := services.NewPortServiceWithScrollFile(¤tScroll.File) - snapshotService := progressService.NewSnapshotService() - - coldStarter := services.NewColdStarter(portService, nil, snapshotService, scrollService.GetDir()) + coldStarter := services.NewColdStarter(portService, nil, scrollService.GetDir()) finish := coldStarter.Start(context.TODO()) <-finish diff --git a/cmd/registry_pull.go b/cmd/registry_pull.go index 20857aa..3c166c7 100644 --- a/cmd/registry_pull.go +++ b/cmd/registry_pull.go @@ -1,14 +1,14 @@ package cmd import ( - "path/filepath" - "github.com/highcard-dev/daemon/internal/core/services/registry" "github.com/highcard-dev/daemon/internal/utils/logger" "github.com/spf13/cobra" "github.com/spf13/viper" ) +var full bool + var PullCommand = &cobra.Command{ Use: "pull", Short: "Generate OCI Artifacts and push to a remote registry", @@ -23,7 +23,7 @@ var PullCommand = &cobra.Command{ registryClient := registry.NewOciClient(host, user, password) - err := registryClient.Pull(filepath.Join(cwd, ".scroll"), artifact) + err := registryClient.PullSelective(cwd, artifact, full, nil) if err != nil { logger.Log().Error("Failed to pull from registry") return err @@ -35,4 +35,5 @@ var PullCommand = &cobra.Command{ } func init() { + PullCommand.Flags().BoolVarP(&full, "full", "", false, "Download full scroll with data files") } diff --git a/cmd/registry_push.go b/cmd/registry_push.go index eccc0bb..9308d5f 100644 --- a/cmd/registry_push.go +++ b/cmd/registry_push.go @@ -79,7 +79,7 @@ var PushCommand = &cobra.Command{ Image: image, Ports: ps, Smart: smart, - }, packMeta) + }, packMeta, &scroll.File) if err != nil { tries++ logger.Log().Error("Failed to push scroll to registry, retrying...", zap.Error(err), zap.Int("tries", tries)) diff --git a/cmd/restore.go b/cmd/restore.go deleted file mode 100644 index 1c9da69..0000000 --- a/cmd/restore.go +++ /dev/null @@ -1,35 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/highcard-dev/daemon/internal/core/ports" - "github.com/highcard-dev/daemon/internal/core/services" - snapshotServices "github.com/highcard-dev/daemon/internal/core/services/snapshot" - "github.com/spf13/cobra" -) - -var RestoreCmd = &cobra.Command{ - Use: "restore", - Short: "Restore the current scroll", - Long: "Restore the current scroll, by specifying the s3 bucket to backup to or a pre-signed url", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - - source := args[0] - - scrollService, err := services.NewScrollService(cwd) - if scrollService == nil { - return fmt.Errorf("failed to load scroll - %w", err) - } - - snapshotService := snapshotServices.NewSnapshotService() - - options := ports.RestoreSnapshotOptions{} - - return snapshotService.RestoreSnapshot(scrollService.GetCwd(), source, options) - }, -} - -func init() { -} diff --git a/cmd/root.go b/cmd/root.go index 478e5f1..5a49a7b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -35,8 +35,6 @@ func init() { RootCmd.AddCommand(ScrollCmd) RootCmd.AddCommand(PortMonitorCmd) RootCmd.AddCommand(ColdstarterCmd) - RootCmd.AddCommand(BackupCmd) - RootCmd.AddCommand(RestoreCmd) c, _ := os.Getwd() diff --git a/cmd/run.go b/cmd/run.go index e577802..965a8e1 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -5,7 +5,6 @@ import ( "github.com/highcard-dev/daemon/internal/core/services" "github.com/highcard-dev/daemon/internal/core/services/registry" - snapshotService "github.com/highcard-dev/daemon/internal/core/services/snapshot" "github.com/highcard-dev/daemon/internal/utils/logger" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -46,10 +45,8 @@ var RunCmd = &cobra.Command{ } queueManager := services.NewQueueManager(scrollService, processLauncher) - snapshotService := snapshotService.NewSnapshotService() - go queueManager.Work() - _, err = initScroll(scrollService, snapshotService, processLauncher) + _, err = initScroll(scrollService, processLauncher) if err != nil { return fmt.Errorf("error initializing scroll: %w", err) } @@ -80,4 +77,5 @@ var RunCmd = &cobra.Command{ func init() { RunCmd.Flags().BoolVarP(&ignoreVersionCheck, "ignore-version-check", "", false, "Ignore version check") RunCmd.Flags().StringVarP(&dependencyResolution, "dependency-resolution", "", "auto", "Dependency resolution strategy. Valid values: auto, nix, external") + RunCmd.Flags().BoolVarP(&allowPluginErrors, "allow-plugin-errors", "", false, "Ignore plugin errors on startup") } diff --git a/cmd/serve.go b/cmd/serve.go index 48f6da2..bf41837 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -11,12 +11,11 @@ import ( "github.com/highcard-dev/daemon/cmd/server/web" "github.com/highcard-dev/daemon/internal/core/domain" - "github.com/highcard-dev/daemon/internal/core/ports" "github.com/highcard-dev/daemon/internal/core/services" "github.com/highcard-dev/daemon/internal/core/services/registry" - snapshotServices "github.com/highcard-dev/daemon/internal/core/services/snapshot" "github.com/highcard-dev/daemon/internal/handler" "github.com/highcard-dev/daemon/internal/signals" + "github.com/highcard-dev/daemon/internal/utils" "github.com/highcard-dev/daemon/internal/utils/logger" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -34,7 +33,6 @@ var watchPortsInterfaces []string var portInactivity uint var useColdstarter bool var maxStartupHealthCheckTimeout uint -var initSnapshotUrl string var skipArtifactDownload bool var allowPluginErrors bool var pprofBind string @@ -99,7 +97,7 @@ to interact and monitor the Scroll Application`, logger.Log().Info("Downloading " + artifact + " into " + scrollService.GetDir()) - err = client.Pull(scrollService.GetDir(), artifact) + err = client.PullSelective(scrollService.GetDir(), artifact, false, nil) if err != nil { return err } @@ -128,8 +126,32 @@ to interact and monitor the Scroll Application`, portService := services.NewPortServiceWithScrollFile(scrollService.GetFile()) - snapshotService := snapshotServices.NewSnapshotService() - coldStarter := services.NewColdStarter(portService, queueManager, snapshotService, scrollService.GetDir()) + coldStarter := services.NewColdStarter(portService, queueManager, scrollService.GetDir()) + + // Set up data pull on coldstarter finish + if artifact != "" { + coldStarter.OnBeforeFinish = func(progress *domain.SnapshotProgress) { + dataDir := scrollService.GetCwd() + dataExists, _ := utils.FileExists(dataDir) + if !dataExists { + logger.Log().Info("Data directory not found, pulling data from registry", zap.String("artifact", artifact)) + progress.Mode.Store("restore") + progress.Percentage.Store(0) + + err := client.PullSelective(scrollService.GetDir(), artifact, true, progress) + if err != nil { + logger.Log().Error("Failed to pull data from registry", zap.Error(err)) + progress.Mode.Store("noop") + } else { + logger.Log().Info("Data pull complete") + progress.Percentage.Store(100) + progress.Mode.Store("noop") + } + } else { + logger.Log().Info("Data directory already exists, skipping data pull") + } + } + } uiDevService := services.NewUiDevService( queueManager, scrollService, @@ -141,7 +163,7 @@ to interact and monitor the Scroll Application`, scrollMetricHandler := handler.NewScrollMetricHandler(scrollService, processMonitor) queueHandler := handler.NewQueueHandler(queueManager) portHandler := handler.NewPortHandler(portService) - healthHandler := handler.NewHealthHandler(portService, maxStartupHealthCheckTimeout, snapshotService) + healthHandler := handler.NewHealthHandler(portService, maxStartupHealthCheckTimeout) coldstarterHandler := handler.NewColdstarterHandler(coldStarter) uiDevHandler := handler.NewWatchHandler(uiDevService, scrollService) @@ -204,7 +226,7 @@ to interact and monitor the Scroll Application`, logger.Log().Info("Coldstarter done, starting scroll") - startup(scrollService, snapshotService, processLauncher, queueManager, portService, coldStarter, healthHandler, cwd, doneChan) + startup(scrollService, processLauncher, queueManager, portService, coldStarter, healthHandler, cwd, doneChan) portService.ResetOpenPorts() @@ -244,14 +266,13 @@ to interact and monitor the Scroll Application`, } } else { logger.Log().Warn("No ports to start, skipping coldstarter") - startup(scrollService, snapshotService, processLauncher, queueManager, portService, coldStarter, healthHandler, cwd, doneChan) + startup(scrollService, processLauncher, queueManager, portService, coldStarter, healthHandler, cwd, doneChan) } } else { - startup(scrollService, snapshotService, processLauncher, queueManager, portService, coldStarter, healthHandler, cwd, doneChan) + startup(scrollService, processLauncher, queueManager, portService, coldStarter, healthHandler, cwd, doneChan) } }() - } else { if useColdstarter { go coldStarter.Start(ctx) @@ -300,8 +321,6 @@ func init() { ServeCommand.Flags().UintVarP(&maxStartupHealthCheckTimeout, "max-health-check-startup-timeout", "", 60, "Sets the max amount of time the health check is allowed to take on startup. If the value is 0, there will be no timeout. This is useful to prevent the health check from blocking the startup of the daemon fully.") - ServeCommand.Flags().StringVarP(&initSnapshotUrl, "init-snapshot-url", "", "", "Snapshot to restore on startup") - ServeCommand.Flags().BoolVarP(&skipArtifactDownload, "skip-artifact-download", "", false, "Skip downloading the artifact on startup") ServeCommand.Flags().BoolVarP(&allowPluginErrors, "allow-plugin-errors", "", false, "Ignore plugin errors on startup") @@ -310,13 +329,13 @@ func init() { } -func startup(scrollService *services.ScrollService, snapshotService ports.SnapshotService, processLauncher *services.ProcedureLauncher, queueManager *services.QueueManager, portSerivce *services.PortMonitor, coldStarter *services.ColdStarter, healthHandler *handler.HealthHandler, cwd string, doneChan chan error) { +func startup(scrollService *services.ScrollService, processLauncher *services.ProcedureLauncher, queueManager *services.QueueManager, portSerivce *services.PortMonitor, coldStarter *services.ColdStarter, healthHandler *handler.HealthHandler, cwd string, doneChan chan error) { now := time.Now() healthHandler.Started = &now logger.Log().Info("Initializing scroll") - newScroll, err := initScroll(scrollService, snapshotService, processLauncher) + newScroll, err := initScroll(scrollService, processLauncher) if err != nil { doneChan <- err @@ -394,7 +413,7 @@ func startup(scrollService *services.ScrollService, snapshotService ports.Snapsh } -func initScroll(scrollService *services.ScrollService, snapshotService ports.SnapshotService, processLauncher *services.ProcedureLauncher) (bool, error) { +func initScroll(scrollService *services.ScrollService, processLauncher *services.ProcedureLauncher) (bool, error) { lock, err := scrollService.ReloadLock(ignoreVersionCheck) if err != nil { @@ -403,46 +422,7 @@ func initScroll(scrollService *services.ScrollService, snapshotService ports.Sna newScroll := len(lock.Statuses) == 0 - if newScroll { - - if initSnapshotUrl != "" { - logger.Log().Info("Starting restore process from Init Snapshot") - err := snapshotService.RestoreSnapshot(scrollService.GetCwd(), initSnapshotUrl, ports.RestoreSnapshotOptions{}) - - if err != nil { - return false, err - } - - logger.Log().Info("Restore process from Init Snapshot done") - - currentScroll, err := scrollService.ReloadScroll() - if err != nil { - return false, err - } - logger.Log().Info("Reloaded scroll after restore", zap.String("Name", currentScroll.Name), zap.Any("Version", currentScroll.Version), zap.String("AppVersion", currentScroll.AppVersion), zap.Any("Ports", currentScroll.Ports)) - - lock, err = scrollService.ReloadLock(ignoreVersionCheck) - if err != nil { - return false, err - } - - newScroll = len(lock.Statuses) == 0 - - //if we have a new scroll, we do this anyways, just ensure that the init command is there, also allready initialized stuff - if !newScroll { - lock.SetStatus(currentScroll.Init, domain.ScrollLockStatusWaiting, nil) - } - - } - - logger.Log().Info("Creating lock and bootstrapping files") - //There is an error here. We need to bootstrap the files before we render out the templates in the bootstrap func above - err := scrollService.CopyingInitFiles() - if err != nil { - return newScroll, err - } - - } else { + if !newScroll { logger.Log().Info("Found lock file, bootstrapping done") } diff --git a/cmd/update.go b/cmd/update.go index 5d80e08..e576b73 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -21,12 +21,11 @@ var UpdateCommand = &cobra.Command{ Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - scrollDir := utils.GetScrollDirFromCwd(cwd) var artifact string if len(args) > 0 { artifact = args[0] } else { - scroll, err := domain.NewScroll(scrollDir) + scroll, err := domain.NewScroll(cwd) if err != nil { return err @@ -47,7 +46,7 @@ var UpdateCommand = &cobra.Command{ canUpdate := false - fileName := utils.GetScrollDirFromCwd(cwd) + "/manifest.json" + fileName := (cwd) + "/manifest.json" b, err := os.ReadFile(fileName) if err != nil { @@ -69,7 +68,7 @@ var UpdateCommand = &cobra.Command{ if canUpdate { logger.Log().Info("Updated scroll files") - err = registryClient.Pull(scrollDir, artifact) + err = registryClient.Pull(cwd, artifact) if err != nil { return fmt.Errorf("error pulling scroll files: %v", err) } diff --git a/examples/.dockerignore b/examples/.dockerignore deleted file mode 100644 index 9aa334b..0000000 --- a/examples/.dockerignore +++ /dev/null @@ -1,6 +0,0 @@ -*.jar -*.tgz - -world - -!**/.scroll \ No newline at end of file diff --git a/examples/.gitignore b/examples/.gitignore deleted file mode 100644 index 111d7ce..0000000 --- a/examples/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -# Ignore everything: -* - -# Except for the themes directories: - -!*/.scroll/** \ No newline at end of file diff --git a/examples/htop/.scroll/scroll-lock.json b/examples/htop/.scroll/scroll-lock.json deleted file mode 100755 index 586c8c3..0000000 --- a/examples/htop/.scroll/scroll-lock.json +++ /dev/null @@ -1 +0,0 @@ -{"statuses":{},"scroll_version":"0.0.1","scroll_name":"registry-1.docker.io/highcard/scroll-htop"} \ No newline at end of file diff --git a/examples/htop/.scroll/scroll.yaml b/examples/htop/.scroll/scroll.yaml deleted file mode 100644 index c21c9d4..0000000 --- a/examples/htop/.scroll/scroll.yaml +++ /dev/null @@ -1,12 +0,0 @@ -name: registry-1.docker.io/highcard/scroll-htop -desc: Nginx server -version: 0.0.1 -app_version: latest -init: start" -commands: - start: - run: restart - procedures: - - mode: exec-tty - data: - - htop diff --git a/examples/long-install/.scroll/scroll.yaml b/examples/long-install/.scroll/scroll.yaml deleted file mode 100644 index 1064910..0000000 --- a/examples/long-install/.scroll/scroll.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: registry-1.docker.io/highcard/scroll-sleep -desc: Sleep test -version: 0.0.1 -app_version: 0.0.1 -init: "start" -commands: - start: - needs: [install] - run: restart - procedures: - - mode: exec - data: - - touch - - testfile - - install: - run: once - procedures: - - mode: exec - data: - - sleep - - "30" diff --git a/examples/meta/de-DE.md b/examples/meta/de-DE.md deleted file mode 100644 index 26e97b8..0000000 --- a/examples/meta/de-DE.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -name: Minecraft Forge -description: Forge ist eine mächtige Modding-API für Minecraft, die es Spielern ermöglicht, ihr Spielerlebnis durch erweiterte Mod-Unterstützung und Performance-Optimierungen zu verbessern. ---- - -Forge ist eine leistungsstarke und extrem flexible Modding-API für Minecraft, die eine breite Palette von Funktionen und Optimierungen für ein verbessertes Spielerlebnis bietet. Als eines der beliebtesten Tools für Minecraft-Modder ermöglicht Forge den Spielern, ein maßgeschneidertes und immersives Spielerlebnis zu schaffen. diff --git a/examples/meta/en-US.md b/examples/meta/en-US.md deleted file mode 100644 index bb16129..0000000 --- a/examples/meta/en-US.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -name: Minecraft Forge -description: Forge is a powerful modding API for Minecraft that allows players to enhance their gaming experience with advanced mod support and performance optimizations. ---- - -Forge is a powerful and highly flexible modding API for Minecraft, offering a wide range of features and optimizations for an improved gaming experience. As one of the most popular tools for Minecraft modders, Forge enables players to create a customized and immersive gaming experience. diff --git a/examples/minecraft/.scroll/json.lua b/examples/minecraft/json.lua similarity index 100% rename from examples/minecraft/.scroll/json.lua rename to examples/minecraft/json.lua diff --git a/examples/minecraft/.scroll/packet_handler/json.lua b/examples/minecraft/packet_handler/json.lua similarity index 100% rename from examples/minecraft/.scroll/packet_handler/json.lua rename to examples/minecraft/packet_handler/json.lua diff --git a/examples/minecraft/.scroll/packet_handler/minecraft.lua b/examples/minecraft/packet_handler/minecraft.lua similarity index 100% rename from examples/minecraft/.scroll/packet_handler/minecraft.lua rename to examples/minecraft/packet_handler/minecraft.lua diff --git a/examples/minecraft/.scroll/packet_handler/query.lua b/examples/minecraft/packet_handler/query.lua similarity index 100% rename from examples/minecraft/.scroll/packet_handler/query.lua rename to examples/minecraft/packet_handler/query.lua diff --git a/examples/minecraft/.scroll/scroll.yaml b/examples/minecraft/scroll.yaml similarity index 100% rename from examples/minecraft/.scroll/scroll.yaml rename to examples/minecraft/scroll.yaml diff --git a/examples/minecraft2/.scroll/scroll.yaml b/examples/minecraft2/.scroll/scroll.yaml deleted file mode 100644 index d10bd11..0000000 --- a/examples/minecraft2/.scroll/scroll.yaml +++ /dev/null @@ -1,50 +0,0 @@ -name: registry-1.docker.io/highcard/scroll-minecraft-vanilla -desc: Minecraft Vanilla -version: 0.0.1 -app_version: 1.20.6 -init: "start" -ports: - - name: minecraft - protocol: tcp - port: 25565 -commands: - start: - needs: [install] - run: restart - procedures: - - mode: exec - data: - - bash - - ./start.sh - stop: - procedures: - - mode: rcon - data: stop - install: - run: once - procedures: - - mode: exec - data: - - wget - - -q - - -O - - server.jar - - https://launcher.mojang.com/v1/objects/145ff0858209bcfc164859ba735d4199aafa1eea/server.jar - - mode: exec - data: - - bash - - -c - - echo eula=true > eula.txt - update: - procedures: - - mode: exec - data: - - sh - - $SCROLL_DIR/update.sh - - mode: exec - data: - - bash - - -c - - echo eula=true > eula.txt -plugins: - rcon: {} diff --git a/examples/nginx/.dockerignore b/examples/nginx/.dockerignore deleted file mode 100644 index 564440f..0000000 --- a/examples/nginx/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -scroll-lock.json \ No newline at end of file diff --git a/examples/nginx/.gitignore b/examples/nginx/.gitignore deleted file mode 100644 index 2601ed3..0000000 --- a/examples/nginx/.gitignore +++ /dev/null @@ -1 +0,0 @@ -scroll/scroll-lock.json \ No newline at end of file diff --git a/examples/nginx/Dockerfile b/examples/nginx/Dockerfile deleted file mode 100644 index 300dc4b..0000000 --- a/examples/nginx/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -#Dockerhub does not allow for setting of build args :/ -ARG IMAGE=nginx -FROM highcard/druidd-base:latest as base -FROM $IMAGE -WORKDIR /app - -RUN apt update -RUN apt install -y ca-certificates wget jq moreutils htop procps nano net-tools gcc make - -RUN wget https://go.dev/dl/go1.21.6.linux-arm64.tar.gz -RUN tar -C /usr/local -xzf go1.21.6.linux-arm64.tar.gz - - - -#/root/go/bin is not in the path -ENV PATH=$PATH:/root/go/bin -ENV PATH=$PATH:/usr/local/go/bin - -#install delve -RUN go install github.com/go-delve/delve/cmd/dlv@latest - -RUN wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq -RUN chmod +x /usr/bin/yq - -#RUN wget "https://github.com/highcard-dev/druidd/releases/download/latest/druid_linux_386.deb" -O "druidd.deb" -#RUN apt install ./druidd.deb - -RUN useradd -ms /bin/bash druid -USER root - -COPY --from=base /usr/bin/druid* /usr/bin -COPY --from=base /entrypoint.sh /entrypoint.sh - - - -ENTRYPOINT [ "/entrypoint.sh" ] \ No newline at end of file diff --git a/examples/nginx/.scroll/scroll-lock.json b/examples/nginx/scroll-lock.json old mode 100755 new mode 100644 similarity index 100% rename from examples/nginx/.scroll/scroll-lock.json rename to examples/nginx/scroll-lock.json diff --git a/examples/nginx/.scroll/scroll.yaml b/examples/nginx/scroll.yaml similarity index 100% rename from examples/nginx/.scroll/scroll.yaml rename to examples/nginx/scroll.yaml diff --git a/examples/scroll-cwd-pull/.scroll/init-files-template/.scroll_config.yml.scroll_template b/examples/scroll-cwd-pull/.scroll/init-files-template/.scroll_config.yml.scroll_template deleted file mode 100644 index 736724a..0000000 --- a/examples/scroll-cwd-pull/.scroll/init-files-template/.scroll_config.yml.scroll_template +++ /dev/null @@ -1,3 +0,0 @@ -rcon: - password: {{ randAlphaNum 50 }} - port: 25575 \ No newline at end of file diff --git a/examples/scroll-cwd-pull/annotations.json b/examples/scroll-cwd-pull/annotations.json new file mode 100644 index 0000000..7385c20 --- /dev/null +++ b/examples/scroll-cwd-pull/annotations.json @@ -0,0 +1 @@ +{"org.opencontainers.image.created":"2026-02-24T12:09:11Z"} \ No newline at end of file diff --git a/examples/scroll-cwd-pull/.scroll/init-files/run.sh b/examples/scroll-cwd-pull/data/run.sh similarity index 100% rename from examples/scroll-cwd-pull/.scroll/init-files/run.sh rename to examples/scroll-cwd-pull/data/run.sh diff --git a/examples/scroll-cwd-pull/.scroll/init-files/server.properties.scroll_template b/examples/scroll-cwd-pull/data/server.properties.scroll_template similarity index 100% rename from examples/scroll-cwd-pull/.scroll/init-files/server.properties.scroll_template rename to examples/scroll-cwd-pull/data/server.properties.scroll_template diff --git a/examples/scroll-cwd-pull/.scroll/init-files/update.sh b/examples/scroll-cwd-pull/data/update.sh old mode 100755 new mode 100644 similarity index 100% rename from examples/scroll-cwd-pull/.scroll/init-files/update.sh rename to examples/scroll-cwd-pull/data/update.sh diff --git a/examples/scroll-cwd-pull/.scroll/init-files/update_user_args.sh b/examples/scroll-cwd-pull/data/update_user_args.sh old mode 100755 new mode 100644 similarity index 100% rename from examples/scroll-cwd-pull/.scroll/init-files/update_user_args.sh rename to examples/scroll-cwd-pull/data/update_user_args.sh diff --git a/examples/scroll-cwd-pull/manifest.json b/examples/scroll-cwd-pull/manifest.json new file mode 100644 index 0000000..18bdd74 --- /dev/null +++ b/examples/scroll-cwd-pull/manifest.json @@ -0,0 +1 @@ +{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:6c9c78ae3f0395028791a99e7fe4990e7ff2d8e8ba07ecea7d46ba9894d3c48d","size":3673} \ No newline at end of file diff --git a/examples/scroll-cwd-pull/packet_handler/json.lua b/examples/scroll-cwd-pull/packet_handler/json.lua new file mode 100644 index 0000000..54d4448 --- /dev/null +++ b/examples/scroll-cwd-pull/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/examples/scroll-cwd-pull/packet_handler/minecraft.lua b/examples/scroll-cwd-pull/packet_handler/minecraft.lua new file mode 100644 index 0000000..34bcfa9 --- /dev/null +++ b/examples/scroll-cwd-pull/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "noop" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " + format("%.2f", snapshotPercentage) + "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " + format("%.2f", snapshotPercentage) + "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/examples/scroll-cwd-pull/.scroll/scroll.yaml b/examples/scroll-cwd-pull/scroll.yaml similarity index 73% rename from examples/scroll-cwd-pull/.scroll/scroll.yaml rename to examples/scroll-cwd-pull/scroll.yaml index 05b17fb..758006b 100644 --- a/examples/scroll-cwd-pull/.scroll/scroll.yaml +++ b/examples/scroll-cwd-pull/scroll.yaml @@ -1,12 +1,23 @@ name: artifacts.druid.gg/druid-team/scroll-minecraft-forge desc: Minecraft Forge version: 0.0.1 -app_version: 1.20.1 +app_version: 1.20.1-test +ports: + - name: main + protocol: tcp + port: 25565 + sleep_handler: packet_handler/minecraft.lua + start_delay: 10 + finish_after_command: install + - name: rcon + protocol: tcp + port: 25575 init: "start" commands: start: needs: [install] run: restart + dependencies: [jdk17] procedures: - mode: exec data: @@ -22,6 +33,7 @@ commands: data: stop install: run: once + dependencies: [wget, cacert, jdk17] procedures: - mode: exec data: @@ -29,7 +41,7 @@ commands: - -q - -O - forge-installer.jar - - https://s3.eu-central-1.wasabisys.com/druid-scroll-artifacts/minecraft/forge/forge-1.20.1.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/forge/forge-1.20.1.jar - mode: exec data: - java diff --git a/examples/scroll-cwd-pull/.scroll/update/.gitkeep b/examples/scroll-cwd-pull/update/.gitkeep similarity index 100% rename from examples/scroll-cwd-pull/.scroll/update/.gitkeep rename to examples/scroll-cwd-pull/update/.gitkeep diff --git a/examples/scroll-cwd/annotations.json b/examples/scroll-cwd/annotations.json new file mode 100644 index 0000000..243beb9 --- /dev/null +++ b/examples/scroll-cwd/annotations.json @@ -0,0 +1 @@ +{"gg.druid.scroll.image":"artifacts.druid.gg/druid-team/druid:stable-nix","gg.druid.scroll.minCpu":"0.25","gg.druid.scroll.minDisk":"3Gi","gg.druid.scroll.minRam":"512Mi","gg.druid.scroll.port.main":"25565","gg.druid.scroll.port.rcon":"25575","gg.druid.scroll.smart":"true","org.opencontainers.image.created":"2026-01-31T12:08:52Z"} \ No newline at end of file diff --git a/examples/scroll-cwd-pull/.scroll/scroll-switch/run.sh b/examples/scroll-cwd/data/run.sh similarity index 100% rename from examples/scroll-cwd-pull/.scroll/scroll-switch/run.sh rename to examples/scroll-cwd/data/run.sh diff --git a/examples/scroll-cwd/data/server.properties.scroll_template b/examples/scroll-cwd/data/server.properties.scroll_template new file mode 100644 index 0000000..6becd21 --- /dev/null +++ b/examples/scroll-cwd/data/server.properties.scroll_template @@ -0,0 +1,56 @@ +allow-flight=false +allow-nether=true +broadcast-console-to-ops=true +broadcast-rcon-to-ops=true +difficulty=easy +enable-command-block=false +enable-jmx-monitoring=false +enable-query=false +enable-rcon=true +enable-status=true +enforce-secure-profile=true +enforce-whitelist=false +entity-broadcast-range-percentage=100 +force-gamemode=false +function-permission-level=2 +gamemode=survival +generate-structures=true +generator-settings={} +hardcore=false +hide-online-players=false +initial-disabled-packs= +initial-enabled-packs=vanilla +level-name=world +level-seed= +level-type=minecraft\:normal +max-chained-neighbor-updates=1000000 +max-players=20 +max-tick-time=60000 +max-world-size=29999984 +motd=A Minecraft Server +network-compression-threshold=256 +online-mode=true +op-permission-level=4 +player-idle-timeout=0 +prevent-proxy-connections=false +pvp=true +query.port=25565 +rate-limit=0 +rcon.password={{ .Config.rcon.password }} +rcon.port=25575 +require-resource-pack=false +resource-pack= +resource-pack-prompt= +resource-pack-sha1= +server-ip= +server-port=25565 +simulation-distance=10 +spawn-animals=true +spawn-monsters=true +spawn-npcs=true +spawn-protection=16 +sync-chunk-writes=true +text-filtering-config= +use-native-transport=true +view-distance=10 +white-list=false \ No newline at end of file diff --git a/examples/scroll-cwd/data/update.sh b/examples/scroll-cwd/data/update.sh new file mode 100755 index 0000000..966812f --- /dev/null +++ b/examples/scroll-cwd/data/update.sh @@ -0,0 +1,34 @@ +#default update script + +SCRIPTPATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" + +if [ ! -f "$SCRIPTPATH/scroll-lock.json" ]; then + echo "Scroll lock not found. Skipping update" + exit 0 +fi + +if [ -z "$(ls $SCRIPTPATH/update)" ]; then + echo "Update directory is empty. Skipping update" +else + versionsDirs=$(find $SCRIPTPATH/update/* -maxdepth 0 -type d | sort --version-sort) + current=$(cat $SCRIPTPATH/scroll-lock.json | jq -r .scroll_version) + + for versionsDir in $versionsDirs + do + version=$(basename $versionsDir) + if [ ! "$(printf '%s\n' "$version" "$current" | sort -V | head -n1)" = "$version" ] ; + then + echo "$versionsDir/update.sh" + if [ -f "$versionsDir/update.sh" ]; then + sh $versionsDir/update.sh + else + echo "Warning: update $version has no update.sh... skipping" + fi + fi + done +fi + + + +LATEST_VERSION=$(cat $SCRIPTPATH/scroll.yaml | yq -r .version) +jq --arg LV "$LATEST_VERSION" -r '.scroll_version = $LV' $SCRIPTPATH/scroll-lock.json | sponge $SCRIPTPATH/scroll-lock.json \ No newline at end of file diff --git a/examples/scroll-cwd/data/update_user_args.sh b/examples/scroll-cwd/data/update_user_args.sh new file mode 100755 index 0000000..0f6686b --- /dev/null +++ b/examples/scroll-cwd/data/update_user_args.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +MAX=${DRUID_MAX_MEMORY%?} +if [ -z "${MAX}" ]; +then + MAX=1024M +fi + + +echo -Xmx$MAX > user_jvm_args.txt \ No newline at end of file diff --git a/examples/scroll-cwd/manifest.json b/examples/scroll-cwd/manifest.json new file mode 100644 index 0000000..2b9e08a --- /dev/null +++ b/examples/scroll-cwd/manifest.json @@ -0,0 +1 @@ +{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:d43488edeca23bc6fe8a29df14743012d961def04fd2e0c6e34fa98a92ec6d2e","size":2758} \ No newline at end of file diff --git a/examples/scroll-cwd/packet_handler/json.lua b/examples/scroll-cwd/packet_handler/json.lua new file mode 100644 index 0000000..54d4448 --- /dev/null +++ b/examples/scroll-cwd/packet_handler/json.lua @@ -0,0 +1,388 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/examples/scroll-cwd/packet_handler/minecraft.lua b/examples/scroll-cwd/packet_handler/minecraft.lua new file mode 100644 index 0000000..34bcfa9 --- /dev/null +++ b/examples/scroll-cwd/packet_handler/minecraft.lua @@ -0,0 +1,262 @@ +json = require("packet_handler/json") + +function string.fromhex(str) + return (str:gsub('..', function(cc) + return string.char(tonumber(cc, 16)) + end)) +end + +function string.tohex(str) + return (str:gsub('.', function(c) + return string.format('%02X', string.byte(c)) + end)) +end + +-- Bitwise AND +local function band(a, b) + local result = 0 + local bitval = 1 + while a > 0 and b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 and bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Bitwise OR +local function bor(a, b) + local result = 0 + local bitval = 1 + while a > 0 or b > 0 do + local abit = a % 2 + local bbit = b % 2 + if abit == 1 or bbit == 1 then + result = result + bitval + end + a = math.floor(a / 2) + b = math.floor(b / 2) + bitval = bitval * 2 + end + return result +end + +-- Right Shift +local function rshift(value, shift) + return math.floor(value / (2 ^ shift)) +end + +-- Left Shift +local function lshift(value, shift) + return value * (2 ^ shift) +end + +function encodeLEB128(value) + local bytes = {} + repeat + local byte = band(value, 0x7F) + value = rshift(value, 7) + if value ~= 0 then + byte = bor(byte, 0x80) + end + table.insert(bytes, byte) + until value == 0 + return bytes +end + +function decodeLEB128(bytes) + local result = 0 + local shift = 0 + local bytesConsumed = 0 -- Track the number of bytes consumed + + for i, byte in ipairs(bytes) do + local value = band(byte, 0x7F) -- Get lower 7 bits + result = bor(result, lshift(value, shift)) -- Add it to result with the correct shift + bytesConsumed = bytesConsumed + 1 -- Increment the byte counter + if band(byte, 0x80) == 0 then -- If the highest bit is not set, we are done + break + end + shift = shift + 7 -- Move to the next group of 7 bits + end + + return result, bytesConsumed -- Return both the result and the number of bytes consumed +end + +function handle(ctx, data) + hex = string.tohex(data) + + debug_print("Received Packet: " .. hex) + + -- check if hex starts with 0x01 0x00 + if hex:sub(1, 4) == "FE01" then + debug_print("Received Legacy Ping Packet") + sendData(string.fromhex( + "ff002300a7003100000034003700000031002e0034002e0032000000410020004d0069006e006500630072006100660074002000530065007200760065007200000030000000320030")) + end + + local packetNo = 0 + + local maxLoops = 2 + + restBytes = data + + while hex ~= "" do + + queue = get_queue() + + hex = string.tohex(restBytes) + + debug_print("Remaining Bytes: " .. hex) + packetNo = packetNo + 1 + debug_print("Packet No: " .. packetNo) + + packetLength, bytesConsumed = decodeLEB128({string.byte(restBytes, 1, 1)}) + debug_print("Packet Length: " .. packetLength) + + -- cut of consumedBytes and read untul packetLength + packetWithLength = string.sub(restBytes, bytesConsumed + 1, packetLength + bytesConsumed) + + -- next varint is the packetid + packetId, bytesConsumed = decodeLEB128({string.byte(packetWithLength, 1, 1)}) + + debug_print("Packet ID: " .. packetId) + + packetWithLengthHex = string.tohex(packetWithLength) + + debug_print("Trimmed Packet: " .. packetWithLengthHex) + + -- make hex to the rest of the data + restBytes = string.sub(restBytes, packetLength + bytesConsumed + 1) + + debug_print("Rest Bytes: " .. string.tohex(restBytes)) + + if packetLength == 1 and packetId == 0 then + debug_print("Received Status Packet " .. packetWithLengthHex) + sendData(pingResponse()) + + -- check if second byte is 0x01 + elseif packetId == 1 then + debug_print("Received Ping Packet " .. packetWithLengthHex) + -- send same packet back + close(data) + -- login packet 0x20 0x00 + elseif packetId == 0 and packetWithLengthHex:sub(-2) == "02" then -- check for enum at the end + debug_print("Received Login Packet " .. packetWithLengthHex) + -- return + -- debug_print("Received Login Packet") + + sendData(disconnectResponse()) + -- sleep for a sec before closing + finish() + -- return + else + debug_print("Received unknown packet " .. packetWithLengthHex) + -- close("") + end + end +end + +function formatResponse(jsonObj) + local response = json.encode(jsonObj) + local responseBuffer = {string.byte(response, 1, -1)} + local additional = {0x00} + local responseBufferLength = encodeLEB128(#responseBuffer) + local packetLenthBuffer = encodeLEB128(#responseBuffer + #responseBufferLength + 1) + + local concatedBytes = {} + + for i = 1, #packetLenthBuffer do + table.insert(concatedBytes, packetLenthBuffer[i]) + end + + for i = 1, #additional do + table.insert(concatedBytes, additional[i]) + end + + for i = 1, #responseBufferLength do + table.insert(concatedBytes, responseBufferLength[i]) + end + + for i = 1, #responseBuffer do + table.insert(concatedBytes, responseBuffer[i]) + end + + -- convert back to string + local finalString = string.char(unpack(concatedBytes)) + + return finalString +end + +function pingResponse() + + local description = { + color = "red", + extra = {"\n", { + color = "gray", + extra = {{ + bold = true, + text = "HINT" + }, ":", " ", { + color = "white", + text = "Get free servers at:" + }, " ", { + color = "green", + text = "druid.gg" + }}, + text = "" + }}, + text = "This server is in standby." + } + + local obj = { + version = { + name = "§9🕐 Waiting...", + protocol = -1 + }, + description = description, + players = { + max = 0, + online = 1 + }, + favicon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAMlQTFRFR3BM6ndq5Wxb3WBQ6HFi0EUvvVxI8IBzzTwm0EUv11RC3GBQ7X1w00w50EUv42pa1lRB3mNT4WZV0Ugz2VlH0ks22lpJ0ks332RU1VI/6XZo8oV4421e63Zn32JR0046ytvZ2FZEieHa5nBgb+fZFerZ1NrZDOrZDurZ1tjYQunZztrZO+jZruDZFOrZDOrZDOrZ6HVoDOrZ09rZ0cvJn+LZbebZi+PZkOPZC+rZ942B7Xpr9op98oR29Id67n1uz9vZH+rZjeTZHadAYQAAADl0Uk5TAOr9sP4WBv4CDXqV8kcf3m277CmGPaAzx1Pg8tD90lw3YxDx/mzTQ+aq/nYk/bT50NSS71SwxIbiWYkesQAABERJREFUeNqll2tfozgUxkshIeF+vxWoiNfRUaszuztDC7rf/0PtISAlpR1dfPLzTZLzz3POIUgXp0XD2PJUkGetfbT4fyJI9+xNsuqVbGx1beDPh7uKnazq7e+96lWSqj79XLihpKv691SrRPU/4YLGtsbCp9quNp5BPjreE1j4KYT9ZxPYDbQt7GObW9XwxxHqTUz/EB/a8hbC2+iVJpiRbUdpokE92RwbdVJQcjp+x3Ztay0N1iFClFLk6oqYMEa3thUKeqp74q7zLYjQdUzIgjBhGiqRBohOdaLjo/FIldm6FhWIEH4NG8pGHgiReywJagnd8eqwzCF0cTAhq/TIDt+stzAE79Rz76pAYKMW4ukZKJDr9nzldJcMIHSd3dloYiAWapCm8iu83ECrO00tIHEH87JojCfP78/O7u/x/pQw3bEcYCM9MKALANht9HH42d3Pn389PF9enw/bLNjWapf4vAUcyDCreaMGn91dfb/49gv09HxNegAS5ZohNIUHuGlrIHVH8bcv/0I40+MDEDoVYGEHkkXMZbAWYBIMjOJfIX7Qw3W/0YjkHSBqOTW4DFQNAElIhvxvX76z+MHDfU+AnUyJPwZQG7jjyv64er34NdbNZb/CvMJmYT0GGCkANAXvDbyCAU7vFkJTZgRNGQP8RAamTsYVeOPiH5/6KqD2LNiteWNALMCUaewBXAZcDjTtHajjJhSCLMvRtARTAAEAEwdYWABoRPwhgJWrkYcUeEAAgNMpPF0P5WLii7g+AJxzReS6AGcxCRZXxKQZAwi5ezlo4+Mz7i9NxeKbRB8DQrPhasD1kcsgTJsOwD/KKAcAdGGv9iq+jUvYG1AE2Amj4l8IWKyaxkRkNANJ7Ak3z+e9gahqmAT+OhMAN6VPRjOYvQ7euqfwso9HQdZ0Mn0eoJtVkymYmzu7vfrn4tvNDbxP+gWqJL0BlgF/HbPJJI5/3N39fXk5vBSRBcd0KteEBxClrCoz5Gf1IEYLMvBc7z2+ykQ0eWPnVVUqmLcV5J6PujnqFmJZNf0wdXIIwB5YyN3FQWWWqWrFuh4Xnlhm1btKDx/51xxl/QJPlcrSNM1SyqpBknjsQwdbZZWZOk81RKmaSLLDaTzrsVSVosFT/UiqMhhVto8/9ZlEQpYE5Qk6EDpl3XACLp7vu5llpoUPPKgOIDIIbSHLyOLy50ULJ5PMNTmoQ6zmzlICLR3bCunitAi1gJDH+MAZaj+7PU8pdJd+9I2ttIQ1nmRHEUIUk8WHQpYjSXlBF3NFaGFKkqkgMhtB41ySnMDFswlYt5fSMorpbBPEDRww4bl4LgKakbcm1gh/IY3WhKjPRhDDa004wXwE1kWzQxhzEciynRYhFuHcx8JQGGKZe7FLZ3a0RbB7qIRzERbUorURWWhuQ9Zq5CyXS0dBs++HbwU5EKwv3FJDh2rk/uILoqFlT38O/QdGyOZnTVzZRwAAAABJRU5ErkJggg==" + } + + local snapshotMode = get_snapshot_mode() + local snapshotPercentage = get_snapshot_percentage() + + if snapshotMode ~= "noop" then + if snapshotMode == "restore" then + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Downloading snapshot... " + format("%.2f", snapshotPercentage) + "%" + else + obj.version.name = "§2▶ Extracting snapshot..." + end + obj.description = "Restoring Minecraft Server, this might take a moment" + else + if snapshotPercentage == nil or snapshotPercentage == 100 then + obj.version.name = "§2▶ Backing up... " + format("%.2f", snapshotPercentage) + "%" + else + obj.version.name = "§2▶ Backing up..." + end + obj.description = "Backing up Minecraft Server, this might take a moment" + end + elseif queue ~= nil and queue["install"] == "running" then + obj.version.name = "§2▶ Installing..." + obj.description = "Installing Minecraft Server, this might take a moment" + elseif get_finish_sec() ~= nil then + obj.version.name = "§2▶ Starting..." + obj.description = "Starting " .. math.ceil(get_finish_sec()) .. "s" + end + + return formatResponse(obj) +end + +function disconnectResponse() + local obj = "Our super cool system will start now... please wait" + return formatResponse(obj) +end diff --git a/examples/minecraft/.scroll/init-files/start.sh b/examples/scroll-cwd/scroll-switch/run.sh old mode 100755 new mode 100644 similarity index 61% rename from examples/minecraft/.scroll/init-files/start.sh rename to examples/scroll-cwd/scroll-switch/run.sh index b1695fc..27160e4 --- a/examples/minecraft/.scroll/init-files/start.sh +++ b/examples/scroll-cwd/scroll-switch/run.sh @@ -6,4 +6,4 @@ then MAX=1024M fi -java -Xmx$MAX -Xms1024M -jar spigot.jar nogui \ No newline at end of file +java -Xmx$MAX -Xms1024M -jar forge-*-shim.jar nogui \ No newline at end of file diff --git a/examples/scroll-cwd-pull/.scroll/scroll-switch/scroll-switch.sh b/examples/scroll-cwd/scroll-switch/scroll-switch.sh similarity index 83% rename from examples/scroll-cwd-pull/.scroll/scroll-switch/scroll-switch.sh rename to examples/scroll-cwd/scroll-switch/scroll-switch.sh index 258ac27..41ddfb1 100755 --- a/examples/scroll-cwd-pull/.scroll/scroll-switch/scroll-switch.sh +++ b/examples/scroll-cwd/scroll-switch/scroll-switch.sh @@ -1,6 +1,6 @@ NEWVERSION=$1 -#wget -O forge-installer-new.jar https://s3.eu-central-1.wasabisys.com/druid-scroll-artifacts/minecraft/forge/forge-$NEWVERSION.jar +#wget -O forge-installer-new.jar https://fsn1.your-objectstorage.com/druid-deployment-assets/minecraft/forge/forge-$NEWVERSION.jar #java -jar forge-installer-new.jar --installServer #rm forge-installer-new.jar diff --git a/examples/scroll-cwd/scroll.yaml b/examples/scroll-cwd/scroll.yaml new file mode 100644 index 0000000..758006b --- /dev/null +++ b/examples/scroll-cwd/scroll.yaml @@ -0,0 +1,72 @@ +name: artifacts.druid.gg/druid-team/scroll-minecraft-forge +desc: Minecraft Forge +version: 0.0.1 +app_version: 1.20.1-test +ports: + - name: main + protocol: tcp + port: 25565 + sleep_handler: packet_handler/minecraft.lua + start_delay: 10 + finish_after_command: install + - name: rcon + protocol: tcp + port: 25575 +init: "start" +commands: + start: + needs: [install] + run: restart + dependencies: [jdk17] + procedures: + - mode: exec + data: + - sh + - ./update_user_args.sh + - mode: exec + data: + - sh + - ./run.sh + stop: + procedures: + - mode: rcon + data: stop + install: + run: once + dependencies: [wget, cacert, jdk17] + procedures: + - mode: exec + data: + - wget + - -q + - -O + - forge-installer.jar + - http://192.168.100.200:9000/snapshot-cache/minecraft/forge/forge-1.20.1.jar + - mode: exec + data: + - java + - -jar + - forge-installer.jar + - --installServer + - mode: exec + data: + - rm + - forge-installer.jar + - mode: exec + data: + - bash + - -c + - echo eula=true > eula.txt + update: + procedures: + - mode: exec + data: + - sh + - $SCROLL_DIR/update.sh + - mode: exec + data: + - bash + - -c + - echo eula=true > eula.txt +plugins: + rcon: {} diff --git a/examples/scroll-cwd/update/.gitkeep b/examples/scroll-cwd/update/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod index ca7c31b..ecd508d 100644 --- a/go.mod +++ b/go.mod @@ -83,19 +83,13 @@ require ( require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/go-ini/ini v1.67.0 // indirect - github.com/goccy/go-json v0.10.4 // indirect github.com/gorilla/mux v1.8.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect - github.com/minio/md5-simd v1.1.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect - github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect @@ -118,7 +112,6 @@ require ( github.com/gopacket/gopacket v1.2.0 github.com/gorilla/websocket v1.5.3 github.com/highcard-dev/gorcon v1.3.10 - github.com/minio/minio-go/v7 v7.0.84 github.com/oapi-codegen/runtime v1.1.2 github.com/otiai10/copy v1.14.0 github.com/packetcap/go-pcap v0.0.0-20240528124601-8c87ecf5dbc5 diff --git a/go.sum b/go.sum index 5c4f262..703a3a3 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fasthttp/websocket v1.5.8 h1:k5DpirKkftIF/w1R8ZzjSgARJrs54Je9YJK37DL/Ah8= github.com/fasthttp/websocket v1.5.8/go.mod h1:d08g8WaT6nnyvg9uMm8K9zMYyDjfKyj3170AtPRuVU0= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -48,8 +46,6 @@ github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= -github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= -github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= @@ -58,8 +54,6 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= -github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofiber/contrib/websocket v1.3.4 h1:tWeBdbJ8q0WFQXariLN4dBIbGH9KBU75s0s7YXplOSg= github.com/gofiber/contrib/websocket v1.3.4/go.mod h1:kTFBPC6YENCnKfKx0BoOFjgXxdz7E85/STdkmZPEmPs= github.com/gofiber/fiber/v2 v2.45.0/go.mod h1:DNl0/c37WLe0g92U6lx1VMQuxGUQY5V7EIaVoEsUffc= @@ -112,9 +106,6 @@ github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPci github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= @@ -142,10 +133,6 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= -github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.84 h1:D1HVmAF8JF8Bpi6IU4V9vIEj+8pc+xU88EWMs2yed0E= -github.com/minio/minio-go/v7 v7.0.84/go.mod h1:57YXpvc5l3rjPdhqNrDsvVlY0qPI6UTk1bflAe+9doY= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= @@ -201,8 +188,6 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= diff --git a/internal/core/domain/oci.go b/internal/core/domain/oci.go index 8f5213a..c0aaa86 100644 --- a/internal/core/domain/oci.go +++ b/internal/core/domain/oci.go @@ -1,13 +1,29 @@ package domain +import "sync/atomic" + type ArtifactType string const ( ArtifactTypeScrollRoot ArtifactType = "application/vnd.highcard.druid.scroll.config.v1+json" ArtifactTypeScrollFs ArtifactType = "application/vnd.highcard.druid.scroll-fs.config.v1+json" + ArtifactTypeScrollData ArtifactType = "application/vnd.highcard.druid.scroll-data.config.v1+json" ArtifactTypeScrollMeta ArtifactType = "application/vnd.highcard.druid.scroll-meta.config.v1+json" ) +// SnapshotProgress tracks the state of a data pull/push operation. +// Mode values: "noop" (idle), "backup" (pushing data), "restore" (pulling data chunks). +type SnapshotProgress struct { + Percentage atomic.Int64 + Mode atomic.Value // stores string +} + +func NewSnapshotProgress() *SnapshotProgress { + sp := &SnapshotProgress{} + sp.Mode.Store("noop") + return sp +} + type AnnotationInfo struct { MinRam string MinDisk string diff --git a/internal/core/domain/queue_item.go b/internal/core/domain/queue_item.go index 20a3c12..fadf323 100644 --- a/internal/core/domain/queue_item.go +++ b/internal/core/domain/queue_item.go @@ -7,4 +7,5 @@ type QueueItem struct { UpdateLockStatus bool RunAfterExecution func() DoneChan chan struct{} + RestartCount uint } diff --git a/internal/core/domain/scroll.go b/internal/core/domain/scroll.go index a0815f6..5b9efb8 100644 --- a/internal/core/domain/scroll.go +++ b/internal/core/domain/scroll.go @@ -26,6 +26,10 @@ type Cronjob struct { Schedule string `yaml:"schedule"` Command string `yaml:"command"` } +type Chunks struct { + Name string `yaml:"name"` + Path string `yaml:"path"` +} type ColdStarterVars struct { Name string `yaml:"name"` @@ -63,6 +67,7 @@ type File struct { Commands map[string]*CommandInstructionSet `yaml:"commands" json:"commands"` Plugins map[string]map[string]string `yaml:"plugins" json:"plugins"` Cronjobs []*Cronjob `yaml:"cronjobs" json:"cronjobs"` + Chunks []*Chunks `yaml:"chunks" json:"chunks"` } type Scroll struct { diff --git a/internal/core/ports/services_ports.go b/internal/core/ports/services_ports.go index 1abc1d8..2e40f80 100644 --- a/internal/core/ports/services_ports.go +++ b/internal/core/ports/services_ports.go @@ -11,14 +11,6 @@ import ( "oras.land/oras-go/v2/registry/remote" ) -type SnapshotMode string - -const ( - SnapshotModeSnapshot SnapshotMode = "snapshot" - SnapshotModeRestore SnapshotMode = "restore" - SnapshotModeNoop SnapshotMode = "noop" -) - type AuthorizerServiceInterface interface { CheckHeader(r *fiber.Ctx) (*time.Time, error) CheckQuery(token string) (*time.Time, error) @@ -34,8 +26,6 @@ type ScrollServiceInterface interface { WriteNewScrollLock() *domain.ScrollLock GetLock() (*domain.ScrollLock, error) GetCommand(cmd string) (*domain.CommandInstructionSet, error) - InitFiles(files ...string) error - InitTemplateFiles(files ...string) error AddTemporaryCommand(cmd string, instructions *domain.CommandInstructionSet) } @@ -93,9 +83,10 @@ type TemplateRendererInterface interface { type OciRegistryInterface interface { GetRepo(repoUrl string) (*remote.Repository, error) Pull(dir string, artifact string) error + PullSelective(dir string, artifact string, includeData bool, progress *domain.SnapshotProgress) error CanUpdateTag(descriptor v1.Descriptor, folder string, tag string) (bool, error) PackFolders(fs *file.Store, dirs []string, artifactType domain.ArtifactType, path string) ([]v1.Descriptor, error) - Push(folder string, repo string, tag string, annotationInfo domain.AnnotationInfo, packMeta bool) (v1.Descriptor, error) + Push(folder string, repo string, tag string, annotationInfo domain.AnnotationInfo, packMeta bool, scrollFile *domain.File) (v1.Descriptor, error) PushMeta(folder string, repo string) (v1.Descriptor, error) CreateMetaDescriptors(fs *file.Store, dir string, artifact string) ([]v1.Descriptor, error) } @@ -138,36 +129,6 @@ type ColdStarterInterface interface { Finish(*domain.AugmentedPort) } -type RestoreSnapshotOptions struct { -} - -type S3Destination struct { - AccessKey string - SecretKey string - Bucket string - Endpoint string - Region string - Insecure bool -} -type SnapshotOptions struct { - CompressionLevel int - S3Destination *S3Destination -} - -type ProgressTracker interface { - LogTrackProgress(current int64) - GetPercent() float64 -} - -type SnapshotService interface { - Snapshot(dir string, destination string, options SnapshotOptions) error - RestoreSnapshot(dir string, source string, options RestoreSnapshotOptions) error - - GetProgressTracker() *ProgressTracker - GetCurrentMode() SnapshotMode - GetCurrentProgressTracker() *ProgressTracker -} - type ColdStarterServerInterface interface { Start(port int, onFinish func()) error Close() error diff --git a/internal/core/services/coldstarter.go b/internal/core/services/coldstarter.go index 18d4f42..349bb9b 100644 --- a/internal/core/services/coldstarter.go +++ b/internal/core/services/coldstarter.go @@ -23,28 +23,28 @@ type ColdStarter struct { finishChan chan *domain.AugmentedPort chandlers []ports.ColdStarterHandlerInterface queueManager ports.QueueManagerInterface - restoreService ports.SnapshotService handlerMu sync.Mutex + progress *domain.SnapshotProgress + OnBeforeFinish func(progress *domain.SnapshotProgress) // optional hook called before signaling finish } // NewColdStarter initializes the ColdStarter struct with proper channel initialization and no initial finishTime. func NewColdStarter( portsService ports.PortServiceInterface, queueManager ports.QueueManagerInterface, - restoreService ports.SnapshotService, dir string, ) *ColdStarter { return &ColdStarter{ - handler: make(map[string]ports.ColdStarterServerInterface), - finishCount: 0, - dir: dir, - finishTime: nil, - portsService: portsService, - finishChan: make(chan *domain.AugmentedPort), - chandlers: nil, - queueManager: queueManager, - restoreService: restoreService, - handlerMu: sync.Mutex{}, + handler: make(map[string]ports.ColdStarterServerInterface), + finishCount: 0, + dir: dir, + finishTime: nil, + portsService: portsService, + finishChan: make(chan *domain.AugmentedPort), + chandlers: nil, + queueManager: queueManager, + handlerMu: sync.Mutex{}, + progress: domain.NewSnapshotProgress(), } } @@ -96,7 +96,7 @@ func (c *ColdStarter) Serve(ctx context.Context) { for _, v := range port.Vars { vars[v.Name] = v.Value } - handler = lua.NewLuaHandler(c.queueManager, c.restoreService, path, c.dir, vars, augmentedPortMap) + handler = lua.NewLuaHandler(c.queueManager, path, c.dir, vars, augmentedPortMap, c.progress) } c.chandlers = append(c.chandlers, handler) @@ -153,6 +153,7 @@ func (c *ColdStarter) Stop() { } // Finish increments the finishCount, logs, and sends the port to the finishChan channel. +// If data directory is missing, it pulls data from the OCI registry before signaling finish. // This method uses a pointer receiver to ensure it modifies the original struct. func (c *ColdStarter) Finish(port *domain.AugmentedPort) { if c.finishTime == nil { @@ -168,6 +169,12 @@ func (c *ColdStarter) Finish(port *domain.AugmentedPort) { } else { logger.Log().Info(fmt.Sprintf("Server on port %d received finish signal", port.Port.Port)) } + + // Run optional before-finish hook (e.g. pull data from registry) + if c.OnBeforeFinish != nil { + c.OnBeforeFinish(c.progress) + } + c.finishChan <- port c.finishCount++ } diff --git a/internal/core/services/coldstarter/handler/lua_handler.go b/internal/core/services/coldstarter/handler/lua_handler.go index 803cd16..7bd6069 100644 --- a/internal/core/services/coldstarter/handler/lua_handler.go +++ b/internal/core/services/coldstarter/handler/lua_handler.go @@ -5,6 +5,7 @@ import ( "sync" "time" + "github.com/highcard-dev/daemon/internal/core/domain" "github.com/highcard-dev/daemon/internal/core/ports" "github.com/highcard-dev/daemon/internal/utils/logger" lua "github.com/yuin/gopher-lua" @@ -12,16 +13,16 @@ import ( ) type LuaHandler struct { - file string - luaPath string - externalVars map[string]string - ports map[string]int - finishedAt *time.Time - queueManager ports.QueueManagerInterface - snapshotService ports.SnapshotService - stateWrapper *LuaWrapper - execWg *sync.WaitGroup - closed bool + file string + luaPath string + externalVars map[string]string + ports map[string]int + finishedAt *time.Time + queueManager ports.QueueManagerInterface + stateWrapper *LuaWrapper + execWg *sync.WaitGroup + closed bool + progress *domain.SnapshotProgress } type LuaWrapper struct { @@ -30,19 +31,19 @@ type LuaWrapper struct { closed *bool } -func NewLuaHandler(queueManager ports.QueueManagerInterface, snapshotService ports.SnapshotService, - file string, luaPath string, externalVars map[string]string, ports map[string]int) *LuaHandler { +func NewLuaHandler(queueManager ports.QueueManagerInterface, + file string, luaPath string, externalVars map[string]string, ports map[string]int, progress *domain.SnapshotProgress) *LuaHandler { handler := &LuaHandler{ - file: file, - luaPath: luaPath, - externalVars: externalVars, - ports: ports, - queueManager: queueManager, - snapshotService: snapshotService, - stateWrapper: nil, - execWg: &sync.WaitGroup{}, - closed: false, + file: file, + luaPath: luaPath, + externalVars: externalVars, + ports: ports, + queueManager: queueManager, + stateWrapper: nil, + execWg: &sync.WaitGroup{}, + closed: false, + progress: progress, } return handler } @@ -182,21 +183,22 @@ func (handler *LuaHandler) GetHandler(funcs map[string]func(data ...string)) (po l.SetGlobal("get_snapshot_percentage", l.NewFunction( func(l *lua.LState) int { - progressTracker := handler.snapshotService.GetProgressTracker() - if progressTracker == nil { + if handler.progress != nil { + l.Push(lua.LNumber(handler.progress.Percentage.Load())) + } else { l.Push(lua.LNumber(100)) - return 1 } - percent := (*progressTracker).GetPercent() - l.Push(lua.LNumber(percent)) return 1 }, )) l.SetGlobal("get_snapshot_mode", l.NewFunction( func(l *lua.LState) int { - mode := handler.snapshotService.GetCurrentMode() - l.Push(lua.LString(mode)) + if handler.progress != nil { + l.Push(lua.LString(handler.progress.Mode.Load().(string))) + } else { + l.Push(lua.LString("noop")) + } return 1 }, )) diff --git a/internal/core/services/queue_manager.go b/internal/core/services/queue_manager.go index 2ac085a..1b99199 100644 --- a/internal/core/services/queue_manager.go +++ b/internal/core/services/queue_manager.go @@ -3,6 +3,7 @@ package services import ( "fmt" "sync" + "time" "github.com/highcard-dev/daemon/internal/core/domain" "github.com/highcard-dev/daemon/internal/core/ports" @@ -306,13 +307,14 @@ func (sc *QueueManager) RunQueue() { continue } - //if run Mode is restart, we need to run it again + //if in error state and not a restart/persistent mode, skip if status == domain.ScrollLockStatusError { continue } - //if run Mode is restart, we need to run it again - if status == domain.ScrollLockStatusDone && command.Run != domain.RunModeRestart { + //if done and not a restart/persistent mode, skip + isRestartMode := command.Run == domain.RunModeRestart || command.Run == domain.RunModePersistent + if status == domain.ScrollLockStatusDone && !isRestartMode { continue } @@ -354,17 +356,45 @@ func (sc *QueueManager) RunQueue() { // FIXED: Non-blocking send to buffered channel sc.taskDoneChan <- struct{}{} }() + + startedAt := time.Now() err := sc.workItem(c) + isRestartMode := command.Run == domain.RunModeRestart || command.Run == domain.RunModePersistent + if err != nil { - sc.setError(c, err, i.UpdateLockStatus) logger.Log().Error("Error running command", zap.String("command", c), zap.Error(err)) - return + if !isRestartMode { + sc.setError(c, err, i.UpdateLockStatus) + return + } } - //restart means we are never done! - if command.Run == domain.RunModeRestart { - logger.Log().Info("Command done, restarting..", zap.String("command", c)) - sc.setStatus(c, domain.ScrollLockStatusWaiting, i.UpdateLockStatus) + if isRestartMode { + // For persistent mode, mark as done (don't auto-restart on graceful shutdown) + // For restart mode, mark as waiting (will auto-restart) + if command.Run == domain.RunModePersistent { + sc.setStatus(c, domain.ScrollLockStatusDone, i.UpdateLockStatus) + } else { + // Set status to waiting immediately so shutdown captures correct state + sc.setStatus(c, domain.ScrollLockStatusWaiting, i.UpdateLockStatus) + } + + // Exponential backoff for fast restarts (1s, 2s, 4s, ... max 5m) + if time.Since(startedAt) < 30*time.Second { + i.RestartCount++ + } else { + i.RestartCount = 0 + } + if i.RestartCount > 0 { + backoff := time.Duration(1<<(i.RestartCount-1)) * time.Second + if backoff > 5*time.Minute { + backoff = 5 * time.Minute + } + logger.Log().Info("Restarting with backoff", zap.String("command", c), zap.Duration("backoff", backoff), zap.Uint("restartCount", i.RestartCount)) + time.Sleep(backoff) + } else { + logger.Log().Info("Command done, restarting..", zap.String("command", c)) + } } else { logger.Log().Info("Command done", zap.String("command", c)) sc.setStatus(c, domain.ScrollLockStatusDone, i.UpdateLockStatus) diff --git a/internal/core/services/registry/oci.go b/internal/core/services/registry/oci.go index 5f5afb3..8641610 100644 --- a/internal/core/services/registry/oci.go +++ b/internal/core/services/registry/oci.go @@ -6,6 +6,8 @@ import ( "fmt" "os" "path/filepath" + "strings" + "sync/atomic" "github.com/highcard-dev/daemon/internal/core/domain" "github.com/highcard-dev/daemon/internal/utils" @@ -63,6 +65,10 @@ func (c *OciClient) GetRepo(repoUrl string) (*remote.Repository, error) { } func (c *OciClient) Pull(dir string, artifact string) error { + return c.PullSelective(dir, artifact, true, nil) +} + +func (c *OciClient) PullSelective(dir string, artifact string, includeData bool, progress *domain.SnapshotProgress) error { repo, tag := utils.SplitArtifact(artifact) @@ -70,11 +76,9 @@ func (c *OciClient) Pull(dir string, artifact string) error { return fmt.Errorf("tag must be set") } - // 1. Connect to a remote repository ctx := context.Background() repoInstance, err := c.GetRepo(repo) - if err != nil { return err } @@ -85,31 +89,98 @@ func (c *OciClient) Pull(dir string, artifact string) error { } fs, err := file.New(filepath.Join(dir)) - if err != nil { return err } - manifestDescriptor, err := oras.Copy(ctx, repoInstance, tag, fs, tag, oras.DefaultCopyOptions) + // Track progress for data pulls + var completed atomic.Int64 + var totalLayers atomic.Int64 + + if progress != nil { + progress.Mode.Store("restore") + progress.Percentage.Store(0) + } + + copyOpts := oras.CopyOptions{ + CopyGraphOptions: oras.CopyGraphOptions{ + FindSuccessors: func(ctx context.Context, fetcher content.Fetcher, desc v1.Descriptor) ([]v1.Descriptor, error) { + successors, err := content.Successors(ctx, fetcher, desc) + if err != nil { + return nil, err + } + + if !includeData { + filtered := make([]v1.Descriptor, 0, len(successors)) + for _, s := range successors { + // Filter out data layers by checking media type. + // ORAS appends +gzip to the media type for directories. + baseType := strings.TrimSuffix(s.MediaType, "+gzip") + if baseType == string(domain.ArtifactTypeScrollData) { + logger.Log().Debug("Skipping data layer", zap.String("digest", s.Digest.String())) + continue + } + filtered = append(filtered, s) + } + totalLayers.Store(int64(len(filtered))) + return filtered, nil + } + + totalLayers.Store(int64(len(successors))) + return successors, nil + }, + PostCopy: func(ctx context.Context, desc v1.Descriptor) error { + if progress != nil { + done := completed.Add(1) + total := totalLayers.Load() + if total > 0 { + pct := done * 100 / total + progress.Percentage.Store(pct) + } + logger.Log().Info("Pulled layer", zap.String("digest", desc.Digest.String()), zap.Int64("completed", done), zap.Int64("total", total)) + } + return nil + }, + OnCopySkipped: func(ctx context.Context, desc v1.Descriptor) error { + if progress != nil { + done := completed.Add(1) + total := totalLayers.Load() + if total > 0 { + pct := done * 100 / total + progress.Percentage.Store(pct) + } + } + return nil + }, + }, + } + + manifestDescriptor, err := oras.Copy(ctx, repoInstance, tag, fs, tag, copyOpts) if err != nil { + if progress != nil { + progress.Mode.Store("noop") + } return err } + + if progress != nil { + progress.Percentage.Store(100) + progress.Mode.Store("noop") + } + logger.Log().Info("Manifest pulled", zap.String("digest", manifestDescriptor.Digest.String()), zap.String("mediaType", manifestDescriptor.MediaType)) jsonData, err := json.Marshal(&manifestDescriptor) - if err != nil { return fmt.Errorf("failed to marshal manifest descriptor: %w", err) } bs, err := content.FetchAll(ctx, repoInstance, manifestDescriptor) - if err != nil { return fmt.Errorf("failed to fetch manifest descriptor: %w", err) } fullDesc := v1.Descriptor{} - err = json.Unmarshal(bs, &fullDesc) if err != nil { return fmt.Errorf("failed to unmarshal manifest descriptor: %w", err) @@ -118,7 +189,6 @@ func (c *OciClient) Pull(dir string, artifact string) error { annotations := fullDesc.Annotations fileName := filepath.Join(dir, "manifest.json") err = os.WriteFile(fileName, jsonData, 0644) - if err != nil { return fmt.Errorf("failed to write manifest descriptor: %w", err) } @@ -129,13 +199,11 @@ func (c *OciClient) Pull(dir string, artifact string) error { } fileName = filepath.Join(dir, "annotations.json") err = os.WriteFile(fileName, annotationsJson, 0644) - if err != nil { return fmt.Errorf("failed to write annotations: %w", err) } return nil - } func (c *OciClient) CanUpdateTag(current v1.Descriptor, r string, tag string) (bool, error) { @@ -179,9 +247,9 @@ func (c *OciClient) PackFolders(fs *file.Store, dirs []string, artifactType doma } // the root has to leaves, one is the real scroll (fs) and the other is meta information about the scroll -func (c *OciClient) Push(folder string, repo string, tag string, annotationInfo domain.AnnotationInfo, packMeta bool) (v1.Descriptor, error) { +func (c *OciClient) Push(folder string, repo string, tag string, annotationInfo domain.AnnotationInfo, packMeta bool, scrollFile *domain.File) (v1.Descriptor, error) { - availableFileNames := []string{"init-files", "init-files-template", "scroll-switch", "update", "scroll.yaml", "packet_handler", "public", "private"} + availableFileNames := []string{"update", "scroll.yaml", "packet_handler", "public", "private"} fsFileNames := []string{} //check if files exisits (file or folder) and remove from slice if not @@ -226,6 +294,38 @@ func (c *OciClient) Push(folder string, repo string, tag string, annotationInfo descriptorsForRoot = append(descriptorsForRoot, scrollMetaManifestDescriptor...) } + // Pack data directory chunks + dataDir := utils.GetDataDirFromScrollDir(folder) + dataExists, _ := utils.FileExists(dataDir) + if dataExists { + var explicitChunks []*domain.Chunks + if scrollFile != nil { + explicitChunks = scrollFile.Chunks + } + chunks, err := utils.AutoChunkDataDir(dataDir, explicitChunks) + if err != nil { + return v1.Descriptor{}, fmt.Errorf("failed to auto-chunk data directory: %w", err) + } + for _, chunk := range chunks { + chunkFullPath := filepath.Join(dataDir, chunk.Path) + chunkExists, _ := utils.FileExists(chunkFullPath) + if !chunkExists { + logger.Log().Warn(fmt.Sprintf("data chunk path %s does not exist, skipping", chunk.Path)) + continue + } + // Name the layer "data/" so it extracts to the correct location on pull + layerName := filepath.Join("data", chunk.Path) + desc, err := fs.Add(context.Background(), layerName, string(domain.ArtifactTypeScrollData), chunkFullPath) + if err != nil { + return v1.Descriptor{}, fmt.Errorf("failed to pack data chunk %s: %w", chunk.Name, err) + } + logger.Log().Info(fmt.Sprintf("packed data chunk %s: %v", chunk.Name, desc.Digest)) + descriptorsForRoot = append(descriptorsForRoot, desc) + } + } else { + logger.Log().Info("No data directory found, skipping data chunk packing") + } + ctx := context.Background() annotations := map[string]string{} @@ -263,7 +363,32 @@ func (c *OciClient) Push(folder string, repo string, tag string, annotationInfo if err = fs.Tag(ctx, rootManifestDescriptor, tag); err != nil { return v1.Descriptor{}, err } - _, err = oras.Copy(ctx, fs, tag, repoInstance, tag, oras.DefaultCopyOptions) + + pushCopyOpts := oras.CopyOptions{ + CopyGraphOptions: oras.CopyGraphOptions{ + PostCopy: func(ctx context.Context, desc v1.Descriptor) error { + title := desc.Annotations["org.opencontainers.image.title"] + logger.Log().Info("Pushed layer", + zap.String("digest", desc.Digest.String()), + zap.String("mediaType", desc.MediaType), + zap.Int64("size", desc.Size), + zap.String("title", title), + ) + return nil + }, + OnCopySkipped: func(ctx context.Context, desc v1.Descriptor) error { + title := desc.Annotations["org.opencontainers.image.title"] + logger.Log().Info("Layer already exists in registry, skipping", + zap.String("digest", desc.Digest.String()), + zap.String("mediaType", desc.MediaType), + zap.Int64("size", desc.Size), + zap.String("title", title), + ) + return nil + }, + }, + } + _, err = oras.Copy(ctx, fs, tag, repoInstance, tag, pushCopyOpts) return rootManifestDescriptor, err } @@ -304,7 +429,32 @@ func (c *OciClient) PushMeta(folder string, repo string) (v1.Descriptor, error) if err = fs.Tag(ctx, rootManifestDescriptor, tag); err != nil { return v1.Descriptor{}, err } - _, err = oras.Copy(ctx, fs, tag, repoInstance, tag, oras.DefaultCopyOptions) + + metaCopyOpts := oras.CopyOptions{ + CopyGraphOptions: oras.CopyGraphOptions{ + PostCopy: func(ctx context.Context, desc v1.Descriptor) error { + title := desc.Annotations["org.opencontainers.image.title"] + logger.Log().Info("Pushed layer", + zap.String("digest", desc.Digest.String()), + zap.String("mediaType", desc.MediaType), + zap.Int64("size", desc.Size), + zap.String("title", title), + ) + return nil + }, + OnCopySkipped: func(ctx context.Context, desc v1.Descriptor) error { + title := desc.Annotations["org.opencontainers.image.title"] + logger.Log().Info("Layer already exists in registry, skipping", + zap.String("digest", desc.Digest.String()), + zap.String("mediaType", desc.MediaType), + zap.Int64("size", desc.Size), + zap.String("title", title), + ) + return nil + }, + }, + } + _, err = oras.Copy(ctx, fs, tag, repoInstance, tag, metaCopyOpts) return rootManifestDescriptor, err } diff --git a/internal/core/services/scroll_service.go b/internal/core/services/scroll_service.go index c697b1c..ed6db0d 100644 --- a/internal/core/services/scroll_service.go +++ b/internal/core/services/scroll_service.go @@ -3,11 +3,8 @@ package services import ( "errors" "os" - "path" "path/filepath" "regexp" - "slices" - "strings" "github.com/highcard-dev/daemon/internal/core/domain" "github.com/highcard-dev/daemon/internal/core/ports" @@ -16,7 +13,7 @@ import ( ) type ScrollService struct { - processCwd string + scrollDir string scroll *domain.Scroll lock *domain.ScrollLock templateRenderer ports.TemplateRendererInterface @@ -29,7 +26,7 @@ func NewScrollService( processCwd string, ) (*ScrollService, error) { s := &ScrollService{ - processCwd: processCwd, + scrollDir: processCwd, templateRenderer: NewTemplateRenderer(), } @@ -47,6 +44,12 @@ func (sc *ScrollService) ReloadScroll() (*domain.Scroll, error) { return nil, err } + //enseure data dir exists + err = os.MkdirAll(sc.GetCwd(), os.ModePerm) + if err != nil { + return nil, err + } + sc.scroll = scroll return scroll, nil @@ -76,80 +79,6 @@ func (sc *ScrollService) ReloadLock(ignoreVersionCheck bool) (*domain.ScrollLock } -func (sc *ScrollService) InitFiles(fls ...string) error { - //init-files-template needs to be rendered - files, err := sc.filterFiles("init-files", fls...) - if err != nil { - return err - } - initFileDir := path.Join(sc.GetDir(), "init-files") - - for _, file := range files { - basePath := strings.TrimPrefix(file, initFileDir) - dest := path.Join(sc.processCwd, basePath) - - err := utils.CopyFile(file, dest) - if err != nil { - return err - } - } - return nil -} - -func (sc *ScrollService) filterFiles(path string, fls ...string) ([]string, error) { - //init-files-template needs to be rendered - initPath := strings.TrimRight(sc.GetDir(), "/") + "/" + path + "/" - exist, _ := utils.FileExists(initPath) - - files := []string{} - - if exist { - err := filepath.Walk(initPath, func(path string, f os.FileInfo, err error) error { - basePath := strings.TrimPrefix(path, initPath) - if !f.IsDir() && (slices.Contains(fls, basePath) || len(fls) == 0) { - files = append(files, path) - } - return nil - }) - - if err != nil { - return []string{}, err - } - - return files, nil - } - - return []string{}, nil -} - -func (sc *ScrollService) InitTemplateFiles(fls ...string) error { - - //init-files-template needs to be rendered - files, err := sc.filterFiles("init-files-template", fls...) - if err != nil { - return err - } - if len(files) == 0 { - return nil - } - templateBase := path.Join(sc.GetDir(), "init-files-template") - - for i, file := range files { - basePath := strings.TrimPrefix(file, templateBase) - files[i] = basePath - } - - return sc.templateRenderer.RenderScrollTemplateFiles(templateBase, files, sc.scroll, sc.processCwd) -} - -func (sc *ScrollService) CopyingInitFiles() error { - err := sc.InitFiles() - if err != nil { - return err - } - return sc.InitTemplateFiles() -} - func (sc *ScrollService) LockExists() bool { exisits, err := utils.FileExists(sc.GetDir() + "/scroll-lock.json") return err == nil && exisits @@ -177,11 +106,11 @@ func (sc *ScrollService) WriteNewScrollLock() *domain.ScrollLock { } func (sc *ScrollService) GetDir() string { - return utils.GetScrollDirFromCwd(sc.processCwd) + return sc.scrollDir } func (sc *ScrollService) GetCwd() string { - return sc.processCwd + return utils.GetDataDirFromScrollDir(sc.scrollDir) } func (s ScrollService) GetCurrent() *domain.Scroll { @@ -199,7 +128,7 @@ func (s ScrollService) ScrollExists() bool { } func (s ScrollService) RenderCwdTemplates() error { - cwd := s.processCwd + cwd := s.scrollDir libRegEx, err := regexp.Compile(`^.+\.(scroll_template)$`) if err != nil { @@ -246,7 +175,7 @@ func (s ScrollService) GetScrollConfig() interface{} { } func (s ScrollService) GetScrollConfigRawYaml() []byte { - path := s.processCwd + "/.scroll_config.yml" + path := s.scrollDir + "/.scroll_config.yml" content, err := os.ReadFile(path) diff --git a/internal/core/services/snapshot/archive.go b/internal/core/services/snapshot/archive.go deleted file mode 100644 index a9b5c0e..0000000 --- a/internal/core/services/snapshot/archive.go +++ /dev/null @@ -1,145 +0,0 @@ -package services - -import ( - "archive/tar" - "compress/gzip" - "fmt" - "io" - "os" - "path/filepath" -) - -// extractTarGzFromReader extracts a gzipped tar archive from a reader -func extractTarGzFromReader(dir string, reader io.Reader) error { - gzReader, err := gzip.NewReader(reader) - if err != nil { - return fmt.Errorf("failed to create gzip reader: %w", err) - } - defer gzReader.Close() - - return extractTarFromReader(dir, gzReader) -} - -// extractTarFromReader extracts a tar archive from an io.Reader -func extractTarFromReader(dir string, reader io.Reader) error { - tarReader := tar.NewReader(reader) - - for { - header, err := tarReader.Next() - if err == io.EOF { - break - } - if err != nil { - return fmt.Errorf("failed to read tar header: %w", err) - } - - if err := extractTarFile(dir, tarReader, header); err != nil { - return err - } - } - - return nil -} - -// extractTarFile extracts a single file from tar archive -func extractTarFile(dir string, tarReader *tar.Reader, header *tar.Header) error { - path := filepath.Join(dir, header.Name) - - switch header.Typeflag { - case tar.TypeDir: - //check if the directory already exists - if _, err := os.Stat(path); err == nil { - // Directory exists, do nothing - return nil - } - - if err := os.Mkdir(path, os.FileMode(header.Mode)); err != nil { - return fmt.Errorf("ExtractTarGz: Mkdir() failed: %w", err) - } - case tar.TypeReg: - outFile, err := os.Create(path) - if err != nil { - return fmt.Errorf("ExtractTarGz: Create() failed: %w", err) - } - if _, err := io.Copy(outFile, tarReader); err != nil { - outFile.Close() - return fmt.Errorf("ExtractTarGz: Copy() failed: %w", err) - } - outFile.Close() - case tar.TypeSymlink: - // Create a symlink - if err := os.Symlink(header.Linkname, path); err != nil { - return fmt.Errorf("ExtractTarGz: Symlink() failed: %w", err) - } - case tar.TypeLink: - // Create a hard link - if err := os.Link(header.Linkname, path); err != nil { - return fmt.Errorf("ExtractTarGz: Link() failed: %w", err) - } - default: - return fmt.Errorf("unsupported tar header type: %c", header.Typeflag) - } - - // Set file ownership (for all types except symlinks and directories) - //if header.Typeflag != tar.TypeSymlink && header.Typeflag != tar.TypeDir { - // if err := os.Chown(path, header.Uid, header.Gid); err != nil { - // return fmt.Errorf("ExtractTarGz: Chown() failed: %w", err) - // } - //} - - // Set file permissions (for all types except symlinks and directories) - if header.Typeflag != tar.TypeSymlink && header.Typeflag != tar.TypeDir { - if err := os.Chmod(path, os.FileMode(header.Mode)); err != nil { - return fmt.Errorf("ExtractTarGz: Chmod() failed: %w", err) - } - } - - // Set file timestamps (for all types except symlinks and directories) - if header.Typeflag != tar.TypeSymlink && header.Typeflag != tar.TypeDir { - if err := os.Chtimes(path, header.AccessTime, header.ModTime); err != nil { - return fmt.Errorf("ExtractTarGz: Chtimes() failed: %w", err) - } - } - - return nil -} - -// extractTarFile extracts a single file from tar archive -func archiveTarGzFile(path string, info os.FileInfo, err error, dir string, tarWriter *tar.Writer) error { - // Walk through the source directory - if err != nil { - return err - } - - linkName := "" - if info.Mode()&os.ModeSymlink == os.ModeSymlink { - linkName, err = os.Readlink(path) - if err != nil { - return err - } - } - - hdr, err := tar.FileInfoHeader(info, linkName) - if err != nil { - return err - } - - hdr.Name, _ = filepath.Rel(dir, path) - - if err := tarWriter.WriteHeader(hdr); err != nil { - return err - } - - if info.Mode().IsRegular() { - file, err := os.Open(path) - if err != nil { - return err - } - defer file.Close() - - _, err = io.Copy(tarWriter, file) - return err - } - - return nil -} diff --git a/internal/core/services/snapshot/downloader.go b/internal/core/services/snapshot/downloader.go deleted file mode 100644 index b621f3d..0000000 --- a/internal/core/services/snapshot/downloader.go +++ /dev/null @@ -1,128 +0,0 @@ -package services - -import ( - "context" - "fmt" - "io" - "net/http" - "net/url" - "os" - "strconv" - "strings" -) - -// FileInfo contains information about a remote file -type FileInfo struct { - Exists bool - Size int64 - ContentType string -} - -// CheckFileInfo checks if a file exists and returns its size and content type. -// Only works for HTTP/HTTPS URLs. Returns error for local files or unsupported schemes. -func CheckFileInfo(ctx context.Context, source string) (*FileInfo, error) { - // Only support HTTP/HTTPS URLs - if !strings.HasPrefix(source, "http://") && !strings.HasPrefix(source, "https://") { - return nil, fmt.Errorf("file info check only supported for HTTP/HTTPS URLs") - } - - // Create HTTP GET request with context - req, err := http.NewRequestWithContext(ctx, "GET", source, nil) - if err != nil { - return nil, fmt.Errorf("failed to create HEAD request: %w", err) - } - - // Make the HTTP HEAD request - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to check file info for %s: %w", source, err) - } - defer resp.Body.Close() - - info := &FileInfo{ - Exists: resp.StatusCode == http.StatusOK, - ContentType: resp.Header.Get("Content-Type"), - } - - // Parse content length if available - if contentLength := resp.Header.Get("Content-Length"); contentLength != "" { - if size, err := strconv.ParseInt(contentLength, 10, 64); err == nil { - info.Size = size - } - } - - // If file doesn't exist, return the info but no error - if !info.Exists { - return info, nil - } - - return info, nil -} - -// DownloadToDirectory downloads and unpacks a snapshot to the specified directory. -func DownloadToDirectory(ctx context.Context, dir string, source string, basicTracker *BasicTracker) error { - // Create destination directory if it doesn't exist - if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("failed to create directory %s: %w", dir, err) - } - - // Check if source is a URL or local file - if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") { - - return downloadAndExtractFromURL(ctx, dir, source, basicTracker) - } - - return fmt.Errorf("scheme is not supported: %s", source) -} - -// downloadAndExtractFromURL downloads a file from URL and extracts it directly -func downloadAndExtractFromURL(ctx context.Context, dir, targetUrl string, basicTracker *BasicTracker) error { - - info, err := CheckFileInfo(ctx, targetUrl) - if err != nil { - return fmt.Errorf("failed to check file info for %s: %w", targetUrl, err) - } - if !info.Exists { - return fmt.Errorf("file does not exist at %s", targetUrl) - } - - // Create HTTP request with context - req, err := http.NewRequestWithContext(ctx, "GET", targetUrl, nil) - if err != nil { - return fmt.Errorf("failed to create request: %w", err) - } - - // Make the HTTP request - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return fmt.Errorf("failed to download from %s: %w", targetUrl, err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("failed to download: HTTP %d", resp.StatusCode) - } - - tracker := NewProgressReader(info.Size, resp.Body, basicTracker) - - parsedURL, err := url.Parse(targetUrl) - if err != nil { - return fmt.Errorf("failed to parse URL: %w", err) - } - - // Extract directly from the response body stream - return extractFromReader(dir, tracker, parsedURL.Path) -} - -// extractFromReader determines the file type and extracts from a reader -func extractFromReader(dir string, reader io.Reader, filename string) error { - if strings.HasSuffix(strings.ToLower(filename), ".tar.gz") || strings.HasSuffix(strings.ToLower(filename), ".tgz") { - return extractTarGzFromReader(dir, reader) - } else if strings.HasSuffix(strings.ToLower(filename), ".tar") { - return extractTarFromReader(dir, reader) - } - - return fmt.Errorf("unsupported file format: %s", filename) -} diff --git a/internal/core/services/snapshot/progress.go b/internal/core/services/snapshot/progress.go deleted file mode 100644 index a4a94cb..0000000 --- a/internal/core/services/snapshot/progress.go +++ /dev/null @@ -1,39 +0,0 @@ -package services - -import ( - "fmt" - - "github.com/highcard-dev/daemon/internal/utils/logger" - "go.uber.org/zap" -) - -type BasicTracker struct { - total int64 - lastPercent float64 -} - -func NewBasicTracker(total int64) *BasicTracker { - return &BasicTracker{ - total: total, - lastPercent: 0, - } -} - -func (bt *BasicTracker) LogTrackProgress(current int64) { - if bt.total > 0 { - // percentage calculation - currentPercent := (float64(current) * 100) / float64(bt.total) - if currentPercent >= bt.lastPercent+0.1 { - bt.lastPercent = currentPercent - logger.Log().Info("Progress", zap.Int64("read", current), zap.Int64("total", bt.total), zap.String("percentage", fmt.Sprintf("%.1f%%", currentPercent))) - } - } else { - logger.Log().Info("Progress", zap.Int64("read", current)) - } -} -func (bt *BasicTracker) GetPercent() float64 { - if bt.total > 0 { - return bt.lastPercent - } - return 0 -} diff --git a/internal/core/services/snapshot/progress_file_walk.go b/internal/core/services/snapshot/progress_file_walk.go deleted file mode 100644 index c522f36..0000000 --- a/internal/core/services/snapshot/progress_file_walk.go +++ /dev/null @@ -1,29 +0,0 @@ -package services - -import ( - "os" - "path/filepath" -) - -type ProgressFileWalk struct { - *BasicTracker - dir string - current int64 -} - -func NewProgressFileWalk(dir string, basicTracker *BasicTracker) *ProgressFileWalk { - return &ProgressFileWalk{ - BasicTracker: basicTracker, - dir: dir, - } -} - -func (pr *ProgressFileWalk) Walk(rootPath string, f filepath.WalkFunc) error { - return filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error { - if info.Mode().IsRegular() { - pr.current++ - pr.LogTrackProgress(pr.current) - } - return f(path, info, err) - }) -} diff --git a/internal/core/services/snapshot/progress_reader.go b/internal/core/services/snapshot/progress_reader.go deleted file mode 100644 index a4a48c4..0000000 --- a/internal/core/services/snapshot/progress_reader.go +++ /dev/null @@ -1,48 +0,0 @@ -package services - -import ( - "io" - - "github.com/highcard-dev/daemon/internal/utils/logger" - "go.uber.org/zap" -) - -type ProgressReader struct { - *BasicTracker - stream io.ReadCloser - current int64 -} - -func NewProgressReader(total int64, stream io.ReadCloser, basicTracker *BasicTracker) *ProgressReader { - return &ProgressReader{ - BasicTracker: basicTracker, - stream: stream, - } -} - -func (pr *ProgressReader) Read(p []byte) (n int, err error) { - n, err = pr.stream.Read(p) - if n > 0 { - pr.current += int64(n) - pr.LogTrackProgress(pr.current) - } else if err != nil { - if err == io.EOF { - pr.LogTrackProgress(pr.BasicTracker.total) - return n, err - } - logger.Log().Error("Error reading progress", zap.Error(err)) - return n, err - } - return n, err -} - -func (pr *ProgressReader) Close() error { - if pr.stream != nil { - err := pr.stream.Close() - if err != nil { - logger.Log().Error("Error closing stream", zap.Error(err)) - return err - } - } - return nil -} diff --git a/internal/core/services/snapshot/snapshot_service.go b/internal/core/services/snapshot/snapshot_service.go deleted file mode 100644 index 92d2506..0000000 --- a/internal/core/services/snapshot/snapshot_service.go +++ /dev/null @@ -1,113 +0,0 @@ -package services - -import ( - "context" - "fmt" - "os" - "path" - - "github.com/highcard-dev/daemon/internal/core/ports" - "github.com/highcard-dev/daemon/internal/utils" - "github.com/highcard-dev/daemon/internal/utils/logger" - "go.uber.org/zap" -) - -type SnapshotService struct { - currentProgressTracker ports.ProgressTracker - currentMode ports.SnapshotMode -} - -func NewSnapshotService() *SnapshotService { - return &SnapshotService{ - currentMode: ports.SnapshotModeNoop, - } -} - -func (rc *SnapshotService) setActivity(mode ports.SnapshotMode, progressTracker ports.ProgressTracker) { - rc.currentMode = mode - rc.currentProgressTracker = progressTracker -} - -func (rc *SnapshotService) GetCurrentProgressTracker() *ports.ProgressTracker { - if rc.currentMode == ports.SnapshotModeNoop { - return nil - } - return &rc.currentProgressTracker -} - -func (rc *SnapshotService) Snapshot(dir string, destination string, options ports.SnapshotOptions) error { - - totalFiles, _ := GetTotalFiles(dir) - progessTracker := NewBasicTracker(totalFiles) - - rc.setActivity(ports.SnapshotModeSnapshot, progessTracker) - defer rc.setActivity(ports.SnapshotModeNoop, nil) - //check if rootPath exists - if _, err := os.Stat(dir); os.IsNotExist(err) { - return fmt.Errorf("source path does not exist: %s", dir) - } - - err := Upload(dir, destination, options.S3Destination, options.CompressionLevel, progessTracker) - if err != nil { - return fmt.Errorf("failed to upload snapshot: %w", err) - } - logger.Log().Info("Snapshot created", zap.String("destination", destination)) - return nil -} - -func (rc *SnapshotService) RestoreSnapshot(dir string, source string, options ports.RestoreSnapshotOptions) error { - - info, err := CheckFileInfo(context.Background(), source) - if err != nil { - logger.Log().Error("Error checking file info", zap.Error(err)) - return err - } - - progressReader := NewBasicTracker(info.Size) - - dest := path.Join(dir, ".snap_dl") - - os.RemoveAll(dest) - defer os.RemoveAll(dest) - - if _, err := os.Stat(dir); err == nil { - logger.Log().Info("Removing folder to make space for backup", zap.String("dir", dir)) - err := utils.RemoveContents(dir) - if err != nil { - return err - } - } - - rc.setActivity(ports.SnapshotModeRestore, progressReader) - defer rc.setActivity(ports.SnapshotModeNoop, nil) - - logger.Log().Info("Restoring backup", zap.String("source", source), zap.String("destination", dir)) - - // Download the file - err = DownloadToDirectory(context.Background(), dest, source, progressReader) - if err != nil { - os.RemoveAll(dest) - logger.Log().Error("Error occured while getting backup", zap.Error(err)) - return err - } - - // Move the downloaded file to the destination - err = utils.MoveContents(dest, dir) - if err != nil { - return err - } - - logger.Log().Info("Backup restored", zap.String("source", source), zap.String("destination", dir)) - return nil -} - -func (rc *SnapshotService) GetProgressTracker() *ports.ProgressTracker { - if rc.currentMode == ports.SnapshotModeNoop { - return nil - } - return &rc.currentProgressTracker -} - -func (rc *SnapshotService) GetCurrentMode() ports.SnapshotMode { - return rc.currentMode -} diff --git a/internal/core/services/snapshot/uploader.go b/internal/core/services/snapshot/uploader.go deleted file mode 100644 index 98293d5..0000000 --- a/internal/core/services/snapshot/uploader.go +++ /dev/null @@ -1,86 +0,0 @@ -package services - -import ( - "archive/tar" - "compress/gzip" - "context" - "fmt" - "io" - "os" - "path/filepath" - - "github.com/highcard-dev/daemon/internal/core/ports" - "github.com/highcard-dev/daemon/internal/utils/logger" - "github.com/minio/minio-go/v7" - "github.com/minio/minio-go/v7/pkg/credentials" - "go.uber.org/zap" -) - -func Upload(rootPath, objectKey string, s3Destination *ports.S3Destination, compressionLevel int, progessTracker *BasicTracker) error { - - pipeReader, pipeWriter := io.Pipe() - - go func() { - defer pipeWriter.Close() - gzipWriter, err := gzip.NewWriterLevel(pipeWriter, compressionLevel) - if err != nil { - logger.Log().Error("Failed to create gzip writer", zap.Error(err)) - pipeWriter.CloseWithError(fmt.Errorf("failed to create gzip writer: %w", err)) - return - } - defer gzipWriter.Close() - tarWriter := tar.NewWriter(gzipWriter) - - defer tarWriter.Close() - - progressFileWalk := NewProgressFileWalk(rootPath, progessTracker) - progressFileWalk.Walk(rootPath, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - return archiveTarGzFile(path, info, err, rootPath, tarWriter) - }) - println("Finished archiving files to tar.gz") - }() - - endpoint := s3Destination.Endpoint - region := s3Destination.Region - if region == "" { - region = "us-east-1" - } - accessKey := s3Destination.AccessKey - secretKey := s3Destination.SecretKey - bucketName := s3Destination.Bucket - - // Load AWS config with custom S3-compatible settings - minioClient, err := minio.New(endpoint, &minio.Options{ - Creds: credentials.NewStaticV4(accessKey, secretKey, ""), - Secure: !s3Destination.Insecure, - }) - if err != nil { - return fmt.Errorf("failed to create S3 client: %v", err) - } - - logger.Log().Info("Uploading snapshot to S3", zap.String("bucket", bucketName), zap.String("objectKey", objectKey), zap.String("endpoint", endpoint), zap.Bool("insecure", s3Destination.Insecure), zap.Int("compressionLevel", compressionLevel)) - - contentType := "application/octet-stream" - _, err = minioClient.PutObject(context.TODO(), bucketName, objectKey, pipeReader, -1, minio.PutObjectOptions{ContentType: contentType}) - return err -} - -func GetTotalFiles(dir string) (int64, error) { - var totalFiles int64 - err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.Mode().IsRegular() { - totalFiles++ - } - return nil - }) - if err != nil { - return 0, err - } - return totalFiles, nil -} diff --git a/internal/handler/health_handler.go b/internal/handler/health_handler.go index 43191a3..26c8a49 100644 --- a/internal/handler/health_handler.go +++ b/internal/handler/health_handler.go @@ -9,23 +9,20 @@ import ( ) type HealthHandler struct { - portService ports.PortServiceInterface - timeoutDone bool - Started *time.Time - snapshotService ports.SnapshotService + portService ports.PortServiceInterface + timeoutDone bool + Started *time.Time } func NewHealthHandler( portService ports.PortServiceInterface, timeoutSec uint, - snapshotService ports.SnapshotService, ) *HealthHandler { h := &HealthHandler{ portService, false, nil, - snapshotService, } // if timeoutSec == 0, we want at some point to not show a bad health status @@ -54,19 +51,6 @@ func (p *HealthHandler) GetHealthAuth(c *fiber.Ctx) error { }) } - if p.snapshotService.GetCurrentMode() != ports.SnapshotModeNoop { - pt := p.snapshotService.GetCurrentProgressTracker() - var perc float64 - if pt != nil { - perc = (*pt).GetPercent() - } - percFloat32 := float32(perc) - return c.JSON(api.HealthResponse{ - Mode: string(p.snapshotService.GetCurrentMode()), - Progress: &percFloat32, - }) - } - return c.JSON(api.HealthResponse{ Mode: "ok", StartDate: p.Started, diff --git a/internal/handler/health_handler_test.go b/internal/handler/health_handler_test.go index 0497089..585296b 100644 --- a/internal/handler/health_handler_test.go +++ b/internal/handler/health_handler_test.go @@ -9,18 +9,16 @@ import ( "github.com/gofiber/fiber/v2" "github.com/highcard-dev/daemon/internal/api" - "github.com/highcard-dev/daemon/internal/core/ports" mock_ports "github.com/highcard-dev/daemon/test/mock" "go.uber.org/mock/gomock" ) // HealthTestContext holds all mocked services for health handler testing type HealthTestContext struct { - App *fiber.App - Ctrl *gomock.Controller - PortService *mock_ports.MockPortServiceInterface - SnapshotService *mock_ports.MockSnapshotService - Handler *HealthHandler + App *fiber.App + Ctrl *gomock.Controller + PortService *mock_ports.MockPortServiceInterface + Handler *HealthHandler } // setupHealthTestApp creates a Fiber app with mocked dependencies for testing @@ -28,19 +26,17 @@ func setupHealthTestApp(t *testing.T, timeoutSec uint) *HealthTestContext { ctrl := gomock.NewController(t) portService := mock_ports.NewMockPortServiceInterface(ctrl) - snapshotService := mock_ports.NewMockSnapshotService(ctrl) - handler := NewHealthHandler(portService, timeoutSec, snapshotService) + handler := NewHealthHandler(portService, timeoutSec) app := fiber.New() app.Get("/api/v1/health", handler.GetHealthAuth) return &HealthTestContext{ - App: app, - Ctrl: ctrl, - PortService: portService, - SnapshotService: snapshotService, - Handler: handler, + App: app, + Ctrl: ctrl, + PortService: portService, + Handler: handler, } } @@ -98,79 +94,6 @@ func TestHealthHandler_Health_Idle(t *testing.T) { } } -func TestHealthHandler_Health_Snapshot(t *testing.T) { - tc := setupHealthTestApp(t, 0) - defer tc.Ctrl.Finish() - - // Set Started time - now := time.Now() - tc.Handler.Started = &now - - tc.PortService.EXPECT().MandatoryPortsOpen().Return(true) - // GetCurrentMode is called twice: once in the condition, once for the response - tc.SnapshotService.EXPECT().GetCurrentMode().Return(ports.SnapshotModeSnapshot).Times(2) - - // Create a mock progress tracker - var pt ports.ProgressTracker = &mockProgressTracker{percent: 50.0} - tc.SnapshotService.EXPECT().GetCurrentProgressTracker().Return(&pt) - - req := httptest.NewRequest("GET", "/api/v1/health", nil) - resp, err := tc.App.Test(req) - if err != nil { - t.Fatalf("Failed to execute request: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - t.Errorf("Expected status 200, got %d", resp.StatusCode) - } - - body, _ := io.ReadAll(resp.Body) - var result api.HealthResponse - json.Unmarshal(body, &result) - - if result.Mode != "snapshot" { - t.Errorf("Expected mode 'snapshot', got '%s'", result.Mode) - } - if *result.Progress != 50.0 { - t.Errorf("Expected progress 50.0, got %f", *result.Progress) - } -} - -func TestHealthHandler_Health_Restore(t *testing.T) { - tc := setupHealthTestApp(t, 0) - defer tc.Ctrl.Finish() - - now := time.Now() - tc.Handler.Started = &now - - tc.PortService.EXPECT().MandatoryPortsOpen().Return(true) - // GetCurrentMode is called twice: once in the condition, once for the response - tc.SnapshotService.EXPECT().GetCurrentMode().Return(ports.SnapshotModeRestore).Times(2) - - var pt ports.ProgressTracker = &mockProgressTracker{percent: 75.0} - tc.SnapshotService.EXPECT().GetCurrentProgressTracker().Return(&pt) - - req := httptest.NewRequest("GET", "/api/v1/health", nil) - resp, err := tc.App.Test(req) - if err != nil { - t.Fatalf("Failed to execute request: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - t.Errorf("Expected status 200, got %d", resp.StatusCode) - } - - body, _ := io.ReadAll(resp.Body) - var result api.HealthResponse - json.Unmarshal(body, &result) - - if result.Mode != "restore" { - t.Errorf("Expected mode 'restore', got '%s'", result.Mode) - } -} - func TestHealthHandler_Health_Ok(t *testing.T) { tc := setupHealthTestApp(t, 0) defer tc.Ctrl.Finish() @@ -179,7 +102,6 @@ func TestHealthHandler_Health_Ok(t *testing.T) { tc.Handler.Started = &now tc.PortService.EXPECT().MandatoryPortsOpen().Return(true) - tc.SnapshotService.EXPECT().GetCurrentMode().Return(ports.SnapshotModeNoop) req := httptest.NewRequest("GET", "/api/v1/health", nil) resp, err := tc.App.Test(req) @@ -234,43 +156,3 @@ func TestHealthHandler_Health_TimeoutDone_PortsClosed(t *testing.T) { t.Errorf("Expected mode 'idle', got '%s'", result.Mode) } } - -func TestHealthHandler_Health_SnapshotNilProgressTracker(t *testing.T) { - tc := setupHealthTestApp(t, 0) - defer tc.Ctrl.Finish() - - now := time.Now() - tc.Handler.Started = &now - - tc.PortService.EXPECT().MandatoryPortsOpen().Return(true) - // GetCurrentMode is called twice: once in the condition, once for the response - tc.SnapshotService.EXPECT().GetCurrentMode().Return(ports.SnapshotModeSnapshot).Times(2) - tc.SnapshotService.EXPECT().GetCurrentProgressTracker().Return(nil) - - req := httptest.NewRequest("GET", "/api/v1/health", nil) - resp, err := tc.App.Test(req) - if err != nil { - t.Fatalf("Failed to execute request: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - t.Errorf("Expected status 200, got %d", resp.StatusCode) - } - - body, _ := io.ReadAll(resp.Body) - var result api.HealthResponse - json.Unmarshal(body, &result) - - if *result.Progress != 0 { - t.Errorf("Expected progress 0, got %f", *result.Progress) - } -} - -// mockProgressTracker implements ports.ProgressTracker for testing -type mockProgressTracker struct { - percent float64 -} - -func (m *mockProgressTracker) LogTrackProgress(current int64) {} -func (m *mockProgressTracker) GetPercent() float64 { return m.percent } diff --git a/internal/utils/fs.go b/internal/utils/fs.go index 6d7231d..8412b83 100644 --- a/internal/utils/fs.go +++ b/internal/utils/fs.go @@ -4,6 +4,8 @@ import ( "io" "os" "path/filepath" + + "github.com/highcard-dev/daemon/internal/core/domain" ) func FileExists(path string) (bool, error) { @@ -94,3 +96,29 @@ func CopyFile(src, dest string) error { // Set same permissions as source return os.Chmod(dest, srcInfo.Mode()) } + +// AutoChunkDataDir returns the list of chunks to pack for the data directory. +// If explicitChunks is non-empty, it returns them as-is. +// Otherwise, it reads top-level entries of dataDir and returns one Chunk per entry. +func AutoChunkDataDir(dataDir string, explicitChunks []*domain.Chunks) ([]*domain.Chunks, error) { + if len(explicitChunks) > 0 { + return explicitChunks, nil + } + + entries, err := os.ReadDir(dataDir) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + + chunks := make([]*domain.Chunks, 0, len(entries)) + for _, entry := range entries { + chunks = append(chunks, &domain.Chunks{ + Name: entry.Name(), + Path: entry.Name(), + }) + } + return chunks, nil +} diff --git a/internal/utils/scroll.go b/internal/utils/scroll.go index bfc55ae..0665db0 100644 --- a/internal/utils/scroll.go +++ b/internal/utils/scroll.go @@ -2,6 +2,6 @@ package utils import "path/filepath" -func GetScrollDirFromCwd(cwd string) string { - return filepath.Join(cwd, ".scroll") +func GetDataDirFromScrollDir(cwd string) string { + return filepath.Join(cwd, "data") } diff --git a/test/integration/commands/serve_test.go b/test/integration/commands/serve_test.go index 6652350..0cef03d 100644 --- a/test/integration/commands/serve_test.go +++ b/test/integration/commands/serve_test.go @@ -29,22 +29,22 @@ func TestServeCommand(t *testing.T) { var testCases = []TestCase{ { Name: "TestServeFull", - ScrollFile: "../../../examples/minecraft/.scroll/scroll.yaml", + ScrollFile: "../../../examples/minecraft/scroll.yaml", Restarts: 0, }, { Name: "TestServeFull With Restart", - ScrollFile: "../../../examples/minecraft/.scroll/scroll.yaml", + ScrollFile: "../../../examples/minecraft/scroll.yaml", Restarts: 1, }, { Name: "TestServeFull With 3 Restarts", - ScrollFile: "../../../examples/minecraft/.scroll/scroll.yaml", + ScrollFile: "../../../examples/minecraft/scroll.yaml", Restarts: 3, }, { Name: "TestServeFull With Restart (Persistent)", - ScrollFile: "../../../examples/minecraft/.scroll/scroll.yaml", + ScrollFile: "../../../examples/minecraft/scroll.yaml", Restarts: 1, RunModeOverwrite: domain.RunModePersistent, }, @@ -58,16 +58,15 @@ func TestServeCommand(t *testing.T) { //observer := logger.SetupLogsCapture() unixTime := time.Now().Unix() path := "./druid-cli-test/" + strconv.FormatInt(unixTime, 10) + "/" - scrollPath := path + ".scroll/" - err := copy.Copy(tc.ScrollFile, scrollPath+"scroll.yaml") + err := copy.Copy(tc.ScrollFile, path+"scroll.yaml") if err != nil { t.Fatalf("Failed to copy test scroll file: %v", err) } if tc.RunModeOverwrite != "" { //overwrite "restart" with RunModeOverwrite - scroll, err := domain.NewScroll(scrollPath) + scroll, err := domain.NewScroll(path) if err != nil { t.Fatalf("Failed to read scroll file: %v", err) } @@ -80,7 +79,7 @@ func TestServeCommand(t *testing.T) { if err != nil { t.Fatalf("Failed to marshal scroll file: %v", err) } - err = os.WriteFile(scrollPath+"scroll.yaml", scrollBytes, 0644) + err = os.WriteFile(path+"scroll.yaml", scrollBytes, 0644) if err != nil { t.Fatalf("Failed to write scroll file: %v", err) } @@ -142,7 +141,7 @@ func TestServeCommand(t *testing.T) { t.Log("Connected to minecraft server") //double check that install was never run again - lock, err := domain.ReadLock(scrollPath + "scroll-lock.json") + lock, err := domain.ReadLock(path + "scroll-lock.json") if err != nil { t.Fatalf("Failed to read lock file: %v", err) } @@ -173,7 +172,7 @@ func TestServeCommand(t *testing.T) { t.Fatalf("Failed to stop daemon server, server still online") } - lock, err = domain.ReadLock(scrollPath + "scroll-lock.json") + lock, err = domain.ReadLock(path + "scroll-lock.json") if err != nil { t.Fatalf("Failed to read lock file: %v", err) } diff --git a/test/integration/example_test.go b/test/integration/example_test.go index 7d95bca..29d2297 100644 --- a/test/integration/example_test.go +++ b/test/integration/example_test.go @@ -48,7 +48,7 @@ func TestExamples(t *testing.T) { configs := []ServiceConfig{ { ServiceName: "minecraft", - ExamplePath: "../../examples/minecraft/.scroll/scroll.yaml", + ExamplePath: "../../examples/minecraft/scroll.yaml", TestAddress: "localhost:25565", TestName: "Minecraft", LockFileStatus: []string{"start", "install"}, @@ -60,7 +60,7 @@ func TestExamples(t *testing.T) { }, { ServiceName: "nginx", - ExamplePath: "../../examples/nginx/.scroll/scroll.yaml", + ExamplePath: "../../examples/nginx/scroll.yaml", TestAddress: "localhost:80", TestName: "Nginx", LockFileStatus: []string{"start"}, @@ -95,7 +95,6 @@ func TestExamples(t *testing.T) { //ensure the path exists and is empty os.RemoveAll(path) path = path + strconv.FormatInt(unixTime, 10) + "/" - scrollPath := path + ".scroll/" err := os.MkdirAll(path, 0755) if err != nil { @@ -103,8 +102,8 @@ func TestExamples(t *testing.T) { return } - //copy example - err = copy.Copy(config.ExamplePath, scrollPath+"scroll.yaml") + //copy example scroll.yaml directly into the scroll directory + err = copy.Copy(config.ExamplePath, path+"scroll.yaml") if err != nil { t.Error(err) return diff --git a/test/mock/services.go b/test/mock/services.go index 7c744d7..e553af6 100644 --- a/test/mock/services.go +++ b/test/mock/services.go @@ -944,19 +944,33 @@ func (mr *MockOciRegistryInterfaceMockRecorder) Pull(dir, artifact any) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pull", reflect.TypeOf((*MockOciRegistryInterface)(nil).Pull), dir, artifact) } +// PullSelective mocks base method. +func (m *MockOciRegistryInterface) PullSelective(dir, artifact string, includeData bool, progress *domain.SnapshotProgress) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PullSelective", dir, artifact, includeData, progress) + ret0, _ := ret[0].(error) + return ret0 +} + +// PullSelective indicates an expected call of PullSelective. +func (mr *MockOciRegistryInterfaceMockRecorder) PullSelective(dir, artifact, includeData, progress any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PullSelective", reflect.TypeOf((*MockOciRegistryInterface)(nil).PullSelective), dir, artifact, includeData, progress) +} + // Push mocks base method. -func (m *MockOciRegistryInterface) Push(folder, repo, tag string, annotationInfo domain.AnnotationInfo, packMeta bool) (v1.Descriptor, error) { +func (m *MockOciRegistryInterface) Push(folder, repo, tag string, annotationInfo domain.AnnotationInfo, packMeta bool, scrollFile *domain.File) (v1.Descriptor, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Push", folder, repo, tag, annotationInfo, packMeta) + ret := m.ctrl.Call(m, "Push", folder, repo, tag, annotationInfo, packMeta, scrollFile) ret0, _ := ret[0].(v1.Descriptor) ret1, _ := ret[1].(error) return ret0, ret1 } // Push indicates an expected call of Push. -func (mr *MockOciRegistryInterfaceMockRecorder) Push(folder, repo, tag, annotationInfo, packMeta any) *gomock.Call { +func (mr *MockOciRegistryInterfaceMockRecorder) Push(folder, repo, tag, annotationInfo, packMeta, scrollFile any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Push", reflect.TypeOf((*MockOciRegistryInterface)(nil).Push), folder, repo, tag, annotationInfo, packMeta) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Push", reflect.TypeOf((*MockOciRegistryInterface)(nil).Push), folder, repo, tag, annotationInfo, packMeta, scrollFile) } // PushMeta mocks base method. @@ -1388,150 +1402,6 @@ func (mr *MockColdStarterInterfaceMockRecorder) StopWithDeplay(arg0 any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopWithDeplay", reflect.TypeOf((*MockColdStarterInterface)(nil).StopWithDeplay), arg0) } -// MockProgressTracker is a mock of ProgressTracker interface. -type MockProgressTracker struct { - ctrl *gomock.Controller - recorder *MockProgressTrackerMockRecorder - isgomock struct{} -} - -// MockProgressTrackerMockRecorder is the mock recorder for MockProgressTracker. -type MockProgressTrackerMockRecorder struct { - mock *MockProgressTracker -} - -// NewMockProgressTracker creates a new mock instance. -func NewMockProgressTracker(ctrl *gomock.Controller) *MockProgressTracker { - mock := &MockProgressTracker{ctrl: ctrl} - mock.recorder = &MockProgressTrackerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockProgressTracker) EXPECT() *MockProgressTrackerMockRecorder { - return m.recorder -} - -// GetPercent mocks base method. -func (m *MockProgressTracker) GetPercent() float64 { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPercent") - ret0, _ := ret[0].(float64) - return ret0 -} - -// GetPercent indicates an expected call of GetPercent. -func (mr *MockProgressTrackerMockRecorder) GetPercent() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPercent", reflect.TypeOf((*MockProgressTracker)(nil).GetPercent)) -} - -// LogTrackProgress mocks base method. -func (m *MockProgressTracker) LogTrackProgress(current int64) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "LogTrackProgress", current) -} - -// LogTrackProgress indicates an expected call of LogTrackProgress. -func (mr *MockProgressTrackerMockRecorder) LogTrackProgress(current any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogTrackProgress", reflect.TypeOf((*MockProgressTracker)(nil).LogTrackProgress), current) -} - -// MockSnapshotService is a mock of SnapshotService interface. -type MockSnapshotService struct { - ctrl *gomock.Controller - recorder *MockSnapshotServiceMockRecorder - isgomock struct{} -} - -// MockSnapshotServiceMockRecorder is the mock recorder for MockSnapshotService. -type MockSnapshotServiceMockRecorder struct { - mock *MockSnapshotService -} - -// NewMockSnapshotService creates a new mock instance. -func NewMockSnapshotService(ctrl *gomock.Controller) *MockSnapshotService { - mock := &MockSnapshotService{ctrl: ctrl} - mock.recorder = &MockSnapshotServiceMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockSnapshotService) EXPECT() *MockSnapshotServiceMockRecorder { - return m.recorder -} - -// GetCurrentMode mocks base method. -func (m *MockSnapshotService) GetCurrentMode() ports.SnapshotMode { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCurrentMode") - ret0, _ := ret[0].(ports.SnapshotMode) - return ret0 -} - -// GetCurrentMode indicates an expected call of GetCurrentMode. -func (mr *MockSnapshotServiceMockRecorder) GetCurrentMode() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentMode", reflect.TypeOf((*MockSnapshotService)(nil).GetCurrentMode)) -} - -// GetCurrentProgressTracker mocks base method. -func (m *MockSnapshotService) GetCurrentProgressTracker() *ports.ProgressTracker { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCurrentProgressTracker") - ret0, _ := ret[0].(*ports.ProgressTracker) - return ret0 -} - -// GetCurrentProgressTracker indicates an expected call of GetCurrentProgressTracker. -func (mr *MockSnapshotServiceMockRecorder) GetCurrentProgressTracker() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentProgressTracker", reflect.TypeOf((*MockSnapshotService)(nil).GetCurrentProgressTracker)) -} - -// GetProgressTracker mocks base method. -func (m *MockSnapshotService) GetProgressTracker() *ports.ProgressTracker { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetProgressTracker") - ret0, _ := ret[0].(*ports.ProgressTracker) - return ret0 -} - -// GetProgressTracker indicates an expected call of GetProgressTracker. -func (mr *MockSnapshotServiceMockRecorder) GetProgressTracker() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProgressTracker", reflect.TypeOf((*MockSnapshotService)(nil).GetProgressTracker)) -} - -// RestoreSnapshot mocks base method. -func (m *MockSnapshotService) RestoreSnapshot(dir, source string, options ports.RestoreSnapshotOptions) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RestoreSnapshot", dir, source, options) - ret0, _ := ret[0].(error) - return ret0 -} - -// RestoreSnapshot indicates an expected call of RestoreSnapshot. -func (mr *MockSnapshotServiceMockRecorder) RestoreSnapshot(dir, source, options any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreSnapshot", reflect.TypeOf((*MockSnapshotService)(nil).RestoreSnapshot), dir, source, options) -} - -// Snapshot mocks base method. -func (m *MockSnapshotService) Snapshot(dir, destination string, options ports.SnapshotOptions) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Snapshot", dir, destination, options) - ret0, _ := ret[0].(error) - return ret0 -} - -// Snapshot indicates an expected call of Snapshot. -func (mr *MockSnapshotServiceMockRecorder) Snapshot(dir, destination, options any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Snapshot", reflect.TypeOf((*MockSnapshotService)(nil).Snapshot), dir, destination, options) -} - // MockColdStarterServerInterface is a mock of ColdStarterServerInterface interface. type MockColdStarterServerInterface struct { ctrl *gomock.Controller diff --git a/test/utils/scroll.go b/test/utils/scroll.go index 155f921..ab081cd 100644 --- a/test/utils/scroll.go +++ b/test/utils/scroll.go @@ -28,7 +28,7 @@ func SetupScroll(t *testing.T, scroll domain.File) (string, string) { //observer := logger.SetupLogsCapture() unixTime := time.Now().Unix() cwd := "./druid-cli-test/" + strconv.FormatInt(unixTime, 10) + "/" - scrollPath := cwd + ".scroll/" + scrollPath := cwd t.Logf("Creating test scroll file in %s", scrollPath)