-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
add fleet CLI #5244
add fleet CLI #5244
Changes from 36 commits
d1bab98
54c14c0
7d1d359
b90034c
7e00779
f9c99d4
8482100
26c4be9
c534881
57c83f8
bea74bb
2191b34
d14e92e
ca07417
326cfa9
6eb1b32
d683af5
14fcd29
ce4a579
57f5858
9dd68b2
4271e5c
12ff9b0
817f253
6d0ad33
9c6013c
4f43721
5c7c1f3
82e0f3f
b939dca
4a28a64
45b29e9
52e6993
1fcd3b7
b27b45b
bcd9ccf
69e0c87
b820ec0
2d4e839
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -243,3 +243,5 @@ | |
/src/orbital/ @thisisdevanshu | ||
|
||
/src/fluid-relay/ @kairu-ms @necusjz @ZengTaoxu | ||
|
||
/src/fleet/ @pdaru |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
.. :changelog: | ||
|
||
Release History | ||
=============== | ||
|
||
0.1.0 | ||
++++++ | ||
* Initial release. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
Microsoft Azure CLI 'fleet' Extension | ||
========================================== | ||
|
||
This package is for the 'fleet' extension. | ||
i.e. 'az fleet' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# -------------------------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for license information. | ||
# -------------------------------------------------------------------------------------------- | ||
|
||
from azure.cli.core import AzCommandsLoader | ||
from azure.cli.core.profiles import register_resource_type, SDKProfile | ||
|
||
# pylint: disable=unused-import | ||
from azext_fleet._help import helps | ||
from azext_fleet._client_factory import CUSTOM_MGMT_FLEET | ||
|
||
|
||
def register_fleet_resource_type(): | ||
register_resource_type( | ||
"latest", | ||
CUSTOM_MGMT_FLEET, | ||
SDKProfile("2022-06-02-preview", {"container_services": "2017-07-01"}), | ||
) | ||
|
||
|
||
class FleetCommandsLoader(AzCommandsLoader): | ||
|
||
def __init__(self, cli_ctx=None): | ||
from azure.cli.core.commands import CliCommandType | ||
register_fleet_resource_type() | ||
|
||
fleet_custom = CliCommandType(operations_tmpl='azext_fleet.custom#{}') | ||
super().__init__(cli_ctx=cli_ctx, resource_type=CUSTOM_MGMT_FLEET, custom_command_type=fleet_custom) | ||
|
||
def load_command_table(self, args): | ||
from azext_fleet.commands import load_command_table | ||
load_command_table(self, args) | ||
return self.command_table | ||
|
||
def load_arguments(self, command): | ||
from azext_fleet._params import load_arguments | ||
load_arguments(self, command) | ||
|
||
|
||
COMMAND_LOADER_CLS = FleetCommandsLoader |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# -------------------------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# 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 | ||
from azure.cli.core.profiles import ( | ||
CustomResourceType, | ||
ResourceType | ||
) | ||
|
||
CUSTOM_MGMT_FLEET = CustomResourceType('azext_fleet.vendored_sdks', 'ContainerServiceClient') | ||
|
||
|
||
# container service clients | ||
def get_container_service_client(cli_ctx, subscription_id=None): | ||
return get_mgmt_service_client(cli_ctx, CUSTOM_MGMT_FLEET, subscription_id=subscription_id) | ||
|
||
|
||
def cf_fleets(cli_ctx, *_): | ||
return get_container_service_client(cli_ctx).fleets | ||
|
||
|
||
def cf_fleet_members(cli_ctx, *_): | ||
return get_container_service_client(cli_ctx).fleet_members | ||
|
||
|
||
def get_resource_groups_client(cli_ctx, subscription_id=None): | ||
return get_mgmt_service_client( | ||
cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, subscription_id=subscription_id).resource_groups |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
# coding=utf-8 | ||
# -------------------------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for license information. | ||
# -------------------------------------------------------------------------------------------- | ||
|
||
from knack.help_files import helps # pylint: disable=unused-import | ||
|
||
|
||
helps['fleet'] = """ | ||
type: group | ||
short-summary: Commands to manage fleet. | ||
""" | ||
|
||
helps['fleet create'] = """ | ||
type: command | ||
short-summary: Create a new fleet. | ||
parameters: | ||
- name: --tags | ||
type: string | ||
short-summary: The tags of the managed cluster. The managed cluster instance and all resources managed by the cloud provider will be tagged. | ||
- name: --dns-name-prefix -p | ||
type: string | ||
short-summary: Prefix for hostnames that are created. If not specified, generate a hostname using the | ||
managed cluster and resource group names. | ||
""" | ||
|
||
helps['fleet update'] = """ | ||
type: command | ||
short-summary: Update an existing fleet. | ||
parameters: | ||
- name: --tags | ||
type: string | ||
short-summary: The tags of the managed cluster. The managed cluster instance and all resources managed by the cloud provider will be tagged. | ||
""" | ||
|
||
helps['fleet show'] = """ | ||
type: command | ||
short-summary: Get an existing fleet. | ||
""" | ||
|
||
helps['fleet list'] = """ | ||
type: command | ||
short-summary: List fleet by resource group & subscription id. | ||
""" | ||
|
||
helps['fleet delete'] = """ | ||
type: command | ||
short-summary: Delete an existing fleet. | ||
""" | ||
|
||
helps['fleet get-credentials'] = """ | ||
type: command | ||
short-summary: List fleet kubeconfig credentials | ||
parameters: | ||
- name: --overwrite-existing | ||
type: bool | ||
short-summary: Overwrite any existing cluster entry with the same name. | ||
- name: --file -f | ||
type: string | ||
short-summary: Kubernetes configuration file to update. Use "-" to print YAML to stdout instead. | ||
""" | ||
|
||
helps['fleet member'] = """ | ||
type: group | ||
short-summary: Commands to manage a fleet member. | ||
""" | ||
|
||
helps['fleet member create'] = """ | ||
type: command | ||
short-summary: Join member cluster to a fleet. | ||
parameters: | ||
- name: --member-cluster-id | ||
type: string | ||
short-summary: ID of the managed cluster. | ||
""" | ||
|
||
helps['fleet member list'] = """ | ||
type: command | ||
short-summary: List member cluster(s) of a fleet. | ||
""" | ||
|
||
helps['fleet member show'] = """ | ||
type: command | ||
short-summary: Get member cluster of a fleet. | ||
""" | ||
|
||
helps['fleet member delete'] = """ | ||
type: command | ||
short-summary: Remove member cluster from a fleet | ||
""" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
# -------------------------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for license information. | ||
# -------------------------------------------------------------------------------------------- | ||
|
||
import errno | ||
import os | ||
import platform | ||
import stat | ||
import tempfile | ||
import yaml | ||
|
||
from knack.log import get_logger | ||
from knack.prompting import NoTTYException, prompt_y_n | ||
from knack.util import CLIError | ||
|
||
logger = get_logger(__name__) | ||
|
||
|
||
def print_or_merge_credentials(path, kubeconfig, overwrite_existing, context_name): | ||
"""Merge an unencrypted kubeconfig into the file at the specified path, or print it to | ||
stdout if the path is "-". | ||
""" | ||
# Special case for printing to stdout | ||
if path == "-": | ||
print(kubeconfig) | ||
return | ||
|
||
# ensure that at least an empty ~/.kube/config exists | ||
directory = os.path.dirname(path) | ||
if directory and not os.path.exists(directory): | ||
try: | ||
os.makedirs(directory) | ||
except OSError as ex: | ||
if ex.errno != errno.EEXIST: | ||
raise | ||
if not os.path.exists(path): | ||
with os.fdopen(os.open(path, os.O_CREAT | os.O_WRONLY, 0o600), 'wt'): | ||
pass | ||
|
||
# merge the new kubeconfig into the existing one | ||
fd, temp_path = tempfile.mkstemp() | ||
additional_file = os.fdopen(fd, 'w+t') | ||
try: | ||
additional_file.write(kubeconfig) | ||
additional_file.flush() | ||
_merge_kubernetes_configurations( | ||
path, temp_path, overwrite_existing, context_name) | ||
except yaml.YAMLError as ex: | ||
logger.warning( | ||
'Failed to merge credentials to kube config file: %s', ex) | ||
finally: | ||
additional_file.close() | ||
os.remove(temp_path) | ||
|
||
|
||
def _merge_kubernetes_configurations(existing_file, addition_file, replace, context_name=None): | ||
existing = _load_kubernetes_configuration(existing_file) | ||
addition = _load_kubernetes_configuration(addition_file) | ||
|
||
if context_name is not None: | ||
addition['contexts'][0]['name'] = context_name | ||
addition['contexts'][0]['context']['cluster'] = context_name | ||
addition['clusters'][0]['name'] = context_name | ||
addition['current-context'] = context_name | ||
|
||
# rename the admin context so it doesn't overwrite the user context | ||
for ctx in addition.get('contexts', []): | ||
try: | ||
if ctx['context']['user'].startswith('clusterAdmin'): | ||
admin_name = ctx['name'] + '-admin' | ||
addition['current-context'] = ctx['name'] = admin_name | ||
break | ||
except (KeyError, TypeError): | ||
continue | ||
|
||
if addition is None: | ||
raise CLIError( | ||
f'failed to load additional configuration from {addition_file}') | ||
|
||
if existing is None: | ||
existing = addition | ||
else: | ||
_handle_merge(existing, addition, 'clusters', replace) | ||
_handle_merge(existing, addition, 'users', replace) | ||
_handle_merge(existing, addition, 'contexts', replace) | ||
existing['current-context'] = addition['current-context'] | ||
|
||
# check that ~/.kube/config is only read- and writable by its owner | ||
if platform.system() != "Windows" and not os.path.islink(existing_file): | ||
existing_file_perms = "{:o}".format(stat.S_IMODE(os.lstat(existing_file).st_mode)) | ||
if not existing_file_perms.endswith("600"): | ||
logger.warning( | ||
'%s has permissions "%s".\nIt should be readable and writable only by its owner.', | ||
existing_file, | ||
existing_file_perms, | ||
) | ||
|
||
with open(existing_file, 'w+') as stream: | ||
yaml.safe_dump(existing, stream, default_flow_style=False) | ||
|
||
current_context = addition.get('current-context', 'UNKNOWN') | ||
msg = f'Merged "{current_context}" as current context in {existing_file}' | ||
print(msg) | ||
|
||
|
||
def _handle_merge(existing, addition, key, replace): | ||
if not addition[key]: | ||
return | ||
if existing[key] is None: | ||
existing[key] = addition[key] | ||
return | ||
|
||
for i in addition[key]: | ||
for j in existing[key]: | ||
if i['name'] == j['name']: | ||
if replace or i == j: | ||
existing[key].remove(j) | ||
else: | ||
msg = 'A different object named {} already exists in your kubeconfig file.\nOverwrite?' | ||
overwrite = False | ||
try: | ||
overwrite = prompt_y_n(msg.format(i['name'])) | ||
except NoTTYException: | ||
pass | ||
if overwrite: | ||
existing[key].remove(j) | ||
else: | ||
msg = 'A different object named {} already exists in {} in your kubeconfig file.' | ||
raise CLIError(msg.format(i['name'], key)) | ||
existing[key].append(i) | ||
|
||
|
||
def _load_kubernetes_configuration(filename): | ||
try: | ||
with open(filename) as stream: | ||
return yaml.safe_load(stream) | ||
except (IOError, OSError) as ex: | ||
if getattr(ex, 'errno', 0) == errno.ENOENT: | ||
raise CLIError(f'{filename} does not exist') | ||
except (yaml.parser.ParserError, UnicodeDecodeError) as ex: | ||
raise CLIError(f'Error parsing {filename} ({str(ex)})') |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# -------------------------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for license information. | ||
# -------------------------------------------------------------------------------------------- | ||
|
||
# pylint: disable=line-too-long | ||
import os | ||
from argcomplete.completers import FilesCompleter | ||
from azure.cli.core.commands.parameters import ( | ||
tags_type, | ||
file_type | ||
) | ||
from azext_fleet._validators import validate_member_cluster_id | ||
|
||
|
||
def load_arguments(self, _): | ||
|
||
with self.argument_context('fleet') as c: | ||
c.argument('name', options_list=['--name', '-n'], help='Specify the fleet name.') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is, but we needed different contexts associated so followed how aks-preview does for nodepool https://github.com/Azure/azure-cli-extensions/blob/main/src/aks-preview/azext_aks_preview/_params.py#L432 |
||
|
||
with self.argument_context('fleet create') as c: | ||
c.argument('tags', tags_type) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unsure, borrowed from aks-preview |
||
c.argument('dns_name_prefix', options_list=['--dns-name-prefix', '-p']) | ||
|
||
with self.argument_context('fleet update') as c: | ||
c.argument('tags', tags_type) | ||
|
||
with self.argument_context('fleet get-credentials') as c: | ||
c.argument('context_name', options_list=['--context'], help='If specified, overwrite the default context name.') | ||
c.argument('path', options_list=['--file', '-f'], type=file_type, completer=FilesCompleter(), default=os.path.join(os.path.expanduser('~'), '.kube', 'config')) | ||
|
||
with self.argument_context('fleet member') as c: | ||
c.argument('name', options_list=['--name', '-n'], help='Specify the fleet member name.') | ||
c.argument('fleet_name', help='Specify the fleet name.') | ||
|
||
with self.argument_context('fleet member create') as c: | ||
c.argument('member_cluster_id', validator=validate_member_cluster_id) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# -------------------------------------------------------------------------------------------- | ||
# Copyright (c) Microsoft Corporation. All rights reserved. | ||
# Licensed under the MIT License. See License.txt in the project root for license information. | ||
# -------------------------------------------------------------------------------------------- | ||
|
||
from knack.util import CLIError | ||
from azext_fleet._client_factory import get_resource_groups_client | ||
|
||
|
||
def get_rg_location(ctx, resource_group_name, subscription_id=None): | ||
groups = get_resource_groups_client(ctx, subscription_id=subscription_id) | ||
# Just do the get, we don't need the result, it will error out if the group doesn't exist. | ||
rg = groups.get(resource_group_name) | ||
if rg is None: | ||
raise CLIError(f"Resource group {resource_group_name} not found.") | ||
return rg.location | ||
Comment on lines
+10
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Usually, we will expose location argument. If you went to get the default location from resource group, please refer this implementation. https://github.com/Azure/azure-cli/blob/76c224121c05f77f6cf6f17130b6c8fde824e10a/src/azure-cli/azure/cli/command_modules/monitor/_params.py#L412 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. borrowed aks-preview implementation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have
--output -o
argument to support yaml output in stdout.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
borrowed aks-preview implementation
https://github.com/Azure/azure-cli-extensions/blob/main/src/aks-preview/azext_aks_preview/_help.py#L1622