diff --git a/juju/application.py b/juju/application.py index 9671c5b0b..c3fddbcad 100644 --- a/juju/application.py +++ b/juju/application.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import hashlib import json import logging import pathlib @@ -433,6 +434,40 @@ async def get_actions(self, schema=False): actions = {k: v.description for k, v in actions.items()} return actions + def attach_resource(self, resource_name, file_name, file_obj): + """Updates the resource for an application by uploading file from + local disk to the Juju controller. + + :param str resource_name: Name of the resource to be updated. + :param str file_name: Name of the local file to be uploaded. + :param TextIOWrapper file_obj: Actual object to be read for data. + """ + conn, headers, path_prefix = self.connection.https_connection() + + url = "{}/applications/{}/resources/{}".format( + path_prefix, self.name, resource_name) + + data = file_obj.read() + + headers['Content-Type'] = 'application/octet-stream' + headers['Content-Length'] = len(data) + headers['Content-Sha384'] = hashlib.sha384(bytes(data, 'utf-8')).hexdigest() + + file_name = str(file_name) + if not file_name.startswith('./'): + file_name = './' + file_name + + headers['Content-Disposition'] = "form-data; filename=\"{}\"".format(file_name) + headers['Accept-Encoding'] = 'gzip' + headers['Bakery-Protocol-Version'] = 3 + headers['Connection'] = 'close' + + conn.request('PUT', url, data, headers) + response = conn.getresponse() + result = response.read().decode() + if not response.status == 200: + raise JujuError(result) + async def get_resources(self): """Return resources for this application. diff --git a/tests/integration/file-resource-charm/test.file b/tests/integration/file-resource-charm/test.file index e69de29bb..191028156 100644 --- a/tests/integration/file-resource-charm/test.file +++ b/tests/integration/file-resource-charm/test.file @@ -0,0 +1 @@ +foo \ No newline at end of file diff --git a/tests/integration/test_model.py b/tests/integration/test_model.py index 2c1888221..69dafefff 100644 --- a/tests/integration/test_model.py +++ b/tests/integration/test_model.py @@ -645,6 +645,22 @@ async def test_local_file_resource_charm(event_loop): assert ress['file-res'] +@base.bootstrapped +@pytest.mark.asyncio +async def test_attach_resource(event_loop): + charm_path = TESTS_DIR / 'integration' / 'file-resource-charm' + async with base.CleanModel() as model: + 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() + assert app.units[0].agent_status == 'idle' + + with open(str(charm_path / 'test.file')) as f: + app.attach_resource('file-res', 'test.file', f) + + @base.bootstrapped @pytest.mark.asyncio async def test_store_resources_bundle(event_loop):