Skip to content

Commit dd8820c

Browse files
authored
fix cache clearing (#359)
wasn't updated to handle the dedicated build namespace
1 parent 025932a commit dd8820c

5 files changed

Lines changed: 208 additions & 8 deletions

File tree

cabotage/celery/tasks/build.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,9 @@ def _dispatch_release_failure(release, error_detail):
268268

269269
def _build_namespace(app_env):
270270
"""Return the namespace where build jobs run."""
271-
return "cabotage-tenant-builds"
271+
return current_app.config.get(
272+
"KUBERNETES_BUILD_NAMESPACE", "cabotage-tenant-builds"
273+
)
272274

273275

274276
Activity = activity_plugin.activity_cls

cabotage/server/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ class Config(metaclass=MetaFlaskEnv):
131131
CELERY_RESULT_BACKEND = "redis://redis:6379"
132132
KUBERNETES_ENABLED = False
133133
KUBERNETES_CONTEXT = "cabotage"
134+
KUBERNETES_BUILD_NAMESPACE = "cabotage-tenant-builds"
134135
NETWORK_POLICIES_ENABLED = False
135136
BACKING_SERVICE_POSTGRES_ENABLED = False
136137
BACKING_SERVICE_REDIS_ENABLED = False

cabotage/server/user/views.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5403,9 +5403,13 @@ def application_clear_cache(org_slug, project_slug, app_slug):
54035403
image = application.images.first()
54045404
if image is not None and current_app.config["KUBERNETES_ENABLED"]:
54055405
from cabotage.celery.tasks.deploy import run_job
5406-
from cabotage.celery.tasks.build import fetch_image_build_cache_volume_claim
5406+
from cabotage.celery.tasks.build import (
5407+
_build_namespace,
5408+
fetch_image_build_cache_volume_claim,
5409+
)
54075410

54085411
buildkit_image = current_app.config["BUILDKIT_IMAGE"]
5412+
build_namespace = _build_namespace(app_env)
54095413

54105414
from cabotage.celery.tasks.deploy import _safe_labels_from_application
54115415

@@ -5490,7 +5494,7 @@ def application_clear_cache(org_slug, project_slug, app_slug):
54905494
)
54915495

54925496
job_complete, job_logs = run_job(
5493-
core_api_instance, batch_api_instance, "default", job_object
5497+
core_api_instance, batch_api_instance, build_namespace, job_object
54945498
)
54955499

54965500
def auth(dxf, response):

tests/test_build_jobs.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,22 @@ def _run_image_build(image, mock_core, mock_run_job):
160160

161161

162162
class TestBuildNamespace:
163-
def test_always_returns_tenant_builds_namespace(self):
163+
def test_always_returns_tenant_builds_namespace(self, mock_app):
164164
app_env = _make_app_env(org_k8s="myorg", env_k8s="staging", env_enabled=True)
165165
assert _build_namespace(app_env) == "cabotage-tenant-builds"
166166

167-
def test_env_disabled_still_returns_tenant_builds(self):
167+
def test_env_disabled_still_returns_tenant_builds(self, mock_app):
168168
app_env = _make_app_env(org_k8s="myorg", env_enabled=False)
169169
assert _build_namespace(app_env) == "cabotage-tenant-builds"
170170

171+
def test_uses_configured_build_namespace(self):
172+
app_env = _make_app_env(org_k8s="myorg", env_k8s="staging", env_enabled=True)
173+
mock_app = MagicMock()
174+
mock_app.config = {"KUBERNETES_BUILD_NAMESPACE": "tenant-builds-custom"}
175+
176+
with patch.object(build_module, "current_app", mock_app):
177+
assert _build_namespace(app_env) == "tenant-builds-custom"
178+
171179

172180
class TestBuildCachePVCName:
173181
def test_includes_environment_when_env_mode_enabled_even_if_app_env_is_legacy(self):
@@ -383,7 +391,7 @@ def test_reaper_label_selector_excludes_build_jobs(self):
383391

384392

385393
class TestBuildCachePVC:
386-
def test_pvc_created_in_tenant_namespace(self):
394+
def test_pvc_created_in_tenant_namespace(self, mock_app):
387395
from kubernetes.client.rest import ApiException
388396

389397
image = _make_image(org_k8s="myorg", env_k8s="prod")
@@ -398,7 +406,7 @@ def test_pvc_created_in_tenant_namespace(self):
398406
create_call = mock_core.create_namespaced_persistent_volume_claim.call_args
399407
assert create_call[0][0] == "cabotage-tenant-builds"
400408

401-
def test_pvc_created_with_safe_labels(self):
409+
def test_pvc_created_with_safe_labels(self, mock_app):
402410
from kubernetes.client.rest import ApiException
403411

404412
image = _make_image(org_k8s="myorg", env_k8s="prod")
@@ -420,7 +428,7 @@ def test_pvc_created_with_safe_labels(self):
420428
"cabotage.io/build-cache": "true",
421429
}
422430

423-
def test_pvc_read_in_tenant_namespace(self):
431+
def test_pvc_read_in_tenant_namespace(self, mock_app):
424432
image = _make_image(org_k8s="myorg", env_k8s="prod")
425433
mock_core = MagicMock()
426434

tests/test_clear_cache.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"""Tests for application cache-clearing behavior."""
2+
3+
import time
4+
import uuid
5+
from unittest.mock import MagicMock, patch
6+
7+
import pytest
8+
from flask_security import hash_password
9+
10+
from cabotage.server import db
11+
from cabotage.server.models.auth import Organization, User
12+
from cabotage.server.models.auth_associations import OrganizationMember
13+
from cabotage.server.models.projects import (
14+
Application,
15+
ApplicationEnvironment,
16+
Environment,
17+
Image,
18+
Project,
19+
)
20+
from cabotage.server.wsgi import app as _app
21+
22+
23+
@pytest.fixture
24+
def app():
25+
_app.config["TESTING"] = True
26+
_app.config["WTF_CSRF_ENABLED"] = False
27+
_app.config["REQUIRE_MFA"] = False
28+
_app.config["KUBERNETES_ENABLED"] = True
29+
_app.config["KUBERNETES_BUILD_NAMESPACE"] = "tenant-builds-custom"
30+
_app.config["BUILDKIT_IMAGE"] = "moby/buildkit:latest"
31+
_app.config["REGISTRY_BUILD"] = "registry.example.com"
32+
_app.config["REGISTRY_SECURE"] = False
33+
_app.config["REGISTRY_VERIFY"] = False
34+
with _app.app_context():
35+
yield _app
36+
_app.config["REQUIRE_MFA"] = True
37+
_app.config["KUBERNETES_ENABLED"] = False
38+
_app.config["KUBERNETES_BUILD_NAMESPACE"] = "cabotage-tenant-builds"
39+
40+
41+
@pytest.fixture
42+
def client(app):
43+
return app.test_client()
44+
45+
46+
@pytest.fixture
47+
def db_session(app):
48+
yield db.session
49+
db.session.rollback()
50+
51+
52+
@pytest.fixture
53+
def admin_user(db_session):
54+
user = User(
55+
username=f"admin-{uuid.uuid4().hex[:8]}",
56+
email=f"admin-{uuid.uuid4().hex[:8]}@example.com",
57+
password=hash_password("password123"),
58+
active=True,
59+
fs_uniquifier=uuid.uuid4().hex,
60+
)
61+
db_session.add(user)
62+
db_session.flush()
63+
return user
64+
65+
66+
@pytest.fixture
67+
def org(db_session, admin_user):
68+
org = Organization(name="Test Org", slug=f"testorg-{uuid.uuid4().hex[:8]}")
69+
db_session.add(org)
70+
db_session.flush()
71+
db_session.add(
72+
OrganizationMember(organization_id=org.id, user_id=admin_user.id, admin=True)
73+
)
74+
db_session.flush()
75+
return org
76+
77+
78+
@pytest.fixture
79+
def project(db_session, org):
80+
project = Project(name="Test Project", organization_id=org.id)
81+
db_session.add(project)
82+
db_session.flush()
83+
return project
84+
85+
86+
@pytest.fixture
87+
def environment(db_session, project):
88+
environment = Environment(name="default", project_id=project.id, ephemeral=False)
89+
db_session.add(environment)
90+
db_session.flush()
91+
return environment
92+
93+
94+
@pytest.fixture
95+
def application(db_session, project):
96+
application = Application(
97+
name="webapp",
98+
slug="webapp",
99+
project_id=project.id,
100+
github_repository="myorg/myrepo",
101+
)
102+
db_session.add(application)
103+
db_session.flush()
104+
return application
105+
106+
107+
@pytest.fixture
108+
def app_env(db_session, application, environment):
109+
app_env = ApplicationEnvironment(
110+
application_id=application.id,
111+
environment_id=environment.id,
112+
)
113+
db_session.add(app_env)
114+
db_session.flush()
115+
return app_env
116+
117+
118+
@pytest.fixture
119+
def image(db_session, application, app_env):
120+
image = Image(
121+
application_id=application.id,
122+
application_environment_id=app_env.id,
123+
_repository_name=application.registry_repository_name(app_env),
124+
build_ref="main",
125+
)
126+
db_session.add(image)
127+
db_session.flush()
128+
return image
129+
130+
131+
def _login(client, user):
132+
with client.session_transaction() as sess:
133+
sess["_user_id"] = user.fs_uniquifier
134+
sess["_fresh"] = True
135+
sess["fs_cc"] = "set"
136+
sess["fs_paa"] = time.time()
137+
sess["identity.id"] = user.id
138+
sess["identity.auth_type"] = "session"
139+
140+
141+
class TestApplicationClearCache:
142+
def test_clear_cache_runs_job_in_configured_build_namespace(
143+
self, client, admin_user, org, project, application, app_env, image
144+
):
145+
_login(client, admin_user)
146+
147+
volume_claim = MagicMock()
148+
volume_claim.metadata.name = "build-cache-pvc"
149+
dxf_client = MagicMock()
150+
151+
with (
152+
patch("cabotage.server.user.views.kubernetes_ext") as mock_kext,
153+
patch(
154+
"cabotage.server.user.views.kubernetes.client.CoreV1Api",
155+
return_value=MagicMock(),
156+
),
157+
patch(
158+
"cabotage.server.user.views.kubernetes.client.BatchV1Api",
159+
return_value=MagicMock(),
160+
),
161+
patch(
162+
"cabotage.celery.tasks.build.fetch_image_build_cache_volume_claim",
163+
return_value=volume_claim,
164+
),
165+
patch(
166+
"cabotage.celery.tasks.deploy.run_job",
167+
return_value=(True, "cleared"),
168+
) as mock_run_job,
169+
patch("cabotage.server.user.views.DXF", return_value=dxf_client),
170+
):
171+
mock_kext.kubernetes_client = MagicMock()
172+
dxf_client.get_alias.side_effect = [
173+
MagicMock(),
174+
MagicMock(),
175+
]
176+
177+
response = client.post(
178+
(
179+
f"/projects/{org.slug}/{project.slug}/applications/"
180+
f"{application.slug}/clearcache"
181+
)
182+
)
183+
184+
assert response.status_code == 302
185+
assert mock_run_job.call_args[0][2] == "tenant-builds-custom"

0 commit comments

Comments
 (0)