Skip to content
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

o/snapstate: refactor Install functions to use a singular implementation that operates on a Target #13949

Open
wants to merge 33 commits into
base: master
Choose a base branch
from

Conversation

andrewphelpsj
Copy link
Collaborator

The Install family of functions in the snapstate has grown, and there are a handful of functions now that increase our API's surface area.

Since a lot of these functions look really similar, we can change up the public API a bit to allow for a single InstallTarget function that operates on a Target, which controls which snaps are installed, and where they come from. In this PR, I've implemented this for installing snaps from the store and local snaps.

I was able to implement Install, InstallMany, InstallPath, InstallWithDeviceContext, and InstallPathWithDeviceContext using the new InstallTarget function. There are two target implementations in this PR, StoreTarget and PathTarget. StoreTarget allows the installation of one or many snaps from the store. PathTarget allows the installation of one local snap. Note that PathTarget does not support installing many local snaps at once, since that looks much more like an update, and should be included in a potential refactor of the Update family.

There is very little change to the current public API of the package, as I was able to implement all of the functions mentioned in terms of the new functions. The only minor change needed, outside of the functions themselves, are the changes inside of functions called from the doPrerequisites task handler. This handler manually set up some lanes for the installed snaps, since InstallWithDeviceContext did not support lanes. This change enables InstallWithDeviceContext to support lanes.

Also something that I took note of while I was in here that may not need to be addressed. We don't currently care about validation sets right now when installing local snaps. Should that happen? If so, I could move that code from the StoreTarget implementation to the InstallTarget function so that all installs always care about validation sets.

@github-actions github-actions bot added the Needs Documentation -auto- Label automatically added which indicates the change needs documentation label May 6, 2024
…s follow correct pattern when returning no error
…terfaces, which controls how snaps are found
InstallPathMany is omitted for now, since it is more of an update rather
than an installation.
Copy link
Contributor

@MiguelPires MiguelPires left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. I left a few comments but this looks much better

overlord/snapstate/target.go Show resolved Hide resolved
// All of these fields are optional and can be left unset. The options in this
// struct apply to all snaps that are part of an operation. Options that apply
// to individual snaps can be found in RevisionOptions.
type SnapstateOptions struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is already in snapstate I don't think we need to repeat it in the name. From the outside it looks like snapstate.SnapstateOptions vs snapstate.Options

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, I had the same thought when I started using it to implement installing components from the store. Although, Samuele suggested that this type might not be something we want at all.

return nil
}

func InstallOne(ctx context.Context, st *state.State, target Target, opts SnapstateOptions) (*snap.Info, *state.TaskSet, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the goal to fully replace the previous public functions with these later or to keep those as wrappers around these? Because if it's the latter, then these can be unexported

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the goal will be to replace it eventually. For example, when installing components (the motivator for this change), the old API won't get that functionality.

overlord/snapstate/target.go Outdated Show resolved Hide resolved
Comment on lines 71 to 72
// verify that StoreTarget implements the Target interface
var _ Target = &StoreTarget{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need these explicit checks? My impression is that this goes against the point of implicit interface implementation. We have them in other places so other people may disagree but I'd say this check is done just by using StoreTarget and if it's not it's because we're not using the interface in which case it doesn't matter

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't really need them, no. The main reason I added them in the first place was to verify that &PathTarget{} is an OptionInitializer, which wouldn't be compile-time checked anywhere else.

overlord/snapstate/target.go Outdated Show resolved Hide resolved
return nil
}

func InstallOne(ctx context.Context, st *state.State, target Target, opts SnapstateOptions) (*snap.Info, *state.TaskSet, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comparing InstallOne with InstallTarget by their names, it's not clear which one does what. Especially since Target can be one or many snaps

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hrm, you're right. I'll think about names. In an ideal world, InstallTarget will become Install one day, once the old API stops being used.

overlord/snapstate/target.go Outdated Show resolved Hide resolved
overlord/snapstate/target.go Outdated Show resolved Hide resolved
overlord/snapstate/target.go Outdated Show resolved Hide resolved
Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you, did a pass, direction looks good, there is some cleaning up to do though

overlord/snapstate/handlers_prereq_test.go Outdated Show resolved Hide resolved
flags.Lane = lanes[0]
} else {
flags.Transaction = client.TransactionPerSnap
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this logic replaces the one below using joinLane?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, now that InstallWithDeviceContext supports lanes, we can use that rather than manually doing it here.

overlord/snapstate/target.go Outdated Show resolved Hide resolved
overlord/snapstate/target.go Outdated Show resolved Hide resolved
overlord/snapstate/target.go Outdated Show resolved Hide resolved
overlord/snapstate/target.go Show resolved Hide resolved

// Installables returns the data needed to setup the snaps from the store for
// installation.
func (s *StoreTarget) Installables(ctx context.Context, st *state.State, installedSnaps map[string]*SnapState, opts Options) ([]Installable, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is mostly replacing installCandidates?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is correct.

overlord/snapstate/target.go Outdated Show resolved Hide resolved
overlord/snapstate/target.go Show resolved Hide resolved
overlord/hookstate/ctlcmd/services_test.go Show resolved Hide resolved
Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the changes, did another quick pass with some comments on target.go

overlord/snapstate/target.go Outdated Show resolved Hide resolved
Comment on lines 342 to 344
if len(infos) != 1 || len(tasksets) != 1 {
return nil, nil, errors.New("internal error: expected exactly one snap and taskset")
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a bit strange because one would expect that we could check whether goal return one target or not earlier

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is a bit annoying. I was trying to avoid a flag being passed into InstallWithGoal to check that. One option is some implementation of InstallGoal that wraps another InstallGoal that makes sure that we return a singular Target.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be a bit too complex though for something that really could just be an if.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also be handled with the RequireOneSnap option introduced in 938cd26.

}

// Target represents the data needed to setup a snap for installation.
type Target struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as we discussed, should we make more things unexported ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is unexported, then the interface should be unexported as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in bda59b7.


results, _, err := str.SnapAction(context.TODO(), curSnaps, actions, nil, user, refreshOpts)
if err != nil {
if len(actions) == 1 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is quite the right approach, passing 1 snap to a Many function previously has different error behavior than passing it to a single target function. We might need to have a Single flag in options or something to control this better

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was really trying to avoid having to do that, since it'd be really nice if the single case is just a single iteration of the many case. Are there more differences than this error string right here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 938cd26.

andrewphelpsj added a commit that referenced this pull request May 30, 2024
…Many (#14027)

Install and InstallMany have subtle differences in how they handle the case where the store provides an empty response.

Install -> returns a snapstate.ErrMissingExpectedResult
InstallMany -> returns a store.SnapActionError

Add tests to define this behavior, and thus requiring #13949 to conform to this behavior.
@pedronis
Copy link
Collaborator

@andrewphelpsj there is a conflict now

Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the changes, couple quick comments

overlord/snapstate/target.go Outdated Show resolved Hide resolved
overlord/snapstate/target.go Outdated Show resolved Hide resolved
Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you, did another pass, some final minor things

overlord/snapstate/target.go Outdated Show resolved Hide resolved
overlord/snapstate/target.go Outdated Show resolved Hide resolved
overlord/snapstate/target.go Show resolved Hide resolved
overlord/snapstate/target.go Outdated Show resolved Hide resolved
overlord/snapstate/target.go Show resolved Hide resolved
snap/revision.go Outdated Show resolved Hide resolved
overlord/devicestate/firstboot.go Outdated Show resolved Hide resolved
@pedronis pedronis dismissed their stale review June 4, 2024 11:45

main issues are solved

Copy link
Collaborator

@pedronis pedronis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you

}

// PathInstallGoal creates a new InstallGoal to install a snap from a given from
// a path on disk. If instanceName is not provided, si.RealName will be used.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would "empty" be clearer than "is not provided"?

Copy link
Contributor

@MiguelPires MiguelPires left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, LGTM. Just some minor questions

overlord/snapstate/target.go Show resolved Hide resolved
//
// TODO: rename this to Install once the API is settled, and we can rename or
// remove the old Install function.
func InstallWithGoal(ctx context.Context, st *state.State, goal InstallGoal, opts Options) ([]*snap.Info, []*state.TaskSet, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really a problem but I'm just wondering if the plan is to keep the Install* functions in target.go? They feel a bit out of place. I expected target.go to have functions and interfaces that create installation goals but snapstate.go to have the functions actually install (handling tasksets and etc). Or is this more to split new functions from old for now?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I discussed with @andrewphelpsj to create an install.go file as well, maybe it could an immediate follow-up

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'll move things around in a follow-up.

overlord/snapstate/target.go Outdated Show resolved Hide resolved
overlord/snapstate/target.go Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Documentation -auto- Label automatically added which indicates the change needs documentation
Projects
None yet
3 participants