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 spring cloud gateway cli #8037

Merged
merged 15 commits into from
Oct 16, 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 up': Support `--registry-identity`, `--system-assigned`, `--user-assigned`
* 'az containerapp containerapp create/up': `--registry-server` and `--source` use managed identity for image pull by default
* 'az containerapp containerapp create': `--registry-server` use managed identity for image pull by default. `--no-wait` will not take effect with system registry identity.
* 'az containerapp env java-component gateway-for-spring': Support create/update/show/delete Gateway for spring.

1.0.0b3
++++++
Expand Down
1 change: 1 addition & 0 deletions src/containerapp/azext_containerapp/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
JAVA_COMPONENT_EUREKA = "SpringCloudEureka"
JAVA_COMPONENT_NACOS = "Nacos"
JAVA_COMPONENT_ADMIN = "SpringBootAdmin"
JAVA_COMPONENT_GATEWAY = "SpringCloudGateway"

DOTNET_COMPONENT_RESOURCE_TYPE = "AspireDashboard"

Expand Down
77 changes: 77 additions & 0 deletions src/containerapp/azext_containerapp/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -1619,6 +1619,83 @@
--configuration PropertyName1=Value1 PropertyName2=Value2
"""

helps['containerapp env java-component gateway-for-spring'] = """
type: group
short-summary: Commands to manage the Gateway for Spring for the Container Apps environment.
"""

helps['containerapp env java-component gateway-for-spring create'] = """
type: command
short-summary: Command to create the Gateway for Spring.
examples:
- name: Create a Gateway for Spring with default configuration.
text: |
az containerapp env java-component gateway-for-spring create -g MyResourceGroup \\
-n MyJavaComponentName \\
--environment MyEnvironment \\
--route-yaml MyRouteYamlFilePath
- name: Create a Gateway for Spring with custom configurations.
text: |
az containerapp env java-component gateway-for-spring create -g MyResourceGroup \\
-n MyJavaComponentName \\
--environment MyEnvironment \\
--route-yaml MyRouteYamlFilePath \\
--configuration PropertyName1=Value1 PropertyName2=Value2
- name: Create a Gateway for Spring with multiple replicas.
text: |
az containerapp env java-component gateway-for-spring create -g MyResourceGroup \\
-n MyJavaComponentName \\
--environment MyEnvironment \\
--route-yaml MyRouteYamlFilePath \\
--min-replicas 2 --max-replicas 2
"""

helps['containerapp env java-component gateway-for-spring delete'] = """
type: command
short-summary: Command to delete the Gateway for Spring.
examples:
- name: Delete a Gateway for Spring.
text: |
az containerapp env java-component gateway-for-spring delete -g MyResourceGroup \\
-n MyJavaComponentName \\
--environment MyEnvironment
"""

helps['containerapp env java-component gateway-for-spring show'] = """
type: command
short-summary: Command to show the Gateway for Spring.
examples:
- name: Show Gateway for Spring.
text: |
az containerapp env java-component gateway-for-spring show -g MyResourceGroup \\
-n MyJavaComponentName \\
--environment MyEnvironment
"""

helps['containerapp env java-component gateway-for-spring update'] = """
type: command
short-summary: Command to update the Gateway for Spring.
examples:
- name: Update a Gateway for Spring with new routes.
text: |
az containerapp env java-component gateway-for-spring update -g MyResourceGroup \\
-n MyJavaComponentName \\
--environment MyEnvironment \\
--route-yaml MyRouteYamlFilePath
- name: Delete all configurations of the Gateway for Spring.
text: |
az containerapp env java-component gateway-for-spring update -g MyResourceGroup \\
-n MyJavaComponentName \\
--environment MyEnvironment \\
--configuration
- name: Update a Gateway for Spring with custom configurations.
text: |
az containerapp env java-component gateway-for-spring update -g MyResourceGroup \\
-n MyJavaComponentName \\
--environment MyEnvironment \\
--configuration PropertyName1=Value1 PropertyName2=Value2
"""

# Container Apps Telemetry Commands

helps['containerapp env telemetry'] = """
Expand Down
1 change: 1 addition & 0 deletions src/containerapp/azext_containerapp/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ def load_arguments(self, _):
c.argument('configuration', nargs="*", help="Java component configuration. Configuration must be in format \"<propertyName>=<value>\" \"<propertyName>=<value>\"...")
c.argument('min_replicas', type=int, help="Minimum number of replicas to run for the Java component.")
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")
Copy link
Contributor

