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

add fleet CLI #5244

Merged
merged 39 commits into from
Aug 23, 2022
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d1bab98
initial: azdev extension creation
Jul 26, 2022
54c14c0
update min cli supported version
Jul 26, 2022
7d1d359
add fleet client sdk
Jul 26, 2022
b90034c
all fleet commands with help and params
Jul 28, 2022
7e00779
update git code owners
Jul 28, 2022
f9c99d4
update git code owners
Jul 28, 2022
8482100
nit: remove redundancy
Aug 11, 2022
26c4be9
add live and unit tests
Aug 11, 2022
c534881
file cleanup
Aug 11, 2022
57c83f8
file cleanup
Aug 11, 2022
bea74bb
file cleanup
Aug 11, 2022
2191b34
file cleanup
Aug 11, 2022
d14e92e
file cleanup
Aug 11, 2022
ca07417
add fleet list credentials command
Aug 15, 2022
326cfa9
Merge pull request #6 from pdaru/fleet-extension
pdaru Aug 15, 2022
6eb1b32
update live test
Aug 15, 2022
d683af5
Merge pull request #7 from pdaru/fleet-extension
pdaru Aug 15, 2022
14fcd29
file clean up
Aug 15, 2022
ce4a579
Merge pull request #8 from pdaru/fleet-extension
pdaru Aug 15, 2022
57f5858
add GET,PATCH commands & update tests
Aug 19, 2022
9dd68b2
Merge pull request #9 from pdaru/fleet-extension
pdaru Aug 19, 2022
4271e5c
update style
Aug 19, 2022
12ff9b0
Merge pull request #10 from pdaru/fleet-extension
pdaru Aug 19, 2022
817f253
add licence
Aug 19, 2022
6d0ad33
Merge pull request #11 from pdaru/fleet-extension
pdaru Aug 19, 2022
9c6013c
add licence
Aug 19, 2022
4f43721
fix checks
Aug 19, 2022
5c7c1f3
add fleet to service_name
Aug 19, 2022
82e0f3f
update owner
Aug 19, 2022
b939dca
update test recording
Aug 19, 2022
4a28a64
update test
Aug 19, 2022
45b29e9
check fix
Aug 22, 2022
52e6993
update recording
Aug 23, 2022
1fcd3b7
update command names as per convention
Aug 23, 2022
b27b45b
update tests
Aug 23, 2022
bcd9ccf
update tests
Aug 23, 2022
69e0c87
update setup url
pdaru Aug 23, 2022
b820ec0
update setup python ver
pdaru Aug 23, 2022
2d4e839
fix bugs
Aug 23, 2022
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
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,5 @@
/src/orbital/ @thisisdevanshu

/src/fluid-relay/ @kairu-ms @necusjz @ZengTaoxu

/src/fleet/ @pdaru
8 changes: 8 additions & 0 deletions src/fleet/HISTORY.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.. :changelog:

Release History
===============

0.1.0
++++++
* Initial release.
5 changes: 5 additions & 0 deletions src/fleet/README.rst
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'
41 changes: 41 additions & 0 deletions src/fleet/azext_fleet/__init__.py
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
30 changes: 30 additions & 0 deletions src/fleet/azext_fleet/_client_factory.py
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
91 changes: 91 additions & 0 deletions src/fleet/azext_fleet/_help.py
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.
Comment on lines +59 to +61
Copy link
Contributor

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

"""

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
"""
142 changes: 142 additions & 0 deletions src/fleet/azext_fleet/_helpers.py
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)})')
37 changes: 37 additions & 0 deletions src/fleet/azext_fleet/_params.py
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.')
Copy link
Member

Choose a reason for hiding this comment

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

name is also supported by default?

Copy link
Member Author

Choose a reason for hiding this comment

The 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)
Copy link
Member

Choose a reason for hiding this comment

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

I suppose tags is supported by CLI framework by default

Copy link
Member Author

Choose a reason for hiding this comment

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

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)
16 changes: 16 additions & 0 deletions src/fleet/azext_fleet/_resourcegroup.py
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
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

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

Loading