Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/containerapp/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ upcoming
* 'az containerapp env http-route-config': Add commands for the http-route-config feature area.
* 'az containerapp env java-component': Support more flexible configuration updates with new parameters `--set-configurations`, `--replace-configurations`, `--remove-configurations` and `--remove-all-configurations`.
* 'az containerapp env java-component gateway-for-spring create/update': Support `--bind` and `--unbind`
* 'az containerapp arc': Enable setup custom core dns for Aks AzureCore on Arc.

1.1.0b1
++++++
Expand Down
358 changes: 358 additions & 0 deletions src/containerapp/azext_containerapp/_arc_utils.py

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions src/containerapp/azext_containerapp/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,23 @@
RUNTIME_GENERIC = "generic"
RUNTIME_JAVA = "java"
SUPPORTED_RUNTIME_LIST = [RUNTIME_GENERIC, RUNTIME_JAVA]

AKS_AZURE_LOCAL_DISTRO = "AksAzureLocal"
SETUP_CORE_DNS_SUPPORTED_DISTRO = [AKS_AZURE_LOCAL_DISTRO]
CUSTOM_CORE_DNS_VOLUME_NAME = 'custom-config-volume'
CUSTOM_CORE_DNS_VOLUME_MOUNT_PATH = '/etc/coredns/custom'
CUSTOM_CORE_DNS = 'coredns-custom'
CORE_DNS = 'coredns'
KUBE_SYSTEM = 'kube-system'
EMPTY_CUSTOM_CORE_DNS = """
apiVersion: v1
data:
kind: ConfigMap
metadata:
labels:
addonmanager.kubernetes.io/mode: EnsureExists
k8s-app: kube-dns
kubernetes.io/cluster-service: "true"
name: coredns-custom
namespace: kube-system
"""
17 changes: 17 additions & 0 deletions src/containerapp/azext_containerapp/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,23 @@
az containerapp connected-env list -g MyResourceGroup
"""

helps['containerapp arc'] = """
type: group
short-summary: Install prerequisites for Kubernetes cluster on Arc
"""

helps['containerapp arc setup-core-dns'] = """
type: command
short-summary: Setup CoreDNS for Kubernetes cluster on Arc
examples:
- name: Setup CoreDNS for Aks on Azure Local on Arc
text: |
az containerapp arc setup-core-dns --distro AksAzureLocal
- name: Setup CoreDNS for Aks on Azure Local on Arc by specifying the kubeconfig and kubecontext.
text: |
az containerapp arc setup-core-dns --distro AksAzureLocal --kube-config /path/to/kubeconfig --kube-context kubeContextName
"""

helps['containerapp connected-env dapr-component'] = """
type: group
short-summary: Commands to manage Dapr components for Container Apps connected environments.
Expand Down
9 changes: 8 additions & 1 deletion src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
from ._validators import (validate_env_name_or_id, validate_build_env_vars,
validate_custom_location_name_or_id, validate_env_name_or_id_for_up,
validate_otlp_headers, validate_target_port_range, validate_timeout_in_seconds)
from ._constants import MAXIMUM_CONTAINER_APP_NAME_LENGTH, MAXIMUM_APP_RESILIENCY_NAME_LENGTH, MAXIMUM_COMPONENT_RESILIENCY_NAME_LENGTH
from ._constants import (MAXIMUM_CONTAINER_APP_NAME_LENGTH, MAXIMUM_APP_RESILIENCY_NAME_LENGTH, MAXIMUM_COMPONENT_RESILIENCY_NAME_LENGTH,
AKS_AZURE_LOCAL_DISTRO)


def load_arguments(self, _):
Expand Down Expand Up @@ -365,6 +366,12 @@ def load_arguments(self, _):
c.argument('environment_name', options_list=['--name', '-n'], help="The environment name.")
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')

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

with self.argument_context('containerapp github-action add') as c:
c.argument('build_env_vars', nargs='*', help="A list of environment variable(s) for the build. Space-separated values in 'key=value' format.",
validator=validate_build_env_vars, is_preview=True)
Expand Down
1 change: 1 addition & 0 deletions src/containerapp/azext_containerapp/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import hashlib
import re
import requests
import shutil
import packaging.version as SemVer

from enum import Enum
Expand Down
3 changes: 3 additions & 0 deletions src/containerapp/azext_containerapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ def load_command_table(self, args):
g.custom_command('set', 'connected_env_create_or_update_storage', supports_no_wait=True, exception_handler=ex_handler_factory())
g.custom_command('remove', 'connected_env_remove_storage', supports_no_wait=True, confirmation=True, exception_handler=ex_handler_factory())

with self.command_group('containerapp arc', is_preview=True) as g:
g.custom_command('setup-core-dns', 'setup_core_dns', confirmation=True, exception_handler=ex_handler_factory())

with self.command_group('containerapp env java-component') as g:
g.custom_command('list', 'list_java_components')

Expand Down
111 changes: 109 additions & 2 deletions src/containerapp/azext_containerapp/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from urllib.parse import urlparse
import json
import requests
import copy
import subprocess
from concurrent.futures import ThreadPoolExecutor

Expand Down Expand Up @@ -127,7 +128,13 @@

from ._ssh_utils import (SSH_DEFAULT_ENCODING, DebugWebSocketConnection, read_debug_ssh)

from ._utils import connected_env_check_cert_name_availability, get_oryx_run_image_tags, patchable_check, get_pack_exec_path, is_docker_running, parse_build_env_vars, env_has_managed_identity
from ._utils import (connected_env_check_cert_name_availability, get_oryx_run_image_tags, patchable_check,
get_pack_exec_path, is_docker_running, parse_build_env_vars, env_has_managed_identity)

from ._arc_utils import (get_core_dns_deployment, get_core_dns_configmap, backup_custom_core_dns_configmap,
replace_configmap, replace_deployment, delete_configmap, patch_coredns,
create_folder, create_sub_folder,
check_kube_connection, create_kube_client)

from ._constants import (CONTAINER_APPS_RP,
NAME_INVALID, NAME_ALREADY_EXISTS, ACR_IMAGE_SUFFIX, DEV_POSTGRES_IMAGE, DEV_POSTGRES_SERVICE_TYPE,
Expand All @@ -136,7 +143,8 @@
DEV_QDRANT_CONTAINER_NAME, DEV_QDRANT_SERVICE_TYPE, DEV_WEAVIATE_IMAGE, DEV_WEAVIATE_CONTAINER_NAME, DEV_WEAVIATE_SERVICE_TYPE,
DEV_MILVUS_IMAGE, DEV_MILVUS_CONTAINER_NAME, DEV_MILVUS_SERVICE_TYPE, DEV_SERVICE_LIST, CONTAINER_APPS_SDK_MODELS, BLOB_STORAGE_TOKEN_STORE_SECRET_SETTING_NAME,
DAPR_SUPPORTED_STATESTORE_DEV_SERVICE_LIST, DAPR_SUPPORTED_PUBSUB_DEV_SERVICE_LIST,
JAVA_COMPONENT_CONFIG, JAVA_COMPONENT_EUREKA, JAVA_COMPONENT_ADMIN, JAVA_COMPONENT_NACOS, JAVA_COMPONENT_GATEWAY, DOTNET_COMPONENT_RESOURCE_TYPE)
JAVA_COMPONENT_CONFIG, JAVA_COMPONENT_EUREKA, JAVA_COMPONENT_ADMIN, JAVA_COMPONENT_NACOS, JAVA_COMPONENT_GATEWAY, DOTNET_COMPONENT_RESOURCE_TYPE,
CUSTOM_CORE_DNS, CORE_DNS, KUBE_SYSTEM)


logger = get_logger(__name__)
Expand Down Expand Up @@ -2064,6 +2072,105 @@ def connected_env_remove_storage(cmd, storage_name, name, resource_group_name, n
handle_raw_exception(e)


def setup_core_dns(cmd, distro=None, kube_config=None, kube_context=None, skip_ssl_verification=False):
Comment thread
Greedygre marked this conversation as resolved.
# Checking the connection to kubernetes cluster.
check_kube_connection(kube_config, kube_context, skip_ssl_verification)

# create a local path to store the original and the changed deployment and core dns configmap.
time_stamp = time.strftime("%Y-%m-%d-%H.%M.%S")

parent_folder, folder_status, error = create_folder("setup-core-dns", time_stamp)
if not folder_status:
raise ValidationError(error)

original_folder, folder_status, error = create_sub_folder(parent_folder, "original")
if not folder_status:
raise ValidationError(error)

kube_client = create_kube_client(kube_config, kube_context, skip_ssl_verification)

# backup original deployment and configmap
logger.info("Backup existing coredns deployment and configmap")
original_coredns_deployment = get_core_dns_deployment(kube_client, original_folder)
coredns_deployment = copy.deepcopy(original_coredns_deployment)

original_coredns_configmap = get_core_dns_configmap(kube_client, original_folder)
coredns_configmap = copy.deepcopy(original_coredns_configmap)

volumes = coredns_deployment.spec.template.spec.volumes
if volumes is None:
raise ValidationError('Unexpected Volumes in coredns deployment, Volumes not found')

volume_mounts = coredns_deployment.spec.template.spec.containers[0].volume_mounts
if volume_mounts is None:
raise ValidationError('Unexpected Volume mounts in coredns deployment, VolumeMounts not found')

coredns_configmap_volume_set = False
custom_coredns_configmap_volume_set = False
custom_coredns_configmap_volume_mounted = False

for volume in volumes:
if volume.config_map is not None:
if volume.config_map.name == CORE_DNS:
for mount in volume_mounts:
if mount.name is not None and mount.name == volume.name:
coredns_configmap_volume_set = True
break
elif volume.config_map.name == CUSTOM_CORE_DNS:
custom_coredns_configmap_volume_set = True
for mount in volume_mounts:
if mount.name is not None and mount.name == volume.name:
custom_coredns_configmap_volume_mounted = True
break

if not coredns_configmap_volume_set:
raise ValidationError("Cannot find volume and volume mounts for core dns config map")

original_custom_core_dns_configmap = backup_custom_core_dns_configmap(kube_client, original_folder)

new_filepath_with_timestamp, folder_status, error = create_sub_folder(parent_folder, "new")
if not folder_status:
raise ValidationError(error)

try:
patch_coredns(kube_client, coredns_configmap, coredns_deployment, new_filepath_with_timestamp,
original_custom_core_dns_configmap is not None, not custom_coredns_configmap_volume_set, not custom_coredns_configmap_volume_mounted)
except Exception as e:
logger.error(f"Failed to setup custom coredns. {e}")
logger.info("Start to reverted coredns")
replace_succeeded = False
retry_count = 0
while not replace_succeeded and retry_count < 10:
logger.info(f"Retry the revert operation with retry count {retry_count}")

try:
logger.info("Start to reverted coredns configmap")
latest_core_dns_configmap = get_core_dns_configmap(kube_client)
latest_core_dns_configmap.data = original_coredns_configmap.data

replace_configmap(CORE_DNS, KUBE_SYSTEM, kube_client, latest_core_dns_configmap)
logger.info("Reverted coredns configmap successfully")

logger.info("Start to reverted coredns deployment")
latest_core_dns_deployment = get_core_dns_deployment(kube_client)
latest_core_dns_deployment.spec.template.spec = original_coredns_deployment.spec.template.spec

replace_deployment(CORE_DNS, KUBE_SYSTEM, kube_client, latest_core_dns_deployment)
logger.info("Reverted coredns deployment successfully")

if original_custom_core_dns_configmap is None:
delete_configmap(CUSTOM_CORE_DNS, KUBE_SYSTEM, kube_client)
replace_succeeded = True
except Exception as revertEx:
logger.warning(f"Failed to revert coredns configmap or deployment {revertEx}")
retry_count = retry_count + 1
time.sleep(2)

if not replace_succeeded:
logger.error(f"Failed to revert the deployment and configuration. "
f"You can get the original coredns config and deployment from {original_folder}")


def init_dapr_components(cmd, resource_group_name, environment_name, statestore="redis", pubsub="redis"):
_validate_subscription_registered(cmd, CONTAINER_APPS_RP)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@


# pylint: disable=too-many-instance-attributes

class ConnectedClusterPreparer(NoTrafficRecordingPreparer, SingleValueReplacer):
def __init__(self, name_prefix='aks', location='eastus2euap', aks_name='my-aks-cluster', connected_cluster_name='my-connected-cluster',
resource_group_parameter_name='resource_group', skip_delete=False):
resource_group_parameter_name='resource_group', skip_delete=False, skip_connected_cluster=False):
super(ConnectedClusterPreparer, self).__init__(name_prefix, 15)
self.cli_ctx = get_dummy_cli()
self.location = location
self.infra_cluster = aks_name
self.connected_cluster_name = connected_cluster_name
self.resource_group_parameter_name = resource_group_parameter_name
self.skip_delete = skip_delete
self.skip_connected_cluster = skip_connected_cluster

def create_resource(self, name, **kwargs):
group = self._get_resource_group(**kwargs)
Expand All @@ -37,11 +39,12 @@ def create_resource(self, name, **kwargs):
self.live_only_execute(self.cli_ctx, f'az aks create --resource-group {group} --name {self.infra_cluster} --enable-aad --generate-ssh-keys --enable-cluster-autoscaler --min-count 4 --max-count 10 --node-count 4 --location {aks_location}')
self.live_only_execute(self.cli_ctx, f'az aks get-credentials --resource-group {group} --name {self.infra_cluster} --overwrite-existing --admin')

self.live_only_execute(self.cli_ctx, f'az connectedk8s connect --resource-group {group} --name {self.connected_cluster_name} --location {arc_location}')
connected_cluster = self.live_only_execute(self.cli_ctx, f'az connectedk8s show --resource-group {group} --name {self.connected_cluster_name}').get_output_in_json()
while connected_cluster is not None and connected_cluster["connectivityStatus"] == "Connecting":
time.sleep(5)
if not self.skip_connected_cluster:
self.live_only_execute(self.cli_ctx, f'az connectedk8s connect --resource-group {group} --name {self.connected_cluster_name} --location {arc_location}')
connected_cluster = self.live_only_execute(self.cli_ctx, f'az connectedk8s show --resource-group {group} --name {self.connected_cluster_name}').get_output_in_json()
while connected_cluster is not None and connected_cluster["connectivityStatus"] == "Connecting":
time.sleep(5)
connected_cluster = self.live_only_execute(self.cli_ctx, f'az connectedk8s show --resource-group {group} --name {self.connected_cluster_name}').get_output_in_json()

except AttributeError: # live only execute returns None if playing from record
pass
Expand Down
Loading