Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
5 changes: 5 additions & 0 deletions src/spring/HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
Release History
===============
1.2.0
---
* Add command `az spring list-marketplace-plan` to list all supported VMware product. For more detail, see https://learn.microsoft.com/en-us/azure/spring-apps/how-to-enterprise-marketplace-offer.
* Add argument `marketplace-plan-id` in command `az spring create` to support purchasing different VMware product plan when creating Enterprise sku Spring resource.
Comment thread
zhoxing-ms marked this conversation as resolved.
Outdated

1.1.14
---
* Add warn when update Config Server or Application Configuration Service with SSH auth.
Expand Down
8 changes: 8 additions & 0 deletions src/spring/azext_spring/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@
az spring create -n MyService -g MyResourceGroup --sku Enterprise --enable-application-configuration-service --enable-service-registry --enable-gateway --enable-api-portal
"""

helps['spring list-marketplace-plan'] = """
type: command
short-summary: (Enterprise Tier Only) List Marketplace plan to be purchased.
examples:
- name: List all plans.
text: az spring list-marketplace-plan -o table
"""

helps['spring update'] = """
type: command
short-summary: Update an Azure Spring Apps.
Expand Down
41 changes: 41 additions & 0 deletions src/spring/azext_spring/_marketplace.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.
# --------------------------------------------------------------------------------------------

# pylint: disable=wrong-import-order
# pylint: disable=unused-argument, logging-format-interpolation, protected-access, wrong-import-order, too-many-lines
from knack.log import get_logger
from ._constant import (MARKETPLACE_OFFER_ID, MARKETPLACE_PUBLISHER_ID)

logger = get_logger(__name__)


def _spring_list_marketplace_plan(cmd, client):
# return get_mgmt_service_client(cli_ctx, AppPlatformManagementClient_20220501preview)
from azure.cli.core.commands.client_factory import get_mgmt_service_client
from .vendored_sdks.marketplace.v2018_08_01_beta import MarketplaceRPService
from .vendored_sdks.marketplace.v2018_08_01_beta.models import Offer

logger.warning('To view the Azure Spring Apps Enterprise tier offering and read a detailed description, see https://aka.ms/ascmpoffer')
client = get_mgmt_service_client(cmd.cli_ctx, MarketplaceRPService)
offer = client.offer.get('{}.{}'.format(MARKETPLACE_PUBLISHER_ID, MARKETPLACE_OFFER_ID))
offer.plans = [x for x in offer.plans if _is_valid_plan(x)]
return Offer.deserialize(offer).serialize(offer)


def _is_valid_plan(plan):
return plan.availabilities


def transform_marketplace_plan_output(result):
def _table_item_view(plan):
return {
'publisher id': result['properties']['publisherId'],
'product id': result['properties']['offerId'],
'plan id': plan['planId'],
'plan display name': plan['displayName']
}

plans = result['properties']['plans']
return [_table_item_view(plan) for plan in plans]
4 changes: 4 additions & 0 deletions src/spring/azext_spring/_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ def load_arguments(self, _):
validator=validate_api_portal_instance_count,
options_list=['--api-portal-instance-count', '--ap-instance'],
help='(Enterprise Tier Only) Number of API portal instances.')
c.argument('marketplace_plan_id',
is_preview=True,
help='(Enterprise Tier Only) Specify a different Marketplace plan to purchase with Spring instance. '
'List all plans by running `az spring list-marketplace-plan -o table`.')

with self.argument_context('spring update') as c:
c.argument('sku', arg_type=sku_type, validator=normalize_sku)
Expand Down
11 changes: 9 additions & 2 deletions src/spring/azext_spring/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def validate_sku(cmd, namespace):
_validate_saas_provider(cmd, namespace)
_validate_terms(cmd, namespace)
else:
_check_saas_not_set(cmd, namespace)
_check_tanzu_components_not_enable(cmd, namespace)
normalize_sku(cmd, namespace)

Expand All @@ -59,6 +60,11 @@ def normalize_sku(cmd, namespace):
namespace.sku = models.Sku(name=_get_sku_name(namespace.sku), tier=namespace.sku)


def _check_saas_not_set(cmd, namespace):
if namespace.marketplace_plan_id:
raise InvalidArgumentValueError('--marketplace-plan-id is supported only when --sku=Enterprise')


def _validate_saas_provider(cmd, namespace):
from azure.cli.core.commands.client_factory import get_mgmt_service_client
from azure.cli.core.profiles import ResourceType
Expand All @@ -72,17 +78,18 @@ def _validate_terms(cmd, namespace):
from azure.mgmt.marketplaceordering import MarketplaceOrderingAgreements
from azure.cli.core.commands.client_factory import get_mgmt_service_client
client = get_mgmt_service_client(cmd.cli_ctx, MarketplaceOrderingAgreements).marketplace_agreements
plan_id = namespace.marketplace_plan_id or MARKETPLACE_PLAN_ID
term = client.get(offer_type="virtualmachine",
publisher_id=MARKETPLACE_PUBLISHER_ID,
offer_id=MARKETPLACE_OFFER_ID,
plan_id=MARKETPLACE_PLAN_ID)
plan_id=plan_id)
if not term.accepted:
raise InvalidArgumentValueError('Terms for Azure Spring Apps Enterprise is not accepted.\n'
'Run "az term accept --publisher {} '
'--product {} '
'--plan {}" to accept the term.'.format(MARKETPLACE_PUBLISHER_ID,
MARKETPLACE_OFFER_ID,
MARKETPLACE_PLAN_ID))
plan_id))


