Skip to content

Commit 8693e1f

Browse files
authored
Merge pull request rossoctl#1072 from pavelanni/feature/1063-auto-create-configmaps
feat(api): auto-create AuthBridge ConfigMaps for new namespaces
2 parents d0f3054 + ecc8daa commit 8693e1f

3 files changed

Lines changed: 267 additions & 0 deletions

File tree

kagenti/backend/app/core/constants.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,166 @@
134134

135135
# Environment variable name for the agent endpoint (the agent card URL for the agent)
136136
AGENT_ENDPOINT = "AGENT_ENDPOINT"
137+
138+
# Default Keycloak in-cluster URL (used by AuthBridge ConfigMaps)
139+
DEFAULT_KEYCLOAK_INTERNAL_URL = "http://keycloak-service.keycloak.svc:8080"
140+
DEFAULT_KEYCLOAK_REALM = "kagenti"
141+
142+
# Default spiffe-helper configuration for AuthBridge sidecars
143+
DEFAULT_SPIFFE_HELPER_CONF = (
144+
'agent_address = "/spiffe-workload-api/spire-agent.sock"\n'
145+
'cmd = ""\n'
146+
'cmd_args = ""\n'
147+
'svid_file_name = "/opt/svid.pem"\n'
148+
'svid_key_file_name = "/opt/svid_key.pem"\n'
149+
'svid_bundle_file_name = "/opt/svid_bundle.pem"\n'
150+
'jwt_svids = [{jwt_audience="kagenti", jwt_svid_file_name="/opt/jwt_svid.token"}]\n'
151+
"jwt_svid_file_mode = 0644\n"
152+
"include_federated_domains = true\n"
153+
)
154+
155+
# Default envoy-config for AuthBridge sidecars.
156+
# Matches the Helm chart template in charts/kagenti/templates/agent-namespaces.yaml.
157+
DEFAULT_ENVOY_YAML = """\
158+
admin:
159+
address:
160+
socket_address:
161+
protocol: TCP
162+
address: 127.0.0.1
163+
port_value: 9901
164+
165+
static_resources:
166+
listeners:
167+
- name: outbound_listener
168+
address:
169+
socket_address:
170+
protocol: TCP
171+
address: 0.0.0.0
172+
port_value: 15123
173+
listener_filters:
174+
- name: envoy.filters.listener.original_dst
175+
typed_config:
176+
"@type": type.googleapis.com/envoy.extensions.filters.listener.original_dst.v3.OriginalDst
177+
- name: envoy.filters.listener.tls_inspector
178+
typed_config:
179+
"@type": type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector
180+
filter_chains:
181+
- filter_chain_match:
182+
transport_protocol: tls
183+
filters:
184+
- name: envoy.filters.network.tcp_proxy
185+
typed_config:
186+
"@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
187+
stat_prefix: outbound_tls_passthrough
188+
cluster: original_destination
189+
- filter_chain_match:
190+
transport_protocol: raw_buffer
191+
filters:
192+
- name: envoy.filters.network.http_connection_manager
193+
typed_config:
194+
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
195+
stat_prefix: outbound_http
196+
codec_type: AUTO
197+
route_config:
198+
name: outbound_routes
199+
virtual_hosts:
200+
- name: catch_all
201+
domains: ["*"]
202+
routes:
203+
- match:
204+
prefix: "/"
205+
route:
206+
cluster: original_destination
207+
timeout: 300s
208+
http_filters:
209+
- name: envoy.filters.http.ext_proc
210+
typed_config:
211+
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor
212+
grpc_service:
213+
envoy_grpc:
214+
cluster_name: ext_proc_cluster
215+
timeout: 300s
216+
processing_mode:
217+
request_header_mode: SEND
218+
response_header_mode: SKIP
219+
request_body_mode: NONE
220+
response_body_mode: NONE
221+
- name: envoy.filters.http.router
222+
typed_config:
223+
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
224+
225+
- name: inbound_listener
226+
address:
227+
socket_address:
228+
protocol: TCP
229+
address: 0.0.0.0
230+
port_value: 15124
231+
listener_filters:
232+
- name: envoy.filters.listener.original_dst
233+
typed_config:
234+
"@type": type.googleapis.com/envoy.extensions.filters.listener.original_dst.v3.OriginalDst
235+
filter_chains:
236+
- filters:
237+
- name: envoy.filters.network.http_connection_manager
238+
typed_config:
239+
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
240+
stat_prefix: inbound_http
241+
codec_type: AUTO
242+
route_config:
243+
name: inbound_routes
244+
virtual_hosts:
245+
- name: local_app
246+
domains: ["*"]
247+
routes:
248+
- match:
249+
prefix: "/"
250+
route:
251+
cluster: original_destination
252+
timeout: 300s
253+
http_filters:
254+
- name: envoy.filters.http.lua
255+
typed_config:
256+
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
257+
inline_code: |
258+
function envoy_on_request(request_handle)
259+
request_handle:headers():add("x-authbridge-direction", "inbound")
260+
end
261+
- name: envoy.filters.http.ext_proc
262+
typed_config:
263+
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor
264+
grpc_service:
265+
envoy_grpc:
266+
cluster_name: ext_proc_cluster
267+
timeout: 300s
268+
processing_mode:
269+
request_header_mode: SEND
270+
response_header_mode: SKIP
271+
request_body_mode: NONE
272+
response_body_mode: NONE
273+
- name: envoy.filters.http.router
274+
typed_config:
275+
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
276+
277+
clusters:
278+
- name: original_destination
279+
connect_timeout: 30s
280+
type: ORIGINAL_DST
281+
lb_policy: CLUSTER_PROVIDED
282+
original_dst_lb_config:
283+
use_http_header: false
284+
285+
- name: ext_proc_cluster
286+
connect_timeout: 5s
287+
type: STATIC
288+
lb_policy: ROUND_ROBIN
289+
http2_protocol_options: {}
290+
load_assignment:
291+
cluster_name: ext_proc_cluster
292+
endpoints:
293+
- lb_endpoints:
294+
- endpoint:
295+
address:
296+
socket_address:
297+
address: 127.0.0.1
298+
port_value: 9090
299+
"""

kagenti/backend/app/routers/agents.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@
6363
# SPIRE identity constants
6464
KAGENTI_SPIRE_LABEL,
6565
KAGENTI_SPIRE_ENABLED_VALUE,
66+
# AuthBridge ConfigMap defaults
67+
DEFAULT_KEYCLOAK_INTERNAL_URL,
68+
DEFAULT_KEYCLOAK_REALM,
69+
DEFAULT_SPIFFE_HELPER_CONF,
70+
DEFAULT_ENVOY_YAML,
6671
)
6772
from app.core.config import settings
6873
from app.models.responses import (
@@ -1811,6 +1816,58 @@ async def get_shipwright_build_info(
18111816
raise HTTPException(status_code=e.status, detail=str(e.reason))
18121817

18131818

1819+
def _ensure_authbridge_configmaps(
1820+
kube: KubernetesService,
1821+
namespace: str,
1822+
spire_enabled: bool = False,
1823+
) -> None:
1824+
"""Ensure the 3 ConfigMaps required by AuthBridge sidecars exist.
1825+
1826+
Creates each ConfigMap only if it does not already exist, so user
1827+
customizations (e.g. pointing at a different Keycloak server) are
1828+
preserved on subsequent agent deploys.
1829+
1830+
The ConfigMaps match what the Helm chart creates in
1831+
charts/kagenti/templates/agent-namespaces.yaml:
1832+
- authbridge-config: Keycloak URLs for go-processor / client-registration
1833+
- envoy-config: Envoy proxy listeners and ext-proc integration
1834+
- spiffe-helper-config: SPIFFE workload API socket paths and SVID output
1835+
"""
1836+
keycloak_url = settings.keycloak_url or DEFAULT_KEYCLOAK_INTERNAL_URL
1837+
realm = settings.effective_keycloak_realm or DEFAULT_KEYCLOAK_REALM
1838+
# ISSUER must use the public/external URL because it must match the
1839+
# "iss" claim in JWT tokens issued by Keycloak (split-horizon DNS).
1840+
issuer = f"{settings.effective_keycloak_url}/realms/{realm}"
1841+
1842+
# 1. authbridge-config
1843+
kube.ensure_configmap(
1844+
namespace=namespace,
1845+
name="authbridge-config",
1846+
data={
1847+
"KEYCLOAK_URL": keycloak_url,
1848+
"KEYCLOAK_REALM": realm,
1849+
"ISSUER": issuer,
1850+
"SPIRE_ENABLED": "true" if spire_enabled else "false",
1851+
},
1852+
)
1853+
1854+
# 2. envoy-config
1855+
kube.ensure_configmap(
1856+
namespace=namespace,
1857+
name="envoy-config",
1858+
data={"envoy.yaml": DEFAULT_ENVOY_YAML},
1859+
)
1860+
1861+
# 3. spiffe-helper-config
1862+
kube.ensure_configmap(
1863+
namespace=namespace,
1864+
name="spiffe-helper-config",
1865+
data={"helper.conf": DEFAULT_SPIFFE_HELPER_CONF},
1866+
)
1867+
1868+
logger.info(f"Ensured AuthBridge ConfigMaps in namespace '{namespace}'")
1869+
1870+
18141871
def _build_agent_shipwright_build_manifest(
18151872
request: CreateAgentRequest, clone_secret_name: Optional[str] = None
18161873
) -> dict:
@@ -2373,6 +2430,14 @@ async def create_agent(
23732430
# SPIFFE identity uses the workload name, not the ReplicaSet hash.
23742431
kube.ensure_service_account(namespace=request.namespace, name=request.name)
23752432

2433+
# Ensure AuthBridge ConfigMaps exist in the target namespace
2434+
if request.authBridgeEnabled:
2435+
_ensure_authbridge_configmaps(
2436+
kube=kube,
2437+
namespace=request.namespace,
2438+
spire_enabled=request.spireEnabled,
2439+
)
2440+
23762441
# Create workload based on workloadType
23772442
if request.workloadType == WORKLOAD_TYPE_DEPLOYMENT:
23782443
workload_manifest = _build_deployment_manifest(
@@ -2761,6 +2826,14 @@ async def finalize_shipwright_build(
27612826
# SPIFFE identity uses the workload name, not the ReplicaSet hash.
27622827
kube.ensure_service_account(namespace=namespace, name=name)
27632828

2829+
# Ensure AuthBridge ConfigMaps exist in the target namespace
2830+
if final_auth_bridge:
2831+
_ensure_authbridge_configmaps(
2832+
kube=kube,
2833+
namespace=namespace,
2834+
spire_enabled=final_spire_enabled,
2835+
)
2836+
27642837
# Create workload based on workloadType
27652838
if final_workload_type == WORKLOAD_TYPE_DEPLOYMENT:
27662839
workload_manifest = _build_deployment_manifest(

kagenti/backend/app/services/kubernetes.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,37 @@ def ensure_service_account(self, namespace: str, name: str) -> None:
228228
logger.error(f"Error checking ServiceAccount '{name}' in {namespace}: {e}")
229229
raise
230230

231+
# -------------------------------------------------------------------------
232+
# ConfigMap Operations
233+
# -------------------------------------------------------------------------
234+
235+
def ensure_configmap(
236+
self, namespace: str, name: str, data: dict, labels: Optional[dict] = None
237+
) -> None:
238+
"""Create a ConfigMap if it does not already exist.
239+
240+
This is idempotent — if the ConfigMap already exists it is left unchanged
241+
so that user customizations are preserved.
242+
"""
243+
try:
244+
self.core_api.read_namespaced_config_map(name=name, namespace=namespace)
245+
logger.debug(f"ConfigMap '{name}' already exists in {namespace}")
246+
except ApiException as e:
247+
if e.status == 404:
248+
cm = kubernetes.client.V1ConfigMap(
249+
metadata=kubernetes.client.V1ObjectMeta(
250+
name=name,
251+
namespace=namespace,
252+
labels=labels or {"kagenti.io/managed-by": "kagenti-api"},
253+
),
254+
data=data,
255+
)
256+
self.core_api.create_namespaced_config_map(namespace=namespace, body=cm)
257+
logger.info(f"Created ConfigMap '{name}' in {namespace}")
258+
else:
259+
logger.error(f"Error checking ConfigMap '{name}' in {namespace}: {e}")
260+
raise
261+
231262
# -------------------------------------------------------------------------
232263
# Deployment Operations
233264
# -------------------------------------------------------------------------

0 commit comments

Comments
 (0)