Skip to content

Commit 00087c3

Browse files
authored
fix: do not add selector labels to allow seamless upgrade (#295)
1 parent 3828e8a commit 00087c3

File tree

10 files changed

+209
-37
lines changed

10 files changed

+209
-37
lines changed

.github/workflows/integrate.yaml

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jobs:
2121
steps:
2222
- name: Check out code
2323
uses: actions/checkout@v4
24+
2425
- name: Install dependencies
2526
run: pipx install tox
2627

@@ -63,7 +64,7 @@ jobs:
6364
- integration-with-profiles
6465
steps:
6566
- name: Maximise GH runner space
66-
uses: jlumbroso/free-disk-space@v1.3.1
67+
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
6768

6869
- name: Check out code
6970
uses: actions/checkout@v4
@@ -85,8 +86,18 @@ jobs:
8586
kubectl -n kube-system patch configmap cilium-config --type merge --patch '{"data":{"bpf-lb-sock-hostns-only":"true"}}'
8687
kubectl -n kube-system rollout restart daemonset cilium
8788
89+
- name: Fetch charm
90+
uses: actions/download-artifact@v5
91+
with:
92+
name: built-charm
93+
path: built/
94+
95+
- name: Get charm path
96+
id: charm-path
97+
run: echo "charm_path=$(find built/ -name '*.charm' -type f -print)" >> $GITHUB_OUTPUT
98+
8899
- name: Run integration tests
89-
run: tox -e ${{ matrix.tox-environment }} -- --model testing
100+
run: tox -e ${{ matrix.tox-environment }} -- --model testing --charm-path="${{ steps.charm-path.outputs.charm_path }}"
90101

91102
- name: Capture k8s resources on failure
92103
run: |
@@ -105,17 +116,25 @@ jobs:
105116
if: failure()
106117

107118
- name: Get secret
108-
run: kubectl get secret -ntesting training-operator-webhook-cert -oyaml
119+
run: kubectl get secret -n testing training-operator-webhook-cert -oyaml
109120
if: failure()
110121

111-
- name: Describe pod
112-
run: kubectl describe pod -ntesting -lapp.kubernetes.io/name=training-operator
122+
- name: Describe operator pod
123+
run: kubectl describe pod -n testing -l app.kubernetes.io/name=training-operator
124+
if: failure()
125+
126+
- name: Describe workload pod
127+
run: kubectl describe pod -n testing -l control-plane=testing-training-operator
113128
if: failure()
114129

115130
- name: Get pods
116131
run: kubectl get pods -A
117132
if: failure()
118133

119134
- name: Get operator logs
120-
run: kubectl logs --tail 100 -ntesting -lapp.kubernetes.io/name=training-operator -ctraining-operator
135+
run: kubectl logs --tail 100 -n testing -l app.kubernetes.io/name=training-operator -c charm
136+
if: failure()
137+
138+
- name: Get workload logs
139+
run: kubectl logs --tail 100 -n testing -l control-plane=testing-training-operator -c training-operator
121140
if: failure()

.github/workflows/on_pull_request.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,43 @@ on:
88
pull_request:
99

1010
jobs:
11+
build-charm:
12+
name: Build charm
13+
runs-on: ubuntu-24.04
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Setup LXD
19+
uses: canonical/setup-lxd@main
20+
with:
21+
channel: 5.21/stable
22+
23+
- name: Install charmcraft
24+
run: sudo snap install charmcraft --classic
25+
26+
- name: Build charm under test
27+
run: charmcraft pack --verbose
28+
29+
- name: Archive charm
30+
uses: actions/upload-artifact@v4
31+
with:
32+
name: built-charm
33+
path: "*.charm"
34+
retention-days: 5
1135

1236
tests:
1337
name: Run Tests
38+
needs:
39+
- build-charm
1440
uses: ./.github/workflows/integrate.yaml
1541
secrets: inherit
1642

1743
# publish runs in parallel with tests, as we always publish in this situation
1844
publish-charm:
1945
name: Publish Charm
46+
needs:
47+
- build-charm
2048
uses: ./.github/workflows/publish.yaml
2149
secrets: inherit
2250

.github/workflows/on_push.yaml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,35 @@ on:
1414
- track/**
1515

1616
jobs:
17+
build-charm:
18+
name: Build charm
19+
runs-on: ubuntu-24.04
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v4
23+
24+
- name: Setup LXD
25+
uses: canonical/setup-lxd@main
26+
with:
27+
channel: 5.21/stable
28+
29+
- name: Install charmcraft
30+
run: sudo snap install charmcraft --classic
31+
32+
- name: Build charm under test
33+
run: charmcraft pack --verbose
34+
35+
- name: Archive charm
36+
uses: actions/upload-artifact@v4
37+
with:
38+
name: built-charm
39+
path: "*.charm"
40+
retention-days: 5
1741

1842
tests:
1943
name: Run Tests
44+
needs:
45+
- build-charm
2046
uses: ./.github/workflows/integrate.yaml
2147
secrets: inherit
2248

.github/workflows/publish.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,23 @@ jobs:
8989
with:
9090
channel: latest/stable
9191

92+
- name: Fetch charm
93+
uses: actions/download-artifact@v5
94+
with:
95+
name: built-charm
96+
path: built/
97+
98+
- name: Get charm path
99+
id: charm-path
100+
run: echo "charm_path=$(find built/ -name '*.charm' -type f -print)" >> $GITHUB_OUTPUT
101+
92102
- name: Upload charm to charmhubpip-tools
93103
uses: canonical/charming-actions/upload-charm@2.6.2
94104
with:
95105
credentials: ${{ secrets.CHARMCRAFT_CREDENTIALS }}
96106
github-token: ${{ secrets.GITHUB_TOKEN }}
97107
charm-path: ${{ matrix.charm-path }}
108+
built-charm-path: ${{ steps.charm-path.outputs.charm_path }}
98109
channel: ${{ steps.parse-inputs.outputs.destination_channel }}
99110
tag-prefix: ${{ steps.parse-inputs.outputs.tag_prefix }}
100111
charmcraft-channel: 3.x/stable

src/charm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ def generate_allow_all_authorization_policy(
196196
spec=AuthorizationPolicySpec(
197197
selector=WorkloadSelector(
198198
# Use the unique label from src/templates/deployment.yaml.j2
199-
matchLabels={"app.kubernetes.io/name": f"{app_name}-manager"},
199+
matchLabels={"control-plane": f"{namespace}-{app_name}"},
200200
),
201201
action=Action.allow,
202202
rules=[Rule()],

src/templates/deployment.yaml.j2

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,19 @@ kind: Deployment
33
metadata:
44
labels:
55
control-plane: {{ namespace }}-{{ app_name }}
6-
# label added to distinguish charm pod from workload pod
7-
app.kubernetes.io/name: {{ app_name }}-manager
86
name: {{ app_name }}
97
namespace: {{ namespace }}
108
spec:
119
replicas: 1
1210
selector:
1311
matchLabels:
1412
control-plane: {{ namespace }}-{{ app_name }}
15-
# label added to distinguish charm pod from workload pod
16-
app.kubernetes.io/name: {{ app_name }}-manager
1713
template:
1814
metadata:
1915
annotations:
2016
sidecar.istio.io/inject: "false"
2117
labels:
2218
control-plane: {{ namespace }}-{{ app_name }}
23-
# label added to distinguish charm pod from workload pod
24-
app.kubernetes.io/name: {{ app_name }}-manager
2519
spec:
2620
containers:
2721
- command:

tests/integration/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2025 Canonical Ltd.
3+
# See LICENSE file for licensing details.
4+
5+
from _pytest.config.argparsing import Parser
6+
7+
8+
def pytest_addoption(parser: Parser):
9+
parser.addoption(
10+
"--charm-path",
11+
help="Path to charm file for performing tests on.",
12+
)

tests/integration/test_charm.py

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import glob
55
import logging
6+
import subprocess
67
from pathlib import Path
78

89
import lightkube
@@ -55,22 +56,25 @@ def lightkube_client() -> Client:
5556

5657

5758
@pytest.mark.abort_on_fail
58-
async def test_build_and_deploy(ops_test: OpsTest):
59+
async def test_build_and_deploy(ops_test: OpsTest, request: pytest.FixtureRequest):
5960
"""Build the charm and deploy it with trust=True.
6061
6162
Assert on the unit status.
6263
"""
63-
charm_under_test = await ops_test.build_charm(".")
64-
65-
await ops_test.model.deploy(charm_under_test, application_name=APP_NAME, trust=True)
64+
entity_url = (
65+
await ops_test.build_charm(".")
66+
if not (entity_url := request.config.getoption("--charm-path"))
67+
else Path(entity_url).resolve()
68+
)
69+
await ops_test.model.deploy(entity_url, application_name=APP_NAME, trust=True)
6670
await ops_test.model.wait_for_idle(
6771
apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=60 * 10
6872
)
6973
assert ops_test.model.applications[APP_NAME].units[0].workload_status == "active"
7074

7175
# store charm location in global to be used in other tests
7276
global CHARM_LOCATION
73-
CHARM_LOCATION = charm_under_test
77+
CHARM_LOCATION = entity_url
7478

7579
# Deploy grafana-agent for COS integration tests
7680
await deploy_and_assert_grafana_agent(ops_test.model, APP_NAME, metrics=True)
@@ -93,7 +97,7 @@ async def ensure_training_operator_is_running(ops_test: OpsTest) -> None:
9397
"wait",
9498
"--for=condition=ready",
9599
"pod",
96-
"-lapp.kubernetes.io/name=training-operator",
100+
f"-l control-plane={ops_test.model_name}-{APP_NAME}",
97101
f"-n{ops_test.model_name}",
98102
"--timeout=10m",
99103
check=True,
@@ -108,7 +112,7 @@ async def ensure_training_operator_is_running(ops_test: OpsTest) -> None:
108112
"status.phase!=Running",
109113
check=True,
110114
)
111-
assert "training-operator" not in out
115+
assert APP_NAME not in out
112116

113117

114118
def lightkube_create_global_resources() -> dict:
@@ -228,6 +232,40 @@ async def test_metrics_endpoint(ops_test: OpsTest):
228232
await assert_metrics_endpoint(app, metrics_port=METRICS_PORT, metrics_path=METRICS_PATH)
229233

230234

235+
def get_deployment_pod_names(model: str, application_name: str) -> list[str]:
236+
"""Retrieve names of all pods belonging to a specific Juju application.
237+
238+
This function uses kubectl to query the Kubernetes cluster for pods that match
239+
the given application name within the specified Juju model namespace. It filters
240+
pods by the label "control-plane".
241+
242+
Args:
243+
model (str): The name of the Juju model, which corresponds to the Kubernetes
244+
namespace where the pods are deployed.
245+
application_name (str): The name of the Juju application whose pods should
246+
be retrieved. This matches the "control-plane" label.
247+
248+
Returns:
249+
list[str]: A list of pod names matching the application. Returns an empty
250+
list if no pods are found or if the kubectl command fails.
251+
"""
252+
cmd = [
253+
"kubectl",
254+
"get",
255+
"pods",
256+
f"-n{model}",
257+
f"-l control-plane={model}-{application_name}",
258+
"--no-headers",
259+
"-o=custom-columns=NAME:.metadata.name",
260+
]
261+
proc = subprocess.run(
262+
cmd,
263+
stdout=subprocess.PIPE,
264+
)
265+
stdout = proc.stdout.decode("utf8")
266+
return stdout.split()
267+
268+
231269
def build_pod_container_map(model_name: str, deployment_template: dict) -> dict[str, dict]:
232270
"""Build full map of pods:containers belonging to this charm.
233271
@@ -236,7 +274,7 @@ def build_pod_container_map(model_name: str, deployment_template: dict) -> dict[
236274
`src/templates/deployment.yaml.j2`.
237275
"""
238276
charm_pods: list = get_pod_names(model_name, APP_NAME)
239-
deployment_pods: list = get_pod_names(model_name, f"{APP_NAME}-manager")
277+
deployment_pods: list = get_deployment_pod_names(model_name, APP_NAME)
240278
deployment_container_name = deployment_template["spec"]["template"]["spec"]["containers"][0][
241279
"name"
242280
]
@@ -295,7 +333,7 @@ async def test_remove_with_resources_present(ops_test: OpsTest):
295333
lightkube_client = lightkube.Client()
296334
crd_list = lightkube_client.list(
297335
CustomResourceDefinition,
298-
labels=[("app.juju.is/created-by", "training-operator")],
336+
labels=[("app.juju.is/created-by", APP_NAME)],
299337
namespace=ops_test.model_name,
300338
)
301339
# testing for empty list (iterator)
@@ -336,7 +374,7 @@ async def test_upgrade(ops_test: OpsTest):
336374
lightkube_client = lightkube.Client()
337375
crd_list = lightkube_client.list(
338376
CustomResourceDefinition,
339-
labels=[("app.juju.is/created-by", "training-operator")],
377+
labels=[("app.juju.is/created-by", APP_NAME)],
340378
namespace=ops_test.model_name,
341379
)
342380
# testing for non empty list (iterator)
@@ -383,7 +421,7 @@ async def test_remove_without_resources(ops_test: OpsTest):
383421
lightkube_client = lightkube.Client()
384422
crd_list = lightkube_client.list(
385423
CustomResourceDefinition,
386-
labels=[("app.juju.is/created-by", "training-operator")],
424+
labels=[("app.juju.is/created-by", APP_NAME)],
387425
namespace=ops_test.model_name,
388426
)
389427
for crd in crd_list:

0 commit comments

Comments
 (0)