Repository: juju/python-libjuju

Issue:
Bundles are not deployed from CharmHub
Using python-libjuju version 2.9.4 I am facing this issue when deploying a bundle from CharmHub:

```
Traceback (most recent call last):
  File "examples/deploy_bundle_charmhub.py", line 36, in <module>
    loop.run(main())
  File "/home/davigar15/code/python-libjuju/venv/lib/python3.8/site-packages/juju/loop.py", line 38, in run
    raise task.exception()
  File "examples/deploy_bundle_charmhub.py", line 21, in main
    application = await model.deploy("ch:lma-light", channel="edge", trust=True)
  File "/home/davigar15/code/python-libjuju/venv/lib/python3.8/site-packages/juju/model.py", line 1603, in deploy
    await handler.execute_plan()
  File "/home/davigar15/code/python-libjuju/venv/lib/python3.8/site-packages/juju/bundle.py", line 296, in execute_plan
    self.references[step.id_] = await change.run(self)
  File "/home/davigar15/code/python-libjuju/venv/lib/python3.8/site-packages/juju/bundle.py", line 496, in run
    resources = await context.model._add_charmhub_resources(
  File "/home/davigar15/code/python-libjuju/venv/lib/python3.8/site-packages/juju/model.py", line 1759, in _add_charmhub_resources
    response = await resources_facade.AddPendingResources(
  File "/home/davigar15/code/python-libjuju/venv/lib/python3.8/site-packages/juju/client/facade.py", line 480, in wrapper
    reply = await f(*args, **kwargs)
  File "/home/davigar15/code/python-libjuju/venv/lib/python3.8/site-packages/juju/client/overrides.py", line 94, in AddPendingResources
    reply = await self.rpc(msg)
  File "/home/davigar15/code/python-libjuju/venv/lib/python3.8/site-packages/juju/client/facade.py", line 623, in rpc
    result = await self.connection.rpc(msg, encoder=TypeEncoder)
  File "/home/davigar15/code/python-libjuju/venv/lib/python3.8/site-packages/juju/client/connection.py", line 502, in rpc
    raise errors.JujuAPIError(result)
juju.errors.JujuAPIError: json: cannot unmarshal string into Go struct field AddPendingResourcesArgsV2.charm-origin of type params.CharmOrigin
```

This is the test I am running: https://github.com/juju/python-libjuju/pull/567/files#diff-3149620266881ce648c7b4665cbfefa78b527bfb5a110061479a178bced0e3d8

Discussion:
cderici: $$merge$$
cderici: $$merge$$
cderici: $$merge$$
cderici: $$merge$$
cderici: (merge job is failing with "bootstrapped environment is needed", and 405 from github api, so trying to fix at least one of them in the job configuration --- i.e. not spamming merge command)
cderici: $$merge$$

Patch:
diff --git a/examples/deploy_bundle_charmhub.py b/examples/deploy_bundle_charmhub.py
new file mode 100644
index 0000000..fc6f480
--- /dev/null
+++ b/examples/deploy_bundle_charmhub.py
@@ -0,0 +1,33 @@
+"""
+This example:
+1. Connects to the current model
+2. Deploy a bundle from charmhub and waits until it reports itself active
+3. Destroys the unit and application
+"""
+from juju import loop
+from juju.model import Model
+
+
+async def main():
+    model = Model()
+    print("Connecting to model")
+    # connect to current model with current user, per Juju CLI
+    await model.connect()
+
+    try:
+        print("Deploying lma-light")
+        applications = await model.deploy("ch:lma-light", channel="edge", trust=True)
+
+        print("Waiting for active")
+        await model.wait_for_idle(status="active")
+
+        print("Removing lma-light")
+        for application in applications:
+            await application.remove()
+    finally:
+        print("Disconnecting from model")
+        await model.disconnect()
+
+
+if __name__ == "__main__":
+    loop.run(main())
diff --git a/juju/bundle.py b/juju/bundle.py
index 65f8a6a..9756407 100644
--- a/juju/bundle.py
+++ b/juju/bundle.py
@@ -13,7 +13,7 @@ from toposort import toposort_flatten
 from .client import client
 from .constraints import parse as parse_constraints, parse_storage_constraint, parse_device_constraint
 from .errors import JujuError
-from . import utils, jasyncio
+from . import utils, jasyncio, charmhub
 from .origin import Channel
 from .url import Schema, URL
 
@@ -491,15 +491,6 @@ class AddApplicationChange(ChangeInfo):
             options["trust"] = "true"
 
         url = URL.parse(str(charm))
-        if Schema.CHARM_STORE.matches(url.schema):
-            resources = await context.model._add_store_resources(
-                self.application, charm, overrides=self.resources)
-        elif Schema.CHARM_HUB.matches(url.schema):
-            resources = await context.model._add_charmhub_resources(
-                self.application, charm, 'store', overrides=self.resources)
-        else:
-            resources = context.bundle.get("applications", {}).get(self.application, {}).get("resources", {})
-
         channel = None
         if self.channel is not None and self.channel != "":
             channel = Channel.parse(self.channel).normalize()
@@ -512,6 +503,20 @@ class AddApplicationChange(ChangeInfo):
             self.series = context.bundle.get("bundle",
                                              context.bundle.get("series", None))
 
+        if Schema.CHARM_STORE.matches(url.schema):
+            resources = await context.model._add_store_resources(
+                self.application, charm, overrides=self.resources)
+        elif Schema.CHARM_HUB.matches(url.schema):
+            c_hub = charmhub.CharmHub(context.model)
+            info = await c_hub.info(url.name, channel=self.channel)
+            if info.errors.error_list.code:
+                raise JujuError("unable to resolve the charm {} with channel {}".format(url.name, channel))
+            origin.id_ = info.result.id_
+            resources = await context.model._add_charmhub_resources(
+                self.application, charm, origin, overrides=self.resources)
+        else:
+            resources = context.bundle.get("applications", {}).get(self.application, {}).get("resources", {})
+
         await context.model._deploy(
             charm_url=charm,
             application=self.application,
@@ -521,8 +526,10 @@ class AddApplicationChange(ChangeInfo):
             endpoint_bindings=self.endpoint_bindings,
             resources=resources,
             storage=self.storage,
+            channel=self.channel,
             devices=self.devices,
             num_units=self.num_units,
+            charm_origin=origin,
         )
         return self.application
 


Test patch:
diff --git a/tests/unit/test_bundle.py b/tests/unit/test_bundle.py
index c46d896..120aaee 100644
--- a/tests/unit/test_bundle.py
+++ b/tests/unit/test_bundle.py
@@ -1,6 +1,7 @@
 from pathlib import Path
 import unittest
 from unittest import mock
+from mock import patch, Mock, ANY
 
 import yaml
 
@@ -19,6 +20,7 @@ from juju.bundle import (
     ScaleChange,
     SetAnnotationsChange,
 )
+from juju import charmhub
 from juju.client import client
 from toposort import CircularDependencyError
 
@@ -211,6 +213,8 @@ class TestAddApplicationChangeRun:
                                          resources=["resource1"],
                                          storage="storage",
                                          devices="devices",
+                                         channel="channel",
+                                         charm_origin=ANY,
                                          num_units="num_units")
 
         # confirm that it's idempotent
@@ -234,18 +238,24 @@ class TestAddApplicationChangeRun:
                                                      "num-units": "num_units",
                                                      "channel": "channel"})
 
-        model = mock.Mock()
+        model = Mock()
         model._deploy = base.AsyncMock(return_value=None)
         model._add_charmhub_resources = base.AsyncMock(return_value=["resource1"])
         model.applications = {}
 
-        context = mock.Mock()
+        context = Mock()
         context.resolve.return_value = "ch:charm1"
-        context.origins = {"ch:charm1": {"channel/stable": {}}}
+        context.origins = {"ch:charm1": Mock()}
         context.trusted = False
         context.model = model
 
-        result = await change.run(context)
+        info = Mock()
+        info.result.id_ = "12345"
+        info.errors.error_list.code = ''
+        info_func = base.AsyncMock(return_value=info)
+
+        with patch.object(charmhub.CharmHub, 'info', info_func):
+            result = await change.run(context)
         assert result == "application"
 
         model._deploy.assert_called_once()
@@ -258,6 +268,8 @@ class TestAddApplicationChangeRun:
                                          resources=["resource1"],
                                          storage="storage",
                                          devices="devices",
+                                         channel="channel",
+                                         charm_origin=ANY,
                                          num_units="num_units")
 
     @pytest.mark.asyncio
@@ -295,7 +307,9 @@ class TestAddApplicationChangeRun:
                                          resources={},
                                          storage="storage",
                                          devices="devices",
-                                         num_units="num_units")
+                                         num_units="num_units",
+                                         channel=None,
+                                         charm_origin=ANY)
 
     @pytest.mark.asyncio
     async def test_run_no_series(self, event_loop):
@@ -308,7 +322,8 @@ class TestAddApplicationChangeRun:
                                                      "endpoint-bindings": "endpoint_bindings",
                                                      "resources": "resources",
                                                      "devices": "devices",
-                                                     "num-units": "num_units"})
+                                                     "num-units": "num_units",
+                                                     "channel": "channel"})
 
         model = mock.Mock()
         model._deploy = base.AsyncMock(return_value=None)
@@ -339,6 +354,8 @@ class TestAddApplicationChangeRun:
                                          resources=["resource1"],
                                          storage="storage",
                                          devices="devices",
+                                         channel="channel",
+                                         charm_origin=ANY,
                                          num_units="num_units")
 
         # confirm that it's idempotent