@Greedygre Greedygre Oct 10, 2024

Choose a reason for hiding this comment

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

I tried https://aka.ms/gateway-for-spring-routes-yaml
But seems it doesn't work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The link will point to our SCG doc, we will release the doc after the cli released, and make the link available.


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())
Expand Down
8 changes: 7 additions & 1 deletion src/containerapp/azext_containerapp/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ def load_command_table(self, args):
with self.command_group('containerapp job replica', is_preview=True) as g:
g.custom_show_command('list', 'list_replica_containerappsjob')

with self.command_group('containerapp env java-component nacos') as g:
with self.command_group('containerapp env java-component nacos', is_preview=True) as g:
Copy link
Contributor

Choose a reason for hiding this comment

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

Why update az containerapp env java-component nacos to preview?

Copy link
Contributor Author

@Descatles Descatles Oct 10, 2024

Choose a reason for hiding this comment

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

The nacos should be in preview status, and the preview is removed from java component level, we need add the preview back to nacos level. Just add in this pr for convenience

g.custom_command('create', 'create_nacos', supports_no_wait=True)
g.custom_command('update', 'update_nacos', supports_no_wait=True)
g.custom_show_command('show', 'show_nacos')
Expand All @@ -225,6 +225,12 @@ def load_command_table(self, args):
g.custom_show_command('show', 'show_admin_for_spring')
g.custom_command('delete', 'delete_admin_for_spring', confirmation=True, supports_no_wait=True)

with self.command_group('containerapp env java-component gateway-for-spring', is_preview=True) as g:
g.custom_command('create', 'create_gateway_for_spring', supports_no_wait=True)
g.custom_command('update', 'update_gateway_for_spring', supports_no_wait=True)
g.custom_show_command('show', 'show_gateway_for_spring')
Copy link
Contributor

Choose a reason for hiding this comment

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

what about list?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The list command is consistent with other Java components at the Java component level.

g.custom_command('delete', 'delete_gateway_for_spring', confirmation=True, supports_no_wait=True)

with self.command_group('containerapp env dotnet-component', is_preview=True) as g:
g.custom_command('list', 'list_dotnet_components')
g.custom_show_command('show', 'show_dotnet_component')
Expand Down
22 changes: 19 additions & 3 deletions src/containerapp/azext_containerapp/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
DEV_QDRANT_CONTAINER_NAME, DEV_QDRANT_SERVICE_TYPE, DEV_WEAVIATE_IMAGE, DEV_WEAVIATE_CONTAINER_NAME, DEV_WEAVIATE_SERVICE_TYPE,
DEV_MILVUS_IMAGE, DEV_MILVUS_CONTAINER_NAME, DEV_MILVUS_SERVICE_TYPE, DEV_SERVICE_LIST, CONTAINER_APPS_SDK_MODELS, BLOB_STORAGE_TOKEN_STORE_SECRET_SETTING_NAME,
DAPR_SUPPORTED_STATESTORE_DEV_SERVICE_LIST, DAPR_SUPPORTED_PUBSUB_DEV_SERVICE_LIST,
JAVA_COMPONENT_CONFIG, JAVA_COMPONENT_EUREKA, JAVA_COMPONENT_ADMIN, JAVA_COMPONENT_NACOS, DOTNET_COMPONENT_RESOURCE_TYPE)
JAVA_COMPONENT_CONFIG, JAVA_COMPONENT_EUREKA, JAVA_COMPONENT_ADMIN, JAVA_COMPONENT_NACOS, JAVA_COMPONENT_GATEWAY, DOTNET_COMPONENT_RESOURCE_TYPE)


logger = get_logger(__name__)
Expand Down Expand Up @@ -2328,7 +2328,7 @@ def delete_java_component(cmd, java_component_name, environment_name, resource_g
return java_component_decorator.delete()


def create_java_component(cmd, java_component_name, environment_name, resource_group_name, target_java_component_type, configuration, service_bindings, unbind_service_bindings, min_replicas, max_replicas, no_wait):
def create_java_component(cmd, java_component_name, environment_name, resource_group_name, target_java_component_type, configuration, service_bindings, unbind_service_bindings, min_replicas, max_replicas, no_wait, route_yaml=None):
raw_parameters = locals()
java_component_decorator = JavaComponentDecorator(
cmd=cmd,
Expand All @@ -2340,7 +2340,7 @@ def create_java_component(cmd, java_component_name, environment_name, resource_g
return java_component_decorator.create()


def update_java_component(cmd, java_component_name, environment_name, resource_group_name, target_java_component_type, configuration, service_bindings, unbind_service_bindings, min_replicas, max_replicas, no_wait):
def update_java_component(cmd, java_component_name, environment_name, resource_group_name, target_java_component_type, configuration, service_bindings, unbind_service_bindings, min_replicas, max_replicas, no_wait, route_yaml=None):
raw_parameters = locals()
java_component_decorator = JavaComponentDecorator(
cmd=cmd,
Expand Down Expand Up @@ -2416,6 +2416,22 @@ def delete_admin_for_spring(cmd, java_component_name, environment_name, resource
return delete_java_component(cmd, java_component_name, environment_name, resource_group_name, JAVA_COMPONENT_ADMIN, no_wait)


def create_gateway_for_spring(cmd, java_component_name, environment_name, resource_group_name, configuration=None, min_replicas=1, max_replicas=1, no_wait=False, route_yaml=None):
return create_java_component(cmd, java_component_name, environment_name, resource_group_name, JAVA_COMPONENT_GATEWAY, configuration, None, None, min_replicas, max_replicas, no_wait, route_yaml)


def update_gateway_for_spring(cmd, java_component_name, environment_name, resource_group_name, configuration=None, min_replicas=None, max_replicas=None, no_wait=False, route_yaml=None):
return update_java_component(cmd, java_component_name, environment_name, resource_group_name, JAVA_COMPONENT_GATEWAY, configuration, None, None, min_replicas, max_replicas, no_wait, route_yaml)


def show_gateway_for_spring(cmd, java_component_name, environment_name, resource_group_name):
return show_java_component(cmd, java_component_name, environment_name, resource_group_name, JAVA_COMPONENT_GATEWAY)


def delete_gateway_for_spring(cmd, java_component_name, environment_name, resource_group_name, no_wait=False):
return delete_java_component(cmd, java_component_name, environment_name, resource_group_name, JAVA_COMPONENT_GATEWAY, no_wait)


def set_environment_telemetry_data_dog(cmd,
name,
resource_group_name,
Expand Down
51 changes: 50 additions & 1 deletion src/containerapp/azext_containerapp/java_component_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from azure.cli.core.commands import AzCliCommand
from azure.cli.core.azclierror import ValidationError, CLIInternalError
from azure.cli.command_modules.containerapp.base_resource import BaseResource
from azure.cli.command_modules.containerapp._decorator_utils import load_yaml_file
from azure.cli.core.commands.client_factory import get_subscription_id

from ._constants import CONTAINER_APPS_RP, MANAGED_ENVIRONMENT_RESOURCE_TYPE
Expand Down Expand Up @@ -61,10 +62,14 @@ def get_argument_min_replicas(self):
def get_argument_max_replicas(self):
return self.get_param("max_replicas")

def get_argument_route_yaml(self):
return self.get_param("route_yaml")

def construct_payload(self):
self.java_component_def["properties"]["componentType"] = self.get_argument_target_java_component_type()
self.set_up_service_bindings()
self.set_up_unbind_service_bindings()
self.set_up_gateway_route()
if self.get_argument_min_replicas() is not None and self.get_argument_max_replicas() is not None:
self.java_component_def["properties"]["scale"] = {
"minReplicas": self.get_argument_min_replicas(),
Expand Down Expand Up @@ -149,7 +154,7 @@ def set_up_service_bindings(self):
self.java_component_def["properties"]["serviceBinds"].append(update_item)

def set_up_unbind_service_bindings(self):
if self.get_argument_unbind_service_bindings():
if self.get_argument_unbind_service_bindings() is not None:
new_template = self.java_component_def.setdefault("properties", {})
existing_template = self.java_component_def["properties"]

Expand All @@ -165,3 +170,47 @@ def set_up_unbind_service_bindings(self):
if item in service_bindings_dict:
new_template["serviceBinds"] = [binding for binding in new_template["serviceBinds"] if
binding["name"] != item]

def set_up_gateway_route(self):
if self.get_argument_route_yaml() is not None:
self.java_component_def["properties"]["springCloudGatewayRoutes"] = self.process_loaded_scg_route()

def process_loaded_scg_route(self):
yaml_scg_routes = load_yaml_file(self.get_argument_route_yaml())

# Check if the loaded YAML is a dictionary
if not isinstance(yaml_scg_routes, dict):
raise ValidationError('Invalid YAML provided. Please see https://aka.ms/gateway-for-spring-routes-yaml for a valid Gateway for Spring routes YAML spec.')

# Ensure that 'springCloudGatewayRoutes' is present and is a list (can be empty)
routes = yaml_scg_routes.get('springCloudGatewayRoutes')
if routes is None:
return []

if not isinstance(routes, list):
raise ValidationError('The "springCloudGatewayRoutes" field must be a list. Please see https://aka.ms/gateway-for-spring-routes-yaml for a valid Gateway for Spring routes YAML spec.')

# Loop through each route and validate the required fields
for route in routes:
if not isinstance(route, dict):
raise ValidationError('Each route must be a dictionary. Please see https://aka.ms/gateway-for-spring-routes-yaml for a valid Gateway for Spring routes YAML spec.')

# Ensure each route has 'id' and 'uri' fields
if 'id' not in route or not route['id']:
raise ValidationError(f'Route is missing required "id" field: {route} Please see https://aka.ms/gateway-for-spring-routes-yaml for a valid Gateway for Spring routes YAML spec.')

if 'uri' not in route or not route['uri']:
raise ValidationError(f'Route is missing required "uri" field: {route} Please see https://aka.ms/gateway-for-spring-routes-yaml for a valid Gateway for Spring routes YAML spec.')

# Ensure predicates and filters are lists; set to empty lists if not provided
if 'predicates' not in route:
route['predicates'] = []
elif not isinstance(route['predicates'], list):
raise ValidationError(f'The "predicates" field must be a list in route {route["id"]}. Please see https://aka.ms/gateway-for-spring-routes-yaml for a valid Gateway for Spring routes YAML spec.')

if 'filters' not in route:
route['filters'] = []
elif not isinstance(route['filters'], list):
raise ValidationError(f'The "filters" field must be a list in route {route["id"]}. Please see https://aka.ms/gateway-for-spring-routes-yaml for a valid Gateway for Spring routes YAML spec.')
Comment on lines +181 to +214
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we put these check in the RP? If you put them in CLI, if schema in the RP change, it will break CLI.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sync with rp coder, we do not have such kind of check in RP, the check is still necessary for a short time period.
And from my point of view, if the schema in RP changes, it will result in either a new API version or a breaking change. In the first case, the old CLI package should still work with old api versions. However, if it's a breaking change, seems in any case we'll still need to update the CLI.


return yaml_scg_routes.get('springCloudGatewayRoutes')
Loading