diff --git a/internals/cli/cmd_run.go b/internals/cli/cmd_run.go index 11559a609..adf17383d 100644 --- a/internals/cli/cmd_run.go +++ b/internals/cli/cmd_run.go @@ -17,6 +17,7 @@ package cli import ( "errors" "fmt" + "maps" "os" "os/signal" "strconv" @@ -62,6 +63,9 @@ var sharedRunEnterArgsHelp = map[string]string{ "--args": "Provide additional arguments to a service", "--identities": "Seed identities from file (like update-identities --replace)", } +var runArgsHelp = map[string]string{ + "--dry": "Initializes {{.DisplayName}} without starting the daemon or side-effects", +} type cmdRun struct { client *client.Client @@ -69,15 +73,19 @@ type cmdRun struct { socketPath string pebbleDir string + DryRun bool `long:"dry"` sharedRunEnterOpts } func init() { + argsHelp := runArgsHelp + maps.Copy(argsHelp, sharedRunEnterArgsHelp) + AddCommand(&CmdInfo{ Name: "run", Summary: cmdRunSummary, Description: cmdRunDescription, - ArgsHelp: sharedRunEnterArgsHelp, + ArgsHelp: argsHelp, New: func(opts *CmdOptions) flags.Commander { return &cmdRun{ client: opts.Client, @@ -172,19 +180,25 @@ func runDaemon(rcmd *cmdRun, ch chan os.Signal, ready chan<- func()) error { t0 := time.Now().Truncate(time.Millisecond) if rcmd.CreateDirs { + if rcmd.DryRun { + return errors.New("cannot use --create-dirs and --dry at the same time") + } err := os.MkdirAll(rcmd.pebbleDir, 0755) if err != nil { return err } } - err = maybeCopyPebbleDir(rcmd.pebbleDir, getCopySource()) - if err != nil { - return err + if !rcmd.DryRun { + err = maybeCopyPebbleDir(rcmd.pebbleDir, getCopySource()) + if err != nil { + return err + } } dopts := daemon.Options{ Dir: rcmd.pebbleDir, SocketPath: rcmd.socketPath, + DryRun: rcmd.DryRun, } if rcmd.Verbose { dopts.ServiceOutput = os.Stdout @@ -195,8 +209,11 @@ func runDaemon(rcmd *cmdRun, ch chan os.Signal, ready chan<- func()) error { if err != nil { return err } - if err := d.Init(); err != nil { - return err + + if !rcmd.DryRun { + if err := d.Init(); err != nil { + return err + } } if rcmd.Args != nil { @@ -204,11 +221,29 @@ func runDaemon(rcmd *cmdRun, ch chan os.Signal, ready chan<- func()) error { if err != nil { return err } + if rcmd.DryRun { + logger.Noticef("Setting service args: %v", mappedArgs) + } if err := d.SetServiceArgs(mappedArgs); err != nil { return err } } + var identities map[string]*client.Identity + if rcmd.Identities != "" { + identities, err = readIdentities(rcmd.Identities) + if err != nil { + return fmt.Errorf("cannot read identities: %w", err) + } + } + + if rcmd.DryRun { + // If a hook for manager-specific dry run logic is added in the future, it + // should be run immediately above this block. + logger.Noticef("No error encountered: dry-run successful.") + return nil + } + // Run sanity check now, if anything goes wrong with the // check we go into "degraded" mode where we always report // the given error to any client. @@ -238,10 +273,6 @@ func runDaemon(rcmd *cmdRun, ch chan os.Signal, ready chan<- func()) error { logger.Debugf("activation done in %v", time.Now().Truncate(time.Millisecond).Sub(t0)) if rcmd.Identities != "" { - identities, err := readIdentities(rcmd.Identities) - if err != nil { - return fmt.Errorf("cannot read identities: %w", err) - } err = rcmd.client.ReplaceIdentities(identities) if err != nil { return fmt.Errorf("cannot replace identities: %w", err) diff --git a/internals/daemon/daemon.go b/internals/daemon/daemon.go index 68cdd9601..bc7bf3c1d 100644 --- a/internals/daemon/daemon.go +++ b/internals/daemon/daemon.go @@ -84,6 +84,9 @@ type Options struct { // OverlordExtension is an optional interface used to extend the capabilities // of the Overlord. OverlordExtension overlord.Extension + + // If true, no state will be written to file. + DryRun bool } // A Daemon listens for requests and routes them to the right command @@ -852,6 +855,7 @@ func New(opts *Options) (*Daemon, error) { RestartHandler: d, ServiceOutput: opts.ServiceOutput, Extension: opts.OverlordExtension, + DryRun: opts.DryRun, } ovld, err := overlord.New(&ovldOptions) diff --git a/internals/overlord/backend.go b/internals/overlord/backend.go index d57c75113..6b9a24ac6 100644 --- a/internals/overlord/backend.go +++ b/internals/overlord/backend.go @@ -34,3 +34,16 @@ func (osb *overlordStateBackend) Checkpoint(data []byte) error { func (osb *overlordStateBackend) EnsureBefore(d time.Duration) { osb.ensureBefore(d) } + +// dryRunStateBackend is a backend that does not actually write anything +type dryRunStateBackend struct { + ensureBefore func(d time.Duration) +} + +func (b *dryRunStateBackend) Checkpoint(data []byte) error { + return nil +} + +func (b *dryRunStateBackend) EnsureBefore(d time.Duration) { + b.ensureBefore(d) +} diff --git a/internals/overlord/overlord.go b/internals/overlord/overlord.go index 03d39dc61..5d58bc319 100644 --- a/internals/overlord/overlord.go +++ b/internals/overlord/overlord.go @@ -76,6 +76,9 @@ type Options struct { ServiceOutput io.Writer // Extension allows extending the overlord with externally defined features. Extension Extension + // DryRun must be true if state in storage is not meant to be altered. + // Otherwise, the Overlord will operate normally. + DryRun bool } // Overlord is the central manager of the system, keeping track @@ -106,6 +109,9 @@ type Overlord struct { logMgr *logstate.LogManager extension Extension + + // If true, no state will be written to file. + DryRun bool } // New creates an Overlord with all its state managers. @@ -116,6 +122,7 @@ func New(opts *Options) (*Overlord, error) { loopTomb: new(tomb.Tomb), inited: true, extension: opts.Extension, + DryRun: opts.DryRun, } if !filepath.IsAbs(o.pebbleDir) { @@ -130,9 +137,16 @@ func New(opts *Options) (*Overlord, error) { statePath := filepath.Join(o.pebbleDir, cmd.StateFile) - backend := &overlordStateBackend{ - path: statePath, - ensureBefore: o.ensureBefore, + var backend state.Backend + if opts.DryRun { + backend = &dryRunStateBackend{ + ensureBefore: o.ensureBefore, + } + } else { + backend = &overlordStateBackend{ + path: statePath, + ensureBefore: o.ensureBefore, + } } s, restartMgr, err := loadState(statePath, opts.RestartHandler, backend) if err != nil {