Skip to content

Commit 9022738

Browse files
DomAyrejepetty
authored andcommitted
[confcom] Add containers from vn2 command (Azure#9560)
* Add containers from_vn2 command * Satisfy azdev style and linter checks * Add some tests * Add tests for containers from_vn2 * Full testing * Fix merge conflict * Fix azdev style * Support multiple containers
1 parent 0f6aca2 commit 9022738

57 files changed

Lines changed: 10062 additions & 11 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

linter_exclusions.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3544,6 +3544,12 @@ neon postgres project:
35443544
rule_exclusions:
35453545
- require_wait_command_if_no_wait
35463546

3547+
confcom containers from_vn2:
3548+
parameters:
3549+
template:
3550+
rule_exclusions:
3551+
- no_positional_parameters
3552+
35473553
confcom fragment push:
35483554
parameters:
35493555
signed_fragment:

src/confcom/HISTORY.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
Release History
44
===============
55

6+
1.7.0
7+
++++++
8+
* Add containers from_vn2 command to generate container definitions from a VN2 template.
9+
610
1.6.0
711
++++++
812
* Added confcom containers from_image command to generate container definitions from an image reference

src/confcom/azext_confcom/_help.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,6 @@
329329
short-summary: Commands which generate Security Policy Container Definitions.
330330
"""
331331

332-
333332
helps[
334333
"confcom containers from_image"
335334
] = """
@@ -346,3 +345,22 @@
346345
- name: Input an image reference and generate container definitions
347346
text: az confcom containers from_image my.azurecr.io/myimage:tag
348347
"""
348+
349+
helps[
350+
"confcom containers from_vn2"
351+
] = """
352+
type: command
353+
short-summary: Create Security Policy Container Definitions based on a VN2 template.
354+
355+
parameters:
356+
- name: --name -n
357+
type: string
358+
short-summary: 'The name of the container to generate the policy for. If omitted, all containers are returned.'
359+
360+
361+
examples:
362+
- name: Input a VN2 Template and generate container definitions
363+
text: az confcom containers from_vn2 vn2.yaml --name mycontainer
364+
- name: Input a VN2 Template and generate container definitions for all containers
365+
text: az confcom containers from_vn2 vn2.yaml
366+
"""

src/confcom/azext_confcom/_params.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,3 +484,17 @@ def load_arguments(self, _):
484484
type=str,
485485
help="Platform to create container definition for",
486486
)
487+
488+
with self.argument_context("confcom containers from_vn2") as c:
489+
c.positional(
490+
"template",
491+
type=str,
492+
help="Template to create container definitions from",
493+
)
494+
c.argument(
495+
"container_name",
496+
options_list=['--name', "-n"],
497+
required=False,
498+
type=str,
499+
help='The name of the container in the template to use. If omitted, all containers are returned.'
500+
)
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
# --------------------------------------------------------------------------------------------
2+
# Copyright (c) Microsoft Corporation. All rights reserved.
3+
# Licensed under the MIT License. See License.txt in the project root for license information.
4+
# --------------------------------------------------------------------------------------------
5+
6+
import base64
7+
from dataclasses import asdict
8+
from hashlib import sha256
9+
import json
10+
from pathlib import Path
11+
import re
12+
from typing import Optional
13+
import yaml
14+
15+
from azext_confcom import config
16+
from azext_confcom.lib.platform import (
17+
PRIVILEDGED_CAPABILITIES,
18+
VN2_PRIVILEGED_MOUNTS,
19+
VN2_WORKLOAD_IDENTITY_ENV_RULES,
20+
VN2_WORKLOAD_IDENTITY_MOUNTS,
21+
)
22+
from azext_confcom.lib.policy import ContainerUser
23+
from azext_confcom.lib.containers import (
24+
from_image as container_from_image,
25+
merge_containers,
26+
)
27+
28+
29+
def find_vn2_containers(vn2_template):
30+
for key, value in vn2_template.items():
31+
if key in ("containers", "initContainers"):
32+
yield from value
33+
elif isinstance(value, dict):
34+
result = find_vn2_containers(value)
35+
if result is not None:
36+
yield from result
37+
elif isinstance(value, list):
38+
for item in value:
39+
if isinstance(item, dict):
40+
result = find_vn2_containers(item)
41+
if result is not None:
42+
yield from result
43+
44+
45+
def vn2_container_env_rules(template: dict, container: dict, template_variables: dict):
46+
47+
for env_var in container.get("env", []):
48+
49+
if "value" in env_var:
50+
is_special = re.match('^===VIRTUALNODE2.CC.THIM.(.+)===$', env_var.get('value'))
51+
yield {
52+
"pattern": f"{env_var.get('name')}={'.*' if is_special else env_var.get('value')}",
53+
"strategy": "re2" if is_special else "string",
54+
"required": False,
55+
}
56+
57+
elif "valueFrom" in env_var:
58+
59+
if "configMapKeyRef" in env_var.get('valueFrom') or "secretKeyRef" in env_var.get('valueFrom'):
60+
var_ref = (
61+
env_var.get('valueFrom').get("configMapKeyRef", None) or
62+
env_var.get('valueFrom').get("secretKeyRef", None)
63+
)
64+
yield {
65+
"pattern": f"{env_var.get('name')}={template_variables[var_ref.get('name')][var_ref.get('key')]}",
66+
"strategy": "string",
67+
"required": False,
68+
}
69+
70+
elif "fieldRef" in env_var.get('valueFrom'):
71+
# Existing behaviour is to wildcard this, there is a correct implementation below
72+
yield {
73+
"pattern": f"{env_var.get('name')}=.*",
74+
"strategy": "re2",
75+
"required": False,
76+
}
77+
# value = template
78+
# for part in env_var.get('valueFrom').get("fieldRef", {}).get("fieldPath", "").split("."):
79+
# value = value.get(part, {})
80+
# yield {
81+
# "pattern": f"{env_var.get('name')}={value}",
82+
# "strategy": "string",
83+
# "required": False,
84+
# })
85+
86+
elif "resourceFieldRef" in env_var.get('valueFrom'):
87+
ref = env_var.get('valueFrom').get("resourceFieldRef", {})
88+
ref_container_name = ref.get("containerName") or container.get("name")
89+
ref_container = next(
90+
(
91+
c for c in template["spec"]["containers"]
92+
if c.get("name") == ref_container_name
93+
),
94+
None,
95+
)
96+
if ref_container is None:
97+
continue
98+
value = ref_container.get("resources", {})
99+
for part in ref["resource"].split("."):
100+
value = value.get(part, {})
101+
yield {
102+
"pattern": f"{env_var.get('name')}={value}",
103+
"strategy": "string",
104+
"required": False,
105+
}
106+
107+
108+
def vn2_container_mounts(template: dict, container: dict) -> list[dict]:
109+
110+
volume_claim_access = {
111+
v["metadata"]["name"]: v.get("spec", {}).get("accessModes", [])
112+
for v in template.get("spec", {}).get("volumeClaimTemplates", [])
113+
}
114+
volume_defs = {
115+
v["name"]: [k for k in v.keys() if k != "name"][0]
116+
for v in template.get("spec", {}).get("volumes", [])
117+
}
118+
119+
return [
120+
{
121+
"destination": m.get("mountPath"),
122+
"options": [
123+
"rbind",
124+
"rshared",
125+
"ro" if (
126+
m.get("readOnly") or
127+
"ReadOnlyMany" in volume_claim_access.get(m.get("name"), []) or
128+
volume_defs.get(m.get("name")) in {"configMap", "secret", "downwardAPI", "projected"}
129+
) else "rw"
130+
],
131+
"source": "sandbox:///tmp/atlas/emptydir/.+",
132+
"type": "bind",
133+
}
134+
for m in container.get("volumeMounts", [])
135+
]
136+
137+
138+
def containers_from_vn2(
139+
template: str,
140+
container_name: Optional[str] = None
141+
) -> str:
142+
143+
with Path(template).open("r") as f:
144+
template_yaml = list(yaml.safe_load_all(f))
145+
146+
# Find containers matching the specified name (if provided)
147+
template_containers = []
148+
variables = {}
149+
for doc in template_yaml:
150+
if not isinstance(doc, dict):
151+
continue
152+
kind = doc.get("kind")
153+
if kind == "ConfigMap":
154+
variables[doc["metadata"]["name"]] = {
155+
**doc.get("data", {}),
156+
**{k: base64.b64decode(v).decode("utf-8") for k, v in doc.get("binaryData", {}).items()},
157+
}
158+
elif kind == "Secret":
159+
variables[doc["metadata"]["name"]] = {
160+
**{k: base64.b64decode(v).decode("utf-8") for k, v in doc.get("data", {}).items()},
161+
**doc.get("stringData", {}),
162+
}
163+
elif kind in ["Pod", "Deployment", "StatefulSet", "DaemonSet", "Job", "CronJob", "ReplicaSet"]:
164+
for container in find_vn2_containers(doc):
165+
if container_name and container.get("name") != container_name:
166+
continue
167+
template_containers.append((container, doc))
168+
169+
if container_name:
170+
if not template_containers:
171+
raise AssertionError(f"No containers with name {container_name} found.")
172+
if len(template_containers) > 1:
173+
raise AssertionError(
174+
f"Multiple containers with name {container_name} found."
175+
)
176+
elif not template_containers:
177+
raise AssertionError("No containers found.")
178+
179+
container_defs = []
180+
for template_container, template_doc in template_containers:
181+
image_container_def = container_from_image(template_container.get("image"), platform="vn2")
182+
183+
template_container_def = {
184+
"name": template_container.get("name"),
185+
"command": template_container.get("command", []) + template_container.get("args", []),
186+
"env_rules": (
187+
[
188+
{
189+
"pattern": rule.get("pattern") or f"{rule.get('name')}={rule.get('value')}",
190+
"strategy": rule.get("strategy", "string"),
191+
"required": rule.get("required", False),
192+
}
193+
for rule in (
194+
config.OPENGCS_ENV_RULES
195+
+ config.FABRIC_ENV_RULES
196+
+ config.MANAGED_IDENTITY_ENV_RULES
197+
+ config.ENABLE_RESTART_ENV_RULE
198+
+ config.VIRTUAL_NODE_ENV_RULES
199+
)
200+
]
201+
+ list(vn2_container_env_rules(template_doc, template_container, variables))
202+
),
203+
"mounts": vn2_container_mounts(template_doc, template_container),
204+
}
205+
206+
# Parse security context
207+
security_context = (
208+
template_doc.get("spec", {}).get("securityContext", {})
209+
| template_container.get("securityContext", {})
210+
)
211+
if security_context.get("privileged", False):
212+
template_container_def["allow_elevated"] = True
213+
template_container_def["mounts"] += VN2_PRIVILEGED_MOUNTS
214+
template_container_def["capabilities"] = PRIVILEDGED_CAPABILITIES
215+
216+
if security_context.get("runAsUser") or security_context.get("runAsGroup"):
217+
template_container_def["user"] = asdict(ContainerUser())
218+
if security_context.get("runAsUser"):
219+
template_container_def["user"]["user_idname"] = {
220+
"pattern": str(security_context.get("runAsUser")),
221+
"strategy": "id",
222+
}
223+
if security_context.get("runAsGroup"):
224+
template_container_def["user"]["group_idnames"] = [{
225+
"pattern": str(security_context.get("runAsGroup")),
226+
"strategy": "id",
227+
}]
228+
229+
if security_context.get("seccompProfile"):
230+
template_container_def["seccomp_profile_sha256"] = sha256(
231+
base64.b64decode(security_context.get("seccompProfile"))
232+
).hexdigest()
233+
234+
if security_context.get("allowPrivilegeEscalation") is False:
235+
template_container_def["no_new_privileges"] = True
236+
237+
# Check for workload identity
238+
labels = template_doc.get("metadata", {}).get("labels", {}) or {}
239+
if labels.get("azure.workload.identity/use", "false") == "true":
240+
template_container_def["env_rules"].extend(VN2_WORKLOAD_IDENTITY_ENV_RULES)
241+
template_container_def["mounts"].extend(VN2_WORKLOAD_IDENTITY_MOUNTS)
242+
243+
exec_processes = [
244+
{
245+
"command": process.get("exec", {}).get("command", []),
246+
"signals": []
247+
}
248+
for process in [
249+
template_container.get("livenessProbe"),
250+
template_container.get("readinessProbe"),
251+
template_container.get("startupProbe"),
252+
template_container.get("lifecycle", {}).get("postStart"),
253+
template_container.get("lifecycle", {}).get("preStop"),
254+
]
255+
if process is not None
256+
]
257+
if exec_processes:
258+
template_container_def["exec_processes"] = exec_processes
259+
260+
container_defs.append(merge_containers(
261+
image_container_def,
262+
template_container_def,
263+
))
264+
265+
return json.dumps(container_defs)

src/confcom/azext_confcom/commands.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ def load_command_table(self, _):
1919
pass
2020

2121
with self.command_group("confcom containers") as g:
22+
g.custom_command("from_vn2", "containers_from_vn2")
2223
g.custom_command("from_image", "containers_from_image")

src/confcom/azext_confcom/custom.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from azext_confcom.command.fragment_attach import fragment_attach as _fragment_attach
2727
from azext_confcom.command.fragment_push import fragment_push as _fragment_push
2828
from azext_confcom.command.containers_from_image import containers_from_image as _containers_from_image
29+
from azext_confcom.command.containers_from_vn2 import containers_from_vn2 as _containers_from_vn2
2930
from knack.log import get_logger
3031
from pkg_resources import parse_version
3132

@@ -563,3 +564,13 @@ def containers_from_image(
563564
image=image,
564565
platform=platform,
565566
)
567+
568+
569+
def containers_from_vn2(
570+
template: str,
571+
container_name: Optional[str] = None,
572+
) -> None:
573+
print(_containers_from_vn2(
574+
template=template,
575+
container_name=container_name,
576+
))

0 commit comments

Comments
 (0)