def _check_tanzu_components_not_enable(cmd, namespace):
Expand Down
4 changes: 4 additions & 0 deletions src/spring/azext_spring/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
transform_spring_cloud_gateway_output,
transform_api_portal_output)
from ._validators import validate_app_insights_command_not_supported_tier
from ._marketplace import (transform_marketplace_plan_output)
from ._validators_enterprise import (validate_gateway_update, validate_api_portal_update)
from ._app_managed_identity_validator import (validate_app_identity_remove_or_warning,
validate_app_identity_assign_or_warning)
Expand Down Expand Up @@ -93,6 +94,9 @@ def load_command_table(self, _):
with self.command_group('spring', custom_command_type=spring_routing_util,
exception_handler=handle_asc_exception) as g:
g.custom_command('create', 'spring_create', supports_no_wait=True)
g.custom_command('list-marketplace-plan', 'spring_list_marketplace_plan',
is_preview=True,
table_transformer=transform_marketplace_plan_output)

with self.command_group('spring', client_factory=cf_spring_20220501preview,
exception_handler=handle_asc_exception) as g:
Expand Down
16 changes: 16 additions & 0 deletions src/spring/azext_spring/spring_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
from ._validators import (_parse_sku_name, validate_instance_not_existed)
from azure.cli.core.commands import LongRunningOperation
from knack.log import get_logger
from ._marketplace import _spring_list_marketplace_plan
from ._constant import (MARKETPLACE_OFFER_ID, MARKETPLACE_PUBLISHER_ID)

logger = get_logger(__name__)

Expand Down Expand Up @@ -62,6 +64,7 @@ def create_service(self,
sku=None,
tags=None,
ingress_read_timeout=None,
marketplace_plan_id=None,
**_):
properties = models.ClusterResourceProperties(
zone_redundant=zone_redundant
Expand All @@ -74,6 +77,13 @@ def create_service(self,
else:
properties.vnet_addons = None

if marketplace_plan_id:
properties.marketplace_resource = models.MarketplaceResource(
plan=marketplace_plan_id,
product=MARKETPLACE_OFFER_ID,
publisher=MARKETPLACE_PUBLISHER_ID
)

if service_runtime_subnet or app_subnet or reserved_cidr_range:
properties.network_profile = models.NetworkProfile(
service_runtime_subnet_id=service_runtime_subnet,
Expand Down Expand Up @@ -153,6 +163,7 @@ def spring_create(cmd, client, resource_group, name,
api_portal_instance_count=None,
enable_log_stream_public_endpoint=None,
ingress_read_timeout=None,
marketplace_plan_id=None,
no_wait=False):
"""
Because Standard/Basic tier vs. Enterprise tier creation are very different. Here routes the command to different
Expand Down Expand Up @@ -183,6 +194,7 @@ def spring_create(cmd, client, resource_group, name,
'enable_api_portal': enable_api_portal,
'api_portal_instance_count': api_portal_instance_count,
'enable_log_stream_public_endpoint': enable_log_stream_public_endpoint,
'marketplace_plan_id': marketplace_plan_id,
'no_wait': no_wait
}

Expand All @@ -198,3 +210,7 @@ def _enable_app_insights(cmd, client, resource_group, name, location, app_insigh
return create_default_buildpack_binding_for_application_insights(cmd, client, resource_group, name,
location, app_insights_key, app_insights,
sampling_rate)


def spring_list_marketplace_plan(cmd, client):
return _spring_list_marketplace_plan(cmd, client)
11 changes: 11 additions & 0 deletions src/spring/azext_spring/tests/latest/test_asa_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ def test_asc_create_enterprise(self):
self.assertEqual('E0', resource.sku.name)
self.assertEqual('Enterprise', resource.sku.tier)
self.assertEqual(False, resource.properties.zone_redundant)
self.assertIsNone(resource.properties.marketplace_resource)

def test_asc_create_enterprise_with_plan(self):
self._execute('rg', 'asc', sku=self._get_sku('Enterprise'), disable_app_insights=True, marketplace_plan_id='my-plan')
resource = self.created_resource
self.assertEqual('E0', resource.sku.name)
self.assertEqual('Enterprise', resource.sku.tier)
self.assertEqual(False, resource.properties.zone_redundant)
self.assertEqual('my-plan', resource.properties.marketplace_resource.plan)
self.assertEqual('azure-spring-cloud-vmware-tanzu-2', resource.properties.marketplace_resource.product)
self.assertEqual('vmware-inc', resource.properties.marketplace_resource.publisher)


class TestSpringCloudCreateWithAI(BasicTest):
Expand Down
6 changes: 3 additions & 3 deletions src/spring/azext_spring/tests/latest/test_asa_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,20 +308,20 @@ def _mock_not_registered_client(cli_ctx, client_type, **kwargs):
class TestSkuValidator(unittest.TestCase):
@mock.patch('azure.cli.core.commands.client_factory.get_mgmt_service_client', _mock_happy_client)
def test_happy_path(self):
ns = Namespace(sku='Enterprise')
ns = Namespace(sku='Enterprise', marketplace_plan_id=None)
validate_sku(_get_test_cmd(), ns)
self.assertEqual('Enterprise', ns.sku.tier)

@mock.patch('azure.cli.core.commands.client_factory.get_mgmt_service_client', _mock_not_accepted_term_client)
def test_term_not_accept(self):
ns = Namespace(sku='Enterprise')
ns = Namespace(sku='Enterprise', marketplace_plan_id=None)
with self.assertRaises(InvalidArgumentValueError) as context:
validate_sku(_get_test_cmd(), ns)
self.assertTrue('Terms for Azure Spring Apps Enterprise is not accepted.' in str(context.exception))

@mock.patch('azure.cli.core.commands.client_factory.get_mgmt_service_client', _mock_not_registered_client)
def test_provider_not_registered(self):
ns = Namespace(sku='Enterprise')
ns = Namespace(sku='Enterprise', marketplace_plan_id=None)
with self.assertRaises(InvalidArgumentValueError) as context:
validate_sku(_get_test_cmd(), ns)
self.assertTrue('Microsoft.SaaS resource provider is not registered.' in str(context.exception))
Expand Down
16 changes: 16 additions & 0 deletions src/spring/azext_spring/vendored_sdks/marketplace/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# 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.
# Code generated by Microsoft (R) AutoRest Code Generator.
# Changes may cause incorrect behavior and will be lost if the code is regenerated.
# --------------------------------------------------------------------------

from ._marketplace_rp_service import MarketplaceRPService
__all__ = ['MarketplaceRPService']

try:
from ._patch import patch_sdk # type: ignore
patch_sdk()
except ImportError:
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 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.
#
# Code generated by Microsoft (R) AutoRest Code Generator.
# Changes may cause incorrect behavior and will be lost if the code is
# regenerated.
# --------------------------------------------------------------------------
from typing import TYPE_CHECKING

from azure.core.configuration import Configuration
from azure.core.pipeline import policies
from azure.mgmt.core.policies import ARMChallengeAuthenticationPolicy, ARMHttpLoggingPolicy

from ._version import VERSION

if TYPE_CHECKING:
# pylint: disable=unused-import,ungrouped-imports
from typing import Any, Optional

from azure.core.credentials import TokenCredential

class MarketplaceRPServiceConfiguration(Configuration):
"""Configuration for MarketplaceRPService.

Note that all parameters used to create this instance are saved as instance
attributes.

:param credential: Credential needed for the client to connect to Azure.
:type credential: ~azure.core.credentials.TokenCredential
:param market: The Market to use for the request. Default value is "US".
:type market: str
:param include_stop_sold_plans: The Market to use for the request. Default value is "true".
:type include_stop_sold_plans: str
"""

def __init__(
self,
credential, # type: "TokenCredential"
market="US", # type: Optional[str]
include_stop_sold_plans="true", # type: Optional[str]
**kwargs # type: Any
):
# type: (...) -> None
if credential is None:
raise ValueError("Parameter 'credential' must not be None.")
super(MarketplaceRPServiceConfiguration, self).__init__(**kwargs)

self.credential = credential
self.market = market
self.include_stop_sold_plans = include_stop_sold_plans
self.credential_scopes = kwargs.pop('credential_scopes', ['https://management.azure.com/.default'])
kwargs.setdefault('sdk_moniker', 'azure-mgmt-marketplace/{}'.format(VERSION))
self._configure(**kwargs)

def _configure(
self,
**kwargs # type: Any
):
# type: (...) -> None
self.user_agent_policy = kwargs.get('user_agent_policy') or policies.UserAgentPolicy(**kwargs)
self.headers_policy = kwargs.get('headers_policy') or policies.HeadersPolicy(**kwargs)
self.proxy_policy = kwargs.get('proxy_policy') or policies.ProxyPolicy(**kwargs)
self.logging_policy = kwargs.get('logging_policy') or policies.NetworkTraceLoggingPolicy(**kwargs)
self.http_logging_policy = kwargs.get('http_logging_policy') or ARMHttpLoggingPolicy(**kwargs)
self.retry_policy = kwargs.get('retry_policy') or policies.RetryPolicy(**kwargs)
self.custom_hook_policy = kwargs.get('custom_hook_policy') or policies.CustomHookPolicy(**kwargs)
self.redirect_policy = kwargs.get('redirect_policy') or policies.RedirectPolicy(**kwargs)
self.authentication_policy = kwargs.get('authentication_policy')
if self.credential and not self.authentication_policy:
self.authentication_policy = ARMChallengeAuthenticationPolicy(self.credential, *self.credential_scopes, **kwargs)
Loading