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
Changes from 15 commits
8c709e0
69201b6
5ed406a
2619eaa
2596c79
d0ffae3
f0823f3
69bfd69
e2efc4b
69d95b5
2317aea
cff6ef5
de0823e
3ebd7fb
6d6f976
5a67061
0a0bbad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- | ||
# | ||
# Copyright (C) 2018 Canonical Ltd | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License version 3 as | ||
# published by the Free Software Foundation. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
from . import errors # noqa: F401 | ||
from ._factory import get_provider_for # noqa: F401 | ||
from ._multipass import Multipass # noqa: F401 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- | ||
# | ||
# Copyright (C) 2018 Canonical Ltd | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License version 3 as | ||
# published by the Free Software Foundation. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
import abc | ||
import shlex | ||
from typing import Callable | ||
|
||
import petname | ||
|
||
|
||
class Provider(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
def __init__(self, *, project, echoer) -> None: | ||
self.project = project | ||
self.echoer = echoer | ||
# Once https://github.com/CanonicalLtd/multipass/issues/220 is | ||
# closed we can prepend snapcraft- again. | ||
self.instance_name = petname.Generate(2, '-') | ||
self.project_dir = shlex.quote(project.info.name) | ||
|
||
if project.info.version: | ||
self.snap_filename = '{}_{}_{}.snap'.format( | ||
project.info.name, project.info.version, project.deb_arch) | ||
else: | ||
self.snap_filename = '{}_{}.snap'.format( | ||
project.info.name, project.deb_arch) | ||
|
||
def __enter__(self): | ||
self.create() | ||
return self | ||
|
||
def __exit__(self, exc_type, exc_val, exc_tb): | ||
self.destroy() | ||
|
||
@property | ||
@abc.abstractmethod | ||
def run(self) -> Callable[[str], None]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
"""Return a callable to run commands on the the instance. | ||
|
||
:returns: a callable which takes one argument, a list, with the | ||
command to run. | ||
""" | ||
|
||
@property | ||
@abc.abstractmethod | ||
def launch(self) -> Callable[[], None]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, feels like it should be a method. |
||
"""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 commentThe reason will be displayed to describe this comment to others. Learn more. Missing |
||
"""Provider steps needed to create a fully functioning environemnt.""" | ||
|
||
@abc.abstractmethod | ||
def destroy(self): | ||
"""Provider steps needed to ensure the instance is destroyed. | ||
|
||
This method should be safe to call multiple times and do nothing | ||
if the instance to destroy is already destroyed. | ||
""" | ||
|
||
@abc.abstractmethod | ||
def provision_project(self, tarball: str) -> None: | ||
"""Provider steps needed to copy project assests to the instance.""" | ||
|
||
@abc.abstractmethod | ||
def build_project(self) -> None: | ||
"""Provider steps needed build the project on the instance.""" | ||
|
||
@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 commentThe reason will be displayed to describe this comment to others. Learn more. Can you please document what this should return? |
||
""" | ||
|
||
def launch_instance(self): | ||
self.echoer.info('Creating a build environment named {!r}'.format( | ||
self.instance_name)) | ||
self.launch() | ||
|
||
def setup_snapcraft(self): | ||
self.echoer.info('Setting up snapcraft in {!r}'.format( | ||
self.instance_name)) | ||
install_cmd = ['sudo', 'snap', 'install', 'snapcraft', '--classic'] | ||
self.run(install_cmd) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- | ||
# | ||
# Copyright (C) 2018 Canonical Ltd | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License version 3 as | ||
# published by the Free Software Foundation. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
from typing import TYPE_CHECKING | ||
|
||
from . import errors | ||
from ._multipass import Multipass | ||
|
||
if TYPE_CHECKING: | ||
from typing import Type # noqa: F401 | ||
|
||
from ._base_provider import Provider # noqa: F401 | ||
|
||
|
||
def get_provider_for(provider_name: str) -> 'Type[Provider]': | ||
"""Returns a Type that can build with provider_name.""" | ||
if provider_name == 'multipass': | ||
return Multipass | ||
else: | ||
raise errors.ProviderNotSupportedError(provider=provider_name) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- | ||
# | ||
# Copyright (C) 2018 Canonical Ltd | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License version 3 as | ||
# published by the Free Software Foundation. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
from ._multipass import Multipass # noqa: F401 | ||
from ._multipass_command import MultipassCommand # noqa: F401 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- | ||
# | ||
# Copyright (C) 2018 Canonical Ltd | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License version 3 as | ||
# published by the Free Software Foundation. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
import json | ||
from typing import Type | ||
|
||
from snapcraft.internal.build_providers import errors | ||
|
||
|
||
class InstanceInfo: | ||
|
||
@classmethod | ||
def from_json(cls: Type['InstanceInfo'], *, instance_name: str, | ||
json_info: str) -> 'InstanceInfo': | ||
"""Create an InstanceInfo from json_info retrieved from multipass. | ||
|
||
:param str instance_name: the name of the instance. | ||
:param str json_info: a json formatted string with the structure | ||
that would follow the output of a json formatted | ||
multipass info command. | ||
:returns: an InstanceInfo. | ||
:rtype: InstanceInfo | ||
:raises snapcraft.internal.build_providers.ProviderInfoDataKeyError: | ||
if the instance name cannot be found in the given json or if a | ||
required key is missing from that data structure for the instance. | ||
""" | ||
try: | ||
json_data = json.loads(json_info) | ||
except json.decoder.JSONDecodeError as decode_error: | ||
raise errors.ProviderBadDataError( | ||
provider_name='multipass', | ||
data=json_info) from decode_error | ||
try: | ||
instance_info = json_data['info'][instance_name] | ||
except KeyError as missing_key: | ||
raise errors.ProviderInfoDataKeyError( | ||
provider_name='multipass', | ||
missing_key=str(missing_key), | ||
data=json_data) from missing_key | ||
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 commentThe 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 |
||
except KeyError as missing_key: | ||
raise errors.ProviderInfoDataKeyError( | ||
provider_name='multipass', | ||
missing_key=str(missing_key), | ||
data=instance_info) from missing_key | ||
|
||
def __init__(self, *, name: str, state: str, image_release: str) -> None: | ||
"""Initialize an InstanceInfo. | ||
|
||
: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 commentThe reason will be displayed to describe this comment to others. Learn more. This seems a perfect use-case for enums. Thoughts? |
||
:param str image_release: the Operating System release string for the | ||
image. | ||
""" | ||
# We do not check for validity of state given that new states could | ||
# be introduced. | ||
self.name = name | ||
self.state = state | ||
self.image_release = image_release | ||
|
||
def is_stopped(self): | ||
return self.state == 'STOPPED' |
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?