|
| 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) |
0 commit comments