Skip to content

Commit 3532d3b

Browse files
committed
[Containerapp]: az containerapp arc: Enable setup custom core dns for Openshift on Arc
1 parent 1734250 commit 3532d3b

4 files changed

Lines changed: 394 additions & 84 deletions

File tree

src/containerapp/azext_containerapp/_arc_utils.py

Lines changed: 291 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from azure.cli.core.azclierror import (ValidationError, ResourceNotFoundError, CLIError, InvalidArgumentValueError)
1717
from ._constants import (CUSTOM_CORE_DNS_VOLUME_NAME, CUSTOM_CORE_DNS_VOLUME_MOUNT_PATH,
18-
CUSTOM_CORE_DNS, CORE_DNS, KUBE_SYSTEM, EMPTY_CUSTOM_CORE_DNS)
18+
CUSTOM_CORE_DNS, CORE_DNS, KUBE_SYSTEM, EMPTY_CUSTOM_CORE_DNS, OPENSHIFT_DNS)
1919

2020
logger = get_logger(__name__)
2121

@@ -278,6 +278,21 @@ def update_deployment(resource_name, resource_namespace, kube_client, deployment
278278
apps_v1_api.patch_namespaced_deployment(name=resource_name, namespace=resource_namespace, body=deployment)
279279
except Exception as e:
280280
raise ValidationError(f"other errors while patching deployment coredns in kube-system {str(e)}")
281+
282+
def create_or_update_deployment(name, namespace, kube_client, deployment):
283+
validate_resource_name_and_resource_namespace_not_empty(name, namespace)
284+
285+
try:
286+
logger.info(f"Start to create deployment {name} in namespace {namespace}")
287+
apps_v1_api = client.AppsV1Api(kube_client)
288+
apps_v1_api.create_namespaced_deployment(namespace=namespace, body=deployment)
289+
except client.exceptions.ApiException as e:
290+
if e.status == 409:
291+
logger.warning(f"Deployment '{name}' already exists, replacing it")
292+
apps_v1_api.replace_namespaced_deployment(name=name,namespace=namespace, body=deployment)
293+
else:
294+
raise CLIError(f"Failed to create or replace Deployment'{name}': {str(e)}")
295+
281296

282297

283298
def replace_deployment(resource_name, resource_namespace, kube_client, deployment):
@@ -319,6 +334,20 @@ def update_configmap(resource_name, resource_namespace, kube_client, config_map)
319334

320335
except Exception as e:
321336
raise CLIError(f"other errors while patching config map coredns in kube-system {str(e)}")
337+
338+
def create_or_update_configmap(name, namespace, kube_client, configmap):
339+
validate_resource_name_and_resource_namespace_not_empty(name, namespace)
340+
341+
try:
342+
logger.info(f"Start to create configmap {name} in namespace {namespace}")
343+
core_v1_api = client.CoreV1Api(kube_client)
344+
core_v1_api.create_namespaced_config_map(namespace=namespace, body=configmap)
345+
except client.exceptions.ApiException as e:
346+
if e.status == 409:
347+
logger.warning(f"Configmap '{name}' already exists, replacing it")
348+
core_v1_api.replace_namespaced_config_map(name=name,namespace=namespace, body=configmap)
349+
else:
350+
raise CLIError(f"Failed to create or replace Deployment'{name}': {str(e)}")
322351

323352

324353
def replace_configmap(resource_name, resource_namespace, kube_client, config_map):
@@ -356,3 +385,264 @@ def validate_resource_name_and_resource_namespace_not_empty(resource_name, resou
356385
raise InvalidArgumentValueError("Arg resource_name should not be None or Empty")
357386
if resource_namespace is None or len(resource_namespace) == 0:
358387
raise InvalidArgumentValueError("Arg resource_namespace should not be None or Empty")
388+
389+
def get_dns_operator_config(kube_client, folder=None):
390+
try:
391+
logger.info("Fetching DNS operator configuration from OpenShift cluster")
392+
custom_objects_api = client.CustomObjectsApi(kube_client)
393+
dns_operator_config = custom_objects_api.get_cluster_custom_object(
394+
group="operator.openshift.io",
395+
version="v1",
396+
plural="dnses",
397+
name="default"
398+
)
399+
400+
# Save the DNS operator configuration to the folder if provided
401+
if folder is not None:
402+
filepath = os.path.join(folder, "dns-operator-config.json")
403+
try:
404+
logger.info(f"Saving DNS operator configuration to {filepath}")
405+
with open(filepath, "w") as f:
406+
f.write(json.dumps(dns_operator_config, indent=2))
407+
except Exception as e:
408+
raise ValidationError(f"Failed to save DNS operator configuration to {filepath}: {str(e)}")
409+
410+
return dns_operator_config
411+
except client.exceptions.ApiException as e:
412+
if e.status == 404:
413+
raise ResourceNotFoundError("DNS operator configuration not found in the OpenShift cluster.")
414+
else:
415+
raise CLIError(f"Failed to fetch DNS operator configuration: {str(e)}")
416+
except Exception as e:
417+
raise CLIError(f"An error occurred while fetching DNS operator configuration: {str(e)}")
418+
419+
def create_or_replace_cluster_role(rbac_api, role_name, role):
420+
try:
421+
logger.info(f"Creating new ClusterRole '{role_name}'")
422+
rbac_api.create_cluster_role(body=role)
423+
except client.exceptions.ApiException as e:
424+
if e.status == 409:
425+
logger.info(f"ClusterRole '{role_name}' already exists, replacing it")
426+
rbac_api.replace_cluster_role(name=role_name, body=role)
427+
else:
428+
raise CLIError(f"Failed to create or replace ClusterRole '{role_name}': {str(e)}")
429+
430+
def create_or_replace_cluster_rolebinding(rbac_api, rolebinding_name, rolebinding):
431+
try:
432+
logger.info(f"Creating new ClusterRolebinding '{rolebinding_name}'")
433+
rbac_api.create_cluster_role_binding(body=rolebinding)
434+
except client.exceptions.ApiException as e:
435+
if e.status == 409:
436+
logger.info(f"ClusterRole '{rolebinding_name}' already exists, replacing it")
437+
rbac_api.replace_cluster_role_binding(name=rolebinding_name, body=rolebinding)
438+
else:
439+
raise CLIError(f"Failed to create or replace ClusterRole '{rolebinding_name}': {str(e)}")
440+
441+
442+
443+
def create_openshift_custom_coredns_resources(kube_client, namespace=OPENSHIFT_DNS):
444+
try:
445+
logger.info("Creating custom CoreDNS resources in OpenShift")
446+
core_v1_api = client.CoreV1Api(kube_client)
447+
rbac_api = client.RbacAuthorizationV1Api(kube_client)
448+
apps_v1_api = client.AppsV1Api(kube_client)
449+
450+
# 1. Create ClusterRole
451+
cluster_role = client.V1ClusterRole(
452+
metadata=client.V1ObjectMeta(
453+
name=CUSTOM_CORE_DNS
454+
),
455+
rules=[
456+
client.V1PolicyRule(
457+
api_groups=[""],
458+
resources=["services", "endpoints", "pods", "namespaces"],
459+
verbs=["list", "watch"]
460+
),
461+
client.V1PolicyRule(
462+
api_groups=["discovery.k8s.io"],
463+
resources=["endpointslices"],
464+
verbs=["list", "watch"]
465+
)
466+
]
467+
)
468+
create_or_replace_cluster_role(rbac_api, CUSTOM_CORE_DNS, cluster_role)
469+
470+
# 2. Create ClusterRoleBinding
471+
cluster_role_binding = client.V1ClusterRoleBinding(
472+
metadata=client.V1ObjectMeta(
473+
name=CUSTOM_CORE_DNS
474+
),
475+
role_ref=client.V1RoleRef(
476+
api_group="rbac.authorization.k8s.io",
477+
kind="ClusterRole",
478+
name=CUSTOM_CORE_DNS
479+
),
480+
subjects=[
481+
client.V1Subject(
482+
kind="ServiceAccount",
483+
name="default",
484+
namespace=namespace
485+
)
486+
]
487+
)
488+
create_or_replace_cluster_rolebinding(rbac_api,CUSTOM_CORE_DNS, cluster_role_binding)
489+
490+
# 3. Create ConfigMap
491+
existing_config_map = core_v1_api.read_namespaced_config_map(name=CUSTOM_CORE_DNS, namespace=KUBE_SYSTEM)
492+
corefile_data = existing_config_map.data.get("k4apps-default.io.server") or existing_config_map.data.get("Corefile")
493+
if not corefile_data:
494+
raise ValidationError(F"Neither 'k4apps-default.io.server' nor 'Corefile' key found in the {CUSTOM_CORE_DNS} ConfigMap in {KUBE_SYSTEM} namespace.")
495+
496+
config_map = client.V1ConfigMap(
497+
metadata=client.V1ObjectMeta(
498+
name=CUSTOM_CORE_DNS,
499+
namespace=namespace
500+
),
501+
data={"Corefile": corefile_data}
502+
)
503+
504+
create_or_update_configmap(name=CUSTOM_CORE_DNS,namespace=namespace,kube_client=kube_client, configmap=config_map)
505+
logger.info("Custom CoreDNS ConfigMap created successfully")
506+
507+
# 4. Create Deployment
508+
deployment = client.V1Deployment(
509+
metadata=client.V1ObjectMeta(
510+
name=CUSTOM_CORE_DNS,
511+
namespace=namespace
512+
),
513+
spec=client.V1DeploymentSpec(
514+
replicas=1,
515+
selector=client.V1LabelSelector(
516+
match_labels={"app": CUSTOM_CORE_DNS}
517+
),
518+
template=client.V1PodTemplateSpec(
519+
metadata=client.V1ObjectMeta(
520+
labels={"app": CUSTOM_CORE_DNS}
521+
),
522+
spec=client.V1PodSpec(
523+
containers=[
524+
client.V1Container(
525+
name="coredns",
526+
image="coredns/coredns:latest",
527+
args=["-conf", "/etc/coredns/Corefile"],
528+
volume_mounts=[
529+
client.V1VolumeMount(
530+
name="config-volume",
531+
mount_path="/etc/coredns"
532+
)
533+
]
534+
)
535+
],
536+
volumes=[
537+
client.V1Volume(
538+
name="config-volume",
539+
config_map=client.V1ConfigMapVolumeSource(
540+
name=CUSTOM_CORE_DNS
541+
)
542+
)
543+
]
544+
)
545+
)
546+
)
547+
)
548+
create_or_update_deployment(name=CUSTOM_CORE_DNS,namespace=namespace,kube_client=kube_client, deployment=deployment)
549+
logger.info("Custom CoreDNS Deployment created successfully")
550+
551+
# 5 Create Service
552+
service = client.V1Service(
553+
metadata=client.V1ObjectMeta(
554+
name=CUSTOM_CORE_DNS,
555+
namespace=namespace
556+
),
557+
spec=client.V1ServiceSpec(
558+
selector={"app": CUSTOM_CORE_DNS},
559+
ports=[
560+
client.V1ServicePort(
561+
protocol="UDP",
562+
port=53,
563+
target_port=53
564+
)
565+
]
566+
)
567+
)
568+
core_v1_api.create_namespaced_service(namespace=namespace, body=service)
569+
logger.info("Custom CoreDNS Service created successfully")
570+
571+
except client.exceptions.ApiException as e:
572+
if e.status == 409:
573+
logger.warning("Custom CoreDNS resources already exist")
574+
else:
575+
raise CLIError(f"Failed to create custom CoreDNS resources: {str(e)}")
576+
except Exception as e:
577+
raise CLIError(f"An error occurred while creating custom CoreDNS resources: {str(e)}")
578+
579+
def patch_openshift_dns_operator(kube_client, domain):
580+
try:
581+
logger.info("Patching OpenShift DNS operator to add custom resolver")
582+
583+
# Fetch the existing DNS operator configuration
584+
custom_objects_api = client.CustomObjectsApi(kube_client)
585+
586+
dns_operator_config = custom_objects_api.get_cluster_custom_object(
587+
group="operator.openshift.io",
588+
version="v1",
589+
plural="dnses",
590+
name="default"
591+
)
592+
593+
# Add the custom resolver to the DNS operator configuration
594+
servers = dns_operator_config.get("spec", {}).get("servers", [])
595+
custom_resolver = {
596+
"name": CUSTOM_CORE_DNS,
597+
"zones": [domain],
598+
"forwardPlugin": {
599+
"upstreams": [f"{CUSTOM_CORE_DNS}.svc.cluster.local"]
600+
}
601+
}
602+
603+
# Check if the custom resolver already exists
604+
if not any(server.get("name") == CUSTOM_CORE_DNS for server in servers):
605+
servers.append(custom_resolver)
606+
dns_operator_config["spec"]["servers"] = servers
607+
608+
# Update the DNS operator configuration
609+
custom_objects_api.patch_cluster_custom_object(
610+
group="operator.openshift.io",
611+
version="v1",
612+
plural="dnses",
613+
name="default",
614+
body=dns_operator_config
615+
)
616+
logger.info("Successfully patched OpenShift DNS operator with custom resolver")
617+
else:
618+
logger.info("Custom resolver already exists in the DNS operator configuration")
619+
620+
except client.exceptions.ApiException as e:
621+
raise CLIError(f"Failed to patch DNS operator: {str(e)}")
622+
except Exception as e:
623+
raise CLIError(f"An error occurred while patching DNS operator: {str(e)}")
624+
625+
626+
def extract_domain_from_configmap(kube_client, resource_name=CUSTOM_CORE_DNS, namespace=KUBE_SYSTEM):
627+
import re
628+
629+
try:
630+
core_v1_api = client.CoreV1Api(kube_client)
631+
configmap = core_v1_api.read_namespaced_config_map(name=CUSTOM_CORE_DNS, namespace=KUBE_SYSTEM)
632+
if configmap is None:
633+
raise ResourceNotFoundError(f"ConfigMap '{resource_name}' not found in namespace '{namespace}'.")
634+
635+
corefile = configmap.data.get("k4apps-default.io.server")
636+
if not corefile:
637+
raise ValidationError("'k4apps-default.io.server' key found in the coredns-custom ConfigMap in kube-system namespace.")
638+
639+
# Extract the domain (excluding 'dapr')
640+
for line in corefile.splitlines():
641+
match = re.match(r'^\s*([a-zA-Z0-9\-\.]+):53\s*{', line)
642+
if match and match.group(1) != "dapr":
643+
return match.group(1)
644+
645+
raise ValidationError("No valid domain found in CoreDNS configmap data.")
646+
except Exception as e:
647+
logger.error(f"Failed to extract domain from configmap: {str(e)}")
648+
return None

src/containerapp/azext_containerapp/_constants.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,13 @@
142142
SUPPORTED_RUNTIME_LIST = [RUNTIME_GENERIC, RUNTIME_JAVA]
143143

144144
AKS_AZURE_LOCAL_DISTRO = "AksAzureLocal"
145-
SETUP_CORE_DNS_SUPPORTED_DISTRO = [AKS_AZURE_LOCAL_DISTRO]
145+
OPENSHIFT_DISTRO = "openshift"
146+
SETUP_CORE_DNS_SUPPORTED_DISTRO = [AKS_AZURE_LOCAL_DISTRO, OPENSHIFT_DISTRO]
146147
CUSTOM_CORE_DNS_VOLUME_NAME = 'custom-config-volume'
147148
CUSTOM_CORE_DNS_VOLUME_MOUNT_PATH = '/etc/coredns/custom'
148149
CUSTOM_CORE_DNS = 'coredns-custom'
149150
CORE_DNS = 'coredns'
151+
OPENSHIFT_DNS = 'openshift-dns'
150152
KUBE_SYSTEM = 'kube-system'
151153
EMPTY_CUSTOM_CORE_DNS = """
152154
apiVersion: v1

src/containerapp/azext_containerapp/_params.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
validate_custom_location_name_or_id, validate_env_name_or_id_for_up,
2121
validate_otlp_headers, validate_target_port_range, validate_timeout_in_seconds)
2222
from ._constants import (MAXIMUM_CONTAINER_APP_NAME_LENGTH, MAXIMUM_APP_RESILIENCY_NAME_LENGTH, MAXIMUM_COMPONENT_RESILIENCY_NAME_LENGTH,
23-
AKS_AZURE_LOCAL_DISTRO)
23+
AKS_AZURE_LOCAL_DISTRO, OPENSHIFT_DISTRO)
2424

2525

2626
def load_arguments(self, _):
@@ -378,7 +378,7 @@ def load_arguments(self, _):
378378
c.argument('yaml', type=file_type, help='Path to a .yaml file with the configuration of a Dapr component. All other parameters will be ignored. For an example, see https://learn.microsoft.com/en-us/azure/container-apps/dapr-overview?tabs=bicep1%2Cyaml#component-schema')
379379

380380
with self.argument_context('containerapp arc setup-core-dns') as c:
381-
c.argument('distro', arg_type=get_enum_type([AKS_AZURE_LOCAL_DISTRO]), required=True, help="The distro supported to setup CoreDNS.")
381+
c.argument('distro', arg_type=get_enum_type([AKS_AZURE_LOCAL_DISTRO, OPENSHIFT_DISTRO]), required=True, help="The distro1 supported to setup CoreDNS.")
382382
c.argument('kube_config', help="Path to the kube config file.")
383383
c.argument('kube_context', help="Kube context from current machine.")
384384
c.argument('skip_ssl_verification', help="Skip SSL verification for any cluster connection.")

0 commit comments

Comments
 (0)