Skip to content
This repository was archived by the owner on May 13, 2025. It is now read-only.
Merged
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
4 changes: 4 additions & 0 deletions src/k8s-configuration/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
Release History
===============

1.0.1
++++++++++++++++++
* Add provider registration check

1.0.0
++++++++++++++++++
* Support api-version 2021-03-01
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

from azure.cli.core.commands.client_factory import get_mgmt_service_client

def cf_k8s_configuration(cli_ctx, *_):

from azure.cli.core.commands.client_factory import get_mgmt_service_client
def cf_k8s_configuration(cli_ctx, *_):
from azext_k8s_configuration.vendored_sdks import SourceControlConfigurationClient
return get_mgmt_service_client(cli_ctx, SourceControlConfigurationClient)


def cf_k8s_configuration_operation(cli_ctx, _):
return cf_k8s_configuration(cli_ctx).source_control_configurations


def _resource_providers_client(cli_ctx):
from azure.mgmt.resource import ResourceManagementClient
return get_mgmt_service_client(cli_ctx, ResourceManagementClient).providers
7 changes: 7 additions & 0 deletions src/k8s-configuration/azext_k8s_configuration/_consts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

PROVIDER_NAMESPACE = 'Microsoft.KubernetesConfiguration'
REGISTERED = "Registered"
8 changes: 4 additions & 4 deletions src/k8s-configuration/azext_k8s_configuration/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
)

from azure.cli.core.commands.validators import get_default_location_from_resource_group
from ._validators import validate_configuration_type, validate_operator_namespace, validate_operator_instance_name
from ._validators import _validate_configuration_type, _validate_operator_namespace, _validate_operator_instance_name


def load_arguments(self, _):
Expand All @@ -38,7 +38,7 @@ def load_arguments(self, _):
arg_type=get_enum_type(['namespace', 'cluster']),
help='''Specify scope of the operator to be 'namespace' or 'cluster' ''')
c.argument('configuration_type',
validator=validate_configuration_type,
validator=_validate_configuration_type,
arg_type=get_enum_type(['sourceControlConfiguration']),
help='Type of the configuration')
c.argument('enable_helm_operator',
Expand All @@ -60,11 +60,11 @@ def load_arguments(self, _):
c.argument('operator_instance_name',
arg_group="Operator",
help='Instance name of the Operator',
validator=validate_operator_instance_name)
validator=_validate_operator_instance_name)
c.argument('operator_namespace',
arg_group="Operator",
help='Namespace in which to install the Operator',
validator=validate_operator_namespace)
validator=_validate_operator_namespace)
c.argument('operator_type',
arg_group="Operator",
help='''Type of the operator. Valid value is 'flux' ''')
Expand Down
61 changes: 61 additions & 0 deletions src/k8s-configuration/azext_k8s_configuration/_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import base64
from azure.cli.core.azclierror import MutuallyExclusiveArgumentError, InvalidArgumentValueError


def _get_cluster_type(cluster_type):
if cluster_type.lower() == 'connectedclusters':
return 'Microsoft.Kubernetes'
# Since cluster_type is an enum of only two values, if not connectedClusters, it will be managedClusters.
return 'Microsoft.ContainerService'


def _fix_compliance_state(config):
# If we get Compliant/NonCompliant as compliance_sate, change them before returning
if config.compliance_status.compliance_state.lower() == 'noncompliant':
config.compliance_status.compliance_state = 'Failed'
elif config.compliance_status.compliance_state.lower() == 'compliant':
config.compliance_status.compliance_state = 'Installed'

return config


def _get_data_from_key_or_file(key, filepath):
if key != '' and filepath != '':
raise MutuallyExclusiveArgumentError(
'Error! Both textual key and key filepath cannot be provided',
'Try providing the file parameter without providing the plaintext parameter')
data = ''
if filepath != '':
data = _read_key_file(filepath)
elif key != '':
data = key
return data


def _read_key_file(path):
try:
with open(path, "r") as myfile: # user passed in filename
data_list = myfile.readlines() # keeps newline characters intact
data_list_len = len(data_list)
if (data_list_len) <= 0:
raise Exception("File provided does not contain any data")
raw_data = ''.join(data_list)
return _to_base64(raw_data)
except Exception as ex:
raise InvalidArgumentValueError(
'Error! Unable to read key file specified with: {0}'.format(ex),
'Verify that the filepath specified exists and contains valid utf-8 data') from ex


def _from_base64(base64_str):
return base64.b64decode(base64_str)


def _to_base64(raw_data):
bytes_data = raw_data.encode('utf-8')
return base64.b64encode(bytes_data).decode('utf-8')
115 changes: 106 additions & 9 deletions src/k8s-configuration/azext_k8s_configuration/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,48 @@
# --------------------------------------------------------------------------------------------

import re
from azure.cli.core.azclierror import InvalidArgumentValueError
import io
from azure.cli.core.azclierror import InvalidArgumentValueError, MutuallyExclusiveArgumentError

from knack.log import get_logger
from azext_k8s_configuration._client_factory import _resource_providers_client
from azext_k8s_configuration._utils import _from_base64
import azext_k8s_configuration._consts as consts
from urllib.parse import urlparse
from paramiko.hostkeys import HostKeyEntry
from paramiko.ed25519key import Ed25519Key
from paramiko.ssh_exception import SSHException
from Crypto.PublicKey import RSA, ECC, DSA


logger = get_logger(__name__)


# Parameter-Level Validation
def validate_configuration_type(configuration_type):
def _validate_configuration_type(configuration_type):
if configuration_type.lower() != 'sourcecontrolconfiguration':
raise InvalidArgumentValueError(
'Invalid configuration-type',
'Try specifying the valid value "sourceControlConfiguration"')


def validate_operator_namespace(namespace):
def _validate_operator_namespace(namespace):
if namespace.operator_namespace:
__validate_k8s_name(namespace.operator_namespace, "--operator-namespace", 23)
_validate_k8s_name(namespace.operator_namespace, "--operator-namespace", 23)


def validate_operator_instance_name(namespace):
def _validate_operator_instance_name(namespace):
if namespace.operator_instance_name:
__validate_k8s_name(namespace.operator_instance_name, "--operator-instance-name", 23)
_validate_k8s_name(namespace.operator_instance_name, "--operator-instance-name", 23)


# Create Parameter Validation
def validate_configuration_name(configuration_name):
__validate_k8s_name(configuration_name, "--name", 63)
def _validate_configuration_name(configuration_name):
_validate_k8s_name(configuration_name, "--name", 63)


# Helper
def __validate_k8s_name(param_value, param_name, max_len):
def _validate_k8s_name(param_value, param_name, max_len):
if len(param_value) > max_len:
raise InvalidArgumentValueError(
'Error! Invalid {0}'.format(param_name),
Expand All @@ -44,3 +58,86 @@ def __validate_k8s_name(param_value, param_name, max_len):
raise InvalidArgumentValueError(
'Error! Invalid {0}'.format(param_name),
'Parameter {0} can only contain lowercase alphanumeric characters and hyphens'.format(param_name))


def _validate_url_with_params(repository_url, ssh_private_key_set, known_hosts_contents_set, https_auth_set):
scheme = urlparse(repository_url).scheme

if scheme.lower() in ('http', 'https'):
if ssh_private_key_set:
raise MutuallyExclusiveArgumentError(
'Error! An --ssh-private-key cannot be used with an http(s) url',
'Verify the url provided is a valid ssh url and not an http(s) url')
if known_hosts_contents_set:
raise MutuallyExclusiveArgumentError(
'Error! --ssh-known-hosts cannot be used with an http(s) url',
'Verify the url provided is a valid ssh url and not an http(s) url')
if not https_auth_set and scheme == 'https':
logger.warning('Warning! https url is being used without https auth params, ensure the repository '
'url provided is not a private repo')
else:
if https_auth_set:
raise MutuallyExclusiveArgumentError(
'Error! https auth (--https-user and --https-key) cannot be used with a non-http(s) url',
'Verify the url provided is a valid http(s) url and not an ssh url')


def _validate_known_hosts(knownhost_data):
try:
knownhost_str = _from_base64(knownhost_data).decode('utf-8')
except Exception as ex:
raise InvalidArgumentValueError(
'Error! ssh known_hosts is not a valid utf-8 base64 encoded string',
'Verify that the string provided safely decodes into a valid utf-8 format') from ex
lines = knownhost_str.split('\n')
for line in lines:
line = line.strip(' ')
line_len = len(line)
if (line_len == 0) or (line[0] == "#"):
continue
try:
host_key = HostKeyEntry.from_line(line)
if not host_key:
raise Exception('not enough fields found in known_hosts line')
except Exception as ex:
raise InvalidArgumentValueError(
'Error! ssh known_hosts provided in wrong format',
'Verify that all lines in the known_hosts contents are provided in a valid sshd(8) format') from ex


def _validate_private_key(ssh_private_key_data):
try:
RSA.import_key(_from_base64(ssh_private_key_data))
return
except ValueError:
try:
ECC.import_key(_from_base64(ssh_private_key_data))
return
except ValueError:
try:
DSA.import_key(_from_base64(ssh_private_key_data))
return
except ValueError:
try:
key_obj = io.StringIO(_from_base64(ssh_private_key_data).decode('utf-8'))
Ed25519Key(file_obj=key_obj)
return
except SSHException:
raise InvalidArgumentValueError(
'Error! --ssh-private-key provided in invalid format',
'Verify the key provided is a valid PEM-formatted key of type RSA, ECC, DSA, or Ed25519')


# pylint: disable=broad-except
def _validate_cc_registration(cmd):
try:
rp_client = _resource_providers_client(cmd.cli_ctx)
registration_state = rp_client.get(consts.PROVIDER_NAMESPACE).registration_state

if registration_state.lower() != consts.REGISTERED.lower():
logger.warning("'Source Control Configuration' cannot be used because '%s' provider has not been "
"registered. More details for registering this provider can be found here - "
"https://aka.ms/RegisterKubernetesConfigurationProvider", consts.PROVIDER_NAMESPACE)
except Exception:
logger.warning("Unable to fetch registration state of '%s' provider. "
"Failed to enable 'source control configuration' feature...", consts.PROVIDER_NAMESPACE)
Loading