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 Maintenance Config Support #8190

Merged
merged 23 commits into from
Nov 12, 2024
1 change: 1 addition & 0 deletions src/containerapp/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ upcoming
* 'az containerapp create': Fix Role assignment error when the default Azure Container Registry could not be found
* Upgrade api-version to 2024-10-02-preview
* 'az containerapp create/update': `--yaml` support property pollingInterval and cooldownPeriod
* 'az containerapp env maintenance-config': Support Add, Update, Show, Remove
p-bouchon marked this conversation as resolved.
Show resolved Hide resolved

1.0.0b4
++++++
Expand Down
72 changes: 72 additions & 0 deletions src/containerapp/azext_containerapp/_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
HEADER_AZURE_ASYNC_OPERATION = "azure-asyncoperation"
HEADER_LOCATION = "location"
SESSION_RESOURCE = "https://dynamicsessions.io"
MAINTENANCE_CONFIG_DEFAULT_NAME = "default"


class GitHubActionPreviewClient(GitHubActionClient):
Expand Down Expand Up @@ -1396,3 +1397,74 @@ def list(cls, cmd, resource_group_name, environment_name):
dotNet_component_list.append(component)

return dotNet_component_list


class MaintenanceConfigPreviewClient():
api_version = PREVIEW_API_VERSION
maintenance_config_name = MAINTENANCE_CONFIG_DEFAULT_NAME

@classmethod
def show(cls, cmd, resource_group_name, environment_name):
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
sub_id = get_subscription_id(cmd.cli_ctx)
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/microsoft.app/managedenvironments/{}/maintenanceConfigurations/{}?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
environment_name,
cls.maintenance_config_name,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "GET", request_url)

return r.json()

@classmethod
def create_or_update(cls, cmd, resource_group_name, environment_name, maintenance_config_envelope):
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
sub_id = get_subscription_id(cmd.cli_ctx)
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/microsoft.app/managedenvironments/{}/maintenanceConfigurations/{}?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
environment_name,
cls.maintenance_config_name,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "PUT", request_url, body=json.dumps(maintenance_config_envelope))

if r.status_code == 201:
operation_url = r.headers.get(HEADER_AZURE_ASYNC_OPERATION)
poll_status(cmd, operation_url)
r = send_raw_request(cmd.cli_ctx, "GET", request_url)
if r.status_code == 202:
operation_url = r.headers.get(HEADER_LOCATION)
response = poll_results(cmd, operation_url)
if response is None:
raise ResourceNotFoundError("Could not find the maintenance config")
else:
return response

return r.json()

@classmethod
def remove(cls, cmd, resource_group_name, environment_name):
management_hostname = cmd.cli_ctx.cloud.endpoints.resource_manager
sub_id = get_subscription_id(cmd.cli_ctx)
url_fmt = "{}/subscriptions/{}/resourceGroups/{}/providers/microsoft.app/managedenvironments/{}/maintenanceConfigurations/{}?api-version={}"
request_url = url_fmt.format(
management_hostname.strip('/'),
sub_id,
resource_group_name,
environment_name,
cls.maintenance_config_name,
cls.api_version)

r = send_raw_request(cmd.cli_ctx, "DELETE", request_url)

if r.status_code in [200, 201, 202, 204]:
if r.status_code == 202:
operation_url = r.headers.get(HEADER_LOCATION)
poll_results(cmd, operation_url)
44 changes: 44 additions & 0 deletions src/containerapp/azext_containerapp/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -2131,3 +2131,47 @@
az containerapp job registry set -n my-containerapp-job -g MyResourceGroup \\
--server MyContainerappJobRegistry.azurecr.io --identity system-environment
"""

# Maintenance Config Commands
helps['containerapp env maintenance-config'] = """
type: group
short-summary: Commands to manage Planned Maintenance for Container Apps
"""

helps['containerapp env maintenance-config add'] = """
p-bouchon marked this conversation as resolved.
Show resolved Hide resolved
type: command
short-summary: Add Planned Maintenance to a Container App Environment
examples:
- name: Configure a Container App Environment to use a Planned Maintenance
text: |
az containerapp env maintenance-config add --environment myEnv -g MyResourceGroup \\
--duration 10 --start-hour-utc 11 --weekday Sunday
"""

helps['containerapp env maintenance-config update'] = """
type: command
short-summary: Update Planned Maintenance in a Container App Environment
examples:
- name: Update the Planned Maintenance in a Container App Environment
text: |
az containerapp env maintenance-config update --environment myEnv -g MyResourceGroup \\
--duration 8 --start-hour-utc 12 --weekday Thursday
"""

helps['containerapp env maintenance-config show'] = """
p-bouchon marked this conversation as resolved.
Show resolved Hide resolved
p-bouchon marked this conversation as resolved.
Show resolved Hide resolved
type: command
short-summary: Show Planned Maintenance in a Container App Environment
examples:
- name: Show Planned Maintenance
text: |
az containerapp env maintenance-config show --environment myEnv -g MyResourceGroup
"""

helps['containerapp env maintenance-config remove'] = """
type: command
short-summary: Remove Planned Maintenance in a Container App Environment
examples:
- name: Remove Planned Maintenance
text: |
az containerapp env maintenance-config remove --environment myEnv -g MyResourceGroup
"""
13 changes: 13 additions & 0 deletions src/containerapp/azext_containerapp/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,19 @@
"tags": None
}

MaintenanceConfiguration = {
"name": "default",
"properties": {
"scheduledEntries": [
{
"weekDay": None,
"startHourUtc": None,
"durationHours": None
}
]
}
}

SessionPool = {
"location": None,
"properties": {
Expand Down
6 changes: 6 additions & 0 deletions src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,12 @@ def load_arguments(self, _):
c.argument('max_replicas', type=int, help="Maximum number of replicas to run for the Java component.")
c.argument('route_yaml', options_list=['--route-yaml', '--yaml'], help="Path to a .yaml file with the configuration of a Spring Cloud Gateway route. For an example, see https://aka.ms/gateway-for-spring-routes-yaml")

with self.argument_context('containerapp env maintenance-config') as c:
c.argument('weekday', options_list=['--weekday', '-w'], arg_type=get_enum_type(["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]), help="The weekday to schedule the maintenance configuration.")
c.argument('start_hour_utc', options_list=['--start-hour-utc', '-s'], type=int, help="The hour to start the maintenance configuration. Valid value from 0 to 23.")
c.argument('duration', options_list=['--duration', '-d'], type=int, help="The duration in hours of the maintenance configuration. Minimum value: 8. Maximum value: 24")
c.argument('env_name', options_list=['--environment'], help="The environment name.")

with self.argument_context('containerapp job logs show') as c:
c.argument('follow', help="Print logs in real time if present.", arg_type=get_three_state_flag())
c.argument('tail', help="The number of past logs to print (0-300)", type=int, default=20)
Expand Down
6 changes: 6 additions & 0 deletions src/containerapp/azext_containerapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,3 +262,9 @@ def load_command_table(self, args):
g.custom_command('set', 'create_or_update_java_logger', supports_no_wait=True)
g.custom_command('delete', 'delete_java_logger', supports_no_wait=True)
g.custom_show_command('show', 'show_java_logger')

with self.command_group('containerapp env maintenance-config', is_preview=True) as g:
g.custom_command('add', 'add_maintenance_config')
g.custom_command('update', 'update_maintenance_config')
g.custom_command('remove', 'remove_maintenance_config', confirmation=True)
g.custom_show_command('show', 'show_maintenance_config')
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# 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.
# --------------------------------------------------------------------------------------------
# pylint: disable=line-too-long, broad-except, logging-format-interpolation

from copy import deepcopy
from knack.log import get_logger
from typing import Any, Dict

from azure.cli.core.azclierror import (ValidationError)
from azure.cli.core.commands import AzCliCommand
from azure.cli.command_modules.containerapp.base_resource import BaseResource

from ._models import MaintenanceConfiguration as MaintenanceConfigurationModel
from ._client_factory import handle_raw_exception, handle_non_404_status_code_exception

logger = get_logger(__name__)


class ContainerappEnvMaintenanceConfigDecorator(BaseResource):
def __init__(self, cmd: AzCliCommand, client: Any, raw_parameters: Dict, models: str):
super().__init__(cmd, client, raw_parameters, models)
self.maintenance_config_def = deepcopy(MaintenanceConfigurationModel)
self.existing_maintenance_config_def = None

def get_argument_environment_name(self):
return self.get_param('env_name')

def get_argument_resource_group_name(self):
return self.get_param('resource_group_name')

def get_argument_weekday(self):
return self.get_param('weekday')

def get_argument_start_hour_utc(self):
return self.get_param('start_hour_utc')

def get_argument_duration(self):
return self.get_param('duration')


class ContainerAppEnvMaintenanceConfigPreviewDecorator(ContainerappEnvMaintenanceConfigDecorator):
def validate_arguments(self):
if self.get_argument_start_hour_utc() is not None:
if not (0 <= int(self.get_argument_start_hour_utc()) <= 23):
raise ValidationError("Start hour must be an integer from 0 to 23")

if self.get_argument_duration() is not None:
if not (8 <= int(self.get_argument_duration()) <= 24):
raise ValidationError("Duration must be an integer from 8 to 24")

if self.get_argument_weekday() is not None:
if self.get_argument_weekday().lower() not in ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]:
raise ValidationError("Weekday must be a day of the week")

def construct_payload(self, forUpdate=False):
if forUpdate:
self.existing_maintenance_config_def = self.client.show(
cmd=self.cmd,
resource_group_name=self.get_argument_resource_group_name(),
environment_name=self.get_argument_environment_name())

self.maintenance_config_def = deepcopy(self.existing_maintenance_config_def)

if self.get_argument_start_hour_utc() is not None:
self.maintenance_config_def["properties"]["scheduledEntries"][0]["startHourUtc"] = self.get_argument_start_hour_utc()
if self.get_argument_duration() is not None:
self.maintenance_config_def["properties"]["scheduledEntries"][0]["durationHours"] = self.get_argument_duration()
if self.get_argument_weekday() is not None:
self.maintenance_config_def["properties"]["scheduledEntries"][0]["weekDay"] = self.get_argument_weekday()

def create_or_update(self):
try:
return self.client.create_or_update(
cmd=self.cmd,
resource_group_name=self.get_argument_resource_group_name(),
environment_name=self.get_argument_environment_name(),
maintenance_config_envelope=self.maintenance_config_def)
except Exception as e:
handle_raw_exception(e)

def remove(self):
try:
return self.client.remove(
cmd=self.cmd,
resource_group_name=self.get_argument_resource_group_name(),
environment_name=self.get_argument_environment_name())
except Exception as e:
handle_raw_exception(e)

def show(self):
try:
return self.client.show(
cmd=self.cmd,
resource_group_name=self.get_argument_resource_group_name(),
environment_name=self.get_argument_environment_name())
except Exception as e:
handle_non_404_status_code_exception(e)
return ""
58 changes: 57 additions & 1 deletion src/containerapp/azext_containerapp/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
from .containerapp_sessionpool_decorator import SessionPoolPreviewDecorator, SessionPoolCreateDecorator, SessionPoolUpdateDecorator
from .containerapp_session_code_interpreter_decorator import SessionCodeInterpreterCommandsPreviewDecorator
from .containerapp_job_registry_decorator import ContainerAppJobRegistryPreviewSetDecorator
from .containerapp_env_maintenance_config_decorator import ContainerAppEnvMaintenanceConfigPreviewDecorator
from .dotnet_component_decorator import DotNetComponentDecorator
from ._client_factory import handle_raw_exception, handle_non_404_status_code_exception
from ._clients import (
Expand All @@ -102,7 +103,8 @@
JavaComponentPreviewClient,
SessionPoolPreviewClient,
SessionCodeInterpreterPreviewClient,
DotNetComponentPreviewClient
DotNetComponentPreviewClient,
MaintenanceConfigPreviewClient
)
from ._dev_service_utils import DevServiceUtils
from ._models import (
Expand Down Expand Up @@ -3259,3 +3261,57 @@ def set_registry_job(cmd, name, resource_group_name, server, username=None, pass
containerapp_job_registry_set_decorator.construct_payload()
r = containerapp_job_registry_set_decorator.set()
return r


# maintenance config
def add_maintenance_config(cmd, resource_group_name, env_name, duration, start_hour_utc, weekday):
p-bouchon marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

Will you support customized name in the future? Current design is not very flexible for extensibility. Consider adding --name support for your commands with default value = "default" which will be extensible in the future.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hi @Juliehzl

#8190 (comment)

only one maintenance config of name "default" is allowed.
If we want to support name in the future, we can add name in that time. If we add it now, it will be exposed to customer.

Copy link
Contributor

@Greedygre Greedygre Nov 12, 2024

Choose a reason for hiding this comment

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

@p-bouchon please correct me if I am wrong

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We currently have no plans to allow for non default names for maintenance configuration. If any were to be added, they would be specific non-customizable names to specify upgrades on specific aspects of the service

raw_parameters = locals()
maintenance_config_decorator = ContainerAppEnvMaintenanceConfigPreviewDecorator(
cmd=cmd,
client=MaintenanceConfigPreviewClient,
raw_parameters=raw_parameters,
models=CONTAINER_APPS_SDK_MODELS
)
maintenance_config_decorator.construct_payload()
maintenance_config_decorator.validate_arguments()
r = maintenance_config_decorator.create_or_update()
return r


def update_maintenance_config(cmd, resource_group_name, env_name, duration=None, start_hour_utc=None, weekday=None):
raw_parameters = locals()
maintenance_config_decorator = ContainerAppEnvMaintenanceConfigPreviewDecorator(
cmd=cmd,
client=MaintenanceConfigPreviewClient,
raw_parameters=raw_parameters,
models=CONTAINER_APPS_SDK_MODELS
)
forUpdate = True
maintenance_config_decorator.construct_payload(forUpdate)
maintenance_config_decorator.validate_arguments()
r = maintenance_config_decorator.create_or_update()
return r


def remove_maintenance_config(cmd, resource_group_name, env_name):
raw_parameters = locals()
maintenance_config_decorator = ContainerAppEnvMaintenanceConfigPreviewDecorator(
cmd=cmd,
client=MaintenanceConfigPreviewClient,
raw_parameters=raw_parameters,
models=CONTAINER_APPS_SDK_MODELS
)
r = maintenance_config_decorator.remove()
return r


def show_maintenance_config(cmd, resource_group_name, env_name):
raw_parameters = locals()
maintenance_config_decorator = ContainerAppEnvMaintenanceConfigPreviewDecorator(
cmd=cmd,
client=MaintenanceConfigPreviewClient,
raw_parameters=raw_parameters,
models=CONTAINER_APPS_SDK_MODELS
)
r = maintenance_config_decorator.show()
return r
Loading
Loading