-
Notifications
You must be signed in to change notification settings - Fork 122
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: add a how-to for setting open ports (#1579)
Add a basic how-to guide that covers using `Unit.set_ports` to set which ports are open, and unit and integration tests for such. The intention is that this replaces the tutorial chapter (see #1511). --------- Co-authored-by: Dave Wilding <tech@dpw.me>
- Loading branch information
1 parent
fdc1943
commit a83ffef
Showing
2 changed files
with
100 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
(manage-opened-ports)= | ||
# How to manage opened ports | ||
> See first: {external+juju:ref}`Juju | Hook Commands | open-port <hook-command-open-port>` | ||
Juju manages the IP of each unit, so you need to instruct Juju | ||
if you want the charm to have a stable address. Typically, charms manage this | ||
by offering to integrate with an ingress charm, but you may also wish to have | ||
the charm itself open a port. | ||
|
||
## Implement the feature | ||
|
||
Use [](ops.Unit.set_ports) to to declare which ports should be open. For | ||
example, to set an open TCP port based on a configuration value, do the | ||
following in your `config-changed` observer in `src/charm.py`: | ||
|
||
```python | ||
def _on_holistic_handler(self, _: ops.EventBase): | ||
port = cast(int, self.config['server-port']) | ||
self.unit.set_ports(port) | ||
``` | ||
|
||
> See more: [](ops.Unit.set_ports) | ||
> Examples: [mysql-k8s opens the MySQL ports](https://github.com/canonical/mysql-k8s-operator/blob/a68147d0fbf66386ab087f4cfcc19784fcc2be6e/src/charm.py#L648), [tempo-coordinator-k8s opens both server and receiver ports](https://github.com/canonical/tempo-coordinator-k8s-operator/blob/ece268eae1158760513807a02972c138fd39afcf/src/charm.py#L95) | ||
`ops` also offers [](ops.Unit.open_port) and [](ops.Unit.close_port) methods, | ||
but the declarative approach is typically simpler. | ||
|
||
## Test the feature | ||
|
||
You'll want to add unit and integration tests. | ||
|
||
### Write unit tests | ||
|
||
> See first: {ref}`get-started-with-charm-testing`, {ref}`write-scenario-tests-for-a-charm` | ||
In your unit tests, use the [](ops.testing.State.opened_ports) component of the | ||
input `State` to specify which ports are already open when the event is | ||
run. Ports that are not listed are assumed to be closed. After events that modify which | ||
ports are open, assert that the output `State` has the correct set of ports. | ||
|
||
For example, in `tests/unit/test_charm.py`, this verifies that when the | ||
`config-changed` event runs, the only opened port is 8000 (for TCP): | ||
|
||
```python | ||
def test_open_port(): | ||
ctx = testing.Context(MyCharm) | ||
state_in = testing.State() | ||
state_out = ctx.run(ctx.on.config_changed(), state_in) | ||
assert state_out.opened_ports == {testing.TCPPort(8000)} | ||
``` | ||
|
||
### Write integration tests | ||
|
||
> See first: {ref}`get-started-with-charm-testing`, {ref}`write-integration-tests-for-a-charm` | ||
To verify that the correct ports are open in an integration test, deploy your | ||
charm as usual, and then try to connect to the appropriate ports. | ||
|
||
By adding the following test to your `tests/integration/test_charm.py` file, you can verify | ||
that your charm opens a port specified in the configuration, but prohibits using port 22: | ||
|
||
```python | ||
async def get_address(ops_test: OpsTest, app_name: str, unit_num: int = 0) -> str: | ||
"""Get the address for service for an app.""" | ||
status = await ops_test.model.get_status() | ||
return status['applications'][app_name].public_address | ||
|
||
def is_port_open(host: str, port: int) -> bool: | ||
"""Check if a port is opened in a particular host.""" | ||
try: | ||
with socket.create_connection((host, port), timeout=5): | ||
return True # If connection succeeds, the port is open | ||
except (ConnectionRefusedError, TimeoutError): | ||
return False # If connection fails, the port is closed | ||
|
||
@pytest.mark.abort_on_fail | ||
async def test_open_ports(ops_test: OpsTest): | ||
"""Verify that setting the server-port in the charm's opens that port. | ||
Assert blocked status in case of port 22 and active status for others. | ||
""" | ||
app = ops_test.model.applications.get('my-charm') | ||
|
||
# Get the public address of the app: | ||
address = await get_address(ops_test=ops_test, app_name=APP_NAME) | ||
# Validate that initial port is opened: | ||
assert is_port_open(address, 8000) | ||
|
||
# Set the port to 22 and validate the app goes to blocked status with the port not opened: | ||
await app.set_config({'server-port': '22'}) | ||
await ops_test.model.wait_for_idle(apps=[APP_NAME], status='blocked', timeout=120) | ||
assert not is_port_open(address, 22) | ||
|
||
# Set the port to 6789 and validate the app goes to active status with the port opened. | ||
await app.set_config({'server-port': '6789'}) | ||
await ops_test.model.wait_for_idle(apps=[APP_NAME], status='active', timeout=120) | ||
assert is_port_open(address, 6789) | ||
``` |