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

Handle pending upload resources deployfromrepository #953

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 52 additions & 29 deletions juju/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1743,6 +1743,8 @@ async def deploy(
if base:
charm_origin.base = utils.parse_base_arg(base)

server_side_deploy = False

if res.is_bundle:
handler = BundleHandler(self, trusted=trust, forced=force)
await handler.fetch_plan(url, charm_origin, overlays=overlays)
Expand Down Expand Up @@ -1774,9 +1776,20 @@ async def deploy(
else:
charm_origin = add_charm_res.charm_origin
if Schema.CHARM_HUB.matches(url.schema):
resources = await self._add_charmhub_resources(res.app_name,
identifier,
add_charm_res.charm_origin)

if client.ApplicationFacade.best_facade_version(self.connection()) >= 19:
server_side_deploy = True
else:
# TODO (cderici): this is an awkward workaround for basically not calling
# the AddPendingResources in case this is a server side deploy.
# If that's the case, then the store resources (and revisioned local
# resources) are handled at the server side if this is a server side deploy
# (local uploads are handled right after we get the pendingIDs returned
# from the facade call).
resources = await self._add_charmhub_resources(res.app_name,
identifier,
add_charm_res.charm_origin)

is_sub = await self.charmhub.is_subordinate(url.name)
if is_sub:
if num_units > 1:
Expand Down Expand Up @@ -1829,6 +1842,7 @@ async def deploy(
charm_origin=charm_origin,
attach_storage=attach_storage,
force=force,
server_side_deploy=server_side_deploy,
)

async def _add_charm(self, charm_url, origin):
Expand Down Expand Up @@ -2029,42 +2043,42 @@ async def add_local_resources(self, application, entity_url, metadata, resources
'username': '',
'password': '',
}

data = yaml.dump(docker_image_details)
else:
p = Path(path)
data = p.read_text() if p.exists() else ''

hash_alg = hashlib.sha3_384

charmresource['fingerprint'] = hash_alg(bytes(data, 'utf-8')).digest()
self._upload(data, path, application, name, resource_type, pending_id)

conn, headers, path_prefix = self.connection().https_connection()
return resource_map

query = "?pendingid={}".format(pending_id)
url = "{}/applications/{}/resources/{}{}".format(
path_prefix, application, name, query)
if resource_type == "oci-image":
disp = "multipart/form-data; filename=\"{}\"".format(path)
else:
disp = "form-data; filename=\"{}\"".format(path)
def _upload(self, data, path, app_name, res_name, res_type, pending_id):
conn, headers, path_prefix = self.connection().https_connection()

headers['Content-Type'] = 'application/octet-stream'
headers['Content-Length'] = len(data)
headers['Content-Sha384'] = charmresource['fingerprint'].hex()
headers['Content-Disposition'] = disp
query = "?pendingid={}".format(pending_id)
url = "{}/applications/{}/resources/{}{}".format(path_prefix, app_name, res_name, query)
if res_type == "oci-image":
disp = "multipart/form-data; filename=\"{}\"".format(path)
else:
disp = "form-data; filename=\"{}\"".format(path)

conn.request('PUT', url, data, headers)
headers['Content-Type'] = 'application/octet-stream'
headers['Content-Length'] = len(data)
headers['Content-Sha384'] = hashlib.sha384(bytes(data, 'utf-8')).hexdigest()
headers['Content-Disposition'] = disp

response = conn.getresponse()
result = response.read().decode()
if not response.status == 200:
raise JujuError(result)
conn.request('PUT', url, data, headers)

return resource_map
response = conn.getresponse()
result = response.read().decode()
if not response.status == 200:
raise JujuError(result)

async def _deploy(self, charm_url, application, series, config,
constraints, endpoint_bindings, resources, storage,
channel=None, num_units=None, placement=None,
devices=None, charm_origin=None, attach_storage=[],
force=False):
force=False, server_side_deploy=False):
"""Logic shared between `Model.deploy` and `BundleHandler.deploy`.
"""
log.info('Deploying %s', charm_url)
Expand All @@ -2077,7 +2091,7 @@ async def _deploy(self, charm_url, application, series, config,

app_facade = client.ApplicationFacade.from_connection(self.connection())

if client.ApplicationFacade.best_facade_version(self.connection()) >= 19:
if server_side_deploy:
# Call DeployFromRepository
app = client.DeployFromRepositoryArg(
applicationname=application,
Expand All @@ -2099,10 +2113,18 @@ async def _deploy(self, charm_url, application, series, config,
revision=charm_origin.revision,
)
result = await app_facade.DeployFromRepository([app])
# Collect the errors
errors = []
for r in result.results:
if r.errors:
errors.extend([e.message for e in r.errors])
# Upload pending local resources if any
for _result in result.results:
for pending_upload_resource in getattr(_result, 'pendingresourceuploads', []):
_path = pending_upload_resource.filename
p = Path(_path)
data = p.read_text() if p.exists() else ''
self._upload(data, _path, application, pending_upload_resource.name, 'file', '')
else:
app = client.ApplicationDeploy(
charm_url=charm_url,
Expand All @@ -2125,6 +2147,7 @@ async def _deploy(self, charm_url, application, series, config,
errors = [r.error.message for r in result.results if r.error]
if errors:
raise JujuError('\n'.join(errors))

return await self._wait_for_new('application', application)

async def destroy_unit(self, unit_id, destroy_storage=False, dry_run=False, force=False, max_wait=None):
Expand Down Expand Up @@ -2729,10 +2752,10 @@ def _raise_for_status(entities, status):
# errors to raise at the end
break
for unit in app.units:
if unit.machine is not None and unit.machine.status == "error":
if raise_on_error and unit.machine is not None and unit.machine.status == "error":
errors.setdefault("Machine", []).append(unit.machine.id)
continue
if unit.agent_status == "error":
if raise_on_error and unit.agent_status == "error":
errors.setdefault("Agent", []).append(unit.name)
continue
if raise_on_error and unit.workload_status == "error":
Expand Down
9 changes: 5 additions & 4 deletions tests/integration/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

logger = logging.getLogger(__name__)

from ..utils import INTEGRATION_TEST_DIR


@base.bootstrapped
async def test_action(event_loop):
Expand Down Expand Up @@ -191,17 +193,16 @@ async def test_upgrade_local_charm(event_loop):
@base.bootstrapped
async def test_upgrade_local_charm_resource(event_loop):
async with base.CleanModel() as model:
tests_dir = Path(__file__).absolute().parent
charm_path = tests_dir / 'file-resource-charm'
charm_path = INTEGRATION_TEST_DIR / 'file-resource-charm'
resources = {"file-res": "test.file"}

app = await model.deploy(str(charm_path), resources=resources)
assert 'file-resource-charm' in model.applications
await model.wait_for_idle()
await model.wait_for_idle(raise_on_error=False)
assert app.units[0].agent_status == 'idle'

await app.upgrade_charm(path=charm_path, resources=resources)
await model.wait_for_idle()
await model.wait_for_idle(raise_on_error=False)
ress = await app.get_resources()
assert 'file-res' in ress
assert ress['file-res']
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@ async def test_local_file_resource_charm(event_loop):
app = await model.deploy(str(charm_path), resources=resources)
assert 'file-resource-charm' in model.applications

await model.wait_for_idle()
await model.wait_for_idle(raise_on_error=False)
assert app.units[0].agent_status == 'idle'

ress = await app.get_resources()
Expand All @@ -718,7 +718,7 @@ async def test_attach_resource(event_loop):
app = await model.deploy(str(charm_path), resources=resources)
assert 'file-resource-charm' in model.applications

await model.wait_for_idle()
await model.wait_for_idle(raise_on_error=False)
assert app.units[0].agent_status == 'idle'

with open(str(charm_path / 'test.file')) as f:
Expand Down