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
Conversation
a7cb3f9
to
23b67c4
Compare
To test out this drop
|
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.
|
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'))) |
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.
Can we have a proper error class for this?
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 do not think it is necessary to create an error class for a feature flag variable
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.
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) |
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 should probably be ProviderNotSupportedError
? Note the suffix.
class ProviderNotSupported(_SnapcraftError): | ||
|
||
fmt = ( | ||
'The {provider!r} provider is not supported, please choose a ' |
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 follow the what/how/fix pattern?
logger = logging.getLogger(__name__) | ||
|
||
|
||
def _run(command: List) -> None: |
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.
Seems like a candidate for sharing with other modules.
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.
is there anything else that needs it, if so, let's identify them and create a follow up 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.
The lxd module would be an obvious candidate. Plugin commands could use it as well. But yeah, it can be in a follow-up.
23b67c4
to
5a16699
Compare
|
||
from snapcraft.internal.build_providers import errors | ||
|
||
T = TypeVar('T', bound='InstanceInfo') |
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.
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, |
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.
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. |
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 seems a perfect use-case for enums. Thoughts?
|
||
|
||
class Multipass(BaseProvider): | ||
"""A multipass provider for snapcraft execute its lifecycle.""" |
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.
To execute?
try: | ||
instance_info = self._get_instance_info() | ||
except errors.ProviderInfoError as info_error: | ||
self.echoer.warning( |
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.
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.
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.
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) |
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.
What.
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.
That was most of my Saturday!
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.
🤗
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.
Fixed in edge
;)
provider_name=self.provider_name, | ||
command=command) from process_error | ||
|
||
def copy_files(self, *, source: str, destination: str): |
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.
Missing a -> None
here
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.
Works for me!
try: | ||
return cls(name=instance_name, | ||
state=instance_info['state'], | ||
image_release=instance_info['image_release']) |
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 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 |
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.
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 ' |
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 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}.' |
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 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}" ?
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 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}.' |
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 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]]: |
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 should probably return a BaseProvider, no?
|
||
|
||
# This class is a mix of a parent and a mixin. | ||
class BaseProvider: |
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'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() |
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.
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.
snapcraft/project/__init__.py
Outdated
@@ -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 |
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.
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?
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.
Fwiw test_config
imports the private module by name today as "from snapcraft.project._project_info import ProjectInfo" for that reason.
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.
also add unit test
aaa8451
to
6d6f976
Compare
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 interface is coming together, but I have a few more suggestions.
|
||
@property | ||
@abc.abstractmethod | ||
def run(self) -> Callable[[str], None]: |
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.
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]: |
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.
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( |
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.
Could not delete?
import petname | ||
|
||
|
||
class Provider(): |
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 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. |
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.
Can you please document what this should return?
"""Return a callable that can be used to launch an instance.""" | ||
|
||
@abc.abstractmethod | ||
def create(self): |
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.
Missing -> None
for both of these.
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 quite nice! Great work 👏 .
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.
./runtests.sh static
?./runtests.sh unit
?