-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(cli): add --dry
option to pebble run
#214
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good overall (though I don't think I was part of the discussions about the new way to approach this -- but this seems reasonable). A few minor comments.
Thanks for your review Ben! Please refer to KO026 where I explain the new approach. I will be fixing this stuff starting tomorrow, and will let you know when it's ready for another review. Thanks again! |
@anpep was this ready for re-review? It's marked as Priority, but doesn't appear to be ready for review? |
Sorry for the confusion, yes, this PR is ready for review. |
71205fb
to
8ecc565
Compare
@@ -133,6 +141,11 @@ func New(pebbleDir string, restartHandler restart.Handler, serviceOutput io.Writ | |||
// the shared task runner should be added last! | |||
o.stateEng.AddManager(o.runner) | |||
|
|||
// Dry start all managers. | |||
if err := o.stateEng.DryStart(); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this only happen if dry
is true?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really; the dry start mechanism, as discussed on KO026, always runs in order to run initialization tasks that do not incur in unwanted side-effects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, but it's a bit awkward to see this here at the end of Init.
I think this shoud be run when starting, or on a specific DryStart
method that only does the dry-starting. This way we also don't need to provide further arguments into the constructor here, and no bools either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue of running this when starting (e.g. at the top of overlord.Loop()
) is that Loop()
might be called too late and other components (e.g. the daemon) might have already done some state-mutating operations.
The issue of requiring callers to call an overlord.DryStart()
method manually is that it's easy to forget, and thus misuse the API. Also, DryStart()
is not an operation you do on the overlord, but rather on the each of the managers. Holding the caller responsible for initializing the managers doesn't seem right to me: the overlord should be the one doing this very internal operation on the managers.
I think running this at the bottom of overlord.New()
is the sweet spot for these reasons. Also, we don't need any kind of arguments (that argument from previous commits was clearly a bug and a misunderstanding of the problem on my side), because DryStart()
is meant to be called always.
* Remove dryStartCallback from sampleManager. * Unexport ErrStateEngineStopped->errStateEngineStopped. * Simplify multiError string when there's only one error. * Fix mistakenly swapped logging of dry start/ensure errors.
* Add StateEngine.dryStarted check on Ensure() in order to make DryStart() call actually required. * Call DryStart() on overlord and state engine tests * Remove noopStateBackend: not required since --dry run is cut after the DryStart() call, so no state is ever persisted. Furthermore, this allows for potential dry-run of state patches. * Remove Overlord.addManager(), since StateEngine.AddManager() already exists for this purpose, and Overlord.AddManager() as well. * Don't plan.ReadDir() on ServiceManager.DryStart(), acquire plan instead so that we actually initialize the manager instead.
ca362d8
to
f615dbf
Compare
internals/overlord/managers_test.go
Outdated
@@ -42,7 +42,7 @@ func (s *mgrsSuite) SetUpTest(c *C) { | |||
|
|||
s.dir = c.MkDir() | |||
|
|||
o, err := overlord.New(s.dir, nil, nil) | |||
o, err := overlord.New(s.dir, nil, nil, false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the reason why I'm not a fan of boolean args: the call site is not readable unless you know the function prototype by heart.
I'm even less of a fan of providing nil
as a valid value to anything, but that's idiomatic Go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function signature is just asking for a refactor of its arguments to a struct:
o, err := overlord.New(s.dir, nil, nil, false) | |
o, err := overlord.New(&overlord.Opts{ | |
PebbleDir: s.dir, | |
RestartHandler: nil, | |
ServiceOutput: nil, | |
Dry: false, | |
}) |
But that's unrelated to this specific PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method has remained quite simple, with those couple of arguments, for several years now. But every few weeks we have a PR that tries to add more parameters to it, and if we had allowed these to go on we'd have a struct with 50 different options by now. Not everything that needs to be passed down a chain of objects needs to be in a constructor. In the case at hand, per the other comment, we should not do DryStart on the Init, but rather inside Start itself and only if it has not yet run externally. On the Daemon, we can call a DryStart on the overlord itself when we're doing a dry run.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Angel. Probably the last pass.
@@ -154,6 +157,9 @@ func runDaemon(rcmd *cmdRun, ch chan os.Signal, ready chan<- func()) error { | |||
if err != nil { | |||
return err | |||
} | |||
if rcmd.Dry { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this go all the way to the sanityCheck below, right before Start?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've extensively discussed this with @flotter, and one of his (rightful) concerns where around system-wide side effects caused by e.g. listening on a port, since it's an "external" side effect (i.e. contributes to Pebble initialization by persisting some state externally, be it listening on a socket or calling .Checkpoint()
on the state backend).
The idea is for the validation to be contained in the .DryStart()
methods, and also sanityCheck()
is a no-op for now anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, there's definitely a limit that is worth considering, so @flotter is right on track. The question here is the cost-benefit: what are we leaving out by following a bit further, and what are the issues that can happen. For example, if we just open the port, it means we've validated that the port may be opened at all. Given that benefit, what's the cost? Will any messages be handled, or maybe not because that's done further down?
@@ -133,6 +141,11 @@ func New(pebbleDir string, restartHandler restart.Handler, serviceOutput io.Writ | |||
// the shared task runner should be added last! | |||
o.stateEng.AddManager(o.runner) | |||
|
|||
// Dry start all managers. | |||
if err := o.stateEng.DryStart(); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, but it's a bit awkward to see this here at the end of Init.
I think this shoud be run when starting, or on a specific DryStart
method that only does the dry-starting. This way we also don't need to provide further arguments into the constructor here, and no bools either.
internals/overlord/managers_test.go
Outdated
@@ -42,7 +42,7 @@ func (s *mgrsSuite) SetUpTest(c *C) { | |||
|
|||
s.dir = c.MkDir() | |||
|
|||
o, err := overlord.New(s.dir, nil, nil) | |||
o, err := overlord.New(s.dir, nil, nil, false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method has remained quite simple, with those couple of arguments, for several years now. But every few weeks we have a PR that tries to add more parameters to it, and if we had allowed these to go on we'd have a struct with 50 different options by now. Not everything that needs to be passed down a chain of objects needs to be in a constructor. In the case at hand, per the other comment, we should not do DryStart on the Init, but rather inside Start itself and only if it has not yet run externally. On the Daemon, we can call a DryStart on the overlord itself when we're doing a dry run.
73f1670
to
782783f
Compare
Not sure if we discussed this before, but as a rule let's please not force-push after the reviews have initiated, as it makes incremental reviews uncomfortatable given the current GitHub features. |
@anpep looks like we could do with a merge conflict fixing here, and still some outstanding feedback. |
This PR still needs some discussion before getting in—will try to catch up rather soon. |
--dry
option to pebble run
--dry
option to pebble run
From the PR description:
From what I can tell looking at the code, Rockcraft currently uses Pydantic for validating the Pebble configuration it creates (implemented in canonical/rockcraft#331, in Is this Or alternatively, would using Pydantic in *craft tools be a good alternative to running Pebble with cc @cjdcordeiro and @tigarmo for the Rockcraft/*craft view on this, so we don't duplicate (and maintain) Pebble layer validation as two different methods in various craft tools. |
@thp-canonical Ideally, the two would coexist. Rockcraft would still use Pydantic to declare the |
I sometimes worry about the two validations "drifting". Since Rockcraft provisions |
Per discussion with @anpep, this is likely to end up in a different form later, so closing this for now. |
This PR adds dry start functionality to managers through the
DryStart()
method on theStateManager
interface (KO026) and adds the--dry
option to thepebble run
command.The
--dry
option is useful for validating the Pebble layers without having to actually start the Pebble daemon.Note that, in any case, pebble will exit. If the plan was valid, pebble will exit with an error code of 0; otherwise, with a non-zero exit code.
The previous iteration of this was done in PR #175.