Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abhilash/ArcGateway-Feature #7753

Draft
wants to merge 19 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions src/connectedk8s/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
Release History
===============

1.8.0
++++++
* Add support for api version 07-01-2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless there was a real requirement to keep these as a single change, it would have been nice to see these are two PRs so that the specific changes for the two were clearly communicated.

* Add support for Arc Gateway feature

1.7.3
++++++
* Skip helm archive download if helm is already installed.
Expand Down
13 changes: 13 additions & 0 deletions src/connectedk8s/azext_connectedk8s/_client_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,19 @@ def cf_connectedk8s_prev_2023_11_01(cli_ctx, *_):
def cf_connected_cluster_prev_2023_11_01(cli_ctx, _):
return cf_connectedk8s_prev_2023_11_01(cli_ctx).connected_cluster

def cf_connectedk8s_prev_2024_07_01(cli_ctx, *_):
from azext_connectedk8s.vendored_sdks.preview_2024_07_01 import ConnectedKubernetesClient
if os.getenv(consts.Azure_Access_Token_Variable):
validate_custom_token()
credential = AccessTokenCredential(access_token=os.getenv(consts.Azure_Access_Token_Variable))
return get_mgmt_service_client(cli_ctx, ConnectedKubernetesClient,
subscription_id=os.getenv('AZURE_SUBSCRIPTION_ID'),
credential=credential)
return get_mgmt_service_client(cli_ctx, ConnectedKubernetesClient)


def cf_connected_cluster_prev_2024_07_01(cli_ctx, _):
return cf_connectedk8s_prev_2024_07_01(cli_ctx).connected_cluster

def cf_connectedmachine(cli_ctx, subscription_id):
from azure.mgmt.hybridcompute import HybridComputeManagementClient
Expand Down
2 changes: 2 additions & 0 deletions src/connectedk8s/azext_connectedk8s/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
Helm_Installation_Fault_Type = 'helm-not-installed-error'
Check_HelmInstallation_Fault_Type = 'check-helm-installed-error'
Get_HelmRegistery_Path_Fault_Type = 'helm-registry-path-fetch-error'
DP_Health_Check = 'dp-health-check-error'
Pull_HelmChart_Fault_Type = 'helm-chart-pull-error'
Export_HelmChart_Fault_Type = 'helm-chart-export-error'
Get_Kubernetes_Distro_Fault_Type = 'kubernetes-get-distribution-error'
Expand Down Expand Up @@ -100,6 +101,7 @@
Kubeconfig_Failed_To_Load_Fault_Type = "failed-to-load-kubeconfig-file"
Proxy_Cert_Path_Does_Not_Exist_Fault_Type = 'proxy-cert-path-does-not-exist-error'
Proxy_Cert_Path_Does_Not_Exist_Error = 'Proxy cert path {} does not exist. Please check the path provided'
Gateway_ArmId_Is_Invalid = "The provided Gateway ArmID in --gateway-resource-id {} is invalid. Please provide a valid Gateway ArmID"
Get_Kubernetes_Infra_Fault_Type = 'kubernetes-get-infrastructure-error'
No_Param_Error = 'No parmeters were specified with update command. Please run az connectedk8s update --help to check parameters available for update'
EnableProxy_Conflict_Error = 'Conflict detected: --disable-proxy can not be set with --https-proxy, --http-proxy, --proxy-skip-range and --proxy-cert at the same time. Please run az connectedk8s update --help for more information about the parameters'
Expand Down
11 changes: 11 additions & 0 deletions src/connectedk8s/azext_connectedk8s/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
from knack.arguments import (CLIArgumentType, CaseInsensitiveList)

from ._validators import validate_private_link_properties
from .action import (
AddConfigurationSettings,
AddConfigurationProtectedSettings,
)

features_types = CLIArgumentType(
nargs='+',
Expand Down Expand Up @@ -51,6 +55,10 @@ def load_arguments(self, _):
c.argument('container_log_path', help='Override the default container log path to enable fluent-bit logging')
c.argument('skip_ssl_verification', action='store_true', help='Skip SSL verification for any cluster connection.')
c.argument('yes', options_list=['--yes', '-y'], help='Do not prompt for confirmation.', action='store_true')
c.argument('enable_gateway', options_list=['--enable-gateway'], help='Pass this value to enable Arc Gateway.')
c.argument('gateway_resource_id', options_list=['--gateway-resource-id'], help='ArmID of the Arc Gateway resource.')
c.argument('configuration_settings', options_list=['--configuration-settings', '--config'], action=AddConfigurationSettings, nargs='+', help='Configuration Settings as key=value pair. Repeat parameter for each setting. Do not use this for secrets, as this value is returned in response.')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would feature_settings and protected_feature_settings be a better name given these relate to features?

c.argument('configuration_protected_settings', options_list=['--config-protected-settings', '--config-protected'], action=AddConfigurationProtectedSettings, nargs='+', help='Configuration Protected Settings as key=value pair. Repeat parameter for each setting. Only the key is returned in response, the value is not.')

with self.argument_context('connectedk8s update') as c:
c.argument('tags', tags_type)
Expand All @@ -69,6 +77,9 @@ def load_arguments(self, _):
c.argument('container_log_path', help='Override the default container log path to enable fluent-bit logging')
c.argument('skip_ssl_verification', action='store_true', help='Skip SSL verification for any cluster connection.')
c.argument('yes', options_list=['--yes', '-y'], help='Do not prompt for confirmation.', action='store_true')
c.argument('disable_gateway', options_list=['--disable_gateway'], help='pass this value to disable Arc Gateway')
c.argument('enable_gateway', options_list=['--enable-gateway'], help='Pass this value to enable Arc Gateway.')
c.argument('gateway_resource_id', options_list=['--gateway-resource-id'], help='ArmID of the Arc Gateway resource.')

with self.argument_context('connectedk8s upgrade') as c:
c.argument('cluster_name', options_list=['--name', '-n'], id_part='name', help='The name of the connected cluster.')
Expand Down
93 changes: 57 additions & 36 deletions src/connectedk8s/azext_connectedk8s/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,11 +375,10 @@ def add_helm_repo(kube_config, kube_context, helm_client_location):
summary='Failed to add helm repository')
raise CLIInternalError("Unable to add repository {} to helm: ".format(repo_url) + error_helm_repo.decode("ascii"))


def get_helm_registry(cmd, config_dp_endpoint, release_train_custom=None):
def get_helm_values(cmd, config_dp_endpoint, release_train_custom=None, request_body=None):
# Setting uri
api_version = "2019-11-01-preview"
chart_location_url_segment = "azure-arc-k8sagents/GetLatestHelmPackagePath?api-version={}".format(api_version)
api_version = "2024-07-01-preview"
chart_location_url_segment = "azure-arc-k8sagents/GetHelmSettings?api-version={}".format(api_version)
release_train = os.getenv('RELEASETRAIN') if os.getenv('RELEASETRAIN') else 'stable'
chart_location_url = "{}/{}".format(config_dp_endpoint, chart_location_url_segment)
if release_train_custom:
Expand All @@ -390,32 +389,51 @@ def get_helm_registry(cmd, config_dp_endpoint, release_train_custom=None):
if os.getenv('AZURE_ACCESS_TOKEN'):
headers = ["Authorization=Bearer {}".format(os.getenv('AZURE_ACCESS_TOKEN'))]
# Sending request with retries
r = send_request_with_retries(cmd.cli_ctx, 'post', chart_location_url, headers=headers, fault_type=consts.Get_HelmRegistery_Path_Fault_Type, summary='Error while fetching helm chart registry path', uri_parameters=uri_parameters, resource=resource)
r = send_request_with_retries(cmd.cli_ctx, 'post', chart_location_url, headers=headers, fault_type=consts.Get_HelmRegistery_Path_Fault_Type, summary='Error while fetching helm chart registry path', uri_parameters=uri_parameters, resource=resource, request_body=request_body)
if r.content:
try:
return r.json().get('repositoryPath')
return r.json()
except Exception as e:
telemetry.set_exception(exception=e, fault_type=consts.Get_HelmRegistery_Path_Fault_Type,
summary='Error while fetching helm chart registry path')
raise CLIInternalError("Error while fetching helm chart registry path from JSON response: " + str(e))
summary='Error while fetching helm values from DP')
raise CLIInternalError("Error while fetching helm values from DP from JSON response: " + str(e))
else:
telemetry.set_exception(exception='No content in response', fault_type=consts.Get_HelmRegistery_Path_Fault_Type,
summary='No content in acr path response')
raise CLIInternalError("No content was found in helm registry path response.")

def health_check_dp(cmd, config_dp_endpoint):
# Setting uri
api_version = "2024-07-01-preview"
chart_location_url_segment = "azure-arc-k8sagents/healthCheck?api-version={}".format(api_version)
chart_location_url = "{}/{}".format(config_dp_endpoint, chart_location_url_segment)
uri_parameters = []
resource = cmd.cli_ctx.cloud.endpoints.active_directory_resource_id
headers = None
if os.getenv('AZURE_ACCESS_TOKEN'):
headers = ["Authorization=Bearer {}".format(os.getenv('AZURE_ACCESS_TOKEN'))]
# Sending request with retries
r = send_request_with_retries(cmd.cli_ctx, 'post', chart_location_url, headers=headers, fault_type=consts.Get_HelmRegistery_Path_Fault_Type, summary='Error while performing DP health check', uri_parameters=uri_parameters, resource=resource)
if r.status_code == 200:
print("Health check for DP is successful.")
return True
else:
telemetry.set_exception(exception="Error while performing DP health check", fault_type=consts.DP_Health_Check,
summary='Error while performing DP health check')
raise CLIInternalError("Error while performing DP health check")


def send_request_with_retries(cli_ctx, method, url, headers, fault_type, summary, uri_parameters=None, resource=None, retry_count=5, retry_delay=3):
def send_request_with_retries(cli_ctx, method, url, headers, fault_type, summary, uri_parameters=None, resource=None, retry_count=5, retry_delay=3, request_body=None):
for i in range(retry_count):
try:
response = send_raw_request(cli_ctx, method, url, headers=headers, uri_parameters=uri_parameters, resource=resource)
response = send_raw_request(cli_ctx, method, url, headers=headers, uri_parameters=uri_parameters, resource=resource, body=request_body)
return response
except Exception as e:
if i == retry_count - 1:
telemetry.set_exception(exception=e, fault_type=fault_type, summary=summary)
raise CLIInternalError("Error while fetching helm chart registry path: " + str(e))
time.sleep(retry_delay)


def arm_exception_handler(ex, fault_type, summary, return_if_not_found=False):
if isinstance(ex, AuthenticationError):
telemetry.set_exception(exception=ex, fault_type=fault_type, summary=summary)
Expand Down Expand Up @@ -565,20 +583,13 @@ def cleanup_release_install_namespace_if_exists():


# DO NOT use this method for re-put scenarios. This method involves new NS creation for helm release. For re-put scenarios, brownfield scenario needs to be handled where helm release still stays in default NS
def helm_install_release(resource_manager, chart_path, subscription_id, kubernetes_distro, kubernetes_infra, resource_group_name,
cluster_name, location, onboarding_tenant_id, http_proxy, https_proxy, no_proxy, proxy_cert, private_key_pem,
kube_config, kube_context, no_wait, values_file, cloud_name, disable_auto_upgrade, enable_custom_locations,
custom_locations_oid, helm_client_location, enable_private_link, arm_metadata, onboarding_timeout="600",
container_log_path=None):
def helm_install_release(resource_manager, chart_path, kubernetes_distro, kubernetes_infra, location, private_key_pem,
kube_config, kube_context, no_wait, values_file, cloud_name, enable_custom_locations, custom_locations_oid,
helm_client_location, enable_private_link, arm_metadata, onboarding_timeout="600", helm_content_values=None):

cmd_helm_install = [helm_client_location, "upgrade", "--install", "azure-arc", chart_path,
"--set", "global.subscriptionId={}".format(subscription_id),
"--set", "global.kubernetesDistro={}".format(kubernetes_distro),
"--set", "global.kubernetesInfra={}".format(kubernetes_infra),
"--set", "global.resourceGroupName={}".format(resource_group_name),
"--set", "global.resourceName={}".format(cluster_name),
"--set", "global.location={}".format(location),
"--set", "global.tenantId={}".format(onboarding_tenant_id),
"--set", "global.onboardingPrivateKey={}".format(private_key_pem),
"--set", "systemDefaultValues.spnOnboarding=false",
"--set", "global.azureEnvironment={}".format(cloud_name),
Expand Down Expand Up @@ -611,6 +622,10 @@ def helm_install_release(resource_manager, chart_path, subscription_id, kubernet
else:
logger.debug("'arcConfigEndpoint' doesn't exist under 'dataplaneEndpoints' in the ARM metadata.")

# Add helmValues content response from DP
for helm_param, helm_value in helm_content_values.items():
cmd_helm_install.extend(["--set", "{}={}".format(helm_param, helm_value)])

# Add custom-locations related params
if enable_custom_locations and not enable_private_link:
cmd_helm_install.extend(["--set", "systemDefaultValues.customLocations.enabled=true"])
Expand All @@ -621,21 +636,6 @@ def helm_install_release(resource_manager, chart_path, subscription_id, kubernet
# To set some other helm parameters through file
if values_file:
cmd_helm_install.extend(["-f", values_file])
if disable_auto_upgrade:
cmd_helm_install.extend(["--set", "systemDefaultValues.azureArcAgents.autoUpdate={}".format("false")])
if https_proxy:
cmd_helm_install.extend(["--set", "global.httpsProxy={}".format(https_proxy)])
if http_proxy:
cmd_helm_install.extend(["--set", "global.httpProxy={}".format(http_proxy)])
if no_proxy:
cmd_helm_install.extend(["--set", "global.noProxy={}".format(no_proxy)])
if proxy_cert:
cmd_helm_install.extend(["--set-file", "global.proxyCert={}".format(proxy_cert)])
cmd_helm_install.extend(["--set", "global.isCustomCert={}".format(True)])
if https_proxy or http_proxy or no_proxy:
cmd_helm_install.extend(["--set", "global.isProxyEnabled={}".format(True)])
if container_log_path is not None:
cmd_helm_install.extend(["--set", "systemDefaultValues.fluent-bit.containerLogPath={}".format(container_log_path)])
if kube_config:
cmd_helm_install.extend(["--kubeconfig", kube_config])
if kube_context:
Expand All @@ -655,6 +655,7 @@ def helm_install_release(resource_manager, chart_path, subscription_id, kubernet
logger.warning("Please check if the azure-arc namespace was deployed and run 'kubectl get pods -n azure-arc' to check if all the pods are in running state. A possible cause for pods stuck in pending state could be insufficient resources on the kubernetes cluster to onboard to arc.")
raise CLIInternalError("Unable to install helm release: " + error_helm_install.decode("ascii"))

# TODO: implement a new helm_release command where you just consume the dp_helm_values and run a for loop and end up creating the helm values command.

def get_release_namespace(kube_config, kube_context, helm_client_location, release_name='azure-arc'):
cmd_helm_release = [helm_client_location, "list", "-a", "--all-namespaces", "--output", "json"]
Expand Down Expand Up @@ -838,3 +839,23 @@ def get_metadata(arm_endpoint, api_version="2022-09-01"):
print(msg, file=sys.stderr)
print(f"Please ensure you have network connection. Error: {str(err)}", file=sys.stderr)
arm_exception_handler(err, msg)

def add_config_protected_settings(https_proxy, http_proxy, no_proxy, proxy_cert, container_log_path, configuration_settings, configuration_protected_settings):
protected_helm_values = {}
if container_log_path:
configuration_settings.setdefault("logging", {"container_log_path": container_log_path})
if any([https_proxy, http_proxy, no_proxy, proxy_cert]):
configuration_protected_settings.setdefault("proxy", {})
if https_proxy:
configuration_protected_settings["proxy"]["https_proxy"] = https_proxy
protected_helm_values["global.httpsProxy"] = https_proxy
if http_proxy:
configuration_protected_settings["proxy"]["http_proxy"] = http_proxy
protected_helm_values["global.httpProxy"] = http_proxy
if no_proxy:
configuration_protected_settings["proxy"]["no_proxy"] = no_proxy
protected_helm_values["global.noProxy"] = no_proxy
if proxy_cert:
configuration_protected_settings["proxy"]["proxy_cert"] = proxy_cert
protected_helm_values["global.proxyCert"] = proxy_cert
return configuration_settings, configuration_protected_settings, protected_helm_values
50 changes: 50 additions & 0 deletions src/connectedk8s/azext_connectedk8s/action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import argparse
from azure.cli.core.azclierror import ArgumentUsageError

# pylint: disable=protected-access, too-few-public-methods
class AddConfigurationSettings(argparse._AppendAction):
def __call__(self, parser, namespace, values, option_string=None):
config_settings = getattr(namespace, self.dest, None)
if config_settings is None:
config_settings = {}
for item in values:
try:
key, value = item.split('=', 1)
feature, setting = key.split(".")
# Check if the feature is already in the dictionary
if feature not in config_settings:
# If not, add it with an empty dictionary as value
config_settings[feature] = {}
# Update the setting in the feature's dictionary
config_settings[feature][setting] = value
except ValueError as ex:
raise ArgumentUsageError('Usage error: {} configuration_setting_key=configuration_setting_value'.
format(option_string)) from ex
setattr(namespace, self.dest, config_settings)

# pylint: disable=protected-access, too-few-public-methods
class AddConfigurationProtectedSettings(argparse._AppendAction):

def __call__(self, parser, namespace, values, option_string=None):
prot_settings = getattr(namespace, self.dest, None)
if prot_settings is None:
prot_settings = {}
for item in values:
try:
key, value = item.split('=', 1)
feature, setting = key.split(".")
# Check if the feature is already in the dictionary
if feature not in prot_settings:
# If not, add it with an empty dictionary as value
prot_settings[feature] = {}
# Add the setting to the feature's dictionary
prot_settings[feature][setting] = value
except ValueError as ex:
raise ArgumentUsageError('Usage error: {} configuration_protected_setting_key='
'configuration_protected_setting_value'.format(option_string)) from ex
setattr(namespace, self.dest, prot_settings)
Loading
Loading