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" extension and deprecate "Spring-Cloud" extension #4770

Merged
merged 13 commits into from
May 16, 2022
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,4 @@

/src/scvmm/ @nascarsayan

/src/spring/ @yuwzho
5 changes: 5 additions & 0 deletions src/service_name.json
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,11 @@
"AzureServiceName": "Azure Spring Cloud",
"URL": "https://docs.microsoft.com/azure/spring-cloud/"
},
{
"Command": "az spring",
"AzureServiceName": "Azure Spring Apps",
"URL": "https://docs.microsoft.com/azure/spring-cloud/"
},
{
"Command": "az sql",
"AzureServiceName": "Azure SQL",
Expand Down
6 changes: 6 additions & 0 deletions src/spring-cloud/HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
Release History
===============
3.1.6
---
* The spring-cloud command group has been deprecated and will be removed in Nov. 2022.
We recommend that you upgrade to the new 'spring' command group by installing the 'spring' extension: run `az extension add -n spring`.
For more information, please visit: https://aka.ms/azure-spring-cloud-rename.

3.1.5
---
* [BREAKING CHANGE] The argument '--build-env' accepts key[=value] instead of json.
Expand Down
3 changes: 2 additions & 1 deletion src/spring-cloud/azext_spring_cloud/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,8 @@ def prepare_logs_argument(c):

for scope in ['spring-cloud app create', 'spring-cloud app update']:
with self.argument_context(scope) as c:
c.argument('enable_persistent_storage', arg_type=get_three_state_flag(),
c.argument('enable_persistent_storage',
arg_type=get_three_state_flag(),
help='If true, mount a 50G (Standard Pricing tier) or 1G (Basic Pricing tier) disk with default path.')

for scope in ['spring-cloud app update', 'spring-cloud app deployment create', 'spring-cloud app deploy', 'spring-cloud app create']:
Expand Down
82 changes: 79 additions & 3 deletions src/spring-cloud/azext_spring_cloud/commands.py

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src/spring/HISTORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Release History
===============
1.0.0
---
* Initialize extension "Spring" to manage Azure Spring Apps resources.
21 changes: 21 additions & 0 deletions src/spring/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
Microsoft Azure CLI 'spring' Extension
==========================================

This package is for the 'spring' extension.
i.e. 'az spring'

### How to use ###
Install this extension using the below CLI command
```
az extension add --name spring
```

### Sample Commands ###
Create a service and not wait
```
az spring create -n <service name> --no-wait
```
Create a green deployment with default configuration
```
az spring app deployment create --app <app name> -n <deployment name> --jar-path <jar path>
```
33 changes: 33 additions & 0 deletions src/spring/README_CONTAINER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Added the following arguments:
```
--container-args : The arguments of the container image.
--container-command : The command of the container image.
--container-image : The container image tag.
--container-registry : The registry of the container image. Default: docker.io.
--registry-password : The password of the container registry.
--registry-username : The username of the container registry.
```

Use `az spring app deploy`

Deploy a container image on Docker Hub to an app.
```
az spring app deploy -n MyApp -s MyCluster -g MyResourceGroup --container-image contoso/your-app:v1
```

Deploy a container image on a private registry to an app.
```
az spring app deploy -n MyApp -s MyCluster -g MyResourceGroup --container-image contoso/your-app:v1 --container-registry myacr.azurecr.io --registry-username <username> --registry-password <password>
```

Or `az spring app deployment create`

Deploy a container image on Docker Hub to an app.
```
az spring app deployment create -n green-deployment --app MyApp -s MyCluster -g MyResourceGroup --container-image contoso/your-app:v1
```

Deploy a container image on a private registry to an app.
```
az spring app deployment create -n green-deployment --app MyApp -s MyCluster -g MyResourceGroup --container-image contoso/your-app:v1 --container-registry myacr.azurecr.io --registry-username <username> --registry-password <password>
```
31 changes: 31 additions & 0 deletions src/spring/azext_spring/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# --------------------------------------------------------------------------------------------
# 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.commands import CliCommandType
from azext_spring._help import helps # pylint: disable=unused-import
from azext_spring._client_factory import cf_spring
from azext_spring.commands import load_command_table
from azext_spring._params import load_arguments


class springCommandsLoader(AzCommandsLoader):

def __init__(self, cli_ctx=None):
spring_custom = CliCommandType(
operations_tmpl='azext_spring.custom#{}',
client_factory=cf_spring)
super(springCommandsLoader, self).__init__(cli_ctx=cli_ctx, custom_command_type=spring_custom)

def load_command_table(self, args):
load_command_table(self, args)
return self.command_table

def load_arguments(self, command):
load_arguments(self, command)


COMMAND_LOADER_CLS = springCommandsLoader
138 changes: 138 additions & 0 deletions src/spring/azext_spring/_app_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

# pylint: disable=wrong-import-order
from azure.cli.core.azclierror import FileOperationError, InvalidArgumentValueError
from .vendored_sdks.appplatform.v2022_01_01_preview import models
from .vendored_sdks.appplatform.v2022_03_01_preview import models as models_20220301preview
from azure.cli.core.util import get_file_json


class DefaultApp:
def format_resource(self, **kwargs):
return models.AppResource(
properties=self._format_properties(**kwargs),
identity=self._format_identity(**kwargs)
)

def _format_properties(self, **kwargs):
kwargs['custom_persistent_disks'] = self._load_custom_persistent_disks(**kwargs)
kwargs['loaded_certificates'] = self._load_public_certificate_file(**kwargs)
kwargs['persistent_disk'] = self._load_persistent_disk(**kwargs)
kwargs['temporary_disk'] = self._load_temp_disk(**kwargs)
return models.AppResourceProperties(**kwargs)

def _format_identity(self, system_assigned=None, user_assigned=None, **_):
target_identity_type = self._get_identity_assign_type(system_assigned, user_assigned)
user_identity_payload = self._get_user_identity_payload(user_assigned)
identity_props = None
if target_identity_type != models_20220301preview.ManagedIdentityType.NONE:
identity_props = models_20220301preview.ManagedIdentityProperties()
identity_props.type = target_identity_type
identity_props.user_assigned_identities = user_identity_payload
return identity_props

def _get_identity_assign_type(self, system_assigned=None, user_assigned=None):
target_identity_type = models_20220301preview.ManagedIdentityType.NONE
if system_assigned and user_assigned:
target_identity_type = models_20220301preview.ManagedIdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED
elif system_assigned:
target_identity_type = models_20220301preview.ManagedIdentityType.SYSTEM_ASSIGNED
elif user_assigned:
target_identity_type = models_20220301preview.ManagedIdentityType.USER_ASSIGNED
return target_identity_type

def _get_user_identity_payload(self, user_assigned=None):
if not user_assigned:
return None
user_identity_payload = {}
for user_identity_resource_id in user_assigned:
user_identity_payload[user_identity_resource_id] = models_20220301preview.UserAssignedManagedIdentity()
if len(user_identity_payload) == 0:
user_identity_payload = None
return user_identity_payload

def _load_temp_disk(self, enable_temporary_disk=None, **_):
if enable_temporary_disk is not None:
return models.TemporaryDisk(
size_in_gb=5, mount_path="/tmp"
)

def _load_persistent_disk(self, enable_persistent_storage=None, **_):
if enable_persistent_storage is not None: # False matters
return models.PersistentDisk(
size_in_gb=self._get_persistent_disk_size(enable_persistent_storage),
mount_path='/persistent'
)

def _get_persistent_disk_size(self, enable_persistent_storage):
return 50 if enable_persistent_storage else 0

def _load_public_certificate_file(self, client, resource_group, service, loaded_public_certificate_file=None, **_):
if not loaded_public_certificate_file:
return
data = get_file_json(loaded_public_certificate_file)
if not data:
return
if not data.get('loadedCertificates'):
raise FileOperationError("loadedCertificates must be provided in the json file")
loaded_certificates = []
for item in data['loadedCertificates']:
if not item.get('certificateName') or not item.get('loadTrustStore'):
raise FileOperationError("certificateName, loadTrustStore must be provided in the json file")
certificate_resource = client.certificates.get(resource_group, service, item['certificateName'])
loaded_certificates.append(models.LoadedCertificate(resource_id=certificate_resource.id,
load_trust_store=item['loadTrustStore']))
return loaded_certificates

def _load_custom_persistent_disks(self, client, resource_group, service, persistent_storage=None, **_):
if not persistent_storage:
return
data = get_file_json(persistent_storage, throw_on_empty=False)
if not data:
return
custom_persistent_disks = []

if not data.get('customPersistentDisks'):
raise InvalidArgumentValueError("CustomPersistentDisks must be provided in the json file")
for item in data['customPersistentDisks']:
invalidProperties = not item.get('storageName') or \
not item.get('customPersistentDiskProperties').get('type') or \
not item.get('customPersistentDiskProperties').get('shareName') or \
not item.get('customPersistentDiskProperties').get('mountPath')
if invalidProperties:
raise InvalidArgumentValueError("StorageName, Type, ShareName, MountPath mast be provided in the json file")
storage_resource = client.storages.get(resource_group, service, item['storageName'])
custom_persistent_disk_properties = models.AzureFileVolume(
type=item['customPersistentDiskProperties']['type'],
share_name=item['customPersistentDiskProperties']['shareName'],
mount_path=item['customPersistentDiskProperties']['mountPath'],
mount_options=item['customPersistentDiskProperties']['mountOptions'] if 'mountOptions' in item['customPersistentDiskProperties'] else None,
read_only=item['customPersistentDiskProperties']['readOnly'] if 'readOnly' in item['customPersistentDiskProperties'] else None)

custom_persistent_disks.append(
models.CustomPersistentDiskResource(
storage_id=storage_resource.id,
custom_persistent_disk_properties=custom_persistent_disk_properties))
return custom_persistent_disks


class BasicTierApp(DefaultApp):
def _get_persistent_disk_size(self, enable_persistent_storage, **_):
return 1 if enable_persistent_storage else 0


class EnterpriseTierApp(DefaultApp):
def _get_persistent_disk_size(self, enable_persistent_storage, **_):
if enable_persistent_storage:
raise InvalidArgumentValueError('Enterprise tier Spring instance does not support --enable-persistent-storage')


def app_selector(sku, **_):
if sku.name == 'E0':
return EnterpriseTierApp()
if sku.name == 'B0':
return BasicTierApp()
return DefaultApp()
Loading