juju / python-libjuju

Python library for the Juju API
Apache License 2.0
61 stars 100 forks source link

[juju 3.6] Switch Charms facade to v7 #1090

Open shayancanonical opened 3 months ago

shayancanonical commented 3 months ago

Description

We are using ops_test.model.deploy in our integration tests. While testing with juju 3.4.4 (along with python-libjuju ^3.2.2) we are able to run the following code:

await ops_test.model.deploy(
    "mysql-test-app",
    application_name="mysql-test-app1",
    num_units=1,
    channel="latest/edge",
)

However, while testing with juju 3.6-beta2 (along with python-libjuju 3.5.2.0), running the above code pulls in base ubuntu@20.04/stable sometimes and base ubuntu@22.04/stable other times. The charm mysql-test-app is available for both focal and jammy.

Example of the same code but different bases pulled:

Because (likely) juju 3.6 no longer supports certain bases (like ubuntu@20.04/stable), the default base python-libjuju uses in ops_test.model.deploy() fails with an error trace (shown in the reproduce section)

Urgency

Annoying bug in our test suite

Python-libjuju version

3.5.2.0

Juju version

3.6-beta2-ubuntu-amd64

Reproduce / Test

Using:

await ops_test.model.deploy(
    "mysql-test-app",
    application_name="mysql-test-app1",
    num_units=1,
    channel="latest/edge",
)

We get the following trace:

=============================================== FAILURES ================================================
_________________________________________ test_build_and_deploy _________________________________________
Traceback (most recent call last):
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/_pytest/runner.py", line 341, in from_call
    result: Optional[TResult] = func()
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/_pytest/runner.py", line 262, in <lambda>
    lambda: ihook(item=item, **kwds), when=when, reraise=reraise
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/pluggy/_hooks.py", line 433, in __call__
    return self._hookexec(self.name, self._hookimpls, kwargs, firstresult)
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/pluggy/_manager.py", line 112, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/pluggy/_callers.py", line 155, in _multicall
    return outcome.get_result()
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/pluggy/_result.py", line 108, in get_result
    raise exc.with_traceback(exc.__traceback__)
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/pluggy/_callers.py", line 80, in _multicall
    res = hook_impl.function(*args)
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/_pytest/runner.py", line 177, in pytest_runtest_call
    raise e
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/_pytest/runner.py", line 169, in pytest_runtest_call
    item.runtest()
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/_pytest/python.py", line 1792, in runtest
    self.ihook.pytest_pyfunc_call(pyfuncitem=self)
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/pluggy/_hooks.py", line 433, in __call__
    return self._hookexec(self.name, self._hookimpls, kwargs, firstresult)
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/pluggy/_manager.py", line 112, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/pluggy/_callers.py", line 155, in _multicall
    return outcome.get_result()
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/pluggy/_result.py", line 108, in get_result
    raise exc.with_traceback(exc.__traceback__)
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/pluggy/_callers.py", line 80, in _multicall
    res = hook_impl.function(*args)
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/_pytest/python.py", line 194, in pytest_pyfunc_call
    result = testfunction(**testargs)
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/pytest_asyncio/plugin.py", line 532, in inner
    _loop.run_until_complete(task)
  File "/usr/lib/python3.10/asyncio/base_events.py", line 649, in run_until_complete
    return future.result()
  File "/home/shayanp/code/mysql-operator/tests/integration/test_deploy_base.py", line 16, in test_build_and_deploy
    await ops_test.model.deploy(
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/juju/model.py", line 1841, in deploy
    return await self._deploy(
  File "/home/shayanp/code/mysql-operator/.tox/integration/lib/python3.10/site-packages/juju/model.py", line 2163, in _deploy
    raise JujuError('\n'.join(errors))
juju.errors.JujuError: base "ubuntu@20.04/stable" is not supported, supported bases are: ubuntu@22.04
------------------------------------------ Captured log setup -----------------------------------------

Using juju 3.6-beta2 directly:

shayanp@brisbane:~/code/mysql-operator$ juju3.6.beta deploy -n 1 mysql-test-app 
Deployed "mysql-test-app" from charm-hub charm "mysql-test-app", revision 46 in channel latest/stable on ubuntu@22.04/stable
shayanp@brisbane:~/code/mysql-operator$ juju3.6.beta deploy -n 1 mysql-test-app mysql-test-app2
Deployed "mysql-test-app2" from charm-hub charm "mysql-test-app", revision 46 in channel latest/stable on ubuntu@22.04/stable
shayanp@brisbane:~/code/mysql-operator$ juju3.6.beta deploy -n 1 mysql-test-app mysql-test-app3
Deployed "mysql-test-app3" from charm-hub charm "mysql-test-app", revision 46 in channel latest/stable on ubuntu@22.04/stable
hmlanigan commented 3 months ago

@shayancanonical are you able to work around this issue by specifying the base to use?

shayancanonical commented 3 months ago

@hmlanigan yes, i had to explicitly specify the base as a workaround to continue with testing our charms with juju 3.6-beta

taurus-forever commented 2 months ago

Hi, we have started testing juju 3.6 nightly and it affects our CI. JFMI: is the fix planned here anytime soon (as Juju 3.6 LTS is approaching).

Thank you!

dimaqq commented 1 month ago

I believe this is due to a subtle change in Juju 3.6: https://bugs.launchpad.net/juju/+bug/2084767

That being said, there's also a bug in this library: #1171

dimaqq commented 3 weeks ago

The cause is a subtle change in Juju 3.6 response to ResolveCharms:

I've analysed the traffic dump:

{
  "type": "Charms",
  "request": "ResolveCharms",
  "version": 6,
  "params": {
    "macaroon": null,
    "resolve": [
      {
        "reference": "ch:keystone",
        "charm-origin": {
          "source": "charm-hub",
          "architecture": "amd64",
          "track": "victoria",
          "risk": "stable",
          "base": {
            "channel": null,
            "name": null
          },
          "revision": null
        }
      }
    ]
  },
  "request-id": 9
}

{
  "request-id": 9,
  "response": {
    "Results": [
      {
        "url": "ch:amd64/keystone-690",
        "charm-origin": {
          "source": "charm-hub",
          "type": "charm",
          "id": "",
          "risk": "stable",
          "revision": 690,
          "track": "victoria",
          "architecture": "amd64",
          "base": {
            "name": "ubuntu",
            "channel": "20.04/stable"
          }
        },
        "supported-series": [
          "focal"
        ]
      }
    ]
  }
}

supported-series are defined in Charms v6 ResolveCharms:

Charms: 6
.ResolveCharms()
in: ResolveCharmsWithChannel
.macaroon: Macaroon
.macaroon: dict[Any, Any]
.resolve[0]: ResolveCharmWithChannel
.resolve[0].charm-origin: CharmOrigin
.resolve[0].charm-origin.architecture: str
.resolve[0].charm-origin.base: Base
.resolve[0].charm-origin.base.channel: str
.resolve[0].charm-origin.base.name: str
.resolve[0].charm-origin.branch: str
.resolve[0].charm-origin.hash: str
.resolve[0].charm-origin.id: str
.resolve[0].charm-origin.instance-key: str
.resolve[0].charm-origin.revision: int
.resolve[0].charm-origin.risk: str
.resolve[0].charm-origin.source: str
.resolve[0].charm-origin.track: str
.resolve[0].charm-origin.type: str
.resolve[0].reference: str
.resolve[0].switch-charm: bool
out: ResolveCharmWithChannelResults
.Results[0]: ResolveCharmWithChannelResult
.Results[0].charm-origin: CharmOrigin
.Results[0].charm-origin.architecture: str
.Results[0].charm-origin.base: Base
.Results[0].charm-origin.base.channel: str
.Results[0].charm-origin.base.name: str
.Results[0].charm-origin.branch: str
.Results[0].charm-origin.hash: str
.Results[0].charm-origin.id: str
.Results[0].charm-origin.instance-key: str
.Results[0].charm-origin.revision: int
.Results[0].charm-origin.risk: str
.Results[0].charm-origin.source: str
.Results[0].charm-origin.track: str
.Results[0].charm-origin.type: str
.Results[0].error: Error
.Results[0].error.code: str
.Results[0].error.info["abc"]: dict[Any, Any]
.Results[0].error.message: str
.Results[0].supported-series[0]: str
.Results[0].url: str

However, since the v7 schema is included in this project, the response definition in juju/client/_definitions.py doesn't have this field, as the v7 schema is as follows:

Charms: 7
.ResolveCharms()
in: ResolveCharmsWithChannel
.macaroon: Macaroon
.macaroon: dict[Any, Any]
.resolve[0]: ResolveCharmWithChannel
.resolve[0].charm-origin: CharmOrigin
.resolve[0].charm-origin.architecture: str
.resolve[0].charm-origin.base: Base
.resolve[0].charm-origin.base.channel: str
.resolve[0].charm-origin.base.name: str
.resolve[0].charm-origin.branch: str
.resolve[0].charm-origin.hash: str
.resolve[0].charm-origin.id: str
.resolve[0].charm-origin.instance-key: str
.resolve[0].charm-origin.revision: int
.resolve[0].charm-origin.risk: str
.resolve[0].charm-origin.source: str
.resolve[0].charm-origin.track: str
.resolve[0].charm-origin.type: str
.resolve[0].reference: str
.resolve[0].switch-charm: bool
out: ResolveCharmWithChannelResults
.Results[0]: ResolveCharmWithChannelResult
.Results[0].charm-origin: CharmOrigin
.Results[0].charm-origin.architecture: str
.Results[0].charm-origin.base: Base
.Results[0].charm-origin.base.channel: str
.Results[0].charm-origin.base.name: str
.Results[0].charm-origin.branch: str
.Results[0].charm-origin.hash: str
.Results[0].charm-origin.id: str
.Results[0].charm-origin.instance-key: str
.Results[0].charm-origin.revision: int
.Results[0].charm-origin.risk: str
.Results[0].charm-origin.source: str
.Results[0].charm-origin.track: str
.Results[0].charm-origin.type: str
.Results[0].error: Error
.Results[0].error.code: str
.Results[0].error.info["abc"]: dict[Any, Any]
.Results[0].error.message: str
.Results[0].supported-bases[0]: Base
.Results[0].supported-bases[0].channel: str
.Results[0].supported-bases[0].name: str
.Results[0].url: str

There's already a hack to account for this, though:

https://github.com/juju/python-libjuju/blob/94e12e7b471ced194af3931a86b71bdf798085e6/juju/model.py#L1920-L1923

I suppose this work-around was never tested, because of the explicit series being returned by Juju up until 3.6

I think the correct resolution is to use the Charms facade version 7 for Juju that supports that (3.3+) and the old code path for older Juju versions we still support.