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

build_providers: new build provider using multipass #2100

Merged
merged 17 commits into from Apr 26, 2018

Conversation

sergiusens
Copy link
Collaborator

@sergiusens sergiusens commented Apr 23, 2018

build_providers is a new package to handle different providers snapcraft
can use to create a snap.

In addition to the scaffolding, an implementation to handle
multipass is provided. This is the MVP for multipass integration.

  • Have you followed the guidelines for contributing?
  • Have you signed the CLA?
  • If this is a bugfix. Have you checked that there is a bug report open for the issue you are trying to fix on bug reports?
  • If this is a new feature. Have you discussed the design on the forum?
  • Have you successfully run ./runtests.sh static?
  • Have you successfully run ./runtests.sh unit?

@sergiusens
Copy link
Collaborator Author

To test out this drop

snap install snapcraft --channel edge/multipass
cd <project>
SNAPCRAFT_BUILD_ENVIRONMENT=multipass snapcraft cleanbuild

@popey
Copy link
Contributor

popey commented Apr 23, 2018

Observations. The naming scheme isn't consistent with lxd. On lxd I end up with snapcraft-foo-bar-baz. With multipass I get foo-bar.

Also, it failed. Took 10 minutes to get to this failure.

$ SNAPCRAFT_BUILD_ENVIRONMENT=multipass snapcraft cleanbuild                                                                                                    
Creating a build environment named 'unreelable-kyndall'
Starting unreelable-kyndall                                                  
failed to launch: Attempt to access value of a disengaged optional object       
An error occurred when trying to launch the instance with 'multipass'.  

@popey
Copy link
Contributor

popey commented Apr 23, 2018

Tried a second time with the same result.

raise errors.SnapcraftEnvironmentError(
'SNAPCRAFT_BUILD_ENVIRONMENT must be one of: host or lxd.')
'SNAPCRAFT_BUILD_ENVIRONMENT must be one of: {}.'.format(
humanize_list(items=valid_providers, conjunction='or')))
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we have a proper error class for this?

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 do not think it is necessary to create an error class for a feature flag variable

Copy link
Contributor

Choose a reason for hiding this comment

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

What about testing? Having #1705 in mind where we want to get rid of messages in tests... or are you suggesting to not test this case?

if provider_name == 'multipass':
return Multipass
else:
raise errors.ProviderNotSupported(provider=provider_name)
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably be ProviderNotSupportedError? Note the suffix.

class ProviderNotSupported(_SnapcraftError):

fmt = (
'The {provider!r} provider is not supported, please choose a '
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this follow the what/how/fix pattern?

logger = logging.getLogger(__name__)


def _run(command: List) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like a candidate for sharing with other modules.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

is there anything else that needs it, if so, let's identify them and create a follow up PR

Copy link
Contributor

Choose a reason for hiding this comment

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

The lxd module would be an obvious candidate. Plugin commands could use it as well. But yeah, it can be in a follow-up.


from snapcraft.internal.build_providers import errors

T = TypeVar('T', bound='InstanceInfo')
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a need for this? You can just use 'InstanceInfo' (the string) as the type; as long as it's valid after this file is processed, mypy will be happy with it.

class InstanceInfo:

@classmethod
def new_instance_info_from_json(cls: Type[T], *, instance_name: str,
Copy link
Contributor

Choose a reason for hiding this comment

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

Callers will be using this as InstanceInfo.new_instance_info_from_json(...) which seems redundant. I find it reads very well to name these types of methods from_X, so InstanceInfo.from_json(...). Thoughts?


:param str name: the instance name.
:param str state: the state of the instance which can be any one of
RUNNING, STOPPED, DELETED.
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems a perfect use-case for enums. Thoughts?



class Multipass(BaseProvider):
"""A multipass provider for snapcraft execute its lifecycle."""
Copy link
Contributor

Choose a reason for hiding this comment

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

To execute?

try:
instance_info = self._get_instance_info()
except errors.ProviderInfoError as info_error:
self.echoer.warning(
Copy link
Contributor

@kyrofa kyrofa Apr 25, 2018

Choose a reason for hiding this comment

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

Is this not an error? We weren't able to do as requested, but to the caller, this will look like a success. This comment applies to the cases below 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.

Let's live chat about this one

raise errors.ProviderCommandNotFound(command=provider_cmd)
self.provider_cmd = provider_cmd
# Workaround for https://github.com/CanonicalLtd/multipass/issues/221
signal.signal(signal.SIGUSR1, _ignore_signal)
Copy link
Contributor

Choose a reason for hiding this comment

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

What.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That was most of my Saturday!

Copy link
Contributor

Choose a reason for hiding this comment

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

🤗

Copy link
Contributor

Choose a reason for hiding this comment

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

Fixed in edge ;)

provider_name=self.provider_name,
command=command) from process_error

def copy_files(self, *, source: str, destination: str):
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing a -> None here

Copy link
Contributor

@Saviq Saviq left a comment

Choose a reason for hiding this comment

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

Works for me!

try:
return cls(name=instance_name,
state=instance_info['state'],
image_release=instance_info['image_release'])
Copy link
Contributor

Choose a reason for hiding this comment

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

This gives you the release of the image from which this instance got launched. Is this enough? OTOH release is only currently available when the instance is running, so probably best to use that anyway.

import signal
import shutil
import subprocess
from typing import List
Copy link
Contributor

Choose a reason for hiding this comment

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

Uh oh, /me needs to school up on new python...

class ProviderCommandNotFound(_SnapcraftError):

fmt = (
'The {command!r} command is necessary to be able to build in this '
Copy link
Contributor

Choose a reason for hiding this comment

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

This jumps straight to why this is a problem instead of starting with the actual problem. Consider "{command!r} command not found: this command is necessary to build in this environment.\nInstall {command!r}...".


fmt = (
'An error occurred when trying to launch the instance with '
'{provider_name!r}.'
Copy link
Contributor

@kyrofa kyrofa Apr 25, 2018

Choose a reason for hiding this comment

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

I realize multipass errors should be above this exception, but would it be helpful to have the multipass exit codes here? This comment applies for all these command error classes. Similarly, should we follow Evan's advice and say something like "this is likely not a problem in Snapcraft, please check {provider_name}" ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Should be optional, these errors should be usable by things that talk over protocols as well (which do have error codes!)


fmt = (
'The data returned by {provider_name!r} was not expected. '
'It is missing some key data {key_info!r} in {data!r}.'
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't quite parse this. "key data {key_info!r}" -> is key_info a key in data? What is key data? Like... key = important?

from ._multipass import Multipass


def get_provider_for(provider_name: str) -> Union[Type[Multipass]]:
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably return a BaseProvider, no?



# This class is a mix of a parent and a mixin.
class BaseProvider:
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd personally rather see this called Provider. "Multipass is a provider" versus "multipass is a base provider".

with build_provider_class(project=project, echoer=echoer) as instance:
instance.provision_project(tar_filename)
instance.build_project()
instance.retrieve_snap()
Copy link
Contributor

@kyrofa kyrofa Apr 25, 2018

Choose a reason for hiding this comment

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

Given the factory nature of this, the interface used here should probably be implemented by the BaseProvider, or this quickly falls down as we add other providers.

@@ -15,3 +15,4 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from ._project import Project # noqa F401
from ._project_info import ProjectInfo # noqa F401
Copy link
Contributor

@kyrofa kyrofa Apr 25, 2018

Choose a reason for hiding this comment

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

Eventually we want to get to the point where the Project can fill out the ProjectInfo based on its __init__, which means this won't need to be imported here. Would it make sense to just import the private module in the test instead of making it publicly available now?

Copy link
Contributor

Choose a reason for hiding this comment

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

Fwiw test_config imports the private module by name today as "from snapcraft.project._project_info import ProjectInfo" for that reason.

Copy link
Contributor

@kyrofa kyrofa left a comment

Choose a reason for hiding this comment

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

The interface is coming together, but I have a few more suggestions.


@property
@abc.abstractmethod
def run(self) -> Callable[[str], None]:
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems odd to have this as a property, especially considering that it's actually a callable, so it LOOKS like a method when used. Why not just make it a method, and the implementer can just pass it on to their command instance?


@property
@abc.abstractmethod
def launch(self) -> Callable[[], None]:
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here, feels like it should be a method.

self.echoer.warning('Could not stop {!r}: {}.'.format(
self.instance_name, stop_error))
except errors.ProviderDeleteError as stop_error:
self.echoer.warning('Could not stop {!r}: {}.'.format(
Copy link
Contributor

Choose a reason for hiding this comment

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

Could not delete?

import petname


class Provider():
Copy link
Contributor

Choose a reason for hiding this comment

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

This is heading the right direction, but I'm losing track of what is and isn't public API ("public" defined to be "meant to be called by a user of Provider). For example, run and launch are both public, but I don't believe they're actually meant to be called by anything except Provider, right?

@abc.abstractmethod
def retrieve_snap(self) -> str:
"""
Provider steps needed to retrieve the built snap from the instance.
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you please document what this should return?

"""Return a callable that can be used to launch an instance."""

@abc.abstractmethod
def create(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing -> None for both of these.

Copy link
Contributor

@kyrofa kyrofa left a comment

Choose a reason for hiding this comment

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

This is quite nice! Great work 👏 .

@kyrofa kyrofa merged commit c44a4ec into canonical:master Apr 26, 2018
@sergiusens sergiusens deleted the build-providers branch April 26, 2018 01:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants