Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Connector pod updates "Forbidden" error #184

Merged
merged 11 commits into from Mar 5, 2024
20 changes: 15 additions & 5 deletions app/handlers/handlers_connectors.py
Expand Up @@ -124,7 +124,7 @@ def twingate_connector_create(body, memo, logger, namespace, patch, **_):


@kopf.on.update("twingateconnector", field=["spec"])
def twingate_connector_update(body, memo, logger, new, diff, status, **_):
def twingate_connector_update(body, memo, logger, new, diff, status, namespace, **_):
logger.info(
"Got TwingateConnector update request: %s. Diff: %s. Status: %s.",
new,
Expand All @@ -143,6 +143,10 @@ def twingate_connector_update(body, memo, logger, new, diff, status, **_):
return fail(error="Update called before Connector has an ID")

updated_connector = client.connector_update(crd.spec)

kapi = kubernetes.client.CoreV1Api()
kapi.delete_namespaced_pod(crd.metadata.name, namespace)

return success(twingate_id=updated_connector.id)


Expand Down Expand Up @@ -178,6 +182,9 @@ def twingate_connector_pod_reconciler(
crd = TwingateConnectorCRD(**body)
kapi = kubernetes.client.CoreV1Api()
k8s_pod = k8s_read_namespaced_pod(namespace, crd.metadata.name, kapi=kapi)
if k8s_pod and k8s_pod.status.phase != "Running":
raise kopf.TemporaryError("Pod not running.", delay=1)

image = k8s_pod.spec.containers[0].image if k8s_pod else None

if crd.spec.image or not k8s_pod:
Expand All @@ -195,12 +202,15 @@ def twingate_connector_pod_reconciler(
ANNOTATION_NEXT_VERSION_CHECK: crd.spec.image_policy.get_next_date_iso8601(),
}

pod = get_connector_pod(crd, memo.twingate_settings.full_url, image)
kopf.adopt(pod, owner=body, strict=True, forced=True)

if k8s_pod:
kapi.patch_namespaced_pod(meta.name, namespace, body=pod)
# When pod exists, can only update the image or we get `Forbidden: pod updates may not change fields other than `spec.containers[*].image`
current_image = k8s_pod.spec.containers[0].image
if current_image != image:
k8s_pod.spec.containers[0].image = image
kapi.patch_namespaced_pod(meta.name, namespace, body=k8s_pod)
else:
pod = get_connector_pod(crd, memo.twingate_settings.full_url, image)
kopf.adopt(pod, owner=body, strict=True, forced=True)
kapi.create_namespaced_pod(namespace, body=pod)

return success()
25 changes: 24 additions & 1 deletion app/handlers/tests/test_handlers_connector.py
Expand Up @@ -133,6 +133,9 @@ def test_twingate_connector_update(
run = kopf_handler_runner(
twingate_connector_update, crd, MagicMock(), new={}, diff={}
)
run.k8s_client_mock.delete_namespaced_pod.assert_called_with(
crd.metadata.name, "default"
)
assert run.result == {"success": True, "twingate_id": connector.id, "ts": ANY}


Expand Down Expand Up @@ -200,6 +203,10 @@ def test_pod_update(
status={"twingate_connector_create": {"success": True}}, with_id=True
)

mock_pod = MagicMock()
mock_pod.status.phase = "Running"
k8s_client_mock.read_namespaced_pod.return_value = mock_pod

run = kopf_handler_runner(twingate_connector_pod_reconciler, crd, MagicMock())
assert run.result == {"success": True, "ts": ANY}
run.k8s_client_mock.patch_namespaced_pod.assert_called_once()
Expand All @@ -225,6 +232,10 @@ def test_no_annotation(
with_id=True,
)

mock_pod = MagicMock()
mock_pod.status.phase = "Running"
k8s_client_mock.read_namespaced_pod.return_value = mock_pod

run = kopf_handler_runner(twingate_connector_pod_reconciler, crd, MagicMock())
assert run.result == {"success": True, "ts": ANY}
run.k8s_client_mock.patch_namespaced_pod.assert_called_once()
Expand All @@ -248,6 +259,10 @@ def test_past_due_annotation(
with_id=True,
)

mock_pod = MagicMock()
mock_pod.status.phase = "Running"
k8s_client_mock.read_namespaced_pod.return_value = mock_pod

run = kopf_handler_runner(twingate_connector_pod_reconciler, crd, MagicMock())
assert run.result == {"success": True, "ts": ANY}
run.k8s_client_mock.patch_namespaced_pod.assert_called_once()
Expand All @@ -273,8 +288,16 @@ def test_not_past_due_annotation(
with_id=True,
)

mock_container = MagicMock()
mock_container.image = "twingate/connector:latest"

mock_pod = MagicMock()
mock_pod.status.phase = "Running"
mock_pod.spec.containers = [mock_container]
k8s_client_mock.read_namespaced_pod.return_value = mock_pod

run = kopf_handler_runner(twingate_connector_pod_reconciler, crd, MagicMock())
assert run.result == {"success": True, "ts": ANY}
run.k8s_client_mock.patch_namespaced_pod.assert_called_once()
assert run.patch_mock.meta == {}
run.k8s_client_mock.patch_namespaced_pod.assert_not_called()
mock_get_image.assert_not_called()