From 828f9d3321effe01fbe74b754a9a1f61fab65f71 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Wed, 10 Mar 2021 13:25:37 -0800 Subject: [PATCH 01/86] Create pull.yml --- .github/pull.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/pull.yml diff --git a/.github/pull.yml b/.github/pull.yml new file mode 100644 index 00000000000..7201bb6d18b --- /dev/null +++ b/.github/pull.yml @@ -0,0 +1,12 @@ +version: "1" +rules: # Array of rules + - base: master # Required. Target branch + upstream: wei:master # Required. Must be in the same fork network. + mergeMethod: hardreset # Optional, one of [none, merge, squash, rebase, hardreset], Default: none. + mergeUnstable: false # Optional, merge pull request even when the mergeable_state is not clean. Default: false + - base: k8s-configuration + upstream: master # Required. Can be a branch in the same forked repo. + - base: k8s-extension/private-preview + upstream: master # Required. Can be a branch in the same forked repo. + - base: k8s-extension/public-preview + upstream: master # Required. Can be a branch in the same forked repo. From 1436bfc8859e33ea69b06e0b746cbd9ef5fe7825 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Wed, 10 Mar 2021 14:00:34 -0800 Subject: [PATCH 02/86] Update pull.yml --- .github/pull.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/pull.yml b/.github/pull.yml index 7201bb6d18b..8f65923a382 100644 --- a/.github/pull.yml +++ b/.github/pull.yml @@ -10,3 +10,5 @@ rules: # Array of rules upstream: master # Required. Can be a branch in the same forked repo. - base: k8s-extension/public-preview upstream: master # Required. Can be a branch in the same forked repo. + - base: release + upstream: master # Required. Can be a branch in the same forked repo. From 6cffd965b4061d66bad7e637a4a55bb93c9440d1 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Wed, 10 Mar 2021 14:15:10 -0800 Subject: [PATCH 03/86] Update azure-pipelines.yml --- azure-pipelines.yml | 405 ++++++++++++++++++++++++++++---------------- 1 file changed, 259 insertions(+), 146 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d43ef5102f0..77ff718d967 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,128 +1,157 @@ resources: -- repo: self + repositories: + - repository: K8sPartnerExtensionTest + type: git + endpoint: AzureReposConnection + name: One/compute-HybridMgmt-K8sPartnerExtensionTest trigger: batch: true branches: include: - - '*' - + - k8s-extension/public-preview + - k8s-extension/private-preview pr: branches: include: - - '*' - -jobs: -- job: CredScan - displayName: "Credential Scan" - pool: - vmImage: "windows-2019" - steps: - - task: ms-codeanalysis.vss-microsoft-security-code-analysis-devops.build-task-credscan.CredScan@2 - displayName: 'Run Credential Scanner' - inputs: - toolMajorVersion: V2 - suppressionsFile: './scripts/ci/credscan/CredScanSuppressions.json' - - task: ms-codeanalysis.vss-microsoft-security-code-analysis-devops.build-task-postanalysis.PostAnalysis@1 - displayName: 'Post Analysis' - inputs: - AllTools: false - BinSkim: false - CredScan: true - RoslynAnalyzers: false - TSLint: false - ToolLogsNotFoundAction: 'Standard' - -- job: CheckLicenseHeader - displayName: "Check License" - pool: - vmImage: 'ubuntu-16.04' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.6' - inputs: - versionSpec: 3.6 - - bash: | - set -ev - - # prepare and activate virtualenv - python -m venv env/ - - chmod +x ./env/bin/activate - source ./env/bin/activate - - # clone azure-cli - git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli - - pip install -q azdev - - azdev setup -c ../azure-cli -r ./ - - azdev --version - az --version - - azdev verify license - -- job: StaticAnalysis - displayName: "Static Analysis" - pool: - vmImage: 'ubuntu-16.04' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.6' - inputs: - versionSpec: 3.6 - - bash: pip install wheel==0.30.0 pylint==1.9.5 flake8==3.5.0 requests - displayName: 'Install wheel, pylint, flake8, requests' - - bash: python scripts/ci/source_code_static_analysis.py - displayName: "Static Analysis" - -- job: IndexVerify - displayName: "Verify Extensions Index" - pool: - vmImage: 'ubuntu-16.04' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.7' - inputs: - versionSpec: 3.7 - - bash: | - #!/usr/bin/env bash - set -ev - pip install wheel==0.30.0 requests packaging - export CI="ADO" - python ./scripts/ci/test_index.py -v - displayName: "Verify Extensions Index" + - k8s-extension/public-preview + - k8s-extension/private-preview -- job: SourceTests - displayName: "Integration Tests, Build Tests" - pool: - vmImage: 'ubuntu-16.04' - strategy: - matrix: - Python36: - python.version: '3.6' - Python38: - python.version: '3.8' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python $(python.version)' - inputs: - versionSpec: '$(python.version)' - - bash: pip install wheel==0.30.0 - displayName: 'Install wheel==0.30.0' - - bash: ./scripts/ci/test_source.sh - displayName: 'Run integration test and build test' - env: - ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) - ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) - -- job: LintModifiedExtensions - displayName: "CLI Linter on Modified Extensions" - condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest')) - pool: - vmImage: 'ubuntu-16.04' - steps: +stages: +- stage: K8sExtensionTestSuite + displayName: "K8s-Extension Test Suite" + variables: + K8S_EXTENSION_REPO_PATH: $(Agent.BuildDirectory)/s/compute-HybridMgmt-K8sPartnerExtensionTest + CLI_REPO_PATH: $(Agent.BuildDirectory)/s/azure-cli-extensions-pr + EXTENSION_NAME: "k8s-extension" + EXTENSION_FILE_NAME: "k8s_extension" + SUBSCRIPTION_ID: "15c06b1b-01d6-407b-bb21-740b8617dea3" + RESOURCE_GROUP: "K8sPartnerExtensionTest" + BASE_CLUSTER_NAME: "k8s-extension-cluster" + jobs: + - job: K8sExtensionTestSuite + displayName: "Run the Test Suite" + pool: + vmImage: 'ubuntu-16.04' + steps: + - checkout: self + - checkout: K8sPartnerExtensionTest + + - bash: | + echo "Installing helm3" + curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 + chmod 700 get_helm.sh + ./get_helm.sh + + echo "Installing kubectl" + curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x ./kubectl + sudo mv ./kubectl /usr/local/bin/kubectl + kubectl version --client + displayName: "Setup the VM with helm3 and kubectl" + - task: UsePythonVersion@0 + displayName: 'Use Python 3.6' + inputs: + versionSpec: 3.6 + - bash: | + set -ev + + # prepare and activate virtualenv + pip install virtualenv + python3 -m venv env/ + source env/bin/activate + + # clone azure-cli + pip install azdev + + ls $(CLI_REPO_PATH) + + azdev --version + azdev setup -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) + azdev extension build $(EXTENSION_NAME) + + workingDirectory: $(CLI_REPO_PATH) + displayName: "Setup and Build $(EXTENSION_NAME) with azdev" + + - bash: | + K8S_EXTENSION_VERSION=$(ls ${EXTENSION_FILE_NAME}* | cut -d "-" -f2) + echo "##vso[task.setvariable variable=K8S_EXTENSION_VERSION]$K8S_EXTENSION_VERSION" + cp * $(K8S_EXTENSION_REPO_PATH)/extensions + workingDirectory: $(CLI_REPO_PATH)/dist + displayName: "Copy the Built .whl to Extension Test Path" + + - bash: | + RAND_STR=$RANDOM + AKS_CLUSTER_NAME="${BASE_CLUSTER_NAME}-${RAND_STR}-aks" + ARC_CLUSTER_NAME="${BASE_CLUSTER_NAME}-${RAND_STR}-arc" + + JSON_STRING=$(jq -n \ + --arg SUB_ID "$SUBSCRIPTION_ID" \ + --arg RG "$RESOURCE_GROUP" \ + --arg AKS_CLUSTER_NAME "$AKS_CLUSTER_NAME" \ + --arg ARC_CLUSTER_NAME "$ARC_CLUSTER_NAME" \ + --arg K8S_EXTENSION_VERSION "$K8S_EXTENSION_VERSION" \ + '{subscriptionId: $SUB_ID, resourceGroup: $RG, aksClusterName: $AKS_CLUSTER_NAME, arcClusterName: $ARC_CLUSTER_NAME, extensionVersion: {"k8s-extension": $K8S_EXTENSION_VERSION, connectedk8s: "1.0.0"}}') + echo $JSON_STRING > settings.json + cat settings.json + workingDirectory: $(K8S_EXTENSION_REPO_PATH) + displayName: "Generate a settings.json file" + + - bash : | + echo "Downloading the kind script" + curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.9.0/kind-linux-amd64 + chmod +x ./kind + ./kind create cluster + displayName: "Create and Start the Kind cluster" + + - task: AzureCLI@2 + displayName: Bootstrap + inputs: + azureSubscription: AzureResourceConnection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + .\Bootstrap.ps1 -CI + workingDirectory: $(K8S_EXTENSION_REPO_PATH) + + - task: AzureCLI@2 + displayName: Run the Test Suite + inputs: + azureSubscription: AzureResourceConnection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + .\Test.ps1 -CI + workingDirectory: $(K8S_EXTENSION_REPO_PATH) + continueOnError: true + + - task: PublishTestResults@2 + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '**/TestResults.xml' + failTaskOnFailedTests: true + condition: succeededOrFailed() + + - task: AzureCLI@2 + displayName: Cleanup + inputs: + azureSubscription: AzureResourceConnection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + .\Cleanup.ps1 -CI + workingDirectory: $(K8S_EXTENSION_REPO_PATH) + condition: succeededOrFailed() + +- stage: AzureCLIOfficial + displayName: "Azure Official CLI Code Checks" + dependsOn: [] + jobs: + - job: CheckLicenseHeader + displayName: "Check License" + pool: + vmImage: 'ubuntu-16.04' + steps: - task: UsePythonVersion@0 displayName: 'Use Python 3.6' inputs: @@ -131,41 +160,125 @@ jobs: set -ev # prepare and activate virtualenv - pip install virtualenv - python -m virtualenv venv/ - source ./venv/bin/activate + python -m venv env/ + + chmod +x ./env/bin/activate + source ./env/bin/activate # clone azure-cli - git clone --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli + git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli - pip install azdev + pip install -q azdev + + azdev setup -c ../azure-cli -r ./ azdev --version + az --version - azdev setup -c ../azure-cli -r ./ + azdev verify license + + - job: StaticAnalysis + displayName: "Static Analysis" + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.6' + inputs: + versionSpec: 3.6 + - bash: pip install wheel==0.30.0 pylint==1.9.5 flake8==3.5.0 requests + displayName: 'Install wheel, pylint, flake8, requests' + - bash: python scripts/ci/source_code_static_analysis.py + displayName: "Static Analysis" + + - job: IndexVerify + displayName: "Verify Extensions Index" + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.7' + inputs: + versionSpec: 3.7 + - bash: | + #!/usr/bin/env bash + set -ev + pip install wheel==0.30.0 requests packaging + export CI="ADO" + python ./scripts/ci/test_index.py -v + displayName: "Verify Extensions Index" + + - job: SourceTests + displayName: "Integration Tests, Build Tests" + pool: + vmImage: 'ubuntu-16.04' + strategy: + matrix: + Python36: + python.version: '3.6' + Python38: + python.version: '3.8' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python $(python.version)' + inputs: + versionSpec: '$(python.version)' + - bash: pip install wheel==0.30.0 + displayName: 'Install wheel==0.30.0' + - bash: ./scripts/ci/test_source.sh + displayName: 'Run integration test and build test' + env: + ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) + ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) + + - job: LintModifiedExtensions + displayName: "CLI Linter on Modified Extensions" + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.6' + inputs: + versionSpec: 3.6 + - bash: | + set -ev - # overwrite the default AZURE_EXTENSION_DIR set by ADO - AZURE_EXTENSION_DIR=~/.azure/cliextensions az --version - - AZURE_EXTENSION_DIR=~/.azure/cliextensions python scripts/ci/verify_linter.py - displayName: "CLI Linter on Modified Extension" - env: - ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) - ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) - -- job: IndexRefDocVerify - displayName: "Verify Ref Docs" - pool: - vmImage: 'ubuntu-16.04' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.7' - inputs: - versionSpec: 3.7 - - bash: pip install wheel==0.30.0 - displayName: 'Install wheel==0.30.0' - - task: Bash@3 - displayName: "Verify Extension Ref Docs" - inputs: - targetType: 'filePath' - filePath: scripts/ci/test_index_ref_doc.sh + # prepare and activate virtualenv + pip install virtualenv + python -m virtualenv venv/ + source ./venv/bin/activate + + # clone azure-cli + git clone --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli + + pip install azdev + + azdev --version + + azdev setup -c ../azure-cli -r ./ -e k8s-extension + + # overwrite the default AZURE_EXTENSION_DIR set by ADO + AZURE_EXTENSION_DIR=~/.azure/cliextensions az --version + + AZURE_EXTENSION_DIR=~/.azure/cliextensions azdev linter --include-whl-extensions k8s-extension + displayName: "CLI Linter on Modified Extension" + env: + ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) + ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) + + - job: IndexRefDocVerify + displayName: "Verify Ref Docs" + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.7' + inputs: + versionSpec: 3.7 + - bash: pip install wheel==0.30.0 + displayName: 'Install wheel==0.30.0' + - task: Bash@3 + displayName: "Verify Extension Ref Docs" + inputs: + targetType: 'filePath' + filePath: scripts/ci/test_index_ref_doc.sh From db0f4bd7b188c2aa1388ed9366994e5508061483 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Wed, 10 Mar 2021 14:19:45 -0800 Subject: [PATCH 04/86] Initial commit of k8s-extension --- src/k8s-extension/HISTORY.rst | 83 +++ src/k8s-extension/README.rst | 5 + .../azext_k8s_extension/__init__.py | 32 + .../azext_k8s_extension/_client_factory.py | 31 + .../azext_k8s_extension/_help.py | 38 ++ .../azext_k8s_extension/_params.py | 73 +++ .../azext_k8s_extension/_validators.py | 21 + .../azext_k8s_extension/action.py | 37 ++ .../azext_k8s_extension/azext_metadata.json | 4 + .../azext_k8s_extension/commands.py | 24 + .../azext_k8s_extension/custom.py | 275 ++++++++ .../partner_extensions/AzureDefender.py | 73 +++ .../partner_extensions/ContainerInsights.py | 460 ++++++++++++++ .../partner_extensions/DefaultExtension.py | 62 ++ .../partner_extensions/OpenServiceMesh.py | 95 +++ .../PartnerExtensionModel.py | 19 + .../partner_extensions/__init__.py | 0 .../azext_k8s_extension/tests/__init__.py | 5 + .../tests/latest/__init__.py | 5 + .../latest/recordings/test_k8s_extension.yaml | 270 ++++++++ .../latest/test_k8s_extension_scenario.py | 67 ++ .../vendored_sdks/__init__.py | 19 + .../vendored_sdks/_configuration.py | 49 ++ .../vendored_sdks/_k8s_extension_client.py | 50 ++ .../vendored_sdks/models/__init__.py | 70 +++ .../models/_k8s_extension_client_enums.py | 68 ++ .../vendored_sdks/models/_models.py | 587 ++++++++++++++++++ .../vendored_sdks/models/_models_py3.py | 587 ++++++++++++++++++ .../vendored_sdks/models/_paged_models.py | 40 ++ .../vendored_sdks/operations/__init__.py | 16 + .../operations/_k8s_extensions_operations.py | 452 ++++++++++++++ .../vendored_sdks/version.py | 13 + src/k8s-extension/setup.cfg | 2 + src/k8s-extension/setup.py | 58 ++ 34 files changed, 3690 insertions(+) create mode 100644 src/k8s-extension/HISTORY.rst create mode 100644 src/k8s-extension/README.rst create mode 100644 src/k8s-extension/azext_k8s_extension/__init__.py create mode 100644 src/k8s-extension/azext_k8s_extension/_client_factory.py create mode 100644 src/k8s-extension/azext_k8s_extension/_help.py create mode 100644 src/k8s-extension/azext_k8s_extension/_params.py create mode 100644 src/k8s-extension/azext_k8s_extension/_validators.py create mode 100644 src/k8s-extension/azext_k8s_extension/action.py create mode 100644 src/k8s-extension/azext_k8s_extension/azext_metadata.json create mode 100644 src/k8s-extension/azext_k8s_extension/commands.py create mode 100644 src/k8s-extension/azext_k8s_extension/custom.py create mode 100644 src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py create mode 100644 src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py create mode 100644 src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py create mode 100644 src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py create mode 100644 src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py create mode 100644 src/k8s-extension/azext_k8s_extension/partner_extensions/__init__.py create mode 100644 src/k8s-extension/azext_k8s_extension/tests/__init__.py create mode 100644 src/k8s-extension/azext_k8s_extension/tests/latest/__init__.py create mode 100644 src/k8s-extension/azext_k8s_extension/tests/latest/recordings/test_k8s_extension.yaml create mode 100644 src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/__init__.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/_configuration.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/_k8s_extension_client.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/models/__init__.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_k8s_extension_client_enums.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_paged_models.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/__init__.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_k8s_extensions_operations.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/version.py create mode 100644 src/k8s-extension/setup.cfg create mode 100644 src/k8s-extension/setup.py diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst new file mode 100644 index 00000000000..695f549b1a2 --- /dev/null +++ b/src/k8s-extension/HISTORY.rst @@ -0,0 +1,83 @@ +.. :changelog: + +Release History +=============== + +0.1.0 +++++++++++++++++++ +* Initial release. + +0.1.1 +++++++++++++++++++ +* Add support for microsoft-azure-defender extension type + +0.1.2 +++++++++++++++++++ + +* Add support for Arc Appliance cluster type + +0.1.3 +++++++++++++++++++ + +* Customization for microsoft.openservicemesh + +0.1PP.4 +++++++++++++++++++ + +* Refactor for clear separation of extension-type specific customizations +* Introduce new versioning scheme to allow Preview releases by Partners + +0.1PP.5 +++++++++++++++++++ + +* OpenServiceMesh customization. +* If Version is passed in, accept None for AutoUpgradeMinorVersion, and not require it to be False. + +0.1PP.6 +++++++++++++++++++ + +* OpenServiceMesh customization. +* Scope is always cluster. Version is mandatory for staging and pilot release-trains. + +0.1PP.7 +++++++++++++++++++ + +* Fix clusterType of Microsoft.ResourceConnector resource + +0.1PP.8 +++++++++++++++++++ + +* Update clusterType validation to allow 'appliances' +* Update identity creation to use the appropriate parent resource's type and api-version +* Throw error if cluster type is not one of the 3 supported types + +0.1PP.9 +++++++++++++++++++ + +* Rename azuremonitor-containers extension type to microsoft.azuremonitor.containers + +0.1PP.10 +++++++++++++++++++ + +* Add azuremonitor-containers back with alternative microsoft.azuremonitor.containers + +0.1PP.11 +++++++++++++++++++ + +* Add shorter aliases for long parameter names + +0.1PP.12 +++++++++++++++++++ + +* Remove support for azuremonitor-containers extension type naming + +0.1PP.13 +++++++++++++++++++ + +* Move CLI errors to non-deprecated error types +* Remove support for update + +0.1PP.14 +++++++++++++++++++ + +* Update help text, group CLI arguments diff --git a/src/k8s-extension/README.rst b/src/k8s-extension/README.rst new file mode 100644 index 00000000000..e91e1b13229 --- /dev/null +++ b/src/k8s-extension/README.rst @@ -0,0 +1,5 @@ +Microsoft Azure CLI 'k8s-extension' Extension +============================================= + +This package is for the 'k8s-extension' extension. +i.e. 'az k8s-extension' diff --git a/src/k8s-extension/azext_k8s_extension/__init__.py b/src/k8s-extension/azext_k8s_extension/__init__.py new file mode 100644 index 00000000000..e2301227d45 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/__init__.py @@ -0,0 +1,32 @@ +# -------------------------------------------------------------------------------------------- +# 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 azext_k8s_extension._help import helps # pylint: disable=unused-import + + +class K8sExtensionCommandsLoader(AzCommandsLoader): + + def __init__(self, cli_ctx=None): + from azure.cli.core.commands import CliCommandType + from azext_k8s_extension._client_factory import cf_k8s_extension + k8s_extension_custom = CliCommandType( + operations_tmpl='azext_k8s_extension.custom#{}', + client_factory=cf_k8s_extension) + super(K8sExtensionCommandsLoader, self).__init__(cli_ctx=cli_ctx, + custom_command_type=k8s_extension_custom) + + def load_command_table(self, args): + from azext_k8s_extension.commands import load_command_table + load_command_table(self, args) + return self.command_table + + def load_arguments(self, command): + from azext_k8s_extension._params import load_arguments + load_arguments(self, command) + + +COMMAND_LOADER_CLS = K8sExtensionCommandsLoader diff --git a/src/k8s-extension/azext_k8s_extension/_client_factory.py b/src/k8s-extension/azext_k8s_extension/_client_factory.py new file mode 100644 index 00000000000..a4ec83ee0cb --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/_client_factory.py @@ -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.commands.client_factory import get_mgmt_service_client +from azure.cli.core.profiles import ResourceType + + +def cf_k8s_extension(cli_ctx, *_): + from azext_k8s_extension.vendored_sdks import K8sExtensionClient + return get_mgmt_service_client(cli_ctx, K8sExtensionClient) + + +def cf_k8s_extension_operation(cli_ctx, _): + return cf_k8s_extension(cli_ctx).k8s_extensions + + +def cf_resource_groups(cli_ctx, subscription_id=None): + return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, + subscription_id=subscription_id).resource_groups + + +def cf_resources(cli_ctx, subscription_id=None): + return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, + subscription_id=subscription_id).resources + + +def cf_log_analytics(cli_ctx, subscription_id=None): + from azure.mgmt.loganalytics import LogAnalyticsManagementClient # pylint: disable=no-name-in-module + return get_mgmt_service_client(cli_ctx, LogAnalyticsManagementClient, subscription_id=subscription_id) diff --git a/src/k8s-extension/azext_k8s_extension/_help.py b/src/k8s-extension/azext_k8s_extension/_help.py new file mode 100644 index 00000000000..69011bb9d92 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/_help.py @@ -0,0 +1,38 @@ +# 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['k8s-extension'] = """ + type: group + short-summary: Commands to manage K8s-extensions. +""" + +helps['k8s-extension create'] = """ + type: command + short-summary: Create a K8s-extension. +""" + +helps['k8s-extension list'] = """ + type: command + short-summary: List K8s-extensions. +""" + +helps['k8s-extension delete'] = """ + type: command + short-summary: Delete a K8s-extension. +""" + +helps['k8s-extension show'] = """ + type: command + short-summary: Show details of a K8s-extension. +""" + +helps['k8s-extension update'] = """ + type: command + short-summary: Update a K8s-extension. +""" diff --git a/src/k8s-extension/azext_k8s_extension/_params.py b/src/k8s-extension/azext_k8s_extension/_params.py new file mode 100644 index 00000000000..0e870204887 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/_params.py @@ -0,0 +1,73 @@ +# -------------------------------------------------------------------------------------------- +# 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.parameters import ( + get_enum_type, + get_three_state_flag, + tags_type +) +from azure.cli.core.commands.validators import get_default_location_from_resource_group + +from azext_k8s_extension.action import ( + AddConfigurationSettings, + AddConfigurationProtectedSettings +) + + +def load_arguments(self, _): + with self.argument_context('k8s-extension') as c: + c.argument('tags', tags_type) + c.argument('location', + validator=get_default_location_from_resource_group) + c.argument('name', + options_list=['--name', '-n'], + help='Name of the extension instance') + c.argument('extension_type', + help='Name of the extension type.') + c.argument('cluster_name', + options_list=['--cluster-name', '-c'], + help='Name of the Kubernetes cluster') + c.argument('cluster_type', + arg_type=get_enum_type(['connectedClusters', 'managedClusters', 'appliances']), + help='Specify Arc clusters or AKS managed clusters or Arc appliances.') + c.argument('scope', + arg_type=get_enum_type(['cluster', 'namespace']), + help='Specify the extension scope.') + c.argument('auto_upgrade_minor_version', + arg_group="Version", + options_list=['--auto-upgrade-minor-version', '--auto-upgrade'], + arg_type=get_three_state_flag(), + help='Automatically upgrade minor version of the extension instance.') + c.argument('version', + arg_group="Version", + help='Specify the version to install for the extension instance if' + ' --auto-upgrade-minor-version is not enabled.') + c.argument('configuration_settings', + arg_group="Configuration", + options_list=['--configuration-settings', '--config'], + action=AddConfigurationSettings, + nargs='+', + help='Configuration Settings as key=value pair. Repeat parameter for each setting') + c.argument('configuration_protected_settings', + arg_group="Configuration", + options_list=['--configuration-protected-settings', '--config-protected'], + action=AddConfigurationProtectedSettings, + nargs='+', + help='Configuration Protected Settings as key=value pair. Repeat parameter for each setting') + c.argument('configuration_settings_file', + arg_group="Configuration", + options_list=['--configuration-settings-file', '--config-file'], + help='JSON file path for configuration-settings') + c.argument('configuration_protected_settings_file', + arg_group="Configuration", + options_list=['--configuration-protected-settings-file', '--config-protected-file'], + help='JSON file path for configuration-protected-settings') + c.argument('release_namespace', + help='Specify the namespace to install the extension release.') + c.argument('release_train', + help='Specify the release train for the extension type.') + c.argument('target_namespace', + help='Specify the target namespace to install to for the extension instance. This' + ' parameter is required if extension scope is set to \'namespace\'') diff --git a/src/k8s-extension/azext_k8s_extension/_validators.py b/src/k8s-extension/azext_k8s_extension/_validators.py new file mode 100644 index 00000000000..72270dab104 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/_validators.py @@ -0,0 +1,21 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +def example_name_or_id_validator(cmd, namespace): + # Example of a storage account name or ID validator. + # See: https://github.com/Azure/azure-cli/blob/dev/doc/authoring_command_modules/authoring_commands.md#supporting-name-or-id-parameters # pylint: disable=line-too-long + + from azure.cli.core.commands.client_factory import get_subscription_id + from msrestazure.tools import is_valid_resource_id, resource_id + if namespace.storage_account: + if not is_valid_resource_id(namespace.RESOURCE): + namespace.storage_account = resource_id( + subscription=get_subscription_id(cmd.cli_ctx), + resource_group=namespace.resource_group_name, + namespace='Microsoft.Storage', + type='storageAccounts', + name=namespace.storage_account + ) diff --git a/src/k8s-extension/azext_k8s_extension/action.py b/src/k8s-extension/azext_k8s_extension/action.py new file mode 100644 index 00000000000..4afbbbcd611 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/action.py @@ -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. +# -------------------------------------------------------------------------------------------- + +import argparse +from azure.cli.core.azclierror import ArgumentUsageError + + +# pylint: disable=protected-access, too-few-public-methods +class AddConfigurationSettings(argparse._AppendAction): + + def __call__(self, parser, namespace, values, option_string=None): + settings = {} + for item in values: + try: + key, value = item.split('=', 1) + settings[key] = value + except ValueError: + raise ArgumentUsageError('Usage error: {} configuration_setting_key=configuration_setting_value'. + format(option_string)) + super(AddConfigurationSettings, self).__call__(parser, namespace, settings, option_string) + + +# pylint: disable=protected-access, too-few-public-methods +class AddConfigurationProtectedSettings(argparse._AppendAction): + + def __call__(self, parser, namespace, values, option_string=None): + prot_settings = {} + for item in values: + try: + key, value = item.split('=', 1) + prot_settings[key] = value + except ValueError: + raise ArgumentUsageError('Usage error: {} configuration_protected_setting_key=' + 'configuration_protected_setting_value'.format(option_string)) + super(AddConfigurationProtectedSettings, self).__call__(parser, namespace, prot_settings, option_string) diff --git a/src/k8s-extension/azext_k8s_extension/azext_metadata.json b/src/k8s-extension/azext_k8s_extension/azext_metadata.json new file mode 100644 index 00000000000..30fdaf614ee --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/azext_metadata.json @@ -0,0 +1,4 @@ +{ + "azext.isPreview": true, + "azext.minCliCoreVersion": "2.15.0" +} \ No newline at end of file diff --git a/src/k8s-extension/azext_k8s_extension/commands.py b/src/k8s-extension/azext_k8s_extension/commands.py new file mode 100644 index 00000000000..63fe78f7d2a --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/commands.py @@ -0,0 +1,24 @@ +# -------------------------------------------------------------------------------------------- +# 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 +from azure.cli.core.commands import CliCommandType +from azext_k8s_extension._client_factory import (cf_k8s_extension, cf_k8s_extension_operation) + + +def load_command_table(self, _): + + k8s_extension_sdk = CliCommandType( + operations_tmpl='azext_k8s_extension.vendored_sdks.operations#K8sExtensionsOperations.{}', + client_factory=cf_k8s_extension) + + with self.command_group('k8s-extension', k8s_extension_sdk, client_factory=cf_k8s_extension_operation, + is_preview=True) \ + as g: + g.custom_command('create', 'create_k8s_extension') + g.custom_command('update', 'update_k8s_extension') + g.custom_command('delete', 'delete_k8s_extension', confirmation=True) + g.custom_command('list', 'list_k8s_extension') + g.custom_show_command('show', 'show_k8s_extension') diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py new file mode 100644 index 00000000000..469b4059dee --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -0,0 +1,275 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=unused-argument,too-many-locals + +import json +from knack.log import get_logger + +from msrestazure.azure_exceptions import CloudError + +from azure.cli.core.azclierror import ResourceNotFoundError, MutuallyExclusiveArgumentError, \ + InvalidArgumentValueError, CommandNotFoundError +from azure.cli.core.commands.client_factory import get_subscription_id +from azext_k8s_extension.vendored_sdks.models import ConfigurationIdentity +# from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate +from azext_k8s_extension.vendored_sdks.models import ErrorResponseException + +from .partner_extensions.ContainerInsights import ContainerInsights +from .partner_extensions.AzureDefender import AzureDefender +from .partner_extensions.OpenServiceMesh import OpenServiceMesh +from .partner_extensions.DefaultExtension import DefaultExtension + +from ._client_factory import cf_resources + +logger = get_logger(__name__) + + +# A factory method to return the correct extension class based off of the extension name +def ExtensionFactory(extension_name): + extension_map = { + 'microsoft.azuremonitor.containers': ContainerInsights, + 'microsoft.azuredefender.kubernetes': AzureDefender, + 'microsoft.openservicemesh': OpenServiceMesh, + } + + # Return the extension if we find it in the map, else return the default + return extension_map.get(extension_name, DefaultExtension)() + + +def show_k8s_extension(client, resource_group_name, cluster_name, name, cluster_type): + """Get an existing K8s Extension. + + """ + # Determine ClusterRP + cluster_rp = __get_cluster_rp(cluster_type) + + try: + extension = client.get(resource_group_name, + cluster_rp, cluster_type, cluster_name, name) + return extension + except ErrorResponseException as ex: + # Customize the error message for resources not found + if ex.response.status_code == 404: + # If Cluster not found + if ex.message.__contains__("(ResourceNotFound)"): + message = "{0} Verify that the cluster-type is correct and the resource exists.".format( + ex.message) + # If Configuration not found + elif ex.message.__contains__("Operation returned an invalid status code 'Not Found'"): + message = "(ExtensionNotFound) The Resource {0}/{1}/{2}/Microsoft.KubernetesConfiguration/" \ + "extensions/{3} could not be found!".format( + cluster_rp, cluster_type, cluster_name, name) + else: + message = ex.message + raise ResourceNotFoundError(message) + + +def create_k8s_extension(cmd, client, resource_group_name, cluster_name, name, cluster_type, + extension_type, scope='cluster', auto_upgrade_minor_version=None, release_train=None, + version=None, target_namespace=None, release_namespace=None, configuration_settings=None, + configuration_protected_settings=None, configuration_settings_file=None, + configuration_protected_settings_file=None, tags=None): + """Create a new Extension Instance. + + """ + extension_type_lower = extension_type.lower() + + # Determine ClusterRP + cluster_rp = __get_cluster_rp(cluster_type) + + # Configuration Settings & Configuration Protected Settings + if configuration_settings is not None and configuration_settings_file is not None: + raise MutuallyExclusiveArgumentError( + 'Error! Both configuration-settings and configuration-settings-file cannot be provided.' + ) + + if configuration_protected_settings is not None and configuration_protected_settings_file is not None: + raise MutuallyExclusiveArgumentError( + 'Error! Both configuration-protected-settings and configuration-protected-settings-file ' + 'cannot be provided.' + ) + + config_settings = {} + config_protected_settings = {} + # Get Configuration Settings from file + if configuration_settings_file is not None: + config_settings = __get_config_settings_from_file(configuration_settings_file) + + if configuration_settings is not None: + for dicts in configuration_settings: + for key, value in dicts.items(): + config_settings[key] = value + + # Get Configuration Protected Settings from file + if configuration_protected_settings_file is not None: + config_protected_settings = __get_config_settings_from_file(configuration_protected_settings_file) + + if configuration_protected_settings is not None: + for dicts in configuration_protected_settings: + for key, value in dicts.items(): + config_protected_settings[key] = value + + # Identity is not created by default. Extension type must specify if identity is required. + create_identity = False + + extension_instance = None + + # Scope & Namespace validation - common to all extension-types + __validate_scope_and_namespace(scope, release_namespace, target_namespace) + + # Give Partners a chance to their extensionType specific validations and to set value over-rides. + + # Get the extension class based on the extension name + extension_class = ExtensionFactory(extension_type_lower) + extension_instance, name, create_identity = extension_class.Create( + cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type_lower, scope, + auto_upgrade_minor_version, release_train, version, target_namespace, release_namespace, config_settings, + config_protected_settings, configuration_settings_file, configuration_protected_settings_file) + + # Common validations + __validate_version_and_auto_upgrade(extension_instance.version, extension_instance.auto_upgrade_minor_version) + + # Create identity, if required + if create_identity: + extension_instance.identity, extension_instance.location = \ + __create_identity(cmd, resource_group_name, cluster_name, cluster_type, cluster_rp) + + # Try to create the resource + return client.create(resource_group_name, cluster_rp, cluster_type, cluster_name, name, extension_instance) + + +def list_k8s_extension(client, resource_group_name, cluster_name, cluster_type): + cluster_rp = __get_cluster_rp(cluster_type) + return client.list(resource_group_name, cluster_rp, cluster_type, cluster_name) + + +def update_k8s_extension(client, resource_group_name, cluster_type, cluster_name, name, + auto_upgrade_minor_version='', release_train='', version='', tags=None): + + """Patch an existing Extension Instance. + + """ + + # TODO: Remove this after we eventually get PATCH implemented for update and uncomment + raise CommandNotFoundError( + "\"k8s-extension update\" currently is not available. " + "Use \"k8s-extension create\" to update a previously created extension instance." + ) + + # # Ensure some values are provided for update + # if auto_upgrade_minor_version is None and release_train is None and version is None: + # message = "Error! No values provided for update. Provide new value(s) for one or more of these properties:" \ + # " auto-upgrade-minor-version, release-train or version." + # raise RequiredArgumentMissingError(message) + + # # Determine ClusterRP + # cluster_rp = __get_cluster_rp(cluster_type) + + # # Get the existing extensionInstance + # extension = client.get(resource_group_name, cluster_rp, cluster_type, cluster_name, name) + + # extension_type_lower = extension.extension_type.lower() + + # # Get the extension class based on the extension name + # extension_class = ExtensionFactory(extension_type_lower) + # upd_extension = extension_class.Update(extension, auto_upgrade_minor_version, release_train, version) + + # __validate_version_and_auto_upgrade(version, auto_upgrade_minor_version) + + # upd_extension = ExtensionInstanceUpdate(auto_upgrade_minor_version=auto_upgrade_minor_version, + # release_train=release_train, version=version) + + # return client.update(resource_group_name, cluster_rp, cluster_type, cluster_name, name, upd_extension) + + +def delete_k8s_extension(client, resource_group_name, cluster_name, name, cluster_type): + """Delete an existing Kubernetes Extension. + + """ + # Determine ClusterRP + cluster_rp = __get_cluster_rp(cluster_type) + + k8s_extension_instance_name = name + + return client.delete(resource_group_name, cluster_rp, cluster_type, cluster_name, k8s_extension_instance_name) + + +def __create_identity(cmd, resource_group_name, cluster_name, cluster_type, cluster_rp): + subscription_id = get_subscription_id(cmd.cli_ctx) + resources = cf_resources(cmd.cli_ctx, subscription_id) + + cluster_resource_id = '/subscriptions/{0}/resourceGroups/{1}/providers/{2}/{3}/{4}'.format(subscription_id, + resource_group_name, + cluster_rp, + cluster_type, + cluster_name) + + if cluster_rp == 'Microsoft.Kubernetes': + parent_api_version = '2020-01-01-preview' + elif cluster_rp == 'Microsoft.ResourceConnector': + parent_api_version = '2020-09-15-privatepreview' + elif cluster_rp == 'Microsoft.ContainerService': + parent_api_version = '2017-07-01' + else: + raise InvalidArgumentValueError( + "Error! Cluster type '{}' is not supported for extension identity".format(cluster_type) + ) + + try: + resource = resources.get_by_id(cluster_resource_id, parent_api_version) + location = str(resource.location.lower()) + except CloudError as ex: + raise ex + identity_type = "SystemAssigned" + + return ConfigurationIdentity(type=identity_type), location + + +def __get_cluster_rp(cluster_type): + rp = "" + if cluster_type.lower() == 'connectedclusters': + rp = 'Microsoft.Kubernetes' + elif cluster_type.lower() == 'appliances': + rp = 'Microsoft.ResourceConnector' + elif cluster_type.lower() == '': + rp = 'Microsoft.ContainerService' + else: + raise InvalidArgumentValueError("Error! Cluster type '{}' is not supported".format(cluster_type)) + return rp + + +def __validate_scope_and_namespace(scope, release_namespace, target_namespace): + if scope == 'cluster': + if target_namespace is not None: + message = "When Scope is 'cluster', target-namespace must not be given." + raise MutuallyExclusiveArgumentError(message) + else: + if release_namespace is not None: + message = "When Scope is 'namespace', release-namespace must not be given." + raise MutuallyExclusiveArgumentError(message) + + +def __validate_version_and_auto_upgrade(version, auto_upgrade_minor_version): + if version is not None: + if auto_upgrade_minor_version: + message = "To pin to specific version, auto-upgrade-minor-version must be set to 'false'." + raise MutuallyExclusiveArgumentError(message) + + auto_upgrade_minor_version = False + + +def __get_config_settings_from_file(file_path): + try: + config_file = open(file_path,) + settings = json.load(config_file) + except ValueError: + raise Exception("File {} is not a valid JSON file".format(file_path)) + + files = len(settings) + if files == 0: + raise Exception("File {} is empty".format(file_path)) + + return settings diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py new file mode 100644 index 00000000000..172662f4e57 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py @@ -0,0 +1,73 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=unused-argument + +from knack.log import get_logger + +from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceForCreate +from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate +from azext_k8s_extension.vendored_sdks.models import ScopeCluster +from azext_k8s_extension.vendored_sdks.models import Scope + +from .PartnerExtensionModel import PartnerExtensionModel +from .ContainerInsights import _get_container_insights_settings + +logger = get_logger(__name__) + + +class AzureDefender(PartnerExtensionModel): + def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, + scope, auto_upgrade_minor_version, release_train, version, target_namespace, + release_namespace, configuration_settings, configuration_protected_settings, + configuration_settings_file, configuration_protected_settings_file): + + """ExtensionType 'microsoft.azuredefender.kubernetes' specific validations & defaults for Create + Must create and return a valid 'ExtensionInstanceForCreate' object. + + """ + # NOTE-1: Replace default scope creation with your customization! + ext_scope = None + # Hardcoding name, release_namespace and scope since ci only supports one instance and cluster scope + # and platform doesnt have support yet extension specific constraints like this + name = extension_type.lower() + release_namespace = "azuredefender" + # Scope is always cluster + scope_cluster = ScopeCluster(release_namespace=release_namespace) + ext_scope = Scope(cluster=scope_cluster, namespace=None) + + is_ci_extension_type = False + + logger.warning('Ignoring name, release-namespace and scope parameters since %s ' + 'only supports cluster scope and single instance of this extension', extension_type) + + _get_container_insights_settings(cmd, resource_group_name, cluster_name, configuration_settings, + configuration_protected_settings, is_ci_extension_type) + + # NOTE-2: Return a valid ExtensionInstanceForCreate object, Instance name and flag for Identity + create_identity = True + extension_instance = ExtensionInstanceForCreate( + extension_type=extension_type, + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version, + scope=ext_scope, + configuration_settings=configuration_settings, + configuration_protected_settings=configuration_protected_settings, + identity=None, + location="" + ) + return extension_instance, name, create_identity + + def Update(self, extension, auto_upgrade_minor_version, release_train, version): + """ExtensionType 'microsoft.azuredefender.kubernetes' specific validations & defaults for Update + Must create and return a valid 'ExtensionInstanceUpdate' object. + + """ + return ExtensionInstanceUpdate( + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version + ) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py new file mode 100644 index 00000000000..ada94d985a9 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py @@ -0,0 +1,460 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=unused-argument + +import datetime +import json + +from knack.log import get_logger + +from azure.cli.core.azclierror import InvalidArgumentValueError +from azure.cli.core.commands import LongRunningOperation +from azure.cli.core.commands.client_factory import get_mgmt_service_client, get_subscription_id +from azure.cli.core.util import sdk_no_wait +from msrestazure.azure_exceptions import CloudError +from msrestazure.tools import parse_resource_id, is_valid_resource_id + +from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceForCreate +from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate +from azext_k8s_extension.vendored_sdks.models import ScopeCluster +from azext_k8s_extension.vendored_sdks.models import Scope + +from .PartnerExtensionModel import PartnerExtensionModel + +from .._client_factory import ( + cf_resources, cf_resource_groups, cf_log_analytics) + +logger = get_logger(__name__) + + +class ContainerInsights(PartnerExtensionModel): + def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, + scope, auto_upgrade_minor_version, release_train, version, target_namespace, + release_namespace, configuration_settings, configuration_protected_settings, + configuration_settings_file, configuration_protected_settings_file): + + """ExtensionType 'microsoft.azuremonitor.containers' specific validations & defaults for Create + Must create and return a valid 'ExtensionInstanceForCreate' object. + + """ + # NOTE-1: Replace default scope creation with your customization! + ext_scope = None + # Hardcoding name, release_namespace and scope since container-insights only supports one instance and cluster + # scope and platform doesnt have support yet extension specific constraints like this + name = 'azuremonitor-containers' + release_namespace = 'azuremonitor-containers' + # Scope is always cluster + scope_cluster = ScopeCluster(release_namespace=release_namespace) + ext_scope = Scope(cluster=scope_cluster, namespace=None) + + is_ci_extension_type = True + + logger.warning('Ignoring name, release-namespace and scope parameters since %s ' + 'only supports cluster scope and single instance of this extension', extension_type) + + _get_container_insights_settings(cmd, resource_group_name, cluster_name, configuration_settings, + configuration_protected_settings, is_ci_extension_type) + + # NOTE-2: Return a valid ExtensionInstanceForCreate object, Instance name and flag for Identity + create_identity = True + extension_instance = ExtensionInstanceForCreate( + extension_type=extension_type, + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version, + scope=ext_scope, + configuration_settings=configuration_settings, + configuration_protected_settings=configuration_protected_settings, + identity=None, + location="" + ) + return extension_instance, name, create_identity + + def Update(self, extension, auto_upgrade_minor_version, release_train, version): + """ExtensionType 'microsoft.azuremonitor.containers' specific validations & defaults for Update + Must create and return a valid 'ExtensionInstanceUpdate' object. + + """ + return ExtensionInstanceUpdate( + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version + ) + + +# Custom Validation Logic for Container Insights + +def _invoke_deployment(cmd, resource_group_name, deployment_name, template, parameters, validate, no_wait, + subscription_id=None): + from azure.cli.core.profiles import ResourceType + deployment_properties = cmd.get_models('DeploymentProperties', resource_type=ResourceType.MGMT_RESOURCE_RESOURCES) + properties = deployment_properties(template=template, parameters=parameters, mode='incremental') + smc = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, + subscription_id=subscription_id).deployments + if validate: + logger.info('==== BEGIN TEMPLATE ====') + logger.info(json.dumps(template, indent=2)) + logger.info('==== END TEMPLATE ====') + + if cmd.supported_api_version(min_api='2019-10-01', resource_type=ResourceType.MGMT_RESOURCE_RESOURCES): + deployment_temp = cmd.get_models('Deployment', resource_type=ResourceType.MGMT_RESOURCE_RESOURCES) + deployment = deployment_temp(properties=properties) + + if validate: + validation_poller = smc.validate(resource_group_name, deployment_name, deployment) + return LongRunningOperation(cmd.cli_ctx)(validation_poller) + return sdk_no_wait(no_wait, smc.create_or_update, resource_group_name, deployment_name, deployment) + + if validate: + return smc.validate(resource_group_name, deployment_name, properties) + return sdk_no_wait(no_wait, smc.create_or_update, resource_group_name, deployment_name, properties) + + +def _ensure_default_log_analytics_workspace_for_monitoring(cmd, subscription_id, + cluster_resource_group_name, cluster_name): + # mapping for azure public cloud + # log analytics workspaces cannot be created in WCUS region due to capacity limits + # so mapped to EUS per discussion with log analytics team + # pylint: disable=too-many-locals,too-many-statements + + azurecloud_location_to_oms_region_code_map = { + "australiasoutheast": "ASE", + "australiaeast": "EAU", + "australiacentral": "CAU", + "canadacentral": "CCA", + "centralindia": "CIN", + "centralus": "CUS", + "eastasia": "EA", + "eastus": "EUS", + "eastus2": "EUS2", + "eastus2euap": "EAP", + "francecentral": "PAR", + "japaneast": "EJP", + "koreacentral": "SE", + "northeurope": "NEU", + "southcentralus": "SCUS", + "southeastasia": "SEA", + "uksouth": "SUK", + "usgovvirginia": "USGV", + "westcentralus": "EUS", + "westeurope": "WEU", + "westus": "WUS", + "westus2": "WUS2" + } + azurecloud_region_to_oms_region_map = { + "australiacentral": "australiacentral", + "australiacentral2": "australiacentral", + "australiaeast": "australiaeast", + "australiasoutheast": "australiasoutheast", + "brazilsouth": "southcentralus", + "canadacentral": "canadacentral", + "canadaeast": "canadacentral", + "centralus": "centralus", + "centralindia": "centralindia", + "eastasia": "eastasia", + "eastus": "eastus", + "eastus2": "eastus2", + "francecentral": "francecentral", + "francesouth": "francecentral", + "japaneast": "japaneast", + "japanwest": "japaneast", + "koreacentral": "koreacentral", + "koreasouth": "koreacentral", + "northcentralus": "eastus", + "northeurope": "northeurope", + "southafricanorth": "westeurope", + "southafricawest": "westeurope", + "southcentralus": "southcentralus", + "southeastasia": "southeastasia", + "southindia": "centralindia", + "uksouth": "uksouth", + "ukwest": "uksouth", + "westcentralus": "eastus", + "westeurope": "westeurope", + "westindia": "centralindia", + "westus": "westus", + "westus2": "westus2" + } + + # mapping for azure china cloud + # currently log analytics supported only China East 2 region + azurechina_location_to_oms_region_code_map = { + "chinaeast": "EAST2", + "chinaeast2": "EAST2", + "chinanorth": "EAST2", + "chinanorth2": "EAST2" + } + azurechina_region_to_oms_region_map = { + "chinaeast": "chinaeast2", + "chinaeast2": "chinaeast2", + "chinanorth": "chinaeast2", + "chinanorth2": "chinaeast2" + } + + # mapping for azure us governmner cloud + azurefairfax_location_to_oms_region_code_map = { + "usgovvirginia": "USGV" + } + azurefairfax_region_to_oms_region_map = { + "usgovvirginia": "usgovvirginia" + } + + cluster_location = '' + resources = cf_resources(cmd.cli_ctx, subscription_id) + + cluster_resource_id = '/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Kubernetes' \ + '/connectedClusters/{2}'.format(subscription_id, cluster_resource_group_name, cluster_name) + try: + resource = resources.get_by_id(cluster_resource_id, '2020-01-01-preview') + cluster_location = resource.location.lower() + except CloudError as ex: + raise ex + + cloud_name = cmd.cli_ctx.cloud.name.lower() + workspace_region = "eastus" + workspace_region_code = "EUS" + + # sanity check that locations and clouds match. + if ((cloud_name == 'azurecloud' and azurechina_region_to_oms_region_map.get(cluster_location, False)) or + (cloud_name == 'azurecloud' and azurefairfax_region_to_oms_region_map.get(cluster_location, False))): + raise InvalidArgumentValueError( + 'Wrong cloud (azurecloud) setting for region {}, please use "az cloud set ..."' + .format(cluster_location) + ) + + if ((cloud_name == 'azurechinacloud' and azurecloud_region_to_oms_region_map.get(cluster_location, False)) or + (cloud_name == 'azurechinacloud' and azurefairfax_region_to_oms_region_map.get(cluster_location, False))): + raise InvalidArgumentValueError( + 'Wrong cloud (azurechinacloud) setting for region {}, please use "az cloud set ..."' + .format(cluster_location) + ) + + if ((cloud_name == 'azureusgovernment' and azurecloud_region_to_oms_region_map.get(cluster_location, False)) or + (cloud_name == 'azureusgovernment' and azurechina_region_to_oms_region_map.get(cluster_location, False))): + raise InvalidArgumentValueError( + 'Wrong cloud (azureusgovernment) setting for region {}, please use "az cloud set ..."' + .format(cluster_location) + ) + + if cloud_name == 'azurecloud': + workspace_region = azurecloud_region_to_oms_region_map.get(cluster_location, "eastus") + workspace_region_code = azurecloud_location_to_oms_region_code_map.get(workspace_region, "EUS") + elif cloud_name == 'azurechinacloud': + workspace_region = azurechina_region_to_oms_region_map.get(cluster_location, "chinaeast2") + workspace_region_code = azurechina_location_to_oms_region_code_map.get(workspace_region, "EAST2") + elif cloud_name == 'azureusgovernment': + workspace_region = azurefairfax_region_to_oms_region_map.get(cluster_location, "usgovvirginia") + workspace_region_code = azurefairfax_location_to_oms_region_code_map.get(workspace_region, "USGV") + else: + logger.error("AKS Monitoring addon not supported in cloud : %s", cloud_name) + + default_workspace_resource_group = 'DefaultResourceGroup-' + workspace_region_code + default_workspace_name = 'DefaultWorkspace-{0}-{1}'.format(subscription_id, workspace_region_code) + default_workspace_resource_id = '/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.OperationalInsights' \ + '/workspaces/{2}'.format(subscription_id, default_workspace_resource_group, default_workspace_name) + resource_groups = cf_resource_groups(cmd.cli_ctx, subscription_id) + + # check if default RG exists + if resource_groups.check_existence(default_workspace_resource_group): + try: + resource = resources.get_by_id(default_workspace_resource_id, '2015-11-01-preview') + return resource.id + except CloudError as ex: + if ex.status_code != 404: + raise ex + else: + resource_groups.create_or_update(default_workspace_resource_group, { + 'location': workspace_region}) + + default_workspace_params = { + 'location': workspace_region, + 'properties': { + 'sku': { + 'name': 'standalone' + } + } + } + async_poller = resources.create_or_update_by_id(default_workspace_resource_id, '2015-11-01-preview', + default_workspace_params) + + ws_resource_id = '' + while True: + result = async_poller.result(15) + if async_poller.done(): + ws_resource_id = result.id + break + + return ws_resource_id + + +def _ensure_container_insights_for_monitoring(cmd, workspace_resource_id): + # extract subscription ID and resource group from workspace_resource_id URL + parsed = parse_resource_id(workspace_resource_id) + subscription_id, resource_group = parsed["subscription"], parsed["resource_group"] + + resources = cf_resources(cmd.cli_ctx, subscription_id) + try: + resource = resources.get_by_id(workspace_resource_id, '2015-11-01-preview') + location = resource.location + except CloudError as ex: + raise ex + + unix_time_in_millis = int( + (datetime.datetime.utcnow() - datetime.datetime.utcfromtimestamp(0)).total_seconds() * 1000.0) + + solution_deployment_name = 'ContainerInsights-{}'.format(unix_time_in_millis) + + # pylint: disable=line-too-long + template = { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "workspaceResourceId": { + "type": "string", + "metadata": { + "description": "Azure Monitor Log Analytics Resource ID" + } + }, + "workspaceRegion": { + "type": "string", + "metadata": { + "description": "Azure Monitor Log Analytics workspace region" + } + }, + "solutionDeploymentName": { + "type": "string", + "metadata": { + "description": "Name of the solution deployment" + } + } + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "name": "[parameters('solutionDeploymentName')]", + "apiVersion": "2017-05-10", + "subscriptionId": "[split(parameters('workspaceResourceId'),'/')[2]]", + "resourceGroup": "[split(parameters('workspaceResourceId'),'/')[4]]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "apiVersion": "2015-11-01-preview", + "type": "Microsoft.OperationsManagement/solutions", + "location": "[parameters('workspaceRegion')]", + "name": "[Concat('ContainerInsights', '(', split(parameters('workspaceResourceId'),'/')" + "[8], ')')]", + "properties": { + "workspaceResourceId": "[parameters('workspaceResourceId')]" + }, + "plan": { + "name": "[Concat('ContainerInsights', '(', split(parameters('workspaceResourceId')," + "'/')[8], ')')]", + "product": "[Concat('OMSGallery/', 'ContainerInsights')]", + "promotionCode": "", + "publisher": "Microsoft" + } + } + ] + }, + "parameters": {} + } + } + ] + } + + params = { + "workspaceResourceId": { + "value": workspace_resource_id + }, + "workspaceRegion": { + "value": location + }, + "solutionDeploymentName": { + "value": solution_deployment_name + } + } + + deployment_name = 'arc-k8s-monitoring-{}'.format(unix_time_in_millis) + # publish the Container Insights solution to the Log Analytics workspace + return _invoke_deployment(cmd, resource_group, deployment_name, template, params, + validate=False, no_wait=False, subscription_id=subscription_id) + + +def _get_container_insights_settings(cmd, cluster_resource_group_name, cluster_name, configuration_settings, + configuration_protected_settings, is_ci_extension_type): + + subscription_id = get_subscription_id(cmd.cli_ctx) + workspace_resource_id = '' + + if configuration_settings is not None: + if 'loganalyticsworkspaceresourceid' in configuration_settings: + configuration_settings['logAnalyticsWorkspaceResourceID'] = \ + configuration_settings.pop('loganalyticsworkspaceresourceid') + + if 'logAnalyticsWorkspaceResourceID' in configuration_settings: + workspace_resource_id = configuration_settings['logAnalyticsWorkspaceResourceID'] + + workspace_resource_id = workspace_resource_id.strip() + + if configuration_protected_settings is not None: + if 'proxyEndpoint' in configuration_protected_settings: + # current supported format for proxy endpoint is http(s)://:@: + # do some basic validation since the ci agent does the complete validation + proxy = configuration_protected_settings['proxyEndpoint'].strip().lower() + proxy_parts = proxy.split('://') + if (not proxy) or (not proxy.startswith('http://') and not proxy.startswith('https://')) or \ + (len(proxy_parts) != 2): + raise InvalidArgumentValueError( + 'proxyEndpoint url should in this format http(s)://:@:' + ) + logger.info("successfully validated proxyEndpoint url hence passing proxy endpoint to extension") + configuration_protected_settings['omsagent.proxy'] = configuration_protected_settings['proxyEndpoint'] + + if not workspace_resource_id: + workspace_resource_id = _ensure_default_log_analytics_workspace_for_monitoring( + cmd, subscription_id, cluster_resource_group_name, cluster_name) + else: + if not is_valid_resource_id(workspace_resource_id): + raise InvalidArgumentValueError('{} is not a valid Azure resource ID.'.format(workspace_resource_id)) + + if is_ci_extension_type: + _ensure_container_insights_for_monitoring(cmd, workspace_resource_id).result() + + # extract subscription ID and resource group from workspace_resource_id URL + parsed = parse_resource_id(workspace_resource_id) + workspace_sub_id, workspace_rg_name, workspace_name = \ + parsed["subscription"], parsed["resource_group"], parsed["name"] + + log_analytics_client = cf_log_analytics(cmd.cli_ctx, workspace_sub_id) + log_analytics_workspace = log_analytics_client.workspaces.get(workspace_rg_name, workspace_name) + if not log_analytics_workspace: + raise InvalidArgumentValueError( + 'Fails to retrieve workspace by {}'.format(workspace_name)) + + shared_keys = log_analytics_client.shared_keys.get_shared_keys( + workspace_rg_name, workspace_name) + if not shared_keys: + raise InvalidArgumentValueError('Fails to retrieve shared key for workspace {}'.format( + log_analytics_workspace)) + configuration_protected_settings['omsagent.secret.wsid'] = log_analytics_workspace.customer_id + configuration_settings['logAnalyticsWorkspaceResourceID'] = workspace_resource_id + configuration_protected_settings['omsagent.secret.key'] = shared_keys.primary_shared_key + # set the domain for the ci agent for non azure public clouds + cloud_name = cmd.cli_ctx.cloud.name + if cloud_name.lower() == 'azurechinacloud': + configuration_settings['omsagent.domain'] = 'opinsights.azure.cn' + elif cloud_name.lower() == 'azureusgovernment': + configuration_settings['omsagent.domain'] = 'opinsights.azure.us' + elif cloud_name.lower() == 'usnat': + configuration_settings['omsagent.domain'] = 'opinsights.azure.eaglex.ic.gov' + elif cloud_name.lower() == 'ussec': + configuration_settings['omsagent.domain'] = 'opinsights.azure.microsoft.scloud' diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py new file mode 100644 index 00000000000..243371e47b7 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py @@ -0,0 +1,62 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=unused-argument + +from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceForCreate +from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate +from azext_k8s_extension.vendored_sdks.models import ScopeCluster +from azext_k8s_extension.vendored_sdks.models import ScopeNamespace +from azext_k8s_extension.vendored_sdks.models import Scope + +from .PartnerExtensionModel import PartnerExtensionModel + + +class DefaultExtension(PartnerExtensionModel): + def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, + scope, auto_upgrade_minor_version, release_train, version, target_namespace, + release_namespace, configuration_settings, configuration_protected_settings, + configuration_settings_file, configuration_protected_settings_file): + + """Default validations & defaults for Create + Must create and return a valid 'ExtensionInstanceForCreate' object. + + """ + ext_scope = None + if scope is None or scope.lower() == 'cluster': + scope_cluster = ScopeCluster(release_namespace=release_namespace) + ext_scope = Scope(cluster=scope_cluster, namespace=None) + else: + scope_namespace = ScopeNamespace(target_namespace=target_namespace) + ext_scope = Scope(namespace=scope_namespace, cluster=None) + + # If release-train is not input, set it to 'stable' + if release_train is None: + release_train = 'stable' + + create_identity = False + extension_instance = ExtensionInstanceForCreate( + extension_type=extension_type, + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version, + scope=ext_scope, + configuration_settings=configuration_settings, + configuration_protected_settings=configuration_protected_settings, + identity=None, + location="" + ) + return extension_instance, name, create_identity + + def Update(self, extension, auto_upgrade_minor_version, release_train, version): + """Default validations & defaults for Update + Must create and return a valid 'ExtensionInstanceUpdate' object. + + """ + return ExtensionInstanceUpdate( + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version + ) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py new file mode 100644 index 00000000000..b9e530877dc --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py @@ -0,0 +1,95 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=unused-argument + +from azure.cli.core.azclierror import InvalidArgumentValueError, RequiredArgumentMissingError +from knack.log import get_logger + +from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceForCreate +from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate +from azext_k8s_extension.vendored_sdks.models import ScopeCluster +from azext_k8s_extension.vendored_sdks.models import Scope + +from .PartnerExtensionModel import PartnerExtensionModel + +logger = get_logger(__name__) + + +class OpenServiceMesh(PartnerExtensionModel): + def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, + scope, auto_upgrade_minor_version, release_train, version, target_namespace, + release_namespace, configuration_settings, configuration_protected_settings, + configuration_settings_file, configuration_protected_settings_file): + + """ExtensionType 'microsoft.openservicemesh' specific validations & defaults for Create + Must create and return a valid 'ExtensionInstanceForCreate' object. + + """ + # NOTE-1: Replace default scope creation with your customization, if required + # Scope must always be cluster + ext_scope = None + if scope == 'namespace': + raise InvalidArgumentValueError("Invalid scope '{}'. This extension can be installed " + "only at 'cluster' scope.".format(scope)) + + scope_cluster = ScopeCluster(release_namespace=release_namespace) + ext_scope = Scope(cluster=scope_cluster, namespace=None) + + valid_release_trains = ['staging', 'pilot'] + # If release-train is not input, set it to 'stable' + if release_train is None: + raise RequiredArgumentMissingError( + "A release-train must be provided. Valid values are 'staging', 'pilot'." + ) + + if release_train.lower() in valid_release_trains: + # version is a mandatory if release-train is staging or pilot + if version is None: + raise RequiredArgumentMissingError( + "A version must be provided for release-train {}.".format(release_train) + ) + # If the release-train is 'staging' or 'pilot' then auto-upgrade-minor-version MUST be set to False + if auto_upgrade_minor_version or auto_upgrade_minor_version is None: + auto_upgrade_minor_version = False + logger.warning("Setting auto-upgrade-minor-version to False since release-train is '%s'", release_train) + else: + raise InvalidArgumentValueError( + "Invalid release-train '{}'. Valid values are 'staging', 'pilot'.".format(release_train) + ) + + # NOTE-2: Return a valid ExtensionInstanceForCreate object, Instance name and flag for Identity + create_identity = False + extension_instance = ExtensionInstanceForCreate( + extension_type=extension_type, + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version, + scope=ext_scope, + configuration_settings=configuration_settings, + configuration_protected_settings=configuration_protected_settings, + identity=None, + location="" + ) + return extension_instance, name, create_identity + + def Update(self, extension, auto_upgrade_minor_version, release_train, version): + """ExtensionType 'microsoft.openservicemesh' specific validations & defaults for Update + Must create and return a valid 'ExtensionInstanceUpdate' object. + + """ + # auto-upgrade-minor-version MUST be set to False if release_train is staging or pilot + if release_train.lower() in 'staging' 'pilot': + if auto_upgrade_minor_version or auto_upgrade_minor_version is None: + auto_upgrade_minor_version = False + # Set version to None to always get the latest version - user cannot override + version = None + logger.warning("Setting auto-upgrade-minor-version to False since release-train is '%s'", release_train) + + return ExtensionInstanceUpdate( + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version + ) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py new file mode 100644 index 00000000000..c863a8d3833 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py @@ -0,0 +1,19 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from abc import ABC, abstractmethod + + +class PartnerExtensionModel(ABC): + @abstractmethod + def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, + scope, auto_upgrade_minor_version, release_train, version, target_namespace, + release_namespace, configuration_settings, configuration_protected_settings, + configuration_settings_file, configuration_protected_settings_file): + pass + + @abstractmethod + def Update(self, extension, auto_upgrade_minor_version, release_train, version): + pass diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/__init__.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/k8s-extension/azext_k8s_extension/tests/__init__.py b/src/k8s-extension/azext_k8s_extension/tests/__init__.py new file mode 100644 index 00000000000..99c0f28cd71 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/tests/__init__.py @@ -0,0 +1,5 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/__init__.py b/src/k8s-extension/azext_k8s_extension/tests/latest/__init__.py new file mode 100644 index 00000000000..99c0f28cd71 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/__init__.py @@ -0,0 +1,5 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/recordings/test_k8s_extension.yaml b/src/k8s-extension/azext_k8s_extension/tests/latest/recordings/test_k8s_extension.yaml new file mode 100644 index 00000000000..127b21ac873 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/recordings/test_k8s_extension.yaml @@ -0,0 +1,270 @@ +interactions: +- request: + body: '{"properties": {"extensionType": "microsoft.openservicemesh", "autoUpgradeMinorVersion": + false, "releaseTrain": "staging", "version": "0.1.0", "scope": {"cluster": {}}, + "configurationSettings": {}, "configurationProtectedSettings": {}}, "location": + ""}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension create + Connection: + - keep-alive + Content-Length: + - '252' + Content-Type: + - application/json; charset=utf-8 + ParameterSetName: + - -g -n -c --cluster-type --extension-type --release-train --version + User-Agent: + - python/3.9.0 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.4 + azure-mgmt-kubernetesconfiguration/0.1.0 Azure-SDK-For-Python AZURECLI/2.19.1 + accept-language: + - en-US + method: PUT + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh?api-version=2020-07-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh","name":"openservice-mesh","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"configurationSettings":{},"statuses":[],"extensionType":"microsoft.openservicemesh","autoUpgradeMinorVersion":false,"releaseTrain":"staging","version":"0.1.0","scope":{"cluster":{"releaseNamespace":"arc-osm-system"}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-03-08T23:14:12.4010326+00:00","lastModifiedTime":"2021-03-08T23:14:12.4010327+00:00"}}' + headers: + api-supported-versions: + - 2020-07-01-Preview + cache-control: + - no-cache + content-length: + - '708' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Mar 2021 23:14:11 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - openresty/1.15.8.2 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-ms-ratelimit-remaining-subscription-writes: + - '1199' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension list + Connection: + - keep-alive + ParameterSetName: + - -c -g --cluster-type + User-Agent: + - python/3.9.0 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.4 + azure-mgmt-kubernetesconfiguration/0.1.0 Azure-SDK-For-Python AZURECLI/2.19.1 + accept-language: + - en-US + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions?api-version=2020-07-01-preview + response: + body: + string: '{"value":[{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh","name":"openservice-mesh","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"extensionType":"microsoft.openservicemesh","autoUpgradeMinorVersion":false,"releaseTrain":"staging","version":"0.1.0","scope":{"cluster":{"releaseNamespace":"arc-osm-system"}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-03-08T23:14:12.4010326+00:00","lastModifiedTime":"2021-03-08T23:14:12.4010327+00:00"}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/hci22jan21","name":"hci22jan21","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"extensionType":"microsoft.azstackhci.operator","autoUpgradeMinorVersion":true,"releaseTrain":"stable","version":"1.0.0","scope":{"cluster":{"releaseNamespace":null}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-01-22T20:49:34.3336157+00:00","lastModifiedTime":"2021-01-22T20:49:34.3336249+00:00"}}],"nextLink":null}' + headers: + api-supported-versions: + - 2020-07-01-Preview + cache-control: + - no-cache + content-length: + - '1341' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Mar 2021 23:14:13 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - openresty/1.15.8.2 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension show + Connection: + - keep-alive + ParameterSetName: + - -c -g -n --cluster-type + User-Agent: + - python/3.9.0 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.4 + azure-mgmt-kubernetesconfiguration/0.1.0 Azure-SDK-For-Python AZURECLI/2.19.1 + accept-language: + - en-US + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh?api-version=2020-07-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh","name":"openservice-mesh","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"configurationSettings":{},"statuses":[],"extensionType":"microsoft.openservicemesh","autoUpgradeMinorVersion":false,"releaseTrain":"staging","version":"0.1.0","scope":{"cluster":{"releaseNamespace":"arc-osm-system"}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-03-08T23:14:12.4010326+00:00","lastModifiedTime":"2021-03-08T23:14:12.4010327+00:00"}}' + headers: + api-supported-versions: + - 2020-07-01-Preview + cache-control: + - no-cache + content-length: + - '708' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Mar 2021 23:14:14 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - openresty/1.15.8.2 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension delete + Connection: + - keep-alive + Content-Length: + - '0' + ParameterSetName: + - -g -c -n --cluster-type -y + User-Agent: + - python/3.9.0 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.4 + azure-mgmt-kubernetesconfiguration/0.1.0 Azure-SDK-For-Python AZURECLI/2.19.1 + accept-language: + - en-US + method: DELETE + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh?api-version=2020-07-01-preview + response: + body: + string: '{"content":null,"statusCode":200,"headers":[],"version":"1.1","reasonPhrase":"OK","trailingHeaders":[],"requestMessage":null,"isSuccessStatusCode":true}' + headers: + api-supported-versions: + - 2020-07-01-Preview + cache-control: + - no-cache + content-length: + - '152' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Mar 2021 23:14:14 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - openresty/1.15.8.2 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + x-ms-ratelimit-remaining-subscription-deletes: + - '14999' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension list + Connection: + - keep-alive + ParameterSetName: + - -c -g --cluster-type + User-Agent: + - python/3.9.0 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.4 + azure-mgmt-kubernetesconfiguration/0.1.0 Azure-SDK-For-Python AZURECLI/2.19.1 + accept-language: + - en-US + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions?api-version=2020-07-01-preview + response: + body: + string: '{"value":[{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/hci22jan21","name":"hci22jan21","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"extensionType":"microsoft.azstackhci.operator","autoUpgradeMinorVersion":true,"releaseTrain":"stable","version":"1.0.0","scope":{"cluster":{"releaseNamespace":null}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-01-22T20:49:34.3336157+00:00","lastModifiedTime":"2021-01-22T20:49:34.3336249+00:00"}}],"nextLink":null}' + headers: + api-supported-versions: + - 2020-07-01-Preview + cache-control: + - no-cache + content-length: + - '673' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Mar 2021 23:14:16 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - openresty/1.15.8.2 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +version: 1 diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py b/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py new file mode 100644 index 00000000000..0e53c9e6691 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py @@ -0,0 +1,67 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import os +import unittest + +from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer, record_only) + + +TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) + + +class K8sExtensionScenarioTest(ScenarioTest): + @record_only() + @ResourceGroupPreparer(name_prefix='cli_test_k8s_extension') + def test_k8s_extension(self): + resource_type = 'microsoft.openservicemesh' + self.kwargs.update({ + 'name': 'openservice-mesh', + 'rg': 'nanthirg0923', + 'cluster_name': 'nanthicluster0923', + 'cluster_type': 'connectedClusters', + 'extension_type': resource_type, + 'release_train': 'staging', + 'version': '0.1.0' + }) + + self.cmd('k8s-extension create -g {rg} -n {name} -c {cluster_name} --cluster-type {cluster_type} --extension-type {extension_type} --release-train {release_train} --version {version}', checks=[ + self.check('name', '{name}'), + self.check('releaseTrain', '{release_train}'), + self.check('version', '{version}'), + self.check('resourceGroup', '{rg}'), + self.check('extensionType', '{extension_type}') + ]) + + # Update is disabled for now + # self.cmd('k8s-extension update -g {rg} -n {name} --tags foo=boo', checks=[ + # self.check('tags.foo', 'boo') + # ]) + + installed_exts = self.cmd('k8s-extension list -c {cluster_name} -g {rg} --cluster-type {cluster_type}').get_output_in_json() + found_extension = False + for item in installed_exts: + if item['extensionType'] == resource_type: + found_extension = True + break + self.assertTrue(found_extension) + + self.cmd('k8s-extension show -c {cluster_name} -g {rg} -n {name} --cluster-type {cluster_type}', checks=[ + self.check('name', '{name}'), + self.check('releaseTrain', '{release_train}'), + self.check('version', '{version}'), + self.check('resourceGroup', '{rg}'), + self.check('extensionType', '{extension_type}') + ]) + + self.cmd('k8s-extension delete -g {rg} -c {cluster_name} -n {name} --cluster-type {cluster_type} -y') + + installed_exts = self.cmd('k8s-extension list -c {cluster_name} -g {rg} --cluster-type {cluster_type}').get_output_in_json() + found_extension = False + for item in installed_exts: + if item['extensionType'] == resource_type: + found_extension = True + break + self.assertFalse(found_extension) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/__init__.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/__init__.py new file mode 100644 index 00000000000..d94dccac4c2 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/__init__.py @@ -0,0 +1,19 @@ +# 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 ._configuration import K8sExtensionClientConfiguration +from ._k8s_extension_client import K8sExtensionClient +__all__ = ['K8sExtensionClient', 'K8sExtensionClientConfiguration'] + +from .version import VERSION + +__version__ = VERSION + diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/_configuration.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_configuration.py new file mode 100644 index 00000000000..48080b2ee7d --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_configuration.py @@ -0,0 +1,49 @@ +# 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 msrestazure import AzureConfiguration + +from .version import VERSION + + +class K8sExtensionClientConfiguration(AzureConfiguration): + """Configuration for K8sExtensionClient + Note that all parameters used to create this instance are saved as instance + attributes. + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param subscription_id: The Azure subscription ID. This is a + GUID-formatted string (e.g. 00000000-0000-0000-0000-000000000000) + :type subscription_id: str + :param str base_url: Service URL + """ + + def __init__( + self, credentials, subscription_id, base_url=None): + + if credentials is None: + raise ValueError("Parameter 'credentials' must not be None.") + if subscription_id is None: + raise ValueError("Parameter 'subscription_id' must not be None.") + if not base_url: + base_url = 'https://management.azure.com' + + super(K8sExtensionClientConfiguration, self).__init__(base_url) + + # Starting Autorest.Python 4.0.64, make connection pool activated by default + self.keep_alive = True + + self.add_user_agent('azure-mgmt-kubernetesconfiguration/{}'.format(VERSION)) + self.add_user_agent('Azure-SDK-For-Python') + + self.credentials = credentials + self.subscription_id = subscription_id diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/_k8s_extension_client.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_k8s_extension_client.py new file mode 100644 index 00000000000..1b63ebfd1ac --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_k8s_extension_client.py @@ -0,0 +1,50 @@ +# 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 msrest.service_client import SDKClient +from msrest import Serializer, Deserializer + +from ._configuration import K8sExtensionClientConfiguration +from .operations import K8sExtensionsOperations +from . import models + + +class K8sExtensionClient(SDKClient): + """K8sExtension Client + + :ivar config: Configuration for client. + :vartype config: K8sExtensionClientConfiguration + + :ivar k8s_extensions: K8sExtensions operations + :vartype k8s_extensions: azure.mgmt.kubernetesconfiguration.operations.K8sExtensionsOperations + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param subscription_id: The Azure subscription ID. This is a + GUID-formatted string (e.g. 00000000-0000-0000-0000-000000000000) + :type subscription_id: str + :param str base_url: Service URL + """ + + def __init__( + self, credentials, subscription_id, base_url=None): + + self.config = K8sExtensionClientConfiguration(credentials, subscription_id, base_url) + super(K8sExtensionClient, self).__init__(self.config.credentials, self.config) + + client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} + self.api_version = '2020-07-01-preview' + self._serialize = Serializer(client_models) + self._deserialize = Deserializer(client_models) + + self.k8s_extensions = K8sExtensionsOperations( + self._client, self.config, self._serialize, self._deserialize) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/__init__.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/__init__.py new file mode 100644 index 00000000000..166f80c01ea --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/__init__.py @@ -0,0 +1,70 @@ +# 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. +# -------------------------------------------------------------------------- + +try: + from ._models_py3 import ErrorDefinition + from ._models_py3 import ErrorResponse, ErrorResponseException + from ._models_py3 import ExtensionInstance + from ._models_py3 import ExtensionInstanceForCreate + from ._models_py3 import ExtensionInstanceForList + from ._models_py3 import ExtensionInstanceUpdate + from ._models_py3 import ExtensionStatus + from ._models_py3 import ProxyResource + from ._models_py3 import Resource + from ._models_py3 import Result + from ._models_py3 import Scope + from ._models_py3 import ScopeCluster + from ._models_py3 import ScopeNamespace + from ._models_py3 import ConfigurationIdentity +except (SyntaxError, ImportError): + from ._models import ErrorDefinition + from ._models import ErrorResponse, ErrorResponseException + from ._models import ExtensionInstance + from ._models import ExtensionInstanceForCreate + from ._models import ExtensionInstanceForList + from ._models import ExtensionInstanceUpdate + from ._models import ExtensionStatus + from ._models import ProxyResource + from ._models import Resource + from ._models import Result + from ._models import Scope + from ._models import ScopeCluster + from ._models import ScopeNamespace + from ._models import ConfigurationIdentity +from ._paged_models import ExtensionInstanceForListPaged +from ._k8s_extension_client_enums import ( + MessageLevelType, + InstallStateType, + LevelType, + ResourceIdentityType, +) + +__all__ = [ + 'ErrorDefinition', + 'ErrorResponse', 'ErrorResponseException', + 'ExtensionInstance', + 'ExtensionInstanceForCreate', + 'ExtensionInstanceForList', + 'ConfigurationIdentity', + 'ExtensionInstanceUpdate', + 'ExtensionStatus', + 'ProxyResource', + 'Resource', + 'Result', + 'Scope', + 'ScopeCluster', + 'ScopeNamespace', + 'ExtensionInstanceForListPaged', + 'MessageLevelType', + 'InstallStateType', + 'LevelType', + 'ResourceIdentityType', +] diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_k8s_extension_client_enums.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_k8s_extension_client_enums.py new file mode 100644 index 00000000000..7be14a4b085 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_k8s_extension_client_enums.py @@ -0,0 +1,68 @@ +# 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 enum import Enum + + +class ComplianceStateType(str, Enum): + + pending = "Pending" + compliant = "Compliant" + noncompliant = "Noncompliant" + installed = "Installed" + failed = "Failed" + + +class MessageLevelType(str, Enum): + + error = "Error" + warning = "Warning" + information = "Information" + + +class OperatorType(str, Enum): + + flux = "Flux" + + +class OperatorScopeType(str, Enum): + + cluster = "cluster" + namespace = "namespace" + + +class ProvisioningStateType(str, Enum): + + accepted = "Accepted" + deleting = "Deleting" + running = "Running" + succeeded = "Succeeded" + failed = "Failed" + + +class InstallStateType(str, Enum): + + pending = "Pending" + installed = "Installed" + failed = "Failed" + + +class LevelType(str, Enum): + + error = "Error" + warning = "Warning" + information = "Information" + + +class ResourceIdentityType(str, Enum): + + system_assigned = "SystemAssigned" + none = "None" diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py new file mode 100644 index 00000000000..0a11097a24f --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py @@ -0,0 +1,587 @@ +# 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 msrest.serialization import Model +from msrest.exceptions import HttpOperationError + + +class CloudError(Model): + """CloudError. + """ + + _attribute_map = { + } + +class ConfigurationIdentity(Model): + """Identity for the managed cluster. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar principal_id: The principal id of the system assigned identity which + is used by the configuration. + :vartype principal_id: str + :ivar tenant_id: The tenant id of the system assigned identity which is + used by the configuration. + :vartype tenant_id: str + :param type: The type of identity used for the configuration. Type + 'SystemAssigned' will use an implicitly created identity. Type 'None' will + not use Managed Identity for the configuration. Possible values include: + 'SystemAssigned', 'None' + :type type: str or + ~azure.mgmt.kubernetesconfiguration.models.ResourceIdentityType + """ + + _validation = { + 'principal_id': {'readonly': True}, + 'tenant_id': {'readonly': True}, + } + + _attribute_map = { + 'principal_id': {'key': 'principalId', 'type': 'str'}, + 'tenant_id': {'key': 'tenantId', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'ResourceIdentityType'}, + } + + def __init__(self, **kwargs): + super(ConfigurationIdentity, self).__init__(**kwargs) + self.principal_id = None + self.tenant_id = None + self.type = kwargs.get('type', None) + + +class ErrorDefinition(Model): + """Error definition. + + All required parameters must be populated in order to send to Azure. + + :param code: Required. Service specific error code which serves as the + substatus for the HTTP error code. + :type code: str + :param message: Required. Description of the error. + :type message: str + """ + + _validation = { + 'code': {'required': True}, + 'message': {'required': True}, + } + + _attribute_map = { + 'code': {'key': 'code', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ErrorDefinition, self).__init__(**kwargs) + self.code = kwargs.get('code', None) + self.message = kwargs.get('message', None) + + +class ErrorResponse(Model): + """Error response. + + :param error: Error definition. + :type error: ~azure.mgmt.kubernetesconfiguration.models.ErrorDefinition + """ + + _attribute_map = { + 'error': {'key': 'error', 'type': 'ErrorDefinition'}, + } + + def __init__(self, **kwargs): + super(ErrorResponse, self).__init__(**kwargs) + self.error = kwargs.get('error', None) + + +class ErrorResponseException(HttpOperationError): + """Server responsed with exception of type: 'ErrorResponse'. + + :param deserialize: A deserializer + :param response: Server response to be deserialized. + """ + + def __init__(self, deserialize, response, *args): + + super(ErrorResponseException, self).__init__(deserialize, response, 'ErrorResponse', *args) + + +class Resource(Model): + """The Resource model definition. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(Resource, self).__init__(**kwargs) + self.id = None + self.name = None + self.type = None + + +class ProxyResource(Resource): + """ARM proxy resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ProxyResource, self).__init__(**kwargs) + + +class ExtensionInstance(ProxyResource): + """The Extension Instance object. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + :param extension_type: Type of the Extension, of which this resource is an + instance of. It must be one of the Extension Types registered with + Microsoft.KubernetesConfiguration by the Extension publisher. + :type extension_type: str + :param auto_upgrade_minor_version: Flag to note if this instance + participates in auto upgrade of minor version, or not. + :type auto_upgrade_minor_version: bool + :param release_train: ReleaseTrain this extension instance participates in + for auto-upgrade (e.g. Stable, Preview, etc.) - only if + autoUpgradeMinorVersion is 'true'. + :type release_train: str + :param version: Version of the extension for this extension instance, if + it is 'pinned' to a specific version. autoUpgradeMinorVersion must be + 'false'. + :type version: str + :param scope: Scope at which the extension instance is installed. + :type scope: ~azure.mgmt.kubernetesconfiguration.models.Scope + :param configuration_settings: Configuration settings, as name-value pairs + for configuring this instance of the extension. + :type configuration_settings: dict[str, str] + :param install_state: Status of installation of this instance of the + extension. Possible values include: 'Pending', 'Installed', 'Failed' + :type install_state: str or + ~azure.mgmt.kubernetesconfiguration.models.InstallStateType + :param statuses: Status from this instance of the extension. + :type statuses: + list[~azure.mgmt.kubernetesconfiguration.models.ExtensionStatus] + :ivar creation_time: DateLiteral (per ISO8601) noting the time the + resource was created by the client (user). + :vartype creation_time: str + :ivar last_modified_time: DateLiteral (per ISO8601) noting the time the + resource was modified by the client (user). + :vartype last_modified_time: str + :ivar last_status_time: DateLiteral (per ISO8601) noting the time of last + status from the agent. + :vartype last_status_time: str + :ivar error_info: Error information from the Agent - e.g. errors during + installation. + :vartype error_info: + ~azure.mgmt.kubernetesconfiguration.models.ErrorDefinition + :param identity: The identity of the configuration. + :type identity: + ~azure.mgmt.kubernetesconfiguration.models.ConfigurationIdentity + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'creation_time': {'readonly': True}, + 'last_modified_time': {'readonly': True}, + 'last_status_time': {'readonly': True}, + 'error_info': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, + 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, + 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, + 'version': {'key': 'properties.version', 'type': 'str'}, + 'scope': {'key': 'properties.scope', 'type': 'Scope'}, + 'configuration_settings': {'key': 'properties.configurationSettings', 'type': '{str}'}, + 'install_state': {'key': 'properties.installState', 'type': 'str'}, + 'statuses': {'key': 'properties.statuses', 'type': '[ExtensionStatus]'}, + 'creation_time': {'key': 'properties.creationTime', 'type': 'str'}, + 'last_modified_time': {'key': 'properties.lastModifiedTime', 'type': 'str'}, + 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, + 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'}, + 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, + } + + def __init__(self, **kwargs): + super(ExtensionInstance, self).__init__(**kwargs) + self.extension_type = kwargs.get('extension_type', None) + self.auto_upgrade_minor_version = kwargs.get('auto_upgrade_minor_version', None) + self.release_train = kwargs.get('release_train', None) + self.version = kwargs.get('version', None) + self.scope = kwargs.get('scope', None) + self.configuration_settings = kwargs.get('configuration_settings', None) + self.install_state = kwargs.get('install_state', None) + self.statuses = kwargs.get('statuses', None) + self.creation_time = None + self.last_modified_time = None + self.last_status_time = None + self.error_info = None + self.identity = kwargs.get('identity', None) + + +class ExtensionInstanceForCreate(ProxyResource): + """Object to create a new Extension Instance. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + :param extension_type: Type of the Extension, of which this resource is an + instance of. It must be one of the Extension Types registered with + Microsoft.KubernetesConfiguration by the Extension publisher. + :type extension_type: str + :param auto_upgrade_minor_version: Flag to note if this instance + participates in auto upgrade of minor version, or not. + :type auto_upgrade_minor_version: bool + :param release_train: ReleaseTrain this extension instance participates in + for auto-upgrade (e.g. Stable, Preview, etc.) - only if + autoUpgradeMinorVersion is 'true'. + :type release_train: str + :param version: Version of the extension for this extension instance, if + it is 'pinned' to a specific version. autoUpgradeMinorVersion must be + 'false'. + :type version: str + :param scope: Scope at which the extension instance is installed. + :type scope: ~azure.mgmt.kubernetesconfiguration.models.Scope + :param configuration_settings: Configuration settings, as name-value pairs + for configuring this instance of the extension. + :type configuration_settings: dict[str, str] + :param configuration_protected_settings: Configuration settings that are + sensitive, as name-value pairs for configuring this instance of the + extension. + :type configuration_protected_settings: dict[str, str] + :param identity: The identity of the configuration. + :type identity: + ~azure.mgmt.kubernetesconfiguration.models.ConfigurationIdentity + :type location: str + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, + 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, + 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, + 'version': {'key': 'properties.version', 'type': 'str'}, + 'scope': {'key': 'properties.scope', 'type': 'Scope'}, + 'configuration_settings': {'key': 'properties.configurationSettings', 'type': '{str}'}, + 'configuration_protected_settings': {'key': 'properties.configurationProtectedSettings', 'type': '{str}'}, + 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, + 'location': {'key': 'location', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ExtensionInstanceForCreate, self).__init__(**kwargs) + self.extension_type = kwargs.get('extension_type', None) + self.auto_upgrade_minor_version = kwargs.get('auto_upgrade_minor_version', None) + self.release_train = kwargs.get('release_train', None) + self.version = kwargs.get('version', None) + self.scope = kwargs.get('scope', None) + self.configuration_settings = kwargs.get('configuration_settings', None) + self.configuration_protected_settings = kwargs.get('configuration_protected_settings', None) + self.identity = kwargs.get('identity', None) + self.location = kwargs.get('location', None) + + +class ExtensionInstanceForList(ProxyResource): + """The Extension Instance object. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + :param extension_type: Type of the Extension, of which this resource is an + instance of. It must be one of the Extension Types registered with + Microsoft.KubernetesConfiguration by the Extension publisher. + :type extension_type: str + :param auto_upgrade_minor_version: Flag to note if this instance + participates in auto upgrade of minor version, or not. + :type auto_upgrade_minor_version: bool + :param release_train: ReleaseTrain this extension instance participates in + for auto-upgrade (e.g. Stable, Preview, etc.) - only if + autoUpgradeMinorVersion is 'true'. + :type release_train: str + :param version: Version of the extension for this extension instance, if + it is 'pinned' to a specific version. + :type version: str + :param scope: Scope at which the extension instance is installed. + :type scope: ~azure.mgmt.kubernetesconfiguration.models.Scope + :param install_state: Status of installation of this instance of the + extension. Possible values include: 'Pending', 'Installed', 'Failed' + :type install_state: str or + ~azure.mgmt.kubernetesconfiguration.models.InstallStateType + :ivar creation_time: DateLiteral (per ISO8601) noting the time the + resource was created by the client (user). + :vartype creation_time: str + :ivar last_modified_time: DateLiteral (per ISO8601) noting the time the + resource was modified by the client (user). + :vartype last_modified_time: str + :ivar last_status_time: DateLiteral (per ISO8601) noting the time of last + status from the agent. + :vartype last_status_time: str + :ivar error_info: Error information from the Agent - e.g. errors during + installation. + :vartype error_info: + ~azure.mgmt.kubernetesconfiguration.models.ErrorDefinition + :param identity: The identity of the configuration. + :type identity: + ~azure.mgmt.kubernetesconfiguration.models.ConfigurationIdentity + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'creation_time': {'readonly': True}, + 'last_modified_time': {'readonly': True}, + 'last_status_time': {'readonly': True}, + 'error_info': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, + 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, + 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, + 'version': {'key': 'properties.version', 'type': 'str'}, + 'scope': {'key': 'properties.scope', 'type': 'Scope'}, + 'install_state': {'key': 'properties.installState', 'type': 'str'}, + 'creation_time': {'key': 'properties.creationTime', 'type': 'str'}, + 'last_modified_time': {'key': 'properties.lastModifiedTime', 'type': 'str'}, + 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, + 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'}, + 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, + } + + def __init__(self, **kwargs): + super(ExtensionInstanceForList, self).__init__(**kwargs) + self.extension_type = kwargs.get('extension_type', None) + self.auto_upgrade_minor_version = kwargs.get('auto_upgrade_minor_version', None) + self.release_train = kwargs.get('release_train', None) + self.version = kwargs.get('version', None) + self.scope = kwargs.get('scope', None) + self.install_state = kwargs.get('install_state', None) + self.creation_time = None + self.last_modified_time = None + self.last_status_time = None + self.error_info = None + self.identity = kwargs.get('identity', None) + + +class ExtensionInstanceUpdate(Model): + """Update Extension Instance request object. + + :param auto_upgrade_minor_version: Flag to note if this instance + participates in Extension Lifecycle Management or not. + :type auto_upgrade_minor_version: bool + :param release_train: ReleaseTrain this extension instance participates in + for auto-upgrade (e.g. Stable, Preview, etc.) - only if + autoUpgradeMinorVersion is 'true'. + :type release_train: str + :param version: Version number of extension, to 'pin' to a specific + version. autoUpgradeMinorVersion must be 'false'. + :type version: str + """ + + _attribute_map = { + 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, + 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, + 'version': {'key': 'properties.version', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ExtensionInstanceUpdate, self).__init__(**kwargs) + self.auto_upgrade_minor_version = kwargs.get('auto_upgrade_minor_version', None) + self.release_train = kwargs.get('release_train', None) + self.version = kwargs.get('version', None) + + +class ExtensionStatus(Model): + """Status from this instance of the extension. + + :param code: Status code provided by the Extension + :type code: str + :param display_status: Short description of status of this instance of the + extension. + :type display_status: str + :param level: Level of the status. Possible values include: 'Error', + 'Warning', 'Information'. Default value: "Information" . + :type level: str or ~azure.mgmt.kubernetesconfiguration.models.LevelType + :param message: Detailed message of the status from the Extension + instance. + :type message: str + :param time: DateLiteral (per ISO8601) noting the time of installation + status. + :type time: str + """ + + _attribute_map = { + 'code': {'key': 'code', 'type': 'str'}, + 'display_status': {'key': 'displayStatus', 'type': 'str'}, + 'level': {'key': 'level', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'time': {'key': 'time', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ExtensionStatus, self).__init__(**kwargs) + self.code = kwargs.get('code', None) + self.display_status = kwargs.get('display_status', None) + self.level = kwargs.get('level', "Information") + self.message = kwargs.get('message', None) + self.time = kwargs.get('time', None) + + +class Result(Model): + """Sample result definition. + + :param sample_property: Sample property of type string + :type sample_property: str + """ + + _attribute_map = { + 'sample_property': {'key': 'sampleProperty', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(Result, self).__init__(**kwargs) + self.sample_property = kwargs.get('sample_property', None) + + +class Scope(Model): + """Scope of the extensionInstance. It can be either Cluster or Namespace; but + not both. + + :param cluster: Specifies that the scope of the extensionInstance is + Cluster + :type cluster: ~azure.mgmt.kubernetesconfiguration.models.ScopeCluster + :param namespace: Specifies that the scope of the extensionInstance is + Namespace + :type namespace: ~azure.mgmt.kubernetesconfiguration.models.ScopeNamespace + """ + + _attribute_map = { + 'cluster': {'key': 'cluster', 'type': 'ScopeCluster'}, + 'namespace': {'key': 'namespace', 'type': 'ScopeNamespace'}, + } + + def __init__(self, **kwargs): + super(Scope, self).__init__(**kwargs) + self.cluster = kwargs.get('cluster', None) + self.namespace = kwargs.get('namespace', None) + + +class ScopeCluster(Model): + """Specifies that the scope of the extensionInstance is Cluster. + + :param release_namespace: Namespace where the extension Release must be + placed, for a Cluster scoped extensionInstance. If this namespace does + not exist, it will be created + :type release_namespace: str + """ + + _attribute_map = { + 'release_namespace': {'key': 'releaseNamespace', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ScopeCluster, self).__init__(**kwargs) + self.release_namespace = kwargs.get('release_namespace', None) + + +class ScopeNamespace(Model): + """Specifies that the scope of the extensionInstance is Namespace. + + :param target_namespace: Namespace where the extensionInstance will be + created for an Namespace scoped extensionInstance. If this namespace does + not exist, it will be created + :type target_namespace: str + """ + + _attribute_map = { + 'target_namespace': {'key': 'targetNamespace', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ScopeNamespace, self).__init__(**kwargs) + self.target_namespace = kwargs.get('target_namespace', None) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py new file mode 100644 index 00000000000..16b408963ab --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py @@ -0,0 +1,587 @@ +# 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 msrest.serialization import Model +from msrest.exceptions import HttpOperationError + + +class CloudError(Model): + """CloudError. + """ + + _attribute_map = { + } + +class ConfigurationIdentity(Model): + """Identity for the managed cluster. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar principal_id: The principal id of the system assigned identity which + is used by the configuration. + :vartype principal_id: str + :ivar tenant_id: The tenant id of the system assigned identity which is + used by the configuration. + :vartype tenant_id: str + :param type: The type of identity used for the configuration. Type + 'SystemAssigned' will use an implicitly created identity. Type 'None' will + not use Managed Identity for the configuration. Possible values include: + 'SystemAssigned', 'None' + :type type: str or + ~azure.mgmt.kubernetesconfiguration.models.ResourceIdentityType + """ + + _validation = { + 'principal_id': {'readonly': True}, + 'tenant_id': {'readonly': True}, + } + + _attribute_map = { + 'principal_id': {'key': 'principalId', 'type': 'str'}, + 'tenant_id': {'key': 'tenantId', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'ResourceIdentityType'}, + } + + def __init__(self, *, type=None, **kwargs) -> None: + super(ConfigurationIdentity, self).__init__(**kwargs) + self.principal_id = None + self.tenant_id = None + self.type = type + + +class ErrorDefinition(Model): + """Error definition. + + All required parameters must be populated in order to send to Azure. + + :param code: Required. Service specific error code which serves as the + substatus for the HTTP error code. + :type code: str + :param message: Required. Description of the error. + :type message: str + """ + + _validation = { + 'code': {'required': True}, + 'message': {'required': True}, + } + + _attribute_map = { + 'code': {'key': 'code', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + } + + def __init__(self, *, code: str, message: str, **kwargs) -> None: + super(ErrorDefinition, self).__init__(**kwargs) + self.code = code + self.message = message + + +class ErrorResponse(Model): + """Error response. + + :param error: Error definition. + :type error: ~azure.mgmt.kubernetesconfiguration.models.ErrorDefinition + """ + + _attribute_map = { + 'error': {'key': 'error', 'type': 'ErrorDefinition'}, + } + + def __init__(self, *, error=None, **kwargs) -> None: + super(ErrorResponse, self).__init__(**kwargs) + self.error = error + + +class ErrorResponseException(HttpOperationError): + """Server responsed with exception of type: 'ErrorResponse'. + + :param deserialize: A deserializer + :param response: Server response to be deserialized. + """ + + def __init__(self, deserialize, response, *args): + + super(ErrorResponseException, self).__init__(deserialize, response, 'ErrorResponse', *args) + + +class Resource(Model): + """The Resource model definition. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, **kwargs) -> None: + super(Resource, self).__init__(**kwargs) + self.id = None + self.name = None + self.type = None + + +class ProxyResource(Resource): + """ARM proxy resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + } + + def __init__(self, **kwargs) -> None: + super(ProxyResource, self).__init__(**kwargs) + + +class ExtensionInstance(ProxyResource): + """The Extension Instance object. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + :param extension_type: Type of the Extension, of which this resource is an + instance of. It must be one of the Extension Types registered with + Microsoft.KubernetesConfiguration by the Extension publisher. + :type extension_type: str + :param auto_upgrade_minor_version: Flag to note if this instance + participates in auto upgrade of minor version, or not. + :type auto_upgrade_minor_version: bool + :param release_train: ReleaseTrain this extension instance participates in + for auto-upgrade (e.g. Stable, Preview, etc.) - only if + autoUpgradeMinorVersion is 'true'. + :type release_train: str + :param version: Version of the extension for this extension instance, if + it is 'pinned' to a specific version. autoUpgradeMinorVersion must be + 'false'. + :type version: str + :param scope: Scope at which the extension instance is installed. + :type scope: ~azure.mgmt.kubernetesconfiguration.models.Scope + :param configuration_settings: Configuration settings, as name-value pairs + for configuring this instance of the extension. + :type configuration_settings: dict[str, str] + :param install_state: Status of installation of this instance of the + extension. Possible values include: 'Pending', 'Installed', 'Failed' + :type install_state: str or + ~azure.mgmt.kubernetesconfiguration.models.InstallStateType + :param statuses: Status from this instance of the extension. + :type statuses: + list[~azure.mgmt.kubernetesconfiguration.models.ExtensionStatus] + :ivar creation_time: DateLiteral (per ISO8601) noting the time the + resource was created by the client (user). + :vartype creation_time: str + :ivar last_modified_time: DateLiteral (per ISO8601) noting the time the + resource was modified by the client (user). + :vartype last_modified_time: str + :ivar last_status_time: DateLiteral (per ISO8601) noting the time of last + status from the agent. + :vartype last_status_time: str + :ivar error_info: Error information from the Agent - e.g. errors during + installation. + :vartype error_info: + ~azure.mgmt.kubernetesconfiguration.models.ErrorDefinition + :param identity: The identity of the configuration. + :type identity: + ~azure.mgmt.kubernetesconfiguration.models.ConfigurationIdentity + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'creation_time': {'readonly': True}, + 'last_modified_time': {'readonly': True}, + 'last_status_time': {'readonly': True}, + 'error_info': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, + 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, + 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, + 'version': {'key': 'properties.version', 'type': 'str'}, + 'scope': {'key': 'properties.scope', 'type': 'Scope'}, + 'configuration_settings': {'key': 'properties.configurationSettings', 'type': '{str}'}, + 'install_state': {'key': 'properties.installState', 'type': 'str'}, + 'statuses': {'key': 'properties.statuses', 'type': '[ExtensionStatus]'}, + 'creation_time': {'key': 'properties.creationTime', 'type': 'str'}, + 'last_modified_time': {'key': 'properties.lastModifiedTime', 'type': 'str'}, + 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, + 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'}, + 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, + } + + def __init__(self, *, extension_type: str=None, auto_upgrade_minor_version: bool=None, release_train: str=None, version: str=None, scope=None, configuration_settings=None, install_state=None, statuses=None, identity=None, **kwargs) -> None: + super(ExtensionInstance, self).__init__(**kwargs) + self.extension_type = extension_type + self.auto_upgrade_minor_version = auto_upgrade_minor_version + self.release_train = release_train + self.version = version + self.scope = scope + self.configuration_settings = configuration_settings + self.install_state = install_state + self.statuses = statuses + self.creation_time = None + self.last_modified_time = None + self.last_status_time = None + self.error_info = None + self.identity = identity + + +class ExtensionInstanceForCreate(ProxyResource): + """Object to create a new Extension Instance. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + :param extension_type: Type of the Extension, of which this resource is an + instance of. It must be one of the Extension Types registered with + Microsoft.KubernetesConfiguration by the Extension publisher. + :type extension_type: str + :param auto_upgrade_minor_version: Flag to note if this instance + participates in auto upgrade of minor version, or not. + :type auto_upgrade_minor_version: bool + :param release_train: ReleaseTrain this extension instance participates in + for auto-upgrade (e.g. Stable, Preview, etc.) - only if + autoUpgradeMinorVersion is 'true'. + :type release_train: str + :param version: Version of the extension for this extension instance, if + it is 'pinned' to a specific version. autoUpgradeMinorVersion must be + 'false'. + :type version: str + :param scope: Scope at which the extension instance is installed. + :type scope: ~azure.mgmt.kubernetesconfiguration.models.Scope + :param configuration_settings: Configuration settings, as name-value pairs + for configuring this instance of the extension. + :type configuration_settings: dict[str, str] + :param configuration_protected_settings: Configuration settings that are + sensitive, as name-value pairs for configuring this instance of the + extension. + :type configuration_protected_settings: dict[str, str] + :param identity: The identity of the configuration. + :type identity: + ~azure.mgmt.kubernetesconfiguration.models.ConfigurationIdentity + :type location: str + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, + 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, + 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, + 'version': {'key': 'properties.version', 'type': 'str'}, + 'scope': {'key': 'properties.scope', 'type': 'Scope'}, + 'configuration_settings': {'key': 'properties.configurationSettings', 'type': '{str}'}, + 'configuration_protected_settings': {'key': 'properties.configurationProtectedSettings', 'type': '{str}'}, + 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, + 'location': {'key': 'location', 'type': 'str'}, + } + + def __init__(self, *, extension_type: str=None, auto_upgrade_minor_version: bool=None, release_train: str=None, version: str=None, scope=None, configuration_settings=None, configuration_protected_settings=None, identity=None, location=None, **kwargs) -> None: + super(ExtensionInstanceForCreate, self).__init__(location=location,**kwargs) + self.extension_type = extension_type + self.auto_upgrade_minor_version = auto_upgrade_minor_version + self.release_train = release_train + self.version = version + self.scope = scope + self.configuration_settings = configuration_settings + self.configuration_protected_settings = configuration_protected_settings + self.identity = identity + self.location = location + + +class ExtensionInstanceForList(ProxyResource): + """The Extension Instance object. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + :param extension_type: Type of the Extension, of which this resource is an + instance of. It must be one of the Extension Types registered with + Microsoft.KubernetesConfiguration by the Extension publisher. + :type extension_type: str + :param auto_upgrade_minor_version: Flag to note if this instance + participates in auto upgrade of minor version, or not. + :type auto_upgrade_minor_version: bool + :param release_train: ReleaseTrain this extension instance participates in + for auto-upgrade (e.g. Stable, Preview, etc.) - only if + autoUpgradeMinorVersion is 'true'. + :type release_train: str + :param version: Version of the extension for this extension instance, if + it is 'pinned' to a specific version. + :type version: str + :param scope: Scope at which the extension instance is installed. + :type scope: ~azure.mgmt.kubernetesconfiguration.models.Scope + :param install_state: Status of installation of this instance of the + extension. Possible values include: 'Pending', 'Installed', 'Failed' + :type install_state: str or + ~azure.mgmt.kubernetesconfiguration.models.InstallStateType + :ivar creation_time: DateLiteral (per ISO8601) noting the time the + resource was created by the client (user). + :vartype creation_time: str + :ivar last_modified_time: DateLiteral (per ISO8601) noting the time the + resource was modified by the client (user). + :vartype last_modified_time: str + :ivar last_status_time: DateLiteral (per ISO8601) noting the time of last + status from the agent. + :vartype last_status_time: str + :ivar error_info: Error information from the Agent - e.g. errors during + installation. + :vartype error_info: + ~azure.mgmt.kubernetesconfiguration.models.ErrorDefinition + :param identity: The identity of the configuration. + :type identity: + ~azure.mgmt.kubernetesconfiguration.models.ConfigurationIdentity + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'creation_time': {'readonly': True}, + 'last_modified_time': {'readonly': True}, + 'last_status_time': {'readonly': True}, + 'error_info': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, + 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, + 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, + 'version': {'key': 'properties.version', 'type': 'str'}, + 'scope': {'key': 'properties.scope', 'type': 'Scope'}, + 'install_state': {'key': 'properties.installState', 'type': 'str'}, + 'creation_time': {'key': 'properties.creationTime', 'type': 'str'}, + 'last_modified_time': {'key': 'properties.lastModifiedTime', 'type': 'str'}, + 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, + 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'}, + 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, + } + + def __init__(self, *, extension_type: str=None, auto_upgrade_minor_version: bool=None, release_train: str=None, version: str=None, scope=None, install_state=None, identity=None, **kwargs) -> None: + super(ExtensionInstanceForList, self).__init__(**kwargs) + self.extension_type = extension_type + self.auto_upgrade_minor_version = auto_upgrade_minor_version + self.release_train = release_train + self.version = version + self.scope = scope + self.install_state = install_state + self.creation_time = None + self.last_modified_time = None + self.last_status_time = None + self.error_info = None + self.identity = identity + + +class ExtensionInstanceUpdate(Model): + """Update Extension Instance request object. + + :param auto_upgrade_minor_version: Flag to note if this instance + participates in Extension Lifecycle Management or not. + :type auto_upgrade_minor_version: bool + :param release_train: ReleaseTrain this extension instance participates in + for auto-upgrade (e.g. Stable, Preview, etc.) - only if + autoUpgradeMinorVersion is 'true'. + :type release_train: str + :param version: Version number of extension, to 'pin' to a specific + version. autoUpgradeMinorVersion must be 'false'. + :type version: str + """ + + _attribute_map = { + 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, + 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, + 'version': {'key': 'properties.version', 'type': 'str'}, + } + + def __init__(self, *, auto_upgrade_minor_version: bool=None, release_train: str=None, version: str=None, **kwargs) -> None: + super(ExtensionInstanceUpdate, self).__init__(**kwargs) + self.auto_upgrade_minor_version = auto_upgrade_minor_version + self.release_train = release_train + self.version = version + + +class ExtensionStatus(Model): + """Status from this instance of the extension. + + :param code: Status code provided by the Extension + :type code: str + :param display_status: Short description of status of this instance of the + extension. + :type display_status: str + :param level: Level of the status. Possible values include: 'Error', + 'Warning', 'Information'. Default value: "Information" . + :type level: str or ~azure.mgmt.kubernetesconfiguration.models.LevelType + :param message: Detailed message of the status from the Extension + instance. + :type message: str + :param time: DateLiteral (per ISO8601) noting the time of installation + status. + :type time: str + """ + + _attribute_map = { + 'code': {'key': 'code', 'type': 'str'}, + 'display_status': {'key': 'displayStatus', 'type': 'str'}, + 'level': {'key': 'level', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'time': {'key': 'time', 'type': 'str'}, + } + + def __init__(self, *, code: str=None, display_status: str=None, level="Information", message: str=None, time: str=None, **kwargs) -> None: + super(ExtensionStatus, self).__init__(**kwargs) + self.code = code + self.display_status = display_status + self.level = level + self.message = message + self.time = time + + +class Result(Model): + """Sample result definition. + + :param sample_property: Sample property of type string + :type sample_property: str + """ + + _attribute_map = { + 'sample_property': {'key': 'sampleProperty', 'type': 'str'}, + } + + def __init__(self, *, sample_property: str=None, **kwargs) -> None: + super(Result, self).__init__(**kwargs) + self.sample_property = sample_property + + +class Scope(Model): + """Scope of the extensionInstance. It can be either Cluster or Namespace; but + not both. + + :param cluster: Specifies that the scope of the extensionInstance is + Cluster + :type cluster: ~azure.mgmt.kubernetesconfiguration.models.ScopeCluster + :param namespace: Specifies that the scope of the extensionInstance is + Namespace + :type namespace: ~azure.mgmt.kubernetesconfiguration.models.ScopeNamespace + """ + + _attribute_map = { + 'cluster': {'key': 'cluster', 'type': 'ScopeCluster'}, + 'namespace': {'key': 'namespace', 'type': 'ScopeNamespace'}, + } + + def __init__(self, *, cluster=None, namespace=None, **kwargs) -> None: + super(Scope, self).__init__(**kwargs) + self.cluster = cluster + self.namespace = namespace + + +class ScopeCluster(Model): + """Specifies that the scope of the extensionInstance is Cluster. + + :param release_namespace: Namespace where the extension Release must be + placed, for a Cluster scoped extensionInstance. If this namespace does + not exist, it will be created + :type release_namespace: str + """ + + _attribute_map = { + 'release_namespace': {'key': 'releaseNamespace', 'type': 'str'}, + } + + def __init__(self, *, release_namespace: str=None, **kwargs) -> None: + super(ScopeCluster, self).__init__(**kwargs) + self.release_namespace = release_namespace + + +class ScopeNamespace(Model): + """Specifies that the scope of the extensionInstance is Namespace. + + :param target_namespace: Namespace where the extensionInstance will be + created for an Namespace scoped extensionInstance. If this namespace does + not exist, it will be created + :type target_namespace: str + """ + + _attribute_map = { + 'target_namespace': {'key': 'targetNamespace', 'type': 'str'}, + } + + def __init__(self, *, target_namespace: str=None, **kwargs) -> None: + super(ScopeNamespace, self).__init__(**kwargs) + self.target_namespace = target_namespace diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_paged_models.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_paged_models.py new file mode 100644 index 00000000000..8f2e7eca24e --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_paged_models.py @@ -0,0 +1,40 @@ +# 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 msrest.paging import Paged + + +class ResourceProviderOperationPaged(Paged): + """ + A paging container for iterating over a list of :class:`ResourceProviderOperation ` object + """ + + _attribute_map = { + 'next_link': {'key': 'nextLink', 'type': 'str'}, + 'current_page': {'key': 'value', 'type': '[ResourceProviderOperation]'} + } + + def __init__(self, *args, **kwargs): + + super(ResourceProviderOperationPaged, self).__init__(*args, **kwargs) +class ExtensionInstanceForListPaged(Paged): + """ + A paging container for iterating over a list of :class:`ExtensionInstanceForList ` object + """ + + _attribute_map = { + 'next_link': {'key': 'nextLink', 'type': 'str'}, + 'current_page': {'key': 'value', 'type': '[ExtensionInstanceForList]'} + } + + def __init__(self, *args, **kwargs): + + super(ExtensionInstanceForListPaged, self).__init__(*args, **kwargs) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/__init__.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/__init__.py new file mode 100644 index 00000000000..e8f158b24a3 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/__init__.py @@ -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 ._k8s_extensions_operations import K8sExtensionsOperations + +__all__ = [ + 'K8sExtensionsOperations', +] diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_k8s_extensions_operations.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_k8s_extensions_operations.py new file mode 100644 index 00000000000..716aefd7b06 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_k8s_extensions_operations.py @@ -0,0 +1,452 @@ +# 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. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse + +from .. import models + + +class K8sExtensionsOperations(object): + """K8sExtensionsOperations operations. + + You should not instantiate directly this class, but create a Client instance that will create it for you and attach + it as attribute. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: The API version to be used with the HTTP request. Constant value: "2020-07-01-preview". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-07-01-preview" + + self.config = config + + def get( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, + custom_headers=None, raw=False, **operation_config): + """Gets details of the Kubernetes Cluster Extension Instance. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters) or appliances (for Arc Appliances). Possible + values include: 'managedClusters','connectedClusters', 'appliances' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param extension_instance_name: Name of an instance of the Extension. + :type extension_instance_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: ExtensionInstance or ClientRawResponse if raw=true + :rtype: ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstance + or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.get.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", + self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + if response.status_code == 200: + deserialized = self._deserialize('ExtensionInstance', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} + + def create( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, + extension_instance, custom_headers=None, raw=False, **operation_config): + """Create a new Kubernetes Cluster Extension Instance. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters) or appliances (for Arc Appliances). Possible + values include: 'managedClusters','connectedClusters', 'appliances' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param extension_instance_name: Name of an instance of the Extension. + :type extension_instance_name: str + :param extension_instance: Properties necessary to Create an Extension + Instance. + :type extension_instance: + ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstanceForCreate + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: ExtensionInstance or ClientRawResponse if raw=true + :rtype: ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstance + or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.create.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", + self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(extension_instance, 'ExtensionInstanceForCreate') + + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + if response.status_code == 200: + deserialized = self._deserialize('ExtensionInstance', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + create.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}' + '/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/' + 'extensions/{extensionInstanceName}'} + + def update( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, + extension_instance, custom_headers=None, raw=False, **operation_config): + """Update an existing Kubernetes Cluster Extension Instance. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters) or appliances (for Arc Appliances). Possible + values include: 'managedClusters','connectedClusters', 'appliances' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param extension_instance_name: Name of an instance of the Extension. + :type extension_instance_name: str + :param extension_instance: Properties to Update in the Extension + Instance. + :type extension_instance: + ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstanceUpdate + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: ExtensionInstance or ClientRawResponse if raw=true + :rtype: ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstance + or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.update.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", + self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(extension_instance, 'ExtensionInstanceUpdate') + + # Construct and send request + request = self._client.patch(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + if response.status_code == 200: + deserialized = self._deserialize('ExtensionInstance', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}' + '/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/' + 'extensions/{extensionInstanceName}'} + + def delete( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, + custom_headers=None, raw=False, **operation_config): + """Delete a Kubernetes Cluster Extension Instance. This will cause the + Agent to Uninstall the extension instance from the cluster. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters) or appliances (for Arc Appliances). Possible + values include: 'managedClusters','connectedClusters', 'appliances' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param extension_instance_name: Name of an instance of the Extension. + :type extension_instance_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.delete.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", + self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 204]: + raise models.ErrorResponseException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + delete.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}' + '/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/' + 'extensions/{extensionInstanceName}'} + + def list( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, custom_headers=None, raw=False, + **operation_config): + """List all Extension Instances. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters) or appliances (for Arc Appliances). Possible + values include: 'managedClusters','connectedClusters', 'appliances' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: An iterator like instance of ExtensionInstanceForList + :rtype: + ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstanceForListPaged[~azure.mgmt.kubernetesconfiguration.models.ExtensionInstanceForList] + :raises: + :class:`ErrorResponseException` + """ + def prepare_request(next_link=None): + if not next_link: + # Construct URL + url = self.list.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, + 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + else: + url = next_link + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", + self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + return request + + def internal_paging(next_link=None): + request = prepare_request(next_link) + + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + return response + + # Deserialize response + header_dict = None + if raw: + header_dict = {} + deserialized = models.ExtensionInstanceForListPaged(internal_paging, self._deserialize.dependencies, + header_dict) + + return deserialized + list.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}' + '/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/' + 'extensions'} diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/version.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/version.py new file mode 100644 index 00000000000..e0ec669828c --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/version.py @@ -0,0 +1,13 @@ +# 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. +# -------------------------------------------------------------------------- + +VERSION = "0.1.0" + diff --git a/src/k8s-extension/setup.cfg b/src/k8s-extension/setup.cfg new file mode 100644 index 00000000000..5eab412034f --- /dev/null +++ b/src/k8s-extension/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py new file mode 100644 index 00000000000..c5fdcbfc5a8 --- /dev/null +++ b/src/k8s-extension/setup.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +from codecs import open +from setuptools import setup, find_packages +try: + from azure_bdist_wheel import cmdclass +except ImportError: + from distutils import log as logger + logger.warn("Wheel is not available, disabling bdist_wheel hook") + +# TODO: Confirm this is the right version number you want and it matches your +# HISTORY.rst entry. +VERSION = '0.1PP.14' + +# The full list of classifiers is available at +# https://pypi.python.org/pypi?%3Aaction=list_classifiers +CLASSIFIERS = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'License :: OSI Approved :: MIT License', +] + +# TODO: Add any additional SDK dependencies here +DEPENDENCIES = [] + +with open('README.rst', 'r', encoding='utf-8') as f: + README = f.read() +with open('HISTORY.rst', 'r', encoding='utf-8') as f: + HISTORY = f.read() + +setup( + name='k8s-extension', + version=VERSION, + description='Microsoft Azure Command-Line Tools K8s-extension Extension', + # TODO: Update author and email, if applicable + author='Microsoft Corporation', + author_email='azpycli@microsoft.com', + # TODO: consider pointing directly to your source code instead of the generic repo + url='https://github.com/Azure/azure-cli-extensions', + long_description=README + '\n\n' + HISTORY, + license='MIT', + classifiers=CLASSIFIERS, + packages=find_packages(), + install_requires=DEPENDENCIES, + package_data={'azext_k8s_extension': ['azext_metadata.json']}, +) From 3e2ea64c089ece94daa605788d308fc4193a064f Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Wed, 10 Mar 2021 14:22:56 -0800 Subject: [PATCH 05/86] Update pipelines file --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 77ff718d967..3e1ef1ccca5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -22,7 +22,7 @@ stages: displayName: "K8s-Extension Test Suite" variables: K8S_EXTENSION_REPO_PATH: $(Agent.BuildDirectory)/s/compute-HybridMgmt-K8sPartnerExtensionTest - CLI_REPO_PATH: $(Agent.BuildDirectory)/s/azure-cli-extensions-pr + CLI_REPO_PATH: $(Agent.BuildDirectory)/s/azure-cli-extensions EXTENSION_NAME: "k8s-extension" EXTENSION_FILE_NAME: "k8s_extension" SUBSCRIPTION_ID: "15c06b1b-01d6-407b-bb21-740b8617dea3" From 9bbc0e462fd0d027ff32099b2657c530522b268c Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Wed, 10 Mar 2021 14:57:51 -0800 Subject: [PATCH 06/86] Update CODEOWNERS --- .github/CODEOWNERS | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 66f026834e8..435dbbae79f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -124,9 +124,11 @@ /src/ssh/ @rlrossiter @danybeam @arrownj -/src/k8sconfiguration/ @NarayanThiru +/src/k8sconfiguration/ @NarayanThiru @jonathan-innis -/src/k8s-configuration/ @NarayanThiru +/src/k8s-configuration/ @NarayanThiru @jonathan-innis + +/src/k8s-extension/ @NarayanThiru @jonathan-innis /src/log-analytics-solution/ @zhoxing-ms From 8d46cbc41b4469cb5795889c32ed28fa348528a5 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Thu, 11 Mar 2021 11:35:18 -0800 Subject: [PATCH 07/86] Update private preview pipelines --- azure-pipelines.yml | 72 +++++++++++++++++-- .../azext_k8s_extension/_consts.py | 7 ++ .../azext_k8s_extension/_consts_private.py | 7 ++ .../azext_k8s_extension/_help.py | 13 ++-- .../azext_k8s_extension/_params.py | 3 +- .../azext_k8s_extension/commands.py | 3 +- src/k8s-extension/setup.py | 3 +- 7 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 src/k8s-extension/azext_k8s_extension/_consts.py create mode 100644 src/k8s-extension/azext_k8s_extension/_consts_private.py diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3e1ef1ccca5..203370976cf 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -23,11 +23,12 @@ stages: variables: K8S_EXTENSION_REPO_PATH: $(Agent.BuildDirectory)/s/compute-HybridMgmt-K8sPartnerExtensionTest CLI_REPO_PATH: $(Agent.BuildDirectory)/s/azure-cli-extensions - EXTENSION_NAME: "k8s-extension" - EXTENSION_FILE_NAME: "k8s_extension" SUBSCRIPTION_ID: "15c06b1b-01d6-407b-bb21-740b8617dea3" RESOURCE_GROUP: "K8sPartnerExtensionTest" BASE_CLUSTER_NAME: "k8s-extension-cluster" + + EXTENSION_NAME: "k8s-extension" + EXTENSION_FILE_NAME: "k8s_extension" jobs: - job: K8sExtensionTestSuite displayName: "Run the Test Suite" @@ -36,7 +37,6 @@ stages: steps: - checkout: self - checkout: K8sPartnerExtensionTest - - bash: | echo "Installing helm3" curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 @@ -55,6 +55,7 @@ stages: versionSpec: 3.6 - bash: | set -ev + echo "Building extension ${EXTENSION_NAME}..." # prepare and activate virtualenv pip install virtualenv @@ -71,12 +72,12 @@ stages: azdev extension build $(EXTENSION_NAME) workingDirectory: $(CLI_REPO_PATH) - displayName: "Setup and Build $(EXTENSION_NAME) with azdev" + displayName: "Setup and Build Extension with azdev" - bash: | K8S_EXTENSION_VERSION=$(ls ${EXTENSION_FILE_NAME}* | cut -d "-" -f2) echo "##vso[task.setvariable variable=K8S_EXTENSION_VERSION]$K8S_EXTENSION_VERSION" - cp * $(K8S_EXTENSION_REPO_PATH)/extensions + cp * $(K8S_EXTENSION_REPO_PATH)/bin workingDirectory: $(CLI_REPO_PATH)/dist displayName: "Copy the Built .whl to Extension Test Path" @@ -142,6 +143,67 @@ stages: .\Cleanup.ps1 -CI workingDirectory: $(K8S_EXTENSION_REPO_PATH) condition: succeededOrFailed() + +- stage: BuildPublishExtension + dependsOn: [] + displayName: "Build and Publish the Extension Artifact" + variables: + CLI_REPO_PATH: $(Agent.BuildDirectory)/s + IS_PRIVATE_BRANCH: $[or(eq(variables['Build.SourceBranchName'], 'refs/heads/k8s-extension/private-preview'), eq(variables['System.PullRequest.TargetBranch'], 'refs/heads/k8s-extension/private-preview'))] + jobs: + - job: BuildPublishExtension + displayName: "Build and Publish the Extension Artifact" + pool: + vmImage: 'ubuntu-16.04' + steps: + - bash: | + if [[ $IS_PRIVATE_BRANCH ]]; then + echo "Using the private preview of k8s-extension to build..." + + cp $(CLI_REPO_PATH)/src/k8s-extension $(CLI_REPO_PATH)/src/k8s-extension-private -r + cp $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension/_consts_private.py $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension/_consts.py + + EXTENSION_NAME="k8s-extension-private" + EXTENSION_FILE_NAME="k8s_extension_private" + + echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" + echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" + else + echo "Using the public version of k8s-extension to build..." + + EXTENSION_NAME="k8s-extension" + EXTENSION_FILE_NAME="k8s_extension" + + echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" + echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" + fi + displayName: "Copy Files, Set Variables based on Private Branch" + - task: UsePythonVersion@0 + displayName: 'Use Python 3.6' + inputs: + versionSpec: 3.6 + - bash: | + set -ev + echo "Building extension ${EXTENSION_NAME}..." + + # prepare and activate virtualenv + pip install virtualenv + python3 -m venv env/ + source env/bin/activate + + # clone azure-cli + pip install azdev + + ls $(CLI_REPO_PATH) + + azdev --version + azdev setup -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) + azdev extension build $(EXTENSION_NAME) + workingDirectory: $(CLI_REPO_PATH) + displayName: "Setup and Build Extension with azdev" + - task: PublishBuildArtifacts@1 + inputs: + pathToPublish: $(CLI_REPO_PATH)/dist - stage: AzureCLIOfficial displayName: "Azure Official CLI Code Checks" diff --git a/src/k8s-extension/azext_k8s_extension/_consts.py b/src/k8s-extension/azext_k8s_extension/_consts.py new file mode 100644 index 00000000000..e9f8156f307 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/_consts.py @@ -0,0 +1,7 @@ +# 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. +# -------------------------------------------------------------------------------------------- + +EXTENSION_NAME = 'k8s-extension' diff --git a/src/k8s-extension/azext_k8s_extension/_consts_private.py b/src/k8s-extension/azext_k8s_extension/_consts_private.py new file mode 100644 index 00000000000..18a01637c2d --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/_consts_private.py @@ -0,0 +1,7 @@ +# 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. +# -------------------------------------------------------------------------------------------- + +EXTENSION_NAME = 'k8s-extension-private' diff --git a/src/k8s-extension/azext_k8s_extension/_help.py b/src/k8s-extension/azext_k8s_extension/_help.py index 69011bb9d92..64e4be612ea 100644 --- a/src/k8s-extension/azext_k8s_extension/_help.py +++ b/src/k8s-extension/azext_k8s_extension/_help.py @@ -5,34 +5,35 @@ # -------------------------------------------------------------------------------------------- from knack.help_files import helps # pylint: disable=unused-import +import azext_k8s_extension._consts as consts -helps['k8s-extension'] = """ +helps[f'{consts.EXTENSION_NAME}'] = """ type: group short-summary: Commands to manage K8s-extensions. """ -helps['k8s-extension create'] = """ +helps[f'{consts.EXTENSION_NAME} create'] = """ type: command short-summary: Create a K8s-extension. """ -helps['k8s-extension list'] = """ +helps[f'{consts.EXTENSION_NAME} list'] = """ type: command short-summary: List K8s-extensions. """ -helps['k8s-extension delete'] = """ +helps[f'{consts.EXTENSION_NAME} delete'] = """ type: command short-summary: Delete a K8s-extension. """ -helps['k8s-extension show'] = """ +helps[f'{consts.EXTENSION_NAME} show'] = """ type: command short-summary: Show details of a K8s-extension. """ -helps['k8s-extension update'] = """ +helps[f'{consts.EXTENSION_NAME} update'] = """ type: command short-summary: Update a K8s-extension. """ diff --git a/src/k8s-extension/azext_k8s_extension/_params.py b/src/k8s-extension/azext_k8s_extension/_params.py index 0e870204887..d96fe3c2ba7 100644 --- a/src/k8s-extension/azext_k8s_extension/_params.py +++ b/src/k8s-extension/azext_k8s_extension/_params.py @@ -9,6 +9,7 @@ tags_type ) from azure.cli.core.commands.validators import get_default_location_from_resource_group +import azext_k8s_extension._consts as consts from azext_k8s_extension.action import ( AddConfigurationSettings, @@ -17,7 +18,7 @@ def load_arguments(self, _): - with self.argument_context('k8s-extension') as c: + with self.argument_context(consts.EXTENSION_NAME) as c: c.argument('tags', tags_type) c.argument('location', validator=get_default_location_from_resource_group) diff --git a/src/k8s-extension/azext_k8s_extension/commands.py b/src/k8s-extension/azext_k8s_extension/commands.py index 63fe78f7d2a..ff72ab62e08 100644 --- a/src/k8s-extension/azext_k8s_extension/commands.py +++ b/src/k8s-extension/azext_k8s_extension/commands.py @@ -6,6 +6,7 @@ # pylint: disable=line-too-long from azure.cli.core.commands import CliCommandType from azext_k8s_extension._client_factory import (cf_k8s_extension, cf_k8s_extension_operation) +import azext_k8s_extension._consts as consts def load_command_table(self, _): @@ -14,7 +15,7 @@ def load_command_table(self, _): operations_tmpl='azext_k8s_extension.vendored_sdks.operations#K8sExtensionsOperations.{}', client_factory=cf_k8s_extension) - with self.command_group('k8s-extension', k8s_extension_sdk, client_factory=cf_k8s_extension_operation, + with self.command_group(consts.EXTENSION_NAME, k8s_extension_sdk, client_factory=cf_k8s_extension_operation, is_preview=True) \ as g: g.custom_command('create', 'create_k8s_extension') diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index c5fdcbfc5a8..878f7f8d87a 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -8,6 +8,7 @@ from codecs import open from setuptools import setup, find_packages +import azext_k8s_extension._consts as consts try: from azure_bdist_wheel import cmdclass except ImportError: @@ -41,7 +42,7 @@ HISTORY = f.read() setup( - name='k8s-extension', + name=consts.EXTENSION_NAME, version=VERSION, description='Microsoft Azure Command-Line Tools K8s-extension Extension', # TODO: Update author and email, if applicable From 6c3ba419f9e0c5d191d0ba4fdce431455c1c5c47 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Thu, 11 Mar 2021 14:56:20 -0800 Subject: [PATCH 08/86] Remove open service mesh from public release --- .../azext_k8s_extension/custom.py | 2 - .../partner_extensions/OpenServiceMesh.py | 95 ------------------- 2 files changed, 97 deletions(-) delete mode 100644 src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index 469b4059dee..4b0657c1814 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -19,7 +19,6 @@ from .partner_extensions.ContainerInsights import ContainerInsights from .partner_extensions.AzureDefender import AzureDefender -from .partner_extensions.OpenServiceMesh import OpenServiceMesh from .partner_extensions.DefaultExtension import DefaultExtension from ._client_factory import cf_resources @@ -32,7 +31,6 @@ def ExtensionFactory(extension_name): extension_map = { 'microsoft.azuremonitor.containers': ContainerInsights, 'microsoft.azuredefender.kubernetes': AzureDefender, - 'microsoft.openservicemesh': OpenServiceMesh, } # Return the extension if we find it in the map, else return the default diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py deleted file mode 100644 index b9e530877dc..00000000000 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py +++ /dev/null @@ -1,95 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - -# pylint: disable=unused-argument - -from azure.cli.core.azclierror import InvalidArgumentValueError, RequiredArgumentMissingError -from knack.log import get_logger - -from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceForCreate -from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate -from azext_k8s_extension.vendored_sdks.models import ScopeCluster -from azext_k8s_extension.vendored_sdks.models import Scope - -from .PartnerExtensionModel import PartnerExtensionModel - -logger = get_logger(__name__) - - -class OpenServiceMesh(PartnerExtensionModel): - def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, - scope, auto_upgrade_minor_version, release_train, version, target_namespace, - release_namespace, configuration_settings, configuration_protected_settings, - configuration_settings_file, configuration_protected_settings_file): - - """ExtensionType 'microsoft.openservicemesh' specific validations & defaults for Create - Must create and return a valid 'ExtensionInstanceForCreate' object. - - """ - # NOTE-1: Replace default scope creation with your customization, if required - # Scope must always be cluster - ext_scope = None - if scope == 'namespace': - raise InvalidArgumentValueError("Invalid scope '{}'. This extension can be installed " - "only at 'cluster' scope.".format(scope)) - - scope_cluster = ScopeCluster(release_namespace=release_namespace) - ext_scope = Scope(cluster=scope_cluster, namespace=None) - - valid_release_trains = ['staging', 'pilot'] - # If release-train is not input, set it to 'stable' - if release_train is None: - raise RequiredArgumentMissingError( - "A release-train must be provided. Valid values are 'staging', 'pilot'." - ) - - if release_train.lower() in valid_release_trains: - # version is a mandatory if release-train is staging or pilot - if version is None: - raise RequiredArgumentMissingError( - "A version must be provided for release-train {}.".format(release_train) - ) - # If the release-train is 'staging' or 'pilot' then auto-upgrade-minor-version MUST be set to False - if auto_upgrade_minor_version or auto_upgrade_minor_version is None: - auto_upgrade_minor_version = False - logger.warning("Setting auto-upgrade-minor-version to False since release-train is '%s'", release_train) - else: - raise InvalidArgumentValueError( - "Invalid release-train '{}'. Valid values are 'staging', 'pilot'.".format(release_train) - ) - - # NOTE-2: Return a valid ExtensionInstanceForCreate object, Instance name and flag for Identity - create_identity = False - extension_instance = ExtensionInstanceForCreate( - extension_type=extension_type, - auto_upgrade_minor_version=auto_upgrade_minor_version, - release_train=release_train, - version=version, - scope=ext_scope, - configuration_settings=configuration_settings, - configuration_protected_settings=configuration_protected_settings, - identity=None, - location="" - ) - return extension_instance, name, create_identity - - def Update(self, extension, auto_upgrade_minor_version, release_train, version): - """ExtensionType 'microsoft.openservicemesh' specific validations & defaults for Update - Must create and return a valid 'ExtensionInstanceUpdate' object. - - """ - # auto-upgrade-minor-version MUST be set to False if release_train is staging or pilot - if release_train.lower() in 'staging' 'pilot': - if auto_upgrade_minor_version or auto_upgrade_minor_version is None: - auto_upgrade_minor_version = False - # Set version to None to always get the latest version - user cannot override - version = None - logger.warning("Setting auto-upgrade-minor-version to False since release-train is '%s'", release_train) - - return ExtensionInstanceUpdate( - auto_upgrade_minor_version=auto_upgrade_minor_version, - release_train=release_train, - version=version - ) From 43c67964d19c0a41ef29ebf56b44cad64bc4f7f3 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Thu, 11 Mar 2021 15:10:35 -0800 Subject: [PATCH 09/86] Update pipeline files --- azure-pipelines.yml | 459 ++++++++++++--------------------------- k8s-custom-pipelines.yml | 346 +++++++++++++++++++++++++++++ 2 files changed, 488 insertions(+), 317 deletions(-) create mode 100644 k8s-custom-pipelines.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 203370976cf..565e9c56793 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,219 +1,128 @@ resources: - repositories: - - repository: K8sPartnerExtensionTest - type: git - endpoint: AzureReposConnection - name: One/compute-HybridMgmt-K8sPartnerExtensionTest +- repo: self trigger: batch: true branches: include: - - k8s-extension/public-preview - - k8s-extension/private-preview + - '*' + pr: branches: include: - - k8s-extension/public-preview - - k8s-extension/private-preview - -stages: -- stage: K8sExtensionTestSuite - displayName: "K8s-Extension Test Suite" - variables: - K8S_EXTENSION_REPO_PATH: $(Agent.BuildDirectory)/s/compute-HybridMgmt-K8sPartnerExtensionTest - CLI_REPO_PATH: $(Agent.BuildDirectory)/s/azure-cli-extensions - SUBSCRIPTION_ID: "15c06b1b-01d6-407b-bb21-740b8617dea3" - RESOURCE_GROUP: "K8sPartnerExtensionTest" - BASE_CLUSTER_NAME: "k8s-extension-cluster" - - EXTENSION_NAME: "k8s-extension" - EXTENSION_FILE_NAME: "k8s_extension" - jobs: - - job: K8sExtensionTestSuite - displayName: "Run the Test Suite" - pool: - vmImage: 'ubuntu-16.04' - steps: - - checkout: self - - checkout: K8sPartnerExtensionTest - - bash: | - echo "Installing helm3" - curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 - chmod 700 get_helm.sh - ./get_helm.sh - - echo "Installing kubectl" - curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl" - chmod +x ./kubectl - sudo mv ./kubectl /usr/local/bin/kubectl - kubectl version --client - displayName: "Setup the VM with helm3 and kubectl" - - task: UsePythonVersion@0 - displayName: 'Use Python 3.6' - inputs: - versionSpec: 3.6 - - bash: | - set -ev - echo "Building extension ${EXTENSION_NAME}..." - - # prepare and activate virtualenv - pip install virtualenv - python3 -m venv env/ - source env/bin/activate - - # clone azure-cli - pip install azdev - - ls $(CLI_REPO_PATH) - - azdev --version - azdev setup -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) - azdev extension build $(EXTENSION_NAME) - - workingDirectory: $(CLI_REPO_PATH) - displayName: "Setup and Build Extension with azdev" - - - bash: | - K8S_EXTENSION_VERSION=$(ls ${EXTENSION_FILE_NAME}* | cut -d "-" -f2) - echo "##vso[task.setvariable variable=K8S_EXTENSION_VERSION]$K8S_EXTENSION_VERSION" - cp * $(K8S_EXTENSION_REPO_PATH)/bin - workingDirectory: $(CLI_REPO_PATH)/dist - displayName: "Copy the Built .whl to Extension Test Path" - - - bash: | - RAND_STR=$RANDOM - AKS_CLUSTER_NAME="${BASE_CLUSTER_NAME}-${RAND_STR}-aks" - ARC_CLUSTER_NAME="${BASE_CLUSTER_NAME}-${RAND_STR}-arc" - - JSON_STRING=$(jq -n \ - --arg SUB_ID "$SUBSCRIPTION_ID" \ - --arg RG "$RESOURCE_GROUP" \ - --arg AKS_CLUSTER_NAME "$AKS_CLUSTER_NAME" \ - --arg ARC_CLUSTER_NAME "$ARC_CLUSTER_NAME" \ - --arg K8S_EXTENSION_VERSION "$K8S_EXTENSION_VERSION" \ - '{subscriptionId: $SUB_ID, resourceGroup: $RG, aksClusterName: $AKS_CLUSTER_NAME, arcClusterName: $ARC_CLUSTER_NAME, extensionVersion: {"k8s-extension": $K8S_EXTENSION_VERSION, connectedk8s: "1.0.0"}}') - echo $JSON_STRING > settings.json - cat settings.json - workingDirectory: $(K8S_EXTENSION_REPO_PATH) - displayName: "Generate a settings.json file" - - - bash : | - echo "Downloading the kind script" - curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.9.0/kind-linux-amd64 - chmod +x ./kind - ./kind create cluster - displayName: "Create and Start the Kind cluster" - - - task: AzureCLI@2 - displayName: Bootstrap - inputs: - azureSubscription: AzureResourceConnection - scriptType: pscore - scriptLocation: inlineScript - inlineScript: | - .\Bootstrap.ps1 -CI - workingDirectory: $(K8S_EXTENSION_REPO_PATH) - - - task: AzureCLI@2 - displayName: Run the Test Suite - inputs: - azureSubscription: AzureResourceConnection - scriptType: pscore - scriptLocation: inlineScript - inlineScript: | - .\Test.ps1 -CI - workingDirectory: $(K8S_EXTENSION_REPO_PATH) - continueOnError: true - - - task: PublishTestResults@2 - inputs: - testResultsFormat: 'JUnit' - testResultsFiles: '**/TestResults.xml' - failTaskOnFailedTests: true - condition: succeededOrFailed() - - - task: AzureCLI@2 - displayName: Cleanup - inputs: - azureSubscription: AzureResourceConnection - scriptType: pscore - scriptLocation: inlineScript - inlineScript: | - .\Cleanup.ps1 -CI - workingDirectory: $(K8S_EXTENSION_REPO_PATH) - condition: succeededOrFailed() - -- stage: BuildPublishExtension - dependsOn: [] - displayName: "Build and Publish the Extension Artifact" - variables: - CLI_REPO_PATH: $(Agent.BuildDirectory)/s - IS_PRIVATE_BRANCH: $[or(eq(variables['Build.SourceBranchName'], 'refs/heads/k8s-extension/private-preview'), eq(variables['System.PullRequest.TargetBranch'], 'refs/heads/k8s-extension/private-preview'))] - jobs: - - job: BuildPublishExtension - displayName: "Build and Publish the Extension Artifact" - pool: - vmImage: 'ubuntu-16.04' - steps: - - bash: | - if [[ $IS_PRIVATE_BRANCH ]]; then - echo "Using the private preview of k8s-extension to build..." - - cp $(CLI_REPO_PATH)/src/k8s-extension $(CLI_REPO_PATH)/src/k8s-extension-private -r - cp $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension/_consts_private.py $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension/_consts.py - - EXTENSION_NAME="k8s-extension-private" - EXTENSION_FILE_NAME="k8s_extension_private" - - echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" - echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" - else - echo "Using the public version of k8s-extension to build..." - - EXTENSION_NAME="k8s-extension" - EXTENSION_FILE_NAME="k8s_extension" - - echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" - echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" - fi - displayName: "Copy Files, Set Variables based on Private Branch" + - '*' + +jobs: +- job: CredScan + displayName: "Credential Scan" + pool: + vmImage: "windows-2019" + steps: + - task: ms-codeanalysis.vss-microsoft-security-code-analysis-devops.build-task-credscan.CredScan@2 + displayName: 'Run Credential Scanner' + inputs: + toolMajorVersion: V2 + suppressionsFile: './scripts/ci/credscan/CredScanSuppressions.json' + - task: ms-codeanalysis.vss-microsoft-security-code-analysis-devops.build-task-postanalysis.PostAnalysis@1 + displayName: 'Post Analysis' + inputs: + AllTools: false + BinSkim: false + CredScan: true + RoslynAnalyzers: false + TSLint: false + ToolLogsNotFoundAction: 'Standard' + +- job: CheckLicenseHeader + displayName: "Check License" + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.6' + inputs: + versionSpec: 3.6 + - bash: | + set -ev + + # prepare and activate virtualenv + python -m venv env/ + + chmod +x ./env/bin/activate + source ./env/bin/activate + + # clone azure-cli + git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli + + pip install -q azdev + + azdev setup -c ../azure-cli -r ./ + + azdev --version + az --version + + azdev verify license + +- job: StaticAnalysis + displayName: "Static Analysis" + pool: + vmImage: 'ubuntu-16.04' + steps: - task: UsePythonVersion@0 displayName: 'Use Python 3.6' inputs: versionSpec: 3.6 - - bash: | - set -ev - echo "Building extension ${EXTENSION_NAME}..." - - # prepare and activate virtualenv - pip install virtualenv - python3 -m venv env/ - source env/bin/activate - - # clone azure-cli - pip install azdev - - ls $(CLI_REPO_PATH) + - bash: pip install wheel==0.30.0 pylint==1.9.5 flake8==3.5.0 requests + displayName: 'Install wheel, pylint, flake8, requests' + - bash: python scripts/ci/source_code_static_analysis.py + displayName: "Static Analysis" + +- job: IndexVerify + displayName: "Verify Extensions Index" + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.7' + inputs: + versionSpec: 3.7 + - bash: | + #!/usr/bin/env bash + set -ev + pip install wheel==0.30.0 requests packaging + export CI="ADO" + python ./scripts/ci/test_index.py -v + displayName: "Verify Extensions Index" - azdev --version - azdev setup -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) - azdev extension build $(EXTENSION_NAME) - workingDirectory: $(CLI_REPO_PATH) - displayName: "Setup and Build Extension with azdev" - - task: PublishBuildArtifacts@1 - inputs: - pathToPublish: $(CLI_REPO_PATH)/dist - -- stage: AzureCLIOfficial - displayName: "Azure Official CLI Code Checks" - dependsOn: [] - jobs: - - job: CheckLicenseHeader - displayName: "Check License" - pool: - vmImage: 'ubuntu-16.04' - steps: +- job: SourceTests + displayName: "Integration Tests, Build Tests" + pool: + vmImage: 'ubuntu-16.04' + strategy: + matrix: + Python36: + python.version: '3.6' + Python38: + python.version: '3.8' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python $(python.version)' + inputs: + versionSpec: '$(python.version)' + - bash: pip install wheel==0.30.0 + displayName: 'Install wheel==0.30.0' + - bash: ./scripts/ci/test_source.sh + displayName: 'Run integration test and build test' + env: + ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) + ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) + +- job: LintModifiedExtensions + displayName: "CLI Linter on Modified Extensions" + condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest')) + pool: + vmImage: 'ubuntu-16.04' + steps: - task: UsePythonVersion@0 displayName: 'Use Python 3.6' inputs: @@ -222,125 +131,41 @@ stages: set -ev # prepare and activate virtualenv - python -m venv env/ - - chmod +x ./env/bin/activate - source ./env/bin/activate + pip install virtualenv + python -m virtualenv venv/ + source ./venv/bin/activate # clone azure-cli - git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli - - pip install -q azdev + git clone --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli - azdev setup -c ../azure-cli -r ./ + pip install azdev azdev --version - az --version - - azdev verify license - - - job: StaticAnalysis - displayName: "Static Analysis" - pool: - vmImage: 'ubuntu-16.04' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.6' - inputs: - versionSpec: 3.6 - - bash: pip install wheel==0.30.0 pylint==1.9.5 flake8==3.5.0 requests - displayName: 'Install wheel, pylint, flake8, requests' - - bash: python scripts/ci/source_code_static_analysis.py - displayName: "Static Analysis" - - - job: IndexVerify - displayName: "Verify Extensions Index" - pool: - vmImage: 'ubuntu-16.04' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.7' - inputs: - versionSpec: 3.7 - - bash: | - #!/usr/bin/env bash - set -ev - pip install wheel==0.30.0 requests packaging - export CI="ADO" - python ./scripts/ci/test_index.py -v - displayName: "Verify Extensions Index" - - - job: SourceTests - displayName: "Integration Tests, Build Tests" - pool: - vmImage: 'ubuntu-16.04' - strategy: - matrix: - Python36: - python.version: '3.6' - Python38: - python.version: '3.8' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python $(python.version)' - inputs: - versionSpec: '$(python.version)' - - bash: pip install wheel==0.30.0 - displayName: 'Install wheel==0.30.0' - - bash: ./scripts/ci/test_source.sh - displayName: 'Run integration test and build test' - env: - ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) - ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) - - - job: LintModifiedExtensions - displayName: "CLI Linter on Modified Extensions" - pool: - vmImage: 'ubuntu-16.04' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.6' - inputs: - versionSpec: 3.6 - - bash: | - set -ev - - # prepare and activate virtualenv - pip install virtualenv - python -m virtualenv venv/ - source ./venv/bin/activate - - # clone azure-cli - git clone --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli - - pip install azdev - - azdev --version - azdev setup -c ../azure-cli -r ./ -e k8s-extension - - # overwrite the default AZURE_EXTENSION_DIR set by ADO - AZURE_EXTENSION_DIR=~/.azure/cliextensions az --version - - AZURE_EXTENSION_DIR=~/.azure/cliextensions azdev linter --include-whl-extensions k8s-extension - displayName: "CLI Linter on Modified Extension" - env: - ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) - ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) + azdev setup -c ../azure-cli -r ./ - - job: IndexRefDocVerify - displayName: "Verify Ref Docs" - pool: - vmImage: 'ubuntu-16.04' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.7' - inputs: - versionSpec: 3.7 - - bash: pip install wheel==0.30.0 - displayName: 'Install wheel==0.30.0' - - task: Bash@3 - displayName: "Verify Extension Ref Docs" - inputs: - targetType: 'filePath' - filePath: scripts/ci/test_index_ref_doc.sh + # overwrite the default AZURE_EXTENSION_DIR set by ADO + AZURE_EXTENSION_DIR=~/.azure/cliextensions az --version + + AZURE_EXTENSION_DIR=~/.azure/cliextensions python scripts/ci/verify_linter.py + displayName: "CLI Linter on Modified Extension" + env: + ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) + ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) + +- job: IndexRefDocVerify + displayName: "Verify Ref Docs" + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.7' + inputs: + versionSpec: 3.7 + - bash: pip install wheel==0.30.0 + displayName: 'Install wheel==0.30.0' + - task: Bash@3 + displayName: "Verify Extension Ref Docs" + inputs: + targetType: 'filePath' + filePath: scripts/ci/test_index_ref_doc.sh \ No newline at end of file diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml new file mode 100644 index 00000000000..b0fa1857598 --- /dev/null +++ b/k8s-custom-pipelines.yml @@ -0,0 +1,346 @@ +resources: + repositories: + - repository: K8sPartnerExtensionTest + type: git + endpoint: AzureReposConnection + name: One/compute-HybridMgmt-K8sPartnerExtensionTest + +trigger: + batch: true + branches: + include: + - k8s-extension/public-preview + - k8s-extension/private-preview +pr: + branches: + include: + - k8s-extension/public-preview + - k8s-extension/private-preview + +stages: +- stage: K8sExtensionTestSuite + displayName: "K8s-Extension Test Suite" + variables: + K8S_EXTENSION_REPO_PATH: $(Agent.BuildDirectory)/s/compute-HybridMgmt-K8sPartnerExtensionTest + CLI_REPO_PATH: $(Agent.BuildDirectory)/s/azure-cli-extensions + SUBSCRIPTION_ID: "15c06b1b-01d6-407b-bb21-740b8617dea3" + RESOURCE_GROUP: "K8sPartnerExtensionTest" + BASE_CLUSTER_NAME: "k8s-extension-cluster" + + EXTENSION_NAME: "k8s-extension" + EXTENSION_FILE_NAME: "k8s_extension" + jobs: + - job: K8sExtensionTestSuite + displayName: "Run the Test Suite" + pool: + vmImage: 'ubuntu-16.04' + steps: + - checkout: self + - checkout: K8sPartnerExtensionTest + - bash: | + echo "Installing helm3" + curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 + chmod 700 get_helm.sh + ./get_helm.sh + + echo "Installing kubectl" + curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x ./kubectl + sudo mv ./kubectl /usr/local/bin/kubectl + kubectl version --client + displayName: "Setup the VM with helm3 and kubectl" + - task: UsePythonVersion@0 + displayName: 'Use Python 3.6' + inputs: + versionSpec: 3.6 + - bash: | + set -ev + echo "Building extension ${EXTENSION_NAME}..." + + # prepare and activate virtualenv + pip install virtualenv + python3 -m venv env/ + source env/bin/activate + + # clone azure-cli + pip install azdev + + ls $(CLI_REPO_PATH) + + azdev --version + azdev setup -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) + azdev extension build $(EXTENSION_NAME) + + workingDirectory: $(CLI_REPO_PATH) + displayName: "Setup and Build Extension with azdev" + + - bash: | + K8S_EXTENSION_VERSION=$(ls ${EXTENSION_FILE_NAME}* | cut -d "-" -f2) + echo "##vso[task.setvariable variable=K8S_EXTENSION_VERSION]$K8S_EXTENSION_VERSION" + cp * $(K8S_EXTENSION_REPO_PATH)/bin + workingDirectory: $(CLI_REPO_PATH)/dist + displayName: "Copy the Built .whl to Extension Test Path" + + - bash: | + RAND_STR=$RANDOM + AKS_CLUSTER_NAME="${BASE_CLUSTER_NAME}-${RAND_STR}-aks" + ARC_CLUSTER_NAME="${BASE_CLUSTER_NAME}-${RAND_STR}-arc" + + JSON_STRING=$(jq -n \ + --arg SUB_ID "$SUBSCRIPTION_ID" \ + --arg RG "$RESOURCE_GROUP" \ + --arg AKS_CLUSTER_NAME "$AKS_CLUSTER_NAME" \ + --arg ARC_CLUSTER_NAME "$ARC_CLUSTER_NAME" \ + --arg K8S_EXTENSION_VERSION "$K8S_EXTENSION_VERSION" \ + '{subscriptionId: $SUB_ID, resourceGroup: $RG, aksClusterName: $AKS_CLUSTER_NAME, arcClusterName: $ARC_CLUSTER_NAME, extensionVersion: {"k8s-extension": $K8S_EXTENSION_VERSION, connectedk8s: "1.0.0"}}') + echo $JSON_STRING > settings.json + cat settings.json + workingDirectory: $(K8S_EXTENSION_REPO_PATH) + displayName: "Generate a settings.json file" + + - bash : | + echo "Downloading the kind script" + curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.9.0/kind-linux-amd64 + chmod +x ./kind + ./kind create cluster + displayName: "Create and Start the Kind cluster" + + - task: AzureCLI@2 + displayName: Bootstrap + inputs: + azureSubscription: AzureResourceConnection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + .\Bootstrap.ps1 -CI + workingDirectory: $(K8S_EXTENSION_REPO_PATH) + + - task: AzureCLI@2 + displayName: Run the Test Suite + inputs: + azureSubscription: AzureResourceConnection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + .\Test.ps1 -CI + workingDirectory: $(K8S_EXTENSION_REPO_PATH) + continueOnError: true + + - task: PublishTestResults@2 + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '**/TestResults.xml' + failTaskOnFailedTests: true + condition: succeededOrFailed() + + - task: AzureCLI@2 + displayName: Cleanup + inputs: + azureSubscription: AzureResourceConnection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + .\Cleanup.ps1 -CI + workingDirectory: $(K8S_EXTENSION_REPO_PATH) + condition: succeededOrFailed() + +- stage: BuildPublishExtension + dependsOn: [] + displayName: "Build and Publish the Extension Artifact" + variables: + CLI_REPO_PATH: $(Agent.BuildDirectory)/s + IS_PRIVATE_BRANCH: $[or(eq(variables['Build.SourceBranchName'], 'refs/heads/k8s-extension/private-preview'), eq(variables['System.PullRequest.TargetBranch'], 'refs/heads/k8s-extension/private-preview'))] + jobs: + - job: BuildPublishExtension + displayName: "Build and Publish the Extension Artifact" + pool: + vmImage: 'ubuntu-16.04' + steps: + - bash: | + if [[ $IS_PRIVATE_BRANCH ]]; then + echo "Using the private preview of k8s-extension to build..." + + cp $(CLI_REPO_PATH)/src/k8s-extension $(CLI_REPO_PATH)/src/k8s-extension-private -r + cp $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension/_consts_private.py $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension/_consts.py + + EXTENSION_NAME="k8s-extension-private" + EXTENSION_FILE_NAME="k8s_extension_private" + + echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" + echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" + else + echo "Using the public version of k8s-extension to build..." + + EXTENSION_NAME="k8s-extension" + EXTENSION_FILE_NAME="k8s_extension" + + echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" + echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" + fi + displayName: "Copy Files, Set Variables based on Private Branch" + - task: UsePythonVersion@0 + displayName: 'Use Python 3.6' + inputs: + versionSpec: 3.6 + - bash: | + set -ev + echo "Building extension ${EXTENSION_NAME}..." + + # prepare and activate virtualenv + pip install virtualenv + python3 -m venv env/ + source env/bin/activate + + # clone azure-cli + pip install azdev + + ls $(CLI_REPO_PATH) + + azdev --version + azdev setup -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) + azdev extension build $(EXTENSION_NAME) + workingDirectory: $(CLI_REPO_PATH) + displayName: "Setup and Build Extension with azdev" + - task: PublishBuildArtifacts@1 + inputs: + pathToPublish: $(CLI_REPO_PATH)/dist + +- stage: AzureCLIOfficial + displayName: "Azure Official CLI Code Checks" + dependsOn: [] + jobs: + - job: CheckLicenseHeader + displayName: "Check License" + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.6' + inputs: + versionSpec: 3.6 + - bash: | + set -ev + + # prepare and activate virtualenv + python -m venv env/ + + chmod +x ./env/bin/activate + source ./env/bin/activate + + # clone azure-cli + git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli + + pip install -q azdev + + azdev setup -c ../azure-cli -r ./ + + azdev --version + az --version + + azdev verify license + + - job: StaticAnalysis + displayName: "Static Analysis" + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.6' + inputs: + versionSpec: 3.6 + - bash: pip install wheel==0.30.0 pylint==1.9.5 flake8==3.5.0 requests + displayName: 'Install wheel, pylint, flake8, requests' + - bash: python scripts/ci/source_code_static_analysis.py + displayName: "Static Analysis" + + - job: IndexVerify + displayName: "Verify Extensions Index" + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.7' + inputs: + versionSpec: 3.7 + - bash: | + #!/usr/bin/env bash + set -ev + pip install wheel==0.30.0 requests packaging + export CI="ADO" + python ./scripts/ci/test_index.py -v + displayName: "Verify Extensions Index" + + - job: SourceTests + displayName: "Integration Tests, Build Tests" + pool: + vmImage: 'ubuntu-16.04' + strategy: + matrix: + Python36: + python.version: '3.6' + Python38: + python.version: '3.8' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python $(python.version)' + inputs: + versionSpec: '$(python.version)' + - bash: pip install wheel==0.30.0 + displayName: 'Install wheel==0.30.0' + - bash: ./scripts/ci/test_source.sh + displayName: 'Run integration test and build test' + env: + ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) + ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) + + - job: LintModifiedExtensions + displayName: "CLI Linter on Modified Extensions" + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.6' + inputs: + versionSpec: 3.6 + - bash: | + set -ev + + # prepare and activate virtualenv + pip install virtualenv + python -m virtualenv venv/ + source ./venv/bin/activate + + # clone azure-cli + git clone --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli + + pip install azdev + + azdev --version + + azdev setup -c ../azure-cli -r ./ -e k8s-extension + + # overwrite the default AZURE_EXTENSION_DIR set by ADO + AZURE_EXTENSION_DIR=~/.azure/cliextensions az --version + + AZURE_EXTENSION_DIR=~/.azure/cliextensions azdev linter --include-whl-extensions k8s-extension + displayName: "CLI Linter on Modified Extension" + env: + ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) + ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) + + - job: IndexRefDocVerify + displayName: "Verify Ref Docs" + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.7' + inputs: + versionSpec: 3.7 + - bash: pip install wheel==0.30.0 + displayName: 'Install wheel==0.30.0' + - task: Bash@3 + displayName: "Verify Extension Ref Docs" + inputs: + targetType: 'filePath' + filePath: scripts/ci/test_index_ref_doc.sh \ No newline at end of file From 009a83e95da65566623e25b4ca43c5553ae58cc2 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Thu, 11 Mar 2021 16:17:09 -0800 Subject: [PATCH 10/86] Update public extension pipeline --- k8s-custom-pipelines.yml | 49 +++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index b0fa1857598..a5dbceabea2 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -18,8 +18,8 @@ pr: - k8s-extension/private-preview stages: -- stage: K8sExtensionTestSuite - displayName: "K8s-Extension Test Suite" +- stage: BuildTestPublishExtension + displayName: "Build, Test, and Publish Extension" variables: K8S_EXTENSION_REPO_PATH: $(Agent.BuildDirectory)/s/compute-HybridMgmt-K8sPartnerExtensionTest CLI_REPO_PATH: $(Agent.BuildDirectory)/s/azure-cli-extensions @@ -144,40 +144,37 @@ stages: workingDirectory: $(K8S_EXTENSION_REPO_PATH) condition: succeededOrFailed() -- stage: BuildPublishExtension - dependsOn: [] - displayName: "Build and Publish the Extension Artifact" - variables: - CLI_REPO_PATH: $(Agent.BuildDirectory)/s - IS_PRIVATE_BRANCH: $[or(eq(variables['Build.SourceBranchName'], 'refs/heads/k8s-extension/private-preview'), eq(variables['System.PullRequest.TargetBranch'], 'refs/heads/k8s-extension/private-preview'))] - jobs: - job: BuildPublishExtension - displayName: "Build and Publish the Extension Artifact" pool: vmImage: 'ubuntu-16.04' + displayName: "Build and Publish the Extension Artifact" + variables: + CLI_REPO_PATH: $(Agent.BuildDirectory)/s + IS_PRIVATE_BRANCH: $[or(and(eq(variables['Build.SourceBranchName'], 'refs/heads/k8s-extension/private-preview'), ne(variables['System.PullRequest.TargetBranch'], 'refs/heads/k8s-extension/public-preview')), eq(variables['System.PullRequest.TargetBranch'], 'refs/heads/k8s-extension/private-preview'))] steps: - bash: | - if [[ $IS_PRIVATE_BRANCH ]]; then - echo "Using the private preview of k8s-extension to build..." + echo "Using the private preview of k8s-extension to build..." - cp $(CLI_REPO_PATH)/src/k8s-extension $(CLI_REPO_PATH)/src/k8s-extension-private -r - cp $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension/_consts_private.py $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension/_consts.py + cp $(CLI_REPO_PATH)/src/k8s-extension $(CLI_REPO_PATH)/src/k8s-extension-private -r + cp $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension/_consts_private.py $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension/_consts.py - EXTENSION_NAME="k8s-extension-private" - EXTENSION_FILE_NAME="k8s_extension_private" + EXTENSION_NAME="k8s-extension-private" + EXTENSION_FILE_NAME="k8s_extension_private" - echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" - echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" - else - echo "Using the public version of k8s-extension to build..." + echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" + echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" + condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'True')) + displayName: "Copy Files, Set Variables for k8s-extension-private" + - bash: | + echo "Using the public version of k8s-extension to build..." - EXTENSION_NAME="k8s-extension" - EXTENSION_FILE_NAME="k8s_extension" + EXTENSION_NAME="k8s-extension" + EXTENSION_FILE_NAME="k8s_extension" - echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" - echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" - fi - displayName: "Copy Files, Set Variables based on Private Branch" + echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" + echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" + condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'False')) + displayName: "Copy Files, Set Variables for k8s-extension" - task: UsePythonVersion@0 displayName: 'Use Python 3.6' inputs: From 8e058c595946f45740c87ef6576349235d41ca76 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Thu, 11 Mar 2021 16:23:38 -0800 Subject: [PATCH 11/86] Change condition variable --- k8s-custom-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index a5dbceabea2..5bf0489f053 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -150,7 +150,7 @@ stages: displayName: "Build and Publish the Extension Artifact" variables: CLI_REPO_PATH: $(Agent.BuildDirectory)/s - IS_PRIVATE_BRANCH: $[or(and(eq(variables['Build.SourceBranchName'], 'refs/heads/k8s-extension/private-preview'), ne(variables['System.PullRequest.TargetBranch'], 'refs/heads/k8s-extension/public-preview')), eq(variables['System.PullRequest.TargetBranch'], 'refs/heads/k8s-extension/private-preview'))] + IS_PRIVATE_BRANCH: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/k8s-extension/private-preview'), eq(variables['System.PullRequest.TargetBranch'], 'refs/heads/k8s-extension/private-preview'))] steps: - bash: | echo "Using the private preview of k8s-extension to build..." From dea40c19ce9c5371ea7527f120c492ab2e6f6619 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Thu, 11 Mar 2021 17:47:00 -0800 Subject: [PATCH 12/86] Add version to public preview/private preview --- src/k8s-extension/azext_k8s_extension/_consts.py | 1 + src/k8s-extension/azext_k8s_extension/_consts_private.py | 1 + src/k8s-extension/setup.py | 6 +----- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/_consts.py b/src/k8s-extension/azext_k8s_extension/_consts.py index e9f8156f307..820e38af31c 100644 --- a/src/k8s-extension/azext_k8s_extension/_consts.py +++ b/src/k8s-extension/azext_k8s_extension/_consts.py @@ -5,3 +5,4 @@ # -------------------------------------------------------------------------------------------- EXTENSION_NAME = 'k8s-extension' +VERSION = "0.1.1" diff --git a/src/k8s-extension/azext_k8s_extension/_consts_private.py b/src/k8s-extension/azext_k8s_extension/_consts_private.py index 18a01637c2d..bc4c8a2b694 100644 --- a/src/k8s-extension/azext_k8s_extension/_consts_private.py +++ b/src/k8s-extension/azext_k8s_extension/_consts_private.py @@ -5,3 +5,4 @@ # -------------------------------------------------------------------------------------------- EXTENSION_NAME = 'k8s-extension-private' +VERSION = "0.1PP.14" diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index 878f7f8d87a..d3d491fd48c 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -15,10 +15,6 @@ from distutils import log as logger logger.warn("Wheel is not available, disabling bdist_wheel hook") -# TODO: Confirm this is the right version number you want and it matches your -# HISTORY.rst entry. -VERSION = '0.1PP.14' - # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers CLASSIFIERS = [ @@ -43,7 +39,7 @@ setup( name=consts.EXTENSION_NAME, - version=VERSION, + version=consts.VERSION, description='Microsoft Azure Command-Line Tools K8s-extension Extension', # TODO: Update author and email, if applicable author='Microsoft Corporation', From e81e0101493c99a361576211c95d3e118449b1dd Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Fri, 12 Mar 2021 12:10:48 -0800 Subject: [PATCH 13/86] Update pipelines --- k8s-custom-pipelines.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index 5bf0489f053..a4be34c3e7b 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -9,13 +9,13 @@ trigger: batch: true branches: include: - - k8s-extension/public-preview - - k8s-extension/private-preview + - k8s-extension/public + - k8s-extension/private pr: branches: include: - - k8s-extension/public-preview - - k8s-extension/private-preview + - k8s-extension/public + - k8s-extension/private stages: - stage: BuildTestPublishExtension @@ -150,7 +150,7 @@ stages: displayName: "Build and Publish the Extension Artifact" variables: CLI_REPO_PATH: $(Agent.BuildDirectory)/s - IS_PRIVATE_BRANCH: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/k8s-extension/private-preview'), eq(variables['System.PullRequest.TargetBranch'], 'refs/heads/k8s-extension/private-preview'))] + IS_PRIVATE_BRANCH: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/k8s-extension/private'), eq(variables['System.PullRequest.TargetBranch'], 'refs/heads/k8s-extension/private'))] steps: - bash: | echo "Using the private preview of k8s-extension to build..." From 9621a48df1feda8134be4a98df3a41d2b44e08b2 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Fri, 12 Mar 2021 13:13:39 -0800 Subject: [PATCH 14/86] Add different testing based on private branch --- k8s-custom-pipelines.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index a4be34c3e7b..3bdfd70eb99 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -26,6 +26,7 @@ stages: SUBSCRIPTION_ID: "15c06b1b-01d6-407b-bb21-740b8617dea3" RESOURCE_GROUP: "K8sPartnerExtensionTest" BASE_CLUSTER_NAME: "k8s-extension-cluster" + IS_PRIVATE_BRANCH: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/k8s-extension/private'), eq(variables['System.PullRequest.TargetBranch'], 'refs/heads/k8s-extension/private'))] EXTENSION_NAME: "k8s-extension" EXTENSION_FILE_NAME: "k8s_extension" @@ -116,7 +117,19 @@ stages: workingDirectory: $(K8S_EXTENSION_REPO_PATH) - task: AzureCLI@2 - displayName: Run the Test Suite + displayName: Run the Test Suite Public Extensions Only + inputs: + azureSubscription: AzureResourceConnection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + .\Test.ps1 -CI -Public + workingDirectory: $(K8S_EXTENSION_REPO_PATH) + continueOnError: true + condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'False')) + + - task: AzureCLI@2 + displayName: Run the Test Suite on Private + Public Extensions inputs: azureSubscription: AzureResourceConnection scriptType: pscore @@ -125,6 +138,7 @@ stages: .\Test.ps1 -CI workingDirectory: $(K8S_EXTENSION_REPO_PATH) continueOnError: true + condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'True')) - task: PublishTestResults@2 inputs: @@ -150,7 +164,6 @@ stages: displayName: "Build and Publish the Extension Artifact" variables: CLI_REPO_PATH: $(Agent.BuildDirectory)/s - IS_PRIVATE_BRANCH: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/k8s-extension/private'), eq(variables['System.PullRequest.TargetBranch'], 'refs/heads/k8s-extension/private'))] steps: - bash: | echo "Using the private preview of k8s-extension to build..." From 862a0355c54da89a13169de57dc92e07655e7a14 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Fri, 12 Mar 2021 13:22:30 -0800 Subject: [PATCH 15/86] Add annotations to extension model --- src/k8s-extension/azext_k8s_extension/custom.py | 6 +++--- .../partner_extensions/AzureDefender.py | 4 ++-- .../partner_extensions/ContainerInsights.py | 4 ++-- .../partner_extensions/DefaultExtension.py | 2 +- .../partner_extensions/PartnerExtensionModel.py | 14 +++++++++----- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index 4b0657c1814..b06567aca0b 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -17,9 +17,9 @@ # from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate from azext_k8s_extension.vendored_sdks.models import ErrorResponseException -from .partner_extensions.ContainerInsights import ContainerInsights -from .partner_extensions.AzureDefender import AzureDefender -from .partner_extensions.DefaultExtension import DefaultExtension +from azext_k8s_extension.partner_extensions.ContainerInsights import ContainerInsights +from azext_k8s_extension.partner_extensions.AzureDefender import AzureDefender +from azext_k8s_extension.partner_extensions.DefaultExtension import DefaultExtension from ._client_factory import cf_resources diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py index 172662f4e57..39329cc36cb 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py @@ -12,8 +12,8 @@ from azext_k8s_extension.vendored_sdks.models import ScopeCluster from azext_k8s_extension.vendored_sdks.models import Scope -from .PartnerExtensionModel import PartnerExtensionModel -from .ContainerInsights import _get_container_insights_settings +from azext_k8s_extension.partner_extensions.PartnerExtensionModel import PartnerExtensionModel +from azext_k8s_extension.partner_extensions.ContainerInsights import _get_container_insights_settings logger = get_logger(__name__) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py index ada94d985a9..f6662c29611 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py @@ -22,9 +22,9 @@ from azext_k8s_extension.vendored_sdks.models import ScopeCluster from azext_k8s_extension.vendored_sdks.models import Scope -from .PartnerExtensionModel import PartnerExtensionModel +from azext_k8s_extension.partner_extensions.PartnerExtensionModel import PartnerExtensionModel -from .._client_factory import ( +from azext_k8s_extension._client_factory import ( cf_resources, cf_resource_groups, cf_log_analytics) logger = get_logger(__name__) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py index 243371e47b7..d3e9a6827c2 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py @@ -11,7 +11,7 @@ from azext_k8s_extension.vendored_sdks.models import ScopeNamespace from azext_k8s_extension.vendored_sdks.models import Scope -from .PartnerExtensionModel import PartnerExtensionModel +from azext_k8s_extension.partner_extensions.PartnerExtensionModel import PartnerExtensionModel class DefaultExtension(PartnerExtensionModel): diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py index c863a8d3833..c0bf3b6e657 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py @@ -4,16 +4,20 @@ # -------------------------------------------------------------------------------------------- from abc import ABC, abstractmethod +from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceForCreate +from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate class PartnerExtensionModel(ABC): @abstractmethod - def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, - scope, auto_upgrade_minor_version, release_train, version, target_namespace, - release_namespace, configuration_settings, configuration_protected_settings, - configuration_settings_file, configuration_protected_settings_file): + def Create(self, cmd, client, resource_group_name: str, cluster_name: str, name: str, cluster_type: str, + extension_type: str, scope: str, auto_upgrade_minor_version: bool, release_train: str, version: str, + target_namespace: str, release_namespace: str, configuration_settings: dict, + configuration_protected_settings: dict, configuration_settings_file: str, + configuration_protected_settings_file: str) -> ExtensionInstanceForCreate: pass @abstractmethod - def Update(self, extension, auto_upgrade_minor_version, release_train, version): + def Update(self, extension: ExtensionInstanceForCreate, auto_upgrade_minor_version: bool, + release_train: str, version: str) -> ExtensionInstanceUpdate: pass From e1c3d12cd8a58e98cd5d2e9575d3d96fdc585bc3 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Mon, 15 Mar 2021 11:41:42 -0700 Subject: [PATCH 16/86] Update k8s-custom-pipelines.yml --- k8s-custom-pipelines.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index 3bdfd70eb99..f4e2984ddf2 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -123,7 +123,7 @@ stages: scriptType: pscore scriptLocation: inlineScript inlineScript: | - .\Test.ps1 -CI -Public + .\Test.ps1 -CI -ExtensionType Public -OnlyPublicTests workingDirectory: $(K8S_EXTENSION_REPO_PATH) continueOnError: true condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'False')) @@ -135,7 +135,7 @@ stages: scriptType: pscore scriptLocation: inlineScript inlineScript: | - .\Test.ps1 -CI + .\Test.ps1 -CI -ExtensionType Public workingDirectory: $(K8S_EXTENSION_REPO_PATH) continueOnError: true condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'True')) @@ -353,4 +353,4 @@ stages: displayName: "Verify Extension Ref Docs" inputs: targetType: 'filePath' - filePath: scripts/ci/test_index_ref_doc.sh \ No newline at end of file + filePath: scripts/ci/test_index_ref_doc.sh From 3e309bf8d1218994b3135a9614e03b0e2cf41e88 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Tue, 16 Mar 2021 11:01:39 -0700 Subject: [PATCH 17/86] Update SDKs with Updated Swagger Spec for 2020-07-01-preview (#13) * Update sdks with updated swagger spec * Update version and history rst * Reorder release history timeline * Fix ExtensionInstanceForCreate for import --- src/k8s-extension/HISTORY.rst | 70 +-- .../azext_k8s_extension/_client_factory.py | 6 +- .../azext_k8s_extension/_consts.py | 2 +- .../partner_extensions/AzureDefender.py | 8 +- .../partner_extensions/ContainerInsights.py | 8 +- .../partner_extensions/DefaultExtension.py | 6 +- .../PartnerExtensionModel.py | 6 +- .../vendored_sdks/__init__.py | 6 +- .../vendored_sdks/_configuration.py | 6 +- .../_source_control_configuration_client.py | 60 +++ .../vendored_sdks/models/__init__.py | 48 +- .../vendored_sdks/models/_models.py | 473 +++++++++++------ .../vendored_sdks/models/_models_py3.py | 483 +++++++++++------- .../vendored_sdks/models/_paged_models.py | 21 +- ...rce_control_configuration_client_enums.py} | 0 .../vendored_sdks/operations/__init__.py | 8 +- ...perations.py => _extensions_operations.py} | 119 ++--- .../vendored_sdks/operations/_operations.py | 101 ++++ ...ource_control_configurations_operations.py | 386 ++++++++++++++ .../vendored_sdks/version.py | 2 +- 20 files changed, 1304 insertions(+), 515 deletions(-) create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/_source_control_configuration_client.py rename src/k8s-extension/azext_k8s_extension/vendored_sdks/models/{_k8s_extension_client_enums.py => _source_control_configuration_client_enums.py} (100%) rename src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/{_k8s_extensions_operations.py => _extensions_operations.py} (84%) create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_operations.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_source_control_configurations_operations.py diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index 695f549b1a2..54c1e375f6f 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -3,81 +3,33 @@ Release History =============== -0.1.0 -++++++++++++++++++ -* Initial release. - -0.1.1 -++++++++++++++++++ -* Add support for microsoft-azure-defender extension type - -0.1.2 -++++++++++++++++++ - -* Add support for Arc Appliance cluster type - -0.1.3 -++++++++++++++++++ - -* Customization for microsoft.openservicemesh - -0.1PP.4 +0.2.0 ++++++++++++++++++ * Refactor for clear separation of extension-type specific customizations -* Introduce new versioning scheme to allow Preview releases by Partners - -0.1PP.5 -++++++++++++++++++ - -* OpenServiceMesh customization. -* If Version is passed in, accept None for AutoUpgradeMinorVersion, and not require it to be False. - -0.1PP.6 -++++++++++++++++++ - * OpenServiceMesh customization. -* Scope is always cluster. Version is mandatory for staging and pilot release-trains. - -0.1PP.7 -++++++++++++++++++ - * Fix clusterType of Microsoft.ResourceConnector resource - -0.1PP.8 -++++++++++++++++++ - * Update clusterType validation to allow 'appliances' * Update identity creation to use the appropriate parent resource's type and api-version * Throw error if cluster type is not one of the 3 supported types - -0.1PP.9 -++++++++++++++++++ - * Rename azuremonitor-containers extension type to microsoft.azuremonitor.containers +* Move CLI errors to non-deprecated error types +* Remove support for update -0.1PP.10 -++++++++++++++++++ - -* Add azuremonitor-containers back with alternative microsoft.azuremonitor.containers - -0.1PP.11 +0.1.3 ++++++++++++++++++ -* Add shorter aliases for long parameter names +* Customization for microsoft.openservicemesh -0.1PP.12 +0.1.2 ++++++++++++++++++ -* Remove support for azuremonitor-containers extension type naming +* Add support for Arc Appliance cluster type -0.1PP.13 +0.1.1 ++++++++++++++++++ +* Add support for microsoft-azure-defender extension type -* Move CLI errors to non-deprecated error types -* Remove support for update - -0.1PP.14 +0.1.0 ++++++++++++++++++ - -* Update help text, group CLI arguments +* Initial release. diff --git a/src/k8s-extension/azext_k8s_extension/_client_factory.py b/src/k8s-extension/azext_k8s_extension/_client_factory.py index a4ec83ee0cb..1a9a10c2615 100644 --- a/src/k8s-extension/azext_k8s_extension/_client_factory.py +++ b/src/k8s-extension/azext_k8s_extension/_client_factory.py @@ -8,12 +8,12 @@ def cf_k8s_extension(cli_ctx, *_): - from azext_k8s_extension.vendored_sdks import K8sExtensionClient - return get_mgmt_service_client(cli_ctx, K8sExtensionClient) + from azext_k8s_extension.vendored_sdks import SourceControlConfigurationClient + return get_mgmt_service_client(cli_ctx, SourceControlConfigurationClient) def cf_k8s_extension_operation(cli_ctx, _): - return cf_k8s_extension(cli_ctx).k8s_extensions + return cf_k8s_extension(cli_ctx).extensions def cf_resource_groups(cli_ctx, subscription_id=None): diff --git a/src/k8s-extension/azext_k8s_extension/_consts.py b/src/k8s-extension/azext_k8s_extension/_consts.py index 820e38af31c..d0fdaf7775f 100644 --- a/src/k8s-extension/azext_k8s_extension/_consts.py +++ b/src/k8s-extension/azext_k8s_extension/_consts.py @@ -5,4 +5,4 @@ # -------------------------------------------------------------------------------------------- EXTENSION_NAME = 'k8s-extension' -VERSION = "0.1.1" +VERSION = "0.2.0" diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py index 39329cc36cb..7721ea8c638 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py @@ -7,7 +7,7 @@ from knack.log import get_logger -from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceForCreate +from azext_k8s_extension.vendored_sdks.models import ExtensionInstance from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate from azext_k8s_extension.vendored_sdks.models import ScopeCluster from azext_k8s_extension.vendored_sdks.models import Scope @@ -25,7 +25,7 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t configuration_settings_file, configuration_protected_settings_file): """ExtensionType 'microsoft.azuredefender.kubernetes' specific validations & defaults for Create - Must create and return a valid 'ExtensionInstanceForCreate' object. + Must create and return a valid 'ExtensionInstance' object. """ # NOTE-1: Replace default scope creation with your customization! @@ -46,9 +46,9 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t _get_container_insights_settings(cmd, resource_group_name, cluster_name, configuration_settings, configuration_protected_settings, is_ci_extension_type) - # NOTE-2: Return a valid ExtensionInstanceForCreate object, Instance name and flag for Identity + # NOTE-2: Return a valid ExtensionInstance object, Instance name and flag for Identity create_identity = True - extension_instance = ExtensionInstanceForCreate( + extension_instance = ExtensionInstance( extension_type=extension_type, auto_upgrade_minor_version=auto_upgrade_minor_version, release_train=release_train, diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py index f6662c29611..a90b807020d 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py @@ -17,7 +17,7 @@ from msrestazure.azure_exceptions import CloudError from msrestazure.tools import parse_resource_id, is_valid_resource_id -from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceForCreate +from azext_k8s_extension.vendored_sdks.models import ExtensionInstance from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate from azext_k8s_extension.vendored_sdks.models import ScopeCluster from azext_k8s_extension.vendored_sdks.models import Scope @@ -37,7 +37,7 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t configuration_settings_file, configuration_protected_settings_file): """ExtensionType 'microsoft.azuremonitor.containers' specific validations & defaults for Create - Must create and return a valid 'ExtensionInstanceForCreate' object. + Must create and return a valid 'ExtensionInstance' object. """ # NOTE-1: Replace default scope creation with your customization! @@ -58,9 +58,9 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t _get_container_insights_settings(cmd, resource_group_name, cluster_name, configuration_settings, configuration_protected_settings, is_ci_extension_type) - # NOTE-2: Return a valid ExtensionInstanceForCreate object, Instance name and flag for Identity + # NOTE-2: Return a valid ExtensionInstance object, Instance name and flag for Identity create_identity = True - extension_instance = ExtensionInstanceForCreate( + extension_instance = ExtensionInstance( extension_type=extension_type, auto_upgrade_minor_version=auto_upgrade_minor_version, release_train=release_train, diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py index d3e9a6827c2..8e813502edd 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py @@ -5,7 +5,7 @@ # pylint: disable=unused-argument -from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceForCreate +from azext_k8s_extension.vendored_sdks.models import ExtensionInstance from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate from azext_k8s_extension.vendored_sdks.models import ScopeCluster from azext_k8s_extension.vendored_sdks.models import ScopeNamespace @@ -21,7 +21,7 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t configuration_settings_file, configuration_protected_settings_file): """Default validations & defaults for Create - Must create and return a valid 'ExtensionInstanceForCreate' object. + Must create and return a valid 'ExtensionInstance' object. """ ext_scope = None @@ -37,7 +37,7 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t release_train = 'stable' create_identity = False - extension_instance = ExtensionInstanceForCreate( + extension_instance = ExtensionInstance( extension_type=extension_type, auto_upgrade_minor_version=auto_upgrade_minor_version, release_train=release_train, diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py index c0bf3b6e657..96c489644e7 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py @@ -4,7 +4,7 @@ # -------------------------------------------------------------------------------------------- from abc import ABC, abstractmethod -from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceForCreate +from azext_k8s_extension.vendored_sdks.models import ExtensionInstance from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate @@ -14,10 +14,10 @@ def Create(self, cmd, client, resource_group_name: str, cluster_name: str, name: extension_type: str, scope: str, auto_upgrade_minor_version: bool, release_train: str, version: str, target_namespace: str, release_namespace: str, configuration_settings: dict, configuration_protected_settings: dict, configuration_settings_file: str, - configuration_protected_settings_file: str) -> ExtensionInstanceForCreate: + configuration_protected_settings_file: str) -> ExtensionInstance: pass @abstractmethod - def Update(self, extension: ExtensionInstanceForCreate, auto_upgrade_minor_version: bool, + def Update(self, extension: ExtensionInstance, auto_upgrade_minor_version: bool, release_train: str, version: str) -> ExtensionInstanceUpdate: pass diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/__init__.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/__init__.py index d94dccac4c2..874177b4d34 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/__init__.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/__init__.py @@ -9,9 +9,9 @@ # regenerated. # -------------------------------------------------------------------------- -from ._configuration import K8sExtensionClientConfiguration -from ._k8s_extension_client import K8sExtensionClient -__all__ = ['K8sExtensionClient', 'K8sExtensionClientConfiguration'] +from ._configuration import SourceControlConfigurationClientConfiguration +from ._source_control_configuration_client import SourceControlConfigurationClient +__all__ = ['SourceControlConfigurationClient', 'SourceControlConfigurationClientConfiguration'] from .version import VERSION diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/_configuration.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_configuration.py index 48080b2ee7d..5043ed69594 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/_configuration.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_configuration.py @@ -13,8 +13,8 @@ from .version import VERSION -class K8sExtensionClientConfiguration(AzureConfiguration): - """Configuration for K8sExtensionClient +class SourceControlConfigurationClientConfiguration(AzureConfiguration): + """Configuration for SourceControlConfigurationClient Note that all parameters used to create this instance are saved as instance attributes. @@ -37,7 +37,7 @@ def __init__( if not base_url: base_url = 'https://management.azure.com' - super(K8sExtensionClientConfiguration, self).__init__(base_url) + super(SourceControlConfigurationClientConfiguration, self).__init__(base_url) # Starting Autorest.Python 4.0.64, make connection pool activated by default self.keep_alive = True diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/_source_control_configuration_client.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_source_control_configuration_client.py new file mode 100644 index 00000000000..a77176d8cb1 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_source_control_configuration_client.py @@ -0,0 +1,60 @@ +# 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 msrest.service_client import SDKClient +from msrest import Serializer, Deserializer + +from ._configuration import SourceControlConfigurationClientConfiguration +from .operations import SourceControlConfigurationsOperations +from .operations import Operations +from .operations import ExtensionsOperations +from . import models + + +class SourceControlConfigurationClient(SDKClient): + """KubernetesConfiguration Client + + :ivar config: Configuration for client. + :vartype config: SourceControlConfigurationClientConfiguration + + :ivar source_control_configurations: SourceControlConfigurations operations + :vartype source_control_configurations: azure.mgmt.kubernetesconfiguration.operations.SourceControlConfigurationsOperations + :ivar operations: Operations operations + :vartype operations: azure.mgmt.kubernetesconfiguration.operations.Operations + :ivar extensions: Extensions operations + :vartype extensions: azure.mgmt.kubernetesconfiguration.operations.ExtensionsOperations + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param subscription_id: The Azure subscription ID. This is a + GUID-formatted string (e.g. 00000000-0000-0000-0000-000000000000) + :type subscription_id: str + :param str base_url: Service URL + """ + + def __init__( + self, credentials, subscription_id, base_url=None): + + self.config = SourceControlConfigurationClientConfiguration(credentials, subscription_id, base_url) + super(SourceControlConfigurationClient, self).__init__(self.config.credentials, self.config) + + client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} + self.api_version = '2020-07-01-preview' + self._serialize = Serializer(client_models) + self._deserialize = Deserializer(client_models) + + self.source_control_configurations = SourceControlConfigurationsOperations( + self._client, self.config, self._serialize, self._deserialize) + self.operations = Operations( + self._client, self.config, self._serialize, self._deserialize) + self.extensions = ExtensionsOperations( + self._client, self.config, self._serialize, self._deserialize) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/__init__.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/__init__.py index 166f80c01ea..e74cb56832b 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/__init__.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/__init__.py @@ -10,60 +10,84 @@ # -------------------------------------------------------------------------- try: + from ._models_py3 import ComplianceStatus + from ._models_py3 import ConfigurationIdentity from ._models_py3 import ErrorDefinition from ._models_py3 import ErrorResponse, ErrorResponseException from ._models_py3 import ExtensionInstance - from ._models_py3 import ExtensionInstanceForCreate - from ._models_py3 import ExtensionInstanceForList from ._models_py3 import ExtensionInstanceUpdate from ._models_py3 import ExtensionStatus + from ._models_py3 import HelmOperatorProperties from ._models_py3 import ProxyResource from ._models_py3 import Resource + from ._models_py3 import ResourceProviderOperation + from ._models_py3 import ResourceProviderOperationDisplay from ._models_py3 import Result from ._models_py3 import Scope from ._models_py3 import ScopeCluster from ._models_py3 import ScopeNamespace - from ._models_py3 import ConfigurationIdentity + from ._models_py3 import SourceControlConfiguration + from ._models_py3 import SystemData except (SyntaxError, ImportError): + from ._models import ComplianceStatus + from ._models import ConfigurationIdentity from ._models import ErrorDefinition from ._models import ErrorResponse, ErrorResponseException from ._models import ExtensionInstance - from ._models import ExtensionInstanceForCreate - from ._models import ExtensionInstanceForList from ._models import ExtensionInstanceUpdate from ._models import ExtensionStatus + from ._models import HelmOperatorProperties from ._models import ProxyResource from ._models import Resource + from ._models import ResourceProviderOperation + from ._models import ResourceProviderOperationDisplay from ._models import Result from ._models import Scope from ._models import ScopeCluster from ._models import ScopeNamespace - from ._models import ConfigurationIdentity -from ._paged_models import ExtensionInstanceForListPaged -from ._k8s_extension_client_enums import ( + from ._models import SourceControlConfiguration + from ._models import SystemData +from ._paged_models import ExtensionInstancePaged +from ._paged_models import ResourceProviderOperationPaged +from ._paged_models import SourceControlConfigurationPaged +from ._source_control_configuration_client_enums import ( + ComplianceStateType, MessageLevelType, + OperatorType, + OperatorScopeType, + ProvisioningStateType, InstallStateType, LevelType, ResourceIdentityType, ) __all__ = [ + 'ComplianceStatus', + 'ConfigurationIdentity', 'ErrorDefinition', 'ErrorResponse', 'ErrorResponseException', 'ExtensionInstance', - 'ExtensionInstanceForCreate', - 'ExtensionInstanceForList', - 'ConfigurationIdentity', 'ExtensionInstanceUpdate', 'ExtensionStatus', + 'HelmOperatorProperties', 'ProxyResource', 'Resource', + 'ResourceProviderOperation', + 'ResourceProviderOperationDisplay', 'Result', 'Scope', 'ScopeCluster', 'ScopeNamespace', - 'ExtensionInstanceForListPaged', + 'SourceControlConfiguration', + 'SystemData', + 'SourceControlConfigurationPaged', + 'ResourceProviderOperationPaged', + 'ExtensionInstancePaged', + 'ComplianceStateType', 'MessageLevelType', + 'OperatorType', + 'OperatorScopeType', + 'ProvisioningStateType', 'InstallStateType', 'LevelType', 'ResourceIdentityType', diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py index 0a11097a24f..da1b94606cd 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py @@ -20,6 +20,47 @@ class CloudError(Model): _attribute_map = { } + +class ComplianceStatus(Model): + """Compliance Status details. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar compliance_state: The compliance state of the configuration. + Possible values include: 'Pending', 'Compliant', 'Noncompliant', + 'Installed', 'Failed' + :vartype compliance_state: str or + ~azure.mgmt.kubernetesconfiguration.models.ComplianceStateType + :param last_config_applied: Datetime the configuration was last applied. + :type last_config_applied: datetime + :param message: Message from when the configuration was applied. + :type message: str + :param message_level: Level of the message. Possible values include: + 'Error', 'Warning', 'Information' + :type message_level: str or + ~azure.mgmt.kubernetesconfiguration.models.MessageLevelType + """ + + _validation = { + 'compliance_state': {'readonly': True}, + } + + _attribute_map = { + 'compliance_state': {'key': 'complianceState', 'type': 'str'}, + 'last_config_applied': {'key': 'lastConfigApplied', 'type': 'iso-8601'}, + 'message': {'key': 'message', 'type': 'str'}, + 'message_level': {'key': 'messageLevel', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ComplianceStatus, self).__init__(**kwargs) + self.compliance_state = None + self.last_config_applied = kwargs.get('last_config_applied', None) + self.message = kwargs.get('message', None) + self.message_level = kwargs.get('message_level', None) + + class ConfigurationIdentity(Model): """Identity for the managed cluster. @@ -126,6 +167,9 @@ class Resource(Model): :vartype name: str :ivar type: Resource type :vartype type: str + :param system_data: Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources + :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData """ _validation = { @@ -138,6 +182,7 @@ class Resource(Model): 'id': {'key': 'id', 'type': 'str'}, 'name': {'key': 'name', 'type': 'str'}, 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, } def __init__(self, **kwargs): @@ -145,6 +190,7 @@ def __init__(self, **kwargs): self.id = None self.name = None self.type = None + self.system_data = kwargs.get('system_data', None) class ProxyResource(Resource): @@ -159,6 +205,9 @@ class ProxyResource(Resource): :vartype name: str :ivar type: Resource type :vartype type: str + :param system_data: Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources + :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData """ _validation = { @@ -171,6 +220,7 @@ class ProxyResource(Resource): 'id': {'key': 'id', 'type': 'str'}, 'name': {'key': 'name', 'type': 'str'}, 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, } def __init__(self, **kwargs): @@ -189,6 +239,9 @@ class ExtensionInstance(ProxyResource): :vartype name: str :ivar type: Resource type :vartype type: str + :param system_data: Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources + :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData :param extension_type: Type of the Extension, of which this resource is an instance of. It must be one of the Extension Types registered with Microsoft.KubernetesConfiguration by the Extension publisher. @@ -209,6 +262,10 @@ class ExtensionInstance(ProxyResource): :param configuration_settings: Configuration settings, as name-value pairs for configuring this instance of the extension. :type configuration_settings: dict[str, str] + :param configuration_protected_settings: Configuration settings that are + sensitive, as name-value pairs for configuring this instance of the + extension. + :type configuration_protected_settings: dict[str, str] :param install_state: Status of installation of this instance of the extension. Possible values include: 'Pending', 'Installed', 'Failed' :type install_state: str or @@ -248,19 +305,21 @@ class ExtensionInstance(ProxyResource): 'id': {'key': 'id', 'type': 'str'}, 'name': {'key': 'name', 'type': 'str'}, 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, 'version': {'key': 'properties.version', 'type': 'str'}, 'scope': {'key': 'properties.scope', 'type': 'Scope'}, 'configuration_settings': {'key': 'properties.configurationSettings', 'type': '{str}'}, + 'configuration_protected_settings': {'key': 'properties.configurationProtectedSettings', 'type': '{str}'}, 'install_state': {'key': 'properties.installState', 'type': 'str'}, 'statuses': {'key': 'properties.statuses', 'type': '[ExtensionStatus]'}, 'creation_time': {'key': 'properties.creationTime', 'type': 'str'}, 'last_modified_time': {'key': 'properties.lastModifiedTime', 'type': 'str'}, 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'}, - 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, + 'identity': {'key': 'properties.identity', 'type': 'ConfigurationIdentity'}, } def __init__(self, **kwargs): @@ -271,176 +330,9 @@ def __init__(self, **kwargs): self.version = kwargs.get('version', None) self.scope = kwargs.get('scope', None) self.configuration_settings = kwargs.get('configuration_settings', None) - self.install_state = kwargs.get('install_state', None) - self.statuses = kwargs.get('statuses', None) - self.creation_time = None - self.last_modified_time = None - self.last_status_time = None - self.error_info = None - self.identity = kwargs.get('identity', None) - - -class ExtensionInstanceForCreate(ProxyResource): - """Object to create a new Extension Instance. - - Variables are only populated by the server, and will be ignored when - sending a request. - - :ivar id: Resource Id - :vartype id: str - :ivar name: Resource name - :vartype name: str - :ivar type: Resource type - :vartype type: str - :param extension_type: Type of the Extension, of which this resource is an - instance of. It must be one of the Extension Types registered with - Microsoft.KubernetesConfiguration by the Extension publisher. - :type extension_type: str - :param auto_upgrade_minor_version: Flag to note if this instance - participates in auto upgrade of minor version, or not. - :type auto_upgrade_minor_version: bool - :param release_train: ReleaseTrain this extension instance participates in - for auto-upgrade (e.g. Stable, Preview, etc.) - only if - autoUpgradeMinorVersion is 'true'. - :type release_train: str - :param version: Version of the extension for this extension instance, if - it is 'pinned' to a specific version. autoUpgradeMinorVersion must be - 'false'. - :type version: str - :param scope: Scope at which the extension instance is installed. - :type scope: ~azure.mgmt.kubernetesconfiguration.models.Scope - :param configuration_settings: Configuration settings, as name-value pairs - for configuring this instance of the extension. - :type configuration_settings: dict[str, str] - :param configuration_protected_settings: Configuration settings that are - sensitive, as name-value pairs for configuring this instance of the - extension. - :type configuration_protected_settings: dict[str, str] - :param identity: The identity of the configuration. - :type identity: - ~azure.mgmt.kubernetesconfiguration.models.ConfigurationIdentity - :type location: str - """ - - _validation = { - 'id': {'readonly': True}, - 'name': {'readonly': True}, - 'type': {'readonly': True}, - } - - _attribute_map = { - 'id': {'key': 'id', 'type': 'str'}, - 'name': {'key': 'name', 'type': 'str'}, - 'type': {'key': 'type', 'type': 'str'}, - 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, - 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, - 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, - 'version': {'key': 'properties.version', 'type': 'str'}, - 'scope': {'key': 'properties.scope', 'type': 'Scope'}, - 'configuration_settings': {'key': 'properties.configurationSettings', 'type': '{str}'}, - 'configuration_protected_settings': {'key': 'properties.configurationProtectedSettings', 'type': '{str}'}, - 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, - 'location': {'key': 'location', 'type': 'str'}, - } - - def __init__(self, **kwargs): - super(ExtensionInstanceForCreate, self).__init__(**kwargs) - self.extension_type = kwargs.get('extension_type', None) - self.auto_upgrade_minor_version = kwargs.get('auto_upgrade_minor_version', None) - self.release_train = kwargs.get('release_train', None) - self.version = kwargs.get('version', None) - self.scope = kwargs.get('scope', None) - self.configuration_settings = kwargs.get('configuration_settings', None) self.configuration_protected_settings = kwargs.get('configuration_protected_settings', None) - self.identity = kwargs.get('identity', None) - self.location = kwargs.get('location', None) - - -class ExtensionInstanceForList(ProxyResource): - """The Extension Instance object. - - Variables are only populated by the server, and will be ignored when - sending a request. - - :ivar id: Resource Id - :vartype id: str - :ivar name: Resource name - :vartype name: str - :ivar type: Resource type - :vartype type: str - :param extension_type: Type of the Extension, of which this resource is an - instance of. It must be one of the Extension Types registered with - Microsoft.KubernetesConfiguration by the Extension publisher. - :type extension_type: str - :param auto_upgrade_minor_version: Flag to note if this instance - participates in auto upgrade of minor version, or not. - :type auto_upgrade_minor_version: bool - :param release_train: ReleaseTrain this extension instance participates in - for auto-upgrade (e.g. Stable, Preview, etc.) - only if - autoUpgradeMinorVersion is 'true'. - :type release_train: str - :param version: Version of the extension for this extension instance, if - it is 'pinned' to a specific version. - :type version: str - :param scope: Scope at which the extension instance is installed. - :type scope: ~azure.mgmt.kubernetesconfiguration.models.Scope - :param install_state: Status of installation of this instance of the - extension. Possible values include: 'Pending', 'Installed', 'Failed' - :type install_state: str or - ~azure.mgmt.kubernetesconfiguration.models.InstallStateType - :ivar creation_time: DateLiteral (per ISO8601) noting the time the - resource was created by the client (user). - :vartype creation_time: str - :ivar last_modified_time: DateLiteral (per ISO8601) noting the time the - resource was modified by the client (user). - :vartype last_modified_time: str - :ivar last_status_time: DateLiteral (per ISO8601) noting the time of last - status from the agent. - :vartype last_status_time: str - :ivar error_info: Error information from the Agent - e.g. errors during - installation. - :vartype error_info: - ~azure.mgmt.kubernetesconfiguration.models.ErrorDefinition - :param identity: The identity of the configuration. - :type identity: - ~azure.mgmt.kubernetesconfiguration.models.ConfigurationIdentity - """ - - _validation = { - 'id': {'readonly': True}, - 'name': {'readonly': True}, - 'type': {'readonly': True}, - 'creation_time': {'readonly': True}, - 'last_modified_time': {'readonly': True}, - 'last_status_time': {'readonly': True}, - 'error_info': {'readonly': True}, - } - - _attribute_map = { - 'id': {'key': 'id', 'type': 'str'}, - 'name': {'key': 'name', 'type': 'str'}, - 'type': {'key': 'type', 'type': 'str'}, - 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, - 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, - 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, - 'version': {'key': 'properties.version', 'type': 'str'}, - 'scope': {'key': 'properties.scope', 'type': 'Scope'}, - 'install_state': {'key': 'properties.installState', 'type': 'str'}, - 'creation_time': {'key': 'properties.creationTime', 'type': 'str'}, - 'last_modified_time': {'key': 'properties.lastModifiedTime', 'type': 'str'}, - 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, - 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'}, - 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, - } - - def __init__(self, **kwargs): - super(ExtensionInstanceForList, self).__init__(**kwargs) - self.extension_type = kwargs.get('extension_type', None) - self.auto_upgrade_minor_version = kwargs.get('auto_upgrade_minor_version', None) - self.release_train = kwargs.get('release_train', None) - self.version = kwargs.get('version', None) - self.scope = kwargs.get('scope', None) self.install_state = kwargs.get('install_state', None) + self.statuses = kwargs.get('statuses', None) self.creation_time = None self.last_modified_time = None self.last_status_time = None @@ -512,6 +404,88 @@ def __init__(self, **kwargs): self.time = kwargs.get('time', None) +class HelmOperatorProperties(Model): + """Properties for Helm operator. + + :param chart_version: Version of the operator Helm chart. + :type chart_version: str + :param chart_values: Values override for the operator Helm chart. + :type chart_values: str + """ + + _attribute_map = { + 'chart_version': {'key': 'chartVersion', 'type': 'str'}, + 'chart_values': {'key': 'chartValues', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(HelmOperatorProperties, self).__init__(**kwargs) + self.chart_version = kwargs.get('chart_version', None) + self.chart_values = kwargs.get('chart_values', None) + + +class ResourceProviderOperation(Model): + """Supported operation of this resource provider. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param name: Operation name, in format of + {provider}/{resource}/{operation} + :type name: str + :param display: Display metadata associated with the operation. + :type display: + ~azure.mgmt.kubernetesconfiguration.models.ResourceProviderOperationDisplay + :ivar is_data_action: The flag that indicates whether the operation + applies to data plane. + :vartype is_data_action: bool + """ + + _validation = { + 'is_data_action': {'readonly': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'display': {'key': 'display', 'type': 'ResourceProviderOperationDisplay'}, + 'is_data_action': {'key': 'isDataAction', 'type': 'bool'}, + } + + def __init__(self, **kwargs): + super(ResourceProviderOperation, self).__init__(**kwargs) + self.name = kwargs.get('name', None) + self.display = kwargs.get('display', None) + self.is_data_action = None + + +class ResourceProviderOperationDisplay(Model): + """Display metadata associated with the operation. + + :param provider: Resource provider: Microsoft KubernetesConfiguration. + :type provider: str + :param resource: Resource on which the operation is performed. + :type resource: str + :param operation: Type of operation: get, read, delete, etc. + :type operation: str + :param description: Description of this operation. + :type description: str + """ + + _attribute_map = { + 'provider': {'key': 'provider', 'type': 'str'}, + 'resource': {'key': 'resource', 'type': 'str'}, + 'operation': {'key': 'operation', 'type': 'str'}, + 'description': {'key': 'description', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ResourceProviderOperationDisplay, self).__init__(**kwargs) + self.provider = kwargs.get('provider', None) + self.resource = kwargs.get('resource', None) + self.operation = kwargs.get('operation', None) + self.description = kwargs.get('description', None) + + class Result(Model): """Sample result definition. @@ -585,3 +559,164 @@ class ScopeNamespace(Model): def __init__(self, **kwargs): super(ScopeNamespace, self).__init__(**kwargs) self.target_namespace = kwargs.get('target_namespace', None) + + +class SourceControlConfiguration(ProxyResource): + """The SourceControl Configuration object returned in Get & Put response. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + :param system_data: Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources + :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData + :param repository_url: Url of the SourceControl Repository. + :type repository_url: str + :param operator_namespace: The namespace to which this operator is + installed to. Maximum of 253 lower case alphanumeric characters, hyphen + and period only. Default value: "default" . + :type operator_namespace: str + :param operator_instance_name: Instance name of the operator - identifying + the specific configuration. + :type operator_instance_name: str + :param operator_type: Type of the operator. Possible values include: + 'Flux' + :type operator_type: str or + ~azure.mgmt.kubernetesconfiguration.models.OperatorType + :param operator_params: Any Parameters for the Operator instance in string + format. + :type operator_params: str + :param configuration_protected_settings: Name-value pairs of protected + configuration settings for the configuration + :type configuration_protected_settings: dict[str, str] + :param operator_scope: Scope at which the operator will be installed. + Possible values include: 'cluster', 'namespace'. Default value: "cluster" + . + :type operator_scope: str or + ~azure.mgmt.kubernetesconfiguration.models.OperatorScopeType + :ivar repository_public_key: Public Key associated with this SourceControl + configuration (either generated within the cluster or provided by the + user). + :vartype repository_public_key: str + :param ssh_known_hosts_contents: Base64-encoded known_hosts contents + containing public SSH keys required to access private Git instances + :type ssh_known_hosts_contents: str + :param enable_helm_operator: Option to enable Helm Operator for this git + configuration. + :type enable_helm_operator: bool + :param helm_operator_properties: Properties for Helm operator. + :type helm_operator_properties: + ~azure.mgmt.kubernetesconfiguration.models.HelmOperatorProperties + :ivar provisioning_state: The provisioning state of the resource provider. + Possible values include: 'Accepted', 'Deleting', 'Running', 'Succeeded', + 'Failed' + :vartype provisioning_state: str or + ~azure.mgmt.kubernetesconfiguration.models.ProvisioningStateType + :ivar compliance_status: Compliance Status of the Configuration + :vartype compliance_status: + ~azure.mgmt.kubernetesconfiguration.models.ComplianceStatus + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'repository_public_key': {'readonly': True}, + 'provisioning_state': {'readonly': True}, + 'compliance_status': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'repository_url': {'key': 'properties.repositoryUrl', 'type': 'str'}, + 'operator_namespace': {'key': 'properties.operatorNamespace', 'type': 'str'}, + 'operator_instance_name': {'key': 'properties.operatorInstanceName', 'type': 'str'}, + 'operator_type': {'key': 'properties.operatorType', 'type': 'str'}, + 'operator_params': {'key': 'properties.operatorParams', 'type': 'str'}, + 'configuration_protected_settings': {'key': 'properties.configurationProtectedSettings', 'type': '{str}'}, + 'operator_scope': {'key': 'properties.operatorScope', 'type': 'str'}, + 'repository_public_key': {'key': 'properties.repositoryPublicKey', 'type': 'str'}, + 'ssh_known_hosts_contents': {'key': 'properties.sshKnownHostsContents', 'type': 'str'}, + 'enable_helm_operator': {'key': 'properties.enableHelmOperator', 'type': 'bool'}, + 'helm_operator_properties': {'key': 'properties.helmOperatorProperties', 'type': 'HelmOperatorProperties'}, + 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, + 'compliance_status': {'key': 'properties.complianceStatus', 'type': 'ComplianceStatus'}, + } + + def __init__(self, **kwargs): + super(SourceControlConfiguration, self).__init__(**kwargs) + self.repository_url = kwargs.get('repository_url', None) + self.operator_namespace = kwargs.get('operator_namespace', "default") + self.operator_instance_name = kwargs.get('operator_instance_name', None) + self.operator_type = kwargs.get('operator_type', None) + self.operator_params = kwargs.get('operator_params', None) + self.configuration_protected_settings = kwargs.get('configuration_protected_settings', None) + self.operator_scope = kwargs.get('operator_scope', "cluster") + self.repository_public_key = None + self.ssh_known_hosts_contents = kwargs.get('ssh_known_hosts_contents', None) + self.enable_helm_operator = kwargs.get('enable_helm_operator', None) + self.helm_operator_properties = kwargs.get('helm_operator_properties', None) + self.provisioning_state = None + self.compliance_status = None + + +class SystemData(Model): + """Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar created_by: A string identifier for the identity that created the + resource + :vartype created_by: str + :ivar created_by_type: The type of identity that created the resource: + user, application, managedIdentity, key + :vartype created_by_type: str + :ivar created_at: The timestamp of resource creation (UTC) + :vartype created_at: datetime + :ivar last_modified_by: A string identifier for the identity that last + modified the resource + :vartype last_modified_by: str + :ivar last_modified_by_type: The type of identity that last modified the + resource: user, application, managedIdentity, key + :vartype last_modified_by_type: str + :ivar last_modified_at: The timestamp of resource last modification (UTC) + :vartype last_modified_at: datetime + """ + + _validation = { + 'created_by': {'readonly': True}, + 'created_by_type': {'readonly': True}, + 'created_at': {'readonly': True}, + 'last_modified_by': {'readonly': True}, + 'last_modified_by_type': {'readonly': True}, + 'last_modified_at': {'readonly': True}, + } + + _attribute_map = { + 'created_by': {'key': 'createdBy', 'type': 'str'}, + 'created_by_type': {'key': 'createdByType', 'type': 'str'}, + 'created_at': {'key': 'createdAt', 'type': 'iso-8601'}, + 'last_modified_by': {'key': 'lastModifiedBy', 'type': 'str'}, + 'last_modified_by_type': {'key': 'lastModifiedByType', 'type': 'str'}, + 'last_modified_at': {'key': 'lastModifiedAt', 'type': 'iso-8601'}, + } + + def __init__(self, **kwargs): + super(SystemData, self).__init__(**kwargs) + self.created_by = None + self.created_by_type = None + self.created_at = None + self.last_modified_by = None + self.last_modified_by_type = None + self.last_modified_at = None diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py index 16b408963ab..36fe12c02ce 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py @@ -20,6 +20,47 @@ class CloudError(Model): _attribute_map = { } + +class ComplianceStatus(Model): + """Compliance Status details. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar compliance_state: The compliance state of the configuration. + Possible values include: 'Pending', 'Compliant', 'Noncompliant', + 'Installed', 'Failed' + :vartype compliance_state: str or + ~azure.mgmt.kubernetesconfiguration.models.ComplianceStateType + :param last_config_applied: Datetime the configuration was last applied. + :type last_config_applied: datetime + :param message: Message from when the configuration was applied. + :type message: str + :param message_level: Level of the message. Possible values include: + 'Error', 'Warning', 'Information' + :type message_level: str or + ~azure.mgmt.kubernetesconfiguration.models.MessageLevelType + """ + + _validation = { + 'compliance_state': {'readonly': True}, + } + + _attribute_map = { + 'compliance_state': {'key': 'complianceState', 'type': 'str'}, + 'last_config_applied': {'key': 'lastConfigApplied', 'type': 'iso-8601'}, + 'message': {'key': 'message', 'type': 'str'}, + 'message_level': {'key': 'messageLevel', 'type': 'str'}, + } + + def __init__(self, *, last_config_applied=None, message: str=None, message_level=None, **kwargs) -> None: + super(ComplianceStatus, self).__init__(**kwargs) + self.compliance_state = None + self.last_config_applied = last_config_applied + self.message = message + self.message_level = message_level + + class ConfigurationIdentity(Model): """Identity for the managed cluster. @@ -126,6 +167,9 @@ class Resource(Model): :vartype name: str :ivar type: Resource type :vartype type: str + :param system_data: Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources + :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData """ _validation = { @@ -138,13 +182,15 @@ class Resource(Model): 'id': {'key': 'id', 'type': 'str'}, 'name': {'key': 'name', 'type': 'str'}, 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, } - def __init__(self, **kwargs) -> None: + def __init__(self, *, system_data=None, **kwargs) -> None: super(Resource, self).__init__(**kwargs) self.id = None self.name = None self.type = None + self.system_data = system_data class ProxyResource(Resource): @@ -159,6 +205,9 @@ class ProxyResource(Resource): :vartype name: str :ivar type: Resource type :vartype type: str + :param system_data: Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources + :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData """ _validation = { @@ -171,10 +220,11 @@ class ProxyResource(Resource): 'id': {'key': 'id', 'type': 'str'}, 'name': {'key': 'name', 'type': 'str'}, 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, } - def __init__(self, **kwargs) -> None: - super(ProxyResource, self).__init__(**kwargs) + def __init__(self, *, system_data=None, **kwargs) -> None: + super(ProxyResource, self).__init__(system_data=system_data, **kwargs) class ExtensionInstance(ProxyResource): @@ -189,6 +239,9 @@ class ExtensionInstance(ProxyResource): :vartype name: str :ivar type: Resource type :vartype type: str + :param system_data: Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources + :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData :param extension_type: Type of the Extension, of which this resource is an instance of. It must be one of the Extension Types registered with Microsoft.KubernetesConfiguration by the Extension publisher. @@ -209,6 +262,10 @@ class ExtensionInstance(ProxyResource): :param configuration_settings: Configuration settings, as name-value pairs for configuring this instance of the extension. :type configuration_settings: dict[str, str] + :param configuration_protected_settings: Configuration settings that are + sensitive, as name-value pairs for configuring this instance of the + extension. + :type configuration_protected_settings: dict[str, str] :param install_state: Status of installation of this instance of the extension. Possible values include: 'Pending', 'Installed', 'Failed' :type install_state: str or @@ -248,103 +305,25 @@ class ExtensionInstance(ProxyResource): 'id': {'key': 'id', 'type': 'str'}, 'name': {'key': 'name', 'type': 'str'}, 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, 'version': {'key': 'properties.version', 'type': 'str'}, 'scope': {'key': 'properties.scope', 'type': 'Scope'}, 'configuration_settings': {'key': 'properties.configurationSettings', 'type': '{str}'}, + 'configuration_protected_settings': {'key': 'properties.configurationProtectedSettings', 'type': '{str}'}, 'install_state': {'key': 'properties.installState', 'type': 'str'}, 'statuses': {'key': 'properties.statuses', 'type': '[ExtensionStatus]'}, 'creation_time': {'key': 'properties.creationTime', 'type': 'str'}, 'last_modified_time': {'key': 'properties.lastModifiedTime', 'type': 'str'}, 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'}, - 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, - } - - def __init__(self, *, extension_type: str=None, auto_upgrade_minor_version: bool=None, release_train: str=None, version: str=None, scope=None, configuration_settings=None, install_state=None, statuses=None, identity=None, **kwargs) -> None: - super(ExtensionInstance, self).__init__(**kwargs) - self.extension_type = extension_type - self.auto_upgrade_minor_version = auto_upgrade_minor_version - self.release_train = release_train - self.version = version - self.scope = scope - self.configuration_settings = configuration_settings - self.install_state = install_state - self.statuses = statuses - self.creation_time = None - self.last_modified_time = None - self.last_status_time = None - self.error_info = None - self.identity = identity - - -class ExtensionInstanceForCreate(ProxyResource): - """Object to create a new Extension Instance. - - Variables are only populated by the server, and will be ignored when - sending a request. - - :ivar id: Resource Id - :vartype id: str - :ivar name: Resource name - :vartype name: str - :ivar type: Resource type - :vartype type: str - :param extension_type: Type of the Extension, of which this resource is an - instance of. It must be one of the Extension Types registered with - Microsoft.KubernetesConfiguration by the Extension publisher. - :type extension_type: str - :param auto_upgrade_minor_version: Flag to note if this instance - participates in auto upgrade of minor version, or not. - :type auto_upgrade_minor_version: bool - :param release_train: ReleaseTrain this extension instance participates in - for auto-upgrade (e.g. Stable, Preview, etc.) - only if - autoUpgradeMinorVersion is 'true'. - :type release_train: str - :param version: Version of the extension for this extension instance, if - it is 'pinned' to a specific version. autoUpgradeMinorVersion must be - 'false'. - :type version: str - :param scope: Scope at which the extension instance is installed. - :type scope: ~azure.mgmt.kubernetesconfiguration.models.Scope - :param configuration_settings: Configuration settings, as name-value pairs - for configuring this instance of the extension. - :type configuration_settings: dict[str, str] - :param configuration_protected_settings: Configuration settings that are - sensitive, as name-value pairs for configuring this instance of the - extension. - :type configuration_protected_settings: dict[str, str] - :param identity: The identity of the configuration. - :type identity: - ~azure.mgmt.kubernetesconfiguration.models.ConfigurationIdentity - :type location: str - """ - - _validation = { - 'id': {'readonly': True}, - 'name': {'readonly': True}, - 'type': {'readonly': True}, - } - - _attribute_map = { - 'id': {'key': 'id', 'type': 'str'}, - 'name': {'key': 'name', 'type': 'str'}, - 'type': {'key': 'type', 'type': 'str'}, - 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, - 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, - 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, - 'version': {'key': 'properties.version', 'type': 'str'}, - 'scope': {'key': 'properties.scope', 'type': 'Scope'}, - 'configuration_settings': {'key': 'properties.configurationSettings', 'type': '{str}'}, - 'configuration_protected_settings': {'key': 'properties.configurationProtectedSettings', 'type': '{str}'}, - 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, - 'location': {'key': 'location', 'type': 'str'}, + 'identity': {'key': 'properties.identity', 'type': 'ConfigurationIdentity'}, } - def __init__(self, *, extension_type: str=None, auto_upgrade_minor_version: bool=None, release_train: str=None, version: str=None, scope=None, configuration_settings=None, configuration_protected_settings=None, identity=None, location=None, **kwargs) -> None: - super(ExtensionInstanceForCreate, self).__init__(location=location,**kwargs) + def __init__(self, *, system_data=None, extension_type: str=None, auto_upgrade_minor_version: bool=None, release_train: str=None, version: str=None, scope=None, configuration_settings=None, configuration_protected_settings=None, install_state=None, statuses=None, identity=None, **kwargs) -> None: + super(ExtensionInstance, self).__init__(system_data=system_data, **kwargs) self.extension_type = extension_type self.auto_upgrade_minor_version = auto_upgrade_minor_version self.release_train = release_train @@ -352,95 +331,8 @@ def __init__(self, *, extension_type: str=None, auto_upgrade_minor_version: bool self.scope = scope self.configuration_settings = configuration_settings self.configuration_protected_settings = configuration_protected_settings - self.identity = identity - self.location = location - - -class ExtensionInstanceForList(ProxyResource): - """The Extension Instance object. - - Variables are only populated by the server, and will be ignored when - sending a request. - - :ivar id: Resource Id - :vartype id: str - :ivar name: Resource name - :vartype name: str - :ivar type: Resource type - :vartype type: str - :param extension_type: Type of the Extension, of which this resource is an - instance of. It must be one of the Extension Types registered with - Microsoft.KubernetesConfiguration by the Extension publisher. - :type extension_type: str - :param auto_upgrade_minor_version: Flag to note if this instance - participates in auto upgrade of minor version, or not. - :type auto_upgrade_minor_version: bool - :param release_train: ReleaseTrain this extension instance participates in - for auto-upgrade (e.g. Stable, Preview, etc.) - only if - autoUpgradeMinorVersion is 'true'. - :type release_train: str - :param version: Version of the extension for this extension instance, if - it is 'pinned' to a specific version. - :type version: str - :param scope: Scope at which the extension instance is installed. - :type scope: ~azure.mgmt.kubernetesconfiguration.models.Scope - :param install_state: Status of installation of this instance of the - extension. Possible values include: 'Pending', 'Installed', 'Failed' - :type install_state: str or - ~azure.mgmt.kubernetesconfiguration.models.InstallStateType - :ivar creation_time: DateLiteral (per ISO8601) noting the time the - resource was created by the client (user). - :vartype creation_time: str - :ivar last_modified_time: DateLiteral (per ISO8601) noting the time the - resource was modified by the client (user). - :vartype last_modified_time: str - :ivar last_status_time: DateLiteral (per ISO8601) noting the time of last - status from the agent. - :vartype last_status_time: str - :ivar error_info: Error information from the Agent - e.g. errors during - installation. - :vartype error_info: - ~azure.mgmt.kubernetesconfiguration.models.ErrorDefinition - :param identity: The identity of the configuration. - :type identity: - ~azure.mgmt.kubernetesconfiguration.models.ConfigurationIdentity - """ - - _validation = { - 'id': {'readonly': True}, - 'name': {'readonly': True}, - 'type': {'readonly': True}, - 'creation_time': {'readonly': True}, - 'last_modified_time': {'readonly': True}, - 'last_status_time': {'readonly': True}, - 'error_info': {'readonly': True}, - } - - _attribute_map = { - 'id': {'key': 'id', 'type': 'str'}, - 'name': {'key': 'name', 'type': 'str'}, - 'type': {'key': 'type', 'type': 'str'}, - 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, - 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, - 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, - 'version': {'key': 'properties.version', 'type': 'str'}, - 'scope': {'key': 'properties.scope', 'type': 'Scope'}, - 'install_state': {'key': 'properties.installState', 'type': 'str'}, - 'creation_time': {'key': 'properties.creationTime', 'type': 'str'}, - 'last_modified_time': {'key': 'properties.lastModifiedTime', 'type': 'str'}, - 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, - 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'}, - 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, - } - - def __init__(self, *, extension_type: str=None, auto_upgrade_minor_version: bool=None, release_train: str=None, version: str=None, scope=None, install_state=None, identity=None, **kwargs) -> None: - super(ExtensionInstanceForList, self).__init__(**kwargs) - self.extension_type = extension_type - self.auto_upgrade_minor_version = auto_upgrade_minor_version - self.release_train = release_train - self.version = version - self.scope = scope self.install_state = install_state + self.statuses = statuses self.creation_time = None self.last_modified_time = None self.last_status_time = None @@ -512,6 +404,88 @@ def __init__(self, *, code: str=None, display_status: str=None, level="Informati self.time = time +class HelmOperatorProperties(Model): + """Properties for Helm operator. + + :param chart_version: Version of the operator Helm chart. + :type chart_version: str + :param chart_values: Values override for the operator Helm chart. + :type chart_values: str + """ + + _attribute_map = { + 'chart_version': {'key': 'chartVersion', 'type': 'str'}, + 'chart_values': {'key': 'chartValues', 'type': 'str'}, + } + + def __init__(self, *, chart_version: str=None, chart_values: str=None, **kwargs) -> None: + super(HelmOperatorProperties, self).__init__(**kwargs) + self.chart_version = chart_version + self.chart_values = chart_values + + +class ResourceProviderOperation(Model): + """Supported operation of this resource provider. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param name: Operation name, in format of + {provider}/{resource}/{operation} + :type name: str + :param display: Display metadata associated with the operation. + :type display: + ~azure.mgmt.kubernetesconfiguration.models.ResourceProviderOperationDisplay + :ivar is_data_action: The flag that indicates whether the operation + applies to data plane. + :vartype is_data_action: bool + """ + + _validation = { + 'is_data_action': {'readonly': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'display': {'key': 'display', 'type': 'ResourceProviderOperationDisplay'}, + 'is_data_action': {'key': 'isDataAction', 'type': 'bool'}, + } + + def __init__(self, *, name: str=None, display=None, **kwargs) -> None: + super(ResourceProviderOperation, self).__init__(**kwargs) + self.name = name + self.display = display + self.is_data_action = None + + +class ResourceProviderOperationDisplay(Model): + """Display metadata associated with the operation. + + :param provider: Resource provider: Microsoft KubernetesConfiguration. + :type provider: str + :param resource: Resource on which the operation is performed. + :type resource: str + :param operation: Type of operation: get, read, delete, etc. + :type operation: str + :param description: Description of this operation. + :type description: str + """ + + _attribute_map = { + 'provider': {'key': 'provider', 'type': 'str'}, + 'resource': {'key': 'resource', 'type': 'str'}, + 'operation': {'key': 'operation', 'type': 'str'}, + 'description': {'key': 'description', 'type': 'str'}, + } + + def __init__(self, *, provider: str=None, resource: str=None, operation: str=None, description: str=None, **kwargs) -> None: + super(ResourceProviderOperationDisplay, self).__init__(**kwargs) + self.provider = provider + self.resource = resource + self.operation = operation + self.description = description + + class Result(Model): """Sample result definition. @@ -585,3 +559,164 @@ class ScopeNamespace(Model): def __init__(self, *, target_namespace: str=None, **kwargs) -> None: super(ScopeNamespace, self).__init__(**kwargs) self.target_namespace = target_namespace + + +class SourceControlConfiguration(ProxyResource): + """The SourceControl Configuration object returned in Get & Put response. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + :param system_data: Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources + :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData + :param repository_url: Url of the SourceControl Repository. + :type repository_url: str + :param operator_namespace: The namespace to which this operator is + installed to. Maximum of 253 lower case alphanumeric characters, hyphen + and period only. Default value: "default" . + :type operator_namespace: str + :param operator_instance_name: Instance name of the operator - identifying + the specific configuration. + :type operator_instance_name: str + :param operator_type: Type of the operator. Possible values include: + 'Flux' + :type operator_type: str or + ~azure.mgmt.kubernetesconfiguration.models.OperatorType + :param operator_params: Any Parameters for the Operator instance in string + format. + :type operator_params: str + :param configuration_protected_settings: Name-value pairs of protected + configuration settings for the configuration + :type configuration_protected_settings: dict[str, str] + :param operator_scope: Scope at which the operator will be installed. + Possible values include: 'cluster', 'namespace'. Default value: "cluster" + . + :type operator_scope: str or + ~azure.mgmt.kubernetesconfiguration.models.OperatorScopeType + :ivar repository_public_key: Public Key associated with this SourceControl + configuration (either generated within the cluster or provided by the + user). + :vartype repository_public_key: str + :param ssh_known_hosts_contents: Base64-encoded known_hosts contents + containing public SSH keys required to access private Git instances + :type ssh_known_hosts_contents: str + :param enable_helm_operator: Option to enable Helm Operator for this git + configuration. + :type enable_helm_operator: bool + :param helm_operator_properties: Properties for Helm operator. + :type helm_operator_properties: + ~azure.mgmt.kubernetesconfiguration.models.HelmOperatorProperties + :ivar provisioning_state: The provisioning state of the resource provider. + Possible values include: 'Accepted', 'Deleting', 'Running', 'Succeeded', + 'Failed' + :vartype provisioning_state: str or + ~azure.mgmt.kubernetesconfiguration.models.ProvisioningStateType + :ivar compliance_status: Compliance Status of the Configuration + :vartype compliance_status: + ~azure.mgmt.kubernetesconfiguration.models.ComplianceStatus + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'repository_public_key': {'readonly': True}, + 'provisioning_state': {'readonly': True}, + 'compliance_status': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'repository_url': {'key': 'properties.repositoryUrl', 'type': 'str'}, + 'operator_namespace': {'key': 'properties.operatorNamespace', 'type': 'str'}, + 'operator_instance_name': {'key': 'properties.operatorInstanceName', 'type': 'str'}, + 'operator_type': {'key': 'properties.operatorType', 'type': 'str'}, + 'operator_params': {'key': 'properties.operatorParams', 'type': 'str'}, + 'configuration_protected_settings': {'key': 'properties.configurationProtectedSettings', 'type': '{str}'}, + 'operator_scope': {'key': 'properties.operatorScope', 'type': 'str'}, + 'repository_public_key': {'key': 'properties.repositoryPublicKey', 'type': 'str'}, + 'ssh_known_hosts_contents': {'key': 'properties.sshKnownHostsContents', 'type': 'str'}, + 'enable_helm_operator': {'key': 'properties.enableHelmOperator', 'type': 'bool'}, + 'helm_operator_properties': {'key': 'properties.helmOperatorProperties', 'type': 'HelmOperatorProperties'}, + 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, + 'compliance_status': {'key': 'properties.complianceStatus', 'type': 'ComplianceStatus'}, + } + + def __init__(self, *, system_data=None, repository_url: str=None, operator_namespace: str="default", operator_instance_name: str=None, operator_type=None, operator_params: str=None, configuration_protected_settings=None, operator_scope="cluster", ssh_known_hosts_contents: str=None, enable_helm_operator: bool=None, helm_operator_properties=None, **kwargs) -> None: + super(SourceControlConfiguration, self).__init__(system_data=system_data, **kwargs) + self.repository_url = repository_url + self.operator_namespace = operator_namespace + self.operator_instance_name = operator_instance_name + self.operator_type = operator_type + self.operator_params = operator_params + self.configuration_protected_settings = configuration_protected_settings + self.operator_scope = operator_scope + self.repository_public_key = None + self.ssh_known_hosts_contents = ssh_known_hosts_contents + self.enable_helm_operator = enable_helm_operator + self.helm_operator_properties = helm_operator_properties + self.provisioning_state = None + self.compliance_status = None + + +class SystemData(Model): + """Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar created_by: A string identifier for the identity that created the + resource + :vartype created_by: str + :ivar created_by_type: The type of identity that created the resource: + user, application, managedIdentity, key + :vartype created_by_type: str + :ivar created_at: The timestamp of resource creation (UTC) + :vartype created_at: datetime + :ivar last_modified_by: A string identifier for the identity that last + modified the resource + :vartype last_modified_by: str + :ivar last_modified_by_type: The type of identity that last modified the + resource: user, application, managedIdentity, key + :vartype last_modified_by_type: str + :ivar last_modified_at: The timestamp of resource last modification (UTC) + :vartype last_modified_at: datetime + """ + + _validation = { + 'created_by': {'readonly': True}, + 'created_by_type': {'readonly': True}, + 'created_at': {'readonly': True}, + 'last_modified_by': {'readonly': True}, + 'last_modified_by_type': {'readonly': True}, + 'last_modified_at': {'readonly': True}, + } + + _attribute_map = { + 'created_by': {'key': 'createdBy', 'type': 'str'}, + 'created_by_type': {'key': 'createdByType', 'type': 'str'}, + 'created_at': {'key': 'createdAt', 'type': 'iso-8601'}, + 'last_modified_by': {'key': 'lastModifiedBy', 'type': 'str'}, + 'last_modified_by_type': {'key': 'lastModifiedByType', 'type': 'str'}, + 'last_modified_at': {'key': 'lastModifiedAt', 'type': 'iso-8601'}, + } + + def __init__(self, **kwargs) -> None: + super(SystemData, self).__init__(**kwargs) + self.created_by = None + self.created_by_type = None + self.created_at = None + self.last_modified_by = None + self.last_modified_by_type = None + self.last_modified_at = None diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_paged_models.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_paged_models.py index 8f2e7eca24e..c545286fe54 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_paged_models.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_paged_models.py @@ -12,6 +12,19 @@ from msrest.paging import Paged +class SourceControlConfigurationPaged(Paged): + """ + A paging container for iterating over a list of :class:`SourceControlConfiguration ` object + """ + + _attribute_map = { + 'next_link': {'key': 'nextLink', 'type': 'str'}, + 'current_page': {'key': 'value', 'type': '[SourceControlConfiguration]'} + } + + def __init__(self, *args, **kwargs): + + super(SourceControlConfigurationPaged, self).__init__(*args, **kwargs) class ResourceProviderOperationPaged(Paged): """ A paging container for iterating over a list of :class:`ResourceProviderOperation ` object @@ -25,16 +38,16 @@ class ResourceProviderOperationPaged(Paged): def __init__(self, *args, **kwargs): super(ResourceProviderOperationPaged, self).__init__(*args, **kwargs) -class ExtensionInstanceForListPaged(Paged): +class ExtensionInstancePaged(Paged): """ - A paging container for iterating over a list of :class:`ExtensionInstanceForList ` object + A paging container for iterating over a list of :class:`ExtensionInstance ` object """ _attribute_map = { 'next_link': {'key': 'nextLink', 'type': 'str'}, - 'current_page': {'key': 'value', 'type': '[ExtensionInstanceForList]'} + 'current_page': {'key': 'value', 'type': '[ExtensionInstance]'} } def __init__(self, *args, **kwargs): - super(ExtensionInstanceForListPaged, self).__init__(*args, **kwargs) + super(ExtensionInstancePaged, self).__init__(*args, **kwargs) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_k8s_extension_client_enums.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_source_control_configuration_client_enums.py similarity index 100% rename from src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_k8s_extension_client_enums.py rename to src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_source_control_configuration_client_enums.py diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/__init__.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/__init__.py index e8f158b24a3..6be16d2582d 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/__init__.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/__init__.py @@ -9,8 +9,12 @@ # regenerated. # -------------------------------------------------------------------------- -from ._k8s_extensions_operations import K8sExtensionsOperations +from ._source_control_configurations_operations import SourceControlConfigurationsOperations +from ._operations import Operations +from ._extensions_operations import ExtensionsOperations __all__ = [ - 'K8sExtensionsOperations', + 'SourceControlConfigurationsOperations', + 'Operations', + 'ExtensionsOperations', ] diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_k8s_extensions_operations.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_extensions_operations.py similarity index 84% rename from src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_k8s_extensions_operations.py rename to src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_extensions_operations.py index 716aefd7b06..e99f3328816 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_k8s_extensions_operations.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_extensions_operations.py @@ -15,11 +15,10 @@ from .. import models -class K8sExtensionsOperations(object): - """K8sExtensionsOperations operations. +class ExtensionsOperations(object): + """ExtensionsOperations operations. - You should not instantiate directly this class, but create a Client instance that will create it for you and attach - it as attribute. + You should not instantiate directly this class, but create a Client instance that will create it for you and attach it as attribute. :param client: Client for service requests. :param config: Configuration of service client. @@ -39,10 +38,9 @@ def __init__(self, client, config, serializer, deserializer): self.config = config - def get( - self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, - custom_headers=None, raw=False, **operation_config): - """Gets details of the Kubernetes Cluster Extension Instance. + def create( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, extension_instance, custom_headers=None, raw=False, **operation_config): + """Create a new Kubernetes Cluster Extension Instance. :param resource_group_name: The name of the resource group. :type resource_group_name: str @@ -53,13 +51,17 @@ def get( :type cluster_rp: str :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters (for AKS clusters) or connectedClusters (for - OnPrem K8S clusters) or appliances (for Arc Appliances). Possible - values include: 'managedClusters','connectedClusters', 'appliances' + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' :type cluster_resource_name: str :param cluster_name: The name of the kubernetes cluster. :type cluster_name: str :param extension_instance_name: Name of an instance of the Extension. :type extension_instance_name: str + :param extension_instance: Properties necessary to Create an Extension + Instance. + :type extension_instance: + ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstance :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -72,7 +74,7 @@ def get( :class:`ErrorResponseException` """ # Construct URL - url = self.get.metadata['url'] + url = self.create.metadata['url'] path_format_arguments = { 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), @@ -90,16 +92,19 @@ def get( # Construct headers header_parameters = {} header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' if self.config.generate_client_request_id: header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) if custom_headers: header_parameters.update(custom_headers) if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", - self.config.accept_language, 'str') + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(extension_instance, 'ExtensionInstance') # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) + request = self._client.put(url, query_parameters, header_parameters, body_content) response = self._client.send(request, stream=False, **operation_config) if response.status_code not in [200]: @@ -114,12 +119,11 @@ def get( return client_raw_response return deserialized - get.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} + create.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} - def create( - self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, - extension_instance, custom_headers=None, raw=False, **operation_config): - """Create a new Kubernetes Cluster Extension Instance. + def get( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, custom_headers=None, raw=False, **operation_config): + """Gets details of the Kubernetes Cluster Extension Instance. :param resource_group_name: The name of the resource group. :type resource_group_name: str @@ -130,17 +134,13 @@ def create( :type cluster_rp: str :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters (for AKS clusters) or connectedClusters (for - OnPrem K8S clusters) or appliances (for Arc Appliances). Possible - values include: 'managedClusters','connectedClusters', 'appliances' + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' :type cluster_resource_name: str :param cluster_name: The name of the kubernetes cluster. :type cluster_name: str :param extension_instance_name: Name of an instance of the Extension. :type extension_instance_name: str - :param extension_instance: Properties necessary to Create an Extension - Instance. - :type extension_instance: - ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstanceForCreate :param dict custom_headers: headers that will be added to the request :param bool raw: returns the direct response alongside the deserialized response @@ -153,7 +153,7 @@ def create( :class:`ErrorResponseException` """ # Construct URL - url = self.create.metadata['url'] + url = self.get.metadata['url'] path_format_arguments = { 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), @@ -171,20 +171,15 @@ def create( # Construct headers header_parameters = {} header_parameters['Accept'] = 'application/json' - header_parameters['Content-Type'] = 'application/json; charset=utf-8' if self.config.generate_client_request_id: header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) if custom_headers: header_parameters.update(custom_headers) if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", - self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(extension_instance, 'ExtensionInstanceForCreate') + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') # Construct and send request - request = self._client.put(url, query_parameters, header_parameters, body_content) + request = self._client.get(url, query_parameters, header_parameters) response = self._client.send(request, stream=False, **operation_config) if response.status_code not in [200]: @@ -199,13 +194,10 @@ def create( return client_raw_response return deserialized - create.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}' - '/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/' - 'extensions/{extensionInstanceName}'} + get.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} def update( - self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, - extension_instance, custom_headers=None, raw=False, **operation_config): + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, extension_instance, custom_headers=None, raw=False, **operation_config): """Update an existing Kubernetes Cluster Extension Instance. :param resource_group_name: The name of the resource group. @@ -217,8 +209,8 @@ def update( :type cluster_rp: str :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters (for AKS clusters) or connectedClusters (for - OnPrem K8S clusters) or appliances (for Arc Appliances). Possible - values include: 'managedClusters','connectedClusters', 'appliances' + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' :type cluster_resource_name: str :param cluster_name: The name of the kubernetes cluster. :type cluster_name: str @@ -264,8 +256,7 @@ def update( if custom_headers: header_parameters.update(custom_headers) if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", - self.config.accept_language, 'str') + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') # Construct body body_content = self._serialize.body(extension_instance, 'ExtensionInstanceUpdate') @@ -286,13 +277,10 @@ def update( return client_raw_response return deserialized - update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}' - '/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/' - 'extensions/{extensionInstanceName}'} + update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} def delete( - self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, - custom_headers=None, raw=False, **operation_config): + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, custom_headers=None, raw=False, **operation_config): """Delete a Kubernetes Cluster Extension Instance. This will cause the Agent to Uninstall the extension instance from the cluster. @@ -305,8 +293,8 @@ def delete( :type cluster_rp: str :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters (for AKS clusters) or connectedClusters (for - OnPrem K8S clusters) or appliances (for Arc Appliances). Possible - values include: 'managedClusters','connectedClusters', 'appliances' + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' :type cluster_resource_name: str :param cluster_name: The name of the kubernetes cluster. :type cluster_name: str @@ -345,8 +333,7 @@ def delete( if custom_headers: header_parameters.update(custom_headers) if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", - self.config.accept_language, 'str') + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') # Construct and send request request = self._client.delete(url, query_parameters, header_parameters) @@ -358,14 +345,11 @@ def delete( if raw: client_raw_response = ClientRawResponse(None, response) return client_raw_response - delete.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}' - '/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/' - 'extensions/{extensionInstanceName}'} + delete.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} def list( - self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, custom_headers=None, raw=False, - **operation_config): - """List all Extension Instances. + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, custom_headers=None, raw=False, **operation_config): + """List all Source Control Configurations. :param resource_group_name: The name of the resource group. :type resource_group_name: str @@ -376,8 +360,8 @@ def list( :type cluster_rp: str :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters (for AKS clusters) or connectedClusters (for - OnPrem K8S clusters) or appliances (for Arc Appliances). Possible - values include: 'managedClusters','connectedClusters', 'appliances' + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' :type cluster_resource_name: str :param cluster_name: The name of the kubernetes cluster. :type cluster_name: str @@ -386,9 +370,9 @@ def list( deserialized response :param operation_config: :ref:`Operation configuration overrides`. - :return: An iterator like instance of ExtensionInstanceForList + :return: An iterator like instance of ExtensionInstance :rtype: - ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstanceForListPaged[~azure.mgmt.kubernetesconfiguration.models.ExtensionInstanceForList] + ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstancePaged[~azure.mgmt.kubernetesconfiguration.models.ExtensionInstance] :raises: :class:`ErrorResponseException` """ @@ -397,8 +381,7 @@ def prepare_request(next_link=None): # Construct URL url = self.list.metadata['url'] path_format_arguments = { - 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, - 'str'), + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), @@ -422,8 +405,7 @@ def prepare_request(next_link=None): if custom_headers: header_parameters.update(custom_headers) if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", - self.config.accept_language, 'str') + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') # Construct and send request request = self._client.get(url, query_parameters, header_parameters) @@ -443,10 +425,7 @@ def internal_paging(next_link=None): header_dict = None if raw: header_dict = {} - deserialized = models.ExtensionInstanceForListPaged(internal_paging, self._deserialize.dependencies, - header_dict) + deserialized = models.ExtensionInstancePaged(internal_paging, self._deserialize.dependencies, header_dict) return deserialized - list.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}' - '/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/' - 'extensions'} + list.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions'} diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_operations.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_operations.py new file mode 100644 index 00000000000..245a93c8294 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_operations.py @@ -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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse + +from .. import models + + +class Operations(object): + """Operations operations. + + You should not instantiate directly this class, but create a Client instance that will create it for you and attach it as attribute. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: The API version to be used with the HTTP request. Constant value: "2020-07-01-preview". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-07-01-preview" + + self.config = config + + def list( + self, custom_headers=None, raw=False, **operation_config): + """List all the available operations the KubernetesConfiguration resource + provider supports. + + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: An iterator like instance of ResourceProviderOperation + :rtype: + ~azure.mgmt.kubernetesconfiguration.models.ResourceProviderOperationPaged[~azure.mgmt.kubernetesconfiguration.models.ResourceProviderOperation] + :raises: + :class:`ErrorResponseException` + """ + def prepare_request(next_link=None): + if not next_link: + # Construct URL + url = self.list.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + else: + url = next_link + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + return request + + def internal_paging(next_link=None): + request = prepare_request(next_link) + + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + return response + + # Deserialize response + header_dict = None + if raw: + header_dict = {} + deserialized = models.ResourceProviderOperationPaged(internal_paging, self._deserialize.dependencies, header_dict) + + return deserialized + list.metadata = {'url': '/providers/Microsoft.KubernetesConfiguration/operations'} diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_source_control_configurations_operations.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_source_control_configurations_operations.py new file mode 100644 index 00000000000..83a49e32146 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_source_control_configurations_operations.py @@ -0,0 +1,386 @@ +# 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. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse +from msrest.polling import LROPoller, NoPolling +from msrestazure.polling.arm_polling import ARMPolling + +from .. import models + + +class SourceControlConfigurationsOperations(object): + """SourceControlConfigurationsOperations operations. + + You should not instantiate directly this class, but create a Client instance that will create it for you and attach it as attribute. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: The API version to be used with the HTTP request. Constant value: "2020-07-01-preview". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-07-01-preview" + + self.config = config + + def get( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, source_control_configuration_name, custom_headers=None, raw=False, **operation_config): + """Gets details of the Source Control Configuration. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param source_control_configuration_name: Name of the Source Control + Configuration. + :type source_control_configuration_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: SourceControlConfiguration or ClientRawResponse if raw=true + :rtype: + ~azure.mgmt.kubernetesconfiguration.models.SourceControlConfiguration + or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.get.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'sourceControlConfigurationName': self._serialize.url("source_control_configuration_name", source_control_configuration_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + if response.status_code == 200: + deserialized = self._deserialize('SourceControlConfiguration', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations/{sourceControlConfigurationName}'} + + def create_or_update( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, source_control_configuration_name, source_control_configuration, custom_headers=None, raw=False, **operation_config): + """Create a new Kubernetes Source Control Configuration. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param source_control_configuration_name: Name of the Source Control + Configuration. + :type source_control_configuration_name: str + :param source_control_configuration: Properties necessary to Create + KubernetesConfiguration. + :type source_control_configuration: + ~azure.mgmt.kubernetesconfiguration.models.SourceControlConfiguration + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: SourceControlConfiguration or ClientRawResponse if raw=true + :rtype: + ~azure.mgmt.kubernetesconfiguration.models.SourceControlConfiguration + or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.create_or_update.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'sourceControlConfigurationName': self._serialize.url("source_control_configuration_name", source_control_configuration_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(source_control_configuration, 'SourceControlConfiguration') + + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 201]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + if response.status_code == 200: + deserialized = self._deserialize('SourceControlConfiguration', response) + if response.status_code == 201: + deserialized = self._deserialize('SourceControlConfiguration', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + create_or_update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations/{sourceControlConfigurationName}'} + + + def _delete_initial( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, source_control_configuration_name, custom_headers=None, raw=False, **operation_config): + # Construct URL + url = self.delete.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'sourceControlConfigurationName': self._serialize.url("source_control_configuration_name", source_control_configuration_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 204]: + raise models.ErrorResponseException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + + def delete( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, source_control_configuration_name, custom_headers=None, raw=False, polling=True, **operation_config): + """This will delete the YAML file used to set up the Source control + configuration, thus stopping future sync from the source repo. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param source_control_configuration_name: Name of the Source Control + Configuration. + :type source_control_configuration_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: The poller return type is ClientRawResponse, the + direct response alongside the deserialized response + :param polling: True for ARMPolling, False for no polling, or a + polling object for personal polling strategy + :return: An instance of LROPoller that returns None or + ClientRawResponse if raw==True + :rtype: ~msrestazure.azure_operation.AzureOperationPoller[None] or + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[None]] + :raises: + :class:`ErrorResponseException` + """ + raw_result = self._delete_initial( + resource_group_name=resource_group_name, + cluster_rp=cluster_rp, + cluster_resource_name=cluster_resource_name, + cluster_name=cluster_name, + source_control_configuration_name=source_control_configuration_name, + custom_headers=custom_headers, + raw=True, + **operation_config + ) + + def get_long_running_output(response): + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + + lro_delay = operation_config.get( + 'long_running_operation_timeout', + self.config.long_running_operation_timeout) + if polling is True: polling_method = ARMPolling(lro_delay, **operation_config) + elif polling is False: polling_method = NoPolling() + else: polling_method = polling + return LROPoller(self._client, raw_result, get_long_running_output, polling_method) + delete.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations/{sourceControlConfigurationName}'} + + def list( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, custom_headers=None, raw=False, **operation_config): + """List all Source Control Configurations. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: An iterator like instance of SourceControlConfiguration + :rtype: + ~azure.mgmt.kubernetesconfiguration.models.SourceControlConfigurationPaged[~azure.mgmt.kubernetesconfiguration.models.SourceControlConfiguration] + :raises: + :class:`ErrorResponseException` + """ + def prepare_request(next_link=None): + if not next_link: + # Construct URL + url = self.list.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + else: + url = next_link + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + return request + + def internal_paging(next_link=None): + request = prepare_request(next_link) + + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + return response + + # Deserialize response + header_dict = None + if raw: + header_dict = {} + deserialized = models.SourceControlConfigurationPaged(internal_paging, self._deserialize.dependencies, header_dict) + + return deserialized + list.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations'} diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/version.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/version.py index e0ec669828c..3e682bbd5fb 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/version.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/version.py @@ -9,5 +9,5 @@ # regenerated. # -------------------------------------------------------------------------- -VERSION = "0.1.0" +VERSION = "0.3.0" From 1df2ef5cebee34e490b40a79981bd2d199a1c50f Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Tue, 16 Mar 2021 11:39:04 -0700 Subject: [PATCH 18/86] remove py2 bdist support --- src/k8s-extension/setup.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/k8s-extension/setup.cfg b/src/k8s-extension/setup.cfg index 5eab412034f..e69de29bb2d 100644 --- a/src/k8s-extension/setup.cfg +++ b/src/k8s-extension/setup.cfg @@ -1,2 +0,0 @@ -[bdist_wheel] -universal=1 From 054a903db08bb126410ba538f553c2420fb2989e Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Tue, 16 Mar 2021 12:30:38 -0700 Subject: [PATCH 19/86] Add custom table formatting --- .../azext_k8s_extension/_format.py | 24 +++++++++++++++++++ .../azext_k8s_extension/commands.py | 5 ++-- 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 src/k8s-extension/azext_k8s_extension/_format.py diff --git a/src/k8s-extension/azext_k8s_extension/_format.py b/src/k8s-extension/azext_k8s_extension/_format.py new file mode 100644 index 00000000000..ef96f7cf88f --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/_format.py @@ -0,0 +1,24 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from collections import OrderedDict + + +def k8s_extension_list_table_format(results): + return [__get_table_row(result) for result in results] + + +def k8s_extension_show_table_format(result): + return __get_table_row(result) + + +def __get_table_row(result): + return OrderedDict([ + ('name', result['name']), + ('extensionType', result['extensionType']), + ('version', result['version']), + ('installState', result['installState']), + ('lastModifiedTime', result['lastModifiedTime']) + ]) diff --git a/src/k8s-extension/azext_k8s_extension/commands.py b/src/k8s-extension/azext_k8s_extension/commands.py index ff72ab62e08..931662814c0 100644 --- a/src/k8s-extension/azext_k8s_extension/commands.py +++ b/src/k8s-extension/azext_k8s_extension/commands.py @@ -6,6 +6,7 @@ # pylint: disable=line-too-long from azure.cli.core.commands import CliCommandType from azext_k8s_extension._client_factory import (cf_k8s_extension, cf_k8s_extension_operation) +from azext_k8s_extension._format import k8s_extension_list_table_format, k8s_extension_show_table_format import azext_k8s_extension._consts as consts @@ -21,5 +22,5 @@ def load_command_table(self, _): g.custom_command('create', 'create_k8s_extension') g.custom_command('update', 'update_k8s_extension') g.custom_command('delete', 'delete_k8s_extension', confirmation=True) - g.custom_command('list', 'list_k8s_extension') - g.custom_show_command('show', 'show_k8s_extension') + g.custom_command('list', 'list_k8s_extension', table_transformer=k8s_extension_list_table_format) + g.custom_show_command('show', 'show_k8s_extension', table_transformer=k8s_extension_show_table_format) From b2982523471694f5bc056d1b9a15e405cddc3932 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Tue, 16 Mar 2021 15:27:43 -0700 Subject: [PATCH 20/86] Remove unnecessary files --- .github/pull.yml | 14 ------ .../azext_k8s_extension/_validators.py | 21 -------- .../partner_extensions/__init__.py | 5 ++ .../vendored_sdks/_k8s_extension_client.py | 50 ------------------- 4 files changed, 5 insertions(+), 85 deletions(-) delete mode 100644 .github/pull.yml delete mode 100644 src/k8s-extension/azext_k8s_extension/_validators.py delete mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/_k8s_extension_client.py diff --git a/.github/pull.yml b/.github/pull.yml deleted file mode 100644 index 8f65923a382..00000000000 --- a/.github/pull.yml +++ /dev/null @@ -1,14 +0,0 @@ -version: "1" -rules: # Array of rules - - base: master # Required. Target branch - upstream: wei:master # Required. Must be in the same fork network. - mergeMethod: hardreset # Optional, one of [none, merge, squash, rebase, hardreset], Default: none. - mergeUnstable: false # Optional, merge pull request even when the mergeable_state is not clean. Default: false - - base: k8s-configuration - upstream: master # Required. Can be a branch in the same forked repo. - - base: k8s-extension/private-preview - upstream: master # Required. Can be a branch in the same forked repo. - - base: k8s-extension/public-preview - upstream: master # Required. Can be a branch in the same forked repo. - - base: release - upstream: master # Required. Can be a branch in the same forked repo. diff --git a/src/k8s-extension/azext_k8s_extension/_validators.py b/src/k8s-extension/azext_k8s_extension/_validators.py deleted file mode 100644 index 72270dab104..00000000000 --- a/src/k8s-extension/azext_k8s_extension/_validators.py +++ /dev/null @@ -1,21 +0,0 @@ -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- - - -def example_name_or_id_validator(cmd, namespace): - # Example of a storage account name or ID validator. - # See: https://github.com/Azure/azure-cli/blob/dev/doc/authoring_command_modules/authoring_commands.md#supporting-name-or-id-parameters # pylint: disable=line-too-long - - from azure.cli.core.commands.client_factory import get_subscription_id - from msrestazure.tools import is_valid_resource_id, resource_id - if namespace.storage_account: - if not is_valid_resource_id(namespace.RESOURCE): - namespace.storage_account = resource_id( - subscription=get_subscription_id(cmd.cli_ctx), - resource_group=namespace.resource_group_name, - namespace='Microsoft.Storage', - type='storageAccounts', - name=namespace.storage_account - ) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/__init__.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/__init__.py index e69de29bb2d..eaff94925e3 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/__init__.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/__init__.py @@ -0,0 +1,5 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- \ No newline at end of file diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/_k8s_extension_client.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_k8s_extension_client.py deleted file mode 100644 index 1b63ebfd1ac..00000000000 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/_k8s_extension_client.py +++ /dev/null @@ -1,50 +0,0 @@ -# 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 msrest.service_client import SDKClient -from msrest import Serializer, Deserializer - -from ._configuration import K8sExtensionClientConfiguration -from .operations import K8sExtensionsOperations -from . import models - - -class K8sExtensionClient(SDKClient): - """K8sExtension Client - - :ivar config: Configuration for client. - :vartype config: K8sExtensionClientConfiguration - - :ivar k8s_extensions: K8sExtensions operations - :vartype k8s_extensions: azure.mgmt.kubernetesconfiguration.operations.K8sExtensionsOperations - - :param credentials: Credentials needed for the client to connect to Azure. - :type credentials: :mod:`A msrestazure Credentials - object` - :param subscription_id: The Azure subscription ID. This is a - GUID-formatted string (e.g. 00000000-0000-0000-0000-000000000000) - :type subscription_id: str - :param str base_url: Service URL - """ - - def __init__( - self, credentials, subscription_id, base_url=None): - - self.config = K8sExtensionClientConfiguration(credentials, subscription_id, base_url) - super(K8sExtensionClient, self).__init__(self.config.credentials, self.config) - - client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} - self.api_version = '2020-07-01-preview' - self._serialize = Serializer(client_models) - self._deserialize = Deserializer(client_models) - - self.k8s_extensions = K8sExtensionsOperations( - self._client, self.config, self._serialize, self._deserialize) From afb4046a7e5d966cf48c968b00c878eb1ec8a50d Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Tue, 16 Mar 2021 15:34:00 -0700 Subject: [PATCH 21/86] Fix style issues --- .../azext_k8s_extension/partner_extensions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/__init__.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/__init__.py index eaff94925e3..9ccaff6c1b8 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/__init__.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/__init__.py @@ -2,4 +2,4 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for # license information. -# ----------------------------------------------------------------------------- \ No newline at end of file +# ----------------------------------------------------------------------------- From 21dff06eae89e8acf628ee463a834ac64e116b5b Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Tue, 16 Mar 2021 20:33:16 -0700 Subject: [PATCH 22/86] Fix branch based on comments --- azure-pipelines.yml | 2 +- src/k8s-extension/azext_k8s_extension/azext_metadata.json | 2 +- src/k8s-extension/azext_k8s_extension/custom.py | 1 - src/k8s-extension/setup.cfg | 0 4 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 src/k8s-extension/setup.cfg diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 565e9c56793..d43ef5102f0 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -168,4 +168,4 @@ jobs: displayName: "Verify Extension Ref Docs" inputs: targetType: 'filePath' - filePath: scripts/ci/test_index_ref_doc.sh \ No newline at end of file + filePath: scripts/ci/test_index_ref_doc.sh diff --git a/src/k8s-extension/azext_k8s_extension/azext_metadata.json b/src/k8s-extension/azext_k8s_extension/azext_metadata.json index 30fdaf614ee..cf7b8927a07 100644 --- a/src/k8s-extension/azext_k8s_extension/azext_metadata.json +++ b/src/k8s-extension/azext_k8s_extension/azext_metadata.json @@ -1,4 +1,4 @@ { "azext.isPreview": true, "azext.minCliCoreVersion": "2.15.0" -} \ No newline at end of file +} diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index b06567aca0b..2b578a301ce 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -14,7 +14,6 @@ InvalidArgumentValueError, CommandNotFoundError from azure.cli.core.commands.client_factory import get_subscription_id from azext_k8s_extension.vendored_sdks.models import ConfigurationIdentity -# from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate from azext_k8s_extension.vendored_sdks.models import ErrorResponseException from azext_k8s_extension.partner_extensions.ContainerInsights import ContainerInsights diff --git a/src/k8s-extension/setup.cfg b/src/k8s-extension/setup.cfg deleted file mode 100644 index e69de29bb2d..00000000000 From 93919f2457af6f244ec8ab7da500591060ee7a06 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Wed, 17 Mar 2021 08:30:49 -0700 Subject: [PATCH 23/86] Update identity piece manually --- .../azext_k8s_extension/vendored_sdks/models/_models.py | 2 +- .../azext_k8s_extension/vendored_sdks/models/_models_py3.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py index da1b94606cd..c821dfc362a 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py @@ -306,6 +306,7 @@ class ExtensionInstance(ProxyResource): 'name': {'key': 'name', 'type': 'str'}, 'type': {'key': 'type', 'type': 'str'}, 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, @@ -319,7 +320,6 @@ class ExtensionInstance(ProxyResource): 'last_modified_time': {'key': 'properties.lastModifiedTime', 'type': 'str'}, 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'}, - 'identity': {'key': 'properties.identity', 'type': 'ConfigurationIdentity'}, } def __init__(self, **kwargs): diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py index 36fe12c02ce..1ca6a98a35a 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py @@ -306,6 +306,7 @@ class ExtensionInstance(ProxyResource): 'name': {'key': 'name', 'type': 'str'}, 'type': {'key': 'type', 'type': 'str'}, 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, @@ -318,8 +319,7 @@ class ExtensionInstance(ProxyResource): 'creation_time': {'key': 'properties.creationTime', 'type': 'str'}, 'last_modified_time': {'key': 'properties.lastModifiedTime', 'type': 'str'}, 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, - 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'}, - 'identity': {'key': 'properties.identity', 'type': 'ConfigurationIdentity'}, + 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'} } def __init__(self, *, system_data=None, extension_type: str=None, auto_upgrade_minor_version: bool=None, release_train: str=None, version: str=None, scope=None, configuration_settings=None, configuration_protected_settings=None, install_state=None, statuses=None, identity=None, **kwargs) -> None: From 7e40b3ad2eee2ae66e4b4f73c92b88b42fda7fe3 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Wed, 17 Mar 2021 09:33:16 -0700 Subject: [PATCH 24/86] Don't handle defaults at the CLI level --- src/k8s-extension/azext_k8s_extension/custom.py | 2 +- .../partner_extensions/DefaultExtension.py | 17 +++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index 2b578a301ce..0ae0247a4b7 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -65,7 +65,7 @@ def show_k8s_extension(client, resource_group_name, cluster_name, name, cluster_ def create_k8s_extension(cmd, client, resource_group_name, cluster_name, name, cluster_type, - extension_type, scope='cluster', auto_upgrade_minor_version=None, release_train=None, + extension_type, scope=None, auto_upgrade_minor_version=None, release_train=None, version=None, target_namespace=None, release_namespace=None, configuration_settings=None, configuration_protected_settings=None, configuration_settings_file=None, configuration_protected_settings_file=None, tags=None): diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py index 8e813502edd..3117734116f 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py @@ -25,16 +25,13 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t """ ext_scope = None - if scope is None or scope.lower() == 'cluster': - scope_cluster = ScopeCluster(release_namespace=release_namespace) - ext_scope = Scope(cluster=scope_cluster, namespace=None) - else: - scope_namespace = ScopeNamespace(target_namespace=target_namespace) - ext_scope = Scope(namespace=scope_namespace, cluster=None) - - # If release-train is not input, set it to 'stable' - if release_train is None: - release_train = 'stable' + if scope is not None: + if scope.lower() == 'cluster': + scope_cluster = ScopeCluster(release_namespace=release_namespace) + ext_scope = Scope(cluster=scope_cluster, namespace=None) + elif scope.lower() == 'namespace': + scope_namespace = ScopeNamespace(target_namespace=target_namespace) + ext_scope = Scope(namespace=scope_namespace, cluster=None) create_identity = False extension_instance = ExtensionInstance( From d1befa8fd237e75cfb2a0582bb71b0e21aae1df3 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Wed, 17 Mar 2021 10:20:59 -0700 Subject: [PATCH 25/86] Remove defaults from CLI client --- src/k8s-extension/azext_k8s_extension/custom.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index 0ae0247a4b7..007e3ba0c07 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -19,6 +19,7 @@ from azext_k8s_extension.partner_extensions.ContainerInsights import ContainerInsights from azext_k8s_extension.partner_extensions.AzureDefender import AzureDefender from azext_k8s_extension.partner_extensions.DefaultExtension import DefaultExtension +import azext_k8s_extension._consts as consts from ._client_factory import cf_resources @@ -111,7 +112,6 @@ def create_k8s_extension(cmd, client, resource_group_name, cluster_name, name, c # Identity is not created by default. Extension type must specify if identity is required. create_identity = False - extension_instance = None # Scope & Namespace validation - common to all extension-types @@ -152,8 +152,8 @@ def update_k8s_extension(client, resource_group_name, cluster_type, cluster_name # TODO: Remove this after we eventually get PATCH implemented for update and uncomment raise CommandNotFoundError( - "\"k8s-extension update\" currently is not available. " - "Use \"k8s-extension create\" to update a previously created extension instance." + f"\"{consts.EXTENSION_NAME} update\" currently is not available. " + f"Use \"{consts.EXTENSION_NAME} create\" to update a previously created extension instance." ) # # Ensure some values are provided for update @@ -188,10 +188,7 @@ def delete_k8s_extension(client, resource_group_name, cluster_name, name, cluste """ # Determine ClusterRP cluster_rp = __get_cluster_rp(cluster_type) - - k8s_extension_instance_name = name - - return client.delete(resource_group_name, cluster_rp, cluster_type, cluster_name, k8s_extension_instance_name) + return client.delete(resource_group_name, cluster_rp, cluster_type, cluster_name, name) def __create_identity(cmd, resource_group_name, cluster_name, cluster_type, cluster_rp): From 076827cb9e052a83c372805087332f9bce5bd29d Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Wed, 17 Mar 2021 12:29:46 -0700 Subject: [PATCH 26/86] Check null target namespace with namespace scope --- src/k8s-extension/azext_k8s_extension/custom.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index 007e3ba0c07..92e5e5513a0 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -11,10 +11,11 @@ from msrestazure.azure_exceptions import CloudError from azure.cli.core.azclierror import ResourceNotFoundError, MutuallyExclusiveArgumentError, \ - InvalidArgumentValueError, CommandNotFoundError + InvalidArgumentValueError, CommandNotFoundError, RequiredArgumentMissingError from azure.cli.core.commands.client_factory import get_subscription_id from azext_k8s_extension.vendored_sdks.models import ConfigurationIdentity from azext_k8s_extension.vendored_sdks.models import ErrorResponseException +from azext_k8s_extension.vendored_sdks.models import Scope from azext_k8s_extension.partner_extensions.ContainerInsights import ContainerInsights from azext_k8s_extension.partner_extensions.AzureDefender import AzureDefender @@ -128,6 +129,7 @@ def create_k8s_extension(cmd, client, resource_group_name, cluster_name, name, c # Common validations __validate_version_and_auto_upgrade(extension_instance.version, extension_instance.auto_upgrade_minor_version) + __validate_scope_after_customization(extension_instance.scope) # Create identity, if required if create_identity: @@ -238,13 +240,17 @@ def __get_cluster_rp(cluster_type): def __validate_scope_and_namespace(scope, release_namespace, target_namespace): if scope == 'cluster': if target_namespace is not None: - message = "When Scope is 'cluster', target-namespace must not be given." + message = "When --scope is 'cluster', --target-namespace must not be given." raise MutuallyExclusiveArgumentError(message) else: if release_namespace is not None: - message = "When Scope is 'namespace', release-namespace must not be given." + message = "When --scope is 'namespace', --release-namespace must not be given." raise MutuallyExclusiveArgumentError(message) +def __validate_scope_after_customization(scope_obj: Scope): + if scope_obj is not None and scope_obj.namespace is not None and scope_obj.namespace.target_namespace is None: + message = "When --scope is 'namespace', --target-namespace must be given." + raise RequiredArgumentMissingError(message) def __validate_version_and_auto_upgrade(version, auto_upgrade_minor_version): if version is not None: From 550eea1b30279f2058d900c561f8851b44ce4c8a Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Wed, 17 Mar 2021 12:30:27 -0700 Subject: [PATCH 27/86] Update style --- src/k8s-extension/azext_k8s_extension/custom.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index 92e5e5513a0..2b29cd036c0 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -247,11 +247,13 @@ def __validate_scope_and_namespace(scope, release_namespace, target_namespace): message = "When --scope is 'namespace', --release-namespace must not be given." raise MutuallyExclusiveArgumentError(message) + def __validate_scope_after_customization(scope_obj: Scope): if scope_obj is not None and scope_obj.namespace is not None and scope_obj.namespace.target_namespace is None: message = "When --scope is 'namespace', --target-namespace must be given." raise RequiredArgumentMissingError(message) + def __validate_version_and_auto_upgrade(version, auto_upgrade_minor_version): if version is not None: if auto_upgrade_minor_version: From fbab3be6fe097d7e2db815954175ca6d45baa796 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Wed, 17 Mar 2021 13:39:49 -0700 Subject: [PATCH 28/86] Add cassandra operator and location to model --- .../azext_k8s_extension/custom.py | 2 + .../partner_extensions/AzureDefender.py | 4 +- .../partner_extensions/Cassandra.py | 57 +++++++++++++++++++ .../partner_extensions/ContainerInsights.py | 4 +- .../partner_extensions/DefaultExtension.py | 4 +- .../vendored_sdks/models/_models.py | 4 ++ .../vendored_sdks/models/_models_py3.py | 6 +- 7 files changed, 71 insertions(+), 10 deletions(-) create mode 100644 src/k8s-extension/azext_k8s_extension/partner_extensions/Cassandra.py diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index 2b29cd036c0..ba9dbfce501 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -19,6 +19,7 @@ from azext_k8s_extension.partner_extensions.ContainerInsights import ContainerInsights from azext_k8s_extension.partner_extensions.AzureDefender import AzureDefender +from azext_k8s_extension.partner_extensions.Cassandra import Cassandra from azext_k8s_extension.partner_extensions.DefaultExtension import DefaultExtension import azext_k8s_extension._consts as consts @@ -32,6 +33,7 @@ def ExtensionFactory(extension_name): extension_map = { 'microsoft.azuremonitor.containers': ContainerInsights, 'microsoft.azuredefender.kubernetes': AzureDefender, + 'cassandradatacentersoperator': Cassandra } # Return the extension if we find it in the map, else return the default diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py index 7721ea8c638..dcd2853affc 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py @@ -55,9 +55,7 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t version=version, scope=ext_scope, configuration_settings=configuration_settings, - configuration_protected_settings=configuration_protected_settings, - identity=None, - location="" + configuration_protected_settings=configuration_protected_settings ) return extension_instance, name, create_identity diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/Cassandra.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/Cassandra.py new file mode 100644 index 00000000000..2a609ce125a --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/Cassandra.py @@ -0,0 +1,57 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=unused-argument + +from azext_k8s_extension.vendored_sdks.models import ExtensionInstance +from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate +from azext_k8s_extension.vendored_sdks.models import ScopeCluster +from azext_k8s_extension.vendored_sdks.models import ScopeNamespace +from azext_k8s_extension.vendored_sdks.models import Scope + +from azext_k8s_extension.partner_extensions.PartnerExtensionModel import PartnerExtensionModel + + +class Cassandra(PartnerExtensionModel): + def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, + scope, auto_upgrade_minor_version, release_train, version, target_namespace, + release_namespace, configuration_settings, configuration_protected_settings, + configuration_settings_file, configuration_protected_settings_file): + + """Default validations & defaults for Create + Must create and return a valid 'ExtensionInstance' object. + + """ + ext_scope = None + if scope is not None: + if scope.lower() == 'cluster': + scope_cluster = ScopeCluster(release_namespace=release_namespace) + ext_scope = Scope(cluster=scope_cluster, namespace=None) + elif scope.lower() == 'namespace': + scope_namespace = ScopeNamespace(target_namespace=target_namespace) + ext_scope = Scope(namespace=scope_namespace, cluster=None) + + create_identity = True + extension_instance = ExtensionInstance( + extension_type=extension_type, + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version, + scope=ext_scope, + configuration_settings=configuration_settings, + configuration_protected_settings=configuration_protected_settings, + ) + return extension_instance, name, create_identity + + def Update(self, extension, auto_upgrade_minor_version, release_train, version): + """Default validations & defaults for Update + Must create and return a valid 'ExtensionInstanceUpdate' object. + + """ + return ExtensionInstanceUpdate( + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version + ) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py index a90b807020d..8c678b34915 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py @@ -67,9 +67,7 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t version=version, scope=ext_scope, configuration_settings=configuration_settings, - configuration_protected_settings=configuration_protected_settings, - identity=None, - location="" + configuration_protected_settings=configuration_protected_settings ) return extension_instance, name, create_identity diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py index 3117734116f..9a69199f838 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py @@ -41,9 +41,7 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t version=version, scope=ext_scope, configuration_settings=configuration_settings, - configuration_protected_settings=configuration_protected_settings, - identity=None, - location="" + configuration_protected_settings=configuration_protected_settings ) return extension_instance, name, create_identity diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py index c821dfc362a..f74ea5d809e 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py @@ -239,6 +239,8 @@ class ExtensionInstance(ProxyResource): :vartype name: str :ivar type: Resource type :vartype type: str + :param location: Location of resource type + :type location: str :param system_data: Top level metadata https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData @@ -305,6 +307,7 @@ class ExtensionInstance(ProxyResource): 'id': {'key': 'id', 'type': 'str'}, 'name': {'key': 'name', 'type': 'str'}, 'type': {'key': 'type', 'type': 'str'}, + 'location': {'key': 'location', 'type': 'str'}, 'system_data': {'key': 'systemData', 'type': 'SystemData'}, 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, @@ -324,6 +327,7 @@ class ExtensionInstance(ProxyResource): def __init__(self, **kwargs): super(ExtensionInstance, self).__init__(**kwargs) + self.location = kwargs.get('location', None) self.extension_type = kwargs.get('extension_type', None) self.auto_upgrade_minor_version = kwargs.get('auto_upgrade_minor_version', None) self.release_train = kwargs.get('release_train', None) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py index 1ca6a98a35a..57f42c85edd 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py @@ -239,6 +239,8 @@ class ExtensionInstance(ProxyResource): :vartype name: str :ivar type: Resource type :vartype type: str + :param location: Location of resource type + :type location: str :param system_data: Top level metadata https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData @@ -305,6 +307,7 @@ class ExtensionInstance(ProxyResource): 'id': {'key': 'id', 'type': 'str'}, 'name': {'key': 'name', 'type': 'str'}, 'type': {'key': 'type', 'type': 'str'}, + 'location': {'key': 'location', 'type': 'str'}, 'system_data': {'key': 'systemData', 'type': 'SystemData'}, 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, @@ -322,8 +325,9 @@ class ExtensionInstance(ProxyResource): 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'} } - def __init__(self, *, system_data=None, extension_type: str=None, auto_upgrade_minor_version: bool=None, release_train: str=None, version: str=None, scope=None, configuration_settings=None, configuration_protected_settings=None, install_state=None, statuses=None, identity=None, **kwargs) -> None: + def __init__(self, *, system_data=None, location: str=None, extension_type: str=None, auto_upgrade_minor_version: bool=None, release_train: str=None, version: str=None, scope=None, configuration_settings=None, configuration_protected_settings=None, install_state=None, statuses=None, identity=None, **kwargs) -> None: super(ExtensionInstance, self).__init__(system_data=system_data, **kwargs) + self.location = location self.extension_type = extension_type self.auto_upgrade_minor_version = auto_upgrade_minor_version self.release_train = release_train From 9aade9f1c5210d2a79ad425f9908b2e73b60faba Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Wed, 17 Mar 2021 17:50:55 -0700 Subject: [PATCH 29/86] Stage Public Version of k8s-extension 0.2.0 for official release (#15) * Create pull.yml * Update pull.yml * Update azure-pipelines.yml * Initial commit of k8s-extension * Update pipelines file * Update CODEOWNERS * Update private preview pipelines * Remove open service mesh from public release * Update pipeline files * Update public extension pipeline * Change condition variable * Add version to public preview/private preview * Update pipelines * Add different testing based on private branch * Add annotations to extension model * Update k8s-custom-pipelines.yml * Update SDKs with Updated Swagger Spec for 2020-07-01-preview (#13) * Update sdks with updated swagger spec * Update version and history rst * Reorder release history timeline * Fix ExtensionInstanceForCreate for import * remove py2 bdist support * Add custom table formatting * Remove unnecessary files * Fix style issues * Fix branch based on comments * Update identity piece manually * Don't handle defaults at the CLI level * Remove defaults from CLI client * Check null target namespace with namespace scope * Update style * Add cassandra operator and location to model Co-authored-by: action@github.com --- .github/CODEOWNERS | 6 +- k8s-custom-pipelines.yml | 356 +++++++++ src/k8s-extension/HISTORY.rst | 35 + src/k8s-extension/README.rst | 5 + .../azext_k8s_extension/__init__.py | 32 + .../azext_k8s_extension/_client_factory.py | 31 + .../azext_k8s_extension/_consts.py | 8 + .../azext_k8s_extension/_consts_private.py | 8 + .../azext_k8s_extension/_format.py | 24 + .../azext_k8s_extension/_help.py | 39 + .../azext_k8s_extension/_params.py | 74 ++ .../azext_k8s_extension/action.py | 37 + .../azext_k8s_extension/azext_metadata.json | 4 + .../azext_k8s_extension/commands.py | 26 + .../azext_k8s_extension/custom.py | 279 +++++++ .../partner_extensions/AzureDefender.py | 71 ++ .../partner_extensions/Cassandra.py | 57 ++ .../partner_extensions/ContainerInsights.py | 458 +++++++++++ .../partner_extensions/DefaultExtension.py | 57 ++ .../PartnerExtensionModel.py | 23 + .../partner_extensions/__init__.py | 5 + .../azext_k8s_extension/tests/__init__.py | 5 + .../tests/latest/__init__.py | 5 + .../latest/recordings/test_k8s_extension.yaml | 270 +++++++ .../latest/test_k8s_extension_scenario.py | 67 ++ .../vendored_sdks/__init__.py | 19 + .../vendored_sdks/_configuration.py | 49 ++ .../_source_control_configuration_client.py | 60 ++ .../vendored_sdks/models/__init__.py | 94 +++ .../vendored_sdks/models/_models.py | 726 ++++++++++++++++++ .../vendored_sdks/models/_models_py3.py | 726 ++++++++++++++++++ .../vendored_sdks/models/_paged_models.py | 53 ++ ...urce_control_configuration_client_enums.py | 68 ++ .../vendored_sdks/operations/__init__.py | 20 + .../operations/_extensions_operations.py | 431 +++++++++++ .../vendored_sdks/operations/_operations.py | 101 +++ ...ource_control_configurations_operations.py | 386 ++++++++++ .../vendored_sdks/version.py | 13 + src/k8s-extension/setup.py | 55 ++ 39 files changed, 4781 insertions(+), 2 deletions(-) create mode 100644 k8s-custom-pipelines.yml create mode 100644 src/k8s-extension/HISTORY.rst create mode 100644 src/k8s-extension/README.rst create mode 100644 src/k8s-extension/azext_k8s_extension/__init__.py create mode 100644 src/k8s-extension/azext_k8s_extension/_client_factory.py create mode 100644 src/k8s-extension/azext_k8s_extension/_consts.py create mode 100644 src/k8s-extension/azext_k8s_extension/_consts_private.py create mode 100644 src/k8s-extension/azext_k8s_extension/_format.py create mode 100644 src/k8s-extension/azext_k8s_extension/_help.py create mode 100644 src/k8s-extension/azext_k8s_extension/_params.py create mode 100644 src/k8s-extension/azext_k8s_extension/action.py create mode 100644 src/k8s-extension/azext_k8s_extension/azext_metadata.json create mode 100644 src/k8s-extension/azext_k8s_extension/commands.py create mode 100644 src/k8s-extension/azext_k8s_extension/custom.py create mode 100644 src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py create mode 100644 src/k8s-extension/azext_k8s_extension/partner_extensions/Cassandra.py create mode 100644 src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py create mode 100644 src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py create mode 100644 src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py create mode 100644 src/k8s-extension/azext_k8s_extension/partner_extensions/__init__.py create mode 100644 src/k8s-extension/azext_k8s_extension/tests/__init__.py create mode 100644 src/k8s-extension/azext_k8s_extension/tests/latest/__init__.py create mode 100644 src/k8s-extension/azext_k8s_extension/tests/latest/recordings/test_k8s_extension.yaml create mode 100644 src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/__init__.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/_configuration.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/_source_control_configuration_client.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/models/__init__.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_paged_models.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_source_control_configuration_client_enums.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/__init__.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_extensions_operations.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_operations.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_source_control_configurations_operations.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/version.py create mode 100644 src/k8s-extension/setup.py diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 66f026834e8..435dbbae79f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -124,9 +124,11 @@ /src/ssh/ @rlrossiter @danybeam @arrownj -/src/k8sconfiguration/ @NarayanThiru +/src/k8sconfiguration/ @NarayanThiru @jonathan-innis -/src/k8s-configuration/ @NarayanThiru +/src/k8s-configuration/ @NarayanThiru @jonathan-innis + +/src/k8s-extension/ @NarayanThiru @jonathan-innis /src/log-analytics-solution/ @zhoxing-ms diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml new file mode 100644 index 00000000000..f4e2984ddf2 --- /dev/null +++ b/k8s-custom-pipelines.yml @@ -0,0 +1,356 @@ +resources: + repositories: + - repository: K8sPartnerExtensionTest + type: git + endpoint: AzureReposConnection + name: One/compute-HybridMgmt-K8sPartnerExtensionTest + +trigger: + batch: true + branches: + include: + - k8s-extension/public + - k8s-extension/private +pr: + branches: + include: + - k8s-extension/public + - k8s-extension/private + +stages: +- stage: BuildTestPublishExtension + displayName: "Build, Test, and Publish Extension" + variables: + K8S_EXTENSION_REPO_PATH: $(Agent.BuildDirectory)/s/compute-HybridMgmt-K8sPartnerExtensionTest + CLI_REPO_PATH: $(Agent.BuildDirectory)/s/azure-cli-extensions + SUBSCRIPTION_ID: "15c06b1b-01d6-407b-bb21-740b8617dea3" + RESOURCE_GROUP: "K8sPartnerExtensionTest" + BASE_CLUSTER_NAME: "k8s-extension-cluster" + IS_PRIVATE_BRANCH: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/k8s-extension/private'), eq(variables['System.PullRequest.TargetBranch'], 'refs/heads/k8s-extension/private'))] + + EXTENSION_NAME: "k8s-extension" + EXTENSION_FILE_NAME: "k8s_extension" + jobs: + - job: K8sExtensionTestSuite + displayName: "Run the Test Suite" + pool: + vmImage: 'ubuntu-16.04' + steps: + - checkout: self + - checkout: K8sPartnerExtensionTest + - bash: | + echo "Installing helm3" + curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 + chmod 700 get_helm.sh + ./get_helm.sh + + echo "Installing kubectl" + curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl" + chmod +x ./kubectl + sudo mv ./kubectl /usr/local/bin/kubectl + kubectl version --client + displayName: "Setup the VM with helm3 and kubectl" + - task: UsePythonVersion@0 + displayName: 'Use Python 3.6' + inputs: + versionSpec: 3.6 + - bash: | + set -ev + echo "Building extension ${EXTENSION_NAME}..." + + # prepare and activate virtualenv + pip install virtualenv + python3 -m venv env/ + source env/bin/activate + + # clone azure-cli + pip install azdev + + ls $(CLI_REPO_PATH) + + azdev --version + azdev setup -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) + azdev extension build $(EXTENSION_NAME) + + workingDirectory: $(CLI_REPO_PATH) + displayName: "Setup and Build Extension with azdev" + + - bash: | + K8S_EXTENSION_VERSION=$(ls ${EXTENSION_FILE_NAME}* | cut -d "-" -f2) + echo "##vso[task.setvariable variable=K8S_EXTENSION_VERSION]$K8S_EXTENSION_VERSION" + cp * $(K8S_EXTENSION_REPO_PATH)/bin + workingDirectory: $(CLI_REPO_PATH)/dist + displayName: "Copy the Built .whl to Extension Test Path" + + - bash: | + RAND_STR=$RANDOM + AKS_CLUSTER_NAME="${BASE_CLUSTER_NAME}-${RAND_STR}-aks" + ARC_CLUSTER_NAME="${BASE_CLUSTER_NAME}-${RAND_STR}-arc" + + JSON_STRING=$(jq -n \ + --arg SUB_ID "$SUBSCRIPTION_ID" \ + --arg RG "$RESOURCE_GROUP" \ + --arg AKS_CLUSTER_NAME "$AKS_CLUSTER_NAME" \ + --arg ARC_CLUSTER_NAME "$ARC_CLUSTER_NAME" \ + --arg K8S_EXTENSION_VERSION "$K8S_EXTENSION_VERSION" \ + '{subscriptionId: $SUB_ID, resourceGroup: $RG, aksClusterName: $AKS_CLUSTER_NAME, arcClusterName: $ARC_CLUSTER_NAME, extensionVersion: {"k8s-extension": $K8S_EXTENSION_VERSION, connectedk8s: "1.0.0"}}') + echo $JSON_STRING > settings.json + cat settings.json + workingDirectory: $(K8S_EXTENSION_REPO_PATH) + displayName: "Generate a settings.json file" + + - bash : | + echo "Downloading the kind script" + curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.9.0/kind-linux-amd64 + chmod +x ./kind + ./kind create cluster + displayName: "Create and Start the Kind cluster" + + - task: AzureCLI@2 + displayName: Bootstrap + inputs: + azureSubscription: AzureResourceConnection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + .\Bootstrap.ps1 -CI + workingDirectory: $(K8S_EXTENSION_REPO_PATH) + + - task: AzureCLI@2 + displayName: Run the Test Suite Public Extensions Only + inputs: + azureSubscription: AzureResourceConnection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + .\Test.ps1 -CI -ExtensionType Public -OnlyPublicTests + workingDirectory: $(K8S_EXTENSION_REPO_PATH) + continueOnError: true + condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'False')) + + - task: AzureCLI@2 + displayName: Run the Test Suite on Private + Public Extensions + inputs: + azureSubscription: AzureResourceConnection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + .\Test.ps1 -CI -ExtensionType Public + workingDirectory: $(K8S_EXTENSION_REPO_PATH) + continueOnError: true + condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'True')) + + - task: PublishTestResults@2 + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '**/TestResults.xml' + failTaskOnFailedTests: true + condition: succeededOrFailed() + + - task: AzureCLI@2 + displayName: Cleanup + inputs: + azureSubscription: AzureResourceConnection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + .\Cleanup.ps1 -CI + workingDirectory: $(K8S_EXTENSION_REPO_PATH) + condition: succeededOrFailed() + + - job: BuildPublishExtension + pool: + vmImage: 'ubuntu-16.04' + displayName: "Build and Publish the Extension Artifact" + variables: + CLI_REPO_PATH: $(Agent.BuildDirectory)/s + steps: + - bash: | + echo "Using the private preview of k8s-extension to build..." + + cp $(CLI_REPO_PATH)/src/k8s-extension $(CLI_REPO_PATH)/src/k8s-extension-private -r + cp $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension/_consts_private.py $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension/_consts.py + + EXTENSION_NAME="k8s-extension-private" + EXTENSION_FILE_NAME="k8s_extension_private" + + echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" + echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" + condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'True')) + displayName: "Copy Files, Set Variables for k8s-extension-private" + - bash: | + echo "Using the public version of k8s-extension to build..." + + EXTENSION_NAME="k8s-extension" + EXTENSION_FILE_NAME="k8s_extension" + + echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" + echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" + condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'False')) + displayName: "Copy Files, Set Variables for k8s-extension" + - task: UsePythonVersion@0 + displayName: 'Use Python 3.6' + inputs: + versionSpec: 3.6 + - bash: | + set -ev + echo "Building extension ${EXTENSION_NAME}..." + + # prepare and activate virtualenv + pip install virtualenv + python3 -m venv env/ + source env/bin/activate + + # clone azure-cli + pip install azdev + + ls $(CLI_REPO_PATH) + + azdev --version + azdev setup -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) + azdev extension build $(EXTENSION_NAME) + workingDirectory: $(CLI_REPO_PATH) + displayName: "Setup and Build Extension with azdev" + - task: PublishBuildArtifacts@1 + inputs: + pathToPublish: $(CLI_REPO_PATH)/dist + +- stage: AzureCLIOfficial + displayName: "Azure Official CLI Code Checks" + dependsOn: [] + jobs: + - job: CheckLicenseHeader + displayName: "Check License" + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.6' + inputs: + versionSpec: 3.6 + - bash: | + set -ev + + # prepare and activate virtualenv + python -m venv env/ + + chmod +x ./env/bin/activate + source ./env/bin/activate + + # clone azure-cli + git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli + + pip install -q azdev + + azdev setup -c ../azure-cli -r ./ + + azdev --version + az --version + + azdev verify license + + - job: StaticAnalysis + displayName: "Static Analysis" + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.6' + inputs: + versionSpec: 3.6 + - bash: pip install wheel==0.30.0 pylint==1.9.5 flake8==3.5.0 requests + displayName: 'Install wheel, pylint, flake8, requests' + - bash: python scripts/ci/source_code_static_analysis.py + displayName: "Static Analysis" + + - job: IndexVerify + displayName: "Verify Extensions Index" + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.7' + inputs: + versionSpec: 3.7 + - bash: | + #!/usr/bin/env bash + set -ev + pip install wheel==0.30.0 requests packaging + export CI="ADO" + python ./scripts/ci/test_index.py -v + displayName: "Verify Extensions Index" + + - job: SourceTests + displayName: "Integration Tests, Build Tests" + pool: + vmImage: 'ubuntu-16.04' + strategy: + matrix: + Python36: + python.version: '3.6' + Python38: + python.version: '3.8' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python $(python.version)' + inputs: + versionSpec: '$(python.version)' + - bash: pip install wheel==0.30.0 + displayName: 'Install wheel==0.30.0' + - bash: ./scripts/ci/test_source.sh + displayName: 'Run integration test and build test' + env: + ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) + ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) + + - job: LintModifiedExtensions + displayName: "CLI Linter on Modified Extensions" + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.6' + inputs: + versionSpec: 3.6 + - bash: | + set -ev + + # prepare and activate virtualenv + pip install virtualenv + python -m virtualenv venv/ + source ./venv/bin/activate + + # clone azure-cli + git clone --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli + + pip install azdev + + azdev --version + + azdev setup -c ../azure-cli -r ./ -e k8s-extension + + # overwrite the default AZURE_EXTENSION_DIR set by ADO + AZURE_EXTENSION_DIR=~/.azure/cliextensions az --version + + AZURE_EXTENSION_DIR=~/.azure/cliextensions azdev linter --include-whl-extensions k8s-extension + displayName: "CLI Linter on Modified Extension" + env: + ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) + ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) + + - job: IndexRefDocVerify + displayName: "Verify Ref Docs" + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.7' + inputs: + versionSpec: 3.7 + - bash: pip install wheel==0.30.0 + displayName: 'Install wheel==0.30.0' + - task: Bash@3 + displayName: "Verify Extension Ref Docs" + inputs: + targetType: 'filePath' + filePath: scripts/ci/test_index_ref_doc.sh diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst new file mode 100644 index 00000000000..54c1e375f6f --- /dev/null +++ b/src/k8s-extension/HISTORY.rst @@ -0,0 +1,35 @@ +.. :changelog: + +Release History +=============== + +0.2.0 +++++++++++++++++++ + +* Refactor for clear separation of extension-type specific customizations +* OpenServiceMesh customization. +* Fix clusterType of Microsoft.ResourceConnector resource +* Update clusterType validation to allow 'appliances' +* Update identity creation to use the appropriate parent resource's type and api-version +* Throw error if cluster type is not one of the 3 supported types +* Rename azuremonitor-containers extension type to microsoft.azuremonitor.containers +* Move CLI errors to non-deprecated error types +* Remove support for update + +0.1.3 +++++++++++++++++++ + +* Customization for microsoft.openservicemesh + +0.1.2 +++++++++++++++++++ + +* Add support for Arc Appliance cluster type + +0.1.1 +++++++++++++++++++ +* Add support for microsoft-azure-defender extension type + +0.1.0 +++++++++++++++++++ +* Initial release. diff --git a/src/k8s-extension/README.rst b/src/k8s-extension/README.rst new file mode 100644 index 00000000000..e91e1b13229 --- /dev/null +++ b/src/k8s-extension/README.rst @@ -0,0 +1,5 @@ +Microsoft Azure CLI 'k8s-extension' Extension +============================================= + +This package is for the 'k8s-extension' extension. +i.e. 'az k8s-extension' diff --git a/src/k8s-extension/azext_k8s_extension/__init__.py b/src/k8s-extension/azext_k8s_extension/__init__.py new file mode 100644 index 00000000000..e2301227d45 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/__init__.py @@ -0,0 +1,32 @@ +# -------------------------------------------------------------------------------------------- +# 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 azext_k8s_extension._help import helps # pylint: disable=unused-import + + +class K8sExtensionCommandsLoader(AzCommandsLoader): + + def __init__(self, cli_ctx=None): + from azure.cli.core.commands import CliCommandType + from azext_k8s_extension._client_factory import cf_k8s_extension + k8s_extension_custom = CliCommandType( + operations_tmpl='azext_k8s_extension.custom#{}', + client_factory=cf_k8s_extension) + super(K8sExtensionCommandsLoader, self).__init__(cli_ctx=cli_ctx, + custom_command_type=k8s_extension_custom) + + def load_command_table(self, args): + from azext_k8s_extension.commands import load_command_table + load_command_table(self, args) + return self.command_table + + def load_arguments(self, command): + from azext_k8s_extension._params import load_arguments + load_arguments(self, command) + + +COMMAND_LOADER_CLS = K8sExtensionCommandsLoader diff --git a/src/k8s-extension/azext_k8s_extension/_client_factory.py b/src/k8s-extension/azext_k8s_extension/_client_factory.py new file mode 100644 index 00000000000..1a9a10c2615 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/_client_factory.py @@ -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.commands.client_factory import get_mgmt_service_client +from azure.cli.core.profiles import ResourceType + + +def cf_k8s_extension(cli_ctx, *_): + from azext_k8s_extension.vendored_sdks import SourceControlConfigurationClient + return get_mgmt_service_client(cli_ctx, SourceControlConfigurationClient) + + +def cf_k8s_extension_operation(cli_ctx, _): + return cf_k8s_extension(cli_ctx).extensions + + +def cf_resource_groups(cli_ctx, subscription_id=None): + return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, + subscription_id=subscription_id).resource_groups + + +def cf_resources(cli_ctx, subscription_id=None): + return get_mgmt_service_client(cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, + subscription_id=subscription_id).resources + + +def cf_log_analytics(cli_ctx, subscription_id=None): + from azure.mgmt.loganalytics import LogAnalyticsManagementClient # pylint: disable=no-name-in-module + return get_mgmt_service_client(cli_ctx, LogAnalyticsManagementClient, subscription_id=subscription_id) diff --git a/src/k8s-extension/azext_k8s_extension/_consts.py b/src/k8s-extension/azext_k8s_extension/_consts.py new file mode 100644 index 00000000000..d0fdaf7775f --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/_consts.py @@ -0,0 +1,8 @@ +# 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. +# -------------------------------------------------------------------------------------------- + +EXTENSION_NAME = 'k8s-extension' +VERSION = "0.2.0" diff --git a/src/k8s-extension/azext_k8s_extension/_consts_private.py b/src/k8s-extension/azext_k8s_extension/_consts_private.py new file mode 100644 index 00000000000..bc4c8a2b694 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/_consts_private.py @@ -0,0 +1,8 @@ +# 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. +# -------------------------------------------------------------------------------------------- + +EXTENSION_NAME = 'k8s-extension-private' +VERSION = "0.1PP.14" diff --git a/src/k8s-extension/azext_k8s_extension/_format.py b/src/k8s-extension/azext_k8s_extension/_format.py new file mode 100644 index 00000000000..ef96f7cf88f --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/_format.py @@ -0,0 +1,24 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from collections import OrderedDict + + +def k8s_extension_list_table_format(results): + return [__get_table_row(result) for result in results] + + +def k8s_extension_show_table_format(result): + return __get_table_row(result) + + +def __get_table_row(result): + return OrderedDict([ + ('name', result['name']), + ('extensionType', result['extensionType']), + ('version', result['version']), + ('installState', result['installState']), + ('lastModifiedTime', result['lastModifiedTime']) + ]) diff --git a/src/k8s-extension/azext_k8s_extension/_help.py b/src/k8s-extension/azext_k8s_extension/_help.py new file mode 100644 index 00000000000..64e4be612ea --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/_help.py @@ -0,0 +1,39 @@ +# 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 +import azext_k8s_extension._consts as consts + + +helps[f'{consts.EXTENSION_NAME}'] = """ + type: group + short-summary: Commands to manage K8s-extensions. +""" + +helps[f'{consts.EXTENSION_NAME} create'] = """ + type: command + short-summary: Create a K8s-extension. +""" + +helps[f'{consts.EXTENSION_NAME} list'] = """ + type: command + short-summary: List K8s-extensions. +""" + +helps[f'{consts.EXTENSION_NAME} delete'] = """ + type: command + short-summary: Delete a K8s-extension. +""" + +helps[f'{consts.EXTENSION_NAME} show'] = """ + type: command + short-summary: Show details of a K8s-extension. +""" + +helps[f'{consts.EXTENSION_NAME} update'] = """ + type: command + short-summary: Update a K8s-extension. +""" diff --git a/src/k8s-extension/azext_k8s_extension/_params.py b/src/k8s-extension/azext_k8s_extension/_params.py new file mode 100644 index 00000000000..d96fe3c2ba7 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/_params.py @@ -0,0 +1,74 @@ +# -------------------------------------------------------------------------------------------- +# 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.parameters import ( + get_enum_type, + get_three_state_flag, + tags_type +) +from azure.cli.core.commands.validators import get_default_location_from_resource_group +import azext_k8s_extension._consts as consts + +from azext_k8s_extension.action import ( + AddConfigurationSettings, + AddConfigurationProtectedSettings +) + + +def load_arguments(self, _): + with self.argument_context(consts.EXTENSION_NAME) as c: + c.argument('tags', tags_type) + c.argument('location', + validator=get_default_location_from_resource_group) + c.argument('name', + options_list=['--name', '-n'], + help='Name of the extension instance') + c.argument('extension_type', + help='Name of the extension type.') + c.argument('cluster_name', + options_list=['--cluster-name', '-c'], + help='Name of the Kubernetes cluster') + c.argument('cluster_type', + arg_type=get_enum_type(['connectedClusters', 'managedClusters', 'appliances']), + help='Specify Arc clusters or AKS managed clusters or Arc appliances.') + c.argument('scope', + arg_type=get_enum_type(['cluster', 'namespace']), + help='Specify the extension scope.') + c.argument('auto_upgrade_minor_version', + arg_group="Version", + options_list=['--auto-upgrade-minor-version', '--auto-upgrade'], + arg_type=get_three_state_flag(), + help='Automatically upgrade minor version of the extension instance.') + c.argument('version', + arg_group="Version", + help='Specify the version to install for the extension instance if' + ' --auto-upgrade-minor-version is not enabled.') + c.argument('configuration_settings', + arg_group="Configuration", + options_list=['--configuration-settings', '--config'], + action=AddConfigurationSettings, + nargs='+', + help='Configuration Settings as key=value pair. Repeat parameter for each setting') + c.argument('configuration_protected_settings', + arg_group="Configuration", + options_list=['--configuration-protected-settings', '--config-protected'], + action=AddConfigurationProtectedSettings, + nargs='+', + help='Configuration Protected Settings as key=value pair. Repeat parameter for each setting') + c.argument('configuration_settings_file', + arg_group="Configuration", + options_list=['--configuration-settings-file', '--config-file'], + help='JSON file path for configuration-settings') + c.argument('configuration_protected_settings_file', + arg_group="Configuration", + options_list=['--configuration-protected-settings-file', '--config-protected-file'], + help='JSON file path for configuration-protected-settings') + c.argument('release_namespace', + help='Specify the namespace to install the extension release.') + c.argument('release_train', + help='Specify the release train for the extension type.') + c.argument('target_namespace', + help='Specify the target namespace to install to for the extension instance. This' + ' parameter is required if extension scope is set to \'namespace\'') diff --git a/src/k8s-extension/azext_k8s_extension/action.py b/src/k8s-extension/azext_k8s_extension/action.py new file mode 100644 index 00000000000..4afbbbcd611 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/action.py @@ -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. +# -------------------------------------------------------------------------------------------- + +import argparse +from azure.cli.core.azclierror import ArgumentUsageError + + +# pylint: disable=protected-access, too-few-public-methods +class AddConfigurationSettings(argparse._AppendAction): + + def __call__(self, parser, namespace, values, option_string=None): + settings = {} + for item in values: + try: + key, value = item.split('=', 1) + settings[key] = value + except ValueError: + raise ArgumentUsageError('Usage error: {} configuration_setting_key=configuration_setting_value'. + format(option_string)) + super(AddConfigurationSettings, self).__call__(parser, namespace, settings, option_string) + + +# pylint: disable=protected-access, too-few-public-methods +class AddConfigurationProtectedSettings(argparse._AppendAction): + + def __call__(self, parser, namespace, values, option_string=None): + prot_settings = {} + for item in values: + try: + key, value = item.split('=', 1) + prot_settings[key] = value + except ValueError: + raise ArgumentUsageError('Usage error: {} configuration_protected_setting_key=' + 'configuration_protected_setting_value'.format(option_string)) + super(AddConfigurationProtectedSettings, self).__call__(parser, namespace, prot_settings, option_string) diff --git a/src/k8s-extension/azext_k8s_extension/azext_metadata.json b/src/k8s-extension/azext_k8s_extension/azext_metadata.json new file mode 100644 index 00000000000..cf7b8927a07 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/azext_metadata.json @@ -0,0 +1,4 @@ +{ + "azext.isPreview": true, + "azext.minCliCoreVersion": "2.15.0" +} diff --git a/src/k8s-extension/azext_k8s_extension/commands.py b/src/k8s-extension/azext_k8s_extension/commands.py new file mode 100644 index 00000000000..931662814c0 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/commands.py @@ -0,0 +1,26 @@ +# -------------------------------------------------------------------------------------------- +# 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 +from azure.cli.core.commands import CliCommandType +from azext_k8s_extension._client_factory import (cf_k8s_extension, cf_k8s_extension_operation) +from azext_k8s_extension._format import k8s_extension_list_table_format, k8s_extension_show_table_format +import azext_k8s_extension._consts as consts + + +def load_command_table(self, _): + + k8s_extension_sdk = CliCommandType( + operations_tmpl='azext_k8s_extension.vendored_sdks.operations#K8sExtensionsOperations.{}', + client_factory=cf_k8s_extension) + + with self.command_group(consts.EXTENSION_NAME, k8s_extension_sdk, client_factory=cf_k8s_extension_operation, + is_preview=True) \ + as g: + g.custom_command('create', 'create_k8s_extension') + g.custom_command('update', 'update_k8s_extension') + g.custom_command('delete', 'delete_k8s_extension', confirmation=True) + g.custom_command('list', 'list_k8s_extension', table_transformer=k8s_extension_list_table_format) + g.custom_show_command('show', 'show_k8s_extension', table_transformer=k8s_extension_show_table_format) diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py new file mode 100644 index 00000000000..ba9dbfce501 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -0,0 +1,279 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=unused-argument,too-many-locals + +import json +from knack.log import get_logger + +from msrestazure.azure_exceptions import CloudError + +from azure.cli.core.azclierror import ResourceNotFoundError, MutuallyExclusiveArgumentError, \ + InvalidArgumentValueError, CommandNotFoundError, RequiredArgumentMissingError +from azure.cli.core.commands.client_factory import get_subscription_id +from azext_k8s_extension.vendored_sdks.models import ConfigurationIdentity +from azext_k8s_extension.vendored_sdks.models import ErrorResponseException +from azext_k8s_extension.vendored_sdks.models import Scope + +from azext_k8s_extension.partner_extensions.ContainerInsights import ContainerInsights +from azext_k8s_extension.partner_extensions.AzureDefender import AzureDefender +from azext_k8s_extension.partner_extensions.Cassandra import Cassandra +from azext_k8s_extension.partner_extensions.DefaultExtension import DefaultExtension +import azext_k8s_extension._consts as consts + +from ._client_factory import cf_resources + +logger = get_logger(__name__) + + +# A factory method to return the correct extension class based off of the extension name +def ExtensionFactory(extension_name): + extension_map = { + 'microsoft.azuremonitor.containers': ContainerInsights, + 'microsoft.azuredefender.kubernetes': AzureDefender, + 'cassandradatacentersoperator': Cassandra + } + + # Return the extension if we find it in the map, else return the default + return extension_map.get(extension_name, DefaultExtension)() + + +def show_k8s_extension(client, resource_group_name, cluster_name, name, cluster_type): + """Get an existing K8s Extension. + + """ + # Determine ClusterRP + cluster_rp = __get_cluster_rp(cluster_type) + + try: + extension = client.get(resource_group_name, + cluster_rp, cluster_type, cluster_name, name) + return extension + except ErrorResponseException as ex: + # Customize the error message for resources not found + if ex.response.status_code == 404: + # If Cluster not found + if ex.message.__contains__("(ResourceNotFound)"): + message = "{0} Verify that the cluster-type is correct and the resource exists.".format( + ex.message) + # If Configuration not found + elif ex.message.__contains__("Operation returned an invalid status code 'Not Found'"): + message = "(ExtensionNotFound) The Resource {0}/{1}/{2}/Microsoft.KubernetesConfiguration/" \ + "extensions/{3} could not be found!".format( + cluster_rp, cluster_type, cluster_name, name) + else: + message = ex.message + raise ResourceNotFoundError(message) + + +def create_k8s_extension(cmd, client, resource_group_name, cluster_name, name, cluster_type, + extension_type, scope=None, auto_upgrade_minor_version=None, release_train=None, + version=None, target_namespace=None, release_namespace=None, configuration_settings=None, + configuration_protected_settings=None, configuration_settings_file=None, + configuration_protected_settings_file=None, tags=None): + """Create a new Extension Instance. + + """ + extension_type_lower = extension_type.lower() + + # Determine ClusterRP + cluster_rp = __get_cluster_rp(cluster_type) + + # Configuration Settings & Configuration Protected Settings + if configuration_settings is not None and configuration_settings_file is not None: + raise MutuallyExclusiveArgumentError( + 'Error! Both configuration-settings and configuration-settings-file cannot be provided.' + ) + + if configuration_protected_settings is not None and configuration_protected_settings_file is not None: + raise MutuallyExclusiveArgumentError( + 'Error! Both configuration-protected-settings and configuration-protected-settings-file ' + 'cannot be provided.' + ) + + config_settings = {} + config_protected_settings = {} + # Get Configuration Settings from file + if configuration_settings_file is not None: + config_settings = __get_config_settings_from_file(configuration_settings_file) + + if configuration_settings is not None: + for dicts in configuration_settings: + for key, value in dicts.items(): + config_settings[key] = value + + # Get Configuration Protected Settings from file + if configuration_protected_settings_file is not None: + config_protected_settings = __get_config_settings_from_file(configuration_protected_settings_file) + + if configuration_protected_settings is not None: + for dicts in configuration_protected_settings: + for key, value in dicts.items(): + config_protected_settings[key] = value + + # Identity is not created by default. Extension type must specify if identity is required. + create_identity = False + extension_instance = None + + # Scope & Namespace validation - common to all extension-types + __validate_scope_and_namespace(scope, release_namespace, target_namespace) + + # Give Partners a chance to their extensionType specific validations and to set value over-rides. + + # Get the extension class based on the extension name + extension_class = ExtensionFactory(extension_type_lower) + extension_instance, name, create_identity = extension_class.Create( + cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type_lower, scope, + auto_upgrade_minor_version, release_train, version, target_namespace, release_namespace, config_settings, + config_protected_settings, configuration_settings_file, configuration_protected_settings_file) + + # Common validations + __validate_version_and_auto_upgrade(extension_instance.version, extension_instance.auto_upgrade_minor_version) + __validate_scope_after_customization(extension_instance.scope) + + # Create identity, if required + if create_identity: + extension_instance.identity, extension_instance.location = \ + __create_identity(cmd, resource_group_name, cluster_name, cluster_type, cluster_rp) + + # Try to create the resource + return client.create(resource_group_name, cluster_rp, cluster_type, cluster_name, name, extension_instance) + + +def list_k8s_extension(client, resource_group_name, cluster_name, cluster_type): + cluster_rp = __get_cluster_rp(cluster_type) + return client.list(resource_group_name, cluster_rp, cluster_type, cluster_name) + + +def update_k8s_extension(client, resource_group_name, cluster_type, cluster_name, name, + auto_upgrade_minor_version='', release_train='', version='', tags=None): + + """Patch an existing Extension Instance. + + """ + + # TODO: Remove this after we eventually get PATCH implemented for update and uncomment + raise CommandNotFoundError( + f"\"{consts.EXTENSION_NAME} update\" currently is not available. " + f"Use \"{consts.EXTENSION_NAME} create\" to update a previously created extension instance." + ) + + # # Ensure some values are provided for update + # if auto_upgrade_minor_version is None and release_train is None and version is None: + # message = "Error! No values provided for update. Provide new value(s) for one or more of these properties:" \ + # " auto-upgrade-minor-version, release-train or version." + # raise RequiredArgumentMissingError(message) + + # # Determine ClusterRP + # cluster_rp = __get_cluster_rp(cluster_type) + + # # Get the existing extensionInstance + # extension = client.get(resource_group_name, cluster_rp, cluster_type, cluster_name, name) + + # extension_type_lower = extension.extension_type.lower() + + # # Get the extension class based on the extension name + # extension_class = ExtensionFactory(extension_type_lower) + # upd_extension = extension_class.Update(extension, auto_upgrade_minor_version, release_train, version) + + # __validate_version_and_auto_upgrade(version, auto_upgrade_minor_version) + + # upd_extension = ExtensionInstanceUpdate(auto_upgrade_minor_version=auto_upgrade_minor_version, + # release_train=release_train, version=version) + + # return client.update(resource_group_name, cluster_rp, cluster_type, cluster_name, name, upd_extension) + + +def delete_k8s_extension(client, resource_group_name, cluster_name, name, cluster_type): + """Delete an existing Kubernetes Extension. + + """ + # Determine ClusterRP + cluster_rp = __get_cluster_rp(cluster_type) + return client.delete(resource_group_name, cluster_rp, cluster_type, cluster_name, name) + + +def __create_identity(cmd, resource_group_name, cluster_name, cluster_type, cluster_rp): + subscription_id = get_subscription_id(cmd.cli_ctx) + resources = cf_resources(cmd.cli_ctx, subscription_id) + + cluster_resource_id = '/subscriptions/{0}/resourceGroups/{1}/providers/{2}/{3}/{4}'.format(subscription_id, + resource_group_name, + cluster_rp, + cluster_type, + cluster_name) + + if cluster_rp == 'Microsoft.Kubernetes': + parent_api_version = '2020-01-01-preview' + elif cluster_rp == 'Microsoft.ResourceConnector': + parent_api_version = '2020-09-15-privatepreview' + elif cluster_rp == 'Microsoft.ContainerService': + parent_api_version = '2017-07-01' + else: + raise InvalidArgumentValueError( + "Error! Cluster type '{}' is not supported for extension identity".format(cluster_type) + ) + + try: + resource = resources.get_by_id(cluster_resource_id, parent_api_version) + location = str(resource.location.lower()) + except CloudError as ex: + raise ex + identity_type = "SystemAssigned" + + return ConfigurationIdentity(type=identity_type), location + + +def __get_cluster_rp(cluster_type): + rp = "" + if cluster_type.lower() == 'connectedclusters': + rp = 'Microsoft.Kubernetes' + elif cluster_type.lower() == 'appliances': + rp = 'Microsoft.ResourceConnector' + elif cluster_type.lower() == '': + rp = 'Microsoft.ContainerService' + else: + raise InvalidArgumentValueError("Error! Cluster type '{}' is not supported".format(cluster_type)) + return rp + + +def __validate_scope_and_namespace(scope, release_namespace, target_namespace): + if scope == 'cluster': + if target_namespace is not None: + message = "When --scope is 'cluster', --target-namespace must not be given." + raise MutuallyExclusiveArgumentError(message) + else: + if release_namespace is not None: + message = "When --scope is 'namespace', --release-namespace must not be given." + raise MutuallyExclusiveArgumentError(message) + + +def __validate_scope_after_customization(scope_obj: Scope): + if scope_obj is not None and scope_obj.namespace is not None and scope_obj.namespace.target_namespace is None: + message = "When --scope is 'namespace', --target-namespace must be given." + raise RequiredArgumentMissingError(message) + + +def __validate_version_and_auto_upgrade(version, auto_upgrade_minor_version): + if version is not None: + if auto_upgrade_minor_version: + message = "To pin to specific version, auto-upgrade-minor-version must be set to 'false'." + raise MutuallyExclusiveArgumentError(message) + + auto_upgrade_minor_version = False + + +def __get_config_settings_from_file(file_path): + try: + config_file = open(file_path,) + settings = json.load(config_file) + except ValueError: + raise Exception("File {} is not a valid JSON file".format(file_path)) + + files = len(settings) + if files == 0: + raise Exception("File {} is empty".format(file_path)) + + return settings diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py new file mode 100644 index 00000000000..dcd2853affc --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py @@ -0,0 +1,71 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=unused-argument + +from knack.log import get_logger + +from azext_k8s_extension.vendored_sdks.models import ExtensionInstance +from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate +from azext_k8s_extension.vendored_sdks.models import ScopeCluster +from azext_k8s_extension.vendored_sdks.models import Scope + +from azext_k8s_extension.partner_extensions.PartnerExtensionModel import PartnerExtensionModel +from azext_k8s_extension.partner_extensions.ContainerInsights import _get_container_insights_settings + +logger = get_logger(__name__) + + +class AzureDefender(PartnerExtensionModel): + def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, + scope, auto_upgrade_minor_version, release_train, version, target_namespace, + release_namespace, configuration_settings, configuration_protected_settings, + configuration_settings_file, configuration_protected_settings_file): + + """ExtensionType 'microsoft.azuredefender.kubernetes' specific validations & defaults for Create + Must create and return a valid 'ExtensionInstance' object. + + """ + # NOTE-1: Replace default scope creation with your customization! + ext_scope = None + # Hardcoding name, release_namespace and scope since ci only supports one instance and cluster scope + # and platform doesnt have support yet extension specific constraints like this + name = extension_type.lower() + release_namespace = "azuredefender" + # Scope is always cluster + scope_cluster = ScopeCluster(release_namespace=release_namespace) + ext_scope = Scope(cluster=scope_cluster, namespace=None) + + is_ci_extension_type = False + + logger.warning('Ignoring name, release-namespace and scope parameters since %s ' + 'only supports cluster scope and single instance of this extension', extension_type) + + _get_container_insights_settings(cmd, resource_group_name, cluster_name, configuration_settings, + configuration_protected_settings, is_ci_extension_type) + + # NOTE-2: Return a valid ExtensionInstance object, Instance name and flag for Identity + create_identity = True + extension_instance = ExtensionInstance( + extension_type=extension_type, + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version, + scope=ext_scope, + configuration_settings=configuration_settings, + configuration_protected_settings=configuration_protected_settings + ) + return extension_instance, name, create_identity + + def Update(self, extension, auto_upgrade_minor_version, release_train, version): + """ExtensionType 'microsoft.azuredefender.kubernetes' specific validations & defaults for Update + Must create and return a valid 'ExtensionInstanceUpdate' object. + + """ + return ExtensionInstanceUpdate( + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version + ) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/Cassandra.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/Cassandra.py new file mode 100644 index 00000000000..2a609ce125a --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/Cassandra.py @@ -0,0 +1,57 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=unused-argument + +from azext_k8s_extension.vendored_sdks.models import ExtensionInstance +from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate +from azext_k8s_extension.vendored_sdks.models import ScopeCluster +from azext_k8s_extension.vendored_sdks.models import ScopeNamespace +from azext_k8s_extension.vendored_sdks.models import Scope + +from azext_k8s_extension.partner_extensions.PartnerExtensionModel import PartnerExtensionModel + + +class Cassandra(PartnerExtensionModel): + def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, + scope, auto_upgrade_minor_version, release_train, version, target_namespace, + release_namespace, configuration_settings, configuration_protected_settings, + configuration_settings_file, configuration_protected_settings_file): + + """Default validations & defaults for Create + Must create and return a valid 'ExtensionInstance' object. + + """ + ext_scope = None + if scope is not None: + if scope.lower() == 'cluster': + scope_cluster = ScopeCluster(release_namespace=release_namespace) + ext_scope = Scope(cluster=scope_cluster, namespace=None) + elif scope.lower() == 'namespace': + scope_namespace = ScopeNamespace(target_namespace=target_namespace) + ext_scope = Scope(namespace=scope_namespace, cluster=None) + + create_identity = True + extension_instance = ExtensionInstance( + extension_type=extension_type, + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version, + scope=ext_scope, + configuration_settings=configuration_settings, + configuration_protected_settings=configuration_protected_settings, + ) + return extension_instance, name, create_identity + + def Update(self, extension, auto_upgrade_minor_version, release_train, version): + """Default validations & defaults for Update + Must create and return a valid 'ExtensionInstanceUpdate' object. + + """ + return ExtensionInstanceUpdate( + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version + ) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py new file mode 100644 index 00000000000..8c678b34915 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py @@ -0,0 +1,458 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=unused-argument + +import datetime +import json + +from knack.log import get_logger + +from azure.cli.core.azclierror import InvalidArgumentValueError +from azure.cli.core.commands import LongRunningOperation +from azure.cli.core.commands.client_factory import get_mgmt_service_client, get_subscription_id +from azure.cli.core.util import sdk_no_wait +from msrestazure.azure_exceptions import CloudError +from msrestazure.tools import parse_resource_id, is_valid_resource_id + +from azext_k8s_extension.vendored_sdks.models import ExtensionInstance +from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate +from azext_k8s_extension.vendored_sdks.models import ScopeCluster +from azext_k8s_extension.vendored_sdks.models import Scope + +from azext_k8s_extension.partner_extensions.PartnerExtensionModel import PartnerExtensionModel + +from azext_k8s_extension._client_factory import ( + cf_resources, cf_resource_groups, cf_log_analytics) + +logger = get_logger(__name__) + + +class ContainerInsights(PartnerExtensionModel): + def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, + scope, auto_upgrade_minor_version, release_train, version, target_namespace, + release_namespace, configuration_settings, configuration_protected_settings, + configuration_settings_file, configuration_protected_settings_file): + + """ExtensionType 'microsoft.azuremonitor.containers' specific validations & defaults for Create + Must create and return a valid 'ExtensionInstance' object. + + """ + # NOTE-1: Replace default scope creation with your customization! + ext_scope = None + # Hardcoding name, release_namespace and scope since container-insights only supports one instance and cluster + # scope and platform doesnt have support yet extension specific constraints like this + name = 'azuremonitor-containers' + release_namespace = 'azuremonitor-containers' + # Scope is always cluster + scope_cluster = ScopeCluster(release_namespace=release_namespace) + ext_scope = Scope(cluster=scope_cluster, namespace=None) + + is_ci_extension_type = True + + logger.warning('Ignoring name, release-namespace and scope parameters since %s ' + 'only supports cluster scope and single instance of this extension', extension_type) + + _get_container_insights_settings(cmd, resource_group_name, cluster_name, configuration_settings, + configuration_protected_settings, is_ci_extension_type) + + # NOTE-2: Return a valid ExtensionInstance object, Instance name and flag for Identity + create_identity = True + extension_instance = ExtensionInstance( + extension_type=extension_type, + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version, + scope=ext_scope, + configuration_settings=configuration_settings, + configuration_protected_settings=configuration_protected_settings + ) + return extension_instance, name, create_identity + + def Update(self, extension, auto_upgrade_minor_version, release_train, version): + """ExtensionType 'microsoft.azuremonitor.containers' specific validations & defaults for Update + Must create and return a valid 'ExtensionInstanceUpdate' object. + + """ + return ExtensionInstanceUpdate( + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version + ) + + +# Custom Validation Logic for Container Insights + +def _invoke_deployment(cmd, resource_group_name, deployment_name, template, parameters, validate, no_wait, + subscription_id=None): + from azure.cli.core.profiles import ResourceType + deployment_properties = cmd.get_models('DeploymentProperties', resource_type=ResourceType.MGMT_RESOURCE_RESOURCES) + properties = deployment_properties(template=template, parameters=parameters, mode='incremental') + smc = get_mgmt_service_client(cmd.cli_ctx, ResourceType.MGMT_RESOURCE_RESOURCES, + subscription_id=subscription_id).deployments + if validate: + logger.info('==== BEGIN TEMPLATE ====') + logger.info(json.dumps(template, indent=2)) + logger.info('==== END TEMPLATE ====') + + if cmd.supported_api_version(min_api='2019-10-01', resource_type=ResourceType.MGMT_RESOURCE_RESOURCES): + deployment_temp = cmd.get_models('Deployment', resource_type=ResourceType.MGMT_RESOURCE_RESOURCES) + deployment = deployment_temp(properties=properties) + + if validate: + validation_poller = smc.validate(resource_group_name, deployment_name, deployment) + return LongRunningOperation(cmd.cli_ctx)(validation_poller) + return sdk_no_wait(no_wait, smc.create_or_update, resource_group_name, deployment_name, deployment) + + if validate: + return smc.validate(resource_group_name, deployment_name, properties) + return sdk_no_wait(no_wait, smc.create_or_update, resource_group_name, deployment_name, properties) + + +def _ensure_default_log_analytics_workspace_for_monitoring(cmd, subscription_id, + cluster_resource_group_name, cluster_name): + # mapping for azure public cloud + # log analytics workspaces cannot be created in WCUS region due to capacity limits + # so mapped to EUS per discussion with log analytics team + # pylint: disable=too-many-locals,too-many-statements + + azurecloud_location_to_oms_region_code_map = { + "australiasoutheast": "ASE", + "australiaeast": "EAU", + "australiacentral": "CAU", + "canadacentral": "CCA", + "centralindia": "CIN", + "centralus": "CUS", + "eastasia": "EA", + "eastus": "EUS", + "eastus2": "EUS2", + "eastus2euap": "EAP", + "francecentral": "PAR", + "japaneast": "EJP", + "koreacentral": "SE", + "northeurope": "NEU", + "southcentralus": "SCUS", + "southeastasia": "SEA", + "uksouth": "SUK", + "usgovvirginia": "USGV", + "westcentralus": "EUS", + "westeurope": "WEU", + "westus": "WUS", + "westus2": "WUS2" + } + azurecloud_region_to_oms_region_map = { + "australiacentral": "australiacentral", + "australiacentral2": "australiacentral", + "australiaeast": "australiaeast", + "australiasoutheast": "australiasoutheast", + "brazilsouth": "southcentralus", + "canadacentral": "canadacentral", + "canadaeast": "canadacentral", + "centralus": "centralus", + "centralindia": "centralindia", + "eastasia": "eastasia", + "eastus": "eastus", + "eastus2": "eastus2", + "francecentral": "francecentral", + "francesouth": "francecentral", + "japaneast": "japaneast", + "japanwest": "japaneast", + "koreacentral": "koreacentral", + "koreasouth": "koreacentral", + "northcentralus": "eastus", + "northeurope": "northeurope", + "southafricanorth": "westeurope", + "southafricawest": "westeurope", + "southcentralus": "southcentralus", + "southeastasia": "southeastasia", + "southindia": "centralindia", + "uksouth": "uksouth", + "ukwest": "uksouth", + "westcentralus": "eastus", + "westeurope": "westeurope", + "westindia": "centralindia", + "westus": "westus", + "westus2": "westus2" + } + + # mapping for azure china cloud + # currently log analytics supported only China East 2 region + azurechina_location_to_oms_region_code_map = { + "chinaeast": "EAST2", + "chinaeast2": "EAST2", + "chinanorth": "EAST2", + "chinanorth2": "EAST2" + } + azurechina_region_to_oms_region_map = { + "chinaeast": "chinaeast2", + "chinaeast2": "chinaeast2", + "chinanorth": "chinaeast2", + "chinanorth2": "chinaeast2" + } + + # mapping for azure us governmner cloud + azurefairfax_location_to_oms_region_code_map = { + "usgovvirginia": "USGV" + } + azurefairfax_region_to_oms_region_map = { + "usgovvirginia": "usgovvirginia" + } + + cluster_location = '' + resources = cf_resources(cmd.cli_ctx, subscription_id) + + cluster_resource_id = '/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Kubernetes' \ + '/connectedClusters/{2}'.format(subscription_id, cluster_resource_group_name, cluster_name) + try: + resource = resources.get_by_id(cluster_resource_id, '2020-01-01-preview') + cluster_location = resource.location.lower() + except CloudError as ex: + raise ex + + cloud_name = cmd.cli_ctx.cloud.name.lower() + workspace_region = "eastus" + workspace_region_code = "EUS" + + # sanity check that locations and clouds match. + if ((cloud_name == 'azurecloud' and azurechina_region_to_oms_region_map.get(cluster_location, False)) or + (cloud_name == 'azurecloud' and azurefairfax_region_to_oms_region_map.get(cluster_location, False))): + raise InvalidArgumentValueError( + 'Wrong cloud (azurecloud) setting for region {}, please use "az cloud set ..."' + .format(cluster_location) + ) + + if ((cloud_name == 'azurechinacloud' and azurecloud_region_to_oms_region_map.get(cluster_location, False)) or + (cloud_name == 'azurechinacloud' and azurefairfax_region_to_oms_region_map.get(cluster_location, False))): + raise InvalidArgumentValueError( + 'Wrong cloud (azurechinacloud) setting for region {}, please use "az cloud set ..."' + .format(cluster_location) + ) + + if ((cloud_name == 'azureusgovernment' and azurecloud_region_to_oms_region_map.get(cluster_location, False)) or + (cloud_name == 'azureusgovernment' and azurechina_region_to_oms_region_map.get(cluster_location, False))): + raise InvalidArgumentValueError( + 'Wrong cloud (azureusgovernment) setting for region {}, please use "az cloud set ..."' + .format(cluster_location) + ) + + if cloud_name == 'azurecloud': + workspace_region = azurecloud_region_to_oms_region_map.get(cluster_location, "eastus") + workspace_region_code = azurecloud_location_to_oms_region_code_map.get(workspace_region, "EUS") + elif cloud_name == 'azurechinacloud': + workspace_region = azurechina_region_to_oms_region_map.get(cluster_location, "chinaeast2") + workspace_region_code = azurechina_location_to_oms_region_code_map.get(workspace_region, "EAST2") + elif cloud_name == 'azureusgovernment': + workspace_region = azurefairfax_region_to_oms_region_map.get(cluster_location, "usgovvirginia") + workspace_region_code = azurefairfax_location_to_oms_region_code_map.get(workspace_region, "USGV") + else: + logger.error("AKS Monitoring addon not supported in cloud : %s", cloud_name) + + default_workspace_resource_group = 'DefaultResourceGroup-' + workspace_region_code + default_workspace_name = 'DefaultWorkspace-{0}-{1}'.format(subscription_id, workspace_region_code) + default_workspace_resource_id = '/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.OperationalInsights' \ + '/workspaces/{2}'.format(subscription_id, default_workspace_resource_group, default_workspace_name) + resource_groups = cf_resource_groups(cmd.cli_ctx, subscription_id) + + # check if default RG exists + if resource_groups.check_existence(default_workspace_resource_group): + try: + resource = resources.get_by_id(default_workspace_resource_id, '2015-11-01-preview') + return resource.id + except CloudError as ex: + if ex.status_code != 404: + raise ex + else: + resource_groups.create_or_update(default_workspace_resource_group, { + 'location': workspace_region}) + + default_workspace_params = { + 'location': workspace_region, + 'properties': { + 'sku': { + 'name': 'standalone' + } + } + } + async_poller = resources.create_or_update_by_id(default_workspace_resource_id, '2015-11-01-preview', + default_workspace_params) + + ws_resource_id = '' + while True: + result = async_poller.result(15) + if async_poller.done(): + ws_resource_id = result.id + break + + return ws_resource_id + + +def _ensure_container_insights_for_monitoring(cmd, workspace_resource_id): + # extract subscription ID and resource group from workspace_resource_id URL + parsed = parse_resource_id(workspace_resource_id) + subscription_id, resource_group = parsed["subscription"], parsed["resource_group"] + + resources = cf_resources(cmd.cli_ctx, subscription_id) + try: + resource = resources.get_by_id(workspace_resource_id, '2015-11-01-preview') + location = resource.location + except CloudError as ex: + raise ex + + unix_time_in_millis = int( + (datetime.datetime.utcnow() - datetime.datetime.utcfromtimestamp(0)).total_seconds() * 1000.0) + + solution_deployment_name = 'ContainerInsights-{}'.format(unix_time_in_millis) + + # pylint: disable=line-too-long + template = { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "workspaceResourceId": { + "type": "string", + "metadata": { + "description": "Azure Monitor Log Analytics Resource ID" + } + }, + "workspaceRegion": { + "type": "string", + "metadata": { + "description": "Azure Monitor Log Analytics workspace region" + } + }, + "solutionDeploymentName": { + "type": "string", + "metadata": { + "description": "Name of the solution deployment" + } + } + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "name": "[parameters('solutionDeploymentName')]", + "apiVersion": "2017-05-10", + "subscriptionId": "[split(parameters('workspaceResourceId'),'/')[2]]", + "resourceGroup": "[split(parameters('workspaceResourceId'),'/')[4]]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "apiVersion": "2015-11-01-preview", + "type": "Microsoft.OperationsManagement/solutions", + "location": "[parameters('workspaceRegion')]", + "name": "[Concat('ContainerInsights', '(', split(parameters('workspaceResourceId'),'/')" + "[8], ')')]", + "properties": { + "workspaceResourceId": "[parameters('workspaceResourceId')]" + }, + "plan": { + "name": "[Concat('ContainerInsights', '(', split(parameters('workspaceResourceId')," + "'/')[8], ')')]", + "product": "[Concat('OMSGallery/', 'ContainerInsights')]", + "promotionCode": "", + "publisher": "Microsoft" + } + } + ] + }, + "parameters": {} + } + } + ] + } + + params = { + "workspaceResourceId": { + "value": workspace_resource_id + }, + "workspaceRegion": { + "value": location + }, + "solutionDeploymentName": { + "value": solution_deployment_name + } + } + + deployment_name = 'arc-k8s-monitoring-{}'.format(unix_time_in_millis) + # publish the Container Insights solution to the Log Analytics workspace + return _invoke_deployment(cmd, resource_group, deployment_name, template, params, + validate=False, no_wait=False, subscription_id=subscription_id) + + +def _get_container_insights_settings(cmd, cluster_resource_group_name, cluster_name, configuration_settings, + configuration_protected_settings, is_ci_extension_type): + + subscription_id = get_subscription_id(cmd.cli_ctx) + workspace_resource_id = '' + + if configuration_settings is not None: + if 'loganalyticsworkspaceresourceid' in configuration_settings: + configuration_settings['logAnalyticsWorkspaceResourceID'] = \ + configuration_settings.pop('loganalyticsworkspaceresourceid') + + if 'logAnalyticsWorkspaceResourceID' in configuration_settings: + workspace_resource_id = configuration_settings['logAnalyticsWorkspaceResourceID'] + + workspace_resource_id = workspace_resource_id.strip() + + if configuration_protected_settings is not None: + if 'proxyEndpoint' in configuration_protected_settings: + # current supported format for proxy endpoint is http(s)://:@: + # do some basic validation since the ci agent does the complete validation + proxy = configuration_protected_settings['proxyEndpoint'].strip().lower() + proxy_parts = proxy.split('://') + if (not proxy) or (not proxy.startswith('http://') and not proxy.startswith('https://')) or \ + (len(proxy_parts) != 2): + raise InvalidArgumentValueError( + 'proxyEndpoint url should in this format http(s)://:@:' + ) + logger.info("successfully validated proxyEndpoint url hence passing proxy endpoint to extension") + configuration_protected_settings['omsagent.proxy'] = configuration_protected_settings['proxyEndpoint'] + + if not workspace_resource_id: + workspace_resource_id = _ensure_default_log_analytics_workspace_for_monitoring( + cmd, subscription_id, cluster_resource_group_name, cluster_name) + else: + if not is_valid_resource_id(workspace_resource_id): + raise InvalidArgumentValueError('{} is not a valid Azure resource ID.'.format(workspace_resource_id)) + + if is_ci_extension_type: + _ensure_container_insights_for_monitoring(cmd, workspace_resource_id).result() + + # extract subscription ID and resource group from workspace_resource_id URL + parsed = parse_resource_id(workspace_resource_id) + workspace_sub_id, workspace_rg_name, workspace_name = \ + parsed["subscription"], parsed["resource_group"], parsed["name"] + + log_analytics_client = cf_log_analytics(cmd.cli_ctx, workspace_sub_id) + log_analytics_workspace = log_analytics_client.workspaces.get(workspace_rg_name, workspace_name) + if not log_analytics_workspace: + raise InvalidArgumentValueError( + 'Fails to retrieve workspace by {}'.format(workspace_name)) + + shared_keys = log_analytics_client.shared_keys.get_shared_keys( + workspace_rg_name, workspace_name) + if not shared_keys: + raise InvalidArgumentValueError('Fails to retrieve shared key for workspace {}'.format( + log_analytics_workspace)) + configuration_protected_settings['omsagent.secret.wsid'] = log_analytics_workspace.customer_id + configuration_settings['logAnalyticsWorkspaceResourceID'] = workspace_resource_id + configuration_protected_settings['omsagent.secret.key'] = shared_keys.primary_shared_key + # set the domain for the ci agent for non azure public clouds + cloud_name = cmd.cli_ctx.cloud.name + if cloud_name.lower() == 'azurechinacloud': + configuration_settings['omsagent.domain'] = 'opinsights.azure.cn' + elif cloud_name.lower() == 'azureusgovernment': + configuration_settings['omsagent.domain'] = 'opinsights.azure.us' + elif cloud_name.lower() == 'usnat': + configuration_settings['omsagent.domain'] = 'opinsights.azure.eaglex.ic.gov' + elif cloud_name.lower() == 'ussec': + configuration_settings['omsagent.domain'] = 'opinsights.azure.microsoft.scloud' diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py new file mode 100644 index 00000000000..9a69199f838 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py @@ -0,0 +1,57 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=unused-argument + +from azext_k8s_extension.vendored_sdks.models import ExtensionInstance +from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate +from azext_k8s_extension.vendored_sdks.models import ScopeCluster +from azext_k8s_extension.vendored_sdks.models import ScopeNamespace +from azext_k8s_extension.vendored_sdks.models import Scope + +from azext_k8s_extension.partner_extensions.PartnerExtensionModel import PartnerExtensionModel + + +class DefaultExtension(PartnerExtensionModel): + def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, + scope, auto_upgrade_minor_version, release_train, version, target_namespace, + release_namespace, configuration_settings, configuration_protected_settings, + configuration_settings_file, configuration_protected_settings_file): + + """Default validations & defaults for Create + Must create and return a valid 'ExtensionInstance' object. + + """ + ext_scope = None + if scope is not None: + if scope.lower() == 'cluster': + scope_cluster = ScopeCluster(release_namespace=release_namespace) + ext_scope = Scope(cluster=scope_cluster, namespace=None) + elif scope.lower() == 'namespace': + scope_namespace = ScopeNamespace(target_namespace=target_namespace) + ext_scope = Scope(namespace=scope_namespace, cluster=None) + + create_identity = False + extension_instance = ExtensionInstance( + extension_type=extension_type, + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version, + scope=ext_scope, + configuration_settings=configuration_settings, + configuration_protected_settings=configuration_protected_settings + ) + return extension_instance, name, create_identity + + def Update(self, extension, auto_upgrade_minor_version, release_train, version): + """Default validations & defaults for Update + Must create and return a valid 'ExtensionInstanceUpdate' object. + + """ + return ExtensionInstanceUpdate( + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version + ) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py new file mode 100644 index 00000000000..96c489644e7 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py @@ -0,0 +1,23 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from abc import ABC, abstractmethod +from azext_k8s_extension.vendored_sdks.models import ExtensionInstance +from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate + + +class PartnerExtensionModel(ABC): + @abstractmethod + def Create(self, cmd, client, resource_group_name: str, cluster_name: str, name: str, cluster_type: str, + extension_type: str, scope: str, auto_upgrade_minor_version: bool, release_train: str, version: str, + target_namespace: str, release_namespace: str, configuration_settings: dict, + configuration_protected_settings: dict, configuration_settings_file: str, + configuration_protected_settings_file: str) -> ExtensionInstance: + pass + + @abstractmethod + def Update(self, extension: ExtensionInstance, auto_upgrade_minor_version: bool, + release_train: str, version: str) -> ExtensionInstanceUpdate: + pass diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/__init__.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/__init__.py new file mode 100644 index 00000000000..9ccaff6c1b8 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/__init__.py @@ -0,0 +1,5 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- diff --git a/src/k8s-extension/azext_k8s_extension/tests/__init__.py b/src/k8s-extension/azext_k8s_extension/tests/__init__.py new file mode 100644 index 00000000000..99c0f28cd71 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/tests/__init__.py @@ -0,0 +1,5 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/__init__.py b/src/k8s-extension/azext_k8s_extension/tests/latest/__init__.py new file mode 100644 index 00000000000..99c0f28cd71 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/__init__.py @@ -0,0 +1,5 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ----------------------------------------------------------------------------- diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/recordings/test_k8s_extension.yaml b/src/k8s-extension/azext_k8s_extension/tests/latest/recordings/test_k8s_extension.yaml new file mode 100644 index 00000000000..127b21ac873 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/recordings/test_k8s_extension.yaml @@ -0,0 +1,270 @@ +interactions: +- request: + body: '{"properties": {"extensionType": "microsoft.openservicemesh", "autoUpgradeMinorVersion": + false, "releaseTrain": "staging", "version": "0.1.0", "scope": {"cluster": {}}, + "configurationSettings": {}, "configurationProtectedSettings": {}}, "location": + ""}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension create + Connection: + - keep-alive + Content-Length: + - '252' + Content-Type: + - application/json; charset=utf-8 + ParameterSetName: + - -g -n -c --cluster-type --extension-type --release-train --version + User-Agent: + - python/3.9.0 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.4 + azure-mgmt-kubernetesconfiguration/0.1.0 Azure-SDK-For-Python AZURECLI/2.19.1 + accept-language: + - en-US + method: PUT + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh?api-version=2020-07-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh","name":"openservice-mesh","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"configurationSettings":{},"statuses":[],"extensionType":"microsoft.openservicemesh","autoUpgradeMinorVersion":false,"releaseTrain":"staging","version":"0.1.0","scope":{"cluster":{"releaseNamespace":"arc-osm-system"}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-03-08T23:14:12.4010326+00:00","lastModifiedTime":"2021-03-08T23:14:12.4010327+00:00"}}' + headers: + api-supported-versions: + - 2020-07-01-Preview + cache-control: + - no-cache + content-length: + - '708' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Mar 2021 23:14:11 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - openresty/1.15.8.2 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + x-ms-ratelimit-remaining-subscription-writes: + - '1199' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension list + Connection: + - keep-alive + ParameterSetName: + - -c -g --cluster-type + User-Agent: + - python/3.9.0 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.4 + azure-mgmt-kubernetesconfiguration/0.1.0 Azure-SDK-For-Python AZURECLI/2.19.1 + accept-language: + - en-US + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions?api-version=2020-07-01-preview + response: + body: + string: '{"value":[{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh","name":"openservice-mesh","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"extensionType":"microsoft.openservicemesh","autoUpgradeMinorVersion":false,"releaseTrain":"staging","version":"0.1.0","scope":{"cluster":{"releaseNamespace":"arc-osm-system"}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-03-08T23:14:12.4010326+00:00","lastModifiedTime":"2021-03-08T23:14:12.4010327+00:00"}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/hci22jan21","name":"hci22jan21","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"extensionType":"microsoft.azstackhci.operator","autoUpgradeMinorVersion":true,"releaseTrain":"stable","version":"1.0.0","scope":{"cluster":{"releaseNamespace":null}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-01-22T20:49:34.3336157+00:00","lastModifiedTime":"2021-01-22T20:49:34.3336249+00:00"}}],"nextLink":null}' + headers: + api-supported-versions: + - 2020-07-01-Preview + cache-control: + - no-cache + content-length: + - '1341' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Mar 2021 23:14:13 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - openresty/1.15.8.2 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension show + Connection: + - keep-alive + ParameterSetName: + - -c -g -n --cluster-type + User-Agent: + - python/3.9.0 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.4 + azure-mgmt-kubernetesconfiguration/0.1.0 Azure-SDK-For-Python AZURECLI/2.19.1 + accept-language: + - en-US + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh?api-version=2020-07-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh","name":"openservice-mesh","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"configurationSettings":{},"statuses":[],"extensionType":"microsoft.openservicemesh","autoUpgradeMinorVersion":false,"releaseTrain":"staging","version":"0.1.0","scope":{"cluster":{"releaseNamespace":"arc-osm-system"}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-03-08T23:14:12.4010326+00:00","lastModifiedTime":"2021-03-08T23:14:12.4010327+00:00"}}' + headers: + api-supported-versions: + - 2020-07-01-Preview + cache-control: + - no-cache + content-length: + - '708' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Mar 2021 23:14:14 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - openresty/1.15.8.2 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension delete + Connection: + - keep-alive + Content-Length: + - '0' + ParameterSetName: + - -g -c -n --cluster-type -y + User-Agent: + - python/3.9.0 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.4 + azure-mgmt-kubernetesconfiguration/0.1.0 Azure-SDK-For-Python AZURECLI/2.19.1 + accept-language: + - en-US + method: DELETE + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh?api-version=2020-07-01-preview + response: + body: + string: '{"content":null,"statusCode":200,"headers":[],"version":"1.1","reasonPhrase":"OK","trailingHeaders":[],"requestMessage":null,"isSuccessStatusCode":true}' + headers: + api-supported-versions: + - 2020-07-01-Preview + cache-control: + - no-cache + content-length: + - '152' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Mar 2021 23:14:14 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - openresty/1.15.8.2 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + x-ms-ratelimit-remaining-subscription-deletes: + - '14999' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension list + Connection: + - keep-alive + ParameterSetName: + - -c -g --cluster-type + User-Agent: + - python/3.9.0 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.4 + azure-mgmt-kubernetesconfiguration/0.1.0 Azure-SDK-For-Python AZURECLI/2.19.1 + accept-language: + - en-US + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions?api-version=2020-07-01-preview + response: + body: + string: '{"value":[{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/hci22jan21","name":"hci22jan21","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"extensionType":"microsoft.azstackhci.operator","autoUpgradeMinorVersion":true,"releaseTrain":"stable","version":"1.0.0","scope":{"cluster":{"releaseNamespace":null}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-01-22T20:49:34.3336157+00:00","lastModifiedTime":"2021-01-22T20:49:34.3336249+00:00"}}],"nextLink":null}' + headers: + api-supported-versions: + - 2020-07-01-Preview + cache-control: + - no-cache + content-length: + - '673' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 08 Mar 2021 23:14:16 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - openresty/1.15.8.2 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +version: 1 diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py b/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py new file mode 100644 index 00000000000..0e53c9e6691 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py @@ -0,0 +1,67 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import os +import unittest + +from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer, record_only) + + +TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) + + +class K8sExtensionScenarioTest(ScenarioTest): + @record_only() + @ResourceGroupPreparer(name_prefix='cli_test_k8s_extension') + def test_k8s_extension(self): + resource_type = 'microsoft.openservicemesh' + self.kwargs.update({ + 'name': 'openservice-mesh', + 'rg': 'nanthirg0923', + 'cluster_name': 'nanthicluster0923', + 'cluster_type': 'connectedClusters', + 'extension_type': resource_type, + 'release_train': 'staging', + 'version': '0.1.0' + }) + + self.cmd('k8s-extension create -g {rg} -n {name} -c {cluster_name} --cluster-type {cluster_type} --extension-type {extension_type} --release-train {release_train} --version {version}', checks=[ + self.check('name', '{name}'), + self.check('releaseTrain', '{release_train}'), + self.check('version', '{version}'), + self.check('resourceGroup', '{rg}'), + self.check('extensionType', '{extension_type}') + ]) + + # Update is disabled for now + # self.cmd('k8s-extension update -g {rg} -n {name} --tags foo=boo', checks=[ + # self.check('tags.foo', 'boo') + # ]) + + installed_exts = self.cmd('k8s-extension list -c {cluster_name} -g {rg} --cluster-type {cluster_type}').get_output_in_json() + found_extension = False + for item in installed_exts: + if item['extensionType'] == resource_type: + found_extension = True + break + self.assertTrue(found_extension) + + self.cmd('k8s-extension show -c {cluster_name} -g {rg} -n {name} --cluster-type {cluster_type}', checks=[ + self.check('name', '{name}'), + self.check('releaseTrain', '{release_train}'), + self.check('version', '{version}'), + self.check('resourceGroup', '{rg}'), + self.check('extensionType', '{extension_type}') + ]) + + self.cmd('k8s-extension delete -g {rg} -c {cluster_name} -n {name} --cluster-type {cluster_type} -y') + + installed_exts = self.cmd('k8s-extension list -c {cluster_name} -g {rg} --cluster-type {cluster_type}').get_output_in_json() + found_extension = False + for item in installed_exts: + if item['extensionType'] == resource_type: + found_extension = True + break + self.assertFalse(found_extension) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/__init__.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/__init__.py new file mode 100644 index 00000000000..874177b4d34 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/__init__.py @@ -0,0 +1,19 @@ +# 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 ._configuration import SourceControlConfigurationClientConfiguration +from ._source_control_configuration_client import SourceControlConfigurationClient +__all__ = ['SourceControlConfigurationClient', 'SourceControlConfigurationClientConfiguration'] + +from .version import VERSION + +__version__ = VERSION + diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/_configuration.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_configuration.py new file mode 100644 index 00000000000..5043ed69594 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_configuration.py @@ -0,0 +1,49 @@ +# 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 msrestazure import AzureConfiguration + +from .version import VERSION + + +class SourceControlConfigurationClientConfiguration(AzureConfiguration): + """Configuration for SourceControlConfigurationClient + Note that all parameters used to create this instance are saved as instance + attributes. + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param subscription_id: The Azure subscription ID. This is a + GUID-formatted string (e.g. 00000000-0000-0000-0000-000000000000) + :type subscription_id: str + :param str base_url: Service URL + """ + + def __init__( + self, credentials, subscription_id, base_url=None): + + if credentials is None: + raise ValueError("Parameter 'credentials' must not be None.") + if subscription_id is None: + raise ValueError("Parameter 'subscription_id' must not be None.") + if not base_url: + base_url = 'https://management.azure.com' + + super(SourceControlConfigurationClientConfiguration, self).__init__(base_url) + + # Starting Autorest.Python 4.0.64, make connection pool activated by default + self.keep_alive = True + + self.add_user_agent('azure-mgmt-kubernetesconfiguration/{}'.format(VERSION)) + self.add_user_agent('Azure-SDK-For-Python') + + self.credentials = credentials + self.subscription_id = subscription_id diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/_source_control_configuration_client.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_source_control_configuration_client.py new file mode 100644 index 00000000000..a77176d8cb1 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_source_control_configuration_client.py @@ -0,0 +1,60 @@ +# 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 msrest.service_client import SDKClient +from msrest import Serializer, Deserializer + +from ._configuration import SourceControlConfigurationClientConfiguration +from .operations import SourceControlConfigurationsOperations +from .operations import Operations +from .operations import ExtensionsOperations +from . import models + + +class SourceControlConfigurationClient(SDKClient): + """KubernetesConfiguration Client + + :ivar config: Configuration for client. + :vartype config: SourceControlConfigurationClientConfiguration + + :ivar source_control_configurations: SourceControlConfigurations operations + :vartype source_control_configurations: azure.mgmt.kubernetesconfiguration.operations.SourceControlConfigurationsOperations + :ivar operations: Operations operations + :vartype operations: azure.mgmt.kubernetesconfiguration.operations.Operations + :ivar extensions: Extensions operations + :vartype extensions: azure.mgmt.kubernetesconfiguration.operations.ExtensionsOperations + + :param credentials: Credentials needed for the client to connect to Azure. + :type credentials: :mod:`A msrestazure Credentials + object` + :param subscription_id: The Azure subscription ID. This is a + GUID-formatted string (e.g. 00000000-0000-0000-0000-000000000000) + :type subscription_id: str + :param str base_url: Service URL + """ + + def __init__( + self, credentials, subscription_id, base_url=None): + + self.config = SourceControlConfigurationClientConfiguration(credentials, subscription_id, base_url) + super(SourceControlConfigurationClient, self).__init__(self.config.credentials, self.config) + + client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} + self.api_version = '2020-07-01-preview' + self._serialize = Serializer(client_models) + self._deserialize = Deserializer(client_models) + + self.source_control_configurations = SourceControlConfigurationsOperations( + self._client, self.config, self._serialize, self._deserialize) + self.operations = Operations( + self._client, self.config, self._serialize, self._deserialize) + self.extensions = ExtensionsOperations( + self._client, self.config, self._serialize, self._deserialize) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/__init__.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/__init__.py new file mode 100644 index 00000000000..e74cb56832b --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/__init__.py @@ -0,0 +1,94 @@ +# 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. +# -------------------------------------------------------------------------- + +try: + from ._models_py3 import ComplianceStatus + from ._models_py3 import ConfigurationIdentity + from ._models_py3 import ErrorDefinition + from ._models_py3 import ErrorResponse, ErrorResponseException + from ._models_py3 import ExtensionInstance + from ._models_py3 import ExtensionInstanceUpdate + from ._models_py3 import ExtensionStatus + from ._models_py3 import HelmOperatorProperties + from ._models_py3 import ProxyResource + from ._models_py3 import Resource + from ._models_py3 import ResourceProviderOperation + from ._models_py3 import ResourceProviderOperationDisplay + from ._models_py3 import Result + from ._models_py3 import Scope + from ._models_py3 import ScopeCluster + from ._models_py3 import ScopeNamespace + from ._models_py3 import SourceControlConfiguration + from ._models_py3 import SystemData +except (SyntaxError, ImportError): + from ._models import ComplianceStatus + from ._models import ConfigurationIdentity + from ._models import ErrorDefinition + from ._models import ErrorResponse, ErrorResponseException + from ._models import ExtensionInstance + from ._models import ExtensionInstanceUpdate + from ._models import ExtensionStatus + from ._models import HelmOperatorProperties + from ._models import ProxyResource + from ._models import Resource + from ._models import ResourceProviderOperation + from ._models import ResourceProviderOperationDisplay + from ._models import Result + from ._models import Scope + from ._models import ScopeCluster + from ._models import ScopeNamespace + from ._models import SourceControlConfiguration + from ._models import SystemData +from ._paged_models import ExtensionInstancePaged +from ._paged_models import ResourceProviderOperationPaged +from ._paged_models import SourceControlConfigurationPaged +from ._source_control_configuration_client_enums import ( + ComplianceStateType, + MessageLevelType, + OperatorType, + OperatorScopeType, + ProvisioningStateType, + InstallStateType, + LevelType, + ResourceIdentityType, +) + +__all__ = [ + 'ComplianceStatus', + 'ConfigurationIdentity', + 'ErrorDefinition', + 'ErrorResponse', 'ErrorResponseException', + 'ExtensionInstance', + 'ExtensionInstanceUpdate', + 'ExtensionStatus', + 'HelmOperatorProperties', + 'ProxyResource', + 'Resource', + 'ResourceProviderOperation', + 'ResourceProviderOperationDisplay', + 'Result', + 'Scope', + 'ScopeCluster', + 'ScopeNamespace', + 'SourceControlConfiguration', + 'SystemData', + 'SourceControlConfigurationPaged', + 'ResourceProviderOperationPaged', + 'ExtensionInstancePaged', + 'ComplianceStateType', + 'MessageLevelType', + 'OperatorType', + 'OperatorScopeType', + 'ProvisioningStateType', + 'InstallStateType', + 'LevelType', + 'ResourceIdentityType', +] diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py new file mode 100644 index 00000000000..f74ea5d809e --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py @@ -0,0 +1,726 @@ +# 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 msrest.serialization import Model +from msrest.exceptions import HttpOperationError + + +class CloudError(Model): + """CloudError. + """ + + _attribute_map = { + } + + +class ComplianceStatus(Model): + """Compliance Status details. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar compliance_state: The compliance state of the configuration. + Possible values include: 'Pending', 'Compliant', 'Noncompliant', + 'Installed', 'Failed' + :vartype compliance_state: str or + ~azure.mgmt.kubernetesconfiguration.models.ComplianceStateType + :param last_config_applied: Datetime the configuration was last applied. + :type last_config_applied: datetime + :param message: Message from when the configuration was applied. + :type message: str + :param message_level: Level of the message. Possible values include: + 'Error', 'Warning', 'Information' + :type message_level: str or + ~azure.mgmt.kubernetesconfiguration.models.MessageLevelType + """ + + _validation = { + 'compliance_state': {'readonly': True}, + } + + _attribute_map = { + 'compliance_state': {'key': 'complianceState', 'type': 'str'}, + 'last_config_applied': {'key': 'lastConfigApplied', 'type': 'iso-8601'}, + 'message': {'key': 'message', 'type': 'str'}, + 'message_level': {'key': 'messageLevel', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ComplianceStatus, self).__init__(**kwargs) + self.compliance_state = None + self.last_config_applied = kwargs.get('last_config_applied', None) + self.message = kwargs.get('message', None) + self.message_level = kwargs.get('message_level', None) + + +class ConfigurationIdentity(Model): + """Identity for the managed cluster. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar principal_id: The principal id of the system assigned identity which + is used by the configuration. + :vartype principal_id: str + :ivar tenant_id: The tenant id of the system assigned identity which is + used by the configuration. + :vartype tenant_id: str + :param type: The type of identity used for the configuration. Type + 'SystemAssigned' will use an implicitly created identity. Type 'None' will + not use Managed Identity for the configuration. Possible values include: + 'SystemAssigned', 'None' + :type type: str or + ~azure.mgmt.kubernetesconfiguration.models.ResourceIdentityType + """ + + _validation = { + 'principal_id': {'readonly': True}, + 'tenant_id': {'readonly': True}, + } + + _attribute_map = { + 'principal_id': {'key': 'principalId', 'type': 'str'}, + 'tenant_id': {'key': 'tenantId', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'ResourceIdentityType'}, + } + + def __init__(self, **kwargs): + super(ConfigurationIdentity, self).__init__(**kwargs) + self.principal_id = None + self.tenant_id = None + self.type = kwargs.get('type', None) + + +class ErrorDefinition(Model): + """Error definition. + + All required parameters must be populated in order to send to Azure. + + :param code: Required. Service specific error code which serves as the + substatus for the HTTP error code. + :type code: str + :param message: Required. Description of the error. + :type message: str + """ + + _validation = { + 'code': {'required': True}, + 'message': {'required': True}, + } + + _attribute_map = { + 'code': {'key': 'code', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ErrorDefinition, self).__init__(**kwargs) + self.code = kwargs.get('code', None) + self.message = kwargs.get('message', None) + + +class ErrorResponse(Model): + """Error response. + + :param error: Error definition. + :type error: ~azure.mgmt.kubernetesconfiguration.models.ErrorDefinition + """ + + _attribute_map = { + 'error': {'key': 'error', 'type': 'ErrorDefinition'}, + } + + def __init__(self, **kwargs): + super(ErrorResponse, self).__init__(**kwargs) + self.error = kwargs.get('error', None) + + +class ErrorResponseException(HttpOperationError): + """Server responsed with exception of type: 'ErrorResponse'. + + :param deserialize: A deserializer + :param response: Server response to be deserialized. + """ + + def __init__(self, deserialize, response, *args): + + super(ErrorResponseException, self).__init__(deserialize, response, 'ErrorResponse', *args) + + +class Resource(Model): + """The Resource model definition. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + :param system_data: Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources + :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + } + + def __init__(self, **kwargs): + super(Resource, self).__init__(**kwargs) + self.id = None + self.name = None + self.type = None + self.system_data = kwargs.get('system_data', None) + + +class ProxyResource(Resource): + """ARM proxy resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + :param system_data: Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources + :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + } + + def __init__(self, **kwargs): + super(ProxyResource, self).__init__(**kwargs) + + +class ExtensionInstance(ProxyResource): + """The Extension Instance object. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + :param location: Location of resource type + :type location: str + :param system_data: Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources + :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData + :param extension_type: Type of the Extension, of which this resource is an + instance of. It must be one of the Extension Types registered with + Microsoft.KubernetesConfiguration by the Extension publisher. + :type extension_type: str + :param auto_upgrade_minor_version: Flag to note if this instance + participates in auto upgrade of minor version, or not. + :type auto_upgrade_minor_version: bool + :param release_train: ReleaseTrain this extension instance participates in + for auto-upgrade (e.g. Stable, Preview, etc.) - only if + autoUpgradeMinorVersion is 'true'. + :type release_train: str + :param version: Version of the extension for this extension instance, if + it is 'pinned' to a specific version. autoUpgradeMinorVersion must be + 'false'. + :type version: str + :param scope: Scope at which the extension instance is installed. + :type scope: ~azure.mgmt.kubernetesconfiguration.models.Scope + :param configuration_settings: Configuration settings, as name-value pairs + for configuring this instance of the extension. + :type configuration_settings: dict[str, str] + :param configuration_protected_settings: Configuration settings that are + sensitive, as name-value pairs for configuring this instance of the + extension. + :type configuration_protected_settings: dict[str, str] + :param install_state: Status of installation of this instance of the + extension. Possible values include: 'Pending', 'Installed', 'Failed' + :type install_state: str or + ~azure.mgmt.kubernetesconfiguration.models.InstallStateType + :param statuses: Status from this instance of the extension. + :type statuses: + list[~azure.mgmt.kubernetesconfiguration.models.ExtensionStatus] + :ivar creation_time: DateLiteral (per ISO8601) noting the time the + resource was created by the client (user). + :vartype creation_time: str + :ivar last_modified_time: DateLiteral (per ISO8601) noting the time the + resource was modified by the client (user). + :vartype last_modified_time: str + :ivar last_status_time: DateLiteral (per ISO8601) noting the time of last + status from the agent. + :vartype last_status_time: str + :ivar error_info: Error information from the Agent - e.g. errors during + installation. + :vartype error_info: + ~azure.mgmt.kubernetesconfiguration.models.ErrorDefinition + :param identity: The identity of the configuration. + :type identity: + ~azure.mgmt.kubernetesconfiguration.models.ConfigurationIdentity + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'creation_time': {'readonly': True}, + 'last_modified_time': {'readonly': True}, + 'last_status_time': {'readonly': True}, + 'error_info': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'location': {'key': 'location', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, + 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, + 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, + 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, + 'version': {'key': 'properties.version', 'type': 'str'}, + 'scope': {'key': 'properties.scope', 'type': 'Scope'}, + 'configuration_settings': {'key': 'properties.configurationSettings', 'type': '{str}'}, + 'configuration_protected_settings': {'key': 'properties.configurationProtectedSettings', 'type': '{str}'}, + 'install_state': {'key': 'properties.installState', 'type': 'str'}, + 'statuses': {'key': 'properties.statuses', 'type': '[ExtensionStatus]'}, + 'creation_time': {'key': 'properties.creationTime', 'type': 'str'}, + 'last_modified_time': {'key': 'properties.lastModifiedTime', 'type': 'str'}, + 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, + 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'}, + } + + def __init__(self, **kwargs): + super(ExtensionInstance, self).__init__(**kwargs) + self.location = kwargs.get('location', None) + self.extension_type = kwargs.get('extension_type', None) + self.auto_upgrade_minor_version = kwargs.get('auto_upgrade_minor_version', None) + self.release_train = kwargs.get('release_train', None) + self.version = kwargs.get('version', None) + self.scope = kwargs.get('scope', None) + self.configuration_settings = kwargs.get('configuration_settings', None) + self.configuration_protected_settings = kwargs.get('configuration_protected_settings', None) + self.install_state = kwargs.get('install_state', None) + self.statuses = kwargs.get('statuses', None) + self.creation_time = None + self.last_modified_time = None + self.last_status_time = None + self.error_info = None + self.identity = kwargs.get('identity', None) + + +class ExtensionInstanceUpdate(Model): + """Update Extension Instance request object. + + :param auto_upgrade_minor_version: Flag to note if this instance + participates in Extension Lifecycle Management or not. + :type auto_upgrade_minor_version: bool + :param release_train: ReleaseTrain this extension instance participates in + for auto-upgrade (e.g. Stable, Preview, etc.) - only if + autoUpgradeMinorVersion is 'true'. + :type release_train: str + :param version: Version number of extension, to 'pin' to a specific + version. autoUpgradeMinorVersion must be 'false'. + :type version: str + """ + + _attribute_map = { + 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, + 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, + 'version': {'key': 'properties.version', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ExtensionInstanceUpdate, self).__init__(**kwargs) + self.auto_upgrade_minor_version = kwargs.get('auto_upgrade_minor_version', None) + self.release_train = kwargs.get('release_train', None) + self.version = kwargs.get('version', None) + + +class ExtensionStatus(Model): + """Status from this instance of the extension. + + :param code: Status code provided by the Extension + :type code: str + :param display_status: Short description of status of this instance of the + extension. + :type display_status: str + :param level: Level of the status. Possible values include: 'Error', + 'Warning', 'Information'. Default value: "Information" . + :type level: str or ~azure.mgmt.kubernetesconfiguration.models.LevelType + :param message: Detailed message of the status from the Extension + instance. + :type message: str + :param time: DateLiteral (per ISO8601) noting the time of installation + status. + :type time: str + """ + + _attribute_map = { + 'code': {'key': 'code', 'type': 'str'}, + 'display_status': {'key': 'displayStatus', 'type': 'str'}, + 'level': {'key': 'level', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'time': {'key': 'time', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ExtensionStatus, self).__init__(**kwargs) + self.code = kwargs.get('code', None) + self.display_status = kwargs.get('display_status', None) + self.level = kwargs.get('level', "Information") + self.message = kwargs.get('message', None) + self.time = kwargs.get('time', None) + + +class HelmOperatorProperties(Model): + """Properties for Helm operator. + + :param chart_version: Version of the operator Helm chart. + :type chart_version: str + :param chart_values: Values override for the operator Helm chart. + :type chart_values: str + """ + + _attribute_map = { + 'chart_version': {'key': 'chartVersion', 'type': 'str'}, + 'chart_values': {'key': 'chartValues', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(HelmOperatorProperties, self).__init__(**kwargs) + self.chart_version = kwargs.get('chart_version', None) + self.chart_values = kwargs.get('chart_values', None) + + +class ResourceProviderOperation(Model): + """Supported operation of this resource provider. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param name: Operation name, in format of + {provider}/{resource}/{operation} + :type name: str + :param display: Display metadata associated with the operation. + :type display: + ~azure.mgmt.kubernetesconfiguration.models.ResourceProviderOperationDisplay + :ivar is_data_action: The flag that indicates whether the operation + applies to data plane. + :vartype is_data_action: bool + """ + + _validation = { + 'is_data_action': {'readonly': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'display': {'key': 'display', 'type': 'ResourceProviderOperationDisplay'}, + 'is_data_action': {'key': 'isDataAction', 'type': 'bool'}, + } + + def __init__(self, **kwargs): + super(ResourceProviderOperation, self).__init__(**kwargs) + self.name = kwargs.get('name', None) + self.display = kwargs.get('display', None) + self.is_data_action = None + + +class ResourceProviderOperationDisplay(Model): + """Display metadata associated with the operation. + + :param provider: Resource provider: Microsoft KubernetesConfiguration. + :type provider: str + :param resource: Resource on which the operation is performed. + :type resource: str + :param operation: Type of operation: get, read, delete, etc. + :type operation: str + :param description: Description of this operation. + :type description: str + """ + + _attribute_map = { + 'provider': {'key': 'provider', 'type': 'str'}, + 'resource': {'key': 'resource', 'type': 'str'}, + 'operation': {'key': 'operation', 'type': 'str'}, + 'description': {'key': 'description', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ResourceProviderOperationDisplay, self).__init__(**kwargs) + self.provider = kwargs.get('provider', None) + self.resource = kwargs.get('resource', None) + self.operation = kwargs.get('operation', None) + self.description = kwargs.get('description', None) + + +class Result(Model): + """Sample result definition. + + :param sample_property: Sample property of type string + :type sample_property: str + """ + + _attribute_map = { + 'sample_property': {'key': 'sampleProperty', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(Result, self).__init__(**kwargs) + self.sample_property = kwargs.get('sample_property', None) + + +class Scope(Model): + """Scope of the extensionInstance. It can be either Cluster or Namespace; but + not both. + + :param cluster: Specifies that the scope of the extensionInstance is + Cluster + :type cluster: ~azure.mgmt.kubernetesconfiguration.models.ScopeCluster + :param namespace: Specifies that the scope of the extensionInstance is + Namespace + :type namespace: ~azure.mgmt.kubernetesconfiguration.models.ScopeNamespace + """ + + _attribute_map = { + 'cluster': {'key': 'cluster', 'type': 'ScopeCluster'}, + 'namespace': {'key': 'namespace', 'type': 'ScopeNamespace'}, + } + + def __init__(self, **kwargs): + super(Scope, self).__init__(**kwargs) + self.cluster = kwargs.get('cluster', None) + self.namespace = kwargs.get('namespace', None) + + +class ScopeCluster(Model): + """Specifies that the scope of the extensionInstance is Cluster. + + :param release_namespace: Namespace where the extension Release must be + placed, for a Cluster scoped extensionInstance. If this namespace does + not exist, it will be created + :type release_namespace: str + """ + + _attribute_map = { + 'release_namespace': {'key': 'releaseNamespace', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ScopeCluster, self).__init__(**kwargs) + self.release_namespace = kwargs.get('release_namespace', None) + + +class ScopeNamespace(Model): + """Specifies that the scope of the extensionInstance is Namespace. + + :param target_namespace: Namespace where the extensionInstance will be + created for an Namespace scoped extensionInstance. If this namespace does + not exist, it will be created + :type target_namespace: str + """ + + _attribute_map = { + 'target_namespace': {'key': 'targetNamespace', 'type': 'str'}, + } + + def __init__(self, **kwargs): + super(ScopeNamespace, self).__init__(**kwargs) + self.target_namespace = kwargs.get('target_namespace', None) + + +class SourceControlConfiguration(ProxyResource): + """The SourceControl Configuration object returned in Get & Put response. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + :param system_data: Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources + :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData + :param repository_url: Url of the SourceControl Repository. + :type repository_url: str + :param operator_namespace: The namespace to which this operator is + installed to. Maximum of 253 lower case alphanumeric characters, hyphen + and period only. Default value: "default" . + :type operator_namespace: str + :param operator_instance_name: Instance name of the operator - identifying + the specific configuration. + :type operator_instance_name: str + :param operator_type: Type of the operator. Possible values include: + 'Flux' + :type operator_type: str or + ~azure.mgmt.kubernetesconfiguration.models.OperatorType + :param operator_params: Any Parameters for the Operator instance in string + format. + :type operator_params: str + :param configuration_protected_settings: Name-value pairs of protected + configuration settings for the configuration + :type configuration_protected_settings: dict[str, str] + :param operator_scope: Scope at which the operator will be installed. + Possible values include: 'cluster', 'namespace'. Default value: "cluster" + . + :type operator_scope: str or + ~azure.mgmt.kubernetesconfiguration.models.OperatorScopeType + :ivar repository_public_key: Public Key associated with this SourceControl + configuration (either generated within the cluster or provided by the + user). + :vartype repository_public_key: str + :param ssh_known_hosts_contents: Base64-encoded known_hosts contents + containing public SSH keys required to access private Git instances + :type ssh_known_hosts_contents: str + :param enable_helm_operator: Option to enable Helm Operator for this git + configuration. + :type enable_helm_operator: bool + :param helm_operator_properties: Properties for Helm operator. + :type helm_operator_properties: + ~azure.mgmt.kubernetesconfiguration.models.HelmOperatorProperties + :ivar provisioning_state: The provisioning state of the resource provider. + Possible values include: 'Accepted', 'Deleting', 'Running', 'Succeeded', + 'Failed' + :vartype provisioning_state: str or + ~azure.mgmt.kubernetesconfiguration.models.ProvisioningStateType + :ivar compliance_status: Compliance Status of the Configuration + :vartype compliance_status: + ~azure.mgmt.kubernetesconfiguration.models.ComplianceStatus + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'repository_public_key': {'readonly': True}, + 'provisioning_state': {'readonly': True}, + 'compliance_status': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'repository_url': {'key': 'properties.repositoryUrl', 'type': 'str'}, + 'operator_namespace': {'key': 'properties.operatorNamespace', 'type': 'str'}, + 'operator_instance_name': {'key': 'properties.operatorInstanceName', 'type': 'str'}, + 'operator_type': {'key': 'properties.operatorType', 'type': 'str'}, + 'operator_params': {'key': 'properties.operatorParams', 'type': 'str'}, + 'configuration_protected_settings': {'key': 'properties.configurationProtectedSettings', 'type': '{str}'}, + 'operator_scope': {'key': 'properties.operatorScope', 'type': 'str'}, + 'repository_public_key': {'key': 'properties.repositoryPublicKey', 'type': 'str'}, + 'ssh_known_hosts_contents': {'key': 'properties.sshKnownHostsContents', 'type': 'str'}, + 'enable_helm_operator': {'key': 'properties.enableHelmOperator', 'type': 'bool'}, + 'helm_operator_properties': {'key': 'properties.helmOperatorProperties', 'type': 'HelmOperatorProperties'}, + 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, + 'compliance_status': {'key': 'properties.complianceStatus', 'type': 'ComplianceStatus'}, + } + + def __init__(self, **kwargs): + super(SourceControlConfiguration, self).__init__(**kwargs) + self.repository_url = kwargs.get('repository_url', None) + self.operator_namespace = kwargs.get('operator_namespace', "default") + self.operator_instance_name = kwargs.get('operator_instance_name', None) + self.operator_type = kwargs.get('operator_type', None) + self.operator_params = kwargs.get('operator_params', None) + self.configuration_protected_settings = kwargs.get('configuration_protected_settings', None) + self.operator_scope = kwargs.get('operator_scope', "cluster") + self.repository_public_key = None + self.ssh_known_hosts_contents = kwargs.get('ssh_known_hosts_contents', None) + self.enable_helm_operator = kwargs.get('enable_helm_operator', None) + self.helm_operator_properties = kwargs.get('helm_operator_properties', None) + self.provisioning_state = None + self.compliance_status = None + + +class SystemData(Model): + """Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar created_by: A string identifier for the identity that created the + resource + :vartype created_by: str + :ivar created_by_type: The type of identity that created the resource: + user, application, managedIdentity, key + :vartype created_by_type: str + :ivar created_at: The timestamp of resource creation (UTC) + :vartype created_at: datetime + :ivar last_modified_by: A string identifier for the identity that last + modified the resource + :vartype last_modified_by: str + :ivar last_modified_by_type: The type of identity that last modified the + resource: user, application, managedIdentity, key + :vartype last_modified_by_type: str + :ivar last_modified_at: The timestamp of resource last modification (UTC) + :vartype last_modified_at: datetime + """ + + _validation = { + 'created_by': {'readonly': True}, + 'created_by_type': {'readonly': True}, + 'created_at': {'readonly': True}, + 'last_modified_by': {'readonly': True}, + 'last_modified_by_type': {'readonly': True}, + 'last_modified_at': {'readonly': True}, + } + + _attribute_map = { + 'created_by': {'key': 'createdBy', 'type': 'str'}, + 'created_by_type': {'key': 'createdByType', 'type': 'str'}, + 'created_at': {'key': 'createdAt', 'type': 'iso-8601'}, + 'last_modified_by': {'key': 'lastModifiedBy', 'type': 'str'}, + 'last_modified_by_type': {'key': 'lastModifiedByType', 'type': 'str'}, + 'last_modified_at': {'key': 'lastModifiedAt', 'type': 'iso-8601'}, + } + + def __init__(self, **kwargs): + super(SystemData, self).__init__(**kwargs) + self.created_by = None + self.created_by_type = None + self.created_at = None + self.last_modified_by = None + self.last_modified_by_type = None + self.last_modified_at = None diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py new file mode 100644 index 00000000000..57f42c85edd --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py @@ -0,0 +1,726 @@ +# 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 msrest.serialization import Model +from msrest.exceptions import HttpOperationError + + +class CloudError(Model): + """CloudError. + """ + + _attribute_map = { + } + + +class ComplianceStatus(Model): + """Compliance Status details. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar compliance_state: The compliance state of the configuration. + Possible values include: 'Pending', 'Compliant', 'Noncompliant', + 'Installed', 'Failed' + :vartype compliance_state: str or + ~azure.mgmt.kubernetesconfiguration.models.ComplianceStateType + :param last_config_applied: Datetime the configuration was last applied. + :type last_config_applied: datetime + :param message: Message from when the configuration was applied. + :type message: str + :param message_level: Level of the message. Possible values include: + 'Error', 'Warning', 'Information' + :type message_level: str or + ~azure.mgmt.kubernetesconfiguration.models.MessageLevelType + """ + + _validation = { + 'compliance_state': {'readonly': True}, + } + + _attribute_map = { + 'compliance_state': {'key': 'complianceState', 'type': 'str'}, + 'last_config_applied': {'key': 'lastConfigApplied', 'type': 'iso-8601'}, + 'message': {'key': 'message', 'type': 'str'}, + 'message_level': {'key': 'messageLevel', 'type': 'str'}, + } + + def __init__(self, *, last_config_applied=None, message: str=None, message_level=None, **kwargs) -> None: + super(ComplianceStatus, self).__init__(**kwargs) + self.compliance_state = None + self.last_config_applied = last_config_applied + self.message = message + self.message_level = message_level + + +class ConfigurationIdentity(Model): + """Identity for the managed cluster. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar principal_id: The principal id of the system assigned identity which + is used by the configuration. + :vartype principal_id: str + :ivar tenant_id: The tenant id of the system assigned identity which is + used by the configuration. + :vartype tenant_id: str + :param type: The type of identity used for the configuration. Type + 'SystemAssigned' will use an implicitly created identity. Type 'None' will + not use Managed Identity for the configuration. Possible values include: + 'SystemAssigned', 'None' + :type type: str or + ~azure.mgmt.kubernetesconfiguration.models.ResourceIdentityType + """ + + _validation = { + 'principal_id': {'readonly': True}, + 'tenant_id': {'readonly': True}, + } + + _attribute_map = { + 'principal_id': {'key': 'principalId', 'type': 'str'}, + 'tenant_id': {'key': 'tenantId', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'ResourceIdentityType'}, + } + + def __init__(self, *, type=None, **kwargs) -> None: + super(ConfigurationIdentity, self).__init__(**kwargs) + self.principal_id = None + self.tenant_id = None + self.type = type + + +class ErrorDefinition(Model): + """Error definition. + + All required parameters must be populated in order to send to Azure. + + :param code: Required. Service specific error code which serves as the + substatus for the HTTP error code. + :type code: str + :param message: Required. Description of the error. + :type message: str + """ + + _validation = { + 'code': {'required': True}, + 'message': {'required': True}, + } + + _attribute_map = { + 'code': {'key': 'code', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + } + + def __init__(self, *, code: str, message: str, **kwargs) -> None: + super(ErrorDefinition, self).__init__(**kwargs) + self.code = code + self.message = message + + +class ErrorResponse(Model): + """Error response. + + :param error: Error definition. + :type error: ~azure.mgmt.kubernetesconfiguration.models.ErrorDefinition + """ + + _attribute_map = { + 'error': {'key': 'error', 'type': 'ErrorDefinition'}, + } + + def __init__(self, *, error=None, **kwargs) -> None: + super(ErrorResponse, self).__init__(**kwargs) + self.error = error + + +class ErrorResponseException(HttpOperationError): + """Server responsed with exception of type: 'ErrorResponse'. + + :param deserialize: A deserializer + :param response: Server response to be deserialized. + """ + + def __init__(self, deserialize, response, *args): + + super(ErrorResponseException, self).__init__(deserialize, response, 'ErrorResponse', *args) + + +class Resource(Model): + """The Resource model definition. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + :param system_data: Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources + :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + } + + def __init__(self, *, system_data=None, **kwargs) -> None: + super(Resource, self).__init__(**kwargs) + self.id = None + self.name = None + self.type = None + self.system_data = system_data + + +class ProxyResource(Resource): + """ARM proxy resource. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + :param system_data: Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources + :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + } + + def __init__(self, *, system_data=None, **kwargs) -> None: + super(ProxyResource, self).__init__(system_data=system_data, **kwargs) + + +class ExtensionInstance(ProxyResource): + """The Extension Instance object. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + :param location: Location of resource type + :type location: str + :param system_data: Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources + :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData + :param extension_type: Type of the Extension, of which this resource is an + instance of. It must be one of the Extension Types registered with + Microsoft.KubernetesConfiguration by the Extension publisher. + :type extension_type: str + :param auto_upgrade_minor_version: Flag to note if this instance + participates in auto upgrade of minor version, or not. + :type auto_upgrade_minor_version: bool + :param release_train: ReleaseTrain this extension instance participates in + for auto-upgrade (e.g. Stable, Preview, etc.) - only if + autoUpgradeMinorVersion is 'true'. + :type release_train: str + :param version: Version of the extension for this extension instance, if + it is 'pinned' to a specific version. autoUpgradeMinorVersion must be + 'false'. + :type version: str + :param scope: Scope at which the extension instance is installed. + :type scope: ~azure.mgmt.kubernetesconfiguration.models.Scope + :param configuration_settings: Configuration settings, as name-value pairs + for configuring this instance of the extension. + :type configuration_settings: dict[str, str] + :param configuration_protected_settings: Configuration settings that are + sensitive, as name-value pairs for configuring this instance of the + extension. + :type configuration_protected_settings: dict[str, str] + :param install_state: Status of installation of this instance of the + extension. Possible values include: 'Pending', 'Installed', 'Failed' + :type install_state: str or + ~azure.mgmt.kubernetesconfiguration.models.InstallStateType + :param statuses: Status from this instance of the extension. + :type statuses: + list[~azure.mgmt.kubernetesconfiguration.models.ExtensionStatus] + :ivar creation_time: DateLiteral (per ISO8601) noting the time the + resource was created by the client (user). + :vartype creation_time: str + :ivar last_modified_time: DateLiteral (per ISO8601) noting the time the + resource was modified by the client (user). + :vartype last_modified_time: str + :ivar last_status_time: DateLiteral (per ISO8601) noting the time of last + status from the agent. + :vartype last_status_time: str + :ivar error_info: Error information from the Agent - e.g. errors during + installation. + :vartype error_info: + ~azure.mgmt.kubernetesconfiguration.models.ErrorDefinition + :param identity: The identity of the configuration. + :type identity: + ~azure.mgmt.kubernetesconfiguration.models.ConfigurationIdentity + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'creation_time': {'readonly': True}, + 'last_modified_time': {'readonly': True}, + 'last_status_time': {'readonly': True}, + 'error_info': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'location': {'key': 'location', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, + 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, + 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, + 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, + 'version': {'key': 'properties.version', 'type': 'str'}, + 'scope': {'key': 'properties.scope', 'type': 'Scope'}, + 'configuration_settings': {'key': 'properties.configurationSettings', 'type': '{str}'}, + 'configuration_protected_settings': {'key': 'properties.configurationProtectedSettings', 'type': '{str}'}, + 'install_state': {'key': 'properties.installState', 'type': 'str'}, + 'statuses': {'key': 'properties.statuses', 'type': '[ExtensionStatus]'}, + 'creation_time': {'key': 'properties.creationTime', 'type': 'str'}, + 'last_modified_time': {'key': 'properties.lastModifiedTime', 'type': 'str'}, + 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, + 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'} + } + + def __init__(self, *, system_data=None, location: str=None, extension_type: str=None, auto_upgrade_minor_version: bool=None, release_train: str=None, version: str=None, scope=None, configuration_settings=None, configuration_protected_settings=None, install_state=None, statuses=None, identity=None, **kwargs) -> None: + super(ExtensionInstance, self).__init__(system_data=system_data, **kwargs) + self.location = location + self.extension_type = extension_type + self.auto_upgrade_minor_version = auto_upgrade_minor_version + self.release_train = release_train + self.version = version + self.scope = scope + self.configuration_settings = configuration_settings + self.configuration_protected_settings = configuration_protected_settings + self.install_state = install_state + self.statuses = statuses + self.creation_time = None + self.last_modified_time = None + self.last_status_time = None + self.error_info = None + self.identity = identity + + +class ExtensionInstanceUpdate(Model): + """Update Extension Instance request object. + + :param auto_upgrade_minor_version: Flag to note if this instance + participates in Extension Lifecycle Management or not. + :type auto_upgrade_minor_version: bool + :param release_train: ReleaseTrain this extension instance participates in + for auto-upgrade (e.g. Stable, Preview, etc.) - only if + autoUpgradeMinorVersion is 'true'. + :type release_train: str + :param version: Version number of extension, to 'pin' to a specific + version. autoUpgradeMinorVersion must be 'false'. + :type version: str + """ + + _attribute_map = { + 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, + 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, + 'version': {'key': 'properties.version', 'type': 'str'}, + } + + def __init__(self, *, auto_upgrade_minor_version: bool=None, release_train: str=None, version: str=None, **kwargs) -> None: + super(ExtensionInstanceUpdate, self).__init__(**kwargs) + self.auto_upgrade_minor_version = auto_upgrade_minor_version + self.release_train = release_train + self.version = version + + +class ExtensionStatus(Model): + """Status from this instance of the extension. + + :param code: Status code provided by the Extension + :type code: str + :param display_status: Short description of status of this instance of the + extension. + :type display_status: str + :param level: Level of the status. Possible values include: 'Error', + 'Warning', 'Information'. Default value: "Information" . + :type level: str or ~azure.mgmt.kubernetesconfiguration.models.LevelType + :param message: Detailed message of the status from the Extension + instance. + :type message: str + :param time: DateLiteral (per ISO8601) noting the time of installation + status. + :type time: str + """ + + _attribute_map = { + 'code': {'key': 'code', 'type': 'str'}, + 'display_status': {'key': 'displayStatus', 'type': 'str'}, + 'level': {'key': 'level', 'type': 'str'}, + 'message': {'key': 'message', 'type': 'str'}, + 'time': {'key': 'time', 'type': 'str'}, + } + + def __init__(self, *, code: str=None, display_status: str=None, level="Information", message: str=None, time: str=None, **kwargs) -> None: + super(ExtensionStatus, self).__init__(**kwargs) + self.code = code + self.display_status = display_status + self.level = level + self.message = message + self.time = time + + +class HelmOperatorProperties(Model): + """Properties for Helm operator. + + :param chart_version: Version of the operator Helm chart. + :type chart_version: str + :param chart_values: Values override for the operator Helm chart. + :type chart_values: str + """ + + _attribute_map = { + 'chart_version': {'key': 'chartVersion', 'type': 'str'}, + 'chart_values': {'key': 'chartValues', 'type': 'str'}, + } + + def __init__(self, *, chart_version: str=None, chart_values: str=None, **kwargs) -> None: + super(HelmOperatorProperties, self).__init__(**kwargs) + self.chart_version = chart_version + self.chart_values = chart_values + + +class ResourceProviderOperation(Model): + """Supported operation of this resource provider. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :param name: Operation name, in format of + {provider}/{resource}/{operation} + :type name: str + :param display: Display metadata associated with the operation. + :type display: + ~azure.mgmt.kubernetesconfiguration.models.ResourceProviderOperationDisplay + :ivar is_data_action: The flag that indicates whether the operation + applies to data plane. + :vartype is_data_action: bool + """ + + _validation = { + 'is_data_action': {'readonly': True}, + } + + _attribute_map = { + 'name': {'key': 'name', 'type': 'str'}, + 'display': {'key': 'display', 'type': 'ResourceProviderOperationDisplay'}, + 'is_data_action': {'key': 'isDataAction', 'type': 'bool'}, + } + + def __init__(self, *, name: str=None, display=None, **kwargs) -> None: + super(ResourceProviderOperation, self).__init__(**kwargs) + self.name = name + self.display = display + self.is_data_action = None + + +class ResourceProviderOperationDisplay(Model): + """Display metadata associated with the operation. + + :param provider: Resource provider: Microsoft KubernetesConfiguration. + :type provider: str + :param resource: Resource on which the operation is performed. + :type resource: str + :param operation: Type of operation: get, read, delete, etc. + :type operation: str + :param description: Description of this operation. + :type description: str + """ + + _attribute_map = { + 'provider': {'key': 'provider', 'type': 'str'}, + 'resource': {'key': 'resource', 'type': 'str'}, + 'operation': {'key': 'operation', 'type': 'str'}, + 'description': {'key': 'description', 'type': 'str'}, + } + + def __init__(self, *, provider: str=None, resource: str=None, operation: str=None, description: str=None, **kwargs) -> None: + super(ResourceProviderOperationDisplay, self).__init__(**kwargs) + self.provider = provider + self.resource = resource + self.operation = operation + self.description = description + + +class Result(Model): + """Sample result definition. + + :param sample_property: Sample property of type string + :type sample_property: str + """ + + _attribute_map = { + 'sample_property': {'key': 'sampleProperty', 'type': 'str'}, + } + + def __init__(self, *, sample_property: str=None, **kwargs) -> None: + super(Result, self).__init__(**kwargs) + self.sample_property = sample_property + + +class Scope(Model): + """Scope of the extensionInstance. It can be either Cluster or Namespace; but + not both. + + :param cluster: Specifies that the scope of the extensionInstance is + Cluster + :type cluster: ~azure.mgmt.kubernetesconfiguration.models.ScopeCluster + :param namespace: Specifies that the scope of the extensionInstance is + Namespace + :type namespace: ~azure.mgmt.kubernetesconfiguration.models.ScopeNamespace + """ + + _attribute_map = { + 'cluster': {'key': 'cluster', 'type': 'ScopeCluster'}, + 'namespace': {'key': 'namespace', 'type': 'ScopeNamespace'}, + } + + def __init__(self, *, cluster=None, namespace=None, **kwargs) -> None: + super(Scope, self).__init__(**kwargs) + self.cluster = cluster + self.namespace = namespace + + +class ScopeCluster(Model): + """Specifies that the scope of the extensionInstance is Cluster. + + :param release_namespace: Namespace where the extension Release must be + placed, for a Cluster scoped extensionInstance. If this namespace does + not exist, it will be created + :type release_namespace: str + """ + + _attribute_map = { + 'release_namespace': {'key': 'releaseNamespace', 'type': 'str'}, + } + + def __init__(self, *, release_namespace: str=None, **kwargs) -> None: + super(ScopeCluster, self).__init__(**kwargs) + self.release_namespace = release_namespace + + +class ScopeNamespace(Model): + """Specifies that the scope of the extensionInstance is Namespace. + + :param target_namespace: Namespace where the extensionInstance will be + created for an Namespace scoped extensionInstance. If this namespace does + not exist, it will be created + :type target_namespace: str + """ + + _attribute_map = { + 'target_namespace': {'key': 'targetNamespace', 'type': 'str'}, + } + + def __init__(self, *, target_namespace: str=None, **kwargs) -> None: + super(ScopeNamespace, self).__init__(**kwargs) + self.target_namespace = target_namespace + + +class SourceControlConfiguration(ProxyResource): + """The SourceControl Configuration object returned in Get & Put response. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar id: Resource Id + :vartype id: str + :ivar name: Resource name + :vartype name: str + :ivar type: Resource type + :vartype type: str + :param system_data: Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources + :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData + :param repository_url: Url of the SourceControl Repository. + :type repository_url: str + :param operator_namespace: The namespace to which this operator is + installed to. Maximum of 253 lower case alphanumeric characters, hyphen + and period only. Default value: "default" . + :type operator_namespace: str + :param operator_instance_name: Instance name of the operator - identifying + the specific configuration. + :type operator_instance_name: str + :param operator_type: Type of the operator. Possible values include: + 'Flux' + :type operator_type: str or + ~azure.mgmt.kubernetesconfiguration.models.OperatorType + :param operator_params: Any Parameters for the Operator instance in string + format. + :type operator_params: str + :param configuration_protected_settings: Name-value pairs of protected + configuration settings for the configuration + :type configuration_protected_settings: dict[str, str] + :param operator_scope: Scope at which the operator will be installed. + Possible values include: 'cluster', 'namespace'. Default value: "cluster" + . + :type operator_scope: str or + ~azure.mgmt.kubernetesconfiguration.models.OperatorScopeType + :ivar repository_public_key: Public Key associated with this SourceControl + configuration (either generated within the cluster or provided by the + user). + :vartype repository_public_key: str + :param ssh_known_hosts_contents: Base64-encoded known_hosts contents + containing public SSH keys required to access private Git instances + :type ssh_known_hosts_contents: str + :param enable_helm_operator: Option to enable Helm Operator for this git + configuration. + :type enable_helm_operator: bool + :param helm_operator_properties: Properties for Helm operator. + :type helm_operator_properties: + ~azure.mgmt.kubernetesconfiguration.models.HelmOperatorProperties + :ivar provisioning_state: The provisioning state of the resource provider. + Possible values include: 'Accepted', 'Deleting', 'Running', 'Succeeded', + 'Failed' + :vartype provisioning_state: str or + ~azure.mgmt.kubernetesconfiguration.models.ProvisioningStateType + :ivar compliance_status: Compliance Status of the Configuration + :vartype compliance_status: + ~azure.mgmt.kubernetesconfiguration.models.ComplianceStatus + """ + + _validation = { + 'id': {'readonly': True}, + 'name': {'readonly': True}, + 'type': {'readonly': True}, + 'repository_public_key': {'readonly': True}, + 'provisioning_state': {'readonly': True}, + 'compliance_status': {'readonly': True}, + } + + _attribute_map = { + 'id': {'key': 'id', 'type': 'str'}, + 'name': {'key': 'name', 'type': 'str'}, + 'type': {'key': 'type', 'type': 'str'}, + 'system_data': {'key': 'systemData', 'type': 'SystemData'}, + 'repository_url': {'key': 'properties.repositoryUrl', 'type': 'str'}, + 'operator_namespace': {'key': 'properties.operatorNamespace', 'type': 'str'}, + 'operator_instance_name': {'key': 'properties.operatorInstanceName', 'type': 'str'}, + 'operator_type': {'key': 'properties.operatorType', 'type': 'str'}, + 'operator_params': {'key': 'properties.operatorParams', 'type': 'str'}, + 'configuration_protected_settings': {'key': 'properties.configurationProtectedSettings', 'type': '{str}'}, + 'operator_scope': {'key': 'properties.operatorScope', 'type': 'str'}, + 'repository_public_key': {'key': 'properties.repositoryPublicKey', 'type': 'str'}, + 'ssh_known_hosts_contents': {'key': 'properties.sshKnownHostsContents', 'type': 'str'}, + 'enable_helm_operator': {'key': 'properties.enableHelmOperator', 'type': 'bool'}, + 'helm_operator_properties': {'key': 'properties.helmOperatorProperties', 'type': 'HelmOperatorProperties'}, + 'provisioning_state': {'key': 'properties.provisioningState', 'type': 'str'}, + 'compliance_status': {'key': 'properties.complianceStatus', 'type': 'ComplianceStatus'}, + } + + def __init__(self, *, system_data=None, repository_url: str=None, operator_namespace: str="default", operator_instance_name: str=None, operator_type=None, operator_params: str=None, configuration_protected_settings=None, operator_scope="cluster", ssh_known_hosts_contents: str=None, enable_helm_operator: bool=None, helm_operator_properties=None, **kwargs) -> None: + super(SourceControlConfiguration, self).__init__(system_data=system_data, **kwargs) + self.repository_url = repository_url + self.operator_namespace = operator_namespace + self.operator_instance_name = operator_instance_name + self.operator_type = operator_type + self.operator_params = operator_params + self.configuration_protected_settings = configuration_protected_settings + self.operator_scope = operator_scope + self.repository_public_key = None + self.ssh_known_hosts_contents = ssh_known_hosts_contents + self.enable_helm_operator = enable_helm_operator + self.helm_operator_properties = helm_operator_properties + self.provisioning_state = None + self.compliance_status = None + + +class SystemData(Model): + """Top level metadata + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources. + + Variables are only populated by the server, and will be ignored when + sending a request. + + :ivar created_by: A string identifier for the identity that created the + resource + :vartype created_by: str + :ivar created_by_type: The type of identity that created the resource: + user, application, managedIdentity, key + :vartype created_by_type: str + :ivar created_at: The timestamp of resource creation (UTC) + :vartype created_at: datetime + :ivar last_modified_by: A string identifier for the identity that last + modified the resource + :vartype last_modified_by: str + :ivar last_modified_by_type: The type of identity that last modified the + resource: user, application, managedIdentity, key + :vartype last_modified_by_type: str + :ivar last_modified_at: The timestamp of resource last modification (UTC) + :vartype last_modified_at: datetime + """ + + _validation = { + 'created_by': {'readonly': True}, + 'created_by_type': {'readonly': True}, + 'created_at': {'readonly': True}, + 'last_modified_by': {'readonly': True}, + 'last_modified_by_type': {'readonly': True}, + 'last_modified_at': {'readonly': True}, + } + + _attribute_map = { + 'created_by': {'key': 'createdBy', 'type': 'str'}, + 'created_by_type': {'key': 'createdByType', 'type': 'str'}, + 'created_at': {'key': 'createdAt', 'type': 'iso-8601'}, + 'last_modified_by': {'key': 'lastModifiedBy', 'type': 'str'}, + 'last_modified_by_type': {'key': 'lastModifiedByType', 'type': 'str'}, + 'last_modified_at': {'key': 'lastModifiedAt', 'type': 'iso-8601'}, + } + + def __init__(self, **kwargs) -> None: + super(SystemData, self).__init__(**kwargs) + self.created_by = None + self.created_by_type = None + self.created_at = None + self.last_modified_by = None + self.last_modified_by_type = None + self.last_modified_at = None diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_paged_models.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_paged_models.py new file mode 100644 index 00000000000..c545286fe54 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_paged_models.py @@ -0,0 +1,53 @@ +# 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 msrest.paging import Paged + + +class SourceControlConfigurationPaged(Paged): + """ + A paging container for iterating over a list of :class:`SourceControlConfiguration ` object + """ + + _attribute_map = { + 'next_link': {'key': 'nextLink', 'type': 'str'}, + 'current_page': {'key': 'value', 'type': '[SourceControlConfiguration]'} + } + + def __init__(self, *args, **kwargs): + + super(SourceControlConfigurationPaged, self).__init__(*args, **kwargs) +class ResourceProviderOperationPaged(Paged): + """ + A paging container for iterating over a list of :class:`ResourceProviderOperation ` object + """ + + _attribute_map = { + 'next_link': {'key': 'nextLink', 'type': 'str'}, + 'current_page': {'key': 'value', 'type': '[ResourceProviderOperation]'} + } + + def __init__(self, *args, **kwargs): + + super(ResourceProviderOperationPaged, self).__init__(*args, **kwargs) +class ExtensionInstancePaged(Paged): + """ + A paging container for iterating over a list of :class:`ExtensionInstance ` object + """ + + _attribute_map = { + 'next_link': {'key': 'nextLink', 'type': 'str'}, + 'current_page': {'key': 'value', 'type': '[ExtensionInstance]'} + } + + def __init__(self, *args, **kwargs): + + super(ExtensionInstancePaged, self).__init__(*args, **kwargs) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_source_control_configuration_client_enums.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_source_control_configuration_client_enums.py new file mode 100644 index 00000000000..7be14a4b085 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_source_control_configuration_client_enums.py @@ -0,0 +1,68 @@ +# 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 enum import Enum + + +class ComplianceStateType(str, Enum): + + pending = "Pending" + compliant = "Compliant" + noncompliant = "Noncompliant" + installed = "Installed" + failed = "Failed" + + +class MessageLevelType(str, Enum): + + error = "Error" + warning = "Warning" + information = "Information" + + +class OperatorType(str, Enum): + + flux = "Flux" + + +class OperatorScopeType(str, Enum): + + cluster = "cluster" + namespace = "namespace" + + +class ProvisioningStateType(str, Enum): + + accepted = "Accepted" + deleting = "Deleting" + running = "Running" + succeeded = "Succeeded" + failed = "Failed" + + +class InstallStateType(str, Enum): + + pending = "Pending" + installed = "Installed" + failed = "Failed" + + +class LevelType(str, Enum): + + error = "Error" + warning = "Warning" + information = "Information" + + +class ResourceIdentityType(str, Enum): + + system_assigned = "SystemAssigned" + none = "None" diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/__init__.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/__init__.py new file mode 100644 index 00000000000..6be16d2582d --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/__init__.py @@ -0,0 +1,20 @@ +# 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 ._source_control_configurations_operations import SourceControlConfigurationsOperations +from ._operations import Operations +from ._extensions_operations import ExtensionsOperations + +__all__ = [ + 'SourceControlConfigurationsOperations', + 'Operations', + 'ExtensionsOperations', +] diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_extensions_operations.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_extensions_operations.py new file mode 100644 index 00000000000..e99f3328816 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_extensions_operations.py @@ -0,0 +1,431 @@ +# 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. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse + +from .. import models + + +class ExtensionsOperations(object): + """ExtensionsOperations operations. + + You should not instantiate directly this class, but create a Client instance that will create it for you and attach it as attribute. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: The API version to be used with the HTTP request. Constant value: "2020-07-01-preview". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-07-01-preview" + + self.config = config + + def create( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, extension_instance, custom_headers=None, raw=False, **operation_config): + """Create a new Kubernetes Cluster Extension Instance. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param extension_instance_name: Name of an instance of the Extension. + :type extension_instance_name: str + :param extension_instance: Properties necessary to Create an Extension + Instance. + :type extension_instance: + ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstance + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: ExtensionInstance or ClientRawResponse if raw=true + :rtype: ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstance + or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.create.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(extension_instance, 'ExtensionInstance') + + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + if response.status_code == 200: + deserialized = self._deserialize('ExtensionInstance', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + create.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} + + def get( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, custom_headers=None, raw=False, **operation_config): + """Gets details of the Kubernetes Cluster Extension Instance. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param extension_instance_name: Name of an instance of the Extension. + :type extension_instance_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: ExtensionInstance or ClientRawResponse if raw=true + :rtype: ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstance + or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.get.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + if response.status_code == 200: + deserialized = self._deserialize('ExtensionInstance', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} + + def update( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, extension_instance, custom_headers=None, raw=False, **operation_config): + """Update an existing Kubernetes Cluster Extension Instance. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param extension_instance_name: Name of an instance of the Extension. + :type extension_instance_name: str + :param extension_instance: Properties to Update in the Extension + Instance. + :type extension_instance: + ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstanceUpdate + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: ExtensionInstance or ClientRawResponse if raw=true + :rtype: ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstance + or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.update.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(extension_instance, 'ExtensionInstanceUpdate') + + # Construct and send request + request = self._client.patch(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + if response.status_code == 200: + deserialized = self._deserialize('ExtensionInstance', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} + + def delete( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, custom_headers=None, raw=False, **operation_config): + """Delete a Kubernetes Cluster Extension Instance. This will cause the + Agent to Uninstall the extension instance from the cluster. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param extension_instance_name: Name of an instance of the Extension. + :type extension_instance_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: None or ClientRawResponse if raw=true + :rtype: None or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.delete.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 204]: + raise models.ErrorResponseException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + delete.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} + + def list( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, custom_headers=None, raw=False, **operation_config): + """List all Source Control Configurations. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: An iterator like instance of ExtensionInstance + :rtype: + ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstancePaged[~azure.mgmt.kubernetesconfiguration.models.ExtensionInstance] + :raises: + :class:`ErrorResponseException` + """ + def prepare_request(next_link=None): + if not next_link: + # Construct URL + url = self.list.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + else: + url = next_link + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + return request + + def internal_paging(next_link=None): + request = prepare_request(next_link) + + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + return response + + # Deserialize response + header_dict = None + if raw: + header_dict = {} + deserialized = models.ExtensionInstancePaged(internal_paging, self._deserialize.dependencies, header_dict) + + return deserialized + list.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions'} diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_operations.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_operations.py new file mode 100644 index 00000000000..245a93c8294 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_operations.py @@ -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. +# +# Code generated by Microsoft (R) AutoRest Code Generator. +# Changes may cause incorrect behavior and will be lost if the code is +# regenerated. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse + +from .. import models + + +class Operations(object): + """Operations operations. + + You should not instantiate directly this class, but create a Client instance that will create it for you and attach it as attribute. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: The API version to be used with the HTTP request. Constant value: "2020-07-01-preview". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-07-01-preview" + + self.config = config + + def list( + self, custom_headers=None, raw=False, **operation_config): + """List all the available operations the KubernetesConfiguration resource + provider supports. + + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: An iterator like instance of ResourceProviderOperation + :rtype: + ~azure.mgmt.kubernetesconfiguration.models.ResourceProviderOperationPaged[~azure.mgmt.kubernetesconfiguration.models.ResourceProviderOperation] + :raises: + :class:`ErrorResponseException` + """ + def prepare_request(next_link=None): + if not next_link: + # Construct URL + url = self.list.metadata['url'] + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + else: + url = next_link + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + return request + + def internal_paging(next_link=None): + request = prepare_request(next_link) + + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + return response + + # Deserialize response + header_dict = None + if raw: + header_dict = {} + deserialized = models.ResourceProviderOperationPaged(internal_paging, self._deserialize.dependencies, header_dict) + + return deserialized + list.metadata = {'url': '/providers/Microsoft.KubernetesConfiguration/operations'} diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_source_control_configurations_operations.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_source_control_configurations_operations.py new file mode 100644 index 00000000000..83a49e32146 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_source_control_configurations_operations.py @@ -0,0 +1,386 @@ +# 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. +# -------------------------------------------------------------------------- + +import uuid +from msrest.pipeline import ClientRawResponse +from msrest.polling import LROPoller, NoPolling +from msrestazure.polling.arm_polling import ARMPolling + +from .. import models + + +class SourceControlConfigurationsOperations(object): + """SourceControlConfigurationsOperations operations. + + You should not instantiate directly this class, but create a Client instance that will create it for you and attach it as attribute. + + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + :ivar api_version: The API version to be used with the HTTP request. Constant value: "2020-07-01-preview". + """ + + models = models + + def __init__(self, client, config, serializer, deserializer): + + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self.api_version = "2020-07-01-preview" + + self.config = config + + def get( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, source_control_configuration_name, custom_headers=None, raw=False, **operation_config): + """Gets details of the Source Control Configuration. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param source_control_configuration_name: Name of the Source Control + Configuration. + :type source_control_configuration_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: SourceControlConfiguration or ClientRawResponse if raw=true + :rtype: + ~azure.mgmt.kubernetesconfiguration.models.SourceControlConfiguration + or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.get.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'sourceControlConfigurationName': self._serialize.url("source_control_configuration_name", source_control_configuration_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + if response.status_code == 200: + deserialized = self._deserialize('SourceControlConfiguration', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + get.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations/{sourceControlConfigurationName}'} + + def create_or_update( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, source_control_configuration_name, source_control_configuration, custom_headers=None, raw=False, **operation_config): + """Create a new Kubernetes Source Control Configuration. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param source_control_configuration_name: Name of the Source Control + Configuration. + :type source_control_configuration_name: str + :param source_control_configuration: Properties necessary to Create + KubernetesConfiguration. + :type source_control_configuration: + ~azure.mgmt.kubernetesconfiguration.models.SourceControlConfiguration + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: SourceControlConfiguration or ClientRawResponse if raw=true + :rtype: + ~azure.mgmt.kubernetesconfiguration.models.SourceControlConfiguration + or ~msrest.pipeline.ClientRawResponse + :raises: + :class:`ErrorResponseException` + """ + # Construct URL + url = self.create_or_update.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'sourceControlConfigurationName': self._serialize.url("source_control_configuration_name", source_control_configuration_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + header_parameters['Content-Type'] = 'application/json; charset=utf-8' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct body + body_content = self._serialize.body(source_control_configuration, 'SourceControlConfiguration') + + # Construct and send request + request = self._client.put(url, query_parameters, header_parameters, body_content) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 201]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + if response.status_code == 200: + deserialized = self._deserialize('SourceControlConfiguration', response) + if response.status_code == 201: + deserialized = self._deserialize('SourceControlConfiguration', response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + create_or_update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations/{sourceControlConfigurationName}'} + + + def _delete_initial( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, source_control_configuration_name, custom_headers=None, raw=False, **operation_config): + # Construct URL + url = self.delete.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'sourceControlConfigurationName': self._serialize.url("source_control_configuration_name", source_control_configuration_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + # Construct headers + header_parameters = {} + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.delete(url, query_parameters, header_parameters) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 204]: + raise models.ErrorResponseException(self._deserialize, response) + + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + + def delete( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, source_control_configuration_name, custom_headers=None, raw=False, polling=True, **operation_config): + """This will delete the YAML file used to set up the Source control + configuration, thus stopping future sync from the source repo. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param source_control_configuration_name: Name of the Source Control + Configuration. + :type source_control_configuration_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: The poller return type is ClientRawResponse, the + direct response alongside the deserialized response + :param polling: True for ARMPolling, False for no polling, or a + polling object for personal polling strategy + :return: An instance of LROPoller that returns None or + ClientRawResponse if raw==True + :rtype: ~msrestazure.azure_operation.AzureOperationPoller[None] or + ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[None]] + :raises: + :class:`ErrorResponseException` + """ + raw_result = self._delete_initial( + resource_group_name=resource_group_name, + cluster_rp=cluster_rp, + cluster_resource_name=cluster_resource_name, + cluster_name=cluster_name, + source_control_configuration_name=source_control_configuration_name, + custom_headers=custom_headers, + raw=True, + **operation_config + ) + + def get_long_running_output(response): + if raw: + client_raw_response = ClientRawResponse(None, response) + return client_raw_response + + lro_delay = operation_config.get( + 'long_running_operation_timeout', + self.config.long_running_operation_timeout) + if polling is True: polling_method = ARMPolling(lro_delay, **operation_config) + elif polling is False: polling_method = NoPolling() + else: polling_method = polling + return LROPoller(self._client, raw_result, get_long_running_output, polling_method) + delete.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations/{sourceControlConfigurationName}'} + + def list( + self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, custom_headers=None, raw=False, **operation_config): + """List all Source Control Configurations. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either + Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes + (for OnPrem K8S clusters). Possible values include: + 'Microsoft.ContainerService', 'Microsoft.Kubernetes' + :type cluster_rp: str + :param cluster_resource_name: The Kubernetes cluster resource name - + either managedClusters (for AKS clusters) or connectedClusters (for + OnPrem K8S clusters). Possible values include: 'managedClusters', + 'connectedClusters' + :type cluster_resource_name: str + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: An iterator like instance of SourceControlConfiguration + :rtype: + ~azure.mgmt.kubernetesconfiguration.models.SourceControlConfigurationPaged[~azure.mgmt.kubernetesconfiguration.models.SourceControlConfiguration] + :raises: + :class:`ErrorResponseException` + """ + def prepare_request(next_link=None): + if not next_link: + # Construct URL + url = self.list.metadata['url'] + path_format_arguments = { + 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str') + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + + else: + url = next_link + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters['Accept'] = 'application/json' + if self.config.generate_client_request_id: + header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) + if custom_headers: + header_parameters.update(custom_headers) + if self.config.accept_language is not None: + header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') + + # Construct and send request + request = self._client.get(url, query_parameters, header_parameters) + return request + + def internal_paging(next_link=None): + request = prepare_request(next_link) + + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200]: + raise models.ErrorResponseException(self._deserialize, response) + + return response + + # Deserialize response + header_dict = None + if raw: + header_dict = {} + deserialized = models.SourceControlConfigurationPaged(internal_paging, self._deserialize.dependencies, header_dict) + + return deserialized + list.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations'} diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/version.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/version.py new file mode 100644 index 00000000000..3e682bbd5fb --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/version.py @@ -0,0 +1,13 @@ +# 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. +# -------------------------------------------------------------------------- + +VERSION = "0.3.0" + diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py new file mode 100644 index 00000000000..d3d491fd48c --- /dev/null +++ b/src/k8s-extension/setup.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + + +from codecs import open +from setuptools import setup, find_packages +import azext_k8s_extension._consts as consts +try: + from azure_bdist_wheel import cmdclass +except ImportError: + from distutils import log as logger + logger.warn("Wheel is not available, disabling bdist_wheel hook") + +# The full list of classifiers is available at +# https://pypi.python.org/pypi?%3Aaction=list_classifiers +CLASSIFIERS = [ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'License :: OSI Approved :: MIT License', +] + +# TODO: Add any additional SDK dependencies here +DEPENDENCIES = [] + +with open('README.rst', 'r', encoding='utf-8') as f: + README = f.read() +with open('HISTORY.rst', 'r', encoding='utf-8') as f: + HISTORY = f.read() + +setup( + name=consts.EXTENSION_NAME, + version=consts.VERSION, + description='Microsoft Azure Command-Line Tools K8s-extension Extension', + # TODO: Update author and email, if applicable + author='Microsoft Corporation', + author_email='azpycli@microsoft.com', + # TODO: consider pointing directly to your source code instead of the generic repo + url='https://github.com/Azure/azure-cli-extensions', + long_description=README + '\n\n' + HISTORY, + license='MIT', + classifiers=CLASSIFIERS, + packages=find_packages(), + install_requires=DEPENDENCIES, + package_data={'azext_k8s_extension': ['azext_metadata.json']}, +) From 7f79cfb16fc8e4ec057d3b64f55b415894db2a45 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Wed, 17 Mar 2021 17:55:01 -0700 Subject: [PATCH 30/86] Remove custom pipelines file --- k8s-custom-pipelines.yml | 356 --------------------------------------- 1 file changed, 356 deletions(-) delete mode 100644 k8s-custom-pipelines.yml diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml deleted file mode 100644 index f4e2984ddf2..00000000000 --- a/k8s-custom-pipelines.yml +++ /dev/null @@ -1,356 +0,0 @@ -resources: - repositories: - - repository: K8sPartnerExtensionTest - type: git - endpoint: AzureReposConnection - name: One/compute-HybridMgmt-K8sPartnerExtensionTest - -trigger: - batch: true - branches: - include: - - k8s-extension/public - - k8s-extension/private -pr: - branches: - include: - - k8s-extension/public - - k8s-extension/private - -stages: -- stage: BuildTestPublishExtension - displayName: "Build, Test, and Publish Extension" - variables: - K8S_EXTENSION_REPO_PATH: $(Agent.BuildDirectory)/s/compute-HybridMgmt-K8sPartnerExtensionTest - CLI_REPO_PATH: $(Agent.BuildDirectory)/s/azure-cli-extensions - SUBSCRIPTION_ID: "15c06b1b-01d6-407b-bb21-740b8617dea3" - RESOURCE_GROUP: "K8sPartnerExtensionTest" - BASE_CLUSTER_NAME: "k8s-extension-cluster" - IS_PRIVATE_BRANCH: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/k8s-extension/private'), eq(variables['System.PullRequest.TargetBranch'], 'refs/heads/k8s-extension/private'))] - - EXTENSION_NAME: "k8s-extension" - EXTENSION_FILE_NAME: "k8s_extension" - jobs: - - job: K8sExtensionTestSuite - displayName: "Run the Test Suite" - pool: - vmImage: 'ubuntu-16.04' - steps: - - checkout: self - - checkout: K8sPartnerExtensionTest - - bash: | - echo "Installing helm3" - curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 - chmod 700 get_helm.sh - ./get_helm.sh - - echo "Installing kubectl" - curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl" - chmod +x ./kubectl - sudo mv ./kubectl /usr/local/bin/kubectl - kubectl version --client - displayName: "Setup the VM with helm3 and kubectl" - - task: UsePythonVersion@0 - displayName: 'Use Python 3.6' - inputs: - versionSpec: 3.6 - - bash: | - set -ev - echo "Building extension ${EXTENSION_NAME}..." - - # prepare and activate virtualenv - pip install virtualenv - python3 -m venv env/ - source env/bin/activate - - # clone azure-cli - pip install azdev - - ls $(CLI_REPO_PATH) - - azdev --version - azdev setup -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) - azdev extension build $(EXTENSION_NAME) - - workingDirectory: $(CLI_REPO_PATH) - displayName: "Setup and Build Extension with azdev" - - - bash: | - K8S_EXTENSION_VERSION=$(ls ${EXTENSION_FILE_NAME}* | cut -d "-" -f2) - echo "##vso[task.setvariable variable=K8S_EXTENSION_VERSION]$K8S_EXTENSION_VERSION" - cp * $(K8S_EXTENSION_REPO_PATH)/bin - workingDirectory: $(CLI_REPO_PATH)/dist - displayName: "Copy the Built .whl to Extension Test Path" - - - bash: | - RAND_STR=$RANDOM - AKS_CLUSTER_NAME="${BASE_CLUSTER_NAME}-${RAND_STR}-aks" - ARC_CLUSTER_NAME="${BASE_CLUSTER_NAME}-${RAND_STR}-arc" - - JSON_STRING=$(jq -n \ - --arg SUB_ID "$SUBSCRIPTION_ID" \ - --arg RG "$RESOURCE_GROUP" \ - --arg AKS_CLUSTER_NAME "$AKS_CLUSTER_NAME" \ - --arg ARC_CLUSTER_NAME "$ARC_CLUSTER_NAME" \ - --arg K8S_EXTENSION_VERSION "$K8S_EXTENSION_VERSION" \ - '{subscriptionId: $SUB_ID, resourceGroup: $RG, aksClusterName: $AKS_CLUSTER_NAME, arcClusterName: $ARC_CLUSTER_NAME, extensionVersion: {"k8s-extension": $K8S_EXTENSION_VERSION, connectedk8s: "1.0.0"}}') - echo $JSON_STRING > settings.json - cat settings.json - workingDirectory: $(K8S_EXTENSION_REPO_PATH) - displayName: "Generate a settings.json file" - - - bash : | - echo "Downloading the kind script" - curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.9.0/kind-linux-amd64 - chmod +x ./kind - ./kind create cluster - displayName: "Create and Start the Kind cluster" - - - task: AzureCLI@2 - displayName: Bootstrap - inputs: - azureSubscription: AzureResourceConnection - scriptType: pscore - scriptLocation: inlineScript - inlineScript: | - .\Bootstrap.ps1 -CI - workingDirectory: $(K8S_EXTENSION_REPO_PATH) - - - task: AzureCLI@2 - displayName: Run the Test Suite Public Extensions Only - inputs: - azureSubscription: AzureResourceConnection - scriptType: pscore - scriptLocation: inlineScript - inlineScript: | - .\Test.ps1 -CI -ExtensionType Public -OnlyPublicTests - workingDirectory: $(K8S_EXTENSION_REPO_PATH) - continueOnError: true - condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'False')) - - - task: AzureCLI@2 - displayName: Run the Test Suite on Private + Public Extensions - inputs: - azureSubscription: AzureResourceConnection - scriptType: pscore - scriptLocation: inlineScript - inlineScript: | - .\Test.ps1 -CI -ExtensionType Public - workingDirectory: $(K8S_EXTENSION_REPO_PATH) - continueOnError: true - condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'True')) - - - task: PublishTestResults@2 - inputs: - testResultsFormat: 'JUnit' - testResultsFiles: '**/TestResults.xml' - failTaskOnFailedTests: true - condition: succeededOrFailed() - - - task: AzureCLI@2 - displayName: Cleanup - inputs: - azureSubscription: AzureResourceConnection - scriptType: pscore - scriptLocation: inlineScript - inlineScript: | - .\Cleanup.ps1 -CI - workingDirectory: $(K8S_EXTENSION_REPO_PATH) - condition: succeededOrFailed() - - - job: BuildPublishExtension - pool: - vmImage: 'ubuntu-16.04' - displayName: "Build and Publish the Extension Artifact" - variables: - CLI_REPO_PATH: $(Agent.BuildDirectory)/s - steps: - - bash: | - echo "Using the private preview of k8s-extension to build..." - - cp $(CLI_REPO_PATH)/src/k8s-extension $(CLI_REPO_PATH)/src/k8s-extension-private -r - cp $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension/_consts_private.py $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension/_consts.py - - EXTENSION_NAME="k8s-extension-private" - EXTENSION_FILE_NAME="k8s_extension_private" - - echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" - echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" - condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'True')) - displayName: "Copy Files, Set Variables for k8s-extension-private" - - bash: | - echo "Using the public version of k8s-extension to build..." - - EXTENSION_NAME="k8s-extension" - EXTENSION_FILE_NAME="k8s_extension" - - echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" - echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" - condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'False')) - displayName: "Copy Files, Set Variables for k8s-extension" - - task: UsePythonVersion@0 - displayName: 'Use Python 3.6' - inputs: - versionSpec: 3.6 - - bash: | - set -ev - echo "Building extension ${EXTENSION_NAME}..." - - # prepare and activate virtualenv - pip install virtualenv - python3 -m venv env/ - source env/bin/activate - - # clone azure-cli - pip install azdev - - ls $(CLI_REPO_PATH) - - azdev --version - azdev setup -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) - azdev extension build $(EXTENSION_NAME) - workingDirectory: $(CLI_REPO_PATH) - displayName: "Setup and Build Extension with azdev" - - task: PublishBuildArtifacts@1 - inputs: - pathToPublish: $(CLI_REPO_PATH)/dist - -- stage: AzureCLIOfficial - displayName: "Azure Official CLI Code Checks" - dependsOn: [] - jobs: - - job: CheckLicenseHeader - displayName: "Check License" - pool: - vmImage: 'ubuntu-16.04' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.6' - inputs: - versionSpec: 3.6 - - bash: | - set -ev - - # prepare and activate virtualenv - python -m venv env/ - - chmod +x ./env/bin/activate - source ./env/bin/activate - - # clone azure-cli - git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli - - pip install -q azdev - - azdev setup -c ../azure-cli -r ./ - - azdev --version - az --version - - azdev verify license - - - job: StaticAnalysis - displayName: "Static Analysis" - pool: - vmImage: 'ubuntu-16.04' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.6' - inputs: - versionSpec: 3.6 - - bash: pip install wheel==0.30.0 pylint==1.9.5 flake8==3.5.0 requests - displayName: 'Install wheel, pylint, flake8, requests' - - bash: python scripts/ci/source_code_static_analysis.py - displayName: "Static Analysis" - - - job: IndexVerify - displayName: "Verify Extensions Index" - pool: - vmImage: 'ubuntu-16.04' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.7' - inputs: - versionSpec: 3.7 - - bash: | - #!/usr/bin/env bash - set -ev - pip install wheel==0.30.0 requests packaging - export CI="ADO" - python ./scripts/ci/test_index.py -v - displayName: "Verify Extensions Index" - - - job: SourceTests - displayName: "Integration Tests, Build Tests" - pool: - vmImage: 'ubuntu-16.04' - strategy: - matrix: - Python36: - python.version: '3.6' - Python38: - python.version: '3.8' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python $(python.version)' - inputs: - versionSpec: '$(python.version)' - - bash: pip install wheel==0.30.0 - displayName: 'Install wheel==0.30.0' - - bash: ./scripts/ci/test_source.sh - displayName: 'Run integration test and build test' - env: - ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) - ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) - - - job: LintModifiedExtensions - displayName: "CLI Linter on Modified Extensions" - pool: - vmImage: 'ubuntu-16.04' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.6' - inputs: - versionSpec: 3.6 - - bash: | - set -ev - - # prepare and activate virtualenv - pip install virtualenv - python -m virtualenv venv/ - source ./venv/bin/activate - - # clone azure-cli - git clone --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli - - pip install azdev - - azdev --version - - azdev setup -c ../azure-cli -r ./ -e k8s-extension - - # overwrite the default AZURE_EXTENSION_DIR set by ADO - AZURE_EXTENSION_DIR=~/.azure/cliextensions az --version - - AZURE_EXTENSION_DIR=~/.azure/cliextensions azdev linter --include-whl-extensions k8s-extension - displayName: "CLI Linter on Modified Extension" - env: - ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) - ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) - - - job: IndexRefDocVerify - displayName: "Verify Ref Docs" - pool: - vmImage: 'ubuntu-16.04' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.7' - inputs: - versionSpec: 3.7 - - bash: pip install wheel==0.30.0 - displayName: 'Install wheel==0.30.0' - - task: Bash@3 - displayName: "Verify Extension Ref Docs" - inputs: - targetType: 'filePath' - filePath: scripts/ci/test_index_ref_doc.sh From 9f06b4919cb089ac6ee1afe3d30de053965f5701 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Wed, 24 Mar 2021 09:26:47 -0700 Subject: [PATCH 31/86] Update extension description, remove private const --- src/k8s-extension/README.rst | 54 +++++++++++++++++++ .../azext_k8s_extension/_consts_private.py | 8 --- .../azext_k8s_extension/_params.py | 5 +- 3 files changed, 57 insertions(+), 10 deletions(-) delete mode 100644 src/k8s-extension/azext_k8s_extension/_consts_private.py diff --git a/src/k8s-extension/README.rst b/src/k8s-extension/README.rst index e91e1b13229..5d4694e14cb 100644 --- a/src/k8s-extension/README.rst +++ b/src/k8s-extension/README.rst @@ -3,3 +3,57 @@ Microsoft Azure CLI 'k8s-extension' Extension This package is for the 'k8s-extension' extension. i.e. 'az k8s-extension' + +### How to use ### +Install this extension using the below CLI command +``` +az extension add --name k8s-extension +``` + +### Included Features +#### Kubernetes Extensions: +Kubernetes Extensions: [more info](https://docs.microsoft.com/en-us/azure/kubernetessconfiguration/)\ +*Examples:* + +##### Create a KubernetesExtension +``` +az k8s-extension create \ + --resource-group groupName \ + --cluster-name clusterName \ + --cluster-type clusterType \ + --name extensionName \ + --extension-type extensionType \ + --scope scopeType \ + --release-train releaseTrain \ + --version versionNumber \ + --auto-upgrade-minor-version autoUpgrade \ + --configuration-settings exampleSetting=exampleValue \ +``` + +##### Get a KubernetesExtension +``` +az k8s-extension show \ + --resource-group groupName \ + --cluster-name clusterName \ + --cluster-type clusterType \ + --name extensionName +``` + +##### Delete a KubernetesExtension +``` +az k8s-extension delete \ + --resource-group groupName \ + --cluster-name clusterName \ + --cluster-type clusterType \ + --name extensionName +``` + +##### List all KubernetesExtension of a cluster +``` +az k8s-extension list \ + --resource-group groupName \ + --cluster-name clusterName \ + --cluster-type clusterType +``` + +If you have issues, please give feedback by opening an issue at https://github.com/Azure/azure-cli-extensions/issues. \ No newline at end of file diff --git a/src/k8s-extension/azext_k8s_extension/_consts_private.py b/src/k8s-extension/azext_k8s_extension/_consts_private.py deleted file mode 100644 index bc4c8a2b694..00000000000 --- a/src/k8s-extension/azext_k8s_extension/_consts_private.py +++ /dev/null @@ -1,8 +0,0 @@ -# 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. -# -------------------------------------------------------------------------------------------- - -EXTENSION_NAME = 'k8s-extension-private' -VERSION = "0.1PP.14" diff --git a/src/k8s-extension/azext_k8s_extension/_params.py b/src/k8s-extension/azext_k8s_extension/_params.py index d96fe3c2ba7..3bbc6056a2e 100644 --- a/src/k8s-extension/azext_k8s_extension/_params.py +++ b/src/k8s-extension/azext_k8s_extension/_params.py @@ -45,6 +45,9 @@ def load_arguments(self, _): arg_group="Version", help='Specify the version to install for the extension instance if' ' --auto-upgrade-minor-version is not enabled.') + c.argument('release_train', + arg_group="Version", + help='Specify the release train for the extension type.') c.argument('configuration_settings', arg_group="Configuration", options_list=['--configuration-settings', '--config'], @@ -67,8 +70,6 @@ def load_arguments(self, _): help='JSON file path for configuration-protected-settings') c.argument('release_namespace', help='Specify the namespace to install the extension release.') - c.argument('release_train', - help='Specify the release train for the extension type.') c.argument('target_namespace', help='Specify the target namespace to install to for the extension instance. This' ' parameter is required if extension scope is set to \'namespace\'') From eb4c58bb30928caef9fbff1778e68f326ef0d116 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Wed, 24 Mar 2021 13:06:14 -0700 Subject: [PATCH 32/86] Update pipeline file --- k8s-custom-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index f4e2984ddf2..a603608b138 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -26,7 +26,7 @@ stages: SUBSCRIPTION_ID: "15c06b1b-01d6-407b-bb21-740b8617dea3" RESOURCE_GROUP: "K8sPartnerExtensionTest" BASE_CLUSTER_NAME: "k8s-extension-cluster" - IS_PRIVATE_BRANCH: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/k8s-extension/private'), eq(variables['System.PullRequest.TargetBranch'], 'refs/heads/k8s-extension/private'))] + IS_PRIVATE_BRANCH: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/k8s-extension/private'), eq(variables['System.PullRequest.TargetBranch'], 'k8s-extension/private'))] EXTENSION_NAME: "k8s-extension" EXTENSION_FILE_NAME: "k8s_extension" From 3290f6e32cdd61b370f475055eb07d77d09753fe Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Fri, 2 Apr 2021 13:35:49 -0700 Subject: [PATCH 33/86] Disable refs docs --- k8s-custom-pipelines.yml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index a603608b138..a3304668086 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -337,20 +337,3 @@ stages: env: ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) - - - job: IndexRefDocVerify - displayName: "Verify Ref Docs" - pool: - vmImage: 'ubuntu-16.04' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.7' - inputs: - versionSpec: 3.7 - - bash: pip install wheel==0.30.0 - displayName: 'Install wheel==0.30.0' - - task: Bash@3 - displayName: "Verify Extension Ref Docs" - inputs: - targetType: 'filePath' - filePath: scripts/ci/test_index_ref_doc.sh From 22c8e9247ed5ae5fa3cb0f273e69dd9e2053fe34 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Fri, 2 Apr 2021 16:15:58 -0700 Subject: [PATCH 34/86] Update to include better create warning logs and remove update context (#20) * Update to include better create warning logs and remove update context * Remove help text for update * Fix spelling error * Update message --- src/k8s-extension/HISTORY.rst | 6 ++++++ src/k8s-extension/azext_k8s_extension/_consts.py | 2 +- src/k8s-extension/azext_k8s_extension/_help.py | 5 ----- src/k8s-extension/azext_k8s_extension/commands.py | 1 - .../azext_k8s_extension/partner_extensions/AzureDefender.py | 3 ++- .../partner_extensions/ContainerInsights.py | 3 ++- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index 54c1e375f6f..d20160a2199 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -3,6 +3,12 @@ Release History =============== +0.2.1 +++++++++++++++++++ + +* Remove `k8s-extension update` until PATCH is supported +* Improved logging for overwriting extension name with default + 0.2.0 ++++++++++++++++++ diff --git a/src/k8s-extension/azext_k8s_extension/_consts.py b/src/k8s-extension/azext_k8s_extension/_consts.py index d0fdaf7775f..144e9450012 100644 --- a/src/k8s-extension/azext_k8s_extension/_consts.py +++ b/src/k8s-extension/azext_k8s_extension/_consts.py @@ -5,4 +5,4 @@ # -------------------------------------------------------------------------------------------- EXTENSION_NAME = 'k8s-extension' -VERSION = "0.2.0" +VERSION = "0.2.1" diff --git a/src/k8s-extension/azext_k8s_extension/_help.py b/src/k8s-extension/azext_k8s_extension/_help.py index 64e4be612ea..562f0af58aa 100644 --- a/src/k8s-extension/azext_k8s_extension/_help.py +++ b/src/k8s-extension/azext_k8s_extension/_help.py @@ -32,8 +32,3 @@ type: command short-summary: Show details of a K8s-extension. """ - -helps[f'{consts.EXTENSION_NAME} update'] = """ - type: command - short-summary: Update a K8s-extension. -""" diff --git a/src/k8s-extension/azext_k8s_extension/commands.py b/src/k8s-extension/azext_k8s_extension/commands.py index 931662814c0..3a823177d1c 100644 --- a/src/k8s-extension/azext_k8s_extension/commands.py +++ b/src/k8s-extension/azext_k8s_extension/commands.py @@ -20,7 +20,6 @@ def load_command_table(self, _): is_preview=True) \ as g: g.custom_command('create', 'create_k8s_extension') - g.custom_command('update', 'update_k8s_extension') g.custom_command('delete', 'delete_k8s_extension', confirmation=True) g.custom_command('list', 'list_k8s_extension', table_transformer=k8s_extension_list_table_format) g.custom_show_command('show', 'show_k8s_extension', table_transformer=k8s_extension_show_table_format) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py index dcd2853affc..3124faec4c1 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py @@ -41,7 +41,8 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t is_ci_extension_type = False logger.warning('Ignoring name, release-namespace and scope parameters since %s ' - 'only supports cluster scope and single instance of this extension', extension_type) + 'only supports cluster scope and single instance of this extension.', extension_type) + logger.warning("Defaulting to extension name '%s' and release-namespace '%s'", name, release_namespace) _get_container_insights_settings(cmd, resource_group_name, cluster_name, configuration_settings, configuration_protected_settings, is_ci_extension_type) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py index 8c678b34915..eade4256205 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py @@ -53,7 +53,8 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t is_ci_extension_type = True logger.warning('Ignoring name, release-namespace and scope parameters since %s ' - 'only supports cluster scope and single instance of this extension', extension_type) + 'only supports cluster scope and single instance of this extension.', extension_type) + logger.warning("Defaulting to extension name '%s' and release-namespace '%s'", name, release_namespace) _get_container_insights_settings(cmd, resource_group_name, cluster_name, configuration_settings, configuration_protected_settings, is_ci_extension_type) From df82dd871408b5b131ab0e8e1714ead98ff20df6 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Wed, 14 Apr 2021 16:24:52 -0700 Subject: [PATCH 35/86] Fix k8s-extension conflict with private version --- k8s-custom-pipelines.yml | 4 +++- .../azext_k8s_extension/__init__.py | 11 ++++++----- .../azext_k8s_extension/_client_factory.py | 2 +- src/k8s-extension/azext_k8s_extension/_help.py | 2 +- .../azext_k8s_extension/_params.py | 4 ++-- .../azext_k8s_extension/commands.py | 8 ++++---- .../{_consts.py => consts.py} | 2 +- .../azext_k8s_extension/custom.py | 18 +++++++++--------- .../partner_extensions/AzureDefender.py | 12 ++++++------ .../partner_extensions/Cassandra.py | 12 ++++++------ .../partner_extensions/ContainerInsights.py | 12 ++++++------ .../partner_extensions/DefaultExtension.py | 12 ++++++------ .../PartnerExtensionModel.py | 4 ++-- src/k8s-extension/setup.py | 8 +++++--- 14 files changed, 58 insertions(+), 53 deletions(-) rename src/k8s-extension/azext_k8s_extension/{_consts.py => consts.py} (88%) diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index a3304668086..451d1e08889 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -169,7 +169,9 @@ stages: echo "Using the private preview of k8s-extension to build..." cp $(CLI_REPO_PATH)/src/k8s-extension $(CLI_REPO_PATH)/src/k8s-extension-private -r - cp $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension/_consts_private.py $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension/_consts.py + mv $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension_private + cp $(CLI_REPO_PATH)/src/k8s-extension-private/setup_private.py $(CLI_REPO_PATH)/src/k8s-extension-private/setup.py + cp $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension_private/_consts_private.py $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension_private/_consts.py EXTENSION_NAME="k8s-extension-private" EXTENSION_FILE_NAME="k8s_extension_private" diff --git a/src/k8s-extension/azext_k8s_extension/__init__.py b/src/k8s-extension/azext_k8s_extension/__init__.py index e2301227d45..77d01d5f327 100644 --- a/src/k8s-extension/azext_k8s_extension/__init__.py +++ b/src/k8s-extension/azext_k8s_extension/__init__.py @@ -4,28 +4,29 @@ # -------------------------------------------------------------------------------------------- from azure.cli.core import AzCommandsLoader +from . import consts -from azext_k8s_extension._help import helps # pylint: disable=unused-import +from ._help import helps # pylint: disable=unused-import class K8sExtensionCommandsLoader(AzCommandsLoader): def __init__(self, cli_ctx=None): from azure.cli.core.commands import CliCommandType - from azext_k8s_extension._client_factory import cf_k8s_extension + from ._client_factory import cf_k8s_extension k8s_extension_custom = CliCommandType( - operations_tmpl='azext_k8s_extension.custom#{}', + operations_tmpl=consts.EXTENSION_PACKAGE_NAME+'.custom#{}', client_factory=cf_k8s_extension) super(K8sExtensionCommandsLoader, self).__init__(cli_ctx=cli_ctx, custom_command_type=k8s_extension_custom) def load_command_table(self, args): - from azext_k8s_extension.commands import load_command_table + from .commands import load_command_table load_command_table(self, args) return self.command_table def load_arguments(self, command): - from azext_k8s_extension._params import load_arguments + from ._params import load_arguments load_arguments(self, command) diff --git a/src/k8s-extension/azext_k8s_extension/_client_factory.py b/src/k8s-extension/azext_k8s_extension/_client_factory.py index 1a9a10c2615..6271246245e 100644 --- a/src/k8s-extension/azext_k8s_extension/_client_factory.py +++ b/src/k8s-extension/azext_k8s_extension/_client_factory.py @@ -8,7 +8,7 @@ def cf_k8s_extension(cli_ctx, *_): - from azext_k8s_extension.vendored_sdks import SourceControlConfigurationClient + from .vendored_sdks import SourceControlConfigurationClient return get_mgmt_service_client(cli_ctx, SourceControlConfigurationClient) diff --git a/src/k8s-extension/azext_k8s_extension/_help.py b/src/k8s-extension/azext_k8s_extension/_help.py index 562f0af58aa..862bf4572ac 100644 --- a/src/k8s-extension/azext_k8s_extension/_help.py +++ b/src/k8s-extension/azext_k8s_extension/_help.py @@ -5,7 +5,7 @@ # -------------------------------------------------------------------------------------------- from knack.help_files import helps # pylint: disable=unused-import -import azext_k8s_extension._consts as consts +from . import consts helps[f'{consts.EXTENSION_NAME}'] = """ diff --git a/src/k8s-extension/azext_k8s_extension/_params.py b/src/k8s-extension/azext_k8s_extension/_params.py index 3bbc6056a2e..52b680a69f7 100644 --- a/src/k8s-extension/azext_k8s_extension/_params.py +++ b/src/k8s-extension/azext_k8s_extension/_params.py @@ -9,9 +9,9 @@ tags_type ) from azure.cli.core.commands.validators import get_default_location_from_resource_group -import azext_k8s_extension._consts as consts +from . import consts -from azext_k8s_extension.action import ( +from .action import ( AddConfigurationSettings, AddConfigurationProtectedSettings ) diff --git a/src/k8s-extension/azext_k8s_extension/commands.py b/src/k8s-extension/azext_k8s_extension/commands.py index 3a823177d1c..d576ed4fb8e 100644 --- a/src/k8s-extension/azext_k8s_extension/commands.py +++ b/src/k8s-extension/azext_k8s_extension/commands.py @@ -5,15 +5,15 @@ # pylint: disable=line-too-long from azure.cli.core.commands import CliCommandType -from azext_k8s_extension._client_factory import (cf_k8s_extension, cf_k8s_extension_operation) -from azext_k8s_extension._format import k8s_extension_list_table_format, k8s_extension_show_table_format -import azext_k8s_extension._consts as consts +from ._client_factory import (cf_k8s_extension, cf_k8s_extension_operation) +from ._format import k8s_extension_list_table_format, k8s_extension_show_table_format +from . import consts def load_command_table(self, _): k8s_extension_sdk = CliCommandType( - operations_tmpl='azext_k8s_extension.vendored_sdks.operations#K8sExtensionsOperations.{}', + operations_tmpl=consts.EXTENSION_PACKAGE_NAME+'.vendored_sdks.operations#K8sExtensionsOperations.{}', client_factory=cf_k8s_extension) with self.command_group(consts.EXTENSION_NAME, k8s_extension_sdk, client_factory=cf_k8s_extension_operation, diff --git a/src/k8s-extension/azext_k8s_extension/_consts.py b/src/k8s-extension/azext_k8s_extension/consts.py similarity index 88% rename from src/k8s-extension/azext_k8s_extension/_consts.py rename to src/k8s-extension/azext_k8s_extension/consts.py index 144e9450012..082fee5def8 100644 --- a/src/k8s-extension/azext_k8s_extension/_consts.py +++ b/src/k8s-extension/azext_k8s_extension/consts.py @@ -5,4 +5,4 @@ # -------------------------------------------------------------------------------------------- EXTENSION_NAME = 'k8s-extension' -VERSION = "0.2.1" +EXTENSION_PACKAGE_NAME = "azext_k8s_extension" \ No newline at end of file diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index ba9dbfce501..5e314269aaf 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -13,15 +13,15 @@ from azure.cli.core.azclierror import ResourceNotFoundError, MutuallyExclusiveArgumentError, \ InvalidArgumentValueError, CommandNotFoundError, RequiredArgumentMissingError from azure.cli.core.commands.client_factory import get_subscription_id -from azext_k8s_extension.vendored_sdks.models import ConfigurationIdentity -from azext_k8s_extension.vendored_sdks.models import ErrorResponseException -from azext_k8s_extension.vendored_sdks.models import Scope +from .vendored_sdks.models import ConfigurationIdentity +from .vendored_sdks.models import ErrorResponseException +from .vendored_sdks.models import Scope -from azext_k8s_extension.partner_extensions.ContainerInsights import ContainerInsights -from azext_k8s_extension.partner_extensions.AzureDefender import AzureDefender -from azext_k8s_extension.partner_extensions.Cassandra import Cassandra -from azext_k8s_extension.partner_extensions.DefaultExtension import DefaultExtension -import azext_k8s_extension._consts as consts +from .partner_extensions.ContainerInsights import ContainerInsights +from .partner_extensions.AzureDefender import AzureDefender +from .partner_extensions.Cassandra import Cassandra +from .partner_extensions.DefaultExtension import DefaultExtension +from . import consts from ._client_factory import cf_resources @@ -33,7 +33,7 @@ def ExtensionFactory(extension_name): extension_map = { 'microsoft.azuremonitor.containers': ContainerInsights, 'microsoft.azuredefender.kubernetes': AzureDefender, - 'cassandradatacentersoperator': Cassandra + 'cassandradatacentersoperator': Cassandra, } # Return the extension if we find it in the map, else return the default diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py index 3124faec4c1..a3e805006de 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py @@ -7,13 +7,13 @@ from knack.log import get_logger -from azext_k8s_extension.vendored_sdks.models import ExtensionInstance -from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate -from azext_k8s_extension.vendored_sdks.models import ScopeCluster -from azext_k8s_extension.vendored_sdks.models import Scope +from ..vendored_sdks.models import ExtensionInstance +from ..vendored_sdks.models import ExtensionInstanceUpdate +from ..vendored_sdks.models import ScopeCluster +from ..vendored_sdks.models import Scope -from azext_k8s_extension.partner_extensions.PartnerExtensionModel import PartnerExtensionModel -from azext_k8s_extension.partner_extensions.ContainerInsights import _get_container_insights_settings +from .PartnerExtensionModel import PartnerExtensionModel +from .ContainerInsights import _get_container_insights_settings logger = get_logger(__name__) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/Cassandra.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/Cassandra.py index 2a609ce125a..2357bf08af6 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/Cassandra.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/Cassandra.py @@ -5,13 +5,13 @@ # pylint: disable=unused-argument -from azext_k8s_extension.vendored_sdks.models import ExtensionInstance -from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate -from azext_k8s_extension.vendored_sdks.models import ScopeCluster -from azext_k8s_extension.vendored_sdks.models import ScopeNamespace -from azext_k8s_extension.vendored_sdks.models import Scope +from ..vendored_sdks.models import ExtensionInstance +from ..vendored_sdks.models import ExtensionInstanceUpdate +from ..vendored_sdks.models import ScopeCluster +from ..vendored_sdks.models import ScopeNamespace +from ..vendored_sdks.models import Scope -from azext_k8s_extension.partner_extensions.PartnerExtensionModel import PartnerExtensionModel +from .PartnerExtensionModel import PartnerExtensionModel class Cassandra(PartnerExtensionModel): diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py index eade4256205..00e82257e2d 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py @@ -17,14 +17,14 @@ from msrestazure.azure_exceptions import CloudError from msrestazure.tools import parse_resource_id, is_valid_resource_id -from azext_k8s_extension.vendored_sdks.models import ExtensionInstance -from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate -from azext_k8s_extension.vendored_sdks.models import ScopeCluster -from azext_k8s_extension.vendored_sdks.models import Scope +from ..vendored_sdks.models import ExtensionInstance +from ..vendored_sdks.models import ExtensionInstanceUpdate +from ..vendored_sdks.models import ScopeCluster +from ..vendored_sdks.models import Scope -from azext_k8s_extension.partner_extensions.PartnerExtensionModel import PartnerExtensionModel +from .PartnerExtensionModel import PartnerExtensionModel -from azext_k8s_extension._client_factory import ( +from .._client_factory import ( cf_resources, cf_resource_groups, cf_log_analytics) logger = get_logger(__name__) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py index 9a69199f838..a72aef847fc 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py @@ -5,13 +5,13 @@ # pylint: disable=unused-argument -from azext_k8s_extension.vendored_sdks.models import ExtensionInstance -from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate -from azext_k8s_extension.vendored_sdks.models import ScopeCluster -from azext_k8s_extension.vendored_sdks.models import ScopeNamespace -from azext_k8s_extension.vendored_sdks.models import Scope +from ..vendored_sdks.models import ExtensionInstance +from ..vendored_sdks.models import ExtensionInstanceUpdate +from ..vendored_sdks.models import ScopeCluster +from ..vendored_sdks.models import ScopeNamespace +from ..vendored_sdks.models import Scope -from azext_k8s_extension.partner_extensions.PartnerExtensionModel import PartnerExtensionModel +from .PartnerExtensionModel import PartnerExtensionModel class DefaultExtension(PartnerExtensionModel): diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py index 96c489644e7..b8cb01334d3 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py @@ -4,8 +4,8 @@ # -------------------------------------------------------------------------------------------- from abc import ABC, abstractmethod -from azext_k8s_extension.vendored_sdks.models import ExtensionInstance -from azext_k8s_extension.vendored_sdks.models import ExtensionInstanceUpdate +from ..vendored_sdks.models import ExtensionInstance +from ..vendored_sdks.models import ExtensionInstanceUpdate class PartnerExtensionModel(ABC): diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index d3d491fd48c..6b3c7556e93 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -8,7 +8,7 @@ from codecs import open from setuptools import setup, find_packages -import azext_k8s_extension._consts as consts + try: from azure_bdist_wheel import cmdclass except ImportError: @@ -32,14 +32,16 @@ # TODO: Add any additional SDK dependencies here DEPENDENCIES = [] +VERSION = "0.2.1" + with open('README.rst', 'r', encoding='utf-8') as f: README = f.read() with open('HISTORY.rst', 'r', encoding='utf-8') as f: HISTORY = f.read() setup( - name=consts.EXTENSION_NAME, - version=consts.VERSION, + name="k8s-extension", + version=VERSION, description='Microsoft Azure Command-Line Tools K8s-extension Extension', # TODO: Update author and email, if applicable author='Microsoft Corporation', From 0228851f933617e969a1b24befaff3e846c68712 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Wed, 14 Apr 2021 16:31:14 -0700 Subject: [PATCH 36/86] Fix style errors --- src/k8s-extension/azext_k8s_extension/__init__.py | 2 +- src/k8s-extension/azext_k8s_extension/commands.py | 2 +- src/k8s-extension/azext_k8s_extension/consts.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/__init__.py b/src/k8s-extension/azext_k8s_extension/__init__.py index 77d01d5f327..2da15ca787d 100644 --- a/src/k8s-extension/azext_k8s_extension/__init__.py +++ b/src/k8s-extension/azext_k8s_extension/__init__.py @@ -15,7 +15,7 @@ def __init__(self, cli_ctx=None): from azure.cli.core.commands import CliCommandType from ._client_factory import cf_k8s_extension k8s_extension_custom = CliCommandType( - operations_tmpl=consts.EXTENSION_PACKAGE_NAME+'.custom#{}', + operations_tmpl=consts.EXTENSION_PACKAGE_NAME + '.custom#{}', client_factory=cf_k8s_extension) super(K8sExtensionCommandsLoader, self).__init__(cli_ctx=cli_ctx, custom_command_type=k8s_extension_custom) diff --git a/src/k8s-extension/azext_k8s_extension/commands.py b/src/k8s-extension/azext_k8s_extension/commands.py index d576ed4fb8e..abe6f501b79 100644 --- a/src/k8s-extension/azext_k8s_extension/commands.py +++ b/src/k8s-extension/azext_k8s_extension/commands.py @@ -13,7 +13,7 @@ def load_command_table(self, _): k8s_extension_sdk = CliCommandType( - operations_tmpl=consts.EXTENSION_PACKAGE_NAME+'.vendored_sdks.operations#K8sExtensionsOperations.{}', + operations_tmpl=consts.EXTENSION_PACKAGE_NAME + '.vendored_sdks.operations#K8sExtensionsOperations.{}', client_factory=cf_k8s_extension) with self.command_group(consts.EXTENSION_NAME, k8s_extension_sdk, client_factory=cf_k8s_extension_operation, diff --git a/src/k8s-extension/azext_k8s_extension/consts.py b/src/k8s-extension/azext_k8s_extension/consts.py index 082fee5def8..4d09158eacd 100644 --- a/src/k8s-extension/azext_k8s_extension/consts.py +++ b/src/k8s-extension/azext_k8s_extension/consts.py @@ -5,4 +5,4 @@ # -------------------------------------------------------------------------------------------- EXTENSION_NAME = 'k8s-extension' -EXTENSION_PACKAGE_NAME = "azext_k8s_extension" \ No newline at end of file +EXTENSION_PACKAGE_NAME = "azext_k8s_extension" From db4c5b2bf3ad51c48d55ea9f94972b12c2cf1da7 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Wed, 14 Apr 2021 16:38:21 -0700 Subject: [PATCH 37/86] Fix filename --- k8s-custom-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index 451d1e08889..296e4b95614 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -171,7 +171,7 @@ stages: cp $(CLI_REPO_PATH)/src/k8s-extension $(CLI_REPO_PATH)/src/k8s-extension-private -r mv $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension_private cp $(CLI_REPO_PATH)/src/k8s-extension-private/setup_private.py $(CLI_REPO_PATH)/src/k8s-extension-private/setup.py - cp $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension_private/_consts_private.py $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension_private/_consts.py + cp $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension_private/consts_private.py $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension_private/consts.py EXTENSION_NAME="k8s-extension-private" EXTENSION_FILE_NAME="k8s_extension_private" From 50771921db45669e0466ddf0566ac9a8f0f3e148 Mon Sep 17 00:00:00 2001 From: yuyue9284 <15863499+yuyue9284@users.noreply.github.com> Date: Sat, 24 Apr 2021 04:30:03 +0800 Subject: [PATCH 38/86] add customization for microsoft.azureml.kubernetes (#23) * add customization for microsoft.azureml.kubernetes * Update release history Co-authored-by: Yue Yu Co-authored-by: jonathan-innis --- src/k8s-extension/HISTORY.rst | 5 + .../azext_k8s_extension/custom.py | 2 + .../partner_extensions/AzureMLKubernetes.py | 402 ++++++++++++++++++ src/k8s-extension/setup.py | 2 +- 4 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index d20160a2199..dbf39b332fe 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -3,6 +3,11 @@ Release History =============== +0.3.0 +++++++++++++++++++ + +* Release customization for microsoft.azureml.kubernetes + 0.2.1 ++++++++++++++++++ diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index 5e314269aaf..2441c69ed90 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -20,6 +20,7 @@ from .partner_extensions.ContainerInsights import ContainerInsights from .partner_extensions.AzureDefender import AzureDefender from .partner_extensions.Cassandra import Cassandra +from .partner_extensions.AzureMLKubernetes import AzureMLKubernetes from .partner_extensions.DefaultExtension import DefaultExtension from . import consts @@ -33,6 +34,7 @@ def ExtensionFactory(extension_name): extension_map = { 'microsoft.azuremonitor.containers': ContainerInsights, 'microsoft.azuredefender.kubernetes': AzureDefender, + 'microsoft.azureml.kubernetes': AzureMLKubernetes, 'cassandradatacentersoperator': Cassandra, } diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py new file mode 100644 index 00000000000..34aac58e017 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py @@ -0,0 +1,402 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=unused-argument +import copy +from hashlib import md5 +from typing import Any, Dict, List, Tuple + +import azure.mgmt.relay +import azure.mgmt.relay.models +import azure.mgmt.resource.locks +import azure.mgmt.servicebus +import azure.mgmt.servicebus.models +import azure.mgmt.storage +import azure.mgmt.storage.models +import azure.mgmt.loganalytics +import azure.mgmt.loganalytics.models +from ..vendored_sdks.models import ( + ExtensionInstance, ExtensionInstanceUpdate, Scope, ScopeCluster) +from azure.cli.core.azclierror import InvalidArgumentValueError +from azure.cli.core.commands.client_factory import get_mgmt_service_client, get_subscription_id +from azure.mgmt.resource.locks.models import ManagementLockObject +from knack.log import get_logger +from msrestazure.azure_exceptions import CloudError + +from .._client_factory import cf_resources +from .PartnerExtensionModel import PartnerExtensionModel + +logger = get_logger(__name__) + +resource_tag = {'created_by': 'amlk8s-extension'} + + +class AzureMLKubernetes(PartnerExtensionModel): + def __init__(self): + # constants for configuration settings. + self.DEFAULT_RELEASE_NAMESPACE = 'azureml' + self.RELAY_CONNECTION_STRING_KEY = 'relayserver.relayConnectionString' + self.RELAY_CONNECTION_STRING_DEPRECATED_KEY = 'RelayConnectionString' # for 3rd party deployment, will be deprecated + self.HC_RESOURCE_ID_KEY = 'relayserver.hybridConnectionResourceID' + self.RELAY_HC_NAME_KEY = 'relayserver.hybridConnectionName' + self.SERVICE_BUS_CONNECTION_STRING_KEY = 'servicebus.connectionString' + self.SERVICE_BUS_RESOURCE_ID_KEY = 'servicebus.resourceID' + self.SERVICE_BUS_TOPIC_SUB_MAPPING_KEY = 'servicebus.topicSubMapping' + self.AZURE_LOG_ANALYTICS_ENABLED_KEY = 'azure_log_analytics.enabled' + self.AZURE_LOG_ANALYTICS_CUSTOMER_ID_KEY = 'azure_log_analytics.customer_id' + self.AZURE_LOG_ANALYTICS_CONNECTION_STRING = 'azure_log_analytics.connection_string' + self.JOB_SCHEDULER_LOCATION_KEY = 'jobSchedulerLocation' + self.CLUSTER_NAME_FRIENDLY_KEY = 'cluster_name_friendly' + + # component flag + self.ENABLE_TRAINING = 'enableTraining' + self.ENABLE_INFERENCE = 'enableInference' + + # constants for determine whether create underlying azure resource + self.RELAY_SERVER_CONNECTION_STRING = 'relayServerConnectionString' # create relay connection string if None + self.SERVICE_BUS_CONNECTION_STRING = 'serviceBusConnectionString' # create service bus if None + self.LOG_ANALYTICS_WS_ENABLED = 'logAnalyticsWS' # create log analytics workspace if true + + # constants for azure resources creation + self.RELAY_HC_AUTH_NAME = 'azureml_rw' + self.SERVICE_BUS_COMPUTE_STATE_TOPIC = 'computestate-updatedby-computeprovider' + self.SERVICE_BUS_COMPUTE_STATE_SUB = 'compute-scheduler-computestate' + self.SERVICE_BUS_JOB_STATE_TOPIC = 'jobstate-updatedby-computeprovider' + self.SERVICE_BUS_JOB_STATE_SUB = 'compute-scheduler-jobstate' + + # reference mapping + self.reference_mapping = { + self.RELAY_SERVER_CONNECTION_STRING: [self.RELAY_CONNECTION_STRING_KEY, self.RELAY_CONNECTION_STRING_DEPRECATED_KEY], + self.SERVICE_BUS_CONNECTION_STRING: [self.SERVICE_BUS_CONNECTION_STRING_KEY], + 'cluster_name': ['clusterId', 'prometheus.prometheusSpec.externalLabels.cluster_name'], + } + + def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, + scope, auto_upgrade_minor_version, release_train, version, target_namespace, + release_namespace, configuration_settings, configuration_protected_settings, + configuration_settings_file, configuration_protected_settings_file): + if scope == 'namespace': + raise InvalidArgumentValueError("Invalid scope '{}'. This extension can be installed " + "only at 'cluster' scope.".format(scope)) + if not release_namespace: + release_namespace = self.DEFAULT_RELEASE_NAMESPACE + scope_cluster = ScopeCluster(release_namespace=release_namespace) + ext_scope = Scope(cluster=scope_cluster, namespace=None) + + # validate the config + self.__validate_config(configuration_settings, configuration_protected_settings) + + # get the arc's location + subscription_id = get_subscription_id(cmd.cli_ctx) + cluster_rp, parent_api_version = _get_cluster_rp_api_version(cluster_type) + cluster_resource_id = '/subscriptions/{0}/resourceGroups/{1}/providers/{2}' \ + '/{3}/{4}'.format(subscription_id, resource_group_name, cluster_rp, cluster_type, cluster_name) + cluster_location = '' + resources = cf_resources(cmd.cli_ctx, subscription_id) + try: + resource = resources.get_by_id( + cluster_resource_id, parent_api_version) + cluster_location = resource.location.lower() + except CloudError as ex: + raise ex + + # generate values for the extension if none is set. + configuration_settings['cluster_name'] = configuration_settings.get('cluster_name', cluster_resource_id) + configuration_settings['domain'] = configuration_settings.get( + 'doamin', '{}.cloudapp.azure.com'.format(cluster_location)) + configuration_settings['location'] = configuration_settings.get('location', cluster_location) + configuration_settings[self.JOB_SCHEDULER_LOCATION_KEY] = configuration_settings.get( + self.JOB_SCHEDULER_LOCATION_KEY, cluster_location) + configuration_settings[self.CLUSTER_NAME_FRIENDLY_KEY] = configuration_settings.get( + self.CLUSTER_NAME_FRIENDLY_KEY, cluster_name) + + # create Azure resources need by the extension based on the config. + self.__create_required_resource( + cmd, configuration_settings, configuration_protected_settings, subscription_id, resource_group_name, + cluster_name, cluster_location) + + # dereference + configuration_settings = _dereference(self.reference_mapping, configuration_settings) + configuration_protected_settings = _dereference(self.reference_mapping, configuration_protected_settings) + + # If release-train is not input, set it to 'stable' + if release_train is None: + release_train = 'stable' + + create_identity = True + extension_instance = ExtensionInstance( + extension_type=extension_type, + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version, + scope=ext_scope, + configuration_settings=configuration_settings, + configuration_protected_settings=configuration_protected_settings, + identity=None, + location="" + ) + return extension_instance, name, create_identity + + def Update(self, extension, auto_upgrade_minor_version, release_train, version): + return ExtensionInstanceUpdate( + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version + ) + + def __validate_config(self, configuration_settings, configuration_protected_settings): + # perform basic validation of the input config + config_keys = configuration_settings.keys() + config_protected_keys = configuration_protected_settings.keys() + dup_keys = set(config_keys) & set(config_protected_keys) + if len(dup_keys) > 0: + for key in dup_keys: + logger.warn( + f'Duplicate keys found in both configuration settings and configuration protected setttings: {key}') + raise InvalidArgumentValueError("Duplicate keys found.") + + enable_training = _get_value_from_config_protected_config( + self.ENABLE_TRAINING, configuration_settings, configuration_protected_settings) + enable_training = str(enable_training).lower() == 'true' + + enable_inference = _get_value_from_config_protected_config( + self.ENABLE_INFERENCE, configuration_settings, configuration_protected_settings) + enable_inference = str(enable_inference).lower() == 'true' + + if not (enable_training or enable_inference): + raise InvalidArgumentValueError( + "Please create Microsoft.AzureML.Kubernetes extension instance either " + "for Machine Learning training or inference by specifying " + f"'--configuration-settings {self.ENABLE_TRAINING}=true' or '--configuration-settings {self.ENABLE_INFERENCE}=true'") + + configuration_settings[self.ENABLE_TRAINING] = configuration_settings.get(self.ENABLE_TRAINING, enable_training) + configuration_settings[self.ENABLE_INFERENCE] = configuration_settings.get( + self.ENABLE_INFERENCE, enable_inference) + configuration_protected_settings.pop(self.ENABLE_TRAINING, None) + configuration_protected_settings.pop(self.ENABLE_INFERENCE, None) + + def __create_required_resource( + self, cmd, configuration_settings, configuration_protected_settings, subscription_id, resource_group_name, + cluster_name, cluster_location): + if str(configuration_settings.get(self.LOG_ANALYTICS_WS_ENABLED, False)).lower() == 'true'\ + and not configuration_settings.get(self.AZURE_LOG_ANALYTICS_CONNECTION_STRING)\ + and not configuration_protected_settings.get(self.AZURE_LOG_ANALYTICS_CONNECTION_STRING): + logger.info('==== BEGIN LOG ANALYTICS WORKSPACE CREATION ====') + ws_costumer_id, shared_key = _get_log_analytics_ws_connection_string( + cmd, subscription_id, resource_group_name, cluster_name, cluster_location) + logger.info('==== END LOG ANALYTICS WORKSPACE CREATION ====') + configuration_settings[self.AZURE_LOG_ANALYTICS_ENABLED_KEY] = True + configuration_settings[self.AZURE_LOG_ANALYTICS_CUSTOMER_ID_KEY] = ws_costumer_id + configuration_protected_settings[self.AZURE_LOG_ANALYTICS_CONNECTION_STRING] = shared_key + + if not configuration_settings.get( + self.RELAY_SERVER_CONNECTION_STRING) and not configuration_protected_settings.get( + self.RELAY_SERVER_CONNECTION_STRING): + logger.info('==== BEGIN RELAY CREATION ====') + relay_connection_string, hc_resource_id, hc_name = _get_relay_connection_str( + cmd, subscription_id, resource_group_name, cluster_name, cluster_location, self.RELAY_HC_AUTH_NAME) + logger.info('==== END RELAY CREATION ====') + configuration_protected_settings[self.RELAY_SERVER_CONNECTION_STRING] = relay_connection_string + configuration_settings[self.HC_RESOURCE_ID_KEY] = hc_resource_id + configuration_settings[self.RELAY_HC_NAME_KEY] = hc_name + + if not configuration_settings.get( + self.SERVICE_BUS_CONNECTION_STRING) and not configuration_protected_settings.get( + self.SERVICE_BUS_CONNECTION_STRING): + logger.info('==== BEGIN SERVICE BUS CREATION ====') + topic_sub_mapping = { + self.SERVICE_BUS_COMPUTE_STATE_TOPIC: self.SERVICE_BUS_COMPUTE_STATE_SUB, + self.SERVICE_BUS_JOB_STATE_TOPIC: self.SERVICE_BUS_JOB_STATE_SUB + } + service_bus_connection_string, service_buse_resource_id = _get_service_bus_connection_string( + cmd, subscription_id, resource_group_name, cluster_name, cluster_location, topic_sub_mapping) + logger.info('==== END SERVICE BUS CREATION ====') + configuration_protected_settings[self.SERVICE_BUS_CONNECTION_STRING] = service_bus_connection_string + configuration_settings[self.SERVICE_BUS_RESOURCE_ID_KEY] = service_buse_resource_id + configuration_settings[f'{self.SERVICE_BUS_TOPIC_SUB_MAPPING_KEY}.{self.SERVICE_BUS_COMPUTE_STATE_TOPIC}'] = self.SERVICE_BUS_COMPUTE_STATE_SUB + configuration_settings[f'{self.SERVICE_BUS_TOPIC_SUB_MAPPING_KEY}.{self.SERVICE_BUS_JOB_STATE_TOPIC}'] = self.SERVICE_BUS_JOB_STATE_SUB + + +def _get_valid_name(input_name: str, suffix_len: int, max_len: int) -> str: + normalized_str = ''.join(filter(str.isalnum, input_name)) + assert len(normalized_str) > 0, "normalized name empty" + + if len(normalized_str) <= max_len: + return normalized_str + + if suffix_len > max_len: + logger.warning( + "suffix length is bigger than max length. Set suffix length to max length.") + suffix_len = max_len + + md5_suffix = md5(input_name.encode("utf8")).hexdigest()[:suffix_len] + new_name = normalized_str[:max_len - suffix_len] + md5_suffix + return new_name + + +def _lock_resource(cmd, lock_scope, lock_level='CanNotDelete'): + lock_client: azure.mgmt.resource.locks.ManagementLockClient = get_mgmt_service_client( + cmd.cli_ctx, azure.mgmt.resource.locks.ManagementLockClient) + # put lock on relay resource + lock_object = ManagementLockObject(level=lock_level, notes='locked by amlk8s.') + try: + lock_client.management_locks.create_or_update_by_scope( + scope=lock_scope, lock_name='amlk8s-resource-lock', parameters=lock_object) + except: + # try to lock the resource if user has the owner privilege + pass + + +def _get_relay_connection_str( + cmd, subscription_id, resource_group_name, cluster_name, cluster_location, auth_rule_name) -> Tuple[ + str, str, str]: + relay_client: azure.mgmt.relay.RelayManagementClient = get_mgmt_service_client( + cmd.cli_ctx, azure.mgmt.relay.RelayManagementClient) + + cluster_id = '{}-{}-{}-relay'.format(cluster_name, subscription_id, resource_group_name) + # create namespace + relay_namespace_name = _get_valid_name( + cluster_id, suffix_len=6, max_len=50) + relay_namespace_params = azure.mgmt.relay.models.RelayNamespace( + location=cluster_location, tags=resource_tag) + + async_poller = relay_client.namespaces.create_or_update( + resource_group_name, relay_namespace_name, relay_namespace_params) + while True: + async_poller.result(15) + if async_poller.done(): + break + + # create hybrid connection + hybrid_connection_name = cluster_name + hybrid_connection_object = relay_client.hybrid_connections.create_or_update( + resource_group_name, relay_namespace_name, hybrid_connection_name, requires_client_authorization=True) + + # relay_namespace_ojbect = relay_client.namespaces.get(resource_group_name, relay_namespace_name) + # relay_namespace_resource_id = relay_namespace_ojbect.id + # _lock_resource(cmd, lock_scope=relay_namespace_resource_id) + + # create authorization rule + auth_rule_rights = [azure.mgmt.relay.models.AccessRights.manage, + azure.mgmt.relay.models.AccessRights.send, azure.mgmt.relay.models.AccessRights.listen] + relay_client.hybrid_connections.create_or_update_authorization_rule( + resource_group_name, relay_namespace_name, hybrid_connection_name, auth_rule_name, rights=auth_rule_rights) + + # get connection string + key: azure.mgmt.relay.models.AccessKeys = relay_client.hybrid_connections.list_keys( + resource_group_name, relay_namespace_name, hybrid_connection_name, auth_rule_name) + return f'{key.primary_connection_string}', hybrid_connection_object.id, hybrid_connection_name + + +def _get_service_bus_connection_string(cmd, subscription_id, resource_group_name, cluster_name, cluster_location, + topic_sub_mapping: Dict[str, str]) -> Tuple[str, str]: + service_bus_client: azure.mgmt.servicebus.ServiceBusManagementClient = get_mgmt_service_client( + cmd.cli_ctx, azure.mgmt.servicebus.ServiceBusManagementClient) + cluster_id = '{}-{}-{}-service-bus'.format(cluster_name, + subscription_id, resource_group_name) + service_bus_namespace_name = _get_valid_name( + cluster_id, suffix_len=6, max_len=50) + + # create namespace + service_bus_sku = azure.mgmt.servicebus.models.SBSku( + name=azure.mgmt.servicebus.models.SkuName.standard.name) + service_bus_namespace = azure.mgmt.servicebus.models.SBNamespace( + location=cluster_location, + sku=service_bus_sku, + tags=resource_tag) + async_poller = service_bus_client.namespaces.create_or_update( + resource_group_name, service_bus_namespace_name, service_bus_namespace) + while True: + async_poller.result(15) + if async_poller.done(): + break + + for topic_name, service_bus_subscription_name in topic_sub_mapping.items(): + # create topic + topic = azure.mgmt.servicebus.models.SBTopic(max_size_in_megabytes=5120, default_message_time_to_live='P60D') + service_bus_client.topics.create_or_update( + resource_group_name, service_bus_namespace_name, topic_name, topic) + + # create subscription + sub = azure.mgmt.servicebus.models.SBSubscription( + max_delivery_count=1, default_message_time_to_live='P14D', lock_duration='PT30S') + service_bus_client.subscriptions.create_or_update( + resource_group_name, service_bus_namespace_name, topic_name, service_bus_subscription_name, sub) + + service_bus_object = service_bus_client.namespaces.get(resource_group_name, service_bus_namespace_name) + service_bus_resource_id = service_bus_object.id + # _lock_resource(cmd, service_bus_resource_id) + + # get connection string + auth_rules = service_bus_client.namespaces.list_authorization_rules( + resource_group_name, service_bus_namespace_name) + for rule in auth_rules: + key: azure.mgmt.servicebus.models.AccessKeys = service_bus_client.namespaces.list_keys( + resource_group_name, service_bus_namespace_name, rule.name) + return key.primary_connection_string, service_bus_resource_id + + +def _get_log_analytics_ws_connection_string( + cmd, subscription_id, resource_group_name, cluster_name, cluster_location) -> Tuple[ + str, str]: + log_analytics_ws_client: azure.mgmt.loganalytics.LogAnalyticsManagementClient = get_mgmt_service_client( + cmd.cli_ctx, azure.mgmt.loganalytics.LogAnalyticsManagementClient) + + # create workspace + cluster_id = '{}-{}-{}'.format(cluster_name, subscription_id, resource_group_name) + log_analytics_ws_name = _get_valid_name(cluster_id, suffix_len=6, max_len=63) + log_analytics_ws = azure.mgmt.loganalytics.models.Workspace(location=cluster_location, tags=resource_tag) + async_poller = log_analytics_ws_client.workspaces.begin_create_or_update( + resource_group_name, log_analytics_ws_name, log_analytics_ws) + customer_id = '' + # log_analytics_ws_resource_id = '' + while True: + log_analytics_ws_object = async_poller.result(15) + if async_poller.done(): + customer_id = log_analytics_ws_object.customer_id + # log_analytics_ws_resource_id = log_analytics_ws_object.id + break + + # _lock_resource(cmd, log_analytics_ws_resource_id) + + # get workspace shared keys + shared_key = log_analytics_ws_client.shared_keys.get_shared_keys( + resource_group_name, log_analytics_ws_name).primary_shared_key + return customer_id, shared_key + + +def _dereference(ref_mapping_dict: Dict[str, List], output_dict: Dict[str, Any]): + output_dict = copy.deepcopy(output_dict) + for ref_key, ref_list in ref_mapping_dict.items(): + if ref_key not in output_dict: + continue + ref_value = output_dict[ref_key] + for key in ref_list: + # if user has set the value, skip. + output_dict[key] = output_dict.get(key, ref_value) + return output_dict + + +def _get_value_from_config_protected_config(key, config, protected_config): + if key in config: + return config[key] + return protected_config.get(key) + + +def _get_cluster_rp_api_version(cluster_type) -> Tuple[str, str]: + rp = '' + parent_api_version = '' + if cluster_type.lower() == 'connectedclusters': + rp = 'Microsoft.Kubernetes' + parent_api_version = '2020-01-01-preview' + elif cluster_type.lower() == 'appliances': + rp = 'Microsoft.ResourceConnector' + parent_api_version = '2020-09-15-privatepreview' + elif cluster_type.lower() == '': + rp = 'Microsoft.ContainerService' + parent_api_version = '2017-07-01' + else: + raise InvalidArgumentValueError("Error! Cluster type '{}' is not supported".format(cluster_type)) + return rp, parent_api_version diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index 6b3c7556e93..3c9a1882a27 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -32,7 +32,7 @@ # TODO: Add any additional SDK dependencies here DEPENDENCIES = [] -VERSION = "0.2.1" +VERSION = "0.3.0" with open('README.rst', 'r', encoding='utf-8') as f: README = f.read() From 3d1515113695db539b87c75afe28fa30f90bad0e Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Mon, 26 Apr 2021 14:18:56 -0700 Subject: [PATCH 39/86] Add E2E Testing from Separate branch into internal code (#26) * Add internal e2e testing * Change to testing folder --- k8s-custom-pipelines.yml | 24 +-- testing/.gitignore | 8 + testing/Bootstrap.ps1 | 80 ++++++++++ testing/Cleanup.ps1 | 24 +++ testing/README.md | 116 ++++++++++++++ testing/Test.ps1 | 76 ++++++++++ .../bin/connectedk8s-1.0.0-py3-none-any.whl | Bin 0 -> 62802 bytes testing/bin/connectedk8s-values.yaml | 3 + .../bin/k8s_extension-0.2.0-py3-none-any.whl | Bin 0 -> 46987 bytes testing/docs/test_authoring.md | 142 ++++++++++++++++++ testing/owners.txt | 2 + testing/settings.template.json | 12 ++ .../private-preview/AzurePolicy.Tests.ps1 | 95 ++++++++++++ .../public/AzureMLKubernetes.Tests.ps1 | 94 ++++++++++++ .../extensions/public/AzureMonitor.Tests.ps1 | 95 ++++++++++++ testing/test/helper/Constants.ps1 | 7 + testing/test/helper/Helper.ps1 | 47 ++++++ 17 files changed, 809 insertions(+), 16 deletions(-) create mode 100644 testing/.gitignore create mode 100644 testing/Bootstrap.ps1 create mode 100644 testing/Cleanup.ps1 create mode 100644 testing/README.md create mode 100644 testing/Test.ps1 create mode 100644 testing/bin/connectedk8s-1.0.0-py3-none-any.whl create mode 100644 testing/bin/connectedk8s-values.yaml create mode 100644 testing/bin/k8s_extension-0.2.0-py3-none-any.whl create mode 100644 testing/docs/test_authoring.md create mode 100644 testing/owners.txt create mode 100644 testing/settings.template.json create mode 100644 testing/test/extensions/private-preview/AzurePolicy.Tests.ps1 create mode 100644 testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 create mode 100644 testing/test/extensions/public/AzureMonitor.Tests.ps1 create mode 100644 testing/test/helper/Constants.ps1 create mode 100644 testing/test/helper/Helper.ps1 diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index 296e4b95614..00d87b68d13 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -1,10 +1,3 @@ -resources: - repositories: - - repository: K8sPartnerExtensionTest - type: git - endpoint: AzureReposConnection - name: One/compute-HybridMgmt-K8sPartnerExtensionTest - trigger: batch: true branches: @@ -21,8 +14,8 @@ stages: - stage: BuildTestPublishExtension displayName: "Build, Test, and Publish Extension" variables: - K8S_EXTENSION_REPO_PATH: $(Agent.BuildDirectory)/s/compute-HybridMgmt-K8sPartnerExtensionTest - CLI_REPO_PATH: $(Agent.BuildDirectory)/s/azure-cli-extensions + TEST_PATH: $(Agent.BuildDirectory)/s/testing + CLI_REPO_PATH: $(Agent.BuildDirectory)/s SUBSCRIPTION_ID: "15c06b1b-01d6-407b-bb21-740b8617dea3" RESOURCE_GROUP: "K8sPartnerExtensionTest" BASE_CLUSTER_NAME: "k8s-extension-cluster" @@ -37,7 +30,6 @@ stages: vmImage: 'ubuntu-16.04' steps: - checkout: self - - checkout: K8sPartnerExtensionTest - bash: | echo "Installing helm3" curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 @@ -78,7 +70,7 @@ stages: - bash: | K8S_EXTENSION_VERSION=$(ls ${EXTENSION_FILE_NAME}* | cut -d "-" -f2) echo "##vso[task.setvariable variable=K8S_EXTENSION_VERSION]$K8S_EXTENSION_VERSION" - cp * $(K8S_EXTENSION_REPO_PATH)/bin + cp * $(TEST_PATH)/bin workingDirectory: $(CLI_REPO_PATH)/dist displayName: "Copy the Built .whl to Extension Test Path" @@ -96,7 +88,7 @@ stages: '{subscriptionId: $SUB_ID, resourceGroup: $RG, aksClusterName: $AKS_CLUSTER_NAME, arcClusterName: $ARC_CLUSTER_NAME, extensionVersion: {"k8s-extension": $K8S_EXTENSION_VERSION, connectedk8s: "1.0.0"}}') echo $JSON_STRING > settings.json cat settings.json - workingDirectory: $(K8S_EXTENSION_REPO_PATH) + workingDirectory: $(TEST_PATH) displayName: "Generate a settings.json file" - bash : | @@ -114,7 +106,7 @@ stages: scriptLocation: inlineScript inlineScript: | .\Bootstrap.ps1 -CI - workingDirectory: $(K8S_EXTENSION_REPO_PATH) + workingDirectory: $(TEST_PATH) - task: AzureCLI@2 displayName: Run the Test Suite Public Extensions Only @@ -124,7 +116,7 @@ stages: scriptLocation: inlineScript inlineScript: | .\Test.ps1 -CI -ExtensionType Public -OnlyPublicTests - workingDirectory: $(K8S_EXTENSION_REPO_PATH) + workingDirectory: $(TEST_PATH) continueOnError: true condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'False')) @@ -136,7 +128,7 @@ stages: scriptLocation: inlineScript inlineScript: | .\Test.ps1 -CI -ExtensionType Public - workingDirectory: $(K8S_EXTENSION_REPO_PATH) + workingDirectory: $(TEST_PATH) continueOnError: true condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'True')) @@ -155,7 +147,7 @@ stages: scriptLocation: inlineScript inlineScript: | .\Cleanup.ps1 -CI - workingDirectory: $(K8S_EXTENSION_REPO_PATH) + workingDirectory: $(TEST_PATH) condition: succeededOrFailed() - job: BuildPublishExtension diff --git a/testing/.gitignore b/testing/.gitignore new file mode 100644 index 00000000000..7df7f6a7294 --- /dev/null +++ b/testing/.gitignore @@ -0,0 +1,8 @@ +settings.json +tmp/ +bin/* +!bin/connectedk8s-1.0.0-py3-none-any.whl +!bin/k8s_extension-0.2.0-py3-none-any.whl +!bin/k8s_extension_private-0.1.0-py3-none-any.whl +!bin/connectedk8s-values.yaml +*.xml \ No newline at end of file diff --git a/testing/Bootstrap.ps1 b/testing/Bootstrap.ps1 new file mode 100644 index 00000000000..c9123ba6d03 --- /dev/null +++ b/testing/Bootstrap.ps1 @@ -0,0 +1,80 @@ +param ( + [switch] $SkipInstall, + [switch] $CI +) + +# Disable confirm prompt for script +az config set core.disable_confirm_prompt=true + +# Configuring the environment +$ENVCONFIG = Get-Content -Path $PSScriptRoot/settings.json | ConvertFrom-Json + +az account set --subscription $ENVCONFIG.subscriptionId + +if (-not (Test-Path -Path $PSScriptRoot/tmp)) { + New-Item -ItemType Directory -Path $PSScriptRoot/tmp +} + +if (!$SkipInstall) { + Write-Host "Removing the old connnectedk8s extension..." + az extension remove -n connectedk8s + $connectedk8sVersion = $ENVCONFIG.extensionVersion.connectedk8s + if (!$connectedk8sVersion) { + Write-Host "connectedk8s extension version wasn't specified" -ForegroundColor Red + Exit 1 + } + Write-Host "Installing connectedk8s version $connectedk8sVersion..." + az extension add --source ./bin/connectedk8s-$connectedk8sVersion-py3-none-any.whl + if (!$?) { + Write-Host "Unable to find connectedk8s version $connectedk8sVersion, exiting..." + exit 1 + } +} + +Write-Host "Onboard cluster to Azure...starting!" + +az group show --name $envConfig.resourceGroup +if (!$?) { + Write-Host "Resource group does not exist, creating it now in region 'eastus2euap'" + az group create --name $envConfig.resourceGroup --location eastus2euap + + if (!$?) { + Write-Host "Failed to create Resource Group - exiting!" + Exit 1 + } +} + +# Skip creating the AKS Cluster if this is CI +if (!$CI) { + az aks show -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.aksClusterName + if (!$?) { + Write-Host "Cluster does not exist, creating it now" + az aks create -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.aksClusterName --generate-ssh-keys + } else { + Write-Host "Cluster already exists, no need to create it." + } + + Write-Host "Retrieving credentials for your AKS cluster..." + + az aks get-credentials -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.aksClusterName -f tmp/KUBECONFIG + if (!$?) + { + Write-Host "Cluster did not create successfully, exiting!" -ForegroundColor Red + Exit 1 + } + Write-Host "Successfully retrieved the AKS kubectl credentials" +} else { + Copy-Item $HOME/.kube/config -Destination $PSScriptRoot/tmp/KUBECONFIG +} + +az connectedk8s show -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.arcClusterName +if ($?) +{ + Write-Host "Cluster is already connected, no need to re-connect" + Exit 0 +} + +Write-Host "Connecting the cluster to Arc with connectedk8s..." +$Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" +$Env:HELMVALUESPATH="$PSScriptRoot/bin/connectedk8s-values.yaml" +az connectedk8s connect -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.arcClusterName diff --git a/testing/Cleanup.ps1 b/testing/Cleanup.ps1 new file mode 100644 index 00000000000..9957a044241 --- /dev/null +++ b/testing/Cleanup.ps1 @@ -0,0 +1,24 @@ +param ( + [switch] $CI +) + +# Disable confirm prompt for script +az config set core.disable_confirm_prompt=true + +$ENVCONFIG = Get-Content -Path $PSScriptRoot/settings.json | ConvertFrom-Json + +az account set --subscription $ENVCONFIG.subscriptionId + +$Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" +Write-Host "Removing the connectedk8s arc agents from the cluster..." +az connectedk8s delete -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.arcClusterName + +# Skip deleting the AKS Cluster if this is CI +if (!$CI) { + Write-Host "Deleting the AKS cluster from Azure..." + az aks delete -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.aksClusterName + if (Test-Path -Path $PSScriptRoot/tmp) { + Write-Host "Deleting the tmp directory from the test directory" + Remove-Item -Path $PSScriptRoot/tmp -Force -Confirm:$false + } +} \ No newline at end of file diff --git a/testing/README.md b/testing/README.md new file mode 100644 index 00000000000..2c2d48070bd --- /dev/null +++ b/testing/README.md @@ -0,0 +1,116 @@ +# K8s Partner Extension Test Suite + +This repository serves as the integration testing suite for the `k8s-extension` Azure CLI module. + +## Testing Requirements + +All partners who wish to merge their __Custom Private Preview Release__ (owner: _Partner_) into the __Official Private Preview Release__ are required to author additional integration tests for their extension to ensure that their extension will continue to function correctly as more extensions are added into the __Official Private Preview Release__. + +For more information on creating these tests, see [Authoring Tests](docs/test_authoring.md) + +## Pre-Requisites + +In order to properly test all regression tests within the test suite, you must onboard an AKS cluster which you will use to generate your Azure Arc resource to test the extensions. Ensure that you have a resource group where you can onboard this cluster. + +### Required Installations + +The following installations are required in your environment for the integration tests to run correctly: + +1. [Helm 3](https://helm.sh/docs/intro/install/) +2. [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) +3. [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) + +## Setup + +### Step 1: Install Pester + +This project contains [Pester](https://pester.dev/) test framework commands that are required for the integration tests to run. In an admin powershell terminal, run + +```powershell +Install-Module Pester -Force -SkipPublisherCheck +Import-Module Pester -PassThru +``` + +If you run into issues installing the framework, refer to the [Installation Guide](https://pester.dev/docs/introduction/installation) provided by the Pester docs. + +### Step 2: Get Test suite files + +You can either clone this repo (preferred option, since you will be adding your tests to this suite) or copy the files in this repo locally. Rest of the instructions here assume your working directory is k8spartner-extension-testing. + +### Step 3: Update the `k8s-extension`/`k8s-extension-private` .whl package + +This integration test suite references the .whl packages found in the `\bin` directory. After generating your `k8s-extension`/`k8s-extension-private` .whl package, copy your updated package into the `\bin` directory. + +### Step 4: Create a `settings.json` + +To onboard the AKS and Arc clusters correctly, you will need to create a `settings.json` configuration. Create a new `settings.json` file by copying the contents of the `settings.template.json` into this file. Update the subscription id, resource group, and AKS and Arc cluster name fields with your specific values. + +### Step 5: Update the extension version value in `settings.json` + +To ensure that the tests point to your `k8s-extension-private` `.whl` package, change the value of the `k8s-extension-private` to match your package versioning in the format (Major.Minor.Patch.Extension). For example, the `k8s_extension_private-0.1.0.openservicemesh_5-py3-none-any.whl` whl package would have extension versions set to +```json +{ + "k8s-extension": "0.1.0", + "k8s-extension-private": "0.1.0.openservicemesh_5", + "connectedk8s": "0.3.5" +} + +``` + +_Note: Updates to the `connectedk8s` version and `k8s-extension` version can also be made by adding a different version of the `connectedk8s` and `k8s-extension` whl packages and changing the `connectedk8s` and `k8s-extension` values to match the (Major.Minor.Patch) version format shown above_ + +### Step 6: Run the Bootstrap Command +To bootstrap the environment with AKS and Arc clusters, run +```powershell +.\Bootstrap.ps1 +``` +This script will provision the AKS and Arc clusters needed to run the integration test suite + +## Testing + +### Testing All Extension Suites +To test all extension test suites, you must call `.\Test.ps1` with the `-ExtensionType` parameter set to either `Public` or `Private`. Based on this flag, the test suite will install the extension type specified below + +| `-ExtensionType` | Installs `az extension` | +| ---------------- | --------------------- | +| `Public` | `k8s-extension` | +| `Private` | `k8s-extension-private` | + +For example, when calling +```bash +.\Test.ps1 -ExtensionType Public +``` +the script will install your `k8s-extension` whl package and run the full test suite of `*.Tests.ps1` files included in the `\test\extensions` directory + +### Testing Public Extensions Only +If you only want to run the test cases against public-preview or GA extension test cases, you can use the `-OnlyPublicTests` flag to specify this +```bash +.\Test.ps1 -ExtensionType Public -OnlyPublicTests +``` + +### Testing Specific Extension Suite + +If you only want to run the test script on your specific test file, you can do so by specifying path to your extension test suite in the execution call + +```powershell +.\Test.ps1 -Path +``` +For example to call the `AzureMonitor.Tests.ps1` test suite, we run +```powershell +.\Test.ps1 -ExtensionType Public -Path .\test\extensions\public\AzureMonitor.Tests.ps1 +``` + +### Skipping Extension Re-Install + +By default the `Test.ps1` script will uninstall any old versions of `k8s-extension`/'`k8s-extension-private` and re-install the version specified in `settings.json`. If you do not want this re-installation to occur, you can specify the `-SkipInstall` flag to skip this process. + +```powershell +.\Test.ps1 -ExtensionType Public -SkipInstall +``` + +## Cleanup +To cleanup the AKS and Arc clusters you have provisioned in testing, run +```powershell +.\Cleanup.ps1 +``` +This will remove the AKS and Arc clusters as well as the `\tmp` directory that were created by the bootstrapping script. \ No newline at end of file diff --git a/testing/Test.ps1 b/testing/Test.ps1 new file mode 100644 index 00000000000..6304c13dc28 --- /dev/null +++ b/testing/Test.ps1 @@ -0,0 +1,76 @@ +param ( + [string] $Path, + [switch] $SkipInstall, + [switch] $CI, + [switch] $OnlyPublicTests, + + [Parameter(Mandatory=$True)] + [ValidateSet('Public','Private')] + [string]$ExtensionType +) + +# Disable confirm prompt for script +az config set core.disable_confirm_prompt=true + +$ENVCONFIG = Get-Content -Path $PSScriptRoot/settings.json | ConvertFrom-Json + +az account set --subscription $ENVCONFIG.subscriptionId + +$Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" + +if ($ExtensionType -eq "Public") { + $k8sExtensionVersion = $ENVCONFIG.extensionVersion.'k8s-extension' + $Env:K8sExtensionName = "k8s-extension" + + if (!$SkipInstall) { + Write-Host "Removing the old k8s-extension extension..." + az extension remove -n k8s-extension + Write-Host "Installing k8s-extension version $k8sExtensionVersion..." + az extension add --source ./bin/k8s_extension-$k8sExtensionVersion-py3-none-any.whl + if (!$?) { + Write-Host "Unable to find k8s-extension version $k8sExtensionVersion, exiting..." + exit 1 + } + } +} else { + $k8sExtensionPrivateVersion = $ENVCONFIG.extensionVersion.'k8s-extension-private' + $Env:K8sExtensionName = "k8s-extension-private" + + if (!$SkipInstall) { + Write-Host "Removing the old k8s-extension-private extension..." + az extension remove -n k8s-extension-private + Write-Host "Installing k8s-extension-private version $k8sExtensionPrivateVersion..." + az extension add --source ./bin/k8s_extension_private-$k8sExtensionPrivateVersion-py3-none-any.whl + if (!$?) { + Write-Host "Unable to find k8s-extension-private version $k8sExtensionPrivateVersion, exiting..." + exit 1 + } + } +} + +if ($CI) { + if ($OnlyPublicTests) { + Write-Host "Invoking Pester to run tests from '$PSScriptRoot/test/extensions/public'" + $testResult = Invoke-Pester $PSScriptRoot/test/extensions/public -Passthru -Output Detailed + $testResult | Export-JUnitReport -Path TestResults.xml + } + else { + Write-Host "Invoking Pester to run tests from '$PSScriptRoot/test/extensions'" + $testResult = Invoke-Pester $PSScriptRoot/test/extensions -Passthru -Output Detailed + $testResult | Export-JUnitReport -Path TestResults.xml + } +} else { + if ($Path) { + Write-Host "Invoking Pester to run tests from '$PSScriptRoot/$Path'" + Invoke-Pester -Output Detailed $PSScriptRoot/$Path + } else { + if ($OnlyPublicTests) { + Write-Host "Invoking Pester to run tests from '$PSScriptRoot/test/extensions/public'" + Invoke-Pester -Output Detailed $PSScriptRoot/test/extensions/public + } + else { + Write-Host "Invoking Pester to run tests from '$PSScriptRoot/test/extensions'" + Invoke-Pester -Output Detailed $PSScriptRoot/test/extensions + } + } +} diff --git a/testing/bin/connectedk8s-1.0.0-py3-none-any.whl b/testing/bin/connectedk8s-1.0.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..08f34250036f455aad7e3e820c65d08d790e1201 GIT binary patch literal 62802 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

YVX(toUj*0P8con1X*tXmm22nWyt~gf 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

4OLf9N*^x0^yG1@+&DtXglO();gI7nFsJg6 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 literal 0 HcmV?d00001 diff --git a/testing/docs/test_authoring.md b/testing/docs/test_authoring.md new file mode 100644 index 00000000000..b41286ba66d --- /dev/null +++ b/testing/docs/test_authoring.md @@ -0,0 +1,142 @@ +# Test Authoring + +All partners are _required_ to author additional integration tests when merging their extension into the __Official Private Preview Release__. The information below outlines how to setup and author these additional tests. + +## Requirements + +All partners are required to cover standard CLI scenarios in your extensions testing suite. When adding these tests and preparing to merge your updated extension whl package, your tests along with the other tests in the test suite must pass at 100%. + +Standard CLI scenarios include: + +1. `az k8s-extension create` +2. `az k8s-extension show` +3. `az k8s-extension list` +4. `az k8s-extension update` +5. `az k8s-extension delete` + +In addition to these standard scenarios, if there are any rigorous parameter validation standards, these should also be included in this test suite. + +## Setup + +The setup process for test authoring is the same as setup for generic testing. See [Setup](../README.md#setup) for guidance. + +## Writing Tests + +This section outlines the common flow for creating and running additional extension integration tests for the `k8s-extension` package. + +The suite utilizes the [Pester](https://pester.dev/) framework. For more information on creating generic Pester tests, see the [Create a Pester Test](https://pester.dev/docs/quick-start#creating-a-pester-test) section in the Pester docs. + +### Step 1: Create Test File + +To create an integration test suite for your extension, create an extension test file in the format `.Tests.ps1` and place the file in one of the following directories +| Extension Type | Directory | +| ---------------------- | ----------------------------------- | +| General Availability | .\test\extensions\public | +| Public Preview | .\test\extensions\public | +| Private Preview | .\test\extensions\private-preview | + +For example, to create a test suite file for the Azure Monitor extension, I create the file `AzureMonitor.Tests.ps1` in the `\test\extensions\public` directory because Container Insights extension is in _Public Preview_. + +### Step 2: Setup Global Variables + +All test suite files must have the following structure for importing the environment config and declaring globals + +```powershell +Describe ' Testing' { + BeforeAll { + $extensionType = "" + $extensionName = "" + $extensionAgentName = "" + $extensionAgentNamespace = "" + + . $PSScriptRoot/../../helper/Constants.ps1 + . $PSScriptRoot/../../helper/Helper.ps1 + } +} +``` + +You can declare additional global variables for your tests by adding additional powershell variable to this `BeforeAll` block. + +_Note: Commonly used constants used by all extension test suites are stored in the `Constants.ps1` file_ + +### Step 3: Add Tests + +Adding tests to the test suite can now be performed by adding `It` blocks to the outer `Describe` block. For instance to test create on a extension in the case of AzureMonitor, I write the following test: + +```powershell +Describe 'Azure Monitor Testing' { + BeforeAll { + $extensionType = "microsoft.azuremonitor.containers" + $extensionName = "azuremonitor-containers" + $extensionAgentName = "omsagent" + $extensionAgentNamespace = "kube-system" + + . $PSScriptRoot/../../helper/Constants.ps1 + . $PSScriptRoot/../../helper/Helper.ps1 + } + + It 'Creates the extension and checks that it onboards correctly' { + $output = az k8s-extension create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType -n $extensionName + $? | Should -BeTrue + + $output = az k8s-extension show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue + + # Loop and retry until the extension installs + $n = 0 + do + { + if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { + if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } +} +``` + +The above test calls `az k8s-extension create` to create the `azuremonitor-containers` extension and retries checking that the extension resource was actually created on the Arc cluster and that the extension status successfully returns `$SUCCESS_MESSAGE` which is equivalent to `Successfully installed the extension`. + +## Tips/Notes + +### Accessing Extension Data + +`.\Test.ps1` assumes that the user has `kubectl` and `az` installed in their environment; therefore, tests are able to access information on the extension at the service and on the arc cluster. For instance, in the above test, we access the `extensionconfig` CRDs on the arc cluster by calling + +```powershell +kubectl get extensionconfigs -A -o json +``` + +If we want to access the extension data on the cluster with a specific `$extensionName`, we run + +```powershell +(kubectl get extensionconfigs -A -o json).items | Where-Object { $_.metadata.name -eq $extensionName } +``` + +Because some of these commands are so common, we provide the following helper commands in the `test\Helper.ps1` file + +| Command | Description | +| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| Get-ExtensionData | Retrieves the ExtensionConfig CRD in JSON format with `.meatadata.name` matching the `extensionName` | +| Get-ExtensionStatus | Retrieves the `.status.status` from the ExtensionConfig CRD with `.meatadata.name` matching the `extensionName` | +| Get-PodStatus -Namespace | Retrieves the `status.phase` from the first pod on the cluster with `.metadata.name` matching `extensionName` | + +### Stdout for Debugging + +To print out to the Console for debugging while writing your test cases use the `Write-Host` command. If you attempt to use the `Write-Output` command, it will not show because of the way that Pester is invoked + +```powershell +Write-Host "Some example output" +``` + +### Global Constants + +Looking at the above test, we can see that we are accessing the `ENVCONFIG` to retrieve the environment variables from the `settings.json`. All variables in the `settings.json` are accessible from the `ENVCONFIG`. The most useful ones for testing will be `ENVCONFIG.arcClusterName` and `ENVCONFIG.resourceGroup`. + diff --git a/testing/owners.txt b/testing/owners.txt new file mode 100644 index 00000000000..ead6f446410 --- /dev/null +++ b/testing/owners.txt @@ -0,0 +1,2 @@ +joinnis +nanthi \ No newline at end of file diff --git a/testing/settings.template.json b/testing/settings.template.json new file mode 100644 index 00000000000..5d459b44bee --- /dev/null +++ b/testing/settings.template.json @@ -0,0 +1,12 @@ +{ + "subscriptionId": "", + "resourceGroup": "", + "aksClusterName": "", + "arcClusterName": "", + + "extensionVersion": { + "k8s-extension": "0.2.0", + "k8s-extension-private": "0.1.0", + "connectedk8s": "0.3.5" + } +} \ No newline at end of file diff --git a/testing/test/extensions/private-preview/AzurePolicy.Tests.ps1 b/testing/test/extensions/private-preview/AzurePolicy.Tests.ps1 new file mode 100644 index 00000000000..ea8c3f46cb4 --- /dev/null +++ b/testing/test/extensions/private-preview/AzurePolicy.Tests.ps1 @@ -0,0 +1,95 @@ +Describe 'Azure Policy Testing' { + BeforeAll { + $extensionType = "microsoft.policyinsights" + $extensionName = "policy" + $extensionAgentName = "azure-policy" + $extensionAgentNamespace = "kube-system" + + . $PSScriptRoot/../../helper/Constants.ps1 + . $PSScriptRoot/../../helper/Helper.ps1 + } + + It 'Creates the extension and checks that it onboards correctly' { + $output = az $Env:K8sExtensionName create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType -n $extensionName + $? | Should -BeTrue + + $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue + + # Loop and retry until the extension installs + $n = 0 + do + { + if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { + if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Performs a show on the extension" { + $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + } + + It "Runs an update on the extension on the cluster" { + Set-ItResult -Skipped -Because "Update is not a valid scenario for now" + + # az $Env:K8sExtensionName update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName --auto-upgrade-minor-version false + # $? | Should -BeTrue + + # $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + # $? | Should -BeTrue + + # $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + # $isAutoUpgradeMinorVersion.ToString() -eq "False" | Should -BeTrue + + # # Loop and retry until the extension config updates + # $n = 0 + # do + # { + # $isAutoUpgradeMinorVersion = (Get-ExtensionData $extensionName).spec.autoUpgradeMinorVersion + # if (!$isAutoUpgradeMinorVersion) { #autoUpgradeMinorVersion doesn't exist in ExtensionConfig CRD if false + # if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { + # if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { + # break + # } + # } + # } + # Start-Sleep -Seconds 10 + # $n += 1 + # } while ($n -le $MAX_RETRY_ATTEMPTS) + # $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Lists the extensions on the cluster" { + $output = az $Env:K8sExtensionName list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $? | Should -BeTrue + + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } + $extensionExists | Should -Not -BeNullOrEmpty + } + + It "Deletes the extension from the cluster" { + az $Env:K8sExtensionName delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + # Extension should not be found on the cluster + az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeFalse + } + + It "Performs another list after the delete" { + $output = az $Env:K8sExtensionName list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } + $extensionExists | Should -BeNullOrEmpty + } +} diff --git a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 new file mode 100644 index 00000000000..344e36a296c --- /dev/null +++ b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 @@ -0,0 +1,94 @@ +Describe 'AzureML Kubernetes Testing' { + BeforeAll { + $extensionType = "Microsoft.AzureML.Kubernetes" + $extensionName = "azureml-kubernetes-connector" + $extensionAgentNamespace = "azureml" + $relayResourceIDKey = "relayserver.hybridConnectionResourceID" + + . $PSScriptRoot/../../helper/Constants.ps1 + . $PSScriptRoot/../../helper/Helper.ps1 + } + + It 'Creates the extension and checks that it onboards correctly' { + $output = az k8s-extension create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType --name $extensionName --release-train preview --config enableTraining=true + $? | Should -BeTrue + + $output = az k8s-extension show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName + $? | Should -BeTrue + + $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue + + # Loop and retry until the extension installs + $n = 0 + do + { + if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + + # check if relay is populated + $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey + $relayResourceID | Should -Not -BeNullOrEmpty + } + + It "Performs a show on the extension" { + $output = az k8s-extension show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + } + + It "Runs an update on the extension on the cluster" { + Set-ItResult -Skipped -Because "Update is not a valid scenario for now" + az k8s-extension update --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName --auto-upgrade-minor-version false + $? | Should -BeTrue + + $output = az k8s-extension show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName + $? | Should -BeTrue + + $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + $isAutoUpgradeMinorVersion.ToString() -eq "False" | Should -BeTrue + + # Loop and retry until the extension config updates + $n = 0 + do + { + $isAutoUpgradeMinorVersion = (Get-ExtensionData $extensionName).spec.autoUpgradeMinorVersion + if (!$isAutoUpgradeMinorVersion) { #autoUpgradeMinorVersion doesn't exist in ExtensionConfig CRD if false + if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Lists the extensions on the cluster" { + $output = az k8s-extension list --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $? | Should -BeTrue + + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } + $extensionExists | Should -Not -BeNullOrEmpty + } + + It "Deletes the extension from the cluster" { + az k8s-extension delete --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName + $? | Should -BeTrue + + # Extension should not be found on the cluster + az k8s-extension show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName + $? | Should -BeFalse + } + + It "Performs another list after the delete" { + $output = az k8s-extension list --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } + $extensionExists | Should -BeNullOrEmpty + } +} diff --git a/testing/test/extensions/public/AzureMonitor.Tests.ps1 b/testing/test/extensions/public/AzureMonitor.Tests.ps1 new file mode 100644 index 00000000000..c9baa2d0e48 --- /dev/null +++ b/testing/test/extensions/public/AzureMonitor.Tests.ps1 @@ -0,0 +1,95 @@ +Describe 'Azure Monitor Testing' { + BeforeAll { + $extensionType = "microsoft.azuremonitor.containers" + $extensionName = "azuremonitor-containers" + $extensionAgentName = "omsagent" + $extensionAgentNamespace = "kube-system" + + . $PSScriptRoot/../../helper/Constants.ps1 + . $PSScriptRoot/../../helper/Helper.ps1 + } + + It 'Creates the extension and checks that it onboards correctly' { + $output = az $Env:K8sExtensionName create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType -n $extensionName + $? | Should -BeTrue + + $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue + + # Loop and retry until the extension installs + $n = 0 + do + { + if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { + if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Performs a show on the extension" { + $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + } + + It "Runs an update on the extension on the cluster" { + Set-ItResult -Skipped -Because "Update is not a valid scenario for now" + + # az $Env:K8sExtensionName update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName --auto-upgrade-minor-version false + # $? | Should -BeTrue + + # $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + # $? | Should -BeTrue + + # $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + # $isAutoUpgradeMinorVersion.ToString() -eq "False" | Should -BeTrue + + # # Loop and retry until the extension config updates + # $n = 0 + # do + # { + # $isAutoUpgradeMinorVersion = (Get-ExtensionData $extensionName).spec.autoUpgradeMinorVersion + # if (!$isAutoUpgradeMinorVersion) { #autoUpgradeMinorVersion doesn't exist in ExtensionConfig CRD if false + # if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { + # if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { + # break + # } + # } + # } + # Start-Sleep -Seconds 10 + # $n += 1 + # } while ($n -le $MAX_RETRY_ATTEMPTS) + # $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Lists the extensions on the cluster" { + $output = az $Env:K8sExtensionName list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $? | Should -BeTrue + + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } + $extensionExists | Should -Not -BeNullOrEmpty + } + + It "Deletes the extension from the cluster" { + az $Env:K8sExtensionName delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + # Extension should not be found on the cluster + az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeFalse + } + + It "Performs another list after the delete" { + $output = az $Env:K8sExtensionName list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } + $extensionExists | Should -BeNullOrEmpty + } +} diff --git a/testing/test/helper/Constants.ps1 b/testing/test/helper/Constants.ps1 new file mode 100644 index 00000000000..3ecd3621bc5 --- /dev/null +++ b/testing/test/helper/Constants.ps1 @@ -0,0 +1,7 @@ +$ENVCONFIG = Get-Content -Path $PSScriptRoot/../../settings.json | ConvertFrom-Json +$SUCCESS_MESSAGE = "Successfully installed the extension" +$FAILED_MESSAGE = "Failed to install the extension" + +$POD_RUNNING = "Running" + +$MAX_RETRY_ATTEMPTS = 10 \ No newline at end of file diff --git a/testing/test/helper/Helper.ps1 b/testing/test/helper/Helper.ps1 new file mode 100644 index 00000000000..362a4eedf18 --- /dev/null +++ b/testing/test/helper/Helper.ps1 @@ -0,0 +1,47 @@ +function Get-ExtensionData { + param( + [string]$extensionName + ) + + $output = kubectl get extensionconfigs -A -o json | ConvertFrom-Json + return $output.items | Where-Object { $_.metadata.name -eq $extensionName } +} + +function Get-ExtensionStatus { + param( + [string]$extensionName + ) + + $extensionData = Get-ExtensionData $extensionName + if ($extensionData) { + return $extensionData.status.status + } + return $null +} + +function Get-PodStatus { + param( + [string]$podName, + [string]$Namespace + ) + + $allPodData = kubectl get pods -n $Namespace -o json | ConvertFrom-Json + $podData = $allPodData.items | Where-Object { $_.metadata.name -Match $podName } + if ($podData.Length -gt 1) { + return $podData[0].status.phase + } + return $podData.status.phase +} + +function Get-ExtensionConfigurationSettings { + param( + [string]$extensionName, + [string]$configKey + ) + + $extensionData = Get-ExtensionData $extensionName + if ($extensionData) { + return $extensionData.spec.parameter."$configKey" + } + return $null +} From 1ded4054cff3b90debca2ef41e2f15da99004ee0 Mon Sep 17 00:00:00 2001 From: Lia Kazakova <58274127+liakaz@users.noreply.github.com> Date: Mon, 26 Apr 2021 21:32:15 -0700 Subject: [PATCH 40/86] Inference CLI validation for Scoring FE (#24) * cli validation starter * added the call to the fe validation function * nodeport validation not required * test fix Co-authored-by: Jonathan Innis --- .../partner_extensions/AzureMLKubernetes.py | 30 +++++++++++++++++++ .../public/AzureMLKubernetes.Tests.ps1 | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py index 34aac58e017..1338765bab2 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py @@ -171,12 +171,42 @@ def __validate_config(self, configuration_settings, configuration_protected_sett "for Machine Learning training or inference by specifying " f"'--configuration-settings {self.ENABLE_TRAINING}=true' or '--configuration-settings {self.ENABLE_INFERENCE}=true'") + self.__validate_scoring_fe_settings(configuration_settings, configuration_protected_settings) + configuration_settings[self.ENABLE_TRAINING] = configuration_settings.get(self.ENABLE_TRAINING, enable_training) configuration_settings[self.ENABLE_INFERENCE] = configuration_settings.get( self.ENABLE_INFERENCE, enable_inference) configuration_protected_settings.pop(self.ENABLE_TRAINING, None) configuration_protected_settings.pop(self.ENABLE_INFERENCE, None) + def __validate_scoring_fe_settings(self, configuration_settings, configuration_protected_settings): + clusterPurpose = _get_value_from_config_protected_config( + 'clusterPurpose', configuration_settings, configuration_protected_settings) + if clusterPurpose and clusterPurpose not in ["DevTest", "FastProd"]: + raise InvalidArgumentValueError( + "Accepted values for '--configuration-settings clusterPurpose' " + "are 'DevTest' and 'FastProd'") + + feSslCert = _get_value_from_config_protected_config( + 'scoringFe.sslCert', configuration_settings, configuration_protected_settings) + sslKey = _get_value_from_config_protected_config( + 'scoringFe.sslKey', configuration_settings, configuration_protected_settings) + allowInsecureConnections = _get_value_from_config_protected_config( + 'allowInsecureConnections', configuration_settings, configuration_protected_settings) + allowInsecureConnections = str(allowInsecureConnections).lower() == 'true' + if (not feSslCert or not sslKey) and not allowInsecureConnections: + raise InvalidArgumentValueError( + "Provide ssl certificate and key. " + "Otherwise explicitly allow insecure connection by specifying " + "'--configuration-settings allowInsecureConnections=true'") + + feIsInternalLoadBalancer = _get_value_from_config_protected_config( + 'scoringFe.serviceType.internalLoadBalancer', configuration_settings, configuration_protected_settings) + feIsInternalLoadBalancer = str(feIsInternalLoadBalancer).lower() == 'true' + if feIsInternalLoadBalancer: + logger.warn( + 'Internal load balancer only supported on AKS and AKS Engine Clusters.') + def __create_required_resource( self, cmd, configuration_settings, configuration_protected_settings, subscription_id, resource_group_name, cluster_name, cluster_location): diff --git a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 index 344e36a296c..a434544da12 100644 --- a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 +++ b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 @@ -10,7 +10,7 @@ Describe 'AzureML Kubernetes Testing' { } It 'Creates the extension and checks that it onboards correctly' { - $output = az k8s-extension create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType --name $extensionName --release-train preview --config enableTraining=true + $output = az k8s-extension create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType --name $extensionName --release-train preview --config enableTraining=true allowInsecureConnections=true $? | Should -BeTrue $output = az k8s-extension show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName From 53303d5e8772a9a85edd0a907ceea4d62e6cc002 Mon Sep 17 00:00:00 2001 From: Lia Kazakova <58274127+liakaz@users.noreply.github.com> Date: Tue, 27 Apr 2021 12:16:22 -0700 Subject: [PATCH 41/86] legal warning added (#27) --- .../partner_extensions/AzureMLKubernetes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py index 1338765bab2..efe261f6a5b 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py @@ -165,7 +165,9 @@ def __validate_config(self, configuration_settings, configuration_protected_sett self.ENABLE_INFERENCE, configuration_settings, configuration_protected_settings) enable_inference = str(enable_inference).lower() == 'true' - if not (enable_training or enable_inference): + if enable_inference: + logger.warn("The installed AzureML extension for AML inference is experimental and not covered by customer support. Please use with discretion.") + elif not (enable_training or enable_inference): raise InvalidArgumentValueError( "Please create Microsoft.AzureML.Kubernetes extension instance either " "for Machine Learning training or inference by specifying " From 3370264880fc0851d9cefa288400060e2c506ddb Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Tue, 27 Apr 2021 12:20:37 -0700 Subject: [PATCH 42/86] Remove deprecated method logger.warn --- .../partner_extensions/AzureMLKubernetes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py index efe261f6a5b..34d71beb829 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py @@ -153,8 +153,8 @@ def __validate_config(self, configuration_settings, configuration_protected_sett dup_keys = set(config_keys) & set(config_protected_keys) if len(dup_keys) > 0: for key in dup_keys: - logger.warn( - f'Duplicate keys found in both configuration settings and configuration protected setttings: {key}') + logger.warning( + 'Duplicate keys found in both configuration settings and configuration protected setttings: %s', key) raise InvalidArgumentValueError("Duplicate keys found.") enable_training = _get_value_from_config_protected_config( @@ -166,7 +166,7 @@ def __validate_config(self, configuration_settings, configuration_protected_sett enable_inference = str(enable_inference).lower() == 'true' if enable_inference: - logger.warn("The installed AzureML extension for AML inference is experimental and not covered by customer support. Please use with discretion.") + logger.warning("The installed AzureML extension for AML inference is experimental and not covered by customer support. Please use with discretion.") elif not (enable_training or enable_inference): raise InvalidArgumentValueError( "Please create Microsoft.AzureML.Kubernetes extension instance either " @@ -206,7 +206,7 @@ def __validate_scoring_fe_settings(self, configuration_settings, configuration_p 'scoringFe.serviceType.internalLoadBalancer', configuration_settings, configuration_protected_settings) feIsInternalLoadBalancer = str(feIsInternalLoadBalancer).lower() == 'true' if feIsInternalLoadBalancer: - logger.warn( + logger.warning( 'Internal load balancer only supported on AKS and AKS Engine Clusters.') def __create_required_resource( From 4c66aeff079be3476da8d3b4673d27371b7a74d4 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Tue, 27 Apr 2021 13:09:31 -0700 Subject: [PATCH 43/86] Update k8s-custom-pipelines.yml for Azure Pipelines --- k8s-custom-pipelines.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index 00d87b68d13..7cc32e49d04 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -2,6 +2,7 @@ trigger: batch: true branches: include: + - release* - k8s-extension/public - k8s-extension/private pr: From e8651f2fa16365adc7d45826d59b6159882784e0 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Tue, 27 Apr 2021 13:10:00 -0700 Subject: [PATCH 44/86] Update k8s-custom-pipelines.yml for Azure Pipelines --- k8s-custom-pipelines.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index 7cc32e49d04..00d87b68d13 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -2,7 +2,6 @@ trigger: batch: true branches: include: - - release* - k8s-extension/public - k8s-extension/private pr: From 9de1e4ef7d455c715e44721e863f7cb3ebd1f749 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Wed, 28 Apr 2021 13:03:33 -0700 Subject: [PATCH 45/86] Add Azure Defender to E2E testing (#28) * Add azure defender testing to e2e * Remove the debug flag --- testing/.gitignore | 2 +- .../bin/k8s_extension-0.2.0-py3-none-any.whl | Bin 46987 -> 0 bytes .../bin/k8s_extension-0.3.0-py3-none-any.whl | Bin 0 -> 52893 bytes testing/settings.template.json | 4 +- .../extensions/public/AzureDefender.Tests.ps1 | 93 ++++++++++++++++++ 5 files changed, 96 insertions(+), 3 deletions(-) delete mode 100644 testing/bin/k8s_extension-0.2.0-py3-none-any.whl create mode 100644 testing/bin/k8s_extension-0.3.0-py3-none-any.whl create mode 100644 testing/test/extensions/public/AzureDefender.Tests.ps1 diff --git a/testing/.gitignore b/testing/.gitignore index 7df7f6a7294..745d8708c4e 100644 --- a/testing/.gitignore +++ b/testing/.gitignore @@ -2,7 +2,7 @@ settings.json tmp/ bin/* !bin/connectedk8s-1.0.0-py3-none-any.whl -!bin/k8s_extension-0.2.0-py3-none-any.whl +!bin/k8s_extension-0.3.0-py3-none-any.whl !bin/k8s_extension_private-0.1.0-py3-none-any.whl !bin/connectedk8s-values.yaml *.xml \ No newline at end of file diff --git a/testing/bin/k8s_extension-0.2.0-py3-none-any.whl b/testing/bin/k8s_extension-0.2.0-py3-none-any.whl deleted file mode 100644 index bdb9dbafdcf64c2526a3baaafdc61ad5538a9c0c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46987 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 diff --git a/testing/bin/k8s_extension-0.3.0-py3-none-any.whl b/testing/bin/k8s_extension-0.3.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..feb28b80b437c8af966b3b87a4fcad1a9b74d108 GIT binary patch literal 52893 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 b*Hx{YBsky%^Yh?A4x|a#NYixu{O$h(4U{Xp literal 0 HcmV?d00001 diff --git a/testing/settings.template.json b/testing/settings.template.json index 5d459b44bee..04eda8e4dfa 100644 --- a/testing/settings.template.json +++ b/testing/settings.template.json @@ -5,8 +5,8 @@ "arcClusterName": "", "extensionVersion": { - "k8s-extension": "0.2.0", + "k8s-extension": "0.3.0", "k8s-extension-private": "0.1.0", - "connectedk8s": "0.3.5" + "connectedk8s": "1.0.0" } } \ No newline at end of file diff --git a/testing/test/extensions/public/AzureDefender.Tests.ps1 b/testing/test/extensions/public/AzureDefender.Tests.ps1 new file mode 100644 index 00000000000..2d199e31638 --- /dev/null +++ b/testing/test/extensions/public/AzureDefender.Tests.ps1 @@ -0,0 +1,93 @@ +Describe 'Azure Defender Testing' { + BeforeAll { + $extensionType = "microsoft.azuredefender.kubernetes" + $extensionName = "microsoft.azuredefender.kubernetes" + $extensionAgentNamespace = "azuredefender" + + . $PSScriptRoot/../../helper/Constants.ps1 + . $PSScriptRoot/../../helper/Helper.ps1 + } + + It 'Creates the extension and checks that it onboards correctly' { + $output = az $Env:K8sExtensionName create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType -n $extensionName + $? | Should -BeTrue + + $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue + + # Loop and retry until the extension installs + $n = 0 + do + { + # Only check the extension config, not the pod since this doesn't bring up pods + if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Performs a show on the extension" { + $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + } + + It "Runs an update on the extension on the cluster" { + Set-ItResult -Skipped -Because "Update is not a valid scenario for now" + + # az $Env:K8sExtensionName update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName --auto-upgrade-minor-version false + # $? | Should -BeTrue + + # $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + # $? | Should -BeTrue + + # $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + # $isAutoUpgradeMinorVersion.ToString() -eq "False" | Should -BeTrue + + # # Loop and retry until the extension config updates + # $n = 0 + # do + # { + # $isAutoUpgradeMinorVersion = (Get-ExtensionData $extensionName).spec.autoUpgradeMinorVersion + # if (!$isAutoUpgradeMinorVersion) { #autoUpgradeMinorVersion doesn't exist in ExtensionConfig CRD if false + # if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { + # if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { + # break + # } + # } + # } + # Start-Sleep -Seconds 10 + # $n += 1 + # } while ($n -le $MAX_RETRY_ATTEMPTS) + # $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Lists the extensions on the cluster" { + $output = az $Env:K8sExtensionName list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $? | Should -BeTrue + + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } + $extensionExists | Should -Not -BeNullOrEmpty + } + + It "Deletes the extension from the cluster" { + az $Env:K8sExtensionName delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + # Extension should not be found on the cluster + az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeFalse + } + + It "Performs another list after the delete" { + $output = az $Env:K8sExtensionName list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } + $extensionExists | Should -BeNullOrEmpty + } +} From 9c0317d7bf9ad42e4d890f3924735ecad4149aef Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Wed, 28 Apr 2021 16:25:30 -0700 Subject: [PATCH 46/86] Add configuration testing --- testing/Test.ps1 | 85 ++++++----- .../Configuration.HTTPS.Tests.ps1 | 54 +++++++ .../Configuration.HelmOperator.Tests.ps1 | 137 ++++++++++++++++++ .../Configuration.KnownHost.Tests.ps1 | 6 + .../Configuration.PrivateKey.Tests.ps1 | 86 +++++++++++ .../configurations/Configuration.Tests.ps1 | 80 ++++++++++ testing/test/configurations/Constants.ps1 | 8 + testing/test/configurations/Helper.ps1 | 45 ++++++ 8 files changed, 461 insertions(+), 40 deletions(-) create mode 100644 testing/test/configurations/Configuration.HTTPS.Tests.ps1 create mode 100644 testing/test/configurations/Configuration.HelmOperator.Tests.ps1 create mode 100644 testing/test/configurations/Configuration.KnownHost.Tests.ps1 create mode 100644 testing/test/configurations/Configuration.PrivateKey.Tests.ps1 create mode 100644 testing/test/configurations/Configuration.Tests.ps1 create mode 100644 testing/test/configurations/Constants.ps1 create mode 100644 testing/test/configurations/Helper.ps1 diff --git a/testing/Test.ps1 b/testing/Test.ps1 index 6304c13dc28..343f0eef2cc 100644 --- a/testing/Test.ps1 +++ b/testing/Test.ps1 @@ -18,59 +18,64 @@ az account set --subscription $ENVCONFIG.subscriptionId $Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" -if ($ExtensionType -eq "Public") { - $k8sExtensionVersion = $ENVCONFIG.extensionVersion.'k8s-extension' - $Env:K8sExtensionName = "k8s-extension" +if ($Type -eq 'Extension') { + if ($ExtensionType -eq "Public") { + $k8sExtensionVersion = $ENVCONFIG.extensionVersion.'k8s-extension' + $Env:K8sExtensionName = "k8s-extension" - if (!$SkipInstall) { - Write-Host "Removing the old k8s-extension extension..." - az extension remove -n k8s-extension - Write-Host "Installing k8s-extension version $k8sExtensionVersion..." - az extension add --source ./bin/k8s_extension-$k8sExtensionVersion-py3-none-any.whl - if (!$?) { - Write-Host "Unable to find k8s-extension version $k8sExtensionVersion, exiting..." - exit 1 + if (!$SkipInstall) { + Write-Host "Removing the old k8s-extension extension..." + az extension remove -n k8s-extension + Write-Host "Installing k8s-extension version $k8sExtensionVersion..." + az extension add --source ./bin/k8s_extension-$k8sExtensionVersion-py3-none-any.whl + if (!$?) { + Write-Host "Unable to find k8s-extension version $k8sExtensionVersion, exiting..." + exit 1 + } + } + } else { + $k8sExtensionPrivateVersion = $ENVCONFIG.extensionVersion.'k8s-extension-private' + $Env:K8sExtensionName = "k8s-extension-private" + + if (!$SkipInstall) { + Write-Host "Removing the old k8s-extension-private extension..." + az extension remove -n k8s-extension-private + Write-Host "Installing k8s-extension-private version $k8sExtensionPrivateVersion..." + az extension add --source ./bin/k8s_extension_private-$k8sExtensionPrivateVersion-py3-none-any.whl + if (!$?) { + Write-Host "Unable to find k8s-extension-private version $k8sExtensionPrivateVersion, exiting..." + exit 1 + } } } -} else { - $k8sExtensionPrivateVersion = $ENVCONFIG.extensionVersion.'k8s-extension-private' - $Env:K8sExtensionName = "k8s-extension-private" + if ($OnlyPublicTests) { + $testFilePath = "$PSScriptRoot/test/extensions/public" + } else { + $testFilePath = "$PSScriptRoot/test/extensions" + } +} +if ($Type -eq 'Configuration') { + $k8sConfigurationVersion = $ENVCONFIG.extensionVersion.'k8s-configuration' if (!$SkipInstall) { - Write-Host "Removing the old k8s-extension-private extension..." - az extension remove -n k8s-extension-private - Write-Host "Installing k8s-extension-private version $k8sExtensionPrivateVersion..." - az extension add --source ./bin/k8s_extension_private-$k8sExtensionPrivateVersion-py3-none-any.whl - if (!$?) { - Write-Host "Unable to find k8s-extension-private version $k8sExtensionPrivateVersion, exiting..." - exit 1 - } + Write-Host "Removing the old k8s-configuration extension..." + az extension remove -n k8s-configuration + Write-Host "Installing k8s-configuration version $k8sConfigurationVersion..." + az extension add --source ./extensions/k8s_configuration-$k8sConfigurationVersion-py3-none-any.whl } + $testFilePath = "$PSScriptRoot/test/configurations" } if ($CI) { - if ($OnlyPublicTests) { - Write-Host "Invoking Pester to run tests from '$PSScriptRoot/test/extensions/public'" - $testResult = Invoke-Pester $PSScriptRoot/test/extensions/public -Passthru -Output Detailed - $testResult | Export-JUnitReport -Path TestResults.xml - } - else { - Write-Host "Invoking Pester to run tests from '$PSScriptRoot/test/extensions'" - $testResult = Invoke-Pester $PSScriptRoot/test/extensions -Passthru -Output Detailed - $testResult | Export-JUnitReport -Path TestResults.xml - } + Write-Host "Invoking Pester to run tests from '$testFilePath'..." + $testResult = Invoke-Pester $testFilePath -Passthru -Output Detailed + $testResult | Export-JUnitReport -Path TestResults.xml } else { if ($Path) { Write-Host "Invoking Pester to run tests from '$PSScriptRoot/$Path'" Invoke-Pester -Output Detailed $PSScriptRoot/$Path } else { - if ($OnlyPublicTests) { - Write-Host "Invoking Pester to run tests from '$PSScriptRoot/test/extensions/public'" - Invoke-Pester -Output Detailed $PSScriptRoot/test/extensions/public - } - else { - Write-Host "Invoking Pester to run tests from '$PSScriptRoot/test/extensions'" - Invoke-Pester -Output Detailed $PSScriptRoot/test/extensions - } + Write-Host "Invoking Pester to run tests from '$testFilePath'..." + Invoke-Pester -Output Detailed $testFilePathc } } diff --git a/testing/test/configurations/Configuration.HTTPS.Tests.ps1 b/testing/test/configurations/Configuration.HTTPS.Tests.ps1 new file mode 100644 index 00000000000..a2dee2b348f --- /dev/null +++ b/testing/test/configurations/Configuration.HTTPS.Tests.ps1 @@ -0,0 +1,54 @@ +Describe 'Source Control Configuration (HTTPS) Testing' { + BeforeAll { + $configurationName = "https-config" + . $PSScriptRoot/Constants.ps1 + . $PSScriptRoot/Helper.ps1 + + $dummyValue = "dummyValue" + $secretName = "git-auth-$configurationName" + } + + It 'Creates a configuration with https user and https key on the cluster' { + $output = az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u "https://github.com/Azure/arc-k8s-demo" -n $configurationName --scope cluster --https-user $dummyValue --https-key $dummyValue --operator-namespace $configurationName + $? | Should -BeTrue + + # Loop and retry until the configuration installs and helm pod comes up + $n = 0 + do + { + if (Get-ConfigStatus $configurationName -eq $SUCCESS_MESSAGE) { + if (Get-PodStatus $configurationName -Namespace $configurationName -eq $POD_RUNNING ) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + + Secret-Exists $secretName -Namespace $configurationName + } + + It "Lists the configurations on the cluster" { + $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $? | Should -BeTrue + + $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } + $configExists | Should -Not -BeNullOrEmpty + } + + It "Deletes the configuration from the cluster" { + az k8s-configuration delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeFalse + } + + It "Performs another list after the delete" { + $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } + $configExists | Should -BeNullOrEmpty + } +} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.HelmOperator.Tests.ps1 b/testing/test/configurations/Configuration.HelmOperator.Tests.ps1 new file mode 100644 index 00000000000..8b89ba24c58 --- /dev/null +++ b/testing/test/configurations/Configuration.HelmOperator.Tests.ps1 @@ -0,0 +1,137 @@ +Describe 'Source Control Configuration (Helm Operator Properties) Testing' { + BeforeAll { + $configurationName = "helm-enabled-config" + . $PSScriptRoot/Constants.ps1 + . $PSScriptRoot/Helper.ps1 + + $customOperatorParams = "--set helm.versions=v3 --set mycustomhelmvalue=yay" + $customChartVersion = "0.6.0" + } + + It 'Creates a configuration with helm enabled on the cluster' { + $output = az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u "https://github.com/Azure/arc-k8s-demo" -n $configurationName --scope cluster --enable-helm-operator --operator-namespace $configurationName --helm-operator-params "--set helm.versions=v3" + $? | Should -BeTrue + + # Loop and retry until the configuration installs and helm pod comes up + $n = 0 + do + { + if (Get-ConfigStatus $configurationName -eq $SUCCESS_MESSAGE) { + if (Get-PodStatus "$configurationName-helm" -Namespace $configurationName -eq $POD_RUNNING ) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Updates the helm operator params and performs a show" { + Set-ItResult -Skipped -Because "Update is not a valid scenario for now" + + az k8s-configuration update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -n $configurationName --helm-operator-params $customOperatorParams + $? | Should -BeTrue + + $output = az k8s-configuration show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeTrue + + $configData = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } + ($configData.helmOperatorProperties.chartValues -eq $customOperatorParams) | Should -BeTrue + + # Loop and retry until the configuration updates + $n = 0 + do + { + $helmOperatorChartValues = (Get-ConfigData $configurationName).spec.helmOperatorProperties.chartValues + if ($helmOperatorChartValues -ne $null -And $helmOperatorChartValues.ToString() -eq $customOperatorParams) { + if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Updates the helm operator chart version and performs a show" { + Set-ItResult -Skipped -Because "Update is not a valid scenario for now" + + az k8s-configuration update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -n $configurationName --helm-operator-chart-version $customChartVersion + $? | Should -BeTrue + + $output = az k8s-configuration show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeTrue + + # Check that the helmOperatorProperties chartValues didn't change + $configData = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } + ($configData.helmOperatorProperties.chartValues -eq $customOperatorParams) | Should -BeTrue + ($configData.helmOperatorProperties.chartVersion -eq $customChartVersion) | Should -BeTrue + + # Loop and retry until the configuration updates + $n = 0 + do + { + $helmOperatorChartVersion = (Get-ConfigData $configurationName).spec.helmOperatorProperties.chartVersion + if ($helmOperatorChartVersion -ne $null -And $helmOperatorChartVersion.ToString() -eq $customChartVersion) { + if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Disables the helm operator on the cluster" { + Set-ItResult -Skipped -Because "Update is not a valid scenario for now" + + az k8s-configuration update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -n $configurationName --enable-helm-operator=false + $? | Should -BeTrue + + $output = az k8s-configuration show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeTrue + + $helmOperatorEnabled = ($output | ConvertFrom-Json).enableHelmOperator + $helmOperatorEnabled.ToString() -eq "False" | Should -BeTrue + + # Loop and retry until the configuration updates + $n = 0 + do { + $helmOperatorEnabled = (Get-ConfigData $configurationName).spec.enableHelmOperator + if ($helmOperatorEnabled -ne $null -And $helmOperatorEnabled.ToString() -eq "False") { + if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Lists the configurations on the cluster" { + $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $? | Should -BeTrue + + $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } + $configExists | Should -Not -BeNullOrEmpty + } + + It "Deletes the configuration from the cluster" { + az k8s-configuration delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeFalse + } + + It "Performs another list after the delete" { + $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } + $configExists | Should -BeNullOrEmpty + } +} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.KnownHost.Tests.ps1 b/testing/test/configurations/Configuration.KnownHost.Tests.ps1 new file mode 100644 index 00000000000..2cb2946bc3e --- /dev/null +++ b/testing/test/configurations/Configuration.KnownHost.Tests.ps1 @@ -0,0 +1,6 @@ +Describe 'Source Control Configuration (SSH Configs) Testing' { + BeforeAll { + . $PSScriptRoot/Constants.ps1 + . $PSScriptRoot/Helper.ps1 + } +} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.PrivateKey.Tests.ps1 b/testing/test/configurations/Configuration.PrivateKey.Tests.ps1 new file mode 100644 index 00000000000..4bf86d52012 --- /dev/null +++ b/testing/test/configurations/Configuration.PrivateKey.Tests.ps1 @@ -0,0 +1,86 @@ +Describe 'Source Control Configuration (SSH Configs) Testing' { + BeforeAll { + . $PSScriptRoot/Constants.ps1 + . $PSScriptRoot/Helper.ps1 + + $RSA_KEYPATH = "$TMP_DIRECTORY\rsa.private" + $DSA_KEYPATH = "$TMP_DIRECTORY\dsa.private" + $ECDSA_KEYPATH = "$TMP_DIRECTORY\ecdsa.private" + $ED25519_KEYPATH = "$TMP_DIRECTORY\ed25519.private" + + $KEY_ARR = [System.Tuple]::Create("rsa", $RSA_KEYPATH), [System.Tuple]::Create("dsa", $DSA_KEYPATH), [System.Tuple]::Create("ecdsa", $ECDSA_KEYPATH), [System.Tuple]::Create("ed25519", $ED25519_KEYPATH) + foreach ($keyTuple in $KEY_ARR) { + # Automattically say yes to overwrite with ssh-keygen + Write-Output "y" | ssh-keygen -t $keyTuple.Item1 -f $keyTuple.Item2 -P """" + } + + $SSH_GIT_URL = "git://github.com/anubhav929/flux-get-started.git" + $HTTP_GIT_URL = "https://github.com/Azure/arc-k8s-demo" + + $configDataRSA = [System.Tuple]::Create("rsa-config", $RSA_KEYPATH) + $configDataDSA = [System.Tuple]::Create("dsa-config", $DSA_KEYPATH) + $configDataECDSA = [System.Tuple]::Create("ecdsa-config", $ECDSA_KEYPATH) + $configDataED25519 = [System.Tuple]::Create("ed25519-config", $ED25519_KEYPATH) + + $CONFIG_ARR = $configDataRSA, $configDataDSA, $configDataECDSA, $configDataED25519 + } + + It 'Creates a configuration with each type of ssh private key' { + foreach($configData in $CONFIG_ARR) { + az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u $SSH_GIT_URL -n $configData.Item1 --scope cluster --operator-namespace $configData.Item1 --ssh-private-key-file $configData.Item2 + $? | Should -BeTrue + } + + # Loop and retry until the configuration installs and helm pod comes up + $n = 0 + do + { + $readyConfigs = 0 + foreach($configData in $CONFIG_ARR) { + # TODO: Change this to checking the success message after we merge in the bugfix into the agent + if (Get-PodStatus $configData.Item1 -Namespace $configData.Item1 -eq $POD_RUNNING) { + $readyConfigs += 1 + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le 30 -And $readyConfigs -ne 4) + $n | Should -BeLessOrEqual 30 + } + + It 'Fails when trying to create a configuration with ssh url and https auth values' { + az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u $HTTP_GIT_URL -n "config-should-fail" --scope cluster --operator-namespace "config-should-fail" --ssh-private-key-file $RSA_KEYPATH + $? | Should -BeFalse + } + + It "Lists the configurations on the cluster" { + $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $? | Should -BeTrue + + foreach ($configData in $CONFIG_ARR) { + $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configData.Item1 } + $configExists | Should -Not -BeNullOrEmpty + } + } + + It "Deletes the configuration from the cluster" { + foreach ($configData in $CONFIG_ARR) { + az k8s-configuration delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configData.Item1 + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configData.Item1 + $? | Should -BeFalse + } + } + + It "Performs another list after the delete" { + $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $? | Should -BeTrue + + foreach ($configData in $CONFIG_ARR) { + $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configData.Item1 } + $configExists | Should -BeNullOrEmpty + } + } +} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.Tests.ps1 b/testing/test/configurations/Configuration.Tests.ps1 new file mode 100644 index 00000000000..a85df42ed2e --- /dev/null +++ b/testing/test/configurations/Configuration.Tests.ps1 @@ -0,0 +1,80 @@ +Describe 'Basic Source Control Configuration Testing' { + BeforeAll { + $configurationName = "basic-config" + . $PSScriptRoot/Constants.ps1 + . $PSScriptRoot/Helper.ps1 + } + + It 'Creates a configuration and checks that it onboards correctly' { + az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u "https://github.com/Azure/arc-k8s-demo" -n $configurationName --scope cluster --enable-helm-operator=false --operator-namespace $configurationName + $? | Should -BeTrue + + # Loop and retry until the configuration installs + $n = 0 + do + { + if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { + break + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Performs a show on the configuration" { + $output = az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -n $configurationName + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + } + + It "Runs an update on the configuration on the cluster" { + Set-ItResult -Skipped -Because "Update is not a valid scenario for now" + + az k8s-configuration update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName --enable-helm-operator + $? | Should -BeTrue + + $output = az k8s-configuration show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeTrue + + $helmOperatorEnabled = ($output | ConvertFrom-Json).enableHelmOperator + $helmOperatorEnabled.ToString() -eq "True" | Should -BeTrue + + # Loop and retry until the configuration updates + $n = 0 + do { + $helmOperatorEnabled = (Get-ConfigData $configurationName).spec.enableHelmOperator + if ($helmOperatorEnabled -And $helmOperatorEnabled.ToString() -eq "True") { + if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Lists the configurations on the cluster" { + $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $? | Should -BeTrue + + $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } + $configExists | Should -Not -BeNullOrEmpty + } + + It "Deletes the configuration from the cluster" { + az k8s-configuration delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeTrue + + # Configuration should be removed from the resource model + az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName + $? | Should -BeFalse + } + + It "Performs another list after the delete" { + $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } + $configExists | Should -BeNullOrEmpty + } +} \ No newline at end of file diff --git a/testing/test/configurations/Constants.ps1 b/testing/test/configurations/Constants.ps1 new file mode 100644 index 00000000000..f1e8c6ffdc3 --- /dev/null +++ b/testing/test/configurations/Constants.ps1 @@ -0,0 +1,8 @@ +$ENVCONFIG = Get-Content -Path $PSScriptRoot\..\..\settings.json | ConvertFrom-Json +$SUCCESS_MESSAGE = "Successfully installed the operator" +$FAILED_MESSAGE = "Failed the install of the operator" +$TMP_DIRECTORY = "$PSScriptRoot\..\..\tmp" + +$POD_RUNNING = "Running" + +$MAX_RETRY_ATTEMPTS = 18 \ No newline at end of file diff --git a/testing/test/configurations/Helper.ps1 b/testing/test/configurations/Helper.ps1 new file mode 100644 index 00000000000..842e2da84aa --- /dev/null +++ b/testing/test/configurations/Helper.ps1 @@ -0,0 +1,45 @@ +function Get-ConfigData { + param( + [string]$configName + ) + + $output = kubectl get gitconfigs -A -o json | ConvertFrom-Json + return $output.items | Where-Object { $_.metadata.name -eq $configurationName } +} + +function Get-ConfigStatus { + param( + [string]$configName + ) + + $configData = Get-ConfigData $configName + if ($configData -ne $null) { + return $configData.status.status + } + return $null +} + +function Get-PodStatus { + param( + [string]$podName, + [string]$Namespace + ) + + $allPodData = kubectl get pods -n $Namespace -o json | ConvertFrom-Json + $podData = $allPodData.items | Where-Object { $_.metadata.name -Match $podName } + return $podData.status.phase +} + +function Secret-Exists { + param( + [string]$secretName, + [string]$Namespace + ) + + $allSecretData = kubectl get secrets -n $Namespace -o json | ConvertFrom-Json + $secretData = $allSecretData.items | Where-Object { $_.metadata.name -Match $secretName } + if ($secretData.Length -ge 1) { + return $true + } + return $false +} \ No newline at end of file From 4c214821dbd3e40b8b6b00db4c6e8c14b25e1494 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Wed, 28 Apr 2021 16:37:40 -0700 Subject: [PATCH 47/86] Fix pipeline failures --- k8s-custom-pipelines.yml | 4 ++-- testing/.gitignore | 1 + testing/Test.ps1 | 10 +++++++--- .../k8s_configuration-1.0.0-py3-none-any.whl | Bin 0 -> 42351 bytes 4 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 testing/bin/k8s_configuration-1.0.0-py3-none-any.whl diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index 00d87b68d13..99f0c95fcb6 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -115,7 +115,7 @@ stages: scriptType: pscore scriptLocation: inlineScript inlineScript: | - .\Test.ps1 -CI -ExtensionType Public -OnlyPublicTests + .\Test.ps1 -CI -ExtensionType Public -OnlyPublicTests -Type k8s-extension workingDirectory: $(TEST_PATH) continueOnError: true condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'False')) @@ -127,7 +127,7 @@ stages: scriptType: pscore scriptLocation: inlineScript inlineScript: | - .\Test.ps1 -CI -ExtensionType Public + .\Test.ps1 -CI -ExtensionType Public -Type k8s-extension workingDirectory: $(TEST_PATH) continueOnError: true condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'True')) diff --git a/testing/.gitignore b/testing/.gitignore index 745d8708c4e..083fdfb5c25 100644 --- a/testing/.gitignore +++ b/testing/.gitignore @@ -4,5 +4,6 @@ bin/* !bin/connectedk8s-1.0.0-py3-none-any.whl !bin/k8s_extension-0.3.0-py3-none-any.whl !bin/k8s_extension_private-0.1.0-py3-none-any.whl +!bin/k8s_configuration-1.0.0-py3-none-any.whl !bin/connectedk8s-values.yaml *.xml \ No newline at end of file diff --git a/testing/Test.ps1 b/testing/Test.ps1 index 343f0eef2cc..ba90e877654 100644 --- a/testing/Test.ps1 +++ b/testing/Test.ps1 @@ -6,7 +6,11 @@ param ( [Parameter(Mandatory=$True)] [ValidateSet('Public','Private')] - [string]$ExtensionType + [string]$ExtensionType, + + [Parameter(Mandatory=$True)] + [ValidateSet('k8s-extension','k8s-configuration')] + [string]$Type ) # Disable confirm prompt for script @@ -18,7 +22,7 @@ az account set --subscription $ENVCONFIG.subscriptionId $Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" -if ($Type -eq 'Extension') { +if ($Type -eq 'k8s-extension') { if ($ExtensionType -eq "Public") { $k8sExtensionVersion = $ENVCONFIG.extensionVersion.'k8s-extension' $Env:K8sExtensionName = "k8s-extension" @@ -55,7 +59,7 @@ if ($Type -eq 'Extension') { } } -if ($Type -eq 'Configuration') { +if ($Type -eq 'k8s-configuration') { $k8sConfigurationVersion = $ENVCONFIG.extensionVersion.'k8s-configuration' if (!$SkipInstall) { Write-Host "Removing the old k8s-configuration extension..." diff --git a/testing/bin/k8s_configuration-1.0.0-py3-none-any.whl b/testing/bin/k8s_configuration-1.0.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..cc8e8e0995f81da31f6f919f83d344cc164e9568 GIT binary patch literal 42351 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 Date: Wed, 28 Apr 2021 17:02:17 -0700 Subject: [PATCH 48/86] Make test script more intuitive --- k8s-custom-pipelines.yml | 4 +-- testing/Test.ps1 | 55 ++++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index 99f0c95fcb6..3c76c8f925b 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -115,7 +115,7 @@ stages: scriptType: pscore scriptLocation: inlineScript inlineScript: | - .\Test.ps1 -CI -ExtensionType Public -OnlyPublicTests -Type k8s-extension + .\Test.ps1 -CI -OnlyPublicTests -Type k8s-extension workingDirectory: $(TEST_PATH) continueOnError: true condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'False')) @@ -127,7 +127,7 @@ stages: scriptType: pscore scriptLocation: inlineScript inlineScript: | - .\Test.ps1 -CI -ExtensionType Public -Type k8s-extension + .\Test.ps1 -CI -Type k8s-extension workingDirectory: $(TEST_PATH) continueOnError: true condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'True')) diff --git a/testing/Test.ps1 b/testing/Test.ps1 index ba90e877654..7429a121a21 100644 --- a/testing/Test.ps1 +++ b/testing/Test.ps1 @@ -9,7 +9,7 @@ param ( [string]$ExtensionType, [Parameter(Mandatory=$True)] - [ValidateSet('k8s-extension','k8s-configuration')] + [ValidateSet('k8s-extension','k8s-configuration', 'k8s-extension-private')] [string]$Type ) @@ -23,33 +23,36 @@ az account set --subscription $ENVCONFIG.subscriptionId $Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" if ($Type -eq 'k8s-extension') { - if ($ExtensionType -eq "Public") { - $k8sExtensionVersion = $ENVCONFIG.extensionVersion.'k8s-extension' - $Env:K8sExtensionName = "k8s-extension" + $k8sExtensionVersion = $ENVCONFIG.extensionVersion.'k8s-extension' + $Env:K8sExtensionName = "k8s-extension" - if (!$SkipInstall) { - Write-Host "Removing the old k8s-extension extension..." - az extension remove -n k8s-extension - Write-Host "Installing k8s-extension version $k8sExtensionVersion..." - az extension add --source ./bin/k8s_extension-$k8sExtensionVersion-py3-none-any.whl - if (!$?) { - Write-Host "Unable to find k8s-extension version $k8sExtensionVersion, exiting..." - exit 1 - } + if (!$SkipInstall) { + Write-Host "Removing the old k8s-extension extension..." + az extension remove -n k8s-extension + Write-Host "Installing k8s-extension version $k8sExtensionVersion..." + az extension add --source ./bin/k8s_extension-$k8sExtensionVersion-py3-none-any.whl + if (!$?) { + Write-Host "Unable to find k8s-extension version $k8sExtensionVersion, exiting..." + exit 1 } + } + if ($OnlyPublicTests) { + $testFilePath = "$PSScriptRoot/test/extensions/public" } else { - $k8sExtensionPrivateVersion = $ENVCONFIG.extensionVersion.'k8s-extension-private' - $Env:K8sExtensionName = "k8s-extension-private" + $testFilePath = "$PSScriptRoot/test/extensions" + } +} elseif ($Type -eq 'k8s-extension-private') { + $k8sExtensionPrivateVersion = $ENVCONFIG.extensionVersion.'k8s-extension-private' + $Env:K8sExtensionName = "k8s-extension-private" - if (!$SkipInstall) { - Write-Host "Removing the old k8s-extension-private extension..." - az extension remove -n k8s-extension-private - Write-Host "Installing k8s-extension-private version $k8sExtensionPrivateVersion..." - az extension add --source ./bin/k8s_extension_private-$k8sExtensionPrivateVersion-py3-none-any.whl - if (!$?) { - Write-Host "Unable to find k8s-extension-private version $k8sExtensionPrivateVersion, exiting..." - exit 1 - } + if (!$SkipInstall) { + Write-Host "Removing the old k8s-extension-private extension..." + az extension remove -n k8s-extension-private + Write-Host "Installing k8s-extension-private version $k8sExtensionPrivateVersion..." + az extension add --source ./bin/k8s_extension_private-$k8sExtensionPrivateVersion-py3-none-any.whl + if (!$?) { + Write-Host "Unable to find k8s-extension-private version $k8sExtensionPrivateVersion, exiting..." + exit 1 } } if ($OnlyPublicTests) { @@ -57,9 +60,7 @@ if ($Type -eq 'k8s-extension') { } else { $testFilePath = "$PSScriptRoot/test/extensions" } -} - -if ($Type -eq 'k8s-configuration') { +} elseif ($Type -eq 'k8s-configuration') { $k8sConfigurationVersion = $ENVCONFIG.extensionVersion.'k8s-configuration' if (!$SkipInstall) { Write-Host "Removing the old k8s-configuration extension..." From 4dca64d6cbf26deba4252764c6aa8718bfbb2f23 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Wed, 28 Apr 2021 17:14:22 -0700 Subject: [PATCH 49/86] Remove parameter from testing --- testing/Test.ps1 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/testing/Test.ps1 b/testing/Test.ps1 index 7429a121a21..e75354355ca 100644 --- a/testing/Test.ps1 +++ b/testing/Test.ps1 @@ -4,10 +4,6 @@ param ( [switch] $CI, [switch] $OnlyPublicTests, - [Parameter(Mandatory=$True)] - [ValidateSet('Public','Private')] - [string]$ExtensionType, - [Parameter(Mandatory=$True)] [ValidateSet('k8s-extension','k8s-configuration', 'k8s-extension-private')] [string]$Type From c93e9584ae4a3f1d4e5568bbeef873b7f774a717 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Thu, 29 Apr 2021 10:22:02 -0700 Subject: [PATCH 50/86] Fix wrong location for k8s config whl --- testing/Test.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/Test.ps1 b/testing/Test.ps1 index e75354355ca..06c089f6f52 100644 --- a/testing/Test.ps1 +++ b/testing/Test.ps1 @@ -62,7 +62,7 @@ if ($Type -eq 'k8s-extension') { Write-Host "Removing the old k8s-configuration extension..." az extension remove -n k8s-configuration Write-Host "Installing k8s-configuration version $k8sConfigurationVersion..." - az extension add --source ./extensions/k8s_configuration-$k8sConfigurationVersion-py3-none-any.whl + az extension add --source ./bin/k8s_configuration-$k8sConfigurationVersion-py3-none-any.whl } $testFilePath = "$PSScriptRoot/test/configurations" } From 3dafc0841fcc322b6bcb4f1ba57d1ae512b12a87 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Thu, 6 May 2021 16:48:22 -0700 Subject: [PATCH 51/86] Fix pip upgrade issue --- k8s-custom-pipelines.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index 3c76c8f925b..b174228e8b6 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -56,6 +56,7 @@ stages: source env/bin/activate # clone azure-cli + pip install --upgrade pip pip install azdev ls $(CLI_REPO_PATH) @@ -196,6 +197,7 @@ stages: source env/bin/activate # clone azure-cli + pip install --upgrade pip pip install azdev ls $(CLI_REPO_PATH) From c5bd1c22e2088c13928cd22313a336823369aee6 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Thu, 6 May 2021 16:50:09 -0700 Subject: [PATCH 52/86] Fix pip install upgrade issue --- k8s-custom-pipelines.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index b174228e8b6..bca200cd7cc 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -236,6 +236,7 @@ stages: # clone azure-cli git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli + pip install --upgrade pip pip install -q azdev azdev setup -c ../azure-cli -r ./ @@ -319,6 +320,7 @@ stages: # clone azure-cli git clone --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli + pip install --upgrade pip pip install azdev azdev --version From d9e87607d7aa59b4ac350c5def45fb91f7ee4b8a Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Thu, 6 May 2021 16:54:33 -0700 Subject: [PATCH 53/86] Fix pip install issue --- azure-pipelines.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 393d779b986..83fc66610d0 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -54,6 +54,7 @@ jobs: # clone azure-cli git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli + pip install --upgrade pip pip install -q azdev==0.1.31 azdev setup -c ../azure-cli -r ./ From 97095048aeedd0e71902c9f9a6fe58a06a11cf30 Mon Sep 17 00:00:00 2001 From: yuyue9284 <15863499+yuyue9284@users.noreply.github.com> Date: Fri, 7 May 2021 08:44:56 +0800 Subject: [PATCH 54/86] delete resurce in testcase (#29) Co-authored-by: Yue Yu Co-authored-by: Jonathan Innis --- .../test/extensions/public/AzureMLKubernetes.Tests.ps1 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 index a434544da12..2bc01244a74 100644 --- a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 +++ b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 @@ -4,6 +4,7 @@ Describe 'AzureML Kubernetes Testing' { $extensionName = "azureml-kubernetes-connector" $extensionAgentNamespace = "azureml" $relayResourceIDKey = "relayserver.hybridConnectionResourceID" + $serviceBusResourceIDKey = "servicebus.resourceID" . $PSScriptRoot/../../helper/Constants.ps1 . $PSScriptRoot/../../helper/Helper.ps1 @@ -78,6 +79,14 @@ Describe 'AzureML Kubernetes Testing' { } It "Deletes the extension from the cluster" { + # cleanup the relay and servicebus + $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey + $serviceBusResourceID = Get-ExtensionConfigurationSettings $extensionName $serviceBusResourceIDKey + $relayNamespaceName = $relayResourceID.split("/")[8] + $serviceBusNamespaceName = $serviceBusResourceID.split("/")[8] + az relay namespace delete --resource-group $ENVCONFIG.resourceGroup --name $relayNamespaceName + az servicebus namespace delete --resource-group $ENVCONFIG.resourceGroup --name $serviceBusNamespaceName + az k8s-extension delete --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName $? | Should -BeTrue From 3f620a85763c9ccaf8b27d9e6af05a0bb62fc02c Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Fri, 7 May 2021 12:23:00 -0700 Subject: [PATCH 55/86] Check Provider is Registered with Subscription Before Making Requests (#18) * Add check for KubernetesConfiguration * Disable pylint and rename * Update provider registration link * Update version * Remove extra blank line * Fix bug in import --- src/k8s-extension/HISTORY.rst | 5 ++++ .../azext_k8s_extension/_client_factory.py | 5 ++++ .../azext_k8s_extension/_validators.py | 26 +++++++++++++++++++ .../azext_k8s_extension/consts.py | 1 + .../azext_k8s_extension/custom.py | 13 ++++++---- src/k8s-extension/setup.py | 2 +- 6 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 src/k8s-extension/azext_k8s_extension/_validators.py diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index dbf39b332fe..89016d5f307 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -3,6 +3,11 @@ Release History =============== +0.3.1 +++++++++++++++++++ + +* Add provider registration to check to validations + 0.3.0 ++++++++++++++++++ diff --git a/src/k8s-extension/azext_k8s_extension/_client_factory.py b/src/k8s-extension/azext_k8s_extension/_client_factory.py index 6271246245e..17bb875a38c 100644 --- a/src/k8s-extension/azext_k8s_extension/_client_factory.py +++ b/src/k8s-extension/azext_k8s_extension/_client_factory.py @@ -29,3 +29,8 @@ def cf_resources(cli_ctx, subscription_id=None): def cf_log_analytics(cli_ctx, subscription_id=None): from azure.mgmt.loganalytics import LogAnalyticsManagementClient # pylint: disable=no-name-in-module return get_mgmt_service_client(cli_ctx, LogAnalyticsManagementClient, subscription_id=subscription_id) + + +def _resource_providers_client(cli_ctx): + from azure.mgmt.resource import ResourceManagementClient + return get_mgmt_service_client(cli_ctx, ResourceManagementClient).providers diff --git a/src/k8s-extension/azext_k8s_extension/_validators.py b/src/k8s-extension/azext_k8s_extension/_validators.py new file mode 100644 index 00000000000..ee44f0e68e8 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/_validators.py @@ -0,0 +1,26 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +from knack.log import get_logger +from azext_k8s_extension._client_factory import _resource_providers_client +from . import consts + + +logger = get_logger(__name__) + + +# pylint: disable=broad-except +def _validate_cc_registration(cmd): + try: + rp_client = _resource_providers_client(cmd.cli_ctx) + registration_state = rp_client.get(consts.PROVIDER_NAMESPACE).registration_state + + if registration_state != "Registered": + logger.warning("'Extensions' cannot be used because '%s' provider has not been registered." + "More details for registering this provider can be found here - " + "https://aka.ms/RegisterKubernetesConfigurationProvider", consts.PROVIDER_NAMESPACE) + except Exception: + logger.warning("Unable to fetch registration state of '%s' provider. " + "Failed to enable 'extensions' feature...", consts.PROVIDER_NAMESPACE) diff --git a/src/k8s-extension/azext_k8s_extension/consts.py b/src/k8s-extension/azext_k8s_extension/consts.py index 4d09158eacd..2f39b4d7247 100644 --- a/src/k8s-extension/azext_k8s_extension/consts.py +++ b/src/k8s-extension/azext_k8s_extension/consts.py @@ -6,3 +6,4 @@ EXTENSION_NAME = 'k8s-extension' EXTENSION_PACKAGE_NAME = "azext_k8s_extension" +PROVIDER_NAMESPACE = 'Microsoft.KubernetesConfiguration' diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index 2441c69ed90..2428fa58ffb 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -13,9 +13,10 @@ from azure.cli.core.azclierror import ResourceNotFoundError, MutuallyExclusiveArgumentError, \ InvalidArgumentValueError, CommandNotFoundError, RequiredArgumentMissingError from azure.cli.core.commands.client_factory import get_subscription_id -from .vendored_sdks.models import ConfigurationIdentity -from .vendored_sdks.models import ErrorResponseException -from .vendored_sdks.models import Scope +from azext_k8s_extension.vendored_sdks.models import ConfigurationIdentity +from azext_k8s_extension.vendored_sdks.models import ErrorResponseException +from azext_k8s_extension.vendored_sdks.models import Scope +from azext_k8s_extension._validators import _validate_cc_registration from .partner_extensions.ContainerInsights import ContainerInsights from .partner_extensions.AzureDefender import AzureDefender @@ -78,9 +79,8 @@ def create_k8s_extension(cmd, client, resource_group_name, cluster_name, name, c """Create a new Extension Instance. """ - extension_type_lower = extension_type.lower() - # Determine ClusterRP + extension_type_lower = extension_type.lower() cluster_rp = __get_cluster_rp(cluster_type) # Configuration Settings & Configuration Protected Settings @@ -135,6 +135,9 @@ def create_k8s_extension(cmd, client, resource_group_name, cluster_name, name, c __validate_version_and_auto_upgrade(extension_instance.version, extension_instance.auto_upgrade_minor_version) __validate_scope_after_customization(extension_instance.scope) + # Check that registration has been done on Microsoft.KubernetesConfiguration for the subscription + _validate_cc_registration(cmd) + # Create identity, if required if create_identity: extension_instance.identity, extension_instance.location = \ diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index 3c9a1882a27..b3512c976db 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -32,7 +32,7 @@ # TODO: Add any additional SDK dependencies here DEPENDENCIES = [] -VERSION = "0.3.0" +VERSION = "0.3.1" with open('README.rst', 'r', encoding='utf-8') as f: README = f.read() From cf95612949f565c2f1c2a74c15f516dda3e6fc16 Mon Sep 17 00:00:00 2001 From: yuyue9284 <15863499+yuyue9284@users.noreply.github.com> Date: Wed, 12 May 2021 02:26:14 +0800 Subject: [PATCH 56/86] only validate scoring fe when inference is enabled (#31) * only validate scoring fe when inference is enabled * Fix versioning Co-authored-by: Yue Yu Co-authored-by: jonathan-innis --- src/k8s-extension/HISTORY.rst | 1 + .../partner_extensions/AzureMLKubernetes.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index 89016d5f307..02562364f13 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -7,6 +7,7 @@ Release History ++++++++++++++++++ * Add provider registration to check to validations +* Only validate scoring fe settings when inference is enabled in microsoft.azureml.kubernetes 0.3.0 ++++++++++++++++++ diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py index 34d71beb829..d907e7d3c63 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py @@ -167,14 +167,13 @@ def __validate_config(self, configuration_settings, configuration_protected_sett if enable_inference: logger.warning("The installed AzureML extension for AML inference is experimental and not covered by customer support. Please use with discretion.") + self.__validate_scoring_fe_settings(configuration_settings, configuration_protected_settings) elif not (enable_training or enable_inference): raise InvalidArgumentValueError( "Please create Microsoft.AzureML.Kubernetes extension instance either " "for Machine Learning training or inference by specifying " f"'--configuration-settings {self.ENABLE_TRAINING}=true' or '--configuration-settings {self.ENABLE_INFERENCE}=true'") - self.__validate_scoring_fe_settings(configuration_settings, configuration_protected_settings) - configuration_settings[self.ENABLE_TRAINING] = configuration_settings.get(self.ENABLE_TRAINING, enable_training) configuration_settings[self.ENABLE_INFERENCE] = configuration_settings.get( self.ENABLE_INFERENCE, enable_inference) From fea6e009c3e5e5b33826d333baca4f2ede3c454a Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Tue, 11 May 2021 16:04:59 -0700 Subject: [PATCH 57/86] Provider registration case insensitive --- src/k8s-extension/azext_k8s_extension/_validators.py | 4 ++-- src/k8s-extension/azext_k8s_extension/consts.py | 1 + src/k8s-extension/azext_k8s_extension/custom.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/_validators.py b/src/k8s-extension/azext_k8s_extension/_validators.py index ee44f0e68e8..9941c4a117b 100644 --- a/src/k8s-extension/azext_k8s_extension/_validators.py +++ b/src/k8s-extension/azext_k8s_extension/_validators.py @@ -12,12 +12,12 @@ # pylint: disable=broad-except -def _validate_cc_registration(cmd): +def validate_cc_registration(cmd): try: rp_client = _resource_providers_client(cmd.cli_ctx) registration_state = rp_client.get(consts.PROVIDER_NAMESPACE).registration_state - if registration_state != "Registered": + if registration_state.lower() != consts.REGISTERED.lower(): logger.warning("'Extensions' cannot be used because '%s' provider has not been registered." "More details for registering this provider can be found here - " "https://aka.ms/RegisterKubernetesConfigurationProvider", consts.PROVIDER_NAMESPACE) diff --git a/src/k8s-extension/azext_k8s_extension/consts.py b/src/k8s-extension/azext_k8s_extension/consts.py index 2f39b4d7247..c75489d2362 100644 --- a/src/k8s-extension/azext_k8s_extension/consts.py +++ b/src/k8s-extension/azext_k8s_extension/consts.py @@ -7,3 +7,4 @@ EXTENSION_NAME = 'k8s-extension' EXTENSION_PACKAGE_NAME = "azext_k8s_extension" PROVIDER_NAMESPACE = 'Microsoft.KubernetesConfiguration' +REGISTERED = "Registered" diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index 2428fa58ffb..30971ba265a 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -16,7 +16,7 @@ from azext_k8s_extension.vendored_sdks.models import ConfigurationIdentity from azext_k8s_extension.vendored_sdks.models import ErrorResponseException from azext_k8s_extension.vendored_sdks.models import Scope -from azext_k8s_extension._validators import _validate_cc_registration +from azext_k8s_extension._validators import validate_cc_registration from .partner_extensions.ContainerInsights import ContainerInsights from .partner_extensions.AzureDefender import AzureDefender @@ -136,7 +136,7 @@ def create_k8s_extension(cmd, client, resource_group_name, cluster_name, name, c __validate_scope_after_customization(extension_instance.scope) # Check that registration has been done on Microsoft.KubernetesConfiguration for the subscription - _validate_cc_registration(cmd) + validate_cc_registration(cmd) # Create identity, if required if create_identity: From 877868c97035f0072ea6f1566ae9822f11826fdc Mon Sep 17 00:00:00 2001 From: yuyue9284 <15863499+yuyue9284@users.noreply.github.com> Date: Fri, 14 May 2021 01:42:09 +0800 Subject: [PATCH 58/86] do not validate against scoring fe if inference is not enabled. (#33) * do not validate against scoring fe if inference is not enabled. * add inference enabled scenario * refine * increase sleeping time * fix Co-authored-by: Yue Yu Co-authored-by: Jonathan Innis --- .../public/AzureMLKubernetes.Tests.ps1 | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 index 2bc01244a74..eb121b7d56b 100644 --- a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 +++ b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 @@ -10,8 +10,8 @@ Describe 'AzureML Kubernetes Testing' { . $PSScriptRoot/../../helper/Helper.ps1 } - It 'Creates the extension and checks that it onboards correctly' { - $output = az k8s-extension create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType --name $extensionName --release-train preview --config enableTraining=true allowInsecureConnections=true + It 'Creates the extension and checks that it onboards correctly with training enabled' { + $output = az k8s-extension create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType --name $extensionName --release-train staging --config enableTraining=true $? | Should -BeTrue $output = az k8s-extension show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName @@ -27,7 +27,7 @@ Describe 'AzureML Kubernetes Testing' { if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { break } - Start-Sleep -Seconds 10 + Start-Sleep -Seconds 20 $n += 1 } while ($n -le $MAX_RETRY_ATTEMPTS) $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS @@ -64,7 +64,7 @@ Describe 'AzureML Kubernetes Testing' { break } } - Start-Sleep -Seconds 10 + Start-Sleep -Seconds 20 $n += 1 } while ($n -le $MAX_RETRY_ATTEMPTS) $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS @@ -100,4 +100,40 @@ Describe 'AzureML Kubernetes Testing' { $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } $extensionExists | Should -BeNullOrEmpty } + + It 'Creates the extension and checks that it onboards correctly with inference enabled' { + $output = az k8s-extension create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType --name $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net allowInsecureConnections=True clusterPurpose=DevTest + $? | Should -BeTrue + + $output = az k8s-extension show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName + $? | Should -BeTrue + + $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue + + # Loop and retry until the extension installs + $n = 0 + do + { + if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { + break + } + Start-Sleep -Seconds 20 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + + # check if relay is populated + $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey + $relayResourceID | Should -Not -BeNullOrEmpty + } + + It "Deletes the extension from the cluster with inference enabled" { + az k8s-extension delete --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName + $? | Should -BeTrue + + # Extension should not be found on the cluster + az k8s-extension show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName + $? | Should -BeFalse + } } From 9b1bedd299e58e1a93455151783d207674171504 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Thu, 13 May 2021 14:41:29 -0700 Subject: [PATCH 59/86] Add OSM as Public Preview Extension (#34) * Add OSM as public preview extension * Add osm testing * Add release train to tests * Fix failing osm test * Upgrade pip in integration testing * Remove ununsed import --- src/k8s-extension/HISTORY.rst | 5 + .../azext_k8s_extension/custom.py | 2 + .../partner_extensions/OpenServiceMesh.py | 95 ++++++++++++++++++ src/k8s-extension/setup.py | 2 +- .../public/OpenServiceMesh.Tests.ps1 | 97 +++++++++++++++++++ 5 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py create mode 100644 testing/test/extensions/public/OpenServiceMesh.Tests.ps1 diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index 02562364f13..75f127f4afd 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -3,6 +3,11 @@ Release History =============== +0.4.0 +++++++++++++++++++ + +* Release customization for microsoft.openservicemesh + 0.3.1 ++++++++++++++++++ diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index 30971ba265a..f4f2e1cafdd 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -22,6 +22,7 @@ from .partner_extensions.AzureDefender import AzureDefender from .partner_extensions.Cassandra import Cassandra from .partner_extensions.AzureMLKubernetes import AzureMLKubernetes +from .partner_extensions.OpenServiceMesh import OpenServiceMesh from .partner_extensions.DefaultExtension import DefaultExtension from . import consts @@ -35,6 +36,7 @@ def ExtensionFactory(extension_name): extension_map = { 'microsoft.azuremonitor.containers': ContainerInsights, 'microsoft.azuredefender.kubernetes': AzureDefender, + 'microsoft.openservicemesh': OpenServiceMesh, 'microsoft.azureml.kubernetes': AzureMLKubernetes, 'cassandradatacentersoperator': Cassandra, } diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py new file mode 100644 index 00000000000..0cbc799ffa4 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py @@ -0,0 +1,95 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=unused-argument + +from azure.cli.core.azclierror import InvalidArgumentValueError, RequiredArgumentMissingError +from knack.log import get_logger + +from ..vendored_sdks.models import ExtensionInstance +from ..vendored_sdks.models import ExtensionInstanceUpdate +from ..vendored_sdks.models import ScopeCluster +from ..vendored_sdks.models import Scope + +from .PartnerExtensionModel import PartnerExtensionModel + +logger = get_logger(__name__) + + +class OpenServiceMesh(PartnerExtensionModel): + def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, + scope, auto_upgrade_minor_version, release_train, version, target_namespace, + release_namespace, configuration_settings, configuration_protected_settings, + configuration_settings_file, configuration_protected_settings_file): + + """ExtensionType 'microsoft.openservicemesh' specific validations & defaults for Create + Must create and return a valid 'ExtensionInstance' object. + + """ + # NOTE-1: Replace default scope creation with your customization, if required + # Scope must always be cluster + ext_scope = None + if scope == 'namespace': + raise InvalidArgumentValueError("Invalid scope '{}'. This extension can be installed " + "only at 'cluster' scope.".format(scope)) + + scope_cluster = ScopeCluster(release_namespace=release_namespace) + ext_scope = Scope(cluster=scope_cluster, namespace=None) + + valid_release_trains = ['staging', 'pilot'] + # If release-train is not input, set it to 'stable' + if release_train is None: + raise RequiredArgumentMissingError( + "A release-train must be provided. Valid values are 'staging', 'pilot'." + ) + + if release_train.lower() in valid_release_trains: + # version is a mandatory if release-train is staging or pilot + if version is None: + raise RequiredArgumentMissingError( + "A version must be provided for release-train {}.".format(release_train) + ) + # If the release-train is 'staging' or 'pilot' then auto-upgrade-minor-version MUST be set to False + if auto_upgrade_minor_version or auto_upgrade_minor_version is None: + auto_upgrade_minor_version = False + logger.warning("Setting auto-upgrade-minor-version to False since release-train is '%s'", release_train) + else: + raise InvalidArgumentValueError( + "Invalid release-train '{}'. Valid values are 'staging', 'pilot'.".format(release_train) + ) + + # NOTE-2: Return a valid ExtensionInstance object, Instance name and flag for Identity + create_identity = False + extension_instance = ExtensionInstance( + extension_type=extension_type, + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version, + scope=ext_scope, + configuration_settings=configuration_settings, + configuration_protected_settings=configuration_protected_settings, + identity=None, + location="" + ) + return extension_instance, name, create_identity + + def Update(self, extension, auto_upgrade_minor_version, release_train, version): + """ExtensionType 'microsoft.openservicemesh' specific validations & defaults for Update + Must create and return a valid 'ExtensionInstanceUpdate' object. + + """ + # auto-upgrade-minor-version MUST be set to False if release_train is staging or pilot + if release_train.lower() in 'staging' 'pilot': + if auto_upgrade_minor_version or auto_upgrade_minor_version is None: + auto_upgrade_minor_version = False + # Set version to None to always get the latest version - user cannot override + version = None + logger.warning("Setting auto-upgrade-minor-version to False since release-train is '%s'", release_train) + + return ExtensionInstanceUpdate( + auto_upgrade_minor_version=auto_upgrade_minor_version, + release_train=release_train, + version=version + ) diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index b3512c976db..64819fc940a 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -32,7 +32,7 @@ # TODO: Add any additional SDK dependencies here DEPENDENCIES = [] -VERSION = "0.3.1" +VERSION = "0.4.0" with open('README.rst', 'r', encoding='utf-8') as f: README = f.read() diff --git a/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 b/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 new file mode 100644 index 00000000000..2d3d549529a --- /dev/null +++ b/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 @@ -0,0 +1,97 @@ +Describe 'Azure OpenServiceMesh Testing' { + BeforeAll { + $extensionType = "microsoft.openservicemesh" + $extensionName = "openservicemesh" + $extensionVersion = "0.8.3" + $extensionAgentName = "osm-controller" + $extensionAgentNamespace = "arc-osm-system" + $releaseTrain = "pilot" + + . $PSScriptRoot/../../helper/Constants.ps1 + . $PSScriptRoot/../../helper/Helper.ps1 + } + + It 'Creates the extension and checks that it onboards correctly' { + $output = az $Env:K8sExtensionName create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train $releaseTrain --version $extensionVersion + $? | Should -BeTrue + + $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + $isAutoUpgradeMinorVersion.ToString() -eq "False" | Should -BeTrue + + # Loop and retry until the extension installs + $n = 0 + do + { + if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { + if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { + break + } + } + Start-Sleep -Seconds 10 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Performs a show on the extension" { + $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + $output | Should -Not -BeNullOrEmpty + } + + It "Runs an update on the extension on the cluster" { + Set-ItResult -Skipped -Because "Update is not a valid scenario for now" + + # az $Env:K8sExtensionName update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName --auto-upgrade-minor-version false + # $? | Should -BeTrue + + # $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + # $? | Should -BeTrue + + # $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + # $isAutoUpgradeMinorVersion.ToString() -eq "False" | Should -BeTrue + + # # Loop and retry until the extension config updates + # $n = 0 + # do + # { + # $isAutoUpgradeMinorVersion = (Get-ExtensionData $extensionName).spec.autoUpgradeMinorVersion + # if (!$isAutoUpgradeMinorVersion) { #autoUpgradeMinorVersion doesn't exist in ExtensionConfig CRD if false + # if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { + # if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { + # break + # } + # } + # } + # Start-Sleep -Seconds 10 + # $n += 1 + # } while ($n -le $MAX_RETRY_ATTEMPTS) + # $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + } + + It "Lists the extensions on the cluster" { + $output = az $Env:K8sExtensionName list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $? | Should -BeTrue + + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } + $extensionExists | Should -Not -BeNullOrEmpty + } + + It "Deletes the extension from the cluster" { + az $Env:K8sExtensionName delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeTrue + + # Extension should not be found on the cluster + az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName + $? | Should -BeFalse + } + + It "Performs another list after the delete" { + $output = az $Env:K8sExtensionName list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } + $extensionExists | Should -BeNullOrEmpty + } +} From 257dbf30a1cb80b237c20019c18b60002b5c47c7 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Fri, 14 May 2021 11:11:24 -0700 Subject: [PATCH 60/86] Fix release train check in update --- .../azext_k8s_extension/partner_extensions/OpenServiceMesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py index 0cbc799ffa4..6d11064821c 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py @@ -81,7 +81,7 @@ def Update(self, extension, auto_upgrade_minor_version, release_train, version): """ # auto-upgrade-minor-version MUST be set to False if release_train is staging or pilot - if release_train.lower() in 'staging' 'pilot': + if release_train.lower() in ['staging', 'pilot']: if auto_upgrade_minor_version or auto_upgrade_minor_version is None: auto_upgrade_minor_version = False # Set version to None to always get the latest version - user cannot override From a20357518e7a494aca7ce6dad38471e254fd458f Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Fri, 14 May 2021 13:35:36 -0700 Subject: [PATCH 61/86] Parallelize E2E Testing (#36) * Add OSM as public preview extension * Add osm testing * Update test logic to parallelize * Fix test success checking * Parallelize extension testing * Better error checking logic --- k8s-custom-pipelines.yml | 2 +- testing/.gitignore | 2 +- testing/Test.ps1 | 43 ++++++++++++++-- ...l => k8s_extension-0.4.0-py3-none-any.whl} | Bin 52893 -> 55464 bytes .../private-preview/AzurePolicy.Tests.ps1 | 31 ++++++----- .../extensions/public/AzureDefender.Tests.ps1 | 31 ++++++----- .../public/AzureMLKubernetes.Tests.ps1 | 48 ++++++++++-------- .../extensions/public/AzureMonitor.Tests.ps1 | 31 ++++++----- .../public/OpenServiceMesh.Tests.ps1 | 33 +++++++----- testing/test/helper/Helper.ps1 | 8 +++ 10 files changed, 151 insertions(+), 78 deletions(-) rename testing/bin/{k8s_extension-0.3.0-py3-none-any.whl => k8s_extension-0.4.0-py3-none-any.whl} (64%) diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index bca200cd7cc..7a133da8da4 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -136,7 +136,7 @@ stages: - task: PublishTestResults@2 inputs: testResultsFormat: 'JUnit' - testResultsFiles: '**/TestResults.xml' + testResultsFiles: '**/testing/results/*.xml' failTaskOnFailedTests: true condition: succeededOrFailed() diff --git a/testing/.gitignore b/testing/.gitignore index 083fdfb5c25..5687a0bf32d 100644 --- a/testing/.gitignore +++ b/testing/.gitignore @@ -2,7 +2,7 @@ settings.json tmp/ bin/* !bin/connectedk8s-1.0.0-py3-none-any.whl -!bin/k8s_extension-0.3.0-py3-none-any.whl +!bin/k8s_extension-0.4.0-py3-none-any.whl !bin/k8s_extension_private-0.1.0-py3-none-any.whl !bin/k8s_configuration-1.0.0-py3-none-any.whl !bin/connectedk8s-values.yaml diff --git a/testing/Test.ps1 b/testing/Test.ps1 index 06c089f6f52..87e746d433f 100644 --- a/testing/Test.ps1 +++ b/testing/Test.ps1 @@ -10,13 +10,20 @@ param ( ) # Disable confirm prompt for script +# Only show errors, don't show warnings az config set core.disable_confirm_prompt=true +az config set core.only_show_errors=true $ENVCONFIG = Get-Content -Path $PSScriptRoot/settings.json | ConvertFrom-Json az account set --subscription $ENVCONFIG.subscriptionId $Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" +$TestFileDirectory="$PSScriptRoot/results" + +if (-not (Test-Path -Path $TestFileDirectory)) { + New-Item -ItemType Directory -Path $TestFileDirectory +} if ($Type -eq 'k8s-extension') { $k8sExtensionVersion = $ENVCONFIG.extensionVersion.'k8s-extension' @@ -68,15 +75,45 @@ if ($Type -eq 'k8s-extension') { } if ($CI) { + # This runs the tests in parallel during the CI pipline to speed up testing + Write-Host "Invoking Pester to run tests from '$testFilePath'..." - $testResult = Invoke-Pester $testFilePath -Passthru -Output Detailed - $testResult | Export-JUnitReport -Path TestResults.xml + $testFiles = Get-ChildItem $testFilePath + $resultFileNumber = 0 + foreach ($testFile in $testFiles) + { + $resultFileNumber++ + $testName = Split-Path $testFile –leaf + Start-Job -ArgumentList $testName, $testFile, $resultFileNumber, $TestFileDirectory -Name $testName -ScriptBlock { + param($name, $testFile, $resultFileNumber, $testFileDirectory) + + Write-Host "$testFile to result file #$resultFileNumber" + $testResult = Invoke-Pester $testFile -Passthru -Output Detailed + $testResult | Export-JUnitReport -Path "$testFileDirectory/$name.xml" + } + } + + do { + Write-Host ">> Still running tests @ $(Get-Date –Format "HH:mm:ss")" –ForegroundColor Blue + Get-Job | Where-Object { $_.State -eq "Running" } | Format-Table –AutoSize + Start-Sleep –Seconds 30 + } while((Get-Job | Where-Object { $_.State -eq "Running" } | Measure-Object).Count -ge 1) + + Get-Job | Wait-Job + $failedJobs = Get-Job | Where-Object { -not ($_.State -eq "Completed")} + Get-Job | Receive-Job –AutoRemoveJob –Wait –ErrorAction 'Continue' + + if ($failedJobs.Count -gt 0) { + Write-Host "Failed Jobs" –ForegroundColor Red + $failedJobs + throw "One or more tests failed" + } } else { if ($Path) { Write-Host "Invoking Pester to run tests from '$PSScriptRoot/$Path'" Invoke-Pester -Output Detailed $PSScriptRoot/$Path } else { Write-Host "Invoking Pester to run tests from '$testFilePath'..." - Invoke-Pester -Output Detailed $testFilePathc + Invoke-Pester -Output Detailed $testFilePath } } diff --git a/testing/bin/k8s_extension-0.3.0-py3-none-any.whl b/testing/bin/k8s_extension-0.4.0-py3-none-any.whl similarity index 64% rename from testing/bin/k8s_extension-0.3.0-py3-none-any.whl rename to testing/bin/k8s_extension-0.4.0-py3-none-any.whl index feb28b80b437c8af966b3b87a4fcad1a9b74d108..6c5113f7d11fe32d2d1e066f08cf859484bae71c 100644 GIT binary patch delta 15157 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 delta 12743 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 diff --git a/testing/test/extensions/private-preview/AzurePolicy.Tests.ps1 b/testing/test/extensions/private-preview/AzurePolicy.Tests.ps1 index ea8c3f46cb4..2793a2e179d 100644 --- a/testing/test/extensions/private-preview/AzurePolicy.Tests.ps1 +++ b/testing/test/extensions/private-preview/AzurePolicy.Tests.ps1 @@ -10,11 +10,11 @@ Describe 'Azure Policy Testing' { } It 'Creates the extension and checks that it onboards correctly' { - $output = az $Env:K8sExtensionName create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType -n $extensionName - $? | Should -BeTrue + Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty - $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue @@ -35,8 +35,8 @@ Describe 'Azure Policy Testing' { } It "Performs a show on the extension" { - $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty $output | Should -Not -BeNullOrEmpty } @@ -71,24 +71,29 @@ Describe 'Azure Policy Testing' { } It "Lists the extensions on the cluster" { - $output = az $Env:K8sExtensionName list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty + $output | Should -Not -BeNullOrEmpty $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } $extensionExists | Should -Not -BeNullOrEmpty } It "Deletes the extension from the cluster" { - az $Env:K8sExtensionName delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty # Extension should not be found on the cluster - az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeFalse + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -Not -BeNullOrEmpty + $output | Should -BeNullOrEmpty } It "Performs another list after the delete" { - $output = az $Env:K8sExtensionName list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty + $output | Should -Not -BeNullOrEmpty + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } $extensionExists | Should -BeNullOrEmpty } diff --git a/testing/test/extensions/public/AzureDefender.Tests.ps1 b/testing/test/extensions/public/AzureDefender.Tests.ps1 index 2d199e31638..1ef498b67e6 100644 --- a/testing/test/extensions/public/AzureDefender.Tests.ps1 +++ b/testing/test/extensions/public/AzureDefender.Tests.ps1 @@ -9,11 +9,11 @@ Describe 'Azure Defender Testing' { } It 'Creates the extension and checks that it onboards correctly' { - $output = az $Env:K8sExtensionName create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType -n $extensionName - $? | Should -BeTrue + Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty - $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue @@ -33,8 +33,8 @@ Describe 'Azure Defender Testing' { } It "Performs a show on the extension" { - $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty $output | Should -Not -BeNullOrEmpty } @@ -69,24 +69,29 @@ Describe 'Azure Defender Testing' { } It "Lists the extensions on the cluster" { - $output = az $Env:K8sExtensionName list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty + $output | Should -Not -BeNullOrEmpty $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } $extensionExists | Should -Not -BeNullOrEmpty } It "Deletes the extension from the cluster" { - az $Env:K8sExtensionName delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty # Extension should not be found on the cluster - az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeFalse + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -Not -BeNullOrEmpty + $output | Should -BeNullOrEmpty } It "Performs another list after the delete" { - $output = az $Env:K8sExtensionName list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty + $output | Should -Not -BeNullOrEmpty + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } $extensionExists | Should -BeNullOrEmpty } diff --git a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 index eb121b7d56b..b70d992e404 100644 --- a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 +++ b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 @@ -11,11 +11,11 @@ Describe 'AzureML Kubernetes Testing' { } It 'Creates the extension and checks that it onboards correctly with training enabled' { - $output = az k8s-extension create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType --name $extensionName --release-train staging --config enableTraining=true - $? | Should -BeTrue + Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableTraining=true" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty - $output = az k8s-extension show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue @@ -38,8 +38,8 @@ Describe 'AzureML Kubernetes Testing' { } It "Performs a show on the extension" { - $output = az k8s-extension show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty $output | Should -Not -BeNullOrEmpty } @@ -71,9 +71,10 @@ Describe 'AzureML Kubernetes Testing' { } It "Lists the extensions on the cluster" { - $output = az k8s-extension list --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty + $output | Should -Not -BeNullOrEmpty $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } $extensionExists | Should -Not -BeNullOrEmpty } @@ -87,26 +88,30 @@ Describe 'AzureML Kubernetes Testing' { az relay namespace delete --resource-group $ENVCONFIG.resourceGroup --name $relayNamespaceName az servicebus namespace delete --resource-group $ENVCONFIG.resourceGroup --name $serviceBusNamespaceName - az k8s-extension delete --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty # Extension should not be found on the cluster - az k8s-extension show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName - $? | Should -BeFalse + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -Not -BeNullOrEmpty + $output | Should -BeNullOrEmpty } It "Performs another list after the delete" { - $output = az k8s-extension list --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty + $output | Should -Not -BeNullOrEmpty + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } $extensionExists | Should -BeNullOrEmpty } It 'Creates the extension and checks that it onboards correctly with inference enabled' { - $output = az k8s-extension create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType --name $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net allowInsecureConnections=True clusterPurpose=DevTest - $? | Should -BeTrue + Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net allowInsecureConnections=True clusterPurpose=DevTest" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty - $output = az k8s-extension show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue @@ -129,11 +134,12 @@ Describe 'AzureML Kubernetes Testing' { } It "Deletes the extension from the cluster with inference enabled" { - az k8s-extension delete --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty # Extension should not be found on the cluster - az k8s-extension show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName - $? | Should -BeFalse + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -Not -BeNullOrEmpty + $output | Should -BeNullOrEmpty } } diff --git a/testing/test/extensions/public/AzureMonitor.Tests.ps1 b/testing/test/extensions/public/AzureMonitor.Tests.ps1 index c9baa2d0e48..a34d8160728 100644 --- a/testing/test/extensions/public/AzureMonitor.Tests.ps1 +++ b/testing/test/extensions/public/AzureMonitor.Tests.ps1 @@ -10,11 +10,11 @@ Describe 'Azure Monitor Testing' { } It 'Creates the extension and checks that it onboards correctly' { - $output = az $Env:K8sExtensionName create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType -n $extensionName - $? | Should -BeTrue + Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty - $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue @@ -35,8 +35,8 @@ Describe 'Azure Monitor Testing' { } It "Performs a show on the extension" { - $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty $output | Should -Not -BeNullOrEmpty } @@ -71,24 +71,29 @@ Describe 'Azure Monitor Testing' { } It "Lists the extensions on the cluster" { - $output = az $Env:K8sExtensionName list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty + $output | Should -Not -BeNullOrEmpty $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } $extensionExists | Should -Not -BeNullOrEmpty } It "Deletes the extension from the cluster" { - az $Env:K8sExtensionName delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty # Extension should not be found on the cluster - az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeFalse + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -Not -BeNullOrEmpty + $output | Should -BeNullOrEmpty } It "Performs another list after the delete" { - $output = az $Env:K8sExtensionName list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty + $output | Should -Not -BeNullOrEmpty + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } $extensionExists | Should -BeNullOrEmpty } diff --git a/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 b/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 index 2d3d549529a..893d3c3e468 100644 --- a/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 +++ b/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 @@ -11,12 +11,14 @@ Describe 'Azure OpenServiceMesh Testing' { . $PSScriptRoot/../../helper/Helper.ps1 } + # Should Not BeNullOrEmpty checks if the command returns JSON output + It 'Creates the extension and checks that it onboards correctly' { - $output = az $Env:K8sExtensionName create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train $releaseTrain --version $extensionVersion - $? | Should -BeTrue + Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train $releaseTrain --version $extensionVersion" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty - $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion $isAutoUpgradeMinorVersion.ToString() -eq "False" | Should -BeTrue @@ -37,8 +39,8 @@ Describe 'Azure OpenServiceMesh Testing' { } It "Performs a show on the extension" { - $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty $output | Should -Not -BeNullOrEmpty } @@ -73,24 +75,29 @@ Describe 'Azure OpenServiceMesh Testing' { } It "Lists the extensions on the cluster" { - $output = az $Env:K8sExtensionName list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty + $output | Should -Not -BeNullOrEmpty $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } $extensionExists | Should -Not -BeNullOrEmpty } It "Deletes the extension from the cluster" { - az $Env:K8sExtensionName delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue + $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty # Extension should not be found on the cluster - az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeFalse + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -Not -BeNullOrEmpty + $output | Should -BeNullOrEmpty } It "Performs another list after the delete" { - $output = az $Env:K8sExtensionName list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters + $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty + $output | Should -Not -BeNullOrEmpty + $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } $extensionExists | Should -BeNullOrEmpty } diff --git a/testing/test/helper/Helper.ps1 b/testing/test/helper/Helper.ps1 index 362a4eedf18..db76c41cff4 100644 --- a/testing/test/helper/Helper.ps1 +++ b/testing/test/helper/Helper.ps1 @@ -45,3 +45,11 @@ function Get-ExtensionConfigurationSettings { } return $null } + +function Check-Error { + param( + [string]$output + ) + $hasError = $output -CMatch "ERROR" + return $hasError +} \ No newline at end of file From 8d65189bbec85860ffb2d2f0c7532bbbb8084251 Mon Sep 17 00:00:00 2001 From: jonathan-innis Date: Fri, 14 May 2021 14:14:42 -0700 Subject: [PATCH 62/86] Fix azureml deletion --- .../test/extensions/public/AzureMLKubernetes.Tests.ps1 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 index eb121b7d56b..f88cb8f9490 100644 --- a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 +++ b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 @@ -129,6 +129,14 @@ Describe 'AzureML Kubernetes Testing' { } It "Deletes the extension from the cluster with inference enabled" { + # cleanup the relay and servicebus + $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey + $serviceBusResourceID = Get-ExtensionConfigurationSettings $extensionName $serviceBusResourceIDKey + $relayNamespaceName = $relayResourceID.split("/")[8] + $serviceBusNamespaceName = $serviceBusResourceID.split("/")[8] + az relay namespace delete --resource-group $ENVCONFIG.resourceGroup --name $relayNamespaceName + az servicebus namespace delete --resource-group $ENVCONFIG.resourceGroup --name $serviceBusNamespaceName + az k8s-extension delete --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName $? | Should -BeTrue From 918bc02c03e31a27ae90a200fc03d3c63ca4463f Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Fri, 21 May 2021 10:55:49 -0700 Subject: [PATCH 63/86] Fix private build (#40) --- src/k8s-extension/azext_k8s_extension/_validators.py | 2 +- src/k8s-extension/azext_k8s_extension/custom.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/_validators.py b/src/k8s-extension/azext_k8s_extension/_validators.py index 9941c4a117b..31078a4ddbb 100644 --- a/src/k8s-extension/azext_k8s_extension/_validators.py +++ b/src/k8s-extension/azext_k8s_extension/_validators.py @@ -4,7 +4,7 @@ # -------------------------------------------------------------------------------------------- from knack.log import get_logger -from azext_k8s_extension._client_factory import _resource_providers_client +from ._client_factory import _resource_providers_client from . import consts diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index f4f2e1cafdd..64c99f25abd 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -13,10 +13,8 @@ from azure.cli.core.azclierror import ResourceNotFoundError, MutuallyExclusiveArgumentError, \ InvalidArgumentValueError, CommandNotFoundError, RequiredArgumentMissingError from azure.cli.core.commands.client_factory import get_subscription_id -from azext_k8s_extension.vendored_sdks.models import ConfigurationIdentity -from azext_k8s_extension.vendored_sdks.models import ErrorResponseException -from azext_k8s_extension.vendored_sdks.models import Scope -from azext_k8s_extension._validators import validate_cc_registration +from .vendored_sdks.models import ConfigurationIdentity, ErrorResponseException, Scope +from ._validators import validate_cc_registration from .partner_extensions.ContainerInsights import ContainerInsights from .partner_extensions.AzureDefender import AzureDefender From 8b2490df8a7ff40f87a3f4663f35aaf36f414cd4 Mon Sep 17 00:00:00 2001 From: yuyue9284 <15863499+yuyue9284@users.noreply.github.com> Date: Tue, 25 May 2021 22:34:24 +0800 Subject: [PATCH 64/86] change amlk8s to amlarc (#42) Co-authored-by: Yue Yu --- src/k8s-extension/HISTORY.rst | 5 +++++ .../partner_extensions/AzureMLKubernetes.py | 6 +++--- src/k8s-extension/setup.py | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index 75f127f4afd..946f73740cd 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -3,6 +3,11 @@ Release History =============== +0.4.1 +++++++++++++++++++ + +* Change resource tag from 'amlk8s' to 'Azure Arc-enabled ML' in microsoft.azureml.kubernetes + 0.4.0 ++++++++++++++++++ diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py index d907e7d3c63..2646dd681e7 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py @@ -30,7 +30,7 @@ logger = get_logger(__name__) -resource_tag = {'created_by': 'amlk8s-extension'} +resource_tag = {'created_by': 'Azure Arc-enabled ML'} class AzureMLKubernetes(PartnerExtensionModel): @@ -271,10 +271,10 @@ def _lock_resource(cmd, lock_scope, lock_level='CanNotDelete'): lock_client: azure.mgmt.resource.locks.ManagementLockClient = get_mgmt_service_client( cmd.cli_ctx, azure.mgmt.resource.locks.ManagementLockClient) # put lock on relay resource - lock_object = ManagementLockObject(level=lock_level, notes='locked by amlk8s.') + lock_object = ManagementLockObject(level=lock_level, notes='locked by amlarc.') try: lock_client.management_locks.create_or_update_by_scope( - scope=lock_scope, lock_name='amlk8s-resource-lock', parameters=lock_object) + scope=lock_scope, lock_name='amlarc-resource-lock', parameters=lock_object) except: # try to lock the resource if user has the owner privilege pass diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index 64819fc940a..4cc69f2cbef 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -32,7 +32,7 @@ # TODO: Add any additional SDK dependencies here DEPENDENCIES = [] -VERSION = "0.4.0" +VERSION = "0.4.1" with open('README.rst', 'r', encoding='utf-8') as f: README = f.read() From 9100ff5c12affee47e6a0c51ca9e871c4515d381 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Thu, 27 May 2021 10:51:02 -0700 Subject: [PATCH 65/86] Servicebus client model changes (#44) * Servicebus client model changes * Fix testing script * Update history file and pipeline * Update min cli core version for track 2 updates --- k8s-custom-pipelines.yml | 33 +++++++++++-------- src/k8s-extension/HISTORY.rst | 1 + .../azext_k8s_extension/azext_metadata.json | 2 +- .../partner_extensions/AzureMLKubernetes.py | 2 +- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index 7a133da8da4..23857260310 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -27,7 +27,7 @@ stages: - job: K8sExtensionTestSuite displayName: "Run the Test Suite" pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-latest' steps: - checkout: self - bash: | @@ -56,15 +56,16 @@ stages: source env/bin/activate # clone azure-cli + git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli + pip install --upgrade pip - pip install azdev + pip install -q azdev ls $(CLI_REPO_PATH) azdev --version - azdev setup -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) + azdev setup -c ../azure-cli -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) azdev extension build $(EXTENSION_NAME) - workingDirectory: $(CLI_REPO_PATH) displayName: "Setup and Build Extension with azdev" @@ -98,6 +99,10 @@ stages: chmod +x ./kind ./kind create cluster displayName: "Create and Start the Kind cluster" + + - bash: | + curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + displayName: "Upgrade az to latest version" - task: AzureCLI@2 displayName: Bootstrap @@ -108,7 +113,7 @@ stages: inlineScript: | .\Bootstrap.ps1 -CI workingDirectory: $(TEST_PATH) - + - task: AzureCLI@2 displayName: Run the Test Suite Public Extensions Only inputs: @@ -153,7 +158,7 @@ stages: - job: BuildPublishExtension pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-latest' displayName: "Build and Publish the Extension Artifact" variables: CLI_REPO_PATH: $(Agent.BuildDirectory)/s @@ -197,13 +202,15 @@ stages: source env/bin/activate # clone azure-cli + git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli + pip install --upgrade pip - pip install azdev + pip install -q azdev ls $(CLI_REPO_PATH) azdev --version - azdev setup -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) + azdev setup -c ../azure-cli -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) azdev extension build $(EXTENSION_NAME) workingDirectory: $(CLI_REPO_PATH) displayName: "Setup and Build Extension with azdev" @@ -218,7 +225,7 @@ stages: - job: CheckLicenseHeader displayName: "Check License" pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 displayName: 'Use Python 3.6' @@ -249,7 +256,7 @@ stages: - job: StaticAnalysis displayName: "Static Analysis" pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 displayName: 'Use Python 3.6' @@ -263,7 +270,7 @@ stages: - job: IndexVerify displayName: "Verify Extensions Index" pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 displayName: 'Use Python 3.7' @@ -280,7 +287,7 @@ stages: - job: SourceTests displayName: "Integration Tests, Build Tests" pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-latest' strategy: matrix: Python36: @@ -303,7 +310,7 @@ stages: - job: LintModifiedExtensions displayName: "CLI Linter on Modified Extensions" pool: - vmImage: 'ubuntu-16.04' + vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 displayName: 'Use Python 3.6' diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index 8b09f647058..2fe8a591fb9 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -6,6 +6,7 @@ Release History 0.4.2 ++++++++++++++++++ +* Hotfix servicebus namespace creation for Track 2 changes * Change resource tag from 'amlk8s' to 'Azure Arc-enabled ML' in microsoft.azureml.kubernetes 0.4.1 diff --git a/src/k8s-extension/azext_k8s_extension/azext_metadata.json b/src/k8s-extension/azext_k8s_extension/azext_metadata.json index cf7b8927a07..f5fcee1c14a 100644 --- a/src/k8s-extension/azext_k8s_extension/azext_metadata.json +++ b/src/k8s-extension/azext_k8s_extension/azext_metadata.json @@ -1,4 +1,4 @@ { "azext.isPreview": true, - "azext.minCliCoreVersion": "2.15.0" + "azext.minCliCoreVersion": "2.24.0" } diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py index 2646dd681e7..3a2544a4e3b 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py @@ -337,7 +337,7 @@ def _get_service_bus_connection_string(cmd, subscription_id, resource_group_name location=cluster_location, sku=service_bus_sku, tags=resource_tag) - async_poller = service_bus_client.namespaces.create_or_update( + async_poller = service_bus_client.namespaces.begin_create_or_update( resource_group_name, service_bus_namespace_name, service_bus_namespace) while True: async_poller.result(15) From 695556712db72c997caf5c5ff00ce2d23a0ed88a Mon Sep 17 00:00:00 2001 From: Lia Kazakova <58274127+liakaz@users.noreply.github.com> Date: Wed, 2 Jun 2021 15:17:55 -0700 Subject: [PATCH 66/86] Read SSL cert and key from files (#38) * first sketch of the change fixes removed extra blank lines changes regarding param renaming added ssl tests added more detail to the unit test additional import moved pem files out of public folder fixed import chenged import changed import unit tests fix unit test fix fixed unit tests fixed unit test unit test fix changes int test cert and key * test protected config * fix test typo * temporary changes reverted * fixing tests * fixed file paths * removed accidentally added file * changes according to review comments * more changes according to review comments * changes according to review comments Co-authored-by: Jonathan Innis --- .../partner_extensions/AzureMLKubernetes.py | 56 ++++++++++++++----- .../data/azure_ml/cert_and_key_encoded.txt | 2 + .../tests/latest/data/azure_ml/test_cert.pem | 1 + .../tests/latest/data/azure_ml/test_key.pem | 1 + .../tests/latest/test_azureml_extension.py | 32 +++++++++++ .../extensions/data/azure_ml/test_cert.pem | 1 + .../extensions/data/azure_ml/test_key.pem | 1 + .../public/AzureMLKubernetes.Tests.ps1 | 47 ++++++++++++++++ 8 files changed, 127 insertions(+), 14 deletions(-) create mode 100644 src/k8s-extension/azext_k8s_extension/tests/latest/data/azure_ml/cert_and_key_encoded.txt create mode 100644 src/k8s-extension/azext_k8s_extension/tests/latest/data/azure_ml/test_cert.pem create mode 100644 src/k8s-extension/azext_k8s_extension/tests/latest/data/azure_ml/test_key.pem create mode 100644 src/k8s-extension/azext_k8s_extension/tests/latest/test_azureml_extension.py create mode 100644 testing/test/extensions/data/azure_ml/test_cert.pem create mode 100644 testing/test/extensions/data/azure_ml/test_key.pem diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py index 3a2544a4e3b..229abcb7492 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py @@ -66,6 +66,12 @@ def __init__(self): self.SERVICE_BUS_JOB_STATE_TOPIC = 'jobstate-updatedby-computeprovider' self.SERVICE_BUS_JOB_STATE_SUB = 'compute-scheduler-jobstate' + # constants for enabling SSL in inference + self.sslKeyPemFile = 'sslKeyPemFile' + self.sslCertPemFile = 'sslCertPemFile' + self.allowInsecureConnections = 'allowInsecureConnections' + self.privateEndpointILB = 'privateEndpointILB' + # reference mapping self.reference_mapping = { self.RELAY_SERVER_CONNECTION_STRING: [self.RELAY_CONNECTION_STRING_KEY, self.RELAY_CONNECTION_STRING_DEPRECATED_KEY], @@ -168,6 +174,7 @@ def __validate_config(self, configuration_settings, configuration_protected_sett if enable_inference: logger.warning("The installed AzureML extension for AML inference is experimental and not covered by customer support. Please use with discretion.") self.__validate_scoring_fe_settings(configuration_settings, configuration_protected_settings) + self.__set_up_inference_ssl(configuration_settings, configuration_protected_settings) elif not (enable_training or enable_inference): raise InvalidArgumentValueError( "Please create Microsoft.AzureML.Kubernetes extension instance either " @@ -181,32 +188,53 @@ def __validate_config(self, configuration_settings, configuration_protected_sett configuration_protected_settings.pop(self.ENABLE_INFERENCE, None) def __validate_scoring_fe_settings(self, configuration_settings, configuration_protected_settings): - clusterPurpose = _get_value_from_config_protected_config( - 'clusterPurpose', configuration_settings, configuration_protected_settings) - if clusterPurpose and clusterPurpose not in ["DevTest", "FastProd"]: - raise InvalidArgumentValueError( - "Accepted values for '--configuration-settings clusterPurpose' " - "are 'DevTest' and 'FastProd'") - - feSslCert = _get_value_from_config_protected_config( - 'scoringFe.sslCert', configuration_settings, configuration_protected_settings) - sslKey = _get_value_from_config_protected_config( - 'scoringFe.sslKey', configuration_settings, configuration_protected_settings) + experimentalCluster = _get_value_from_config_protected_config( + 'experimental', configuration_settings, configuration_protected_settings) + experimentalCluster = str(experimentalCluster).lower() == 'true' + if experimentalCluster: + configuration_settings['clusterPurpose'] = 'DevTest' + else: + configuration_settings['clusterPurpose'] = 'FastProd' + feSslCertFile = configuration_protected_settings.get(self.sslCertPemFile) + feSslKeyFile = configuration_protected_settings.get(self.sslKeyPemFile) allowInsecureConnections = _get_value_from_config_protected_config( - 'allowInsecureConnections', configuration_settings, configuration_protected_settings) + self.allowInsecureConnections, configuration_settings, configuration_protected_settings) allowInsecureConnections = str(allowInsecureConnections).lower() == 'true' - if (not feSslCert or not sslKey) and not allowInsecureConnections: + if (not feSslCertFile or not feSslKeyFile) and not allowInsecureConnections: raise InvalidArgumentValueError( "Provide ssl certificate and key. " "Otherwise explicitly allow insecure connection by specifying " "'--configuration-settings allowInsecureConnections=true'") feIsInternalLoadBalancer = _get_value_from_config_protected_config( - 'scoringFe.serviceType.internalLoadBalancer', configuration_settings, configuration_protected_settings) + self.privateEndpointILB, configuration_settings, configuration_protected_settings) feIsInternalLoadBalancer = str(feIsInternalLoadBalancer).lower() == 'true' if feIsInternalLoadBalancer: logger.warning( 'Internal load balancer only supported on AKS and AKS Engine Clusters.') + configuration_protected_settings['scoringFe.%s' % self.privateEndpointILB] = feIsInternalLoadBalancer + + def __set_up_inference_ssl(self, configuration_settings, configuration_protected_settings): + allowInsecureConnections = _get_value_from_config_protected_config( + self.allowInsecureConnections, configuration_settings, configuration_protected_settings) + allowInsecureConnections = str(allowInsecureConnections).lower() == 'true' + if not allowInsecureConnections: + import base64 + feSslCertFile = configuration_protected_settings.get(self.sslCertPemFile) + feSslKeyFile = configuration_protected_settings.get(self.sslKeyPemFile) + with open(feSslCertFile) as f: + cert_data = f.read() + cert_data_bytes = cert_data.encode("ascii") + ssl_cert = base64.b64encode(cert_data_bytes) + configuration_protected_settings['scoringFe.sslCert'] = ssl_cert + with open(feSslKeyFile) as f: + key_data = f.read() + key_data_bytes = key_data.encode("ascii") + ssl_key = base64.b64encode(key_data_bytes) + configuration_protected_settings['scoringFe.sslKey'] = ssl_key + else: + logger.warning( + 'SSL is not enabled. Allowing insecure connections to the deployed services.') def __create_required_resource( self, cmd, configuration_settings, configuration_protected_settings, subscription_id, resource_group_name, diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/data/azure_ml/cert_and_key_encoded.txt b/src/k8s-extension/azext_k8s_extension/tests/latest/data/azure_ml/cert_and_key_encoded.txt new file mode 100644 index 00000000000..4c2cb46c832 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/data/azure_ml/cert_and_key_encoded.txt @@ -0,0 +1,2 @@ +dGVzdGNlcnQ= +dGVzdGtleQ== \ No newline at end of file diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/data/azure_ml/test_cert.pem b/src/k8s-extension/azext_k8s_extension/tests/latest/data/azure_ml/test_cert.pem new file mode 100644 index 00000000000..e7529e3fdea --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/data/azure_ml/test_cert.pem @@ -0,0 +1 @@ +testcert \ No newline at end of file diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/data/azure_ml/test_key.pem b/src/k8s-extension/azext_k8s_extension/tests/latest/data/azure_ml/test_key.pem new file mode 100644 index 00000000000..7ef00201c75 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/data/azure_ml/test_key.pem @@ -0,0 +1 @@ +testkey \ No newline at end of file diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/test_azureml_extension.py b/src/k8s-extension/azext_k8s_extension/tests/latest/test_azureml_extension.py new file mode 100644 index 00000000000..26d0b85abfb --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/test_azureml_extension.py @@ -0,0 +1,32 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import os +import unittest + +from azext_k8s_extension.partner_extensions.AzureMLKubernetes import AzureMLKubernetes + + +TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) + + +class TestAzureMlExtension(unittest.TestCase): + + def test_set_up_inference_ssl(self): + azremlk8sInstance = AzureMLKubernetes() + config = {'allowInsecureConnections': 'false'} + # read and encode dummy cert and key + sslKeyPemFile = os.path.join(TEST_DIR, 'data', 'azure_ml', 'test_key.pem') + sslCertPemFile = os.path.join(TEST_DIR, 'data', 'azure_ml', 'test_cert.pem') + protected_config = {'sslKeyPemFile': sslKeyPemFile, 'sslCertPemFile': sslCertPemFile} + azremlk8sInstance._AzureMLKubernetes__set_up_inference_ssl(config, protected_config) + self.assertTrue('scoringFe.sslCert' in protected_config) + self.assertTrue('scoringFe.sslKey' in protected_config) + encoded_cert_and_key_file = os.path.join(TEST_DIR, 'data', 'azure_ml', 'cert_and_key_encoded.txt') + with open(encoded_cert_and_key_file, "rb") as text_file: + cert = text_file.readline().rstrip() + self.assertEquals(cert, protected_config['scoringFe.sslCert']) + key = text_file.readline() + self.assertEquals(key, protected_config['scoringFe.sslKey']) \ No newline at end of file diff --git a/testing/test/extensions/data/azure_ml/test_cert.pem b/testing/test/extensions/data/azure_ml/test_cert.pem new file mode 100644 index 00000000000..e7529e3fdea --- /dev/null +++ b/testing/test/extensions/data/azure_ml/test_cert.pem @@ -0,0 +1 @@ +testcert \ No newline at end of file diff --git a/testing/test/extensions/data/azure_ml/test_key.pem b/testing/test/extensions/data/azure_ml/test_key.pem new file mode 100644 index 00000000000..7ef00201c75 --- /dev/null +++ b/testing/test/extensions/data/azure_ml/test_key.pem @@ -0,0 +1 @@ +testkey \ No newline at end of file diff --git a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 index 35790d5f896..77b1cbdb343 100644 --- a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 +++ b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 @@ -150,4 +150,51 @@ Describe 'AzureML Kubernetes Testing' { $badOut | Should -Not -BeNullOrEmpty $output | Should -BeNullOrEmpty } + + It 'Creates the extension and checks that it onboards correctly with inference and SSL enabled' { + $sslKeyPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_key.pem" + $sslCertPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_cert.pem" + Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net experimental=True --config-protected sslKeyPemFile=$sslKeyPemFile sslCertPemFile=$sslCertPemFile" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty + + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty + + $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue + + # Loop and retry until the extension installs + $n = 0 + do + { + if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { + break + } + Start-Sleep -Seconds 20 + $n += 1 + } while ($n -le $MAX_RETRY_ATTEMPTS) + $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + + # check if relay is populated + $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey + $relayResourceID | Should -Not -BeNullOrEmpty + } + + It "Deletes the extension from the cluster with inference enabled" { + # cleanup the relay and servicebus + $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey + $serviceBusResourceID = Get-ExtensionConfigurationSettings $extensionName $serviceBusResourceIDKey + $relayNamespaceName = $relayResourceID.split("/")[8] + $serviceBusNamespaceName = $serviceBusResourceID.split("/")[8] + az relay namespace delete --resource-group $ENVCONFIG.resourceGroup --name $relayNamespaceName + az servicebus namespace delete --resource-group $ENVCONFIG.resourceGroup --name $serviceBusNamespaceName + + $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty + + # Extension should not be found on the cluster + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -Not -BeNullOrEmpty + $output | Should -BeNullOrEmpty + } } From a46235bf6554bb25c14e5af2532b4ba3377110b5 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Wed, 2 Jun 2021 15:21:20 -0700 Subject: [PATCH 67/86] Upgrade release version --- src/k8s-extension/HISTORY.rst | 5 +++++ src/k8s-extension/setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index 2fe8a591fb9..4a96664d562 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -3,6 +3,11 @@ Release History =============== +0.4.3 +++++++++++++++++++ +* Add SSL support for AzureML + + 0.4.2 ++++++++++++++++++ diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index 908ecc53009..7937b085006 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -32,7 +32,7 @@ # TODO: Add any additional SDK dependencies here DEPENDENCIES = [] -VERSION = "0.4.2" +VERSION = "0.4.3" with open('README.rst', 'r', encoding='utf-8') as f: README = f.read() From 2889d53948aa9ad34c45959ddd7a372ae082b701 Mon Sep 17 00:00:00 2001 From: Lia Kazakova <58274127+liakaz@users.noreply.github.com> Date: Wed, 2 Jun 2021 19:31:43 -0700 Subject: [PATCH 68/86] Liakaz/inference read ssl from file (#47) * first sketch of the change fixes removed extra blank lines changes regarding param renaming added ssl tests added more detail to the unit test additional import moved pem files out of public folder fixed import chenged import changed import unit tests fix unit test fix fixed unit tests fixed unit test unit test fix changes int test cert and key * test protected config * fix test typo * temporary changes reverted * fixing tests * fixed file paths * removed accidentally added file * changes according to review comments * more changes according to review comments * changes according to review comments * fixed decode error * renamed the experimental param Co-authored-by: Jonathan Innis --- .../partner_extensions/AzureMLKubernetes.py | 6 +++--- .../tests/latest/test_azureml_extension.py | 2 +- testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py index 229abcb7492..68e06a84692 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py @@ -189,7 +189,7 @@ def __validate_config(self, configuration_settings, configuration_protected_sett def __validate_scoring_fe_settings(self, configuration_settings, configuration_protected_settings): experimentalCluster = _get_value_from_config_protected_config( - 'experimental', configuration_settings, configuration_protected_settings) + 'inferenceLoadBalancerHA', configuration_settings, configuration_protected_settings) experimentalCluster = str(experimentalCluster).lower() == 'true' if experimentalCluster: configuration_settings['clusterPurpose'] = 'DevTest' @@ -225,12 +225,12 @@ def __set_up_inference_ssl(self, configuration_settings, configuration_protected with open(feSslCertFile) as f: cert_data = f.read() cert_data_bytes = cert_data.encode("ascii") - ssl_cert = base64.b64encode(cert_data_bytes) + ssl_cert = base64.b64encode(cert_data_bytes).decode() configuration_protected_settings['scoringFe.sslCert'] = ssl_cert with open(feSslKeyFile) as f: key_data = f.read() key_data_bytes = key_data.encode("ascii") - ssl_key = base64.b64encode(key_data_bytes) + ssl_key = base64.b64encode(key_data_bytes).decode() configuration_protected_settings['scoringFe.sslKey'] = ssl_key else: logger.warning( diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/test_azureml_extension.py b/src/k8s-extension/azext_k8s_extension/tests/latest/test_azureml_extension.py index 26d0b85abfb..6814a578398 100644 --- a/src/k8s-extension/azext_k8s_extension/tests/latest/test_azureml_extension.py +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/test_azureml_extension.py @@ -25,7 +25,7 @@ def test_set_up_inference_ssl(self): self.assertTrue('scoringFe.sslCert' in protected_config) self.assertTrue('scoringFe.sslKey' in protected_config) encoded_cert_and_key_file = os.path.join(TEST_DIR, 'data', 'azure_ml', 'cert_and_key_encoded.txt') - with open(encoded_cert_and_key_file, "rb") as text_file: + with open(encoded_cert_and_key_file, "r") as text_file: cert = text_file.readline().rstrip() self.assertEquals(cert, protected_config['scoringFe.sslCert']) key = text_file.readline() diff --git a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 index 77b1cbdb343..ac5573ad955 100644 --- a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 +++ b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 @@ -107,7 +107,7 @@ Describe 'AzureML Kubernetes Testing' { } It 'Creates the extension and checks that it onboards correctly with inference enabled' { - Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net allowInsecureConnections=True clusterPurpose=DevTest" -ErrorVariable badOut + Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net allowInsecureConnections=True inferenceLoadBalancerHA=true" -ErrorVariable badOut $badOut | Should -BeNullOrEmpty $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut @@ -154,7 +154,7 @@ Describe 'AzureML Kubernetes Testing' { It 'Creates the extension and checks that it onboards correctly with inference and SSL enabled' { $sslKeyPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_key.pem" $sslCertPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_cert.pem" - Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net experimental=True --config-protected sslKeyPemFile=$sslKeyPemFile sslCertPemFile=$sslCertPemFile" -ErrorVariable badOut + Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net inferenceLoadBalancerHA=True --config-protected sslKeyPemFile=$sslKeyPemFile sslCertPemFile=$sslCertPemFile" -ErrorVariable badOut $badOut | Should -BeNullOrEmpty $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut From 67babd86531042e7618395f58a9dfe665e1bb8a3 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Fri, 4 Jun 2021 11:51:05 -0700 Subject: [PATCH 69/86] Fix style issues (#51) --- k8s-custom-pipelines.yml | 2 +- .../azext_k8s_extension/custom.py | 2 -- .../partner_extensions/AzureMLKubernetes.py | 35 +++++++++++-------- .../partner_extensions/ContainerInsights.py | 4 +-- .../tests/latest/test_azureml_extension.py | 8 +++-- .../latest/test_k8s_extension_scenario.py | 21 ++++++----- 6 files changed, 39 insertions(+), 33 deletions(-) diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index 23857260310..76666940070 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -341,4 +341,4 @@ stages: displayName: "CLI Linter on Modified Extension" env: ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) - ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) + ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) \ No newline at end of file diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index d07c0015792..504539179d1 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -8,8 +8,6 @@ import json from knack.log import get_logger -from msrestazure.azure_exceptions import CloudError - from azure.cli.core.azclierror import ResourceNotFoundError, MutuallyExclusiveArgumentError, \ InvalidArgumentValueError, CommandNotFoundError, RequiredArgumentMissingError from azure.cli.core.commands.client_factory import get_subscription_id diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py index 68e06a84692..b9f039e9293 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py @@ -4,6 +4,9 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=unused-argument +# pylint: disable=line-too-long +# pylint: disable=too-many-locals + import copy from hashlib import md5 from typing import Any, Dict, List, Tuple @@ -17,8 +20,6 @@ import azure.mgmt.storage.models import azure.mgmt.loganalytics import azure.mgmt.loganalytics.models -from ..vendored_sdks.models import ( - ExtensionInstance, ExtensionInstanceUpdate, Scope, ScopeCluster) from azure.cli.core.azclierror import InvalidArgumentValueError from azure.cli.core.commands.client_factory import get_mgmt_service_client, get_subscription_id from azure.mgmt.resource.locks.models import ManagementLockObject @@ -27,12 +28,19 @@ from .._client_factory import cf_resources from .PartnerExtensionModel import PartnerExtensionModel +from ..vendored_sdks.models import ( + ExtensionInstance, + ExtensionInstanceUpdate, + Scope, + ScopeCluster +) logger = get_logger(__name__) resource_tag = {'created_by': 'Azure Arc-enabled ML'} +# pylint: disable=too-many-instance-attributes class AzureMLKubernetes(PartnerExtensionModel): def __init__(self): # constants for configuration settings. @@ -157,7 +165,7 @@ def __validate_config(self, configuration_settings, configuration_protected_sett config_keys = configuration_settings.keys() config_protected_keys = configuration_protected_settings.keys() dup_keys = set(config_keys) & set(config_protected_keys) - if len(dup_keys) > 0: + if dup_keys: for key in dup_keys: logger.warning( 'Duplicate keys found in both configuration settings and configuration protected setttings: %s', key) @@ -250,9 +258,8 @@ def __create_required_resource( configuration_settings[self.AZURE_LOG_ANALYTICS_CUSTOMER_ID_KEY] = ws_costumer_id configuration_protected_settings[self.AZURE_LOG_ANALYTICS_CONNECTION_STRING] = shared_key - if not configuration_settings.get( - self.RELAY_SERVER_CONNECTION_STRING) and not configuration_protected_settings.get( - self.RELAY_SERVER_CONNECTION_STRING): + if not configuration_settings.get(self.RELAY_SERVER_CONNECTION_STRING) and \ + not configuration_protected_settings.get(self.RELAY_SERVER_CONNECTION_STRING): logger.info('==== BEGIN RELAY CREATION ====') relay_connection_string, hc_resource_id, hc_name = _get_relay_connection_str( cmd, subscription_id, resource_group_name, cluster_name, cluster_location, self.RELAY_HC_AUTH_NAME) @@ -261,9 +268,8 @@ def __create_required_resource( configuration_settings[self.HC_RESOURCE_ID_KEY] = hc_resource_id configuration_settings[self.RELAY_HC_NAME_KEY] = hc_name - if not configuration_settings.get( - self.SERVICE_BUS_CONNECTION_STRING) and not configuration_protected_settings.get( - self.SERVICE_BUS_CONNECTION_STRING): + if not configuration_settings.get(self.SERVICE_BUS_CONNECTION_STRING) and \ + not configuration_protected_settings.get(self.SERVICE_BUS_CONNECTION_STRING): logger.info('==== BEGIN SERVICE BUS CREATION ====') topic_sub_mapping = { self.SERVICE_BUS_COMPUTE_STATE_TOPIC: self.SERVICE_BUS_COMPUTE_STATE_SUB, @@ -280,7 +286,7 @@ def __create_required_resource( def _get_valid_name(input_name: str, suffix_len: int, max_len: int) -> str: normalized_str = ''.join(filter(str.isalnum, input_name)) - assert len(normalized_str) > 0, "normalized name empty" + assert normalized_str, "normalized name empty" if len(normalized_str) <= max_len: return normalized_str @@ -295,6 +301,7 @@ def _get_valid_name(input_name: str, suffix_len: int, max_len: int) -> str: return new_name +# pylint: disable=broad-except def _lock_resource(cmd, lock_scope, lock_level='CanNotDelete'): lock_client: azure.mgmt.resource.locks.ManagementLockClient = get_mgmt_service_client( cmd.cli_ctx, azure.mgmt.resource.locks.ManagementLockClient) @@ -303,14 +310,13 @@ def _lock_resource(cmd, lock_scope, lock_level='CanNotDelete'): try: lock_client.management_locks.create_or_update_by_scope( scope=lock_scope, lock_name='amlarc-resource-lock', parameters=lock_object) - except: + except Exception: # try to lock the resource if user has the owner privilege pass def _get_relay_connection_str( - cmd, subscription_id, resource_group_name, cluster_name, cluster_location, auth_rule_name) -> Tuple[ - str, str, str]: + cmd, subscription_id, resource_group_name, cluster_name, cluster_location, auth_rule_name) -> Tuple[str, str, str]: relay_client: azure.mgmt.relay.RelayManagementClient = get_mgmt_service_client( cmd.cli_ctx, azure.mgmt.relay.RelayManagementClient) @@ -398,8 +404,7 @@ def _get_service_bus_connection_string(cmd, subscription_id, resource_group_name def _get_log_analytics_ws_connection_string( - cmd, subscription_id, resource_group_name, cluster_name, cluster_location) -> Tuple[ - str, str]: + cmd, subscription_id, resource_group_name, cluster_name, cluster_location) -> Tuple[str, str]: log_analytics_ws_client: azure.mgmt.loganalytics.LogAnalyticsManagementClient = get_mgmt_service_client( cmd.cli_ctx, azure.mgmt.loganalytics.LogAnalyticsManagementClient) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py index 3514122e391..e42f22199da 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py @@ -14,7 +14,6 @@ from azure.cli.core.commands import LongRunningOperation from azure.cli.core.commands.client_factory import get_mgmt_service_client, get_subscription_id from azure.cli.core.util import sdk_no_wait -from msrestazure.azure_exceptions import CloudError from msrestazure.tools import parse_resource_id, is_valid_resource_id from ..vendored_sdks.models import ExtensionInstance @@ -104,8 +103,7 @@ def _invoke_deployment(cmd, resource_group_name, deployment_name, template, para if cmd.supported_api_version(min_api='2019-10-01', resource_type=ResourceType.MGMT_RESOURCE_RESOURCES): validation_poller = smc.begin_validate(resource_group_name, deployment_name, deployment) return LongRunningOperation(cmd.cli_ctx)(validation_poller) - else: - return smc.validate(resource_group_name, deployment_name, deployment) + return smc.validate(resource_group_name, deployment_name, deployment) return sdk_no_wait(no_wait, smc.begin_create_or_update, resource_group_name, deployment_name, deployment) diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/test_azureml_extension.py b/src/k8s-extension/azext_k8s_extension/tests/latest/test_azureml_extension.py index 6814a578398..8ddf4dfaef2 100644 --- a/src/k8s-extension/azext_k8s_extension/tests/latest/test_azureml_extension.py +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/test_azureml_extension.py @@ -3,6 +3,8 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +# pylint: disable=protected-access + import os import unittest @@ -13,7 +15,7 @@ class TestAzureMlExtension(unittest.TestCase): - + def test_set_up_inference_ssl(self): azremlk8sInstance = AzureMLKubernetes() config = {'allowInsecureConnections': 'false'} @@ -27,6 +29,6 @@ def test_set_up_inference_ssl(self): encoded_cert_and_key_file = os.path.join(TEST_DIR, 'data', 'azure_ml', 'cert_and_key_encoded.txt') with open(encoded_cert_and_key_file, "r") as text_file: cert = text_file.readline().rstrip() - self.assertEquals(cert, protected_config['scoringFe.sslCert']) + assert cert == protected_config['scoringFe.sslCert'] key = text_file.readline() - self.assertEquals(key, protected_config['scoringFe.sslKey']) \ No newline at end of file + assert key == protected_config['scoringFe.sslKey'] diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py b/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py index 0e53c9e6691..010df2e3077 100644 --- a/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py @@ -3,9 +3,9 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -import os -import unittest +# pylint: disable=line-too-long +import os from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer, record_only) @@ -27,13 +27,16 @@ def test_k8s_extension(self): 'version': '0.1.0' }) - self.cmd('k8s-extension create -g {rg} -n {name} -c {cluster_name} --cluster-type {cluster_type} --extension-type {extension_type} --release-train {release_train} --version {version}', checks=[ - self.check('name', '{name}'), - self.check('releaseTrain', '{release_train}'), - self.check('version', '{version}'), - self.check('resourceGroup', '{rg}'), - self.check('extensionType', '{extension_type}') - ]) + self.cmd('k8s-extension create -g {rg} -n {name} -c {cluster_name} --cluster-type {cluster_type} ' + '--extension-type {extension_type} --release-train {release_train} --version {version}', + checks=[ + self.check('name', '{name}'), + self.check('releaseTrain', '{release_train}'), + self.check('version', '{version}'), + self.check('resourceGroup', '{rg}'), + self.check('extensionType', '{extension_type}') + ] + ) # Update is disabled for now # self.cmd('k8s-extension update -g {rg} -n {name} --tags foo=boo', checks=[ From 590e64218a93a16dfbd10fb358bc2fdac0a225c0 Mon Sep 17 00:00:00 2001 From: Lia Kazakova <58274127+liakaz@users.noreply.github.com> Date: Tue, 8 Jun 2021 10:08:02 -0700 Subject: [PATCH 70/86] Fixed scoring fe related extension param names (#49) * fixed scoring fe related extension params * bug fix and style fixes * variable rename * fixed the error type * set cluster to prod by default --- .../partner_extensions/AzureMLKubernetes.py | 25 +++++++++++++------ .../public/AzureMLKubernetes.Tests.ps1 | 4 +-- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py index b9f039e9293..bd49a164a3b 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py @@ -20,7 +20,7 @@ import azure.mgmt.storage.models import azure.mgmt.loganalytics import azure.mgmt.loganalytics.models -from azure.cli.core.azclierror import InvalidArgumentValueError +from azure.cli.core.azclierror import InvalidArgumentValueError, MutuallyExclusiveArgumentError from azure.cli.core.commands.client_factory import get_mgmt_service_client, get_subscription_id from azure.mgmt.resource.locks.models import ManagementLockObject from knack.log import get_logger @@ -79,6 +79,8 @@ def __init__(self): self.sslCertPemFile = 'sslCertPemFile' self.allowInsecureConnections = 'allowInsecureConnections' self.privateEndpointILB = 'privateEndpointILB' + self.privateEndpointNodeport = 'privateEndpointNodeport' + self.inferenceLoadBalancerHA = 'inferenceLoadBalancerHA' # reference mapping self.reference_mapping = { @@ -196,10 +198,10 @@ def __validate_config(self, configuration_settings, configuration_protected_sett configuration_protected_settings.pop(self.ENABLE_INFERENCE, None) def __validate_scoring_fe_settings(self, configuration_settings, configuration_protected_settings): - experimentalCluster = _get_value_from_config_protected_config( - 'inferenceLoadBalancerHA', configuration_settings, configuration_protected_settings) - experimentalCluster = str(experimentalCluster).lower() == 'true' - if experimentalCluster: + isTestCluster = _get_value_from_config_protected_config( + self.inferenceLoadBalancerHA, configuration_settings, configuration_protected_settings) + isTestCluster = str(isTestCluster).lower() == 'false' + if isTestCluster: configuration_settings['clusterPurpose'] = 'DevTest' else: configuration_settings['clusterPurpose'] = 'FastProd' @@ -214,13 +216,22 @@ def __validate_scoring_fe_settings(self, configuration_settings, configuration_p "Otherwise explicitly allow insecure connection by specifying " "'--configuration-settings allowInsecureConnections=true'") + feIsNodePort = _get_value_from_config_protected_config( + self.privateEndpointNodeport, configuration_settings, configuration_protected_settings) + feIsNodePort = str(feIsNodePort).lower() == 'true' feIsInternalLoadBalancer = _get_value_from_config_protected_config( self.privateEndpointILB, configuration_settings, configuration_protected_settings) feIsInternalLoadBalancer = str(feIsInternalLoadBalancer).lower() == 'true' - if feIsInternalLoadBalancer: + + if feIsNodePort and feIsInternalLoadBalancer: + raise MutuallyExclusiveArgumentError( + "Specify either privateEndpointNodeport=true or privateEndpointILB=true, but not both.") + elif feIsNodePort: + configuration_settings['scoringFe.serviceType.nodePort'] = feIsNodePort + elif feIsInternalLoadBalancer: + configuration_settings['scoringFe.serviceType.internalLoadBalancer'] = feIsInternalLoadBalancer logger.warning( 'Internal load balancer only supported on AKS and AKS Engine Clusters.') - configuration_protected_settings['scoringFe.%s' % self.privateEndpointILB] = feIsInternalLoadBalancer def __set_up_inference_ssl(self, configuration_settings, configuration_protected_settings): allowInsecureConnections = _get_value_from_config_protected_config( diff --git a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 index ac5573ad955..20b6a802b73 100644 --- a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 +++ b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 @@ -107,7 +107,7 @@ Describe 'AzureML Kubernetes Testing' { } It 'Creates the extension and checks that it onboards correctly with inference enabled' { - Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net allowInsecureConnections=True inferenceLoadBalancerHA=true" -ErrorVariable badOut + Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net allowInsecureConnections=True inferenceLoadBalancerHA=false" -ErrorVariable badOut $badOut | Should -BeNullOrEmpty $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut @@ -154,7 +154,7 @@ Describe 'AzureML Kubernetes Testing' { It 'Creates the extension and checks that it onboards correctly with inference and SSL enabled' { $sslKeyPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_key.pem" $sslCertPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_cert.pem" - Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net inferenceLoadBalancerHA=True --config-protected sslKeyPemFile=$sslKeyPemFile sslCertPemFile=$sslCertPemFile" -ErrorVariable badOut + Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net inferenceLoadBalancerHA=False --config-protected sslKeyPemFile=$sslKeyPemFile sslCertPemFile=$sslCertPemFile" -ErrorVariable badOut $badOut | Should -BeNullOrEmpty $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut From fc661a827672b0f43f94b6f6bd68d319c2944343 Mon Sep 17 00:00:00 2001 From: Niranjan Shankar Date: Fri, 11 Jun 2021 12:44:34 -0400 Subject: [PATCH 71/86] Add distro validation for osm-arc (#50) * Add distro validation for osm-arc * fixed indentation * Fix linting * Resolve comments * Add unit test * fix lint Co-authored-by: Jonathan Innis --- .../partner_extensions/OpenServiceMesh.py | 86 +++++++++++++++++-- .../tests/latest/test_open_service_mesh.py | 22 +++++ src/k8s-extension/setup.py | 4 +- 3 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py index 6d11064821c..2d477119c9d 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py @@ -5,20 +5,36 @@ # pylint: disable=unused-argument -from azure.cli.core.azclierror import InvalidArgumentValueError, RequiredArgumentMissingError from knack.log import get_logger -from ..vendored_sdks.models import ExtensionInstance -from ..vendored_sdks.models import ExtensionInstanceUpdate -from ..vendored_sdks.models import ScopeCluster -from ..vendored_sdks.models import Scope +from azure.cli.core.azclierror import InvalidArgumentValueError, RequiredArgumentMissingError +from azure.cli.core.commands.client_factory import get_subscription_id + +from pyhelm.chartbuilder import ChartBuilder +from pyhelm.repo import VersionError +from packaging import version +import yaml + +from ..partner_extensions import PartnerExtensionModel from .PartnerExtensionModel import PartnerExtensionModel +from ..vendored_sdks.models import ( + ExtensionInstance, + ExtensionInstanceUpdate, + ScopeCluster, + Scope +) + +from .._client_factory import cf_resources + logger = get_logger(__name__) class OpenServiceMesh(PartnerExtensionModel): + CHART_NAME = "osm-arc" + CHART_LOCATION = "https://azure.github.io/osm-azure" + def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, scope, auto_upgrade_minor_version, release_train, version, target_namespace, release_namespace, configuration_settings, configuration_protected_settings, @@ -62,6 +78,9 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t # NOTE-2: Return a valid ExtensionInstance object, Instance name and flag for Identity create_identity = False + + _validate_tested_distro(cmd, resource_group_name, cluster_name, version) + extension_instance = ExtensionInstance( extension_type=extension_type, auto_upgrade_minor_version=auto_upgrade_minor_version, @@ -93,3 +112,60 @@ def Update(self, extension, auto_upgrade_minor_version, release_train, version): release_train=release_train, version=version ) + + +def _validate_tested_distro(cmd, cluster_resource_group_name, cluster_name, extension_version): + + field_unavailable_error = '\"testedDistros\" field unavailable for version {0} of microsoft.openservicemesh, ' \ + 'cannot determine if this Kubernetes distribution has been properly tested'.format(extension_version) + + if version.parse(str(extension_version)) <= version.parse("0.8.3"): + logger.warning(field_unavailable_error) + return + + subscription_id = get_subscription_id(cmd.cli_ctx) + resources = cf_resources(cmd.cli_ctx, subscription_id) + + cluster_resource_id = '/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Kubernetes' \ + '/connectedClusters/{2}'.format(subscription_id, cluster_resource_group_name, cluster_name) + + resource = resources.get_by_id(cluster_resource_id, '2020-01-01-preview') + cluster_distro = resource.properties['distribution'].lower() + + if cluster_distro == "general": + logger.warning('Unable to determine if distro has been tested for microsoft.openservicemesh, ' + 'kubernetes distro: \"general\"') + return + + tested_distros = _get_tested_distros(extension_version) + + if tested_distros is None: + logger.warning(field_unavailable_error) + elif cluster_distro not in tested_distros.split(): + logger.warning('Untested kubernetes distro for microsoft.openservicemesh, Kubernetes distro is %s', + cluster_distro) + + +def _get_tested_distros(chart_version): + + try: + chart_arc = ChartBuilder({ + "name": OpenServiceMesh.CHART_NAME, + "version": str(chart_version), + "source": { + "type": "repo", + "location": OpenServiceMesh.CHART_LOCATION + } + }) + except VersionError: + raise InvalidArgumentValueError( + "Invalid version '{}' for microsoft.openservicemesh".format(chart_version) + ) + + values = chart_arc.get_values() + values_yaml = yaml.load(values.raw, Loader=yaml.FullLoader) + + try: + return values_yaml['OpenServiceMesh']['testedDistros'] + except KeyError: + return None diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py b/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py new file mode 100644 index 00000000000..d3b322ef799 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py @@ -0,0 +1,22 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=protected-access + +import os +import unittest + +from azext_k8s_extension.partner_extensions.OpenServiceMesh import _get_tested_distros +from azure.cli.core.azclierror import InvalidArgumentValueError + +TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) + +class TestOpenServiceMesh(unittest.TestCase): + def test_bad_osm_arc_version(self): + version = "0.7.1" + err = "Invalid version \'" + str(version) + "\' for microsoft.openservicemesh" + with self.assertRaises(InvalidArgumentValueError) as argError: + _get_tested_distros(version) + self.assertEqual(str(argError.exception), err) diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index 7937b085006..566d36f4b4f 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -30,7 +30,9 @@ ] # TODO: Add any additional SDK dependencies here -DEPENDENCIES = [] +DEPENDENCIES = [ + 'pyhelm' +] VERSION = "0.4.3" From eb466aa494c02370ae47881fa871ffdbbbc67fd4 Mon Sep 17 00:00:00 2001 From: Niranjan Shankar Date: Fri, 11 Jun 2021 12:44:34 -0400 Subject: [PATCH 72/86] Add distro validation for osm-arc (#50) * Add distro validation for osm-arc * fixed indentation * Fix linting * Resolve comments * Add unit test * fix lint Co-authored-by: Jonathan Innis --- src/k8s-extension/HISTORY.rst | 1 - .../partner_extensions/OpenServiceMesh.py | 86 +++++++++++++++++-- .../tests/latest/test_open_service_mesh.py | 22 +++++ src/k8s-extension/setup.py | 4 +- 4 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index 4a96664d562..297c5ed46cb 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -7,7 +7,6 @@ Release History ++++++++++++++++++ * Add SSL support for AzureML - 0.4.2 ++++++++++++++++++ diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py index 6d11064821c..2d477119c9d 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py @@ -5,20 +5,36 @@ # pylint: disable=unused-argument -from azure.cli.core.azclierror import InvalidArgumentValueError, RequiredArgumentMissingError from knack.log import get_logger -from ..vendored_sdks.models import ExtensionInstance -from ..vendored_sdks.models import ExtensionInstanceUpdate -from ..vendored_sdks.models import ScopeCluster -from ..vendored_sdks.models import Scope +from azure.cli.core.azclierror import InvalidArgumentValueError, RequiredArgumentMissingError +from azure.cli.core.commands.client_factory import get_subscription_id + +from pyhelm.chartbuilder import ChartBuilder +from pyhelm.repo import VersionError +from packaging import version +import yaml + +from ..partner_extensions import PartnerExtensionModel from .PartnerExtensionModel import PartnerExtensionModel +from ..vendored_sdks.models import ( + ExtensionInstance, + ExtensionInstanceUpdate, + ScopeCluster, + Scope +) + +from .._client_factory import cf_resources + logger = get_logger(__name__) class OpenServiceMesh(PartnerExtensionModel): + CHART_NAME = "osm-arc" + CHART_LOCATION = "https://azure.github.io/osm-azure" + def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, scope, auto_upgrade_minor_version, release_train, version, target_namespace, release_namespace, configuration_settings, configuration_protected_settings, @@ -62,6 +78,9 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t # NOTE-2: Return a valid ExtensionInstance object, Instance name and flag for Identity create_identity = False + + _validate_tested_distro(cmd, resource_group_name, cluster_name, version) + extension_instance = ExtensionInstance( extension_type=extension_type, auto_upgrade_minor_version=auto_upgrade_minor_version, @@ -93,3 +112,60 @@ def Update(self, extension, auto_upgrade_minor_version, release_train, version): release_train=release_train, version=version ) + + +def _validate_tested_distro(cmd, cluster_resource_group_name, cluster_name, extension_version): + + field_unavailable_error = '\"testedDistros\" field unavailable for version {0} of microsoft.openservicemesh, ' \ + 'cannot determine if this Kubernetes distribution has been properly tested'.format(extension_version) + + if version.parse(str(extension_version)) <= version.parse("0.8.3"): + logger.warning(field_unavailable_error) + return + + subscription_id = get_subscription_id(cmd.cli_ctx) + resources = cf_resources(cmd.cli_ctx, subscription_id) + + cluster_resource_id = '/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Kubernetes' \ + '/connectedClusters/{2}'.format(subscription_id, cluster_resource_group_name, cluster_name) + + resource = resources.get_by_id(cluster_resource_id, '2020-01-01-preview') + cluster_distro = resource.properties['distribution'].lower() + + if cluster_distro == "general": + logger.warning('Unable to determine if distro has been tested for microsoft.openservicemesh, ' + 'kubernetes distro: \"general\"') + return + + tested_distros = _get_tested_distros(extension_version) + + if tested_distros is None: + logger.warning(field_unavailable_error) + elif cluster_distro not in tested_distros.split(): + logger.warning('Untested kubernetes distro for microsoft.openservicemesh, Kubernetes distro is %s', + cluster_distro) + + +def _get_tested_distros(chart_version): + + try: + chart_arc = ChartBuilder({ + "name": OpenServiceMesh.CHART_NAME, + "version": str(chart_version), + "source": { + "type": "repo", + "location": OpenServiceMesh.CHART_LOCATION + } + }) + except VersionError: + raise InvalidArgumentValueError( + "Invalid version '{}' for microsoft.openservicemesh".format(chart_version) + ) + + values = chart_arc.get_values() + values_yaml = yaml.load(values.raw, Loader=yaml.FullLoader) + + try: + return values_yaml['OpenServiceMesh']['testedDistros'] + except KeyError: + return None diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py b/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py new file mode 100644 index 00000000000..d3b322ef799 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py @@ -0,0 +1,22 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +# pylint: disable=protected-access + +import os +import unittest + +from azext_k8s_extension.partner_extensions.OpenServiceMesh import _get_tested_distros +from azure.cli.core.azclierror import InvalidArgumentValueError + +TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) + +class TestOpenServiceMesh(unittest.TestCase): + def test_bad_osm_arc_version(self): + version = "0.7.1" + err = "Invalid version \'" + str(version) + "\' for microsoft.openservicemesh" + with self.assertRaises(InvalidArgumentValueError) as argError: + _get_tested_distros(version) + self.assertEqual(str(argError.exception), err) diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index 7937b085006..566d36f4b4f 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -30,7 +30,9 @@ ] # TODO: Add any additional SDK dependencies here -DEPENDENCIES = [] +DEPENDENCIES = [ + 'pyhelm' +] VERSION = "0.4.3" From b0af5983e881b110d2101ade59a78dd3196f275a Mon Sep 17 00:00:00 2001 From: Niranjan Shankar Date: Tue, 15 Jun 2021 19:09:10 -0400 Subject: [PATCH 73/86] Add distro validation for osm-arc (#53) removed release-train logic --- .../partner_extensions/OpenServiceMesh.py | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py index 2d477119c9d..4839c5d763a 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py @@ -54,27 +54,15 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t scope_cluster = ScopeCluster(release_namespace=release_namespace) ext_scope = Scope(cluster=scope_cluster, namespace=None) - valid_release_trains = ['staging', 'pilot'] - # If release-train is not input, set it to 'stable' - if release_train is None: + # version is a mandatory if release-train is staging or pilot + if version is None: raise RequiredArgumentMissingError( - "A release-train must be provided. Valid values are 'staging', 'pilot'." - ) - - if release_train.lower() in valid_release_trains: - # version is a mandatory if release-train is staging or pilot - if version is None: - raise RequiredArgumentMissingError( - "A version must be provided for release-train {}.".format(release_train) - ) - # If the release-train is 'staging' or 'pilot' then auto-upgrade-minor-version MUST be set to False - if auto_upgrade_minor_version or auto_upgrade_minor_version is None: - auto_upgrade_minor_version = False - logger.warning("Setting auto-upgrade-minor-version to False since release-train is '%s'", release_train) - else: - raise InvalidArgumentValueError( - "Invalid release-train '{}'. Valid values are 'staging', 'pilot'.".format(release_train) + "A version must be provided for release-train {}.".format(release_train) ) + # If the release-train is 'staging' or 'pilot' then auto-upgrade-minor-version MUST be set to False + if auto_upgrade_minor_version or auto_upgrade_minor_version is None: + auto_upgrade_minor_version = False + logger.warning("Setting auto-upgrade-minor-version to False since release-train is '%s'", release_train) # NOTE-2: Return a valid ExtensionInstance object, Instance name and flag for Identity create_identity = False From c355ad5fb99874b63e433d17856de0b1f9e11927 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Tue, 22 Jun 2021 16:54:11 -0700 Subject: [PATCH 74/86] Add Custom Delete Logic for Partners (#54) * Add custom delete logic * Fix failing unit tests --- .../azext_k8s_extension/custom.py | 11 ++ .../partner_extensions/AzureDefender.py | 3 + .../partner_extensions/AzureMLKubernetes.py | 5 +- .../partner_extensions/Cassandra.py | 3 + .../partner_extensions/ContainerInsights.py | 3 + .../partner_extensions/DefaultExtension.py | 3 + .../partner_extensions/OpenServiceMesh.py | 5 + .../PartnerExtensionModel.py | 4 + .../latest/recordings/test_k8s_extension.yaml | 158 ++++++++++++++---- .../latest/test_k8s_extension_scenario.py | 9 +- .../tests/latest/test_open_service_mesh.py | 2 +- 11 files changed, 170 insertions(+), 36 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index 504539179d1..8d6b82d1885 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -195,6 +195,17 @@ def delete_k8s_extension(client, resource_group_name, cluster_name, name, cluste """ # Determine ClusterRP cluster_rp = __get_cluster_rp(cluster_type) + extension = None + try: + extension = client.get(resource_group_name, cluster_rp, cluster_type, cluster_name, name) + except ErrorResponseException: + logger.warning("No extension with name '%s' found on cluster '%s', so nothing to delete", cluster_name, name) + return None + extension_class = ExtensionFactory(extension.extension_type.lower()) + + # If there is any custom delete logic, this will call the logic + extension_class.Delete(client, resource_group_name, cluster_name, name, cluster_type) + return client.delete(resource_group_name, cluster_rp, cluster_type, cluster_name, name) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py index a3e805006de..ffb6a926328 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureDefender.py @@ -70,3 +70,6 @@ def Update(self, extension, auto_upgrade_minor_version, release_train, version): release_train=release_train, version=version ) + + def Delete(self, client, resource_group_name, cluster_name, name, cluster_type): + pass diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py index bd49a164a3b..83a38b7f25d 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py @@ -162,6 +162,9 @@ def Update(self, extension, auto_upgrade_minor_version, release_train, version): version=version ) + def Delete(self, client, resource_group_name, cluster_name, name, cluster_type): + pass + def __validate_config(self, configuration_settings, configuration_protected_settings): # perform basic validation of the input config config_keys = configuration_settings.keys() @@ -226,7 +229,7 @@ def __validate_scoring_fe_settings(self, configuration_settings, configuration_p if feIsNodePort and feIsInternalLoadBalancer: raise MutuallyExclusiveArgumentError( "Specify either privateEndpointNodeport=true or privateEndpointILB=true, but not both.") - elif feIsNodePort: + if feIsNodePort: configuration_settings['scoringFe.serviceType.nodePort'] = feIsNodePort elif feIsInternalLoadBalancer: configuration_settings['scoringFe.serviceType.internalLoadBalancer'] = feIsInternalLoadBalancer diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/Cassandra.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/Cassandra.py index 2357bf08af6..289e8053223 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/Cassandra.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/Cassandra.py @@ -55,3 +55,6 @@ def Update(self, extension, auto_upgrade_minor_version, release_train, version): release_train=release_train, version=version ) + + def Delete(self, client, resource_group_name, cluster_name, name, cluster_type): + pass diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py index e42f22199da..1c2a334cdda 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/ContainerInsights.py @@ -82,6 +82,9 @@ def Update(self, extension, auto_upgrade_minor_version, release_train, version): version=version ) + def Delete(self, client, resource_group_name, cluster_name, name, cluster_type): + pass + # Custom Validation Logic for Container Insights diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py index a72aef847fc..8977ec4187e 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/DefaultExtension.py @@ -55,3 +55,6 @@ def Update(self, extension, auto_upgrade_minor_version, release_train, version): release_train=release_train, version=version ) + + def Delete(self, client, resource_group_name, cluster_name, name, cluster_type): + pass diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py index 4839c5d763a..c5ad73ac35f 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py @@ -4,6 +4,8 @@ # -------------------------------------------------------------------------------------------- # pylint: disable=unused-argument +# pylint: disable=redefined-outer-name +# pylint: disable=no-member from knack.log import get_logger @@ -101,6 +103,9 @@ def Update(self, extension, auto_upgrade_minor_version, release_train, version): version=version ) + def Delete(self, client, resource_group_name, cluster_name, name, cluster_type): + pass + def _validate_tested_distro(cmd, cluster_resource_group_name, cluster_name, extension_version): diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py index b8cb01334d3..98a6c1ea63f 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/PartnerExtensionModel.py @@ -21,3 +21,7 @@ def Create(self, cmd, client, resource_group_name: str, cluster_name: str, name: def Update(self, extension: ExtensionInstance, auto_upgrade_minor_version: bool, release_train: str, version: str) -> ExtensionInstanceUpdate: pass + + @abstractmethod + def Delete(self, client, resource_group_name: str, cluster_name: str, name: str, cluster_type: str): + pass diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/recordings/test_k8s_extension.yaml b/src/k8s-extension/azext_k8s_extension/tests/latest/recordings/test_k8s_extension.yaml index 127b21ac873..c55ebb0a737 100644 --- a/src/k8s-extension/azext_k8s_extension/tests/latest/recordings/test_k8s_extension.yaml +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/recordings/test_k8s_extension.yaml @@ -1,9 +1,58 @@ interactions: - request: - body: '{"properties": {"extensionType": "microsoft.openservicemesh", "autoUpgradeMinorVersion": - false, "releaseTrain": "staging", "version": "0.1.0", "scope": {"cluster": {}}, - "configurationSettings": {}, "configurationProtectedSettings": {}}, "location": - ""}' + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension create + Connection: + - keep-alive + ParameterSetName: + - -g -n -c --cluster-type --extension-type --release-train --version + User-Agent: + - AZURECLI/2.24.2 azsdk-python-azure-mgmt-resource/18.0.0 Python/3.9.0 (Windows-10-10.0.19041-SP0) + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration?api-version=2021-04-01 + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.KubernetesConfiguration","namespace":"Microsoft.KubernetesConfiguration","authorizations":[{"applicationId":"c699bf69-fb1d-4eaf-999b-99e6b2ae4d85","roleDefinitionId":"90155430-a360-410f-af5d-89dc284d85c6"},{"applicationId":"03db181c-e9d3-4868-9097-f0b728327182","roleDefinitionId":"DE2ADB97-42D8-49C8-8FCF-DBB53EF936AC"},{"applicationId":"a0f92522-89de-4c5e-9a75-0044ccf66efd","roleDefinitionId":"b3429810-7d5c-420e-8605-cf280f3099f2"},{"applicationId":"bd9b7cd5-dac1-495f-b013-ac871e98fa5f","roleDefinitionId":"0d44c8f0-08b9-44d4-9f59-e51c83f95200"}],"resourceTypes":[{"resourceType":"sourceControlConfigurations","locations":["East + US","West Europe","West Central US","West US 2","South Central US","East US + 2","North Europe","UK South","Southeast Asia","Australia East","France Central","East + US 2 EUAP"],"apiVersions":["2021-03-01","2020-10-01-preview","2020-07-01-preview","2019-11-01-preview"],"defaultApiVersion":"2021-03-01","capabilities":"SupportsExtension"},{"resourceType":"extensions","locations":["East + US","West Europe","West Central US","West US 2","South Central US","East US + 2","North Europe","UK South","Southeast Asia","Australia East","France Central","East + US 2 EUAP"],"apiVersions":["2021-05-01-preview","2020-07-01-preview"],"capabilities":"SystemAssignedResourceIdentity, + SupportsExtension"},{"resourceType":"operations","locations":[],"apiVersions":["2021-05-01-preview","2021-03-01","2020-10-01-preview","2020-07-01-preview","2019-11-01-preview"],"capabilities":"None"}],"registrationState":"Registered","registrationPolicy":"RegistrationRequired"}' + headers: + cache-control: + - no-cache + content-length: + - '1654' + content-type: + - application/json; charset=utf-8 + date: + - Tue, 22 Jun 2021 23:19:11 GMT + expires: + - '-1' + pragma: + - no-cache + strict-transport-security: + - max-age=31536000; includeSubDomains + vary: + - Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: '{"location": "", "properties": {"extensionType": "microsoft.openservicemesh", + "autoUpgradeMinorVersion": false, "releaseTrain": "pilot", "version": "0.8.3", + "scope": {"cluster": {}}, "configurationSettings": {}, "configurationProtectedSettings": + {}}}' headers: Accept: - application/json @@ -14,32 +63,32 @@ interactions: Connection: - keep-alive Content-Length: - - '252' + - '250' Content-Type: - application/json; charset=utf-8 ParameterSetName: - -g -n -c --cluster-type --extension-type --release-train --version User-Agent: - python/3.9.0 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.4 - azure-mgmt-kubernetesconfiguration/0.1.0 Azure-SDK-For-Python AZURECLI/2.19.1 + azure-mgmt-kubernetesconfiguration/0.3.0 Azure-SDK-For-Python AZURECLI/2.24.2 accept-language: - en-US method: PUT - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh?api-version=2020-07-01-preview + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservicemesh?api-version=2020-07-01-preview response: body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh","name":"openservice-mesh","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"configurationSettings":{},"statuses":[],"extensionType":"microsoft.openservicemesh","autoUpgradeMinorVersion":false,"releaseTrain":"staging","version":"0.1.0","scope":{"cluster":{"releaseNamespace":"arc-osm-system"}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-03-08T23:14:12.4010326+00:00","lastModifiedTime":"2021-03-08T23:14:12.4010327+00:00"}}' + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservicemesh","name":"openservicemesh","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"configurationSettings":{},"statuses":[],"extensionType":"microsoft.openservicemesh","autoUpgradeMinorVersion":false,"releaseTrain":"pilot","version":"0.8.3","scope":{"cluster":{"releaseNamespace":"arc-osm-system"}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-06-22T23:19:13.9750935+00:00","lastModifiedTime":"2021-06-22T23:19:13.9750935+00:00"}}' headers: api-supported-versions: - - 2020-07-01-Preview + - 2020-07-01-Preview, 2021-05-01-preview cache-control: - no-cache content-length: - - '708' + - '704' content-type: - application/json; charset=utf-8 date: - - Mon, 08 Mar 2021 23:14:11 GMT + - Tue, 22 Jun 2021 23:19:13 GMT expires: - '-1' pragma: @@ -74,25 +123,25 @@ interactions: - -c -g --cluster-type User-Agent: - python/3.9.0 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.4 - azure-mgmt-kubernetesconfiguration/0.1.0 Azure-SDK-For-Python AZURECLI/2.19.1 + azure-mgmt-kubernetesconfiguration/0.3.0 Azure-SDK-For-Python AZURECLI/2.24.2 accept-language: - en-US method: GET uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions?api-version=2020-07-01-preview response: body: - string: '{"value":[{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh","name":"openservice-mesh","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"extensionType":"microsoft.openservicemesh","autoUpgradeMinorVersion":false,"releaseTrain":"staging","version":"0.1.0","scope":{"cluster":{"releaseNamespace":"arc-osm-system"}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-03-08T23:14:12.4010326+00:00","lastModifiedTime":"2021-03-08T23:14:12.4010327+00:00"}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/hci22jan21","name":"hci22jan21","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"extensionType":"microsoft.azstackhci.operator","autoUpgradeMinorVersion":true,"releaseTrain":"stable","version":"1.0.0","scope":{"cluster":{"releaseNamespace":null}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-01-22T20:49:34.3336157+00:00","lastModifiedTime":"2021-01-22T20:49:34.3336249+00:00"}}],"nextLink":null}' + string: '{"value":[{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservicemesh","name":"openservicemesh","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"extensionType":"microsoft.openservicemesh","autoUpgradeMinorVersion":false,"releaseTrain":"pilot","version":"0.8.3","scope":{"cluster":{"releaseNamespace":"arc-osm-system"}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-06-22T23:19:13.9750935+00:00","lastModifiedTime":"2021-06-22T23:19:13.9750935+00:00"}},{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/hci22jan21","name":"hci22jan21","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"extensionType":"microsoft.azstackhci.operator","autoUpgradeMinorVersion":true,"releaseTrain":"stable","version":"1.0.0","scope":{"cluster":{"releaseNamespace":null}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-01-22T20:49:34.3336157+00:00","lastModifiedTime":"2021-01-22T20:49:34.3336249+00:00"}}],"nextLink":null}' headers: api-supported-versions: - - 2020-07-01-Preview + - 2020-07-01-Preview, 2021-05-01-preview cache-control: - no-cache content-length: - - '1341' + - '1337' content-type: - application/json; charset=utf-8 date: - - Mon, 08 Mar 2021 23:14:13 GMT + - Tue, 22 Jun 2021 23:19:14 GMT expires: - '-1' pragma: @@ -125,25 +174,76 @@ interactions: - -c -g -n --cluster-type User-Agent: - python/3.9.0 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.4 - azure-mgmt-kubernetesconfiguration/0.1.0 Azure-SDK-For-Python AZURECLI/2.19.1 + azure-mgmt-kubernetesconfiguration/0.3.0 Azure-SDK-For-Python AZURECLI/2.24.2 + accept-language: + - en-US + method: GET + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservicemesh?api-version=2020-07-01-preview + response: + body: + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservicemesh","name":"openservicemesh","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"configurationSettings":{},"statuses":[],"extensionType":"microsoft.openservicemesh","autoUpgradeMinorVersion":false,"releaseTrain":"pilot","version":"0.8.3","scope":{"cluster":{"releaseNamespace":"arc-osm-system"}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-06-22T23:19:13.9750935+00:00","lastModifiedTime":"2021-06-22T23:19:13.9750935+00:00"}}' + headers: + api-supported-versions: + - 2020-07-01-Preview, 2021-05-01-preview + cache-control: + - no-cache + content-length: + - '704' + content-type: + - application/json; charset=utf-8 + date: + - Tue, 22 Jun 2021 23:19:15 GMT + expires: + - '-1' + pragma: + - no-cache + server: + - openresty/1.15.8.2 + strict-transport-security: + - max-age=31536000; includeSubDomains + transfer-encoding: + - chunked + vary: + - Accept-Encoding,Accept-Encoding + x-content-type-options: + - nosniff + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - k8s-extension delete + Connection: + - keep-alive + ParameterSetName: + - -g -c -n --cluster-type -y + User-Agent: + - python/3.9.0 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.4 + azure-mgmt-kubernetesconfiguration/0.3.0 Azure-SDK-For-Python AZURECLI/2.24.2 accept-language: - en-US method: GET - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh?api-version=2020-07-01-preview + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservicemesh?api-version=2020-07-01-preview response: body: - string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh","name":"openservice-mesh","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"configurationSettings":{},"statuses":[],"extensionType":"microsoft.openservicemesh","autoUpgradeMinorVersion":false,"releaseTrain":"staging","version":"0.1.0","scope":{"cluster":{"releaseNamespace":"arc-osm-system"}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-03-08T23:14:12.4010326+00:00","lastModifiedTime":"2021-03-08T23:14:12.4010327+00:00"}}' + string: '{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservicemesh","name":"openservicemesh","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"configurationSettings":{},"statuses":[],"extensionType":"microsoft.openservicemesh","autoUpgradeMinorVersion":false,"releaseTrain":"pilot","version":"0.8.3","scope":{"cluster":{"releaseNamespace":"arc-osm-system"}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-06-22T23:19:13.9750935+00:00","lastModifiedTime":"2021-06-22T23:19:13.9750935+00:00"}}' headers: api-supported-versions: - - 2020-07-01-Preview + - 2020-07-01-Preview, 2021-05-01-preview cache-control: - no-cache content-length: - - '708' + - '704' content-type: - application/json; charset=utf-8 date: - - Mon, 08 Mar 2021 23:14:14 GMT + - Tue, 22 Jun 2021 23:19:16 GMT expires: - '-1' pragma: @@ -178,17 +278,17 @@ interactions: - -g -c -n --cluster-type -y User-Agent: - python/3.9.0 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.4 - azure-mgmt-kubernetesconfiguration/0.1.0 Azure-SDK-For-Python AZURECLI/2.19.1 + azure-mgmt-kubernetesconfiguration/0.3.0 Azure-SDK-For-Python AZURECLI/2.24.2 accept-language: - en-US method: DELETE - uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservice-mesh?api-version=2020-07-01-preview + uri: https://management.azure.com/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/openservicemesh?api-version=2020-07-01-preview response: body: string: '{"content":null,"statusCode":200,"headers":[],"version":"1.1","reasonPhrase":"OK","trailingHeaders":[],"requestMessage":null,"isSuccessStatusCode":true}' headers: api-supported-versions: - - 2020-07-01-Preview + - 2020-07-01-Preview, 2021-05-01-preview cache-control: - no-cache content-length: @@ -196,7 +296,7 @@ interactions: content-type: - application/json; charset=utf-8 date: - - Mon, 08 Mar 2021 23:14:14 GMT + - Tue, 22 Jun 2021 23:19:17 GMT expires: - '-1' pragma: @@ -231,7 +331,7 @@ interactions: - -c -g --cluster-type User-Agent: - python/3.9.0 (Windows-10-10.0.19041-SP0) msrest/0.6.21 msrest_azure/0.6.4 - azure-mgmt-kubernetesconfiguration/0.1.0 Azure-SDK-For-Python AZURECLI/2.19.1 + azure-mgmt-kubernetesconfiguration/0.3.0 Azure-SDK-For-Python AZURECLI/2.24.2 accept-language: - en-US method: GET @@ -241,7 +341,7 @@ interactions: string: '{"value":[{"id":"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/nanthirg0923/providers/Microsoft.Kubernetes/connectedClusters/nanthicluster0923/providers/Microsoft.KubernetesConfiguration/extensions/hci22jan21","name":"hci22jan21","type":"Microsoft.KubernetesConfiguration/extensions","properties":{"extensionType":"microsoft.azstackhci.operator","autoUpgradeMinorVersion":true,"releaseTrain":"stable","version":"1.0.0","scope":{"cluster":{"releaseNamespace":null}},"installState":"Pending","lastStatusTime":null,"errorInfo":{},"creationTime":"2021-01-22T20:49:34.3336157+00:00","lastModifiedTime":"2021-01-22T20:49:34.3336249+00:00"}}],"nextLink":null}' headers: api-supported-versions: - - 2020-07-01-Preview + - 2020-07-01-Preview, 2021-05-01-preview cache-control: - no-cache content-length: @@ -249,7 +349,7 @@ interactions: content-type: - application/json; charset=utf-8 date: - - Mon, 08 Mar 2021 23:14:16 GMT + - Tue, 22 Jun 2021 23:19:19 GMT expires: - '-1' pragma: diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py b/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py index 010df2e3077..53db4ce2c2d 100644 --- a/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/test_k8s_extension_scenario.py @@ -6,7 +6,7 @@ # pylint: disable=line-too-long import os -from azure.cli.testsdk import (ScenarioTest, ResourceGroupPreparer, record_only) +from azure.cli.testsdk import (ScenarioTest, record_only) TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) @@ -14,17 +14,16 @@ class K8sExtensionScenarioTest(ScenarioTest): @record_only() - @ResourceGroupPreparer(name_prefix='cli_test_k8s_extension') def test_k8s_extension(self): resource_type = 'microsoft.openservicemesh' self.kwargs.update({ - 'name': 'openservice-mesh', + 'name': 'openservicemesh', 'rg': 'nanthirg0923', 'cluster_name': 'nanthicluster0923', 'cluster_type': 'connectedClusters', 'extension_type': resource_type, - 'release_train': 'staging', - 'version': '0.1.0' + 'release_train': 'pilot', + 'version': '0.8.3' }) self.cmd('k8s-extension create -g {rg} -n {name} -c {cluster_name} --cluster-type {cluster_type} ' diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py b/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py index d3b322ef799..72e94a06831 100644 --- a/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py @@ -8,8 +8,8 @@ import os import unittest -from azext_k8s_extension.partner_extensions.OpenServiceMesh import _get_tested_distros from azure.cli.core.azclierror import InvalidArgumentValueError +from azext_k8s_extension.partner_extensions.OpenServiceMesh import _get_tested_distros TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) From cda5e41a82a5cdd4ddec4721fa6354e5ed9d5c39 Mon Sep 17 00:00:00 2001 From: jingyizhu99 <83610845+jingyizhu99@users.noreply.github.com> Date: Wed, 23 Jun 2021 13:45:57 -0700 Subject: [PATCH 75/86] Add warning message when deleting amlarc extension (#55) * add warning message * fix indentation --- .../partner_extensions/AzureMLKubernetes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py index 83a38b7f25d..9f6aff45aac 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/AzureMLKubernetes.py @@ -163,7 +163,10 @@ def Update(self, extension, auto_upgrade_minor_version, release_train, version): ) def Delete(self, client, resource_group_name, cluster_name, name, cluster_type): - pass + # Give a warning message + logger.warning("If nvidia.com/gpu or fuse resource is not recognized by kubernetes after this deletion, " + "you probably have installed nvidia-device-plugin or fuse-device-plugin before installing AMLArc extension. " + "Please try to reinstall device plugins to fix this issue.") def __validate_config(self, configuration_settings, configuration_protected_settings): # perform basic validation of the input config From c423e5464a1d72ded075185a4257bda9729e6151 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Thu, 24 Jun 2021 10:28:57 -0700 Subject: [PATCH 76/86] Update release version --- src/k8s-extension/HISTORY.rst | 5 +++++ src/k8s-extension/setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index 297c5ed46cb..eeed091c6cc 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -3,6 +3,11 @@ Release History =============== +0.5.0 +++++++++++++++++++ +* Add microsoft.openservicemesh customization to check distros +* Delete customization for partners + 0.4.3 ++++++++++++++++++ * Add SSL support for AzureML diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index 566d36f4b4f..a8400af15cc 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -34,7 +34,7 @@ 'pyhelm' ] -VERSION = "0.4.3" +VERSION = "0.5.0" with open('README.rst', 'r', encoding='utf-8') as f: README = f.read() From 58b4df8bb01790dc6905a56f5926c6d6aca3f987 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Thu, 8 Jul 2021 17:03:09 -0700 Subject: [PATCH 77/86] Remove Pyhelm from OSM customization (#58) * Fix OSM pyhelm bug * Debug bootstrap error --- k8s-custom-pipelines.yml | 2 +- src/k8s-extension/HISTORY.rst | 4 + .../partner_extensions/OpenServiceMesh.py | 88 +++++++++--------- .../tests/latest/test_open_service_mesh.py | 13 +-- src/k8s-extension/setup.py | 6 +- testing/Bootstrap.ps1 | 18 ++-- .../bin/connectedk8s-1.0.0-py3-none-any.whl | Bin 62802 -> 0 bytes testing/bin/connectedk8s-values.yaml | 3 - testing/settings.template.json | 3 +- 9 files changed, 67 insertions(+), 70 deletions(-) delete mode 100644 testing/bin/connectedk8s-1.0.0-py3-none-any.whl delete mode 100644 testing/bin/connectedk8s-values.yaml diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml index 76666940070..185af022510 100644 --- a/k8s-custom-pipelines.yml +++ b/k8s-custom-pipelines.yml @@ -95,7 +95,7 @@ stages: - bash : | echo "Downloading the kind script" - curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.9.0/kind-linux-amd64 + curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.11.1/kind-linux-amd64 chmod +x ./kind ./kind create cluster displayName: "Create and Start the Kind cluster" diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index eeed091c6cc..e5560ac1e81 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -3,6 +3,10 @@ Release History =============== +0.5.1 +++++++++++++++++++ +* Remove pyhelm which was causing users to have git installed + 0.5.0 ++++++++++++++++++ * Add microsoft.openservicemesh customization to check distros diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py index c5ad73ac35f..048dfe58637 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py @@ -12,8 +12,6 @@ from azure.cli.core.azclierror import InvalidArgumentValueError, RequiredArgumentMissingError from azure.cli.core.commands.client_factory import get_subscription_id -from pyhelm.chartbuilder import ChartBuilder -from pyhelm.repo import VersionError from packaging import version import yaml @@ -69,7 +67,7 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t # NOTE-2: Return a valid ExtensionInstance object, Instance name and flag for Identity create_identity = False - _validate_tested_distro(cmd, resource_group_name, cluster_name, version) + # _validate_tested_distro(cmd, resource_group_name, cluster_name, version) extension_instance = ExtensionInstance( extension_type=extension_type, @@ -107,58 +105,58 @@ def Delete(self, client, resource_group_name, cluster_name, name, cluster_type): pass -def _validate_tested_distro(cmd, cluster_resource_group_name, cluster_name, extension_version): +# def _validate_tested_distro(cmd, cluster_resource_group_name, cluster_name, extension_version): - field_unavailable_error = '\"testedDistros\" field unavailable for version {0} of microsoft.openservicemesh, ' \ - 'cannot determine if this Kubernetes distribution has been properly tested'.format(extension_version) +# field_unavailable_error = '\"testedDistros\" field unavailable for version {0} of microsoft.openservicemesh, ' \ +# 'cannot determine if this Kubernetes distribution has been properly tested'.format(extension_version) - if version.parse(str(extension_version)) <= version.parse("0.8.3"): - logger.warning(field_unavailable_error) - return +# if version.parse(str(extension_version)) <= version.parse("0.8.3"): +# logger.warning(field_unavailable_error) +# return - subscription_id = get_subscription_id(cmd.cli_ctx) - resources = cf_resources(cmd.cli_ctx, subscription_id) +# subscription_id = get_subscription_id(cmd.cli_ctx) +# resources = cf_resources(cmd.cli_ctx, subscription_id) - cluster_resource_id = '/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Kubernetes' \ - '/connectedClusters/{2}'.format(subscription_id, cluster_resource_group_name, cluster_name) +# cluster_resource_id = '/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Kubernetes' \ +# '/connectedClusters/{2}'.format(subscription_id, cluster_resource_group_name, cluster_name) - resource = resources.get_by_id(cluster_resource_id, '2020-01-01-preview') - cluster_distro = resource.properties['distribution'].lower() +# resource = resources.get_by_id(cluster_resource_id, '2020-01-01-preview') +# cluster_distro = resource.properties['distribution'].lower() - if cluster_distro == "general": - logger.warning('Unable to determine if distro has been tested for microsoft.openservicemesh, ' - 'kubernetes distro: \"general\"') - return +# if cluster_distro == "general": +# logger.warning('Unable to determine if distro has been tested for microsoft.openservicemesh, ' +# 'kubernetes distro: \"general\"') +# return - tested_distros = _get_tested_distros(extension_version) +# tested_distros = _get_tested_distros(extension_version) - if tested_distros is None: - logger.warning(field_unavailable_error) - elif cluster_distro not in tested_distros.split(): - logger.warning('Untested kubernetes distro for microsoft.openservicemesh, Kubernetes distro is %s', - cluster_distro) +# if tested_distros is None: +# logger.warning(field_unavailable_error) +# elif cluster_distro not in tested_distros.split(): +# logger.warning('Untested kubernetes distro for microsoft.openservicemesh, Kubernetes distro is %s', +# cluster_distro) -def _get_tested_distros(chart_version): +# def _get_tested_distros(chart_version): - try: - chart_arc = ChartBuilder({ - "name": OpenServiceMesh.CHART_NAME, - "version": str(chart_version), - "source": { - "type": "repo", - "location": OpenServiceMesh.CHART_LOCATION - } - }) - except VersionError: - raise InvalidArgumentValueError( - "Invalid version '{}' for microsoft.openservicemesh".format(chart_version) - ) +# try: +# chart_arc = ChartBuilder({ +# "name": OpenServiceMesh.CHART_NAME, +# "version": str(chart_version), +# "source": { +# "type": "repo", +# "location": OpenServiceMesh.CHART_LOCATION +# } +# }) +# except VersionError: +# raise InvalidArgumentValueError( +# "Invalid version '{}' for microsoft.openservicemesh".format(chart_version) +# ) - values = chart_arc.get_values() - values_yaml = yaml.load(values.raw, Loader=yaml.FullLoader) +# values = chart_arc.get_values() +# values_yaml = yaml.load(values.raw, Loader=yaml.FullLoader) - try: - return values_yaml['OpenServiceMesh']['testedDistros'] - except KeyError: - return None +# try: +# return values_yaml['OpenServiceMesh']['testedDistros'] +# except KeyError: +# return None diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py b/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py index 72e94a06831..61b774045ce 100644 --- a/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py @@ -9,14 +9,15 @@ import unittest from azure.cli.core.azclierror import InvalidArgumentValueError -from azext_k8s_extension.partner_extensions.OpenServiceMesh import _get_tested_distros +# from azext_k8s_extension.partner_extensions.OpenServiceMesh import _get_tested_distros TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) class TestOpenServiceMesh(unittest.TestCase): def test_bad_osm_arc_version(self): - version = "0.7.1" - err = "Invalid version \'" + str(version) + "\' for microsoft.openservicemesh" - with self.assertRaises(InvalidArgumentValueError) as argError: - _get_tested_distros(version) - self.assertEqual(str(argError.exception), err) + # version = "0.7.1" + # err = "Invalid version \'" + str(version) + "\' for microsoft.openservicemesh" + # with self.assertRaises(InvalidArgumentValueError) as argError: + # _get_tested_distros(version) + # self.assertEqual(str(argError.exception), err) + pass diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index a8400af15cc..a9a7f60fbaa 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -30,11 +30,9 @@ ] # TODO: Add any additional SDK dependencies here -DEPENDENCIES = [ - 'pyhelm' -] +DEPENDENCIES = [] -VERSION = "0.5.0" +VERSION = "0.5.1" with open('README.rst', 'r', encoding='utf-8') as f: README = f.read() diff --git a/testing/Bootstrap.ps1 b/testing/Bootstrap.ps1 index c9123ba6d03..0598b139c79 100644 --- a/testing/Bootstrap.ps1 +++ b/testing/Bootstrap.ps1 @@ -18,15 +18,10 @@ if (-not (Test-Path -Path $PSScriptRoot/tmp)) { if (!$SkipInstall) { Write-Host "Removing the old connnectedk8s extension..." az extension remove -n connectedk8s - $connectedk8sVersion = $ENVCONFIG.extensionVersion.connectedk8s - if (!$connectedk8sVersion) { - Write-Host "connectedk8s extension version wasn't specified" -ForegroundColor Red - Exit 1 - } - Write-Host "Installing connectedk8s version $connectedk8sVersion..." - az extension add --source ./bin/connectedk8s-$connectedk8sVersion-py3-none-any.whl + Write-Host "Installing connectedk8s..." + az extension add -n connectedk8s if (!$?) { - Write-Host "Unable to find connectedk8s version $connectedk8sVersion, exiting..." + Write-Host "Unable to install connectedk8s, exiting..." exit 1 } } @@ -76,5 +71,10 @@ if ($?) Write-Host "Connecting the cluster to Arc with connectedk8s..." $Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" -$Env:HELMVALUESPATH="$PSScriptRoot/bin/connectedk8s-values.yaml" az connectedk8s connect -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.arcClusterName +if (!$?) +{ + kubectl get pods -A + Exit 1 +} +Write-Host "Successfully onboarded the cluster to Azure" \ No newline at end of file diff --git a/testing/bin/connectedk8s-1.0.0-py3-none-any.whl b/testing/bin/connectedk8s-1.0.0-py3-none-any.whl deleted file mode 100644 index 08f34250036f455aad7e3e820c65d08d790e1201..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 62802 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

YVX(toUj*0P8con1X*tXmm22nWyt~gf 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

4OLf9N*^x0^yG1@+&DtXglO();gI7nFsJg6 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 t?|!AXoz(-D1V4AjNV;_XqKSV$h1XIg0B%9ihR@ Date: Thu, 8 Jul 2021 17:12:52 -0700 Subject: [PATCH 78/86] Update release message --- src/k8s-extension/HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index e5560ac1e81..bfa65dac229 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -5,7 +5,7 @@ Release History 0.5.1 ++++++++++++++++++ -* Remove pyhelm which was causing users to have git installed +* Remove pyhelm dependency 0.5.0 ++++++++++++++++++ From 4ca94bde44aa82e17dded343568c85f08b09bc5f Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Thu, 8 Jul 2021 18:00:37 -0700 Subject: [PATCH 79/86] Remove pyhelm dependency --- src/k8s-extension/setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index 218b0d47aa5..a9a7f60fbaa 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -30,9 +30,7 @@ ] # TODO: Add any additional SDK dependencies here -DEPENDENCIES = [ - 'pyhelm' -] +DEPENDENCIES = [] VERSION = "0.5.1" From a5ef607747a1d2e81a6c9839346bf9f6a18599c4 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Fri, 9 Jul 2021 16:32:00 -0700 Subject: [PATCH 80/86] Update tests to only check extensionconfig creation (#61) * Update tests to only check extensionconfig creation * Single set of CRUD for AzureML * Debug logs for connectedk8s --- testing/Cleanup.ps1 | 5 + .../extensions/public/AzureDefender.Tests.ps1 | 2 +- .../public/AzureMLKubernetes.Tests.ps1 | 201 +++++++++--------- .../extensions/public/AzureMonitor.Tests.ps1 | 6 +- .../public/OpenServiceMesh.Tests.ps1 | 6 +- testing/test/helper/Helper.ps1 | 11 + 6 files changed, 122 insertions(+), 109 deletions(-) diff --git a/testing/Cleanup.ps1 b/testing/Cleanup.ps1 index 9957a044241..dc1d1d3d908 100644 --- a/testing/Cleanup.ps1 +++ b/testing/Cleanup.ps1 @@ -12,6 +12,11 @@ az account set --subscription $ENVCONFIG.subscriptionId $Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" Write-Host "Removing the connectedk8s arc agents from the cluster..." az connectedk8s delete -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.arcClusterName +if (!$?) +{ + kubectl get pods -A + Exit 1 +} # Skip deleting the AKS Cluster if this is CI if (!$CI) { diff --git a/testing/test/extensions/public/AzureDefender.Tests.ps1 b/testing/test/extensions/public/AzureDefender.Tests.ps1 index 1ef498b67e6..e60d443f620 100644 --- a/testing/test/extensions/public/AzureDefender.Tests.ps1 +++ b/testing/test/extensions/public/AzureDefender.Tests.ps1 @@ -23,7 +23,7 @@ Describe 'Azure Defender Testing' { do { # Only check the extension config, not the pod since this doesn't bring up pods - if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { + if (Has-ExtensionData $extensionName) { break } Start-Sleep -Seconds 10 diff --git a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 index 20b6a802b73..4625ff0016a 100644 --- a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 +++ b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 @@ -10,8 +10,10 @@ Describe 'AzureML Kubernetes Testing' { . $PSScriptRoot/../../helper/Helper.ps1 } - It 'Creates the extension and checks that it onboards correctly with training enabled' { - Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableTraining=true" -ErrorVariable badOut + It 'Creates the extension and checks that it onboards correctly with inference and SSL enabled' { + $sslKeyPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_key.pem" + $sslCertPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_cert.pem" + Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net inferenceLoadBalancerHA=False --config-protected sslKeyPemFile=$sslKeyPemFile sslCertPemFile=$sslCertPemFile" -ErrorVariable badOut $badOut | Should -BeNullOrEmpty $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut @@ -24,7 +26,7 @@ Describe 'AzureML Kubernetes Testing' { $n = 0 do { - if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { + if (Has-ExtensionData $extensionName) { break } Start-Sleep -Seconds 20 @@ -37,12 +39,6 @@ Describe 'AzureML Kubernetes Testing' { $relayResourceID | Should -Not -BeNullOrEmpty } - It "Performs a show on the extension" { - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - $output | Should -Not -BeNullOrEmpty - } - It "Runs an update on the extension on the cluster" { Set-ItResult -Skipped -Because "Update is not a valid scenario for now" az k8s-extension update --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName --auto-upgrade-minor-version false @@ -70,6 +66,13 @@ Describe 'AzureML Kubernetes Testing' { $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS } + It "Performs a show on the extension" { + $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + $badOut | Should -BeNullOrEmpty + $output | Should -Not -BeNullOrEmpty + } + + It "Lists the extensions on the cluster" { $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut $badOut | Should -BeNullOrEmpty @@ -79,7 +82,7 @@ Describe 'AzureML Kubernetes Testing' { $extensionExists | Should -Not -BeNullOrEmpty } - It "Deletes the extension from the cluster" { + It "Deletes the extension from the cluster with inference enabled" { # cleanup the relay and servicebus $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey $serviceBusResourceID = Get-ExtensionConfigurationSettings $extensionName $serviceBusResourceIDKey @@ -96,7 +99,7 @@ Describe 'AzureML Kubernetes Testing' { $badOut | Should -Not -BeNullOrEmpty $output | Should -BeNullOrEmpty } - + It "Performs another list after the delete" { $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut $badOut | Should -BeNullOrEmpty @@ -106,95 +109,93 @@ Describe 'AzureML Kubernetes Testing' { $extensionExists | Should -BeNullOrEmpty } - It 'Creates the extension and checks that it onboards correctly with inference enabled' { - Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net allowInsecureConnections=True inferenceLoadBalancerHA=false" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue - - # Loop and retry until the extension installs - $n = 0 - do - { - if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { - break - } - Start-Sleep -Seconds 20 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + # It 'Creates the extension and checks that it onboards correctly with training enabled' { + # Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableTraining=true" -ErrorVariable badOut + # $badOut | Should -BeNullOrEmpty + + # $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + # $badOut | Should -BeNullOrEmpty + + # $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + # $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue + + # # Loop and retry until the extension installs + # $n = 0 + # do + # { + # if (Has-ExtensionData $extensionName) { + # break + # } + # Start-Sleep -Seconds 20 + # $n += 1 + # } while ($n -le $MAX_RETRY_ATTEMPTS) + # $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - # check if relay is populated - $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey - $relayResourceID | Should -Not -BeNullOrEmpty - } - - It "Deletes the extension from the cluster with inference enabled" { - # cleanup the relay and servicebus - $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey - $serviceBusResourceID = Get-ExtensionConfigurationSettings $extensionName $serviceBusResourceIDKey - $relayNamespaceName = $relayResourceID.split("/")[8] - $serviceBusNamespaceName = $serviceBusResourceID.split("/")[8] - az relay namespace delete --resource-group $ENVCONFIG.resourceGroup --name $relayNamespaceName - az servicebus namespace delete --resource-group $ENVCONFIG.resourceGroup --name $serviceBusNamespaceName - - $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - # Extension should not be found on the cluster - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -Not -BeNullOrEmpty - $output | Should -BeNullOrEmpty - } - - It 'Creates the extension and checks that it onboards correctly with inference and SSL enabled' { - $sslKeyPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_key.pem" - $sslCertPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_cert.pem" - Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net inferenceLoadBalancerHA=False --config-protected sslKeyPemFile=$sslKeyPemFile sslCertPemFile=$sslCertPemFile" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue - - # Loop and retry until the extension installs - $n = 0 - do - { - if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { - break - } - Start-Sleep -Seconds 20 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS + # # check if relay is populated + # $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey + # $relayResourceID | Should -Not -BeNullOrEmpty + # } + + # It "Deletes the extension from the cluster" { + # # cleanup the relay and servicebus + # $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey + # $serviceBusResourceID = Get-ExtensionConfigurationSettings $extensionName $serviceBusResourceIDKey + # $relayNamespaceName = $relayResourceID.split("/")[8] + # $serviceBusNamespaceName = $serviceBusResourceID.split("/")[8] + # az relay namespace delete --resource-group $ENVCONFIG.resourceGroup --name $relayNamespaceName + # az servicebus namespace delete --resource-group $ENVCONFIG.resourceGroup --name $serviceBusNamespaceName + + # $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + # $badOut | Should -BeNullOrEmpty + + # # Extension should not be found on the cluster + # $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + # $badOut | Should -Not -BeNullOrEmpty + # $output | Should -BeNullOrEmpty + # } + + # It 'Creates the extension and checks that it onboards correctly with inference enabled' { + # Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net allowInsecureConnections=True inferenceLoadBalancerHA=false" -ErrorVariable badOut + # $badOut | Should -BeNullOrEmpty + + # $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + # $badOut | Should -BeNullOrEmpty + + # $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion + # $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue + + # # Loop and retry until the extension installs + # $n = 0 + # do + # { + # if (Has-ExtensionData $extensionName) { + # break + # } + # Start-Sleep -Seconds 20 + # $n += 1 + # } while ($n -le $MAX_RETRY_ATTEMPTS) + # $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - # check if relay is populated - $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey - $relayResourceID | Should -Not -BeNullOrEmpty - } - - It "Deletes the extension from the cluster with inference enabled" { - # cleanup the relay and servicebus - $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey - $serviceBusResourceID = Get-ExtensionConfigurationSettings $extensionName $serviceBusResourceIDKey - $relayNamespaceName = $relayResourceID.split("/")[8] - $serviceBusNamespaceName = $serviceBusResourceID.split("/")[8] - az relay namespace delete --resource-group $ENVCONFIG.resourceGroup --name $relayNamespaceName - az servicebus namespace delete --resource-group $ENVCONFIG.resourceGroup --name $serviceBusNamespaceName - - $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - # Extension should not be found on the cluster - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -Not -BeNullOrEmpty - $output | Should -BeNullOrEmpty - } + # # check if relay is populated + # $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey + # $relayResourceID | Should -Not -BeNullOrEmpty + # } + + # It "Deletes the extension from the cluster with inference enabled" { + # # cleanup the relay and servicebus + # $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey + # $serviceBusResourceID = Get-ExtensionConfigurationSettings $extensionName $serviceBusResourceIDKey + # $relayNamespaceName = $relayResourceID.split("/")[8] + # $serviceBusNamespaceName = $serviceBusResourceID.split("/")[8] + # az relay namespace delete --resource-group $ENVCONFIG.resourceGroup --name $relayNamespaceName + # az servicebus namespace delete --resource-group $ENVCONFIG.resourceGroup --name $serviceBusNamespaceName + + # $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + # $badOut | Should -BeNullOrEmpty + + # # Extension should not be found on the cluster + # $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut + # $badOut | Should -Not -BeNullOrEmpty + # $output | Should -BeNullOrEmpty + # } } diff --git a/testing/test/extensions/public/AzureMonitor.Tests.ps1 b/testing/test/extensions/public/AzureMonitor.Tests.ps1 index a34d8160728..a78ec6ba980 100644 --- a/testing/test/extensions/public/AzureMonitor.Tests.ps1 +++ b/testing/test/extensions/public/AzureMonitor.Tests.ps1 @@ -23,10 +23,8 @@ Describe 'Azure Monitor Testing' { $n = 0 do { - if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { - if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { - break - } + if (Has-ExtensionData $extensionName) { + break } Start-Sleep -Seconds 10 $n += 1 diff --git a/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 b/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 index 893d3c3e468..a637ee48302 100644 --- a/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 +++ b/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 @@ -27,10 +27,8 @@ Describe 'Azure OpenServiceMesh Testing' { $n = 0 do { - if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { - if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { - break - } + if (Has-ExtensionData $extensionName) { + break } Start-Sleep -Seconds 10 $n += 1 diff --git a/testing/test/helper/Helper.ps1 b/testing/test/helper/Helper.ps1 index db76c41cff4..7bb11146ab2 100644 --- a/testing/test/helper/Helper.ps1 +++ b/testing/test/helper/Helper.ps1 @@ -7,6 +7,17 @@ function Get-ExtensionData { return $output.items | Where-Object { $_.metadata.name -eq $extensionName } } +function Has-ExtensionData { + param( + [string]$extensionName + ) + $extensionData = Get-ExtensionData $extensionName + if ($extensionData) { + return $true + } + return $false +} + function Get-ExtensionStatus { param( [string]$extensionName From d8528be8bcb3816b4b990666b4b3dbe108fd5145 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Mon, 19 Jul 2021 13:44:41 -0700 Subject: [PATCH 81/86] Increase open service mesh version number --- testing/test/extensions/public/OpenServiceMesh.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 b/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 index a637ee48302..3070d579731 100644 --- a/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 +++ b/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 @@ -2,7 +2,7 @@ Describe 'Azure OpenServiceMesh Testing' { BeforeAll { $extensionType = "microsoft.openservicemesh" $extensionName = "openservicemesh" - $extensionVersion = "0.8.3" + $extensionVersion = "0.9.1" $extensionAgentName = "osm-controller" $extensionAgentNamespace = "arc-osm-system" $releaseTrain = "pilot" From 64ac05a683fe4ba910a4cba95157169568089003 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Tue, 20 Jul 2021 11:18:45 -0700 Subject: [PATCH 82/86] Update k8s-extension Models to Track2 (#64) * Update k8s-extension models to track2 * Add debug for failed cleanup * Increase version number * Exit 0 on failed cleanup --- src/k8s-extension/HISTORY.rst | 4 + .../azext_k8s_extension/custom.py | 8 +- .../vendored_sdks/__init__.py | 18 +- .../vendored_sdks/_configuration.py | 82 ++- .../_source_control_configuration_client.py | 101 ++- .../vendored_sdks/{version.py => _version.py} | 10 +- .../vendored_sdks/aio/__init__.py | 10 + .../vendored_sdks/aio/_configuration.py | 67 ++ .../_source_control_configuration_client.py | 92 +++ .../vendored_sdks/aio/operations/__init__.py | 17 + .../aio/operations/_extensions_operations.py | 433 ++++++++++++ .../aio/operations/_operations.py | 105 +++ ...ource_control_configurations_operations.py | 420 ++++++++++++ .../vendored_sdks/models/__init__.py | 79 ++- .../vendored_sdks/models/_models.py | 575 +++++++++------- .../vendored_sdks/models/_models_py3.py | 638 +++++++++++------- .../vendored_sdks/models/_paged_models.py | 53 -- ...urce_control_configuration_client_enums.py | 112 +-- .../vendored_sdks/operations/__init__.py | 7 +- .../operations/_extensions_operations.py | 533 ++++++++------- .../vendored_sdks/operations/_operations.py | 127 ++-- ...ource_control_configurations_operations.py | 529 ++++++++------- src/k8s-extension/setup.py | 2 +- testing/Cleanup.ps1 | 3 +- 24 files changed, 2766 insertions(+), 1259 deletions(-) rename src/k8s-extension/azext_k8s_extension/vendored_sdks/{version.py => _version.py} (84%) create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/__init__.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/_configuration.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/_source_control_configuration_client.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/operations/__init__.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/operations/_extensions_operations.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/operations/_operations.py create mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/operations/_source_control_configurations_operations.py delete mode 100644 src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_paged_models.py diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index bfa65dac229..5855f96591f 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -3,6 +3,10 @@ Release History =============== +0.6.0 +++++++++++++++++++ +* Update extension resource models to Track2 + 0.5.1 ++++++++++++++++++ * Remove pyhelm dependency diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index 8d6b82d1885..9a6bdb10656 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -11,7 +11,8 @@ from azure.cli.core.azclierror import ResourceNotFoundError, MutuallyExclusiveArgumentError, \ InvalidArgumentValueError, CommandNotFoundError, RequiredArgumentMissingError from azure.cli.core.commands.client_factory import get_subscription_id -from .vendored_sdks.models import ConfigurationIdentity, ErrorResponseException, Scope +from azure.core.exceptions import HttpResponseError +from .vendored_sdks.models import ConfigurationIdentity, Scope from ._validators import validate_cc_registration from .partner_extensions.ContainerInsights import ContainerInsights @@ -52,7 +53,7 @@ def show_k8s_extension(client, resource_group_name, cluster_name, name, cluster_ extension = client.get(resource_group_name, cluster_rp, cluster_type, cluster_name, name) return extension - except ErrorResponseException as ex: + except HttpResponseError as ex: # Customize the error message for resources not found if ex.response.status_code == 404: # If Cluster not found @@ -198,7 +199,7 @@ def delete_k8s_extension(client, resource_group_name, cluster_name, name, cluste extension = None try: extension = client.get(resource_group_name, cluster_rp, cluster_type, cluster_name, name) - except ErrorResponseException: + except HttpResponseError: logger.warning("No extension with name '%s' found on cluster '%s', so nothing to delete", cluster_name, name) return None extension_class = ExtensionFactory(extension.extension_type.lower()) @@ -230,7 +231,6 @@ def __create_identity(cmd, resource_group_name, cluster_name, cluster_type, clus "Error! Cluster type '{}' is not supported for extension identity".format(cluster_type) ) - from azure.core.exceptions import HttpResponseError try: resource = resources.get_by_id(cluster_resource_id, parent_api_version) location = str(resource.location.lower()) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/__init__.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/__init__.py index 874177b4d34..f13062376d7 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/__init__.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/__init__.py @@ -1,19 +1,19 @@ # 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. -# +# 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. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -from ._configuration import SourceControlConfigurationClientConfiguration from ._source_control_configuration_client import SourceControlConfigurationClient -__all__ = ['SourceControlConfigurationClient', 'SourceControlConfigurationClientConfiguration'] - -from .version import VERSION +from ._version import VERSION __version__ = VERSION +__all__ = ['SourceControlConfigurationClient'] +try: + from ._patch import patch_sdk # type: ignore + patch_sdk() +except ImportError: + pass diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/_configuration.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_configuration.py index 5043ed69594..89bdc90391d 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/_configuration.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_configuration.py @@ -1,49 +1,71 @@ # 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. -# +# 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. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -from msrestazure import AzureConfiguration -from .version import VERSION +from typing import TYPE_CHECKING +from azure.core.configuration import Configuration +from azure.core.pipeline import policies +from azure.mgmt.core.policies import ARMHttpLoggingPolicy + +from ._version import VERSION + +if TYPE_CHECKING: + # pylint: disable=unused-import,ungrouped-imports + from typing import Any + + from azure.core.credentials import TokenCredential + + +class SourceControlConfigurationClientConfiguration(Configuration): + """Configuration for SourceControlConfigurationClient. -class SourceControlConfigurationClientConfiguration(AzureConfiguration): - """Configuration for SourceControlConfigurationClient Note that all parameters used to create this instance are saved as instance attributes. - :param credentials: Credentials needed for the client to connect to Azure. - :type credentials: :mod:`A msrestazure Credentials - object` - :param subscription_id: The Azure subscription ID. This is a - GUID-formatted string (e.g. 00000000-0000-0000-0000-000000000000) + :param credential: Credential needed for the client to connect to Azure. + :type credential: ~azure.core.credentials.TokenCredential + :param subscription_id: The Azure subscription ID. This is a GUID-formatted string (e.g. 00000000-0000-0000-0000-000000000000). :type subscription_id: str - :param str base_url: Service URL """ def __init__( - self, credentials, subscription_id, base_url=None): - - if credentials is None: - raise ValueError("Parameter 'credentials' must not be None.") + self, + credential, # type: "TokenCredential" + subscription_id, # type: str + **kwargs # type: Any + ): + # type: (...) -> None + if credential is None: + raise ValueError("Parameter 'credential' must not be None.") if subscription_id is None: raise ValueError("Parameter 'subscription_id' must not be None.") - if not base_url: - base_url = 'https://management.azure.com' - - super(SourceControlConfigurationClientConfiguration, self).__init__(base_url) - - # Starting Autorest.Python 4.0.64, make connection pool activated by default - self.keep_alive = True + super(SourceControlConfigurationClientConfiguration, self).__init__(**kwargs) - self.add_user_agent('azure-mgmt-kubernetesconfiguration/{}'.format(VERSION)) - self.add_user_agent('Azure-SDK-For-Python') - - self.credentials = credentials + self.credential = credential self.subscription_id = subscription_id + self.api_version = "2020-07-01-preview" + self.credential_scopes = kwargs.pop('credential_scopes', ['https://management.azure.com/.default']) + kwargs.setdefault('sdk_moniker', 'mgmt-kubernetesconfiguration/{}'.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 = policies.BearerTokenCredentialPolicy(self.credential, *self.credential_scopes, **kwargs) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/_source_control_configuration_client.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_source_control_configuration_client.py index a77176d8cb1..71f3974f333 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/_source_control_configuration_client.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_source_control_configuration_client.py @@ -1,16 +1,22 @@ # 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. -# +# 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. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -from msrest.service_client import SDKClient -from msrest import Serializer, Deserializer +from typing import TYPE_CHECKING + +from azure.mgmt.core import ARMPipelineClient +from msrest import Deserializer, Serializer + +if TYPE_CHECKING: + # pylint: disable=unused-import,ungrouped-imports + from typing import Any, Optional + + from azure.core.credentials import TokenCredential + from azure.core.pipeline.transport import HttpRequest, HttpResponse from ._configuration import SourceControlConfigurationClientConfiguration from .operations import SourceControlConfigurationsOperations @@ -19,42 +25,75 @@ from . import models -class SourceControlConfigurationClient(SDKClient): - """KubernetesConfiguration Client +class SourceControlConfigurationClient(object): + """KubernetesConfiguration Client. - :ivar config: Configuration for client. - :vartype config: SourceControlConfigurationClientConfiguration - - :ivar source_control_configurations: SourceControlConfigurations operations - :vartype source_control_configurations: azure.mgmt.kubernetesconfiguration.operations.SourceControlConfigurationsOperations + :ivar source_control_configurations: SourceControlConfigurationsOperations operations + :vartype source_control_configurations: azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.operations.SourceControlConfigurationsOperations :ivar operations: Operations operations - :vartype operations: azure.mgmt.kubernetesconfiguration.operations.Operations - :ivar extensions: Extensions operations - :vartype extensions: azure.mgmt.kubernetesconfiguration.operations.ExtensionsOperations - - :param credentials: Credentials needed for the client to connect to Azure. - :type credentials: :mod:`A msrestazure Credentials - object` - :param subscription_id: The Azure subscription ID. This is a - GUID-formatted string (e.g. 00000000-0000-0000-0000-000000000000) + :vartype operations: azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.operations.Operations + :ivar extensions: ExtensionsOperations operations + :vartype extensions: azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.operations.ExtensionsOperations + :param credential: Credential needed for the client to connect to Azure. + :type credential: ~azure.core.credentials.TokenCredential + :param subscription_id: The Azure subscription ID. This is a GUID-formatted string (e.g. 00000000-0000-0000-0000-000000000000). :type subscription_id: str :param str base_url: Service URL + :keyword int polling_interval: Default waiting time between two polls for LRO operations if no Retry-After header is present. """ def __init__( - self, credentials, subscription_id, base_url=None): - - self.config = SourceControlConfigurationClientConfiguration(credentials, subscription_id, base_url) - super(SourceControlConfigurationClient, self).__init__(self.config.credentials, self.config) + self, + credential, # type: "TokenCredential" + subscription_id, # type: str + base_url=None, # type: Optional[str] + **kwargs # type: Any + ): + # type: (...) -> None + if not base_url: + base_url = 'https://management.azure.com' + self._config = SourceControlConfigurationClientConfiguration(credential, subscription_id, **kwargs) + self._client = ARMPipelineClient(base_url=base_url, config=self._config, **kwargs) client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} - self.api_version = '2020-07-01-preview' self._serialize = Serializer(client_models) + self._serialize.client_side_validation = False self._deserialize = Deserializer(client_models) self.source_control_configurations = SourceControlConfigurationsOperations( - self._client, self.config, self._serialize, self._deserialize) + self._client, self._config, self._serialize, self._deserialize) self.operations = Operations( - self._client, self.config, self._serialize, self._deserialize) + self._client, self._config, self._serialize, self._deserialize) self.extensions = ExtensionsOperations( - self._client, self.config, self._serialize, self._deserialize) + self._client, self._config, self._serialize, self._deserialize) + + def _send_request(self, http_request, **kwargs): + # type: (HttpRequest, Any) -> HttpResponse + """Runs the network request through the client's chained policies. + + :param http_request: The network request you want to make. Required. + :type http_request: ~azure.core.pipeline.transport.HttpRequest + :keyword bool stream: Whether the response payload will be streamed. Defaults to True. + :return: The response of your network call. Does not do error handling on your response. + :rtype: ~azure.core.pipeline.transport.HttpResponse + """ + path_format_arguments = { + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), + } + http_request.url = self._client.format_url(http_request.url, **path_format_arguments) + stream = kwargs.pop("stream", True) + pipeline_response = self._client._pipeline.run(http_request, stream=stream, **kwargs) + return pipeline_response.http_response + + def close(self): + # type: () -> None + self._client.close() + + def __enter__(self): + # type: () -> SourceControlConfigurationClient + self._client.__enter__() + return self + + def __exit__(self, *exc_details): + # type: (Any) -> None + self._client.__exit__(*exc_details) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/version.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_version.py similarity index 84% rename from src/k8s-extension/azext_k8s_extension/vendored_sdks/version.py rename to src/k8s-extension/azext_k8s_extension/vendored_sdks/_version.py index 3e682bbd5fb..e5754a47ce6 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/version.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/_version.py @@ -1,13 +1,9 @@ # 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. -# +# 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. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -VERSION = "0.3.0" - +VERSION = "1.0.0b1" diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/__init__.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/__init__.py new file mode 100644 index 00000000000..ba52c91a7ba --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/__init__.py @@ -0,0 +1,10 @@ +# 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 ._source_control_configuration_client import SourceControlConfigurationClient +__all__ = ['SourceControlConfigurationClient'] diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/_configuration.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/_configuration.py new file mode 100644 index 00000000000..e15dc551cae --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/_configuration.py @@ -0,0 +1,67 @@ +# 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 Any, TYPE_CHECKING + +from azure.core.configuration import Configuration +from azure.core.pipeline import policies +from azure.mgmt.core.policies import ARMHttpLoggingPolicy + +from .._version import VERSION + +if TYPE_CHECKING: + # pylint: disable=unused-import,ungrouped-imports + from azure.core.credentials_async import AsyncTokenCredential + + +class SourceControlConfigurationClientConfiguration(Configuration): + """Configuration for SourceControlConfigurationClient. + + 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_async.AsyncTokenCredential + :param subscription_id: The Azure subscription ID. This is a GUID-formatted string (e.g. 00000000-0000-0000-0000-000000000000). + :type subscription_id: str + """ + + def __init__( + self, + credential: "AsyncTokenCredential", + subscription_id: str, + **kwargs: Any + ) -> None: + if credential is None: + raise ValueError("Parameter 'credential' must not be None.") + if subscription_id is None: + raise ValueError("Parameter 'subscription_id' must not be None.") + super(SourceControlConfigurationClientConfiguration, self).__init__(**kwargs) + + self.credential = credential + self.subscription_id = subscription_id + self.api_version = "2020-07-01-preview" + self.credential_scopes = kwargs.pop('credential_scopes', ['https://management.azure.com/.default']) + kwargs.setdefault('sdk_moniker', 'mgmt-kubernetesconfiguration/{}'.format(VERSION)) + self._configure(**kwargs) + + def _configure( + self, + **kwargs: Any + ) -> 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.AsyncRetryPolicy(**kwargs) + self.custom_hook_policy = kwargs.get('custom_hook_policy') or policies.CustomHookPolicy(**kwargs) + self.redirect_policy = kwargs.get('redirect_policy') or policies.AsyncRedirectPolicy(**kwargs) + self.authentication_policy = kwargs.get('authentication_policy') + if self.credential and not self.authentication_policy: + self.authentication_policy = policies.AsyncBearerTokenCredentialPolicy(self.credential, *self.credential_scopes, **kwargs) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/_source_control_configuration_client.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/_source_control_configuration_client.py new file mode 100644 index 00000000000..a03dc102973 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/_source_control_configuration_client.py @@ -0,0 +1,92 @@ +# 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 Any, Optional, TYPE_CHECKING + +from azure.core.pipeline.transport import AsyncHttpResponse, HttpRequest +from azure.mgmt.core import AsyncARMPipelineClient +from msrest import Deserializer, Serializer + +if TYPE_CHECKING: + # pylint: disable=unused-import,ungrouped-imports + from azure.core.credentials_async import AsyncTokenCredential + +from ._configuration import SourceControlConfigurationClientConfiguration +from .operations import SourceControlConfigurationsOperations +from .operations import Operations +from .operations import ExtensionsOperations +from .. import models + + +class SourceControlConfigurationClient(object): + """KubernetesConfiguration Client. + + :ivar source_control_configurations: SourceControlConfigurationsOperations operations + :vartype source_control_configurations: azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.aio.operations.SourceControlConfigurationsOperations + :ivar operations: Operations operations + :vartype operations: azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.aio.operations.Operations + :ivar extensions: ExtensionsOperations operations + :vartype extensions: azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.aio.operations.ExtensionsOperations + :param credential: Credential needed for the client to connect to Azure. + :type credential: ~azure.core.credentials_async.AsyncTokenCredential + :param subscription_id: The Azure subscription ID. This is a GUID-formatted string (e.g. 00000000-0000-0000-0000-000000000000). + :type subscription_id: str + :param str base_url: Service URL + :keyword int polling_interval: Default waiting time between two polls for LRO operations if no Retry-After header is present. + """ + + def __init__( + self, + credential: "AsyncTokenCredential", + subscription_id: str, + base_url: Optional[str] = None, + **kwargs: Any + ) -> None: + if not base_url: + base_url = 'https://management.azure.com' + self._config = SourceControlConfigurationClientConfiguration(credential, subscription_id, **kwargs) + self._client = AsyncARMPipelineClient(base_url=base_url, config=self._config, **kwargs) + + client_models = {k: v for k, v in models.__dict__.items() if isinstance(v, type)} + self._serialize = Serializer(client_models) + self._serialize.client_side_validation = False + self._deserialize = Deserializer(client_models) + + self.source_control_configurations = SourceControlConfigurationsOperations( + self._client, self._config, self._serialize, self._deserialize) + self.operations = Operations( + self._client, self._config, self._serialize, self._deserialize) + self.extensions = ExtensionsOperations( + self._client, self._config, self._serialize, self._deserialize) + + async def _send_request(self, http_request: HttpRequest, **kwargs: Any) -> AsyncHttpResponse: + """Runs the network request through the client's chained policies. + + :param http_request: The network request you want to make. Required. + :type http_request: ~azure.core.pipeline.transport.HttpRequest + :keyword bool stream: Whether the response payload will be streamed. Defaults to True. + :return: The response of your network call. Does not do error handling on your response. + :rtype: ~azure.core.pipeline.transport.AsyncHttpResponse + """ + path_format_arguments = { + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), + } + http_request.url = self._client.format_url(http_request.url, **path_format_arguments) + stream = kwargs.pop("stream", True) + pipeline_response = await self._client._pipeline.run(http_request, stream=stream, **kwargs) + return pipeline_response.http_response + + async def close(self) -> None: + await self._client.close() + + async def __aenter__(self) -> "SourceControlConfigurationClient": + await self._client.__aenter__() + return self + + async def __aexit__(self, *exc_details) -> None: + await self._client.__aexit__(*exc_details) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/operations/__init__.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/operations/__init__.py new file mode 100644 index 00000000000..2e68d5ecb0c --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/operations/__init__.py @@ -0,0 +1,17 @@ +# 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 ._source_control_configurations_operations import SourceControlConfigurationsOperations +from ._operations import Operations +from ._extensions_operations import ExtensionsOperations + +__all__ = [ + 'SourceControlConfigurationsOperations', + 'Operations', + 'ExtensionsOperations', +] diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/operations/_extensions_operations.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/operations/_extensions_operations.py new file mode 100644 index 00000000000..e92980035ad --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/operations/_extensions_operations.py @@ -0,0 +1,433 @@ +# 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 Any, AsyncIterable, Callable, Dict, Generic, Optional, TypeVar, Union +import warnings + +from azure.core.async_paging import AsyncItemPaged, AsyncList +from azure.core.exceptions import ClientAuthenticationError, HttpResponseError, ResourceExistsError, ResourceNotFoundError, map_error +from azure.core.pipeline import PipelineResponse +from azure.core.pipeline.transport import AsyncHttpResponse, HttpRequest +from azure.mgmt.core.exceptions import ARMErrorFormat + +from ... import models as _models + +T = TypeVar('T') +ClsType = Optional[Callable[[PipelineResponse[HttpRequest, AsyncHttpResponse], T, Dict[str, Any]], Any]] + +class ExtensionsOperations: + """ExtensionsOperations async operations. + + You should not instantiate this class directly. Instead, you should create a Client instance that + instantiates it for you and attaches it as an attribute. + + :ivar models: Alias to model classes used in this operation group. + :type models: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + """ + + models = _models + + def __init__(self, client, config, serializer, deserializer) -> None: + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self._config = config + + async def create( + self, + resource_group_name: str, + cluster_rp: Union[str, "_models.Enum0"], + cluster_resource_name: Union[str, "_models.Enum1"], + cluster_name: str, + extension_instance_name: str, + extension_instance: "_models.ExtensionInstance", + **kwargs: Any + ) -> "_models.ExtensionInstance": + """Create a new Kubernetes Cluster Extension Instance. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param extension_instance_name: Name of an instance of the Extension. + :type extension_instance_name: str + :param extension_instance: Properties necessary to Create an Extension Instance. + :type extension_instance: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ExtensionInstance + :keyword callable cls: A custom type or function that will be passed the direct response + :return: ExtensionInstance, or the result of cls(response) + :rtype: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ExtensionInstance + :raises: ~azure.core.exceptions.HttpResponseError + """ + cls = kwargs.pop('cls', None) # type: ClsType["_models.ExtensionInstance"] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + content_type = kwargs.pop("content_type", "application/json") + accept = "application/json" + + # Construct URL + url = self.create.metadata['url'] # type: ignore + path_format_arguments = { + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str'), + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} # type: Dict[str, Any] + header_parameters['Content-Type'] = self._serialize.header("content_type", content_type, 'str') + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') + + body_content_kwargs = {} # type: Dict[str, Any] + body_content = self._serialize.body(extension_instance, 'ExtensionInstance') + body_content_kwargs['content'] = body_content + request = self._client.put(url, query_parameters, header_parameters, **body_content_kwargs) + pipeline_response = await self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) + + deserialized = self._deserialize('ExtensionInstance', pipeline_response) + + if cls: + return cls(pipeline_response, deserialized, {}) + + return deserialized + create.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} # type: ignore + + async def get( + self, + resource_group_name: str, + cluster_rp: Union[str, "_models.Enum0"], + cluster_resource_name: Union[str, "_models.Enum1"], + cluster_name: str, + extension_instance_name: str, + **kwargs: Any + ) -> "_models.ExtensionInstance": + """Gets details of the Kubernetes Cluster Extension Instance. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param extension_instance_name: Name of an instance of the Extension. + :type extension_instance_name: str + :keyword callable cls: A custom type or function that will be passed the direct response + :return: ExtensionInstance, or the result of cls(response) + :rtype: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ExtensionInstance + :raises: ~azure.core.exceptions.HttpResponseError + """ + cls = kwargs.pop('cls', None) # type: ClsType["_models.ExtensionInstance"] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + accept = "application/json" + + # Construct URL + url = self.get.metadata['url'] # type: ignore + path_format_arguments = { + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str'), + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} # type: Dict[str, Any] + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') + + request = self._client.get(url, query_parameters, header_parameters) + pipeline_response = await self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) + + deserialized = self._deserialize('ExtensionInstance', pipeline_response) + + if cls: + return cls(pipeline_response, deserialized, {}) + + return deserialized + get.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} # type: ignore + + async def update( + self, + resource_group_name: str, + cluster_rp: Union[str, "_models.Enum0"], + cluster_resource_name: Union[str, "_models.Enum1"], + cluster_name: str, + extension_instance_name: str, + extension_instance: "_models.ExtensionInstanceUpdate", + **kwargs: Any + ) -> "_models.ExtensionInstance": + """Update an existing Kubernetes Cluster Extension Instance. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param extension_instance_name: Name of an instance of the Extension. + :type extension_instance_name: str + :param extension_instance: Properties to Update in the Extension Instance. + :type extension_instance: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ExtensionInstanceUpdate + :keyword callable cls: A custom type or function that will be passed the direct response + :return: ExtensionInstance, or the result of cls(response) + :rtype: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ExtensionInstance + :raises: ~azure.core.exceptions.HttpResponseError + """ + cls = kwargs.pop('cls', None) # type: ClsType["_models.ExtensionInstance"] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + content_type = kwargs.pop("content_type", "application/json") + accept = "application/json" + + # Construct URL + url = self.update.metadata['url'] # type: ignore + path_format_arguments = { + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str'), + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} # type: Dict[str, Any] + header_parameters['Content-Type'] = self._serialize.header("content_type", content_type, 'str') + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') + + body_content_kwargs = {} # type: Dict[str, Any] + body_content = self._serialize.body(extension_instance, 'ExtensionInstanceUpdate') + body_content_kwargs['content'] = body_content + request = self._client.patch(url, query_parameters, header_parameters, **body_content_kwargs) + pipeline_response = await self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) + + deserialized = self._deserialize('ExtensionInstance', pipeline_response) + + if cls: + return cls(pipeline_response, deserialized, {}) + + return deserialized + update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} # type: ignore + + async def delete( + self, + resource_group_name: str, + cluster_rp: Union[str, "_models.Enum0"], + cluster_resource_name: Union[str, "_models.Enum1"], + cluster_name: str, + extension_instance_name: str, + **kwargs: Any + ) -> None: + """Delete a Kubernetes Cluster Extension Instance. This will cause the Agent to Uninstall the + extension instance from the cluster. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param extension_instance_name: Name of an instance of the Extension. + :type extension_instance_name: str + :keyword callable cls: A custom type or function that will be passed the direct response + :return: None, or the result of cls(response) + :rtype: None + :raises: ~azure.core.exceptions.HttpResponseError + """ + cls = kwargs.pop('cls', None) # type: ClsType[None] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + accept = "application/json" + + # Construct URL + url = self.delete.metadata['url'] # type: ignore + path_format_arguments = { + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str'), + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} # type: Dict[str, Any] + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') + + request = self._client.delete(url, query_parameters, header_parameters) + pipeline_response = await self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response + + if response.status_code not in [200, 204]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) + + if cls: + return cls(pipeline_response, None, {}) + + delete.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} # type: ignore + + def list( + self, + resource_group_name: str, + cluster_rp: Union[str, "_models.Enum0"], + cluster_resource_name: Union[str, "_models.Enum1"], + cluster_name: str, + **kwargs: Any + ) -> AsyncIterable["_models.ExtensionInstancesList"]: + """List all Source Control Configurations. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :keyword callable cls: A custom type or function that will be passed the direct response + :return: An iterator like instance of either ExtensionInstancesList or the result of cls(response) + :rtype: ~azure.core.async_paging.AsyncItemPaged[~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ExtensionInstancesList] + :raises: ~azure.core.exceptions.HttpResponseError + """ + cls = kwargs.pop('cls', None) # type: ClsType["_models.ExtensionInstancesList"] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + accept = "application/json" + + def prepare_request(next_link=None): + # Construct headers + header_parameters = {} # type: Dict[str, Any] + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') + + if not next_link: + # Construct URL + url = self.list.metadata['url'] # type: ignore + path_format_arguments = { + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + } + url = self._client.format_url(url, **path_format_arguments) + # Construct parameters + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + request = self._client.get(url, query_parameters, header_parameters) + else: + url = next_link + query_parameters = {} # type: Dict[str, Any] + request = self._client.get(url, query_parameters, header_parameters) + return request + + async def extract_data(pipeline_response): + deserialized = self._deserialize('ExtensionInstancesList', pipeline_response) + list_of_elem = deserialized.value + if cls: + list_of_elem = cls(list_of_elem) + return deserialized.next_link or None, AsyncList(list_of_elem) + + async def get_next(next_link=None): + request = prepare_request(next_link) + + pipeline_response = await self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response + + if response.status_code not in [200]: + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + map_error(status_code=response.status_code, response=response, error_map=error_map) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) + + return pipeline_response + + return AsyncItemPaged( + get_next, extract_data + ) + list.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions'} # type: ignore diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/operations/_operations.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/operations/_operations.py new file mode 100644 index 00000000000..b9e2205f961 --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/operations/_operations.py @@ -0,0 +1,105 @@ +# 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 Any, AsyncIterable, Callable, Dict, Generic, Optional, TypeVar +import warnings + +from azure.core.async_paging import AsyncItemPaged, AsyncList +from azure.core.exceptions import ClientAuthenticationError, HttpResponseError, ResourceExistsError, ResourceNotFoundError, map_error +from azure.core.pipeline import PipelineResponse +from azure.core.pipeline.transport import AsyncHttpResponse, HttpRequest +from azure.mgmt.core.exceptions import ARMErrorFormat + +from ... import models as _models + +T = TypeVar('T') +ClsType = Optional[Callable[[PipelineResponse[HttpRequest, AsyncHttpResponse], T, Dict[str, Any]], Any]] + +class Operations: + """Operations async operations. + + You should not instantiate this class directly. Instead, you should create a Client instance that + instantiates it for you and attaches it as an attribute. + + :ivar models: Alias to model classes used in this operation group. + :type models: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + """ + + models = _models + + def __init__(self, client, config, serializer, deserializer) -> None: + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self._config = config + + def list( + self, + **kwargs: Any + ) -> AsyncIterable["_models.ResourceProviderOperationList"]: + """List all the available operations the KubernetesConfiguration resource provider supports. + + :keyword callable cls: A custom type or function that will be passed the direct response + :return: An iterator like instance of either ResourceProviderOperationList or the result of cls(response) + :rtype: ~azure.core.async_paging.AsyncItemPaged[~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ResourceProviderOperationList] + :raises: ~azure.core.exceptions.HttpResponseError + """ + cls = kwargs.pop('cls', None) # type: ClsType["_models.ResourceProviderOperationList"] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + accept = "application/json" + + def prepare_request(next_link=None): + # Construct headers + header_parameters = {} # type: Dict[str, Any] + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') + + if not next_link: + # Construct URL + url = self.list.metadata['url'] # type: ignore + # Construct parameters + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + request = self._client.get(url, query_parameters, header_parameters) + else: + url = next_link + query_parameters = {} # type: Dict[str, Any] + request = self._client.get(url, query_parameters, header_parameters) + return request + + async def extract_data(pipeline_response): + deserialized = self._deserialize('ResourceProviderOperationList', pipeline_response) + list_of_elem = deserialized.value + if cls: + list_of_elem = cls(list_of_elem) + return deserialized.next_link or None, AsyncList(list_of_elem) + + async def get_next(next_link=None): + request = prepare_request(next_link) + + pipeline_response = await self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response + + if response.status_code not in [200]: + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + map_error(status_code=response.status_code, response=response, error_map=error_map) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) + + return pipeline_response + + return AsyncItemPaged( + get_next, extract_data + ) + list.metadata = {'url': '/providers/Microsoft.KubernetesConfiguration/operations'} # type: ignore diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/operations/_source_control_configurations_operations.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/operations/_source_control_configurations_operations.py new file mode 100644 index 00000000000..116ecb3713e --- /dev/null +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/aio/operations/_source_control_configurations_operations.py @@ -0,0 +1,420 @@ +# 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 Any, AsyncIterable, Callable, Dict, Generic, Optional, TypeVar, Union +import warnings + +from azure.core.async_paging import AsyncItemPaged, AsyncList +from azure.core.exceptions import ClientAuthenticationError, HttpResponseError, ResourceExistsError, ResourceNotFoundError, map_error +from azure.core.pipeline import PipelineResponse +from azure.core.pipeline.transport import AsyncHttpResponse, HttpRequest +from azure.core.polling import AsyncLROPoller, AsyncNoPolling, AsyncPollingMethod +from azure.mgmt.core.exceptions import ARMErrorFormat +from azure.mgmt.core.polling.async_arm_polling import AsyncARMPolling + +from ... import models as _models + +T = TypeVar('T') +ClsType = Optional[Callable[[PipelineResponse[HttpRequest, AsyncHttpResponse], T, Dict[str, Any]], Any]] + +class SourceControlConfigurationsOperations: + """SourceControlConfigurationsOperations async operations. + + You should not instantiate this class directly. Instead, you should create a Client instance that + instantiates it for you and attaches it as an attribute. + + :ivar models: Alias to model classes used in this operation group. + :type models: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models + :param client: Client for service requests. + :param config: Configuration of service client. + :param serializer: An object model serializer. + :param deserializer: An object model deserializer. + """ + + models = _models + + def __init__(self, client, config, serializer, deserializer) -> None: + self._client = client + self._serialize = serializer + self._deserialize = deserializer + self._config = config + + async def get( + self, + resource_group_name: str, + cluster_rp: Union[str, "_models.Enum0"], + cluster_resource_name: Union[str, "_models.Enum1"], + cluster_name: str, + source_control_configuration_name: str, + **kwargs: Any + ) -> "_models.SourceControlConfiguration": + """Gets details of the Source Control Configuration. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param source_control_configuration_name: Name of the Source Control Configuration. + :type source_control_configuration_name: str + :keyword callable cls: A custom type or function that will be passed the direct response + :return: SourceControlConfiguration, or the result of cls(response) + :rtype: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SourceControlConfiguration + :raises: ~azure.core.exceptions.HttpResponseError + """ + cls = kwargs.pop('cls', None) # type: ClsType["_models.SourceControlConfiguration"] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + accept = "application/json" + + # Construct URL + url = self.get.metadata['url'] # type: ignore + path_format_arguments = { + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'sourceControlConfigurationName': self._serialize.url("source_control_configuration_name", source_control_configuration_name, 'str'), + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} # type: Dict[str, Any] + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') + + request = self._client.get(url, query_parameters, header_parameters) + pipeline_response = await self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) + + deserialized = self._deserialize('SourceControlConfiguration', pipeline_response) + + if cls: + return cls(pipeline_response, deserialized, {}) + + return deserialized + get.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations/{sourceControlConfigurationName}'} # type: ignore + + async def create_or_update( + self, + resource_group_name: str, + cluster_rp: Union[str, "_models.Enum0"], + cluster_resource_name: Union[str, "_models.Enum1"], + cluster_name: str, + source_control_configuration_name: str, + source_control_configuration: "_models.SourceControlConfiguration", + **kwargs: Any + ) -> "_models.SourceControlConfiguration": + """Create a new Kubernetes Source Control Configuration. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param source_control_configuration_name: Name of the Source Control Configuration. + :type source_control_configuration_name: str + :param source_control_configuration: Properties necessary to Create KubernetesConfiguration. + :type source_control_configuration: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SourceControlConfiguration + :keyword callable cls: A custom type or function that will be passed the direct response + :return: SourceControlConfiguration, or the result of cls(response) + :rtype: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SourceControlConfiguration + :raises: ~azure.core.exceptions.HttpResponseError + """ + cls = kwargs.pop('cls', None) # type: ClsType["_models.SourceControlConfiguration"] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + content_type = kwargs.pop("content_type", "application/json") + accept = "application/json" + + # Construct URL + url = self.create_or_update.metadata['url'] # type: ignore + path_format_arguments = { + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'sourceControlConfigurationName': self._serialize.url("source_control_configuration_name", source_control_configuration_name, 'str'), + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} # type: Dict[str, Any] + header_parameters['Content-Type'] = self._serialize.header("content_type", content_type, 'str') + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') + + body_content_kwargs = {} # type: Dict[str, Any] + body_content = self._serialize.body(source_control_configuration, 'SourceControlConfiguration') + body_content_kwargs['content'] = body_content + request = self._client.put(url, query_parameters, header_parameters, **body_content_kwargs) + pipeline_response = await self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response + + if response.status_code not in [200, 201]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) + + if response.status_code == 200: + deserialized = self._deserialize('SourceControlConfiguration', pipeline_response) + + if response.status_code == 201: + deserialized = self._deserialize('SourceControlConfiguration', pipeline_response) + + if cls: + return cls(pipeline_response, deserialized, {}) + + return deserialized + create_or_update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations/{sourceControlConfigurationName}'} # type: ignore + + async def _delete_initial( + self, + resource_group_name: str, + cluster_rp: Union[str, "_models.Enum0"], + cluster_resource_name: Union[str, "_models.Enum1"], + cluster_name: str, + source_control_configuration_name: str, + **kwargs: Any + ) -> None: + cls = kwargs.pop('cls', None) # type: ClsType[None] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + accept = "application/json" + + # Construct URL + url = self._delete_initial.metadata['url'] # type: ignore + path_format_arguments = { + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'sourceControlConfigurationName': self._serialize.url("source_control_configuration_name", source_control_configuration_name, 'str'), + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + # Construct headers + header_parameters = {} # type: Dict[str, Any] + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') + + request = self._client.delete(url, query_parameters, header_parameters) + pipeline_response = await self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response + + if response.status_code not in [200, 204]: + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) + + if cls: + return cls(pipeline_response, None, {}) + + _delete_initial.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations/{sourceControlConfigurationName}'} # type: ignore + + async def begin_delete( + self, + resource_group_name: str, + cluster_rp: Union[str, "_models.Enum0"], + cluster_resource_name: Union[str, "_models.Enum1"], + cluster_name: str, + source_control_configuration_name: str, + **kwargs: Any + ) -> AsyncLROPoller[None]: + """This will delete the YAML file used to set up the Source control configuration, thus stopping + future sync from the source repo. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :param source_control_configuration_name: Name of the Source Control Configuration. + :type source_control_configuration_name: str + :keyword callable cls: A custom type or function that will be passed the direct response + :keyword str continuation_token: A continuation token to restart a poller from a saved state. + :keyword polling: By default, your polling method will be AsyncARMPolling. + Pass in False for this operation to not poll, or pass in your own initialized polling object for a personal polling strategy. + :paramtype polling: bool or ~azure.core.polling.AsyncPollingMethod + :keyword int polling_interval: Default waiting time between two polls for LRO operations if no Retry-After header is present. + :return: An instance of AsyncLROPoller that returns either None or the result of cls(response) + :rtype: ~azure.core.polling.AsyncLROPoller[None] + :raises ~azure.core.exceptions.HttpResponseError: + """ + polling = kwargs.pop('polling', True) # type: Union[bool, AsyncPollingMethod] + cls = kwargs.pop('cls', None) # type: ClsType[None] + lro_delay = kwargs.pop( + 'polling_interval', + self._config.polling_interval + ) + cont_token = kwargs.pop('continuation_token', None) # type: Optional[str] + if cont_token is None: + raw_result = await self._delete_initial( + resource_group_name=resource_group_name, + cluster_rp=cluster_rp, + cluster_resource_name=cluster_resource_name, + cluster_name=cluster_name, + source_control_configuration_name=source_control_configuration_name, + cls=lambda x,y,z: x, + **kwargs + ) + + kwargs.pop('error_map', None) + kwargs.pop('content_type', None) + + def get_long_running_output(pipeline_response): + if cls: + return cls(pipeline_response, None, {}) + + path_format_arguments = { + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'sourceControlConfigurationName': self._serialize.url("source_control_configuration_name", source_control_configuration_name, 'str'), + } + + if polling is True: polling_method = AsyncARMPolling(lro_delay, path_format_arguments=path_format_arguments, **kwargs) + elif polling is False: polling_method = AsyncNoPolling() + else: polling_method = polling + if cont_token: + return AsyncLROPoller.from_continuation_token( + polling_method=polling_method, + continuation_token=cont_token, + client=self._client, + deserialization_callback=get_long_running_output + ) + else: + return AsyncLROPoller(self._client, raw_result, get_long_running_output, polling_method) + begin_delete.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations/{sourceControlConfigurationName}'} # type: ignore + + def list( + self, + resource_group_name: str, + cluster_rp: Union[str, "_models.Enum0"], + cluster_resource_name: Union[str, "_models.Enum1"], + cluster_name: str, + **kwargs: Any + ) -> AsyncIterable["_models.SourceControlConfigurationList"]: + """List all Source Control Configurations. + + :param resource_group_name: The name of the resource group. + :type resource_group_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 + :param cluster_name: The name of the kubernetes cluster. + :type cluster_name: str + :keyword callable cls: A custom type or function that will be passed the direct response + :return: An iterator like instance of either SourceControlConfigurationList or the result of cls(response) + :rtype: ~azure.core.async_paging.AsyncItemPaged[~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SourceControlConfigurationList] + :raises: ~azure.core.exceptions.HttpResponseError + """ + cls = kwargs.pop('cls', None) # type: ClsType["_models.SourceControlConfigurationList"] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + accept = "application/json" + + def prepare_request(next_link=None): + # Construct headers + header_parameters = {} # type: Dict[str, Any] + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') + + if not next_link: + # Construct URL + url = self.list.metadata['url'] # type: ignore + path_format_arguments = { + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + } + url = self._client.format_url(url, **path_format_arguments) + # Construct parameters + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + + request = self._client.get(url, query_parameters, header_parameters) + else: + url = next_link + query_parameters = {} # type: Dict[str, Any] + request = self._client.get(url, query_parameters, header_parameters) + return request + + async def extract_data(pipeline_response): + deserialized = self._deserialize('SourceControlConfigurationList', pipeline_response) + list_of_elem = deserialized.value + if cls: + list_of_elem = cls(list_of_elem) + return deserialized.next_link or None, AsyncList(list_of_elem) + + async def get_next(next_link=None): + request = prepare_request(next_link) + + pipeline_response = await self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response + + if response.status_code not in [200]: + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + map_error(status_code=response.status_code, response=response, error_map=error_map) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) + + return pipeline_response + + return AsyncItemPaged( + get_next, extract_data + ) + list.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations'} # type: ignore diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/__init__.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/__init__.py index e74cb56832b..5d3f8aea197 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/__init__.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/__init__.py @@ -1,63 +1,66 @@ # 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. -# +# 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. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- try: from ._models_py3 import ComplianceStatus from ._models_py3 import ConfigurationIdentity from ._models_py3 import ErrorDefinition - from ._models_py3 import ErrorResponse, ErrorResponseException + from ._models_py3 import ErrorResponse from ._models_py3 import ExtensionInstance from ._models_py3 import ExtensionInstanceUpdate + from ._models_py3 import ExtensionInstancesList from ._models_py3 import ExtensionStatus from ._models_py3 import HelmOperatorProperties from ._models_py3 import ProxyResource from ._models_py3 import Resource from ._models_py3 import ResourceProviderOperation from ._models_py3 import ResourceProviderOperationDisplay + from ._models_py3 import ResourceProviderOperationList from ._models_py3 import Result from ._models_py3 import Scope from ._models_py3 import ScopeCluster from ._models_py3 import ScopeNamespace from ._models_py3 import SourceControlConfiguration + from ._models_py3 import SourceControlConfigurationList from ._models_py3 import SystemData except (SyntaxError, ImportError): - from ._models import ComplianceStatus - from ._models import ConfigurationIdentity - from ._models import ErrorDefinition - from ._models import ErrorResponse, ErrorResponseException - from ._models import ExtensionInstance - from ._models import ExtensionInstanceUpdate - from ._models import ExtensionStatus - from ._models import HelmOperatorProperties - from ._models import ProxyResource - from ._models import Resource - from ._models import ResourceProviderOperation - from ._models import ResourceProviderOperationDisplay - from ._models import Result - from ._models import Scope - from ._models import ScopeCluster - from ._models import ScopeNamespace - from ._models import SourceControlConfiguration - from ._models import SystemData -from ._paged_models import ExtensionInstancePaged -from ._paged_models import ResourceProviderOperationPaged -from ._paged_models import SourceControlConfigurationPaged + from ._models import ComplianceStatus # type: ignore + from ._models import ConfigurationIdentity # type: ignore + from ._models import ErrorDefinition # type: ignore + from ._models import ErrorResponse # type: ignore + from ._models import ExtensionInstance # type: ignore + from ._models import ExtensionInstanceUpdate # type: ignore + from ._models import ExtensionInstancesList # type: ignore + from ._models import ExtensionStatus # type: ignore + from ._models import HelmOperatorProperties # type: ignore + from ._models import ProxyResource # type: ignore + from ._models import Resource # type: ignore + from ._models import ResourceProviderOperation # type: ignore + from ._models import ResourceProviderOperationDisplay # type: ignore + from ._models import ResourceProviderOperationList # type: ignore + from ._models import Result # type: ignore + from ._models import Scope # type: ignore + from ._models import ScopeCluster # type: ignore + from ._models import ScopeNamespace # type: ignore + from ._models import SourceControlConfiguration # type: ignore + from ._models import SourceControlConfigurationList # type: ignore + from ._models import SystemData # type: ignore + from ._source_control_configuration_client_enums import ( ComplianceStateType, + Enum0, + Enum1, + InstallStateType, + LevelType, MessageLevelType, - OperatorType, OperatorScopeType, + OperatorType, ProvisioningStateType, - InstallStateType, - LevelType, ResourceIdentityType, ) @@ -65,30 +68,32 @@ 'ComplianceStatus', 'ConfigurationIdentity', 'ErrorDefinition', - 'ErrorResponse', 'ErrorResponseException', + 'ErrorResponse', 'ExtensionInstance', 'ExtensionInstanceUpdate', + 'ExtensionInstancesList', 'ExtensionStatus', 'HelmOperatorProperties', 'ProxyResource', 'Resource', 'ResourceProviderOperation', 'ResourceProviderOperationDisplay', + 'ResourceProviderOperationList', 'Result', 'Scope', 'ScopeCluster', 'ScopeNamespace', 'SourceControlConfiguration', + 'SourceControlConfigurationList', 'SystemData', - 'SourceControlConfigurationPaged', - 'ResourceProviderOperationPaged', - 'ExtensionInstancePaged', 'ComplianceStateType', + 'Enum0', + 'Enum1', + 'InstallStateType', + 'LevelType', 'MessageLevelType', - 'OperatorType', 'OperatorScopeType', + 'OperatorType', 'ProvisioningStateType', - 'InstallStateType', - 'LevelType', 'ResourceIdentityType', ] diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py index f74ea5d809e..ef31b453ddc 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py @@ -1,45 +1,32 @@ # 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. -# +# 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. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -from msrest.serialization import Model -from msrest.exceptions import HttpOperationError +from azure.core.exceptions import HttpResponseError +import msrest.serialization -class CloudError(Model): - """CloudError. - """ - - _attribute_map = { - } - - -class ComplianceStatus(Model): +class ComplianceStatus(msrest.serialization.Model): """Compliance Status details. - Variables are only populated by the server, and will be ignored when - sending a request. + Variables are only populated by the server, and will be ignored when sending a request. - :ivar compliance_state: The compliance state of the configuration. - Possible values include: 'Pending', 'Compliant', 'Noncompliant', - 'Installed', 'Failed' + :ivar compliance_state: The compliance state of the configuration. Possible values include: + "Pending", "Compliant", "Noncompliant", "Installed", "Failed". :vartype compliance_state: str or - ~azure.mgmt.kubernetesconfiguration.models.ComplianceStateType + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ComplianceStateType :param last_config_applied: Datetime the configuration was last applied. - :type last_config_applied: datetime + :type last_config_applied: ~datetime.datetime :param message: Message from when the configuration was applied. :type message: str - :param message_level: Level of the message. Possible values include: - 'Error', 'Warning', 'Information' + :param message_level: Level of the message. Possible values include: "Error", "Warning", + "Information". :type message_level: str or - ~azure.mgmt.kubernetesconfiguration.models.MessageLevelType + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.MessageLevelType """ _validation = { @@ -53,7 +40,10 @@ class ComplianceStatus(Model): 'message_level': {'key': 'messageLevel', 'type': 'str'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(ComplianceStatus, self).__init__(**kwargs) self.compliance_state = None self.last_config_applied = kwargs.get('last_config_applied', None) @@ -61,24 +51,22 @@ def __init__(self, **kwargs): self.message_level = kwargs.get('message_level', None) -class ConfigurationIdentity(Model): +class ConfigurationIdentity(msrest.serialization.Model): """Identity for the managed cluster. - Variables are only populated by the server, and will be ignored when - sending a request. + Variables are only populated by the server, and will be ignored when sending a request. - :ivar principal_id: The principal id of the system assigned identity which - is used by the configuration. + :ivar principal_id: The principal id of the system assigned identity which is used by the + configuration. :vartype principal_id: str - :ivar tenant_id: The tenant id of the system assigned identity which is - used by the configuration. + :ivar tenant_id: The tenant id of the system assigned identity which is used by the + configuration. :vartype tenant_id: str - :param type: The type of identity used for the configuration. Type - 'SystemAssigned' will use an implicitly created identity. Type 'None' will - not use Managed Identity for the configuration. Possible values include: - 'SystemAssigned', 'None' + :param type: The type of identity used for the configuration. Type 'SystemAssigned' will use an + implicitly created identity. Type 'None' will not use Managed Identity for the configuration. + Possible values include: "SystemAssigned", "None". :type type: str or - ~azure.mgmt.kubernetesconfiguration.models.ResourceIdentityType + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ResourceIdentityType """ _validation = { @@ -89,23 +77,26 @@ class ConfigurationIdentity(Model): _attribute_map = { 'principal_id': {'key': 'principalId', 'type': 'str'}, 'tenant_id': {'key': 'tenantId', 'type': 'str'}, - 'type': {'key': 'type', 'type': 'ResourceIdentityType'}, + 'type': {'key': 'type', 'type': 'str'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(ConfigurationIdentity, self).__init__(**kwargs) self.principal_id = None self.tenant_id = None self.type = kwargs.get('type', None) -class ErrorDefinition(Model): +class ErrorDefinition(msrest.serialization.Model): """Error definition. All required parameters must be populated in order to send to Azure. - :param code: Required. Service specific error code which serves as the - substatus for the HTTP error code. + :param code: Required. Service specific error code which serves as the substatus for the HTTP + error code. :type code: str :param message: Required. Description of the error. :type message: str @@ -121,55 +112,54 @@ class ErrorDefinition(Model): 'message': {'key': 'message', 'type': 'str'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(ErrorDefinition, self).__init__(**kwargs) - self.code = kwargs.get('code', None) - self.message = kwargs.get('message', None) + self.code = kwargs['code'] + self.message = kwargs['message'] -class ErrorResponse(Model): +class ErrorResponse(msrest.serialization.Model): """Error response. - :param error: Error definition. - :type error: ~azure.mgmt.kubernetesconfiguration.models.ErrorDefinition + Variables are only populated by the server, and will be ignored when sending a request. + + :ivar error: Error definition. + :vartype error: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ErrorDefinition """ + _validation = { + 'error': {'readonly': True}, + } + _attribute_map = { 'error': {'key': 'error', 'type': 'ErrorDefinition'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(ErrorResponse, self).__init__(**kwargs) - self.error = kwargs.get('error', None) + self.error = None -class ErrorResponseException(HttpOperationError): - """Server responsed with exception of type: 'ErrorResponse'. - - :param deserialize: A deserializer - :param response: Server response to be deserialized. - """ - - def __init__(self, deserialize, response, *args): - - super(ErrorResponseException, self).__init__(deserialize, response, 'ErrorResponse', *args) - - -class Resource(Model): +class Resource(msrest.serialization.Model): """The Resource model definition. - Variables are only populated by the server, and will be ignored when - sending a request. + Variables are only populated by the server, and will be ignored when sending a request. - :ivar id: Resource Id + :ivar id: Resource Id. :vartype id: str - :ivar name: Resource name + :ivar name: Resource name. :vartype name: str - :ivar type: Resource type + :ivar type: Resource type. :vartype type: str :param system_data: Top level metadata - https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources - :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources. + :type system_data: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SystemData """ _validation = { @@ -185,7 +175,10 @@ class Resource(Model): 'system_data': {'key': 'systemData', 'type': 'SystemData'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(Resource, self).__init__(**kwargs) self.id = None self.name = None @@ -196,18 +189,17 @@ def __init__(self, **kwargs): class ProxyResource(Resource): """ARM proxy resource. - Variables are only populated by the server, and will be ignored when - sending a request. + Variables are only populated by the server, and will be ignored when sending a request. - :ivar id: Resource Id + :ivar id: Resource Id. :vartype id: str - :ivar name: Resource name + :ivar name: Resource name. :vartype name: str - :ivar type: Resource type + :ivar type: Resource type. :vartype type: str :param system_data: Top level metadata - https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources - :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources. + :type system_data: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SystemData """ _validation = { @@ -223,80 +215,77 @@ class ProxyResource(Resource): 'system_data': {'key': 'systemData', 'type': 'SystemData'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(ProxyResource, self).__init__(**kwargs) class ExtensionInstance(ProxyResource): """The Extension Instance object. - Variables are only populated by the server, and will be ignored when - sending a request. + Variables are only populated by the server, and will be ignored when sending a request. - :ivar id: Resource Id + :ivar id: Resource Id. :vartype id: str - :ivar name: Resource name + :ivar name: Resource name. :vartype name: str - :ivar type: Resource type + :ivar type: Resource type. :vartype type: str - :param location: Location of resource type - :type location: str :param system_data: Top level metadata - https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources - :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData - :param extension_type: Type of the Extension, of which this resource is an - instance of. It must be one of the Extension Types registered with - Microsoft.KubernetesConfiguration by the Extension publisher. + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources. + :type system_data: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SystemData + :param extension_type: Type of the Extension, of which this resource is an instance of. It + must be one of the Extension Types registered with Microsoft.KubernetesConfiguration by the + Extension publisher. :type extension_type: str - :param auto_upgrade_minor_version: Flag to note if this instance - participates in auto upgrade of minor version, or not. + :param auto_upgrade_minor_version: Flag to note if this instance participates in auto upgrade + of minor version, or not. :type auto_upgrade_minor_version: bool - :param release_train: ReleaseTrain this extension instance participates in - for auto-upgrade (e.g. Stable, Preview, etc.) - only if - autoUpgradeMinorVersion is 'true'. + :param release_train: ReleaseTrain this extension instance participates in for auto-upgrade + (e.g. Stable, Preview, etc.) - only if autoUpgradeMinorVersion is 'true'. :type release_train: str - :param version: Version of the extension for this extension instance, if - it is 'pinned' to a specific version. autoUpgradeMinorVersion must be - 'false'. + :param version: Version of the extension for this extension instance, if it is 'pinned' to a + specific version. autoUpgradeMinorVersion must be 'false'. :type version: str :param scope: Scope at which the extension instance is installed. - :type scope: ~azure.mgmt.kubernetesconfiguration.models.Scope - :param configuration_settings: Configuration settings, as name-value pairs - for configuring this instance of the extension. + :type scope: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Scope + :param configuration_settings: Configuration settings, as name-value pairs for configuring this + instance of the extension. :type configuration_settings: dict[str, str] - :param configuration_protected_settings: Configuration settings that are - sensitive, as name-value pairs for configuring this instance of the - extension. + :param configuration_protected_settings: Configuration settings that are sensitive, as + name-value pairs for configuring this instance of the extension. :type configuration_protected_settings: dict[str, str] - :param install_state: Status of installation of this instance of the - extension. Possible values include: 'Pending', 'Installed', 'Failed' - :type install_state: str or - ~azure.mgmt.kubernetesconfiguration.models.InstallStateType + :ivar install_state: Status of installation of this instance of the extension. Possible values + include: "Pending", "Installed", "Failed". + :vartype install_state: str or + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.InstallStateType :param statuses: Status from this instance of the extension. :type statuses: - list[~azure.mgmt.kubernetesconfiguration.models.ExtensionStatus] - :ivar creation_time: DateLiteral (per ISO8601) noting the time the - resource was created by the client (user). + list[~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ExtensionStatus] + :ivar creation_time: DateLiteral (per ISO8601) noting the time the resource was created by the + client (user). :vartype creation_time: str - :ivar last_modified_time: DateLiteral (per ISO8601) noting the time the - resource was modified by the client (user). + :ivar last_modified_time: DateLiteral (per ISO8601) noting the time the resource was modified + by the client (user). :vartype last_modified_time: str - :ivar last_status_time: DateLiteral (per ISO8601) noting the time of last - status from the agent. + :ivar last_status_time: DateLiteral (per ISO8601) noting the time of last status from the + agent. :vartype last_status_time: str - :ivar error_info: Error information from the Agent - e.g. errors during - installation. + :ivar error_info: Error information from the Agent - e.g. errors during installation. :vartype error_info: - ~azure.mgmt.kubernetesconfiguration.models.ErrorDefinition + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ErrorDefinition :param identity: The identity of the configuration. :type identity: - ~azure.mgmt.kubernetesconfiguration.models.ConfigurationIdentity + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ConfigurationIdentity """ _validation = { 'id': {'readonly': True}, 'name': {'readonly': True}, 'type': {'readonly': True}, + 'install_state': {'readonly': True}, 'creation_time': {'readonly': True}, 'last_modified_time': {'readonly': True}, 'last_status_time': {'readonly': True}, @@ -307,9 +296,7 @@ class ExtensionInstance(ProxyResource): 'id': {'key': 'id', 'type': 'str'}, 'name': {'key': 'name', 'type': 'str'}, 'type': {'key': 'type', 'type': 'str'}, - 'location': {'key': 'location', 'type': 'str'}, 'system_data': {'key': 'systemData', 'type': 'SystemData'}, - 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, @@ -323,19 +310,22 @@ class ExtensionInstance(ProxyResource): 'last_modified_time': {'key': 'properties.lastModifiedTime', 'type': 'str'}, 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'}, + 'identity': {'key': 'properties.identity', 'type': 'ConfigurationIdentity'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(ExtensionInstance, self).__init__(**kwargs) - self.location = kwargs.get('location', None) self.extension_type = kwargs.get('extension_type', None) self.auto_upgrade_minor_version = kwargs.get('auto_upgrade_minor_version', None) - self.release_train = kwargs.get('release_train', None) + self.release_train = kwargs.get('release_train', "Stable") self.version = kwargs.get('version', None) self.scope = kwargs.get('scope', None) self.configuration_settings = kwargs.get('configuration_settings', None) self.configuration_protected_settings = kwargs.get('configuration_protected_settings', None) - self.install_state = kwargs.get('install_state', None) + self.install_state = None self.statuses = kwargs.get('statuses', None) self.creation_time = None self.last_modified_time = None @@ -344,18 +334,48 @@ def __init__(self, **kwargs): self.identity = kwargs.get('identity', None) -class ExtensionInstanceUpdate(Model): +class ExtensionInstancesList(msrest.serialization.Model): + """Result of the request to list Extension Instances. It contains a list of ExtensionInstance objects and a URL link to get the next set of results. + + Variables are only populated by the server, and will be ignored when sending a request. + + :ivar value: List of Extension Instances within a Kubernetes cluster. + :vartype value: + list[~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ExtensionInstance] + :ivar next_link: URL to get the next set of extension instance objects, if any. + :vartype next_link: str + """ + + _validation = { + 'value': {'readonly': True}, + 'next_link': {'readonly': True}, + } + + _attribute_map = { + 'value': {'key': 'value', 'type': '[ExtensionInstance]'}, + 'next_link': {'key': 'nextLink', 'type': 'str'}, + } + + def __init__( + self, + **kwargs + ): + super(ExtensionInstancesList, self).__init__(**kwargs) + self.value = None + self.next_link = None + + +class ExtensionInstanceUpdate(msrest.serialization.Model): """Update Extension Instance request object. - :param auto_upgrade_minor_version: Flag to note if this instance - participates in Extension Lifecycle Management or not. + :param auto_upgrade_minor_version: Flag to note if this instance participates in Extension + Lifecycle Management or not. :type auto_upgrade_minor_version: bool - :param release_train: ReleaseTrain this extension instance participates in - for auto-upgrade (e.g. Stable, Preview, etc.) - only if - autoUpgradeMinorVersion is 'true'. + :param release_train: ReleaseTrain this extension instance participates in for auto-upgrade + (e.g. Stable, Preview, etc.) - only if autoUpgradeMinorVersion is 'true'. :type release_train: str - :param version: Version number of extension, to 'pin' to a specific - version. autoUpgradeMinorVersion must be 'false'. + :param version: Version number of extension, to 'pin' to a specific version. + autoUpgradeMinorVersion must be 'false'. :type version: str """ @@ -365,29 +385,29 @@ class ExtensionInstanceUpdate(Model): 'version': {'key': 'properties.version', 'type': 'str'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(ExtensionInstanceUpdate, self).__init__(**kwargs) self.auto_upgrade_minor_version = kwargs.get('auto_upgrade_minor_version', None) - self.release_train = kwargs.get('release_train', None) + self.release_train = kwargs.get('release_train', "Stable") self.version = kwargs.get('version', None) -class ExtensionStatus(Model): +class ExtensionStatus(msrest.serialization.Model): """Status from this instance of the extension. - :param code: Status code provided by the Extension + :param code: Status code provided by the Extension. :type code: str - :param display_status: Short description of status of this instance of the - extension. + :param display_status: Short description of status of this instance of the extension. :type display_status: str - :param level: Level of the status. Possible values include: 'Error', - 'Warning', 'Information'. Default value: "Information" . - :type level: str or ~azure.mgmt.kubernetesconfiguration.models.LevelType - :param message: Detailed message of the status from the Extension - instance. + :param level: Level of the status. Possible values include: "Error", "Warning", "Information". + Default value: "Information". + :type level: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.LevelType + :param message: Detailed message of the status from the Extension instance. :type message: str - :param time: DateLiteral (per ISO8601) noting the time of installation - status. + :param time: DateLiteral (per ISO8601) noting the time of installation status. :type time: str """ @@ -399,7 +419,10 @@ class ExtensionStatus(Model): 'time': {'key': 'time', 'type': 'str'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(ExtensionStatus, self).__init__(**kwargs) self.code = kwargs.get('code', None) self.display_status = kwargs.get('display_status', None) @@ -408,7 +431,7 @@ def __init__(self, **kwargs): self.time = kwargs.get('time', None) -class HelmOperatorProperties(Model): +class HelmOperatorProperties(msrest.serialization.Model): """Properties for Helm operator. :param chart_version: Version of the operator Helm chart. @@ -422,26 +445,26 @@ class HelmOperatorProperties(Model): 'chart_values': {'key': 'chartValues', 'type': 'str'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(HelmOperatorProperties, self).__init__(**kwargs) self.chart_version = kwargs.get('chart_version', None) self.chart_values = kwargs.get('chart_values', None) -class ResourceProviderOperation(Model): +class ResourceProviderOperation(msrest.serialization.Model): """Supported operation of this resource provider. - Variables are only populated by the server, and will be ignored when - sending a request. + Variables are only populated by the server, and will be ignored when sending a request. - :param name: Operation name, in format of - {provider}/{resource}/{operation} + :param name: Operation name, in format of {provider}/{resource}/{operation}. :type name: str :param display: Display metadata associated with the operation. :type display: - ~azure.mgmt.kubernetesconfiguration.models.ResourceProviderOperationDisplay - :ivar is_data_action: The flag that indicates whether the operation - applies to data plane. + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ResourceProviderOperationDisplay + :ivar is_data_action: The flag that indicates whether the operation applies to data plane. :vartype is_data_action: bool """ @@ -455,14 +478,17 @@ class ResourceProviderOperation(Model): 'is_data_action': {'key': 'isDataAction', 'type': 'bool'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(ResourceProviderOperation, self).__init__(**kwargs) self.name = kwargs.get('name', None) self.display = kwargs.get('display', None) self.is_data_action = None -class ResourceProviderOperationDisplay(Model): +class ResourceProviderOperationDisplay(msrest.serialization.Model): """Display metadata associated with the operation. :param provider: Resource provider: Microsoft KubernetesConfiguration. @@ -482,7 +508,10 @@ class ResourceProviderOperationDisplay(Model): 'description': {'key': 'description', 'type': 'str'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(ResourceProviderOperationDisplay, self).__init__(**kwargs) self.provider = kwargs.get('provider', None) self.resource = kwargs.get('resource', None) @@ -490,10 +519,40 @@ def __init__(self, **kwargs): self.description = kwargs.get('description', None) -class Result(Model): +class ResourceProviderOperationList(msrest.serialization.Model): + """Result of the request to list operations. + + Variables are only populated by the server, and will be ignored when sending a request. + + :param value: List of operations supported by this resource provider. + :type value: + list[~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ResourceProviderOperation] + :ivar next_link: URL to the next set of results, if any. + :vartype next_link: str + """ + + _validation = { + 'next_link': {'readonly': True}, + } + + _attribute_map = { + 'value': {'key': 'value', 'type': '[ResourceProviderOperation]'}, + 'next_link': {'key': 'nextLink', 'type': 'str'}, + } + + def __init__( + self, + **kwargs + ): + super(ResourceProviderOperationList, self).__init__(**kwargs) + self.value = kwargs.get('value', None) + self.next_link = None + + +class Result(msrest.serialization.Model): """Sample result definition. - :param sample_property: Sample property of type string + :param sample_property: Sample property of type string. :type sample_property: str """ @@ -501,21 +560,21 @@ class Result(Model): 'sample_property': {'key': 'sampleProperty', 'type': 'str'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(Result, self).__init__(**kwargs) self.sample_property = kwargs.get('sample_property', None) -class Scope(Model): - """Scope of the extensionInstance. It can be either Cluster or Namespace; but - not both. +class Scope(msrest.serialization.Model): + """Scope of the extensionInstance. It can be either Cluster or Namespace; but not both. - :param cluster: Specifies that the scope of the extensionInstance is - Cluster - :type cluster: ~azure.mgmt.kubernetesconfiguration.models.ScopeCluster - :param namespace: Specifies that the scope of the extensionInstance is - Namespace - :type namespace: ~azure.mgmt.kubernetesconfiguration.models.ScopeNamespace + :param cluster: Specifies that the scope of the extensionInstance is Cluster. + :type cluster: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ScopeCluster + :param namespace: Specifies that the scope of the extensionInstance is Namespace. + :type namespace: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ScopeNamespace """ _attribute_map = { @@ -523,18 +582,20 @@ class Scope(Model): 'namespace': {'key': 'namespace', 'type': 'ScopeNamespace'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(Scope, self).__init__(**kwargs) self.cluster = kwargs.get('cluster', None) self.namespace = kwargs.get('namespace', None) -class ScopeCluster(Model): +class ScopeCluster(msrest.serialization.Model): """Specifies that the scope of the extensionInstance is Cluster. - :param release_namespace: Namespace where the extension Release must be - placed, for a Cluster scoped extensionInstance. If this namespace does - not exist, it will be created + :param release_namespace: Namespace where the extension Release must be placed, for a Cluster + scoped extensionInstance. If this namespace does not exist, it will be created. :type release_namespace: str """ @@ -542,17 +603,19 @@ class ScopeCluster(Model): 'release_namespace': {'key': 'releaseNamespace', 'type': 'str'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(ScopeCluster, self).__init__(**kwargs) self.release_namespace = kwargs.get('release_namespace', None) -class ScopeNamespace(Model): +class ScopeNamespace(msrest.serialization.Model): """Specifies that the scope of the extensionInstance is Namespace. - :param target_namespace: Namespace where the extensionInstance will be - created for an Namespace scoped extensionInstance. If this namespace does - not exist, it will be created + :param target_namespace: Namespace where the extensionInstance will be created for an Namespace + scoped extensionInstance. If this namespace does not exist, it will be created. :type target_namespace: str """ @@ -560,7 +623,10 @@ class ScopeNamespace(Model): 'target_namespace': {'key': 'targetNamespace', 'type': 'str'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(ScopeNamespace, self).__init__(**kwargs) self.target_namespace = kwargs.get('target_namespace', None) @@ -568,63 +634,55 @@ def __init__(self, **kwargs): class SourceControlConfiguration(ProxyResource): """The SourceControl Configuration object returned in Get & Put response. - Variables are only populated by the server, and will be ignored when - sending a request. + Variables are only populated by the server, and will be ignored when sending a request. - :ivar id: Resource Id + :ivar id: Resource Id. :vartype id: str - :ivar name: Resource name + :ivar name: Resource name. :vartype name: str - :ivar type: Resource type + :ivar type: Resource type. :vartype type: str :param system_data: Top level metadata - https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources - :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources. + :type system_data: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SystemData :param repository_url: Url of the SourceControl Repository. :type repository_url: str - :param operator_namespace: The namespace to which this operator is - installed to. Maximum of 253 lower case alphanumeric characters, hyphen - and period only. Default value: "default" . + :param operator_namespace: The namespace to which this operator is installed to. Maximum of 253 + lower case alphanumeric characters, hyphen and period only. :type operator_namespace: str - :param operator_instance_name: Instance name of the operator - identifying - the specific configuration. + :param operator_instance_name: Instance name of the operator - identifying the specific + configuration. :type operator_instance_name: str - :param operator_type: Type of the operator. Possible values include: - 'Flux' + :param operator_type: Type of the operator. Possible values include: "Flux". :type operator_type: str or - ~azure.mgmt.kubernetesconfiguration.models.OperatorType - :param operator_params: Any Parameters for the Operator instance in string - format. + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.OperatorType + :param operator_params: Any Parameters for the Operator instance in string format. :type operator_params: str - :param configuration_protected_settings: Name-value pairs of protected - configuration settings for the configuration + :param configuration_protected_settings: Name-value pairs of protected configuration settings + for the configuration. :type configuration_protected_settings: dict[str, str] - :param operator_scope: Scope at which the operator will be installed. - Possible values include: 'cluster', 'namespace'. Default value: "cluster" - . + :param operator_scope: Scope at which the operator will be installed. Possible values include: + "cluster", "namespace". Default value: "cluster". :type operator_scope: str or - ~azure.mgmt.kubernetesconfiguration.models.OperatorScopeType - :ivar repository_public_key: Public Key associated with this SourceControl - configuration (either generated within the cluster or provided by the - user). + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.OperatorScopeType + :ivar repository_public_key: Public Key associated with this SourceControl configuration + (either generated within the cluster or provided by the user). :vartype repository_public_key: str - :param ssh_known_hosts_contents: Base64-encoded known_hosts contents - containing public SSH keys required to access private Git instances + :param ssh_known_hosts_contents: Base64-encoded known_hosts contents containing public SSH keys + required to access private Git instances. :type ssh_known_hosts_contents: str - :param enable_helm_operator: Option to enable Helm Operator for this git - configuration. + :param enable_helm_operator: Option to enable Helm Operator for this git configuration. :type enable_helm_operator: bool :param helm_operator_properties: Properties for Helm operator. :type helm_operator_properties: - ~azure.mgmt.kubernetesconfiguration.models.HelmOperatorProperties - :ivar provisioning_state: The provisioning state of the resource provider. - Possible values include: 'Accepted', 'Deleting', 'Running', 'Succeeded', - 'Failed' + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.HelmOperatorProperties + :ivar provisioning_state: The provisioning state of the resource provider. Possible values + include: "Accepted", "Deleting", "Running", "Succeeded", "Failed". :vartype provisioning_state: str or - ~azure.mgmt.kubernetesconfiguration.models.ProvisioningStateType - :ivar compliance_status: Compliance Status of the Configuration + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ProvisioningStateType + :ivar compliance_status: Compliance Status of the Configuration. :vartype compliance_status: - ~azure.mgmt.kubernetesconfiguration.models.ComplianceStatus + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ComplianceStatus """ _validation = { @@ -656,7 +714,10 @@ class SourceControlConfiguration(ProxyResource): 'compliance_status': {'key': 'properties.complianceStatus', 'type': 'ComplianceStatus'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(SourceControlConfiguration, self).__init__(**kwargs) self.repository_url = kwargs.get('repository_url', None) self.operator_namespace = kwargs.get('operator_namespace', "default") @@ -673,29 +734,56 @@ def __init__(self, **kwargs): self.compliance_status = None -class SystemData(Model): - """Top level metadata - https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources. +class SourceControlConfigurationList(msrest.serialization.Model): + """Result of the request to list Source Control Configurations. It contains a list of SourceControlConfiguration objects and a URL link to get the next set of results. + + Variables are only populated by the server, and will be ignored when sending a request. + + :ivar value: List of Source Control Configurations within a Kubernetes cluster. + :vartype value: + list[~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SourceControlConfiguration] + :ivar next_link: URL to get the next set of configuration objects, if any. + :vartype next_link: str + """ + + _validation = { + 'value': {'readonly': True}, + 'next_link': {'readonly': True}, + } + + _attribute_map = { + 'value': {'key': 'value', 'type': '[SourceControlConfiguration]'}, + 'next_link': {'key': 'nextLink', 'type': 'str'}, + } + + def __init__( + self, + **kwargs + ): + super(SourceControlConfigurationList, self).__init__(**kwargs) + self.value = None + self.next_link = None + + +class SystemData(msrest.serialization.Model): + """Top level metadata https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources. - Variables are only populated by the server, and will be ignored when - sending a request. + Variables are only populated by the server, and will be ignored when sending a request. - :ivar created_by: A string identifier for the identity that created the - resource + :ivar created_by: A string identifier for the identity that created the resource. :vartype created_by: str - :ivar created_by_type: The type of identity that created the resource: - user, application, managedIdentity, key + :ivar created_by_type: The type of identity that created the resource: user, application, + managedIdentity, key. :vartype created_by_type: str - :ivar created_at: The timestamp of resource creation (UTC) - :vartype created_at: datetime - :ivar last_modified_by: A string identifier for the identity that last - modified the resource + :ivar created_at: The timestamp of resource creation (UTC). + :vartype created_at: ~datetime.datetime + :ivar last_modified_by: A string identifier for the identity that last modified the resource. :vartype last_modified_by: str - :ivar last_modified_by_type: The type of identity that last modified the - resource: user, application, managedIdentity, key + :ivar last_modified_by_type: The type of identity that last modified the resource: user, + application, managedIdentity, key. :vartype last_modified_by_type: str - :ivar last_modified_at: The timestamp of resource last modification (UTC) - :vartype last_modified_at: datetime + :ivar last_modified_at: The timestamp of resource last modification (UTC). + :vartype last_modified_at: ~datetime.datetime """ _validation = { @@ -716,7 +804,10 @@ class SystemData(Model): 'last_modified_at': {'key': 'lastModifiedAt', 'type': 'iso-8601'}, } - def __init__(self, **kwargs): + def __init__( + self, + **kwargs + ): super(SystemData, self).__init__(**kwargs) self.created_by = None self.created_by_type = None diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py index 57f42c85edd..7d8c10c5306 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py @@ -1,45 +1,37 @@ # 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. -# +# 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. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -from msrest.serialization import Model -from msrest.exceptions import HttpOperationError +import datetime +from typing import Dict, List, Optional, Union +from azure.core.exceptions import HttpResponseError +import msrest.serialization -class CloudError(Model): - """CloudError. - """ - - _attribute_map = { - } +from ._source_control_configuration_client_enums import * -class ComplianceStatus(Model): +class ComplianceStatus(msrest.serialization.Model): """Compliance Status details. - Variables are only populated by the server, and will be ignored when - sending a request. + Variables are only populated by the server, and will be ignored when sending a request. - :ivar compliance_state: The compliance state of the configuration. - Possible values include: 'Pending', 'Compliant', 'Noncompliant', - 'Installed', 'Failed' + :ivar compliance_state: The compliance state of the configuration. Possible values include: + "Pending", "Compliant", "Noncompliant", "Installed", "Failed". :vartype compliance_state: str or - ~azure.mgmt.kubernetesconfiguration.models.ComplianceStateType + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ComplianceStateType :param last_config_applied: Datetime the configuration was last applied. - :type last_config_applied: datetime + :type last_config_applied: ~datetime.datetime :param message: Message from when the configuration was applied. :type message: str - :param message_level: Level of the message. Possible values include: - 'Error', 'Warning', 'Information' + :param message_level: Level of the message. Possible values include: "Error", "Warning", + "Information". :type message_level: str or - ~azure.mgmt.kubernetesconfiguration.models.MessageLevelType + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.MessageLevelType """ _validation = { @@ -53,7 +45,14 @@ class ComplianceStatus(Model): 'message_level': {'key': 'messageLevel', 'type': 'str'}, } - def __init__(self, *, last_config_applied=None, message: str=None, message_level=None, **kwargs) -> None: + def __init__( + self, + *, + last_config_applied: Optional[datetime.datetime] = None, + message: Optional[str] = None, + message_level: Optional[Union[str, "MessageLevelType"]] = None, + **kwargs + ): super(ComplianceStatus, self).__init__(**kwargs) self.compliance_state = None self.last_config_applied = last_config_applied @@ -61,24 +60,22 @@ def __init__(self, *, last_config_applied=None, message: str=None, message_level self.message_level = message_level -class ConfigurationIdentity(Model): +class ConfigurationIdentity(msrest.serialization.Model): """Identity for the managed cluster. - Variables are only populated by the server, and will be ignored when - sending a request. + Variables are only populated by the server, and will be ignored when sending a request. - :ivar principal_id: The principal id of the system assigned identity which - is used by the configuration. + :ivar principal_id: The principal id of the system assigned identity which is used by the + configuration. :vartype principal_id: str - :ivar tenant_id: The tenant id of the system assigned identity which is - used by the configuration. + :ivar tenant_id: The tenant id of the system assigned identity which is used by the + configuration. :vartype tenant_id: str - :param type: The type of identity used for the configuration. Type - 'SystemAssigned' will use an implicitly created identity. Type 'None' will - not use Managed Identity for the configuration. Possible values include: - 'SystemAssigned', 'None' + :param type: The type of identity used for the configuration. Type 'SystemAssigned' will use an + implicitly created identity. Type 'None' will not use Managed Identity for the configuration. + Possible values include: "SystemAssigned", "None". :type type: str or - ~azure.mgmt.kubernetesconfiguration.models.ResourceIdentityType + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ResourceIdentityType """ _validation = { @@ -89,23 +86,28 @@ class ConfigurationIdentity(Model): _attribute_map = { 'principal_id': {'key': 'principalId', 'type': 'str'}, 'tenant_id': {'key': 'tenantId', 'type': 'str'}, - 'type': {'key': 'type', 'type': 'ResourceIdentityType'}, + 'type': {'key': 'type', 'type': 'str'}, } - def __init__(self, *, type=None, **kwargs) -> None: + def __init__( + self, + *, + type: Optional[Union[str, "ResourceIdentityType"]] = None, + **kwargs + ): super(ConfigurationIdentity, self).__init__(**kwargs) self.principal_id = None self.tenant_id = None self.type = type -class ErrorDefinition(Model): +class ErrorDefinition(msrest.serialization.Model): """Error definition. All required parameters must be populated in order to send to Azure. - :param code: Required. Service specific error code which serves as the - substatus for the HTTP error code. + :param code: Required. Service specific error code which serves as the substatus for the HTTP + error code. :type code: str :param message: Required. Description of the error. :type message: str @@ -121,55 +123,57 @@ class ErrorDefinition(Model): 'message': {'key': 'message', 'type': 'str'}, } - def __init__(self, *, code: str, message: str, **kwargs) -> None: + def __init__( + self, + *, + code: str, + message: str, + **kwargs + ): super(ErrorDefinition, self).__init__(**kwargs) self.code = code self.message = message -class ErrorResponse(Model): +class ErrorResponse(msrest.serialization.Model): """Error response. - :param error: Error definition. - :type error: ~azure.mgmt.kubernetesconfiguration.models.ErrorDefinition + Variables are only populated by the server, and will be ignored when sending a request. + + :ivar error: Error definition. + :vartype error: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ErrorDefinition """ + _validation = { + 'error': {'readonly': True}, + } + _attribute_map = { 'error': {'key': 'error', 'type': 'ErrorDefinition'}, } - def __init__(self, *, error=None, **kwargs) -> None: + def __init__( + self, + **kwargs + ): super(ErrorResponse, self).__init__(**kwargs) - self.error = error - - -class ErrorResponseException(HttpOperationError): - """Server responsed with exception of type: 'ErrorResponse'. - - :param deserialize: A deserializer - :param response: Server response to be deserialized. - """ - - def __init__(self, deserialize, response, *args): + self.error = None - super(ErrorResponseException, self).__init__(deserialize, response, 'ErrorResponse', *args) - -class Resource(Model): +class Resource(msrest.serialization.Model): """The Resource model definition. - Variables are only populated by the server, and will be ignored when - sending a request. + Variables are only populated by the server, and will be ignored when sending a request. - :ivar id: Resource Id + :ivar id: Resource Id. :vartype id: str - :ivar name: Resource name + :ivar name: Resource name. :vartype name: str - :ivar type: Resource type + :ivar type: Resource type. :vartype type: str :param system_data: Top level metadata - https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources - :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources. + :type system_data: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SystemData """ _validation = { @@ -185,7 +189,12 @@ class Resource(Model): 'system_data': {'key': 'systemData', 'type': 'SystemData'}, } - def __init__(self, *, system_data=None, **kwargs) -> None: + def __init__( + self, + *, + system_data: Optional["SystemData"] = None, + **kwargs + ): super(Resource, self).__init__(**kwargs) self.id = None self.name = None @@ -196,18 +205,17 @@ def __init__(self, *, system_data=None, **kwargs) -> None: class ProxyResource(Resource): """ARM proxy resource. - Variables are only populated by the server, and will be ignored when - sending a request. + Variables are only populated by the server, and will be ignored when sending a request. - :ivar id: Resource Id + :ivar id: Resource Id. :vartype id: str - :ivar name: Resource name + :ivar name: Resource name. :vartype name: str - :ivar type: Resource type + :ivar type: Resource type. :vartype type: str :param system_data: Top level metadata - https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources - :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources. + :type system_data: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SystemData """ _validation = { @@ -223,80 +231,79 @@ class ProxyResource(Resource): 'system_data': {'key': 'systemData', 'type': 'SystemData'}, } - def __init__(self, *, system_data=None, **kwargs) -> None: + def __init__( + self, + *, + system_data: Optional["SystemData"] = None, + **kwargs + ): super(ProxyResource, self).__init__(system_data=system_data, **kwargs) class ExtensionInstance(ProxyResource): """The Extension Instance object. - Variables are only populated by the server, and will be ignored when - sending a request. + Variables are only populated by the server, and will be ignored when sending a request. - :ivar id: Resource Id + :ivar id: Resource Id. :vartype id: str - :ivar name: Resource name + :ivar name: Resource name. :vartype name: str - :ivar type: Resource type + :ivar type: Resource type. :vartype type: str - :param location: Location of resource type - :type location: str :param system_data: Top level metadata - https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources - :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData - :param extension_type: Type of the Extension, of which this resource is an - instance of. It must be one of the Extension Types registered with - Microsoft.KubernetesConfiguration by the Extension publisher. + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources. + :type system_data: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SystemData + :param extension_type: Type of the Extension, of which this resource is an instance of. It + must be one of the Extension Types registered with Microsoft.KubernetesConfiguration by the + Extension publisher. :type extension_type: str - :param auto_upgrade_minor_version: Flag to note if this instance - participates in auto upgrade of minor version, or not. + :param auto_upgrade_minor_version: Flag to note if this instance participates in auto upgrade + of minor version, or not. :type auto_upgrade_minor_version: bool - :param release_train: ReleaseTrain this extension instance participates in - for auto-upgrade (e.g. Stable, Preview, etc.) - only if - autoUpgradeMinorVersion is 'true'. + :param release_train: ReleaseTrain this extension instance participates in for auto-upgrade + (e.g. Stable, Preview, etc.) - only if autoUpgradeMinorVersion is 'true'. :type release_train: str - :param version: Version of the extension for this extension instance, if - it is 'pinned' to a specific version. autoUpgradeMinorVersion must be - 'false'. + :param version: Version of the extension for this extension instance, if it is 'pinned' to a + specific version. autoUpgradeMinorVersion must be 'false'. :type version: str :param scope: Scope at which the extension instance is installed. - :type scope: ~azure.mgmt.kubernetesconfiguration.models.Scope - :param configuration_settings: Configuration settings, as name-value pairs - for configuring this instance of the extension. + :type scope: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Scope + :param configuration_settings: Configuration settings, as name-value pairs for configuring this + instance of the extension. :type configuration_settings: dict[str, str] - :param configuration_protected_settings: Configuration settings that are - sensitive, as name-value pairs for configuring this instance of the - extension. + :param configuration_protected_settings: Configuration settings that are sensitive, as + name-value pairs for configuring this instance of the extension. :type configuration_protected_settings: dict[str, str] - :param install_state: Status of installation of this instance of the - extension. Possible values include: 'Pending', 'Installed', 'Failed' - :type install_state: str or - ~azure.mgmt.kubernetesconfiguration.models.InstallStateType + :ivar install_state: Status of installation of this instance of the extension. Possible values + include: "Pending", "Installed", "Failed". + :vartype install_state: str or + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.InstallStateType :param statuses: Status from this instance of the extension. :type statuses: - list[~azure.mgmt.kubernetesconfiguration.models.ExtensionStatus] - :ivar creation_time: DateLiteral (per ISO8601) noting the time the - resource was created by the client (user). + list[~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ExtensionStatus] + :ivar creation_time: DateLiteral (per ISO8601) noting the time the resource was created by the + client (user). :vartype creation_time: str - :ivar last_modified_time: DateLiteral (per ISO8601) noting the time the - resource was modified by the client (user). + :ivar last_modified_time: DateLiteral (per ISO8601) noting the time the resource was modified + by the client (user). :vartype last_modified_time: str - :ivar last_status_time: DateLiteral (per ISO8601) noting the time of last - status from the agent. + :ivar last_status_time: DateLiteral (per ISO8601) noting the time of last status from the + agent. :vartype last_status_time: str - :ivar error_info: Error information from the Agent - e.g. errors during - installation. + :ivar error_info: Error information from the Agent - e.g. errors during installation. :vartype error_info: - ~azure.mgmt.kubernetesconfiguration.models.ErrorDefinition + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ErrorDefinition :param identity: The identity of the configuration. :type identity: - ~azure.mgmt.kubernetesconfiguration.models.ConfigurationIdentity + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ConfigurationIdentity """ _validation = { 'id': {'readonly': True}, 'name': {'readonly': True}, 'type': {'readonly': True}, + 'install_state': {'readonly': True}, 'creation_time': {'readonly': True}, 'last_modified_time': {'readonly': True}, 'last_status_time': {'readonly': True}, @@ -307,9 +314,7 @@ class ExtensionInstance(ProxyResource): 'id': {'key': 'id', 'type': 'str'}, 'name': {'key': 'name', 'type': 'str'}, 'type': {'key': 'type', 'type': 'str'}, - 'location': {'key': 'location', 'type': 'str'}, 'system_data': {'key': 'systemData', 'type': 'SystemData'}, - 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, 'extension_type': {'key': 'properties.extensionType', 'type': 'str'}, 'auto_upgrade_minor_version': {'key': 'properties.autoUpgradeMinorVersion', 'type': 'bool'}, 'release_train': {'key': 'properties.releaseTrain', 'type': 'str'}, @@ -322,12 +327,26 @@ class ExtensionInstance(ProxyResource): 'creation_time': {'key': 'properties.creationTime', 'type': 'str'}, 'last_modified_time': {'key': 'properties.lastModifiedTime', 'type': 'str'}, 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, - 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'} + 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'}, + 'identity': {'key': 'properties.identity', 'type': 'ConfigurationIdentity'}, } - def __init__(self, *, system_data=None, location: str=None, extension_type: str=None, auto_upgrade_minor_version: bool=None, release_train: str=None, version: str=None, scope=None, configuration_settings=None, configuration_protected_settings=None, install_state=None, statuses=None, identity=None, **kwargs) -> None: + def __init__( + self, + *, + system_data: Optional["SystemData"] = None, + extension_type: Optional[str] = None, + auto_upgrade_minor_version: Optional[bool] = None, + release_train: Optional[str] = "Stable", + version: Optional[str] = None, + scope: Optional["Scope"] = None, + configuration_settings: Optional[Dict[str, str]] = None, + configuration_protected_settings: Optional[Dict[str, str]] = None, + statuses: Optional[List["ExtensionStatus"]] = None, + identity: Optional["ConfigurationIdentity"] = None, + **kwargs + ): super(ExtensionInstance, self).__init__(system_data=system_data, **kwargs) - self.location = location self.extension_type = extension_type self.auto_upgrade_minor_version = auto_upgrade_minor_version self.release_train = release_train @@ -335,7 +354,7 @@ def __init__(self, *, system_data=None, location: str=None, extension_type: str= self.scope = scope self.configuration_settings = configuration_settings self.configuration_protected_settings = configuration_protected_settings - self.install_state = install_state + self.install_state = None self.statuses = statuses self.creation_time = None self.last_modified_time = None @@ -344,18 +363,48 @@ def __init__(self, *, system_data=None, location: str=None, extension_type: str= self.identity = identity -class ExtensionInstanceUpdate(Model): +class ExtensionInstancesList(msrest.serialization.Model): + """Result of the request to list Extension Instances. It contains a list of ExtensionInstance objects and a URL link to get the next set of results. + + Variables are only populated by the server, and will be ignored when sending a request. + + :ivar value: List of Extension Instances within a Kubernetes cluster. + :vartype value: + list[~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ExtensionInstance] + :ivar next_link: URL to get the next set of extension instance objects, if any. + :vartype next_link: str + """ + + _validation = { + 'value': {'readonly': True}, + 'next_link': {'readonly': True}, + } + + _attribute_map = { + 'value': {'key': 'value', 'type': '[ExtensionInstance]'}, + 'next_link': {'key': 'nextLink', 'type': 'str'}, + } + + def __init__( + self, + **kwargs + ): + super(ExtensionInstancesList, self).__init__(**kwargs) + self.value = None + self.next_link = None + + +class ExtensionInstanceUpdate(msrest.serialization.Model): """Update Extension Instance request object. - :param auto_upgrade_minor_version: Flag to note if this instance - participates in Extension Lifecycle Management or not. + :param auto_upgrade_minor_version: Flag to note if this instance participates in Extension + Lifecycle Management or not. :type auto_upgrade_minor_version: bool - :param release_train: ReleaseTrain this extension instance participates in - for auto-upgrade (e.g. Stable, Preview, etc.) - only if - autoUpgradeMinorVersion is 'true'. + :param release_train: ReleaseTrain this extension instance participates in for auto-upgrade + (e.g. Stable, Preview, etc.) - only if autoUpgradeMinorVersion is 'true'. :type release_train: str - :param version: Version number of extension, to 'pin' to a specific - version. autoUpgradeMinorVersion must be 'false'. + :param version: Version number of extension, to 'pin' to a specific version. + autoUpgradeMinorVersion must be 'false'. :type version: str """ @@ -365,29 +414,33 @@ class ExtensionInstanceUpdate(Model): 'version': {'key': 'properties.version', 'type': 'str'}, } - def __init__(self, *, auto_upgrade_minor_version: bool=None, release_train: str=None, version: str=None, **kwargs) -> None: + def __init__( + self, + *, + auto_upgrade_minor_version: Optional[bool] = None, + release_train: Optional[str] = "Stable", + version: Optional[str] = None, + **kwargs + ): super(ExtensionInstanceUpdate, self).__init__(**kwargs) self.auto_upgrade_minor_version = auto_upgrade_minor_version self.release_train = release_train self.version = version -class ExtensionStatus(Model): +class ExtensionStatus(msrest.serialization.Model): """Status from this instance of the extension. - :param code: Status code provided by the Extension + :param code: Status code provided by the Extension. :type code: str - :param display_status: Short description of status of this instance of the - extension. + :param display_status: Short description of status of this instance of the extension. :type display_status: str - :param level: Level of the status. Possible values include: 'Error', - 'Warning', 'Information'. Default value: "Information" . - :type level: str or ~azure.mgmt.kubernetesconfiguration.models.LevelType - :param message: Detailed message of the status from the Extension - instance. + :param level: Level of the status. Possible values include: "Error", "Warning", "Information". + Default value: "Information". + :type level: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.LevelType + :param message: Detailed message of the status from the Extension instance. :type message: str - :param time: DateLiteral (per ISO8601) noting the time of installation - status. + :param time: DateLiteral (per ISO8601) noting the time of installation status. :type time: str """ @@ -399,7 +452,16 @@ class ExtensionStatus(Model): 'time': {'key': 'time', 'type': 'str'}, } - def __init__(self, *, code: str=None, display_status: str=None, level="Information", message: str=None, time: str=None, **kwargs) -> None: + def __init__( + self, + *, + code: Optional[str] = None, + display_status: Optional[str] = None, + level: Optional[Union[str, "LevelType"]] = "Information", + message: Optional[str] = None, + time: Optional[str] = None, + **kwargs + ): super(ExtensionStatus, self).__init__(**kwargs) self.code = code self.display_status = display_status @@ -408,7 +470,7 @@ def __init__(self, *, code: str=None, display_status: str=None, level="Informati self.time = time -class HelmOperatorProperties(Model): +class HelmOperatorProperties(msrest.serialization.Model): """Properties for Helm operator. :param chart_version: Version of the operator Helm chart. @@ -422,26 +484,29 @@ class HelmOperatorProperties(Model): 'chart_values': {'key': 'chartValues', 'type': 'str'}, } - def __init__(self, *, chart_version: str=None, chart_values: str=None, **kwargs) -> None: + def __init__( + self, + *, + chart_version: Optional[str] = None, + chart_values: Optional[str] = None, + **kwargs + ): super(HelmOperatorProperties, self).__init__(**kwargs) self.chart_version = chart_version self.chart_values = chart_values -class ResourceProviderOperation(Model): +class ResourceProviderOperation(msrest.serialization.Model): """Supported operation of this resource provider. - Variables are only populated by the server, and will be ignored when - sending a request. + Variables are only populated by the server, and will be ignored when sending a request. - :param name: Operation name, in format of - {provider}/{resource}/{operation} + :param name: Operation name, in format of {provider}/{resource}/{operation}. :type name: str :param display: Display metadata associated with the operation. :type display: - ~azure.mgmt.kubernetesconfiguration.models.ResourceProviderOperationDisplay - :ivar is_data_action: The flag that indicates whether the operation - applies to data plane. + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ResourceProviderOperationDisplay + :ivar is_data_action: The flag that indicates whether the operation applies to data plane. :vartype is_data_action: bool """ @@ -455,14 +520,20 @@ class ResourceProviderOperation(Model): 'is_data_action': {'key': 'isDataAction', 'type': 'bool'}, } - def __init__(self, *, name: str=None, display=None, **kwargs) -> None: + def __init__( + self, + *, + name: Optional[str] = None, + display: Optional["ResourceProviderOperationDisplay"] = None, + **kwargs + ): super(ResourceProviderOperation, self).__init__(**kwargs) self.name = name self.display = display self.is_data_action = None -class ResourceProviderOperationDisplay(Model): +class ResourceProviderOperationDisplay(msrest.serialization.Model): """Display metadata associated with the operation. :param provider: Resource provider: Microsoft KubernetesConfiguration. @@ -482,7 +553,15 @@ class ResourceProviderOperationDisplay(Model): 'description': {'key': 'description', 'type': 'str'}, } - def __init__(self, *, provider: str=None, resource: str=None, operation: str=None, description: str=None, **kwargs) -> None: + def __init__( + self, + *, + provider: Optional[str] = None, + resource: Optional[str] = None, + operation: Optional[str] = None, + description: Optional[str] = None, + **kwargs + ): super(ResourceProviderOperationDisplay, self).__init__(**kwargs) self.provider = provider self.resource = resource @@ -490,10 +569,42 @@ def __init__(self, *, provider: str=None, resource: str=None, operation: str=Non self.description = description -class Result(Model): +class ResourceProviderOperationList(msrest.serialization.Model): + """Result of the request to list operations. + + Variables are only populated by the server, and will be ignored when sending a request. + + :param value: List of operations supported by this resource provider. + :type value: + list[~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ResourceProviderOperation] + :ivar next_link: URL to the next set of results, if any. + :vartype next_link: str + """ + + _validation = { + 'next_link': {'readonly': True}, + } + + _attribute_map = { + 'value': {'key': 'value', 'type': '[ResourceProviderOperation]'}, + 'next_link': {'key': 'nextLink', 'type': 'str'}, + } + + def __init__( + self, + *, + value: Optional[List["ResourceProviderOperation"]] = None, + **kwargs + ): + super(ResourceProviderOperationList, self).__init__(**kwargs) + self.value = value + self.next_link = None + + +class Result(msrest.serialization.Model): """Sample result definition. - :param sample_property: Sample property of type string + :param sample_property: Sample property of type string. :type sample_property: str """ @@ -501,21 +612,23 @@ class Result(Model): 'sample_property': {'key': 'sampleProperty', 'type': 'str'}, } - def __init__(self, *, sample_property: str=None, **kwargs) -> None: + def __init__( + self, + *, + sample_property: Optional[str] = None, + **kwargs + ): super(Result, self).__init__(**kwargs) self.sample_property = sample_property -class Scope(Model): - """Scope of the extensionInstance. It can be either Cluster or Namespace; but - not both. +class Scope(msrest.serialization.Model): + """Scope of the extensionInstance. It can be either Cluster or Namespace; but not both. - :param cluster: Specifies that the scope of the extensionInstance is - Cluster - :type cluster: ~azure.mgmt.kubernetesconfiguration.models.ScopeCluster - :param namespace: Specifies that the scope of the extensionInstance is - Namespace - :type namespace: ~azure.mgmt.kubernetesconfiguration.models.ScopeNamespace + :param cluster: Specifies that the scope of the extensionInstance is Cluster. + :type cluster: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ScopeCluster + :param namespace: Specifies that the scope of the extensionInstance is Namespace. + :type namespace: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ScopeNamespace """ _attribute_map = { @@ -523,18 +636,23 @@ class Scope(Model): 'namespace': {'key': 'namespace', 'type': 'ScopeNamespace'}, } - def __init__(self, *, cluster=None, namespace=None, **kwargs) -> None: + def __init__( + self, + *, + cluster: Optional["ScopeCluster"] = None, + namespace: Optional["ScopeNamespace"] = None, + **kwargs + ): super(Scope, self).__init__(**kwargs) self.cluster = cluster self.namespace = namespace -class ScopeCluster(Model): +class ScopeCluster(msrest.serialization.Model): """Specifies that the scope of the extensionInstance is Cluster. - :param release_namespace: Namespace where the extension Release must be - placed, for a Cluster scoped extensionInstance. If this namespace does - not exist, it will be created + :param release_namespace: Namespace where the extension Release must be placed, for a Cluster + scoped extensionInstance. If this namespace does not exist, it will be created. :type release_namespace: str """ @@ -542,17 +660,21 @@ class ScopeCluster(Model): 'release_namespace': {'key': 'releaseNamespace', 'type': 'str'}, } - def __init__(self, *, release_namespace: str=None, **kwargs) -> None: + def __init__( + self, + *, + release_namespace: Optional[str] = None, + **kwargs + ): super(ScopeCluster, self).__init__(**kwargs) self.release_namespace = release_namespace -class ScopeNamespace(Model): +class ScopeNamespace(msrest.serialization.Model): """Specifies that the scope of the extensionInstance is Namespace. - :param target_namespace: Namespace where the extensionInstance will be - created for an Namespace scoped extensionInstance. If this namespace does - not exist, it will be created + :param target_namespace: Namespace where the extensionInstance will be created for an Namespace + scoped extensionInstance. If this namespace does not exist, it will be created. :type target_namespace: str """ @@ -560,7 +682,12 @@ class ScopeNamespace(Model): 'target_namespace': {'key': 'targetNamespace', 'type': 'str'}, } - def __init__(self, *, target_namespace: str=None, **kwargs) -> None: + def __init__( + self, + *, + target_namespace: Optional[str] = None, + **kwargs + ): super(ScopeNamespace, self).__init__(**kwargs) self.target_namespace = target_namespace @@ -568,63 +695,55 @@ def __init__(self, *, target_namespace: str=None, **kwargs) -> None: class SourceControlConfiguration(ProxyResource): """The SourceControl Configuration object returned in Get & Put response. - Variables are only populated by the server, and will be ignored when - sending a request. + Variables are only populated by the server, and will be ignored when sending a request. - :ivar id: Resource Id + :ivar id: Resource Id. :vartype id: str - :ivar name: Resource name + :ivar name: Resource name. :vartype name: str - :ivar type: Resource type + :ivar type: Resource type. :vartype type: str :param system_data: Top level metadata - https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources - :type system_data: ~azure.mgmt.kubernetesconfiguration.models.SystemData + https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources. + :type system_data: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SystemData :param repository_url: Url of the SourceControl Repository. :type repository_url: str - :param operator_namespace: The namespace to which this operator is - installed to. Maximum of 253 lower case alphanumeric characters, hyphen - and period only. Default value: "default" . + :param operator_namespace: The namespace to which this operator is installed to. Maximum of 253 + lower case alphanumeric characters, hyphen and period only. :type operator_namespace: str - :param operator_instance_name: Instance name of the operator - identifying - the specific configuration. + :param operator_instance_name: Instance name of the operator - identifying the specific + configuration. :type operator_instance_name: str - :param operator_type: Type of the operator. Possible values include: - 'Flux' + :param operator_type: Type of the operator. Possible values include: "Flux". :type operator_type: str or - ~azure.mgmt.kubernetesconfiguration.models.OperatorType - :param operator_params: Any Parameters for the Operator instance in string - format. + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.OperatorType + :param operator_params: Any Parameters for the Operator instance in string format. :type operator_params: str - :param configuration_protected_settings: Name-value pairs of protected - configuration settings for the configuration + :param configuration_protected_settings: Name-value pairs of protected configuration settings + for the configuration. :type configuration_protected_settings: dict[str, str] - :param operator_scope: Scope at which the operator will be installed. - Possible values include: 'cluster', 'namespace'. Default value: "cluster" - . + :param operator_scope: Scope at which the operator will be installed. Possible values include: + "cluster", "namespace". Default value: "cluster". :type operator_scope: str or - ~azure.mgmt.kubernetesconfiguration.models.OperatorScopeType - :ivar repository_public_key: Public Key associated with this SourceControl - configuration (either generated within the cluster or provided by the - user). + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.OperatorScopeType + :ivar repository_public_key: Public Key associated with this SourceControl configuration + (either generated within the cluster or provided by the user). :vartype repository_public_key: str - :param ssh_known_hosts_contents: Base64-encoded known_hosts contents - containing public SSH keys required to access private Git instances + :param ssh_known_hosts_contents: Base64-encoded known_hosts contents containing public SSH keys + required to access private Git instances. :type ssh_known_hosts_contents: str - :param enable_helm_operator: Option to enable Helm Operator for this git - configuration. + :param enable_helm_operator: Option to enable Helm Operator for this git configuration. :type enable_helm_operator: bool :param helm_operator_properties: Properties for Helm operator. :type helm_operator_properties: - ~azure.mgmt.kubernetesconfiguration.models.HelmOperatorProperties - :ivar provisioning_state: The provisioning state of the resource provider. - Possible values include: 'Accepted', 'Deleting', 'Running', 'Succeeded', - 'Failed' + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.HelmOperatorProperties + :ivar provisioning_state: The provisioning state of the resource provider. Possible values + include: "Accepted", "Deleting", "Running", "Succeeded", "Failed". :vartype provisioning_state: str or - ~azure.mgmt.kubernetesconfiguration.models.ProvisioningStateType - :ivar compliance_status: Compliance Status of the Configuration + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ProvisioningStateType + :ivar compliance_status: Compliance Status of the Configuration. :vartype compliance_status: - ~azure.mgmt.kubernetesconfiguration.models.ComplianceStatus + ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ComplianceStatus """ _validation = { @@ -656,7 +775,22 @@ class SourceControlConfiguration(ProxyResource): 'compliance_status': {'key': 'properties.complianceStatus', 'type': 'ComplianceStatus'}, } - def __init__(self, *, system_data=None, repository_url: str=None, operator_namespace: str="default", operator_instance_name: str=None, operator_type=None, operator_params: str=None, configuration_protected_settings=None, operator_scope="cluster", ssh_known_hosts_contents: str=None, enable_helm_operator: bool=None, helm_operator_properties=None, **kwargs) -> None: + def __init__( + self, + *, + system_data: Optional["SystemData"] = None, + repository_url: Optional[str] = None, + operator_namespace: Optional[str] = "default", + operator_instance_name: Optional[str] = None, + operator_type: Optional[Union[str, "OperatorType"]] = None, + operator_params: Optional[str] = None, + configuration_protected_settings: Optional[Dict[str, str]] = None, + operator_scope: Optional[Union[str, "OperatorScopeType"]] = "cluster", + ssh_known_hosts_contents: Optional[str] = None, + enable_helm_operator: Optional[bool] = None, + helm_operator_properties: Optional["HelmOperatorProperties"] = None, + **kwargs + ): super(SourceControlConfiguration, self).__init__(system_data=system_data, **kwargs) self.repository_url = repository_url self.operator_namespace = operator_namespace @@ -673,29 +807,56 @@ def __init__(self, *, system_data=None, repository_url: str=None, operator_names self.compliance_status = None -class SystemData(Model): - """Top level metadata - https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources. +class SourceControlConfigurationList(msrest.serialization.Model): + """Result of the request to list Source Control Configurations. It contains a list of SourceControlConfiguration objects and a URL link to get the next set of results. + + Variables are only populated by the server, and will be ignored when sending a request. + + :ivar value: List of Source Control Configurations within a Kubernetes cluster. + :vartype value: + list[~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SourceControlConfiguration] + :ivar next_link: URL to get the next set of configuration objects, if any. + :vartype next_link: str + """ + + _validation = { + 'value': {'readonly': True}, + 'next_link': {'readonly': True}, + } + + _attribute_map = { + 'value': {'key': 'value', 'type': '[SourceControlConfiguration]'}, + 'next_link': {'key': 'nextLink', 'type': 'str'}, + } + + def __init__( + self, + **kwargs + ): + super(SourceControlConfigurationList, self).__init__(**kwargs) + self.value = None + self.next_link = None + + +class SystemData(msrest.serialization.Model): + """Top level metadata https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-contracts.md#system-metadata-for-all-azure-resources. - Variables are only populated by the server, and will be ignored when - sending a request. + Variables are only populated by the server, and will be ignored when sending a request. - :ivar created_by: A string identifier for the identity that created the - resource + :ivar created_by: A string identifier for the identity that created the resource. :vartype created_by: str - :ivar created_by_type: The type of identity that created the resource: - user, application, managedIdentity, key + :ivar created_by_type: The type of identity that created the resource: user, application, + managedIdentity, key. :vartype created_by_type: str - :ivar created_at: The timestamp of resource creation (UTC) - :vartype created_at: datetime - :ivar last_modified_by: A string identifier for the identity that last - modified the resource + :ivar created_at: The timestamp of resource creation (UTC). + :vartype created_at: ~datetime.datetime + :ivar last_modified_by: A string identifier for the identity that last modified the resource. :vartype last_modified_by: str - :ivar last_modified_by_type: The type of identity that last modified the - resource: user, application, managedIdentity, key + :ivar last_modified_by_type: The type of identity that last modified the resource: user, + application, managedIdentity, key. :vartype last_modified_by_type: str - :ivar last_modified_at: The timestamp of resource last modification (UTC) - :vartype last_modified_at: datetime + :ivar last_modified_at: The timestamp of resource last modification (UTC). + :vartype last_modified_at: ~datetime.datetime """ _validation = { @@ -716,7 +877,10 @@ class SystemData(Model): 'last_modified_at': {'key': 'lastModifiedAt', 'type': 'iso-8601'}, } - def __init__(self, **kwargs) -> None: + def __init__( + self, + **kwargs + ): super(SystemData, self).__init__(**kwargs) self.created_by = None self.created_by_type = None diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_paged_models.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_paged_models.py deleted file mode 100644 index c545286fe54..00000000000 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_paged_models.py +++ /dev/null @@ -1,53 +0,0 @@ -# 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 msrest.paging import Paged - - -class SourceControlConfigurationPaged(Paged): - """ - A paging container for iterating over a list of :class:`SourceControlConfiguration ` object - """ - - _attribute_map = { - 'next_link': {'key': 'nextLink', 'type': 'str'}, - 'current_page': {'key': 'value', 'type': '[SourceControlConfiguration]'} - } - - def __init__(self, *args, **kwargs): - - super(SourceControlConfigurationPaged, self).__init__(*args, **kwargs) -class ResourceProviderOperationPaged(Paged): - """ - A paging container for iterating over a list of :class:`ResourceProviderOperation ` object - """ - - _attribute_map = { - 'next_link': {'key': 'nextLink', 'type': 'str'}, - 'current_page': {'key': 'value', 'type': '[ResourceProviderOperation]'} - } - - def __init__(self, *args, **kwargs): - - super(ResourceProviderOperationPaged, self).__init__(*args, **kwargs) -class ExtensionInstancePaged(Paged): - """ - A paging container for iterating over a list of :class:`ExtensionInstance ` object - """ - - _attribute_map = { - 'next_link': {'key': 'nextLink', 'type': 'str'}, - 'current_page': {'key': 'value', 'type': '[ExtensionInstance]'} - } - - def __init__(self, *args, **kwargs): - - super(ExtensionInstancePaged, self).__init__(*args, **kwargs) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_source_control_configuration_client_enums.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_source_control_configuration_client_enums.py index 7be14a4b085..825cfccadd1 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_source_control_configuration_client_enums.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_source_control_configuration_client_enums.py @@ -1,68 +1,102 @@ # 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. -# +# 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. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -from enum import Enum +from enum import Enum, EnumMeta +from six import with_metaclass +class _CaseInsensitiveEnumMeta(EnumMeta): + def __getitem__(self, name): + return super().__getitem__(name.upper()) -class ComplianceStateType(str, Enum): + def __getattr__(cls, name): + """Return the enum member matching `name` + We use __getattr__ instead of descriptors or inserting into the enum + class' __dict__ in order to support `name` and `value` being both + properties for enum members (which live in the class' __dict__) and + enum members themselves. + """ + try: + return cls._member_map_[name.upper()] + except KeyError: + raise AttributeError(name) - pending = "Pending" - compliant = "Compliant" - noncompliant = "Noncompliant" - installed = "Installed" - failed = "Failed" +class ComplianceStateType(with_metaclass(_CaseInsensitiveEnumMeta, str, Enum)): + """The compliance state of the configuration. + """ -class MessageLevelType(str, Enum): + PENDING = "Pending" + COMPLIANT = "Compliant" + NONCOMPLIANT = "Noncompliant" + INSTALLED = "Installed" + FAILED = "Failed" - error = "Error" - warning = "Warning" - information = "Information" +class Enum0(with_metaclass(_CaseInsensitiveEnumMeta, str, Enum)): + MICROSOFT_CONTAINER_SERVICE = "Microsoft.ContainerService" + MICROSOFT_KUBERNETES = "Microsoft.Kubernetes" -class OperatorType(str, Enum): +class Enum1(with_metaclass(_CaseInsensitiveEnumMeta, str, Enum)): - flux = "Flux" + MANAGED_CLUSTERS = "managedClusters" + CONNECTED_CLUSTERS = "connectedClusters" +class InstallStateType(with_metaclass(_CaseInsensitiveEnumMeta, str, Enum)): + """Status of installation of this instance of the extension. + """ -class OperatorScopeType(str, Enum): + PENDING = "Pending" + INSTALLED = "Installed" + FAILED = "Failed" - cluster = "cluster" - namespace = "namespace" +class LevelType(with_metaclass(_CaseInsensitiveEnumMeta, str, Enum)): + """Level of the status. + """ + ERROR = "Error" + WARNING = "Warning" + INFORMATION = "Information" -class ProvisioningStateType(str, Enum): +class MessageLevelType(with_metaclass(_CaseInsensitiveEnumMeta, str, Enum)): + """Level of the message. + """ - accepted = "Accepted" - deleting = "Deleting" - running = "Running" - succeeded = "Succeeded" - failed = "Failed" + ERROR = "Error" + WARNING = "Warning" + INFORMATION = "Information" +class OperatorScopeType(with_metaclass(_CaseInsensitiveEnumMeta, str, Enum)): + """Scope at which the operator will be installed. + """ -class InstallStateType(str, Enum): + CLUSTER = "cluster" + NAMESPACE = "namespace" - pending = "Pending" - installed = "Installed" - failed = "Failed" +class OperatorType(with_metaclass(_CaseInsensitiveEnumMeta, str, Enum)): + """Type of the operator + """ + FLUX = "Flux" -class LevelType(str, Enum): +class ProvisioningStateType(with_metaclass(_CaseInsensitiveEnumMeta, str, Enum)): + """The provisioning state of the resource provider. + """ - error = "Error" - warning = "Warning" - information = "Information" + ACCEPTED = "Accepted" + DELETING = "Deleting" + RUNNING = "Running" + SUCCEEDED = "Succeeded" + FAILED = "Failed" +class ResourceIdentityType(with_metaclass(_CaseInsensitiveEnumMeta, str, Enum)): + """The type of identity used for the configuration. Type 'SystemAssigned' will use an implicitly + created identity. Type 'None' will not use Managed Identity for the configuration. + """ -class ResourceIdentityType(str, Enum): - - system_assigned = "SystemAssigned" - none = "None" + SYSTEM_ASSIGNED = "SystemAssigned" + NONE = "None" diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/__init__.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/__init__.py index 6be16d2582d..2e68d5ecb0c 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/__init__.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/__init__.py @@ -1,12 +1,9 @@ # 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. -# +# 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. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- from ._source_control_configurations_operations import SourceControlConfigurationsOperations diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_extensions_operations.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_extensions_operations.py index e99f3328816..c3e2b0cf5f4 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_extensions_operations.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_extensions_operations.py @@ -1,431 +1,442 @@ # 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. -# +# 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. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- +from typing import TYPE_CHECKING +import warnings -import uuid -from msrest.pipeline import ClientRawResponse +from azure.core.exceptions import ClientAuthenticationError, HttpResponseError, ResourceExistsError, ResourceNotFoundError, map_error +from azure.core.paging import ItemPaged +from azure.core.pipeline import PipelineResponse +from azure.core.pipeline.transport import HttpRequest, HttpResponse +from azure.mgmt.core.exceptions import ARMErrorFormat -from .. import models +from .. import models as _models +if TYPE_CHECKING: + # pylint: disable=unused-import,ungrouped-imports + from typing import Any, Callable, Dict, Generic, Iterable, Optional, TypeVar, Union + + T = TypeVar('T') + ClsType = Optional[Callable[[PipelineResponse[HttpRequest, HttpResponse], T, Dict[str, Any]], Any]] class ExtensionsOperations(object): """ExtensionsOperations operations. - You should not instantiate directly this class, but create a Client instance that will create it for you and attach it as attribute. + You should not instantiate this class directly. Instead, you should create a Client instance that + instantiates it for you and attaches it as an attribute. + :ivar models: Alias to model classes used in this operation group. + :type models: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models :param client: Client for service requests. :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: The API version to be used with the HTTP request. Constant value: "2020-07-01-preview". """ - models = models + models = _models def __init__(self, client, config, serializer, deserializer): - self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2020-07-01-preview" - - self.config = config + self._config = config def create( - self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, extension_instance, custom_headers=None, raw=False, **operation_config): + self, + resource_group_name, # type: str + cluster_rp, # type: Union[str, "_models.Enum0"] + cluster_resource_name, # type: Union[str, "_models.Enum1"] + cluster_name, # type: str + extension_instance_name, # type: str + extension_instance, # type: "_models.ExtensionInstance" + **kwargs # type: Any + ): + # type: (...) -> "_models.ExtensionInstance" """Create a new Kubernetes Cluster Extension Instance. :param resource_group_name: The name of the resource group. :type resource_group_name: str - :param cluster_rp: The Kubernetes cluster RP - either - Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes - (for OnPrem K8S clusters). Possible values include: - 'Microsoft.ContainerService', 'Microsoft.Kubernetes' - :type cluster_rp: str - :param cluster_resource_name: The Kubernetes cluster resource name - - either managedClusters (for AKS clusters) or connectedClusters (for - OnPrem K8S clusters). Possible values include: 'managedClusters', - 'connectedClusters' - :type cluster_resource_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 :param cluster_name: The name of the kubernetes cluster. :type cluster_name: str :param extension_instance_name: Name of an instance of the Extension. :type extension_instance_name: str - :param extension_instance: Properties necessary to Create an Extension - Instance. - :type extension_instance: - ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstance - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: ExtensionInstance or ClientRawResponse if raw=true - :rtype: ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstance - or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ErrorResponseException` + :param extension_instance: Properties necessary to Create an Extension Instance. + :type extension_instance: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ExtensionInstance + :keyword callable cls: A custom type or function that will be passed the direct response + :return: ExtensionInstance, or the result of cls(response) + :rtype: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ExtensionInstance + :raises: ~azure.core.exceptions.HttpResponseError """ + cls = kwargs.pop('cls', None) # type: ClsType["_models.ExtensionInstance"] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + content_type = kwargs.pop("content_type", "application/json") + accept = "application/json" + # Construct URL - url = self.create.metadata['url'] + url = self.create.metadata['url'] # type: ignore path_format_arguments = { - 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), - 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str') + 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str'), } url = self._client.format_url(url, **path_format_arguments) # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(extension_instance, 'ExtensionInstance') + header_parameters = {} # type: Dict[str, Any] + header_parameters['Content-Type'] = self._serialize.header("content_type", content_type, 'str') + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') - # Construct and send request - request = self._client.put(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) + body_content_kwargs = {} # type: Dict[str, Any] + body_content = self._serialize.body(extension_instance, 'ExtensionInstance') + body_content_kwargs['content'] = body_content + request = self._client.put(url, query_parameters, header_parameters, **body_content_kwargs) + pipeline_response = self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response if response.status_code not in [200]: - raise models.ErrorResponseException(self._deserialize, response) + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) - deserialized = None - if response.status_code == 200: - deserialized = self._deserialize('ExtensionInstance', response) + deserialized = self._deserialize('ExtensionInstance', pipeline_response) - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response + if cls: + return cls(pipeline_response, deserialized, {}) return deserialized - create.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} + create.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} # type: ignore def get( - self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, custom_headers=None, raw=False, **operation_config): + self, + resource_group_name, # type: str + cluster_rp, # type: Union[str, "_models.Enum0"] + cluster_resource_name, # type: Union[str, "_models.Enum1"] + cluster_name, # type: str + extension_instance_name, # type: str + **kwargs # type: Any + ): + # type: (...) -> "_models.ExtensionInstance" """Gets details of the Kubernetes Cluster Extension Instance. :param resource_group_name: The name of the resource group. :type resource_group_name: str - :param cluster_rp: The Kubernetes cluster RP - either - Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes - (for OnPrem K8S clusters). Possible values include: - 'Microsoft.ContainerService', 'Microsoft.Kubernetes' - :type cluster_rp: str - :param cluster_resource_name: The Kubernetes cluster resource name - - either managedClusters (for AKS clusters) or connectedClusters (for - OnPrem K8S clusters). Possible values include: 'managedClusters', - 'connectedClusters' - :type cluster_resource_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 :param cluster_name: The name of the kubernetes cluster. :type cluster_name: str :param extension_instance_name: Name of an instance of the Extension. :type extension_instance_name: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: ExtensionInstance or ClientRawResponse if raw=true - :rtype: ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstance - or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ErrorResponseException` + :keyword callable cls: A custom type or function that will be passed the direct response + :return: ExtensionInstance, or the result of cls(response) + :rtype: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ExtensionInstance + :raises: ~azure.core.exceptions.HttpResponseError """ + cls = kwargs.pop('cls', None) # type: ClsType["_models.ExtensionInstance"] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + accept = "application/json" + # Construct URL - url = self.get.metadata['url'] + url = self.get.metadata['url'] # type: ignore path_format_arguments = { - 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), - 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str') + 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str'), } url = self._client.format_url(url, **path_format_arguments) # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request + header_parameters = {} # type: Dict[str, Any] + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') + request = self._client.get(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) + pipeline_response = self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response if response.status_code not in [200]: - raise models.ErrorResponseException(self._deserialize, response) + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) - deserialized = None - if response.status_code == 200: - deserialized = self._deserialize('ExtensionInstance', response) + deserialized = self._deserialize('ExtensionInstance', pipeline_response) - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response + if cls: + return cls(pipeline_response, deserialized, {}) return deserialized - get.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} + get.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} # type: ignore def update( - self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, extension_instance, custom_headers=None, raw=False, **operation_config): + self, + resource_group_name, # type: str + cluster_rp, # type: Union[str, "_models.Enum0"] + cluster_resource_name, # type: Union[str, "_models.Enum1"] + cluster_name, # type: str + extension_instance_name, # type: str + extension_instance, # type: "_models.ExtensionInstanceUpdate" + **kwargs # type: Any + ): + # type: (...) -> "_models.ExtensionInstance" """Update an existing Kubernetes Cluster Extension Instance. :param resource_group_name: The name of the resource group. :type resource_group_name: str - :param cluster_rp: The Kubernetes cluster RP - either - Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes - (for OnPrem K8S clusters). Possible values include: - 'Microsoft.ContainerService', 'Microsoft.Kubernetes' - :type cluster_rp: str - :param cluster_resource_name: The Kubernetes cluster resource name - - either managedClusters (for AKS clusters) or connectedClusters (for - OnPrem K8S clusters). Possible values include: 'managedClusters', - 'connectedClusters' - :type cluster_resource_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 :param cluster_name: The name of the kubernetes cluster. :type cluster_name: str :param extension_instance_name: Name of an instance of the Extension. :type extension_instance_name: str - :param extension_instance: Properties to Update in the Extension - Instance. - :type extension_instance: - ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstanceUpdate - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: ExtensionInstance or ClientRawResponse if raw=true - :rtype: ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstance - or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ErrorResponseException` + :param extension_instance: Properties to Update in the Extension Instance. + :type extension_instance: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ExtensionInstanceUpdate + :keyword callable cls: A custom type or function that will be passed the direct response + :return: ExtensionInstance, or the result of cls(response) + :rtype: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ExtensionInstance + :raises: ~azure.core.exceptions.HttpResponseError """ + cls = kwargs.pop('cls', None) # type: ClsType["_models.ExtensionInstance"] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + content_type = kwargs.pop("content_type", "application/json") + accept = "application/json" + # Construct URL - url = self.update.metadata['url'] + url = self.update.metadata['url'] # type: ignore path_format_arguments = { - 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), - 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str') + 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str'), } url = self._client.format_url(url, **path_format_arguments) # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(extension_instance, 'ExtensionInstanceUpdate') + header_parameters = {} # type: Dict[str, Any] + header_parameters['Content-Type'] = self._serialize.header("content_type", content_type, 'str') + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') - # Construct and send request - request = self._client.patch(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) + body_content_kwargs = {} # type: Dict[str, Any] + body_content = self._serialize.body(extension_instance, 'ExtensionInstanceUpdate') + body_content_kwargs['content'] = body_content + request = self._client.patch(url, query_parameters, header_parameters, **body_content_kwargs) + pipeline_response = self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response if response.status_code not in [200]: - raise models.ErrorResponseException(self._deserialize, response) + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) - deserialized = None - if response.status_code == 200: - deserialized = self._deserialize('ExtensionInstance', response) + deserialized = self._deserialize('ExtensionInstance', pipeline_response) - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response + if cls: + return cls(pipeline_response, deserialized, {}) return deserialized - update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} + update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} # type: ignore def delete( - self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, extension_instance_name, custom_headers=None, raw=False, **operation_config): - """Delete a Kubernetes Cluster Extension Instance. This will cause the - Agent to Uninstall the extension instance from the cluster. + self, + resource_group_name, # type: str + cluster_rp, # type: Union[str, "_models.Enum0"] + cluster_resource_name, # type: Union[str, "_models.Enum1"] + cluster_name, # type: str + extension_instance_name, # type: str + **kwargs # type: Any + ): + # type: (...) -> None + """Delete a Kubernetes Cluster Extension Instance. This will cause the Agent to Uninstall the + extension instance from the cluster. :param resource_group_name: The name of the resource group. :type resource_group_name: str - :param cluster_rp: The Kubernetes cluster RP - either - Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes - (for OnPrem K8S clusters). Possible values include: - 'Microsoft.ContainerService', 'Microsoft.Kubernetes' - :type cluster_rp: str - :param cluster_resource_name: The Kubernetes cluster resource name - - either managedClusters (for AKS clusters) or connectedClusters (for - OnPrem K8S clusters). Possible values include: 'managedClusters', - 'connectedClusters' - :type cluster_resource_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 :param cluster_name: The name of the kubernetes cluster. :type cluster_name: str :param extension_instance_name: Name of an instance of the Extension. :type extension_instance_name: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: None or ClientRawResponse if raw=true - :rtype: None or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ErrorResponseException` + :keyword callable cls: A custom type or function that will be passed the direct response + :return: None, or the result of cls(response) + :rtype: None + :raises: ~azure.core.exceptions.HttpResponseError """ + cls = kwargs.pop('cls', None) # type: ClsType[None] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + accept = "application/json" + # Construct URL - url = self.delete.metadata['url'] + url = self.delete.metadata['url'] # type: ignore path_format_arguments = { - 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), - 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str') + 'extensionInstanceName': self._serialize.url("extension_instance_name", extension_instance_name, 'str'), } url = self._client.format_url(url, **path_format_arguments) # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') # Construct headers - header_parameters = {} - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request + header_parameters = {} # type: Dict[str, Any] + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') + request = self._client.delete(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) + pipeline_response = self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response if response.status_code not in [200, 204]: - raise models.ErrorResponseException(self._deserialize, response) + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) + + if cls: + return cls(pipeline_response, None, {}) - if raw: - client_raw_response = ClientRawResponse(None, response) - return client_raw_response - delete.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} + delete.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions/{extensionInstanceName}'} # type: ignore def list( - self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, custom_headers=None, raw=False, **operation_config): + self, + resource_group_name, # type: str + cluster_rp, # type: Union[str, "_models.Enum0"] + cluster_resource_name, # type: Union[str, "_models.Enum1"] + cluster_name, # type: str + **kwargs # type: Any + ): + # type: (...) -> Iterable["_models.ExtensionInstancesList"] """List all Source Control Configurations. :param resource_group_name: The name of the resource group. :type resource_group_name: str - :param cluster_rp: The Kubernetes cluster RP - either - Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes - (for OnPrem K8S clusters). Possible values include: - 'Microsoft.ContainerService', 'Microsoft.Kubernetes' - :type cluster_rp: str - :param cluster_resource_name: The Kubernetes cluster resource name - - either managedClusters (for AKS clusters) or connectedClusters (for - OnPrem K8S clusters). Possible values include: 'managedClusters', - 'connectedClusters' - :type cluster_resource_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 :param cluster_name: The name of the kubernetes cluster. :type cluster_name: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: An iterator like instance of ExtensionInstance - :rtype: - ~azure.mgmt.kubernetesconfiguration.models.ExtensionInstancePaged[~azure.mgmt.kubernetesconfiguration.models.ExtensionInstance] - :raises: - :class:`ErrorResponseException` + :keyword callable cls: A custom type or function that will be passed the direct response + :return: An iterator like instance of either ExtensionInstancesList or the result of cls(response) + :rtype: ~azure.core.paging.ItemPaged[~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ExtensionInstancesList] + :raises: ~azure.core.exceptions.HttpResponseError """ + cls = kwargs.pop('cls', None) # type: ClsType["_models.ExtensionInstancesList"] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + accept = "application/json" + def prepare_request(next_link=None): + # Construct headers + header_parameters = {} # type: Dict[str, Any] + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') + if not next_link: # Construct URL - url = self.list.metadata['url'] + url = self.list.metadata['url'] # type: ignore path_format_arguments = { - 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), - 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str') + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), } url = self._client.format_url(url, **path_format_arguments) - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + request = self._client.get(url, query_parameters, header_parameters) else: url = next_link - query_parameters = {} - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) + query_parameters = {} # type: Dict[str, Any] + request = self._client.get(url, query_parameters, header_parameters) return request - def internal_paging(next_link=None): + def extract_data(pipeline_response): + deserialized = self._deserialize('ExtensionInstancesList', pipeline_response) + list_of_elem = deserialized.value + if cls: + list_of_elem = cls(list_of_elem) + return deserialized.next_link or None, iter(list_of_elem) + + def get_next(next_link=None): request = prepare_request(next_link) - response = self._client.send(request, stream=False, **operation_config) + pipeline_response = self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response if response.status_code not in [200]: - raise models.ErrorResponseException(self._deserialize, response) - - return response + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + map_error(status_code=response.status_code, response=response, error_map=error_map) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) - # Deserialize response - header_dict = None - if raw: - header_dict = {} - deserialized = models.ExtensionInstancePaged(internal_paging, self._deserialize.dependencies, header_dict) + return pipeline_response - return deserialized - list.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions'} + return ItemPaged( + get_next, extract_data + ) + list.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/extensions'} # type: ignore diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_operations.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_operations.py index 245a93c8294..1fe1fbf39b1 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_operations.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_operations.py @@ -1,101 +1,110 @@ # 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. -# +# 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. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- +from typing import TYPE_CHECKING +import warnings -import uuid -from msrest.pipeline import ClientRawResponse +from azure.core.exceptions import ClientAuthenticationError, HttpResponseError, ResourceExistsError, ResourceNotFoundError, map_error +from azure.core.paging import ItemPaged +from azure.core.pipeline import PipelineResponse +from azure.core.pipeline.transport import HttpRequest, HttpResponse +from azure.mgmt.core.exceptions import ARMErrorFormat -from .. import models +from .. import models as _models +if TYPE_CHECKING: + # pylint: disable=unused-import,ungrouped-imports + from typing import Any, Callable, Dict, Generic, Iterable, Optional, TypeVar + + T = TypeVar('T') + ClsType = Optional[Callable[[PipelineResponse[HttpRequest, HttpResponse], T, Dict[str, Any]], Any]] class Operations(object): """Operations operations. - You should not instantiate directly this class, but create a Client instance that will create it for you and attach it as attribute. + You should not instantiate this class directly. Instead, you should create a Client instance that + instantiates it for you and attaches it as an attribute. + :ivar models: Alias to model classes used in this operation group. + :type models: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models :param client: Client for service requests. :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: The API version to be used with the HTTP request. Constant value: "2020-07-01-preview". """ - models = models + models = _models def __init__(self, client, config, serializer, deserializer): - self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2020-07-01-preview" - - self.config = config + self._config = config def list( - self, custom_headers=None, raw=False, **operation_config): - """List all the available operations the KubernetesConfiguration resource - provider supports. - - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: An iterator like instance of ResourceProviderOperation - :rtype: - ~azure.mgmt.kubernetesconfiguration.models.ResourceProviderOperationPaged[~azure.mgmt.kubernetesconfiguration.models.ResourceProviderOperation] - :raises: - :class:`ErrorResponseException` + self, + **kwargs # type: Any + ): + # type: (...) -> Iterable["_models.ResourceProviderOperationList"] + """List all the available operations the KubernetesConfiguration resource provider supports. + + :keyword callable cls: A custom type or function that will be passed the direct response + :return: An iterator like instance of either ResourceProviderOperationList or the result of cls(response) + :rtype: ~azure.core.paging.ItemPaged[~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ResourceProviderOperationList] + :raises: ~azure.core.exceptions.HttpResponseError """ + cls = kwargs.pop('cls', None) # type: ClsType["_models.ResourceProviderOperationList"] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + accept = "application/json" + def prepare_request(next_link=None): + # Construct headers + header_parameters = {} # type: Dict[str, Any] + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') + if not next_link: # Construct URL - url = self.list.metadata['url'] - + url = self.list.metadata['url'] # type: ignore # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + request = self._client.get(url, query_parameters, header_parameters) else: url = next_link - query_parameters = {} - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) + query_parameters = {} # type: Dict[str, Any] + request = self._client.get(url, query_parameters, header_parameters) return request - def internal_paging(next_link=None): + def extract_data(pipeline_response): + deserialized = self._deserialize('ResourceProviderOperationList', pipeline_response) + list_of_elem = deserialized.value + if cls: + list_of_elem = cls(list_of_elem) + return deserialized.next_link or None, iter(list_of_elem) + + def get_next(next_link=None): request = prepare_request(next_link) - response = self._client.send(request, stream=False, **operation_config) + pipeline_response = self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response if response.status_code not in [200]: - raise models.ErrorResponseException(self._deserialize, response) - - return response + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + map_error(status_code=response.status_code, response=response, error_map=error_map) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) - # Deserialize response - header_dict = None - if raw: - header_dict = {} - deserialized = models.ResourceProviderOperationPaged(internal_paging, self._deserialize.dependencies, header_dict) + return pipeline_response - return deserialized - list.metadata = {'url': '/providers/Microsoft.KubernetesConfiguration/operations'} + return ItemPaged( + get_next, extract_data + ) + list.metadata = {'url': '/providers/Microsoft.KubernetesConfiguration/operations'} # type: ignore diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_source_control_configurations_operations.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_source_control_configurations_operations.py index 83a49e32146..d192efa6d03 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_source_control_configurations_operations.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/operations/_source_control_configurations_operations.py @@ -1,386 +1,429 @@ # 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. -# +# 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. +# Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- +from typing import TYPE_CHECKING +import warnings -import uuid -from msrest.pipeline import ClientRawResponse -from msrest.polling import LROPoller, NoPolling -from msrestazure.polling.arm_polling import ARMPolling +from azure.core.exceptions import ClientAuthenticationError, HttpResponseError, ResourceExistsError, ResourceNotFoundError, map_error +from azure.core.paging import ItemPaged +from azure.core.pipeline import PipelineResponse +from azure.core.pipeline.transport import HttpRequest, HttpResponse +from azure.core.polling import LROPoller, NoPolling, PollingMethod +from azure.mgmt.core.exceptions import ARMErrorFormat +from azure.mgmt.core.polling.arm_polling import ARMPolling -from .. import models +from .. import models as _models +if TYPE_CHECKING: + # pylint: disable=unused-import,ungrouped-imports + from typing import Any, Callable, Dict, Generic, Iterable, Optional, TypeVar, Union + + T = TypeVar('T') + ClsType = Optional[Callable[[PipelineResponse[HttpRequest, HttpResponse], T, Dict[str, Any]], Any]] class SourceControlConfigurationsOperations(object): """SourceControlConfigurationsOperations operations. - You should not instantiate directly this class, but create a Client instance that will create it for you and attach it as attribute. + You should not instantiate this class directly. Instead, you should create a Client instance that + instantiates it for you and attaches it as an attribute. + :ivar models: Alias to model classes used in this operation group. + :type models: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models :param client: Client for service requests. :param config: Configuration of service client. :param serializer: An object model serializer. :param deserializer: An object model deserializer. - :ivar api_version: The API version to be used with the HTTP request. Constant value: "2020-07-01-preview". """ - models = models + models = _models def __init__(self, client, config, serializer, deserializer): - self._client = client self._serialize = serializer self._deserialize = deserializer - self.api_version = "2020-07-01-preview" - - self.config = config + self._config = config def get( - self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, source_control_configuration_name, custom_headers=None, raw=False, **operation_config): + self, + resource_group_name, # type: str + cluster_rp, # type: Union[str, "_models.Enum0"] + cluster_resource_name, # type: Union[str, "_models.Enum1"] + cluster_name, # type: str + source_control_configuration_name, # type: str + **kwargs # type: Any + ): + # type: (...) -> "_models.SourceControlConfiguration" """Gets details of the Source Control Configuration. :param resource_group_name: The name of the resource group. :type resource_group_name: str - :param cluster_rp: The Kubernetes cluster RP - either - Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes - (for OnPrem K8S clusters). Possible values include: - 'Microsoft.ContainerService', 'Microsoft.Kubernetes' - :type cluster_rp: str - :param cluster_resource_name: The Kubernetes cluster resource name - - either managedClusters (for AKS clusters) or connectedClusters (for - OnPrem K8S clusters). Possible values include: 'managedClusters', - 'connectedClusters' - :type cluster_resource_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 :param cluster_name: The name of the kubernetes cluster. :type cluster_name: str - :param source_control_configuration_name: Name of the Source Control - Configuration. + :param source_control_configuration_name: Name of the Source Control Configuration. :type source_control_configuration_name: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: SourceControlConfiguration or ClientRawResponse if raw=true - :rtype: - ~azure.mgmt.kubernetesconfiguration.models.SourceControlConfiguration - or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ErrorResponseException` + :keyword callable cls: A custom type or function that will be passed the direct response + :return: SourceControlConfiguration, or the result of cls(response) + :rtype: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SourceControlConfiguration + :raises: ~azure.core.exceptions.HttpResponseError """ + cls = kwargs.pop('cls', None) # type: ClsType["_models.SourceControlConfiguration"] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + accept = "application/json" + # Construct URL - url = self.get.metadata['url'] + url = self.get.metadata['url'] # type: ignore path_format_arguments = { - 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), - 'sourceControlConfigurationName': self._serialize.url("source_control_configuration_name", source_control_configuration_name, 'str') + 'sourceControlConfigurationName': self._serialize.url("source_control_configuration_name", source_control_configuration_name, 'str'), } url = self._client.format_url(url, **path_format_arguments) # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request + header_parameters = {} # type: Dict[str, Any] + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') + request = self._client.get(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) + pipeline_response = self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response if response.status_code not in [200]: - raise models.ErrorResponseException(self._deserialize, response) + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) - deserialized = None - if response.status_code == 200: - deserialized = self._deserialize('SourceControlConfiguration', response) + deserialized = self._deserialize('SourceControlConfiguration', pipeline_response) - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response + if cls: + return cls(pipeline_response, deserialized, {}) return deserialized - get.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations/{sourceControlConfigurationName}'} + get.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations/{sourceControlConfigurationName}'} # type: ignore def create_or_update( - self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, source_control_configuration_name, source_control_configuration, custom_headers=None, raw=False, **operation_config): + self, + resource_group_name, # type: str + cluster_rp, # type: Union[str, "_models.Enum0"] + cluster_resource_name, # type: Union[str, "_models.Enum1"] + cluster_name, # type: str + source_control_configuration_name, # type: str + source_control_configuration, # type: "_models.SourceControlConfiguration" + **kwargs # type: Any + ): + # type: (...) -> "_models.SourceControlConfiguration" """Create a new Kubernetes Source Control Configuration. :param resource_group_name: The name of the resource group. :type resource_group_name: str - :param cluster_rp: The Kubernetes cluster RP - either - Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes - (for OnPrem K8S clusters). Possible values include: - 'Microsoft.ContainerService', 'Microsoft.Kubernetes' - :type cluster_rp: str - :param cluster_resource_name: The Kubernetes cluster resource name - - either managedClusters (for AKS clusters) or connectedClusters (for - OnPrem K8S clusters). Possible values include: 'managedClusters', - 'connectedClusters' - :type cluster_resource_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 :param cluster_name: The name of the kubernetes cluster. :type cluster_name: str - :param source_control_configuration_name: Name of the Source Control - Configuration. + :param source_control_configuration_name: Name of the Source Control Configuration. :type source_control_configuration_name: str - :param source_control_configuration: Properties necessary to Create - KubernetesConfiguration. - :type source_control_configuration: - ~azure.mgmt.kubernetesconfiguration.models.SourceControlConfiguration - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: SourceControlConfiguration or ClientRawResponse if raw=true - :rtype: - ~azure.mgmt.kubernetesconfiguration.models.SourceControlConfiguration - or ~msrest.pipeline.ClientRawResponse - :raises: - :class:`ErrorResponseException` + :param source_control_configuration: Properties necessary to Create KubernetesConfiguration. + :type source_control_configuration: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SourceControlConfiguration + :keyword callable cls: A custom type or function that will be passed the direct response + :return: SourceControlConfiguration, or the result of cls(response) + :rtype: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SourceControlConfiguration + :raises: ~azure.core.exceptions.HttpResponseError """ + cls = kwargs.pop('cls', None) # type: ClsType["_models.SourceControlConfiguration"] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + content_type = kwargs.pop("content_type", "application/json") + accept = "application/json" + # Construct URL - url = self.create_or_update.metadata['url'] + url = self.create_or_update.metadata['url'] # type: ignore path_format_arguments = { - 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), - 'sourceControlConfigurationName': self._serialize.url("source_control_configuration_name", source_control_configuration_name, 'str') + 'sourceControlConfigurationName': self._serialize.url("source_control_configuration_name", source_control_configuration_name, 'str'), } url = self._client.format_url(url, **path_format_arguments) # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - header_parameters['Content-Type'] = 'application/json; charset=utf-8' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct body - body_content = self._serialize.body(source_control_configuration, 'SourceControlConfiguration') + header_parameters = {} # type: Dict[str, Any] + header_parameters['Content-Type'] = self._serialize.header("content_type", content_type, 'str') + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') - # Construct and send request - request = self._client.put(url, query_parameters, header_parameters, body_content) - response = self._client.send(request, stream=False, **operation_config) + body_content_kwargs = {} # type: Dict[str, Any] + body_content = self._serialize.body(source_control_configuration, 'SourceControlConfiguration') + body_content_kwargs['content'] = body_content + request = self._client.put(url, query_parameters, header_parameters, **body_content_kwargs) + pipeline_response = self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response if response.status_code not in [200, 201]: - raise models.ErrorResponseException(self._deserialize, response) + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) - deserialized = None if response.status_code == 200: - deserialized = self._deserialize('SourceControlConfiguration', response) + deserialized = self._deserialize('SourceControlConfiguration', pipeline_response) + if response.status_code == 201: - deserialized = self._deserialize('SourceControlConfiguration', response) + deserialized = self._deserialize('SourceControlConfiguration', pipeline_response) - if raw: - client_raw_response = ClientRawResponse(deserialized, response) - return client_raw_response + if cls: + return cls(pipeline_response, deserialized, {}) return deserialized - create_or_update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations/{sourceControlConfigurationName}'} - + create_or_update.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations/{sourceControlConfigurationName}'} # type: ignore def _delete_initial( - self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, source_control_configuration_name, custom_headers=None, raw=False, **operation_config): + self, + resource_group_name, # type: str + cluster_rp, # type: Union[str, "_models.Enum0"] + cluster_resource_name, # type: Union[str, "_models.Enum1"] + cluster_name, # type: str + source_control_configuration_name, # type: str + **kwargs # type: Any + ): + # type: (...) -> None + cls = kwargs.pop('cls', None) # type: ClsType[None] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + accept = "application/json" + # Construct URL - url = self.delete.metadata['url'] + url = self._delete_initial.metadata['url'] # type: ignore path_format_arguments = { - 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), - 'sourceControlConfigurationName': self._serialize.url("source_control_configuration_name", source_control_configuration_name, 'str') + 'sourceControlConfigurationName': self._serialize.url("source_control_configuration_name", source_control_configuration_name, 'str'), } url = self._client.format_url(url, **path_format_arguments) # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') # Construct headers - header_parameters = {} - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request + header_parameters = {} # type: Dict[str, Any] + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') + request = self._client.delete(url, query_parameters, header_parameters) - response = self._client.send(request, stream=False, **operation_config) + pipeline_response = self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response if response.status_code not in [200, 204]: - raise models.ErrorResponseException(self._deserialize, response) - - if raw: - client_raw_response = ClientRawResponse(None, response) - return client_raw_response - - def delete( - self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, source_control_configuration_name, custom_headers=None, raw=False, polling=True, **operation_config): - """This will delete the YAML file used to set up the Source control - configuration, thus stopping future sync from the source repo. + map_error(status_code=response.status_code, response=response, error_map=error_map) + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) + + if cls: + return cls(pipeline_response, None, {}) + + _delete_initial.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations/{sourceControlConfigurationName}'} # type: ignore + + def begin_delete( + self, + resource_group_name, # type: str + cluster_rp, # type: Union[str, "_models.Enum0"] + cluster_resource_name, # type: Union[str, "_models.Enum1"] + cluster_name, # type: str + source_control_configuration_name, # type: str + **kwargs # type: Any + ): + # type: (...) -> LROPoller[None] + """This will delete the YAML file used to set up the Source control configuration, thus stopping + future sync from the source repo. :param resource_group_name: The name of the resource group. :type resource_group_name: str - :param cluster_rp: The Kubernetes cluster RP - either - Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes - (for OnPrem K8S clusters). Possible values include: - 'Microsoft.ContainerService', 'Microsoft.Kubernetes' - :type cluster_rp: str - :param cluster_resource_name: The Kubernetes cluster resource name - - either managedClusters (for AKS clusters) or connectedClusters (for - OnPrem K8S clusters). Possible values include: 'managedClusters', - 'connectedClusters' - :type cluster_resource_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 :param cluster_name: The name of the kubernetes cluster. :type cluster_name: str - :param source_control_configuration_name: Name of the Source Control - Configuration. + :param source_control_configuration_name: Name of the Source Control Configuration. :type source_control_configuration_name: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: The poller return type is ClientRawResponse, the - direct response alongside the deserialized response - :param polling: True for ARMPolling, False for no polling, or a - polling object for personal polling strategy - :return: An instance of LROPoller that returns None or - ClientRawResponse if raw==True - :rtype: ~msrestazure.azure_operation.AzureOperationPoller[None] or - ~msrestazure.azure_operation.AzureOperationPoller[~msrest.pipeline.ClientRawResponse[None]] - :raises: - :class:`ErrorResponseException` + :keyword callable cls: A custom type or function that will be passed the direct response + :keyword str continuation_token: A continuation token to restart a poller from a saved state. + :keyword polling: By default, your polling method will be ARMPolling. + Pass in False for this operation to not poll, or pass in your own initialized polling object for a personal polling strategy. + :paramtype polling: bool or ~azure.core.polling.PollingMethod + :keyword int polling_interval: Default waiting time between two polls for LRO operations if no Retry-After header is present. + :return: An instance of LROPoller that returns either None or the result of cls(response) + :rtype: ~azure.core.polling.LROPoller[None] + :raises ~azure.core.exceptions.HttpResponseError: """ - raw_result = self._delete_initial( - resource_group_name=resource_group_name, - cluster_rp=cluster_rp, - cluster_resource_name=cluster_resource_name, - cluster_name=cluster_name, - source_control_configuration_name=source_control_configuration_name, - custom_headers=custom_headers, - raw=True, - **operation_config + polling = kwargs.pop('polling', True) # type: Union[bool, PollingMethod] + cls = kwargs.pop('cls', None) # type: ClsType[None] + lro_delay = kwargs.pop( + 'polling_interval', + self._config.polling_interval ) + cont_token = kwargs.pop('continuation_token', None) # type: Optional[str] + if cont_token is None: + raw_result = self._delete_initial( + resource_group_name=resource_group_name, + cluster_rp=cluster_rp, + cluster_resource_name=cluster_resource_name, + cluster_name=cluster_name, + source_control_configuration_name=source_control_configuration_name, + cls=lambda x,y,z: x, + **kwargs + ) + + kwargs.pop('error_map', None) + kwargs.pop('content_type', None) + + def get_long_running_output(pipeline_response): + if cls: + return cls(pipeline_response, None, {}) - def get_long_running_output(response): - if raw: - client_raw_response = ClientRawResponse(None, response) - return client_raw_response + path_format_arguments = { + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), + 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), + 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), + 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), + 'sourceControlConfigurationName': self._serialize.url("source_control_configuration_name", source_control_configuration_name, 'str'), + } - lro_delay = operation_config.get( - 'long_running_operation_timeout', - self.config.long_running_operation_timeout) - if polling is True: polling_method = ARMPolling(lro_delay, **operation_config) + if polling is True: polling_method = ARMPolling(lro_delay, path_format_arguments=path_format_arguments, **kwargs) elif polling is False: polling_method = NoPolling() else: polling_method = polling - return LROPoller(self._client, raw_result, get_long_running_output, polling_method) - delete.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations/{sourceControlConfigurationName}'} + if cont_token: + return LROPoller.from_continuation_token( + polling_method=polling_method, + continuation_token=cont_token, + client=self._client, + deserialization_callback=get_long_running_output + ) + else: + return LROPoller(self._client, raw_result, get_long_running_output, polling_method) + begin_delete.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations/{sourceControlConfigurationName}'} # type: ignore def list( - self, resource_group_name, cluster_rp, cluster_resource_name, cluster_name, custom_headers=None, raw=False, **operation_config): + self, + resource_group_name, # type: str + cluster_rp, # type: Union[str, "_models.Enum0"] + cluster_resource_name, # type: Union[str, "_models.Enum1"] + cluster_name, # type: str + **kwargs # type: Any + ): + # type: (...) -> Iterable["_models.SourceControlConfigurationList"] """List all Source Control Configurations. :param resource_group_name: The name of the resource group. :type resource_group_name: str - :param cluster_rp: The Kubernetes cluster RP - either - Microsoft.ContainerService (for AKS clusters) or Microsoft.Kubernetes - (for OnPrem K8S clusters). Possible values include: - 'Microsoft.ContainerService', 'Microsoft.Kubernetes' - :type cluster_rp: str - :param cluster_resource_name: The Kubernetes cluster resource name - - either managedClusters (for AKS clusters) or connectedClusters (for - OnPrem K8S clusters). Possible values include: 'managedClusters', - 'connectedClusters' - :type cluster_resource_name: str + :param cluster_rp: The Kubernetes cluster RP - either Microsoft.ContainerService (for AKS + clusters) or Microsoft.Kubernetes (for OnPrem K8S clusters). + :type cluster_rp: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum0 + :param cluster_resource_name: The Kubernetes cluster resource name - either managedClusters + (for AKS clusters) or connectedClusters (for OnPrem K8S clusters). + :type cluster_resource_name: str or ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.Enum1 :param cluster_name: The name of the kubernetes cluster. :type cluster_name: str - :param dict custom_headers: headers that will be added to the request - :param bool raw: returns the direct response alongside the - deserialized response - :param operation_config: :ref:`Operation configuration - overrides`. - :return: An iterator like instance of SourceControlConfiguration - :rtype: - ~azure.mgmt.kubernetesconfiguration.models.SourceControlConfigurationPaged[~azure.mgmt.kubernetesconfiguration.models.SourceControlConfiguration] - :raises: - :class:`ErrorResponseException` + :keyword callable cls: A custom type or function that will be passed the direct response + :return: An iterator like instance of either SourceControlConfigurationList or the result of cls(response) + :rtype: ~azure.core.paging.ItemPaged[~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.SourceControlConfigurationList] + :raises: ~azure.core.exceptions.HttpResponseError """ + cls = kwargs.pop('cls', None) # type: ClsType["_models.SourceControlConfigurationList"] + error_map = { + 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError + } + error_map.update(kwargs.pop('error_map', {})) + api_version = "2020-07-01-preview" + accept = "application/json" + def prepare_request(next_link=None): + # Construct headers + header_parameters = {} # type: Dict[str, Any] + header_parameters['Accept'] = self._serialize.header("accept", accept, 'str') + if not next_link: # Construct URL - url = self.list.metadata['url'] + url = self.list.metadata['url'] # type: ignore path_format_arguments = { - 'subscriptionId': self._serialize.url("self.config.subscription_id", self.config.subscription_id, 'str'), + 'subscriptionId': self._serialize.url("self._config.subscription_id", self._config.subscription_id, 'str'), 'resourceGroupName': self._serialize.url("resource_group_name", resource_group_name, 'str'), 'clusterRp': self._serialize.url("cluster_rp", cluster_rp, 'str'), 'clusterResourceName': self._serialize.url("cluster_resource_name", cluster_resource_name, 'str'), - 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str') + 'clusterName': self._serialize.url("cluster_name", cluster_name, 'str'), } url = self._client.format_url(url, **path_format_arguments) - # Construct parameters - query_parameters = {} - query_parameters['api-version'] = self._serialize.query("self.api_version", self.api_version, 'str') + query_parameters = {} # type: Dict[str, Any] + query_parameters['api-version'] = self._serialize.query("api_version", api_version, 'str') + request = self._client.get(url, query_parameters, header_parameters) else: url = next_link - query_parameters = {} - - # Construct headers - header_parameters = {} - header_parameters['Accept'] = 'application/json' - if self.config.generate_client_request_id: - header_parameters['x-ms-client-request-id'] = str(uuid.uuid1()) - if custom_headers: - header_parameters.update(custom_headers) - if self.config.accept_language is not None: - header_parameters['accept-language'] = self._serialize.header("self.config.accept_language", self.config.accept_language, 'str') - - # Construct and send request - request = self._client.get(url, query_parameters, header_parameters) + query_parameters = {} # type: Dict[str, Any] + request = self._client.get(url, query_parameters, header_parameters) return request - def internal_paging(next_link=None): + def extract_data(pipeline_response): + deserialized = self._deserialize('SourceControlConfigurationList', pipeline_response) + list_of_elem = deserialized.value + if cls: + list_of_elem = cls(list_of_elem) + return deserialized.next_link or None, iter(list_of_elem) + + def get_next(next_link=None): request = prepare_request(next_link) - response = self._client.send(request, stream=False, **operation_config) + pipeline_response = self._client._pipeline.run(request, stream=False, **kwargs) + response = pipeline_response.http_response if response.status_code not in [200]: - raise models.ErrorResponseException(self._deserialize, response) - - return response + error = self._deserialize.failsafe_deserialize(_models.ErrorResponse, response) + map_error(status_code=response.status_code, response=response, error_map=error_map) + raise HttpResponseError(response=response, model=error, error_format=ARMErrorFormat) - # Deserialize response - header_dict = None - if raw: - header_dict = {} - deserialized = models.SourceControlConfigurationPaged(internal_paging, self._deserialize.dependencies, header_dict) + return pipeline_response - return deserialized - list.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations'} + return ItemPaged( + get_next, extract_data + ) + list.metadata = {'url': '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{clusterRp}/{clusterResourceName}/{clusterName}/providers/Microsoft.KubernetesConfiguration/sourceControlConfigurations'} # type: ignore diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index a9a7f60fbaa..6efb1bc5a59 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -32,7 +32,7 @@ # TODO: Add any additional SDK dependencies here DEPENDENCIES = [] -VERSION = "0.5.1" +VERSION = "0.6.0" with open('README.rst', 'r', encoding='utf-8') as f: README = f.read() diff --git a/testing/Cleanup.ps1 b/testing/Cleanup.ps1 index dc1d1d3d908..cef1e2d1bc3 100644 --- a/testing/Cleanup.ps1 +++ b/testing/Cleanup.ps1 @@ -15,7 +15,8 @@ az connectedk8s delete -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.arcClusterName if (!$?) { kubectl get pods -A - Exit 1 + kubectl logs -l app.kubernetes.io/component=cluster-metadata-operator -n azure-arc -c cluster-metadata-operator + Exit 0 } # Skip deleting the AKS Cluster if this is CI From bf15d99a905fe583671fdfe0176f5625c44d80f9 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Fri, 23 Jul 2021 15:12:10 -0700 Subject: [PATCH 83/86] Fix identity in wrong place in model (#66) --- .../azext_k8s_extension/vendored_sdks/models/_models.py | 2 +- .../azext_k8s_extension/vendored_sdks/models/_models_py3.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py index ef31b453ddc..315237c6587 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py @@ -310,7 +310,7 @@ class ExtensionInstance(ProxyResource): 'last_modified_time': {'key': 'properties.lastModifiedTime', 'type': 'str'}, 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'}, - 'identity': {'key': 'properties.identity', 'type': 'ConfigurationIdentity'}, + 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, } def __init__( diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py index 7d8c10c5306..f749d3d7078 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py @@ -328,7 +328,7 @@ class ExtensionInstance(ProxyResource): 'last_modified_time': {'key': 'properties.lastModifiedTime', 'type': 'str'}, 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'}, - 'identity': {'key': 'properties.identity', 'type': 'ConfigurationIdentity'}, + 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, } def __init__( From bc48fdb320fb8f729a07bc2b43bd4ced755d188c Mon Sep 17 00:00:00 2001 From: Niranjan Shankar Date: Mon, 26 Jul 2021 20:04:51 -0400 Subject: [PATCH 84/86] Readd osm-arc distro validation (#62) * Add distro validation for osm-arc removed release-train logic * Readd osm_arc distro validation * Fix style * Rm space * Edit test * Fixed tests and error logic * Remove dependency * Add delete method Co-authored-by: Jonathan Innis --- .../partner_extensions/OpenServiceMesh.py | 83 +++++++++---------- .../tests/latest/test_open_service_mesh.py | 13 ++- 2 files changed, 44 insertions(+), 52 deletions(-) diff --git a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py index 048dfe58637..ff65193b3dd 100644 --- a/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py +++ b/src/k8s-extension/azext_k8s_extension/partner_extensions/OpenServiceMesh.py @@ -14,6 +14,7 @@ from packaging import version import yaml +import requests from ..partner_extensions import PartnerExtensionModel @@ -32,8 +33,6 @@ class OpenServiceMesh(PartnerExtensionModel): - CHART_NAME = "osm-arc" - CHART_LOCATION = "https://azure.github.io/osm-azure" def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_type, extension_type, scope, auto_upgrade_minor_version, release_train, version, target_namespace, @@ -67,7 +66,7 @@ def Create(self, cmd, client, resource_group_name, cluster_name, name, cluster_t # NOTE-2: Return a valid ExtensionInstance object, Instance name and flag for Identity create_identity = False - # _validate_tested_distro(cmd, resource_group_name, cluster_name, version) + _validate_tested_distro(cmd, resource_group_name, cluster_name, version) extension_instance = ExtensionInstance( extension_type=extension_type, @@ -105,58 +104,52 @@ def Delete(self, client, resource_group_name, cluster_name, name, cluster_type): pass -# def _validate_tested_distro(cmd, cluster_resource_group_name, cluster_name, extension_version): +def _validate_tested_distro(cmd, cluster_resource_group_name, cluster_name, extension_version): -# field_unavailable_error = '\"testedDistros\" field unavailable for version {0} of microsoft.openservicemesh, ' \ -# 'cannot determine if this Kubernetes distribution has been properly tested'.format(extension_version) + field_unavailable_error = '\"testedDistros\" field unavailable for version {0} of microsoft.openservicemesh, ' \ + 'cannot determine if this Kubernetes distribution has been properly tested'.format(extension_version) -# if version.parse(str(extension_version)) <= version.parse("0.8.3"): -# logger.warning(field_unavailable_error) -# return + if version.parse(str(extension_version)) <= version.parse("0.8.3"): + logger.warning(field_unavailable_error) + return -# subscription_id = get_subscription_id(cmd.cli_ctx) -# resources = cf_resources(cmd.cli_ctx, subscription_id) + subscription_id = get_subscription_id(cmd.cli_ctx) + resources = cf_resources(cmd.cli_ctx, subscription_id) -# cluster_resource_id = '/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Kubernetes' \ -# '/connectedClusters/{2}'.format(subscription_id, cluster_resource_group_name, cluster_name) + cluster_resource_id = '/subscriptions/{0}/resourceGroups/{1}/providers/Microsoft.Kubernetes' \ + '/connectedClusters/{2}'.format(subscription_id, cluster_resource_group_name, cluster_name) -# resource = resources.get_by_id(cluster_resource_id, '2020-01-01-preview') -# cluster_distro = resource.properties['distribution'].lower() + resource = resources.get_by_id(cluster_resource_id, '2020-01-01-preview') + cluster_distro = resource.properties['distribution'].lower() -# if cluster_distro == "general": -# logger.warning('Unable to determine if distro has been tested for microsoft.openservicemesh, ' -# 'kubernetes distro: \"general\"') -# return + if cluster_distro == "general": + logger.warning('Unable to determine if distro has been tested for microsoft.openservicemesh, ' + 'kubernetes distro: \"general\"') + return -# tested_distros = _get_tested_distros(extension_version) + tested_distros = _get_tested_distros(extension_version) -# if tested_distros is None: -# logger.warning(field_unavailable_error) -# elif cluster_distro not in tested_distros.split(): -# logger.warning('Untested kubernetes distro for microsoft.openservicemesh, Kubernetes distro is %s', -# cluster_distro) + if tested_distros is None: + logger.warning(field_unavailable_error) + elif cluster_distro not in tested_distros.split(): + logger.warning('Untested kubernetes distro for microsoft.openservicemesh, Kubernetes distro is %s', + cluster_distro) -# def _get_tested_distros(chart_version): +def _get_tested_distros(chart_version): -# try: -# chart_arc = ChartBuilder({ -# "name": OpenServiceMesh.CHART_NAME, -# "version": str(chart_version), -# "source": { -# "type": "repo", -# "location": OpenServiceMesh.CHART_LOCATION -# } -# }) -# except VersionError: -# raise InvalidArgumentValueError( -# "Invalid version '{}' for microsoft.openservicemesh".format(chart_version) -# ) + chart_url = 'https://raw.githubusercontent.com/Azure/osm-azure/' \ + 'v{0}/charts/osm-arc/values.yaml'.format(chart_version) + chart_request = requests.get(url=chart_url) -# values = chart_arc.get_values() -# values_yaml = yaml.load(values.raw, Loader=yaml.FullLoader) + if chart_request.status_code == 404: + raise InvalidArgumentValueError( + "Invalid version '{}' for microsoft.openservicemesh".format(chart_version) + ) + + values_yaml = yaml.load(chart_request.text, Loader=yaml.FullLoader) -# try: -# return values_yaml['OpenServiceMesh']['testedDistros'] -# except KeyError: -# return None + try: + return values_yaml['OpenServiceMesh']['testedDistros'] + except KeyError: + return None diff --git a/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py b/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py index 61b774045ce..72e94a06831 100644 --- a/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py +++ b/src/k8s-extension/azext_k8s_extension/tests/latest/test_open_service_mesh.py @@ -9,15 +9,14 @@ import unittest from azure.cli.core.azclierror import InvalidArgumentValueError -# from azext_k8s_extension.partner_extensions.OpenServiceMesh import _get_tested_distros +from azext_k8s_extension.partner_extensions.OpenServiceMesh import _get_tested_distros TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..')) class TestOpenServiceMesh(unittest.TestCase): def test_bad_osm_arc_version(self): - # version = "0.7.1" - # err = "Invalid version \'" + str(version) + "\' for microsoft.openservicemesh" - # with self.assertRaises(InvalidArgumentValueError) as argError: - # _get_tested_distros(version) - # self.assertEqual(str(argError.exception), err) - pass + version = "0.7.1" + err = "Invalid version \'" + str(version) + "\' for microsoft.openservicemesh" + with self.assertRaises(InvalidArgumentValueError) as argError: + _get_tested_distros(version) + self.assertEqual(str(argError.exception), err) From 611b81fcd59015c9021eb7136aa78cb0bf844fb9 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Tue, 27 Jul 2021 14:51:02 -0700 Subject: [PATCH 85/86] Don't Send Identity Headers If In DF (#67) * Don't send identity for clusters in dogfood * Add location to model for identity * Add identity validation to testing --- src/k8s-extension/HISTORY.rst | 6 ++++++ src/k8s-extension/azext_k8s_extension/consts.py | 1 + src/k8s-extension/azext_k8s_extension/custom.py | 7 ++++++- .../azext_k8s_extension/vendored_sdks/models/_models.py | 4 ++++ .../vendored_sdks/models/_models_py3.py | 5 +++++ src/k8s-extension/setup.py | 2 +- testing/test/extensions/public/AzureMonitor.Tests.ps1 | 4 +++- testing/test/helper/Helper.ps1 | 6 ++++++ 8 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/k8s-extension/HISTORY.rst b/src/k8s-extension/HISTORY.rst index 5855f96591f..295bb0e4136 100644 --- a/src/k8s-extension/HISTORY.rst +++ b/src/k8s-extension/HISTORY.rst @@ -3,6 +3,12 @@ Release History =============== +0.6.1 +++++++++++++++++++ +* Remove sending identity for clusters in Dogfood +* Provide fix for getting tested distros for microsoft.openservicemesh +* Add location to model for identity + 0.6.0 ++++++++++++++++++ * Update extension resource models to Track2 diff --git a/src/k8s-extension/azext_k8s_extension/consts.py b/src/k8s-extension/azext_k8s_extension/consts.py index c75489d2362..b7fe91cb8eb 100644 --- a/src/k8s-extension/azext_k8s_extension/consts.py +++ b/src/k8s-extension/azext_k8s_extension/consts.py @@ -8,3 +8,4 @@ EXTENSION_PACKAGE_NAME = "azext_k8s_extension" PROVIDER_NAMESPACE = 'Microsoft.KubernetesConfiguration' REGISTERED = "Registered" +DF_RM_ENDPOINT = 'https://api-dogfood.resources.windows-int.net/' diff --git a/src/k8s-extension/azext_k8s_extension/custom.py b/src/k8s-extension/azext_k8s_extension/custom.py index 9a6bdb10656..42f5d3234fc 100644 --- a/src/k8s-extension/azext_k8s_extension/custom.py +++ b/src/k8s-extension/azext_k8s_extension/custom.py @@ -138,7 +138,8 @@ def create_k8s_extension(cmd, client, resource_group_name, cluster_name, name, c validate_cc_registration(cmd) # Create identity, if required - if create_identity: + # We don't create the identity if we are in DF + if create_identity and not __is_dogfood_cluster(cmd): extension_instance.identity, extension_instance.location = \ __create_identity(cmd, resource_group_name, cluster_name, cluster_type, cluster_rp) @@ -292,3 +293,7 @@ def __get_config_settings_from_file(file_path): raise Exception("File {} is empty".format(file_path)) return settings + + +def __is_dogfood_cluster(cmd): + return cmd.cli_ctx.cloud.endpoints.resource_manager == consts.DF_RM_ENDPOINT diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py index 315237c6587..9370d28bbff 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models.py @@ -279,6 +279,8 @@ class ExtensionInstance(ProxyResource): :param identity: The identity of the configuration. :type identity: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ConfigurationIdentity + :param location: Location of resource type + :type location: str """ _validation = { @@ -311,6 +313,7 @@ class ExtensionInstance(ProxyResource): 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'}, 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, + 'location': {'key': 'location', 'type': 'str'}, } def __init__( @@ -332,6 +335,7 @@ def __init__( self.last_status_time = None self.error_info = None self.identity = kwargs.get('identity', None) + self.location = kwargs.get('location', None) class ExtensionInstancesList(msrest.serialization.Model): diff --git a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py index f749d3d7078..a3cb0d37faf 100644 --- a/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py +++ b/src/k8s-extension/azext_k8s_extension/vendored_sdks/models/_models_py3.py @@ -297,6 +297,8 @@ class ExtensionInstance(ProxyResource): :param identity: The identity of the configuration. :type identity: ~azure.mgmt.kubernetesconfiguration.v2020_07_01_preview.models.ConfigurationIdentity + :param location: Location of resource type + :type location: str """ _validation = { @@ -329,6 +331,7 @@ class ExtensionInstance(ProxyResource): 'last_status_time': {'key': 'properties.lastStatusTime', 'type': 'str'}, 'error_info': {'key': 'properties.errorInfo', 'type': 'ErrorDefinition'}, 'identity': {'key': 'identity', 'type': 'ConfigurationIdentity'}, + 'location': {'key': 'location', 'type': 'str'}, } def __init__( @@ -344,6 +347,7 @@ def __init__( configuration_protected_settings: Optional[Dict[str, str]] = None, statuses: Optional[List["ExtensionStatus"]] = None, identity: Optional["ConfigurationIdentity"] = None, + location: Optional[str] = None, **kwargs ): super(ExtensionInstance, self).__init__(system_data=system_data, **kwargs) @@ -361,6 +365,7 @@ def __init__( self.last_status_time = None self.error_info = None self.identity = identity + self.location = location class ExtensionInstancesList(msrest.serialization.Model): diff --git a/src/k8s-extension/setup.py b/src/k8s-extension/setup.py index 6efb1bc5a59..014ed287490 100644 --- a/src/k8s-extension/setup.py +++ b/src/k8s-extension/setup.py @@ -32,7 +32,7 @@ # TODO: Add any additional SDK dependencies here DEPENDENCIES = [] -VERSION = "0.6.0" +VERSION = "0.6.1" with open('README.rst', 'r', encoding='utf-8') as f: README = f.read() diff --git a/testing/test/extensions/public/AzureMonitor.Tests.ps1 b/testing/test/extensions/public/AzureMonitor.Tests.ps1 index a78ec6ba980..bc7b1fedd8f 100644 --- a/testing/test/extensions/public/AzureMonitor.Tests.ps1 +++ b/testing/test/extensions/public/AzureMonitor.Tests.ps1 @@ -24,7 +24,9 @@ Describe 'Azure Monitor Testing' { do { if (Has-ExtensionData $extensionName) { - break + if (Has-Identity-Provisioned) { + break + } } Start-Sleep -Seconds 10 $n += 1 diff --git a/testing/test/helper/Helper.ps1 b/testing/test/helper/Helper.ps1 index 7bb11146ab2..4ff949e7ab4 100644 --- a/testing/test/helper/Helper.ps1 +++ b/testing/test/helper/Helper.ps1 @@ -18,6 +18,12 @@ function Has-ExtensionData { return $false } + +function Has-Identity-Provisioned { + $output = kubectl get azureclusteridentityrequests -n azure-arc container-insights-clusteridentityrequest -o json | ConvertFrom-Json + return ($null -ne $output.status.expirationTime) -and ($null -ne $output.status.tokenReference.dataName) -and ($null -ne $output.status.tokenReference.secretName) +} + function Get-ExtensionStatus { param( [string]$extensionName From df3f8ad2f6f8480eb61fec4b0f0ebf7c2286f302 Mon Sep 17 00:00:00 2001 From: Jonathan Innis Date: Tue, 27 Jul 2021 14:51:56 -0700 Subject: [PATCH 86/86] Remove unneeded files --- k8s-custom-pipelines.yml | 344 ------------------ testing/.gitignore | 9 - testing/Bootstrap.ps1 | 80 ---- testing/Cleanup.ps1 | 30 -- testing/README.md | 116 ------ testing/Test.ps1 | 119 ------ .../k8s_configuration-1.0.0-py3-none-any.whl | Bin 42351 -> 0 bytes .../bin/k8s_extension-0.4.0-py3-none-any.whl | Bin 55464 -> 0 bytes testing/docs/test_authoring.md | 142 -------- testing/owners.txt | 2 - testing/settings.template.json | 11 - .../Configuration.HTTPS.Tests.ps1 | 54 --- .../Configuration.HelmOperator.Tests.ps1 | 137 ------- .../Configuration.KnownHost.Tests.ps1 | 6 - .../Configuration.PrivateKey.Tests.ps1 | 86 ----- .../configurations/Configuration.Tests.ps1 | 80 ---- testing/test/configurations/Constants.ps1 | 8 - testing/test/configurations/Helper.ps1 | 45 --- .../extensions/data/azure_ml/test_cert.pem | 1 - .../extensions/data/azure_ml/test_key.pem | 1 - .../private-preview/AzurePolicy.Tests.ps1 | 100 ----- .../extensions/public/AzureDefender.Tests.ps1 | 98 ----- .../public/AzureMLKubernetes.Tests.ps1 | 201 ---------- .../extensions/public/AzureMonitor.Tests.ps1 | 100 ----- .../public/OpenServiceMesh.Tests.ps1 | 102 ------ testing/test/helper/Constants.ps1 | 7 - testing/test/helper/Helper.ps1 | 72 ---- 27 files changed, 1951 deletions(-) delete mode 100644 k8s-custom-pipelines.yml delete mode 100644 testing/.gitignore delete mode 100644 testing/Bootstrap.ps1 delete mode 100644 testing/Cleanup.ps1 delete mode 100644 testing/README.md delete mode 100644 testing/Test.ps1 delete mode 100644 testing/bin/k8s_configuration-1.0.0-py3-none-any.whl delete mode 100644 testing/bin/k8s_extension-0.4.0-py3-none-any.whl delete mode 100644 testing/docs/test_authoring.md delete mode 100644 testing/owners.txt delete mode 100644 testing/settings.template.json delete mode 100644 testing/test/configurations/Configuration.HTTPS.Tests.ps1 delete mode 100644 testing/test/configurations/Configuration.HelmOperator.Tests.ps1 delete mode 100644 testing/test/configurations/Configuration.KnownHost.Tests.ps1 delete mode 100644 testing/test/configurations/Configuration.PrivateKey.Tests.ps1 delete mode 100644 testing/test/configurations/Configuration.Tests.ps1 delete mode 100644 testing/test/configurations/Constants.ps1 delete mode 100644 testing/test/configurations/Helper.ps1 delete mode 100644 testing/test/extensions/data/azure_ml/test_cert.pem delete mode 100644 testing/test/extensions/data/azure_ml/test_key.pem delete mode 100644 testing/test/extensions/private-preview/AzurePolicy.Tests.ps1 delete mode 100644 testing/test/extensions/public/AzureDefender.Tests.ps1 delete mode 100644 testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 delete mode 100644 testing/test/extensions/public/AzureMonitor.Tests.ps1 delete mode 100644 testing/test/extensions/public/OpenServiceMesh.Tests.ps1 delete mode 100644 testing/test/helper/Constants.ps1 delete mode 100644 testing/test/helper/Helper.ps1 diff --git a/k8s-custom-pipelines.yml b/k8s-custom-pipelines.yml deleted file mode 100644 index 185af022510..00000000000 --- a/k8s-custom-pipelines.yml +++ /dev/null @@ -1,344 +0,0 @@ -trigger: - batch: true - branches: - include: - - k8s-extension/public - - k8s-extension/private -pr: - branches: - include: - - k8s-extension/public - - k8s-extension/private - -stages: -- stage: BuildTestPublishExtension - displayName: "Build, Test, and Publish Extension" - variables: - TEST_PATH: $(Agent.BuildDirectory)/s/testing - CLI_REPO_PATH: $(Agent.BuildDirectory)/s - SUBSCRIPTION_ID: "15c06b1b-01d6-407b-bb21-740b8617dea3" - RESOURCE_GROUP: "K8sPartnerExtensionTest" - BASE_CLUSTER_NAME: "k8s-extension-cluster" - IS_PRIVATE_BRANCH: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/k8s-extension/private'), eq(variables['System.PullRequest.TargetBranch'], 'k8s-extension/private'))] - - EXTENSION_NAME: "k8s-extension" - EXTENSION_FILE_NAME: "k8s_extension" - jobs: - - job: K8sExtensionTestSuite - displayName: "Run the Test Suite" - pool: - vmImage: 'ubuntu-latest' - steps: - - checkout: self - - bash: | - echo "Installing helm3" - curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 - chmod 700 get_helm.sh - ./get_helm.sh - - echo "Installing kubectl" - curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl" - chmod +x ./kubectl - sudo mv ./kubectl /usr/local/bin/kubectl - kubectl version --client - displayName: "Setup the VM with helm3 and kubectl" - - task: UsePythonVersion@0 - displayName: 'Use Python 3.6' - inputs: - versionSpec: 3.6 - - bash: | - set -ev - echo "Building extension ${EXTENSION_NAME}..." - - # prepare and activate virtualenv - pip install virtualenv - python3 -m venv env/ - source env/bin/activate - - # clone azure-cli - git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli - - pip install --upgrade pip - pip install -q azdev - - ls $(CLI_REPO_PATH) - - azdev --version - azdev setup -c ../azure-cli -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) - azdev extension build $(EXTENSION_NAME) - workingDirectory: $(CLI_REPO_PATH) - displayName: "Setup and Build Extension with azdev" - - - bash: | - K8S_EXTENSION_VERSION=$(ls ${EXTENSION_FILE_NAME}* | cut -d "-" -f2) - echo "##vso[task.setvariable variable=K8S_EXTENSION_VERSION]$K8S_EXTENSION_VERSION" - cp * $(TEST_PATH)/bin - workingDirectory: $(CLI_REPO_PATH)/dist - displayName: "Copy the Built .whl to Extension Test Path" - - - bash: | - RAND_STR=$RANDOM - AKS_CLUSTER_NAME="${BASE_CLUSTER_NAME}-${RAND_STR}-aks" - ARC_CLUSTER_NAME="${BASE_CLUSTER_NAME}-${RAND_STR}-arc" - - JSON_STRING=$(jq -n \ - --arg SUB_ID "$SUBSCRIPTION_ID" \ - --arg RG "$RESOURCE_GROUP" \ - --arg AKS_CLUSTER_NAME "$AKS_CLUSTER_NAME" \ - --arg ARC_CLUSTER_NAME "$ARC_CLUSTER_NAME" \ - --arg K8S_EXTENSION_VERSION "$K8S_EXTENSION_VERSION" \ - '{subscriptionId: $SUB_ID, resourceGroup: $RG, aksClusterName: $AKS_CLUSTER_NAME, arcClusterName: $ARC_CLUSTER_NAME, extensionVersion: {"k8s-extension": $K8S_EXTENSION_VERSION, connectedk8s: "1.0.0"}}') - echo $JSON_STRING > settings.json - cat settings.json - workingDirectory: $(TEST_PATH) - displayName: "Generate a settings.json file" - - - bash : | - echo "Downloading the kind script" - curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.11.1/kind-linux-amd64 - chmod +x ./kind - ./kind create cluster - displayName: "Create and Start the Kind cluster" - - - bash: | - curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash - displayName: "Upgrade az to latest version" - - - task: AzureCLI@2 - displayName: Bootstrap - inputs: - azureSubscription: AzureResourceConnection - scriptType: pscore - scriptLocation: inlineScript - inlineScript: | - .\Bootstrap.ps1 -CI - workingDirectory: $(TEST_PATH) - - - task: AzureCLI@2 - displayName: Run the Test Suite Public Extensions Only - inputs: - azureSubscription: AzureResourceConnection - scriptType: pscore - scriptLocation: inlineScript - inlineScript: | - .\Test.ps1 -CI -OnlyPublicTests -Type k8s-extension - workingDirectory: $(TEST_PATH) - continueOnError: true - condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'False')) - - - task: AzureCLI@2 - displayName: Run the Test Suite on Private + Public Extensions - inputs: - azureSubscription: AzureResourceConnection - scriptType: pscore - scriptLocation: inlineScript - inlineScript: | - .\Test.ps1 -CI -Type k8s-extension - workingDirectory: $(TEST_PATH) - continueOnError: true - condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'True')) - - - task: PublishTestResults@2 - inputs: - testResultsFormat: 'JUnit' - testResultsFiles: '**/testing/results/*.xml' - failTaskOnFailedTests: true - condition: succeededOrFailed() - - - task: AzureCLI@2 - displayName: Cleanup - inputs: - azureSubscription: AzureResourceConnection - scriptType: pscore - scriptLocation: inlineScript - inlineScript: | - .\Cleanup.ps1 -CI - workingDirectory: $(TEST_PATH) - condition: succeededOrFailed() - - - job: BuildPublishExtension - pool: - vmImage: 'ubuntu-latest' - displayName: "Build and Publish the Extension Artifact" - variables: - CLI_REPO_PATH: $(Agent.BuildDirectory)/s - steps: - - bash: | - echo "Using the private preview of k8s-extension to build..." - - cp $(CLI_REPO_PATH)/src/k8s-extension $(CLI_REPO_PATH)/src/k8s-extension-private -r - mv $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension_private - cp $(CLI_REPO_PATH)/src/k8s-extension-private/setup_private.py $(CLI_REPO_PATH)/src/k8s-extension-private/setup.py - cp $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension_private/consts_private.py $(CLI_REPO_PATH)/src/k8s-extension-private/azext_k8s_extension_private/consts.py - - EXTENSION_NAME="k8s-extension-private" - EXTENSION_FILE_NAME="k8s_extension_private" - - echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" - echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" - condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'True')) - displayName: "Copy Files, Set Variables for k8s-extension-private" - - bash: | - echo "Using the public version of k8s-extension to build..." - - EXTENSION_NAME="k8s-extension" - EXTENSION_FILE_NAME="k8s_extension" - - echo "##vso[task.setvariable variable=EXTENSION_NAME]$EXTENSION_NAME" - echo "##vso[task.setvariable variable=EXTENSION_FILE_NAME]$EXTENSION_FILE_NAME" - condition: and(succeeded(), eq(variables['IS_PRIVATE_BRANCH'], 'False')) - displayName: "Copy Files, Set Variables for k8s-extension" - - task: UsePythonVersion@0 - displayName: 'Use Python 3.6' - inputs: - versionSpec: 3.6 - - bash: | - set -ev - echo "Building extension ${EXTENSION_NAME}..." - - # prepare and activate virtualenv - pip install virtualenv - python3 -m venv env/ - source env/bin/activate - - # clone azure-cli - git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli - - pip install --upgrade pip - pip install -q azdev - - ls $(CLI_REPO_PATH) - - azdev --version - azdev setup -c ../azure-cli -r $(CLI_REPO_PATH) -e $(EXTENSION_NAME) - azdev extension build $(EXTENSION_NAME) - workingDirectory: $(CLI_REPO_PATH) - displayName: "Setup and Build Extension with azdev" - - task: PublishBuildArtifacts@1 - inputs: - pathToPublish: $(CLI_REPO_PATH)/dist - -- stage: AzureCLIOfficial - displayName: "Azure Official CLI Code Checks" - dependsOn: [] - jobs: - - job: CheckLicenseHeader - displayName: "Check License" - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.6' - inputs: - versionSpec: 3.6 - - bash: | - set -ev - - # prepare and activate virtualenv - python -m venv env/ - - chmod +x ./env/bin/activate - source ./env/bin/activate - - # clone azure-cli - git clone -q --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli - - pip install --upgrade pip - pip install -q azdev - - azdev setup -c ../azure-cli -r ./ - - azdev --version - az --version - - azdev verify license - - - job: StaticAnalysis - displayName: "Static Analysis" - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.6' - inputs: - versionSpec: 3.6 - - bash: pip install wheel==0.30.0 pylint==1.9.5 flake8==3.5.0 requests - displayName: 'Install wheel, pylint, flake8, requests' - - bash: python scripts/ci/source_code_static_analysis.py - displayName: "Static Analysis" - - - job: IndexVerify - displayName: "Verify Extensions Index" - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.7' - inputs: - versionSpec: 3.7 - - bash: | - #!/usr/bin/env bash - set -ev - pip install wheel==0.30.0 requests packaging - export CI="ADO" - python ./scripts/ci/test_index.py -v - displayName: "Verify Extensions Index" - - - job: SourceTests - displayName: "Integration Tests, Build Tests" - pool: - vmImage: 'ubuntu-latest' - strategy: - matrix: - Python36: - python.version: '3.6' - Python38: - python.version: '3.8' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python $(python.version)' - inputs: - versionSpec: '$(python.version)' - - bash: pip install wheel==0.30.0 - displayName: 'Install wheel==0.30.0' - - bash: ./scripts/ci/test_source.sh - displayName: 'Run integration test and build test' - env: - ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) - ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) - - - job: LintModifiedExtensions - displayName: "CLI Linter on Modified Extensions" - pool: - vmImage: 'ubuntu-latest' - steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.6' - inputs: - versionSpec: 3.6 - - bash: | - set -ev - - # prepare and activate virtualenv - pip install virtualenv - python -m virtualenv venv/ - source ./venv/bin/activate - - # clone azure-cli - git clone --single-branch -b dev https://github.com/Azure/azure-cli.git ../azure-cli - - pip install --upgrade pip - pip install azdev - - azdev --version - - azdev setup -c ../azure-cli -r ./ -e k8s-extension - - # overwrite the default AZURE_EXTENSION_DIR set by ADO - AZURE_EXTENSION_DIR=~/.azure/cliextensions az --version - - AZURE_EXTENSION_DIR=~/.azure/cliextensions azdev linter --include-whl-extensions k8s-extension - displayName: "CLI Linter on Modified Extension" - env: - ADO_PULL_REQUEST_LATEST_COMMIT: $(System.PullRequest.SourceCommitId) - ADO_PULL_REQUEST_TARGET_BRANCH: $(System.PullRequest.TargetBranch) \ No newline at end of file diff --git a/testing/.gitignore b/testing/.gitignore deleted file mode 100644 index 5687a0bf32d..00000000000 --- a/testing/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -settings.json -tmp/ -bin/* -!bin/connectedk8s-1.0.0-py3-none-any.whl -!bin/k8s_extension-0.4.0-py3-none-any.whl -!bin/k8s_extension_private-0.1.0-py3-none-any.whl -!bin/k8s_configuration-1.0.0-py3-none-any.whl -!bin/connectedk8s-values.yaml -*.xml \ No newline at end of file diff --git a/testing/Bootstrap.ps1 b/testing/Bootstrap.ps1 deleted file mode 100644 index 0598b139c79..00000000000 --- a/testing/Bootstrap.ps1 +++ /dev/null @@ -1,80 +0,0 @@ -param ( - [switch] $SkipInstall, - [switch] $CI -) - -# Disable confirm prompt for script -az config set core.disable_confirm_prompt=true - -# Configuring the environment -$ENVCONFIG = Get-Content -Path $PSScriptRoot/settings.json | ConvertFrom-Json - -az account set --subscription $ENVCONFIG.subscriptionId - -if (-not (Test-Path -Path $PSScriptRoot/tmp)) { - New-Item -ItemType Directory -Path $PSScriptRoot/tmp -} - -if (!$SkipInstall) { - Write-Host "Removing the old connnectedk8s extension..." - az extension remove -n connectedk8s - Write-Host "Installing connectedk8s..." - az extension add -n connectedk8s - if (!$?) { - Write-Host "Unable to install connectedk8s, exiting..." - exit 1 - } -} - -Write-Host "Onboard cluster to Azure...starting!" - -az group show --name $envConfig.resourceGroup -if (!$?) { - Write-Host "Resource group does not exist, creating it now in region 'eastus2euap'" - az group create --name $envConfig.resourceGroup --location eastus2euap - - if (!$?) { - Write-Host "Failed to create Resource Group - exiting!" - Exit 1 - } -} - -# Skip creating the AKS Cluster if this is CI -if (!$CI) { - az aks show -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.aksClusterName - if (!$?) { - Write-Host "Cluster does not exist, creating it now" - az aks create -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.aksClusterName --generate-ssh-keys - } else { - Write-Host "Cluster already exists, no need to create it." - } - - Write-Host "Retrieving credentials for your AKS cluster..." - - az aks get-credentials -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.aksClusterName -f tmp/KUBECONFIG - if (!$?) - { - Write-Host "Cluster did not create successfully, exiting!" -ForegroundColor Red - Exit 1 - } - Write-Host "Successfully retrieved the AKS kubectl credentials" -} else { - Copy-Item $HOME/.kube/config -Destination $PSScriptRoot/tmp/KUBECONFIG -} - -az connectedk8s show -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.arcClusterName -if ($?) -{ - Write-Host "Cluster is already connected, no need to re-connect" - Exit 0 -} - -Write-Host "Connecting the cluster to Arc with connectedk8s..." -$Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" -az connectedk8s connect -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.arcClusterName -if (!$?) -{ - kubectl get pods -A - Exit 1 -} -Write-Host "Successfully onboarded the cluster to Azure" \ No newline at end of file diff --git a/testing/Cleanup.ps1 b/testing/Cleanup.ps1 deleted file mode 100644 index cef1e2d1bc3..00000000000 --- a/testing/Cleanup.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -param ( - [switch] $CI -) - -# Disable confirm prompt for script -az config set core.disable_confirm_prompt=true - -$ENVCONFIG = Get-Content -Path $PSScriptRoot/settings.json | ConvertFrom-Json - -az account set --subscription $ENVCONFIG.subscriptionId - -$Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" -Write-Host "Removing the connectedk8s arc agents from the cluster..." -az connectedk8s delete -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.arcClusterName -if (!$?) -{ - kubectl get pods -A - kubectl logs -l app.kubernetes.io/component=cluster-metadata-operator -n azure-arc -c cluster-metadata-operator - Exit 0 -} - -# Skip deleting the AKS Cluster if this is CI -if (!$CI) { - Write-Host "Deleting the AKS cluster from Azure..." - az aks delete -g $ENVCONFIG.resourceGroup -n $ENVCONFIG.aksClusterName - if (Test-Path -Path $PSScriptRoot/tmp) { - Write-Host "Deleting the tmp directory from the test directory" - Remove-Item -Path $PSScriptRoot/tmp -Force -Confirm:$false - } -} \ No newline at end of file diff --git a/testing/README.md b/testing/README.md deleted file mode 100644 index 2c2d48070bd..00000000000 --- a/testing/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# K8s Partner Extension Test Suite - -This repository serves as the integration testing suite for the `k8s-extension` Azure CLI module. - -## Testing Requirements - -All partners who wish to merge their __Custom Private Preview Release__ (owner: _Partner_) into the __Official Private Preview Release__ are required to author additional integration tests for their extension to ensure that their extension will continue to function correctly as more extensions are added into the __Official Private Preview Release__. - -For more information on creating these tests, see [Authoring Tests](docs/test_authoring.md) - -## Pre-Requisites - -In order to properly test all regression tests within the test suite, you must onboard an AKS cluster which you will use to generate your Azure Arc resource to test the extensions. Ensure that you have a resource group where you can onboard this cluster. - -### Required Installations - -The following installations are required in your environment for the integration tests to run correctly: - -1. [Helm 3](https://helm.sh/docs/intro/install/) -2. [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) -3. [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) - -## Setup - -### Step 1: Install Pester - -This project contains [Pester](https://pester.dev/) test framework commands that are required for the integration tests to run. In an admin powershell terminal, run - -```powershell -Install-Module Pester -Force -SkipPublisherCheck -Import-Module Pester -PassThru -``` - -If you run into issues installing the framework, refer to the [Installation Guide](https://pester.dev/docs/introduction/installation) provided by the Pester docs. - -### Step 2: Get Test suite files - -You can either clone this repo (preferred option, since you will be adding your tests to this suite) or copy the files in this repo locally. Rest of the instructions here assume your working directory is k8spartner-extension-testing. - -### Step 3: Update the `k8s-extension`/`k8s-extension-private` .whl package - -This integration test suite references the .whl packages found in the `\bin` directory. After generating your `k8s-extension`/`k8s-extension-private` .whl package, copy your updated package into the `\bin` directory. - -### Step 4: Create a `settings.json` - -To onboard the AKS and Arc clusters correctly, you will need to create a `settings.json` configuration. Create a new `settings.json` file by copying the contents of the `settings.template.json` into this file. Update the subscription id, resource group, and AKS and Arc cluster name fields with your specific values. - -### Step 5: Update the extension version value in `settings.json` - -To ensure that the tests point to your `k8s-extension-private` `.whl` package, change the value of the `k8s-extension-private` to match your package versioning in the format (Major.Minor.Patch.Extension). For example, the `k8s_extension_private-0.1.0.openservicemesh_5-py3-none-any.whl` whl package would have extension versions set to -```json -{ - "k8s-extension": "0.1.0", - "k8s-extension-private": "0.1.0.openservicemesh_5", - "connectedk8s": "0.3.5" -} - -``` - -_Note: Updates to the `connectedk8s` version and `k8s-extension` version can also be made by adding a different version of the `connectedk8s` and `k8s-extension` whl packages and changing the `connectedk8s` and `k8s-extension` values to match the (Major.Minor.Patch) version format shown above_ - -### Step 6: Run the Bootstrap Command -To bootstrap the environment with AKS and Arc clusters, run -```powershell -.\Bootstrap.ps1 -``` -This script will provision the AKS and Arc clusters needed to run the integration test suite - -## Testing - -### Testing All Extension Suites -To test all extension test suites, you must call `.\Test.ps1` with the `-ExtensionType` parameter set to either `Public` or `Private`. Based on this flag, the test suite will install the extension type specified below - -| `-ExtensionType` | Installs `az extension` | -| ---------------- | --------------------- | -| `Public` | `k8s-extension` | -| `Private` | `k8s-extension-private` | - -For example, when calling -```bash -.\Test.ps1 -ExtensionType Public -``` -the script will install your `k8s-extension` whl package and run the full test suite of `*.Tests.ps1` files included in the `\test\extensions` directory - -### Testing Public Extensions Only -If you only want to run the test cases against public-preview or GA extension test cases, you can use the `-OnlyPublicTests` flag to specify this -```bash -.\Test.ps1 -ExtensionType Public -OnlyPublicTests -``` - -### Testing Specific Extension Suite - -If you only want to run the test script on your specific test file, you can do so by specifying path to your extension test suite in the execution call - -```powershell -.\Test.ps1 -Path -``` -For example to call the `AzureMonitor.Tests.ps1` test suite, we run -```powershell -.\Test.ps1 -ExtensionType Public -Path .\test\extensions\public\AzureMonitor.Tests.ps1 -``` - -### Skipping Extension Re-Install - -By default the `Test.ps1` script will uninstall any old versions of `k8s-extension`/'`k8s-extension-private` and re-install the version specified in `settings.json`. If you do not want this re-installation to occur, you can specify the `-SkipInstall` flag to skip this process. - -```powershell -.\Test.ps1 -ExtensionType Public -SkipInstall -``` - -## Cleanup -To cleanup the AKS and Arc clusters you have provisioned in testing, run -```powershell -.\Cleanup.ps1 -``` -This will remove the AKS and Arc clusters as well as the `\tmp` directory that were created by the bootstrapping script. \ No newline at end of file diff --git a/testing/Test.ps1 b/testing/Test.ps1 deleted file mode 100644 index 87e746d433f..00000000000 --- a/testing/Test.ps1 +++ /dev/null @@ -1,119 +0,0 @@ -param ( - [string] $Path, - [switch] $SkipInstall, - [switch] $CI, - [switch] $OnlyPublicTests, - - [Parameter(Mandatory=$True)] - [ValidateSet('k8s-extension','k8s-configuration', 'k8s-extension-private')] - [string]$Type -) - -# Disable confirm prompt for script -# Only show errors, don't show warnings -az config set core.disable_confirm_prompt=true -az config set core.only_show_errors=true - -$ENVCONFIG = Get-Content -Path $PSScriptRoot/settings.json | ConvertFrom-Json - -az account set --subscription $ENVCONFIG.subscriptionId - -$Env:KUBECONFIG="$PSScriptRoot/tmp/KUBECONFIG" -$TestFileDirectory="$PSScriptRoot/results" - -if (-not (Test-Path -Path $TestFileDirectory)) { - New-Item -ItemType Directory -Path $TestFileDirectory -} - -if ($Type -eq 'k8s-extension') { - $k8sExtensionVersion = $ENVCONFIG.extensionVersion.'k8s-extension' - $Env:K8sExtensionName = "k8s-extension" - - if (!$SkipInstall) { - Write-Host "Removing the old k8s-extension extension..." - az extension remove -n k8s-extension - Write-Host "Installing k8s-extension version $k8sExtensionVersion..." - az extension add --source ./bin/k8s_extension-$k8sExtensionVersion-py3-none-any.whl - if (!$?) { - Write-Host "Unable to find k8s-extension version $k8sExtensionVersion, exiting..." - exit 1 - } - } - if ($OnlyPublicTests) { - $testFilePath = "$PSScriptRoot/test/extensions/public" - } else { - $testFilePath = "$PSScriptRoot/test/extensions" - } -} elseif ($Type -eq 'k8s-extension-private') { - $k8sExtensionPrivateVersion = $ENVCONFIG.extensionVersion.'k8s-extension-private' - $Env:K8sExtensionName = "k8s-extension-private" - - if (!$SkipInstall) { - Write-Host "Removing the old k8s-extension-private extension..." - az extension remove -n k8s-extension-private - Write-Host "Installing k8s-extension-private version $k8sExtensionPrivateVersion..." - az extension add --source ./bin/k8s_extension_private-$k8sExtensionPrivateVersion-py3-none-any.whl - if (!$?) { - Write-Host "Unable to find k8s-extension-private version $k8sExtensionPrivateVersion, exiting..." - exit 1 - } - } - if ($OnlyPublicTests) { - $testFilePath = "$PSScriptRoot/test/extensions/public" - } else { - $testFilePath = "$PSScriptRoot/test/extensions" - } -} elseif ($Type -eq 'k8s-configuration') { - $k8sConfigurationVersion = $ENVCONFIG.extensionVersion.'k8s-configuration' - if (!$SkipInstall) { - Write-Host "Removing the old k8s-configuration extension..." - az extension remove -n k8s-configuration - Write-Host "Installing k8s-configuration version $k8sConfigurationVersion..." - az extension add --source ./bin/k8s_configuration-$k8sConfigurationVersion-py3-none-any.whl - } - $testFilePath = "$PSScriptRoot/test/configurations" -} - -if ($CI) { - # This runs the tests in parallel during the CI pipline to speed up testing - - Write-Host "Invoking Pester to run tests from '$testFilePath'..." - $testFiles = Get-ChildItem $testFilePath - $resultFileNumber = 0 - foreach ($testFile in $testFiles) - { - $resultFileNumber++ - $testName = Split-Path $testFile –leaf - Start-Job -ArgumentList $testName, $testFile, $resultFileNumber, $TestFileDirectory -Name $testName -ScriptBlock { - param($name, $testFile, $resultFileNumber, $testFileDirectory) - - Write-Host "$testFile to result file #$resultFileNumber" - $testResult = Invoke-Pester $testFile -Passthru -Output Detailed - $testResult | Export-JUnitReport -Path "$testFileDirectory/$name.xml" - } - } - - do { - Write-Host ">> Still running tests @ $(Get-Date –Format "HH:mm:ss")" –ForegroundColor Blue - Get-Job | Where-Object { $_.State -eq "Running" } | Format-Table –AutoSize - Start-Sleep –Seconds 30 - } while((Get-Job | Where-Object { $_.State -eq "Running" } | Measure-Object).Count -ge 1) - - Get-Job | Wait-Job - $failedJobs = Get-Job | Where-Object { -not ($_.State -eq "Completed")} - Get-Job | Receive-Job –AutoRemoveJob –Wait –ErrorAction 'Continue' - - if ($failedJobs.Count -gt 0) { - Write-Host "Failed Jobs" –ForegroundColor Red - $failedJobs - throw "One or more tests failed" - } -} else { - if ($Path) { - Write-Host "Invoking Pester to run tests from '$PSScriptRoot/$Path'" - Invoke-Pester -Output Detailed $PSScriptRoot/$Path - } else { - Write-Host "Invoking Pester to run tests from '$testFilePath'..." - Invoke-Pester -Output Detailed $testFilePath - } -} diff --git a/testing/bin/k8s_configuration-1.0.0-py3-none-any.whl b/testing/bin/k8s_configuration-1.0.0-py3-none-any.whl deleted file mode 100644 index cc8e8e0995f81da31f6f919f83d344cc164e9568..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42351 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%0De{h@2i2mo+>9hAiTOe4VIP9}qG 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 diff --git a/testing/docs/test_authoring.md b/testing/docs/test_authoring.md deleted file mode 100644 index b41286ba66d..00000000000 --- a/testing/docs/test_authoring.md +++ /dev/null @@ -1,142 +0,0 @@ -# Test Authoring - -All partners are _required_ to author additional integration tests when merging their extension into the __Official Private Preview Release__. The information below outlines how to setup and author these additional tests. - -## Requirements - -All partners are required to cover standard CLI scenarios in your extensions testing suite. When adding these tests and preparing to merge your updated extension whl package, your tests along with the other tests in the test suite must pass at 100%. - -Standard CLI scenarios include: - -1. `az k8s-extension create` -2. `az k8s-extension show` -3. `az k8s-extension list` -4. `az k8s-extension update` -5. `az k8s-extension delete` - -In addition to these standard scenarios, if there are any rigorous parameter validation standards, these should also be included in this test suite. - -## Setup - -The setup process for test authoring is the same as setup for generic testing. See [Setup](../README.md#setup) for guidance. - -## Writing Tests - -This section outlines the common flow for creating and running additional extension integration tests for the `k8s-extension` package. - -The suite utilizes the [Pester](https://pester.dev/) framework. For more information on creating generic Pester tests, see the [Create a Pester Test](https://pester.dev/docs/quick-start#creating-a-pester-test) section in the Pester docs. - -### Step 1: Create Test File - -To create an integration test suite for your extension, create an extension test file in the format `.Tests.ps1` and place the file in one of the following directories -| Extension Type | Directory | -| ---------------------- | ----------------------------------- | -| General Availability | .\test\extensions\public | -| Public Preview | .\test\extensions\public | -| Private Preview | .\test\extensions\private-preview | - -For example, to create a test suite file for the Azure Monitor extension, I create the file `AzureMonitor.Tests.ps1` in the `\test\extensions\public` directory because Container Insights extension is in _Public Preview_. - -### Step 2: Setup Global Variables - -All test suite files must have the following structure for importing the environment config and declaring globals - -```powershell -Describe ' Testing' { - BeforeAll { - $extensionType = "" - $extensionName = "" - $extensionAgentName = "" - $extensionAgentNamespace = "" - - . $PSScriptRoot/../../helper/Constants.ps1 - . $PSScriptRoot/../../helper/Helper.ps1 - } -} -``` - -You can declare additional global variables for your tests by adding additional powershell variable to this `BeforeAll` block. - -_Note: Commonly used constants used by all extension test suites are stored in the `Constants.ps1` file_ - -### Step 3: Add Tests - -Adding tests to the test suite can now be performed by adding `It` blocks to the outer `Describe` block. For instance to test create on a extension in the case of AzureMonitor, I write the following test: - -```powershell -Describe 'Azure Monitor Testing' { - BeforeAll { - $extensionType = "microsoft.azuremonitor.containers" - $extensionName = "azuremonitor-containers" - $extensionAgentName = "omsagent" - $extensionAgentNamespace = "kube-system" - - . $PSScriptRoot/../../helper/Constants.ps1 - . $PSScriptRoot/../../helper/Helper.ps1 - } - - It 'Creates the extension and checks that it onboards correctly' { - $output = az k8s-extension create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --extension-type $extensionType -n $extensionName - $? | Should -BeTrue - - $output = az k8s-extension show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - $? | Should -BeTrue - - $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue - - # Loop and retry until the extension installs - $n = 0 - do - { - if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { - if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { - break - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } -} -``` - -The above test calls `az k8s-extension create` to create the `azuremonitor-containers` extension and retries checking that the extension resource was actually created on the Arc cluster and that the extension status successfully returns `$SUCCESS_MESSAGE` which is equivalent to `Successfully installed the extension`. - -## Tips/Notes - -### Accessing Extension Data - -`.\Test.ps1` assumes that the user has `kubectl` and `az` installed in their environment; therefore, tests are able to access information on the extension at the service and on the arc cluster. For instance, in the above test, we access the `extensionconfig` CRDs on the arc cluster by calling - -```powershell -kubectl get extensionconfigs -A -o json -``` - -If we want to access the extension data on the cluster with a specific `$extensionName`, we run - -```powershell -(kubectl get extensionconfigs -A -o json).items | Where-Object { $_.metadata.name -eq $extensionName } -``` - -Because some of these commands are so common, we provide the following helper commands in the `test\Helper.ps1` file - -| Command | Description | -| ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | -| Get-ExtensionData | Retrieves the ExtensionConfig CRD in JSON format with `.meatadata.name` matching the `extensionName` | -| Get-ExtensionStatus | Retrieves the `.status.status` from the ExtensionConfig CRD with `.meatadata.name` matching the `extensionName` | -| Get-PodStatus -Namespace | Retrieves the `status.phase` from the first pod on the cluster with `.metadata.name` matching `extensionName` | - -### Stdout for Debugging - -To print out to the Console for debugging while writing your test cases use the `Write-Host` command. If you attempt to use the `Write-Output` command, it will not show because of the way that Pester is invoked - -```powershell -Write-Host "Some example output" -``` - -### Global Constants - -Looking at the above test, we can see that we are accessing the `ENVCONFIG` to retrieve the environment variables from the `settings.json`. All variables in the `settings.json` are accessible from the `ENVCONFIG`. The most useful ones for testing will be `ENVCONFIG.arcClusterName` and `ENVCONFIG.resourceGroup`. - diff --git a/testing/owners.txt b/testing/owners.txt deleted file mode 100644 index ead6f446410..00000000000 --- a/testing/owners.txt +++ /dev/null @@ -1,2 +0,0 @@ -joinnis -nanthi \ No newline at end of file diff --git a/testing/settings.template.json b/testing/settings.template.json deleted file mode 100644 index 5129dbd0a20..00000000000 --- a/testing/settings.template.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "subscriptionId": "", - "resourceGroup": "", - "aksClusterName": "", - "arcClusterName": "", - - "extensionVersion": { - "k8s-extension": "0.3.0", - "k8s-extension-private": "0.1.0" - } -} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.HTTPS.Tests.ps1 b/testing/test/configurations/Configuration.HTTPS.Tests.ps1 deleted file mode 100644 index a2dee2b348f..00000000000 --- a/testing/test/configurations/Configuration.HTTPS.Tests.ps1 +++ /dev/null @@ -1,54 +0,0 @@ -Describe 'Source Control Configuration (HTTPS) Testing' { - BeforeAll { - $configurationName = "https-config" - . $PSScriptRoot/Constants.ps1 - . $PSScriptRoot/Helper.ps1 - - $dummyValue = "dummyValue" - $secretName = "git-auth-$configurationName" - } - - It 'Creates a configuration with https user and https key on the cluster' { - $output = az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u "https://github.com/Azure/arc-k8s-demo" -n $configurationName --scope cluster --https-user $dummyValue --https-key $dummyValue --operator-namespace $configurationName - $? | Should -BeTrue - - # Loop and retry until the configuration installs and helm pod comes up - $n = 0 - do - { - if (Get-ConfigStatus $configurationName -eq $SUCCESS_MESSAGE) { - if (Get-PodStatus $configurationName -Namespace $configurationName -eq $POD_RUNNING ) { - break - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - - Secret-Exists $secretName -Namespace $configurationName - } - - It "Lists the configurations on the cluster" { - $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $? | Should -BeTrue - - $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } - $configExists | Should -Not -BeNullOrEmpty - } - - It "Deletes the configuration from the cluster" { - az k8s-configuration delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeTrue - - # Configuration should be removed from the resource model - az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeFalse - } - - It "Performs another list after the delete" { - $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } - $configExists | Should -BeNullOrEmpty - } -} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.HelmOperator.Tests.ps1 b/testing/test/configurations/Configuration.HelmOperator.Tests.ps1 deleted file mode 100644 index 8b89ba24c58..00000000000 --- a/testing/test/configurations/Configuration.HelmOperator.Tests.ps1 +++ /dev/null @@ -1,137 +0,0 @@ -Describe 'Source Control Configuration (Helm Operator Properties) Testing' { - BeforeAll { - $configurationName = "helm-enabled-config" - . $PSScriptRoot/Constants.ps1 - . $PSScriptRoot/Helper.ps1 - - $customOperatorParams = "--set helm.versions=v3 --set mycustomhelmvalue=yay" - $customChartVersion = "0.6.0" - } - - It 'Creates a configuration with helm enabled on the cluster' { - $output = az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u "https://github.com/Azure/arc-k8s-demo" -n $configurationName --scope cluster --enable-helm-operator --operator-namespace $configurationName --helm-operator-params "--set helm.versions=v3" - $? | Should -BeTrue - - # Loop and retry until the configuration installs and helm pod comes up - $n = 0 - do - { - if (Get-ConfigStatus $configurationName -eq $SUCCESS_MESSAGE) { - if (Get-PodStatus "$configurationName-helm" -Namespace $configurationName -eq $POD_RUNNING ) { - break - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Updates the helm operator params and performs a show" { - Set-ItResult -Skipped -Because "Update is not a valid scenario for now" - - az k8s-configuration update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -n $configurationName --helm-operator-params $customOperatorParams - $? | Should -BeTrue - - $output = az k8s-configuration show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeTrue - - $configData = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } - ($configData.helmOperatorProperties.chartValues -eq $customOperatorParams) | Should -BeTrue - - # Loop and retry until the configuration updates - $n = 0 - do - { - $helmOperatorChartValues = (Get-ConfigData $configurationName).spec.helmOperatorProperties.chartValues - if ($helmOperatorChartValues -ne $null -And $helmOperatorChartValues.ToString() -eq $customOperatorParams) { - if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { - break - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Updates the helm operator chart version and performs a show" { - Set-ItResult -Skipped -Because "Update is not a valid scenario for now" - - az k8s-configuration update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -n $configurationName --helm-operator-chart-version $customChartVersion - $? | Should -BeTrue - - $output = az k8s-configuration show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeTrue - - # Check that the helmOperatorProperties chartValues didn't change - $configData = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } - ($configData.helmOperatorProperties.chartValues -eq $customOperatorParams) | Should -BeTrue - ($configData.helmOperatorProperties.chartVersion -eq $customChartVersion) | Should -BeTrue - - # Loop and retry until the configuration updates - $n = 0 - do - { - $helmOperatorChartVersion = (Get-ConfigData $configurationName).spec.helmOperatorProperties.chartVersion - if ($helmOperatorChartVersion -ne $null -And $helmOperatorChartVersion.ToString() -eq $customChartVersion) { - if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { - break - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Disables the helm operator on the cluster" { - Set-ItResult -Skipped -Because "Update is not a valid scenario for now" - - az k8s-configuration update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -n $configurationName --enable-helm-operator=false - $? | Should -BeTrue - - $output = az k8s-configuration show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeTrue - - $helmOperatorEnabled = ($output | ConvertFrom-Json).enableHelmOperator - $helmOperatorEnabled.ToString() -eq "False" | Should -BeTrue - - # Loop and retry until the configuration updates - $n = 0 - do { - $helmOperatorEnabled = (Get-ConfigData $configurationName).spec.enableHelmOperator - if ($helmOperatorEnabled -ne $null -And $helmOperatorEnabled.ToString() -eq "False") { - if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { - break - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Lists the configurations on the cluster" { - $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $? | Should -BeTrue - - $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } - $configExists | Should -Not -BeNullOrEmpty - } - - It "Deletes the configuration from the cluster" { - az k8s-configuration delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeTrue - - # Configuration should be removed from the resource model - az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeFalse - } - - It "Performs another list after the delete" { - $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } - $configExists | Should -BeNullOrEmpty - } -} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.KnownHost.Tests.ps1 b/testing/test/configurations/Configuration.KnownHost.Tests.ps1 deleted file mode 100644 index 2cb2946bc3e..00000000000 --- a/testing/test/configurations/Configuration.KnownHost.Tests.ps1 +++ /dev/null @@ -1,6 +0,0 @@ -Describe 'Source Control Configuration (SSH Configs) Testing' { - BeforeAll { - . $PSScriptRoot/Constants.ps1 - . $PSScriptRoot/Helper.ps1 - } -} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.PrivateKey.Tests.ps1 b/testing/test/configurations/Configuration.PrivateKey.Tests.ps1 deleted file mode 100644 index 4bf86d52012..00000000000 --- a/testing/test/configurations/Configuration.PrivateKey.Tests.ps1 +++ /dev/null @@ -1,86 +0,0 @@ -Describe 'Source Control Configuration (SSH Configs) Testing' { - BeforeAll { - . $PSScriptRoot/Constants.ps1 - . $PSScriptRoot/Helper.ps1 - - $RSA_KEYPATH = "$TMP_DIRECTORY\rsa.private" - $DSA_KEYPATH = "$TMP_DIRECTORY\dsa.private" - $ECDSA_KEYPATH = "$TMP_DIRECTORY\ecdsa.private" - $ED25519_KEYPATH = "$TMP_DIRECTORY\ed25519.private" - - $KEY_ARR = [System.Tuple]::Create("rsa", $RSA_KEYPATH), [System.Tuple]::Create("dsa", $DSA_KEYPATH), [System.Tuple]::Create("ecdsa", $ECDSA_KEYPATH), [System.Tuple]::Create("ed25519", $ED25519_KEYPATH) - foreach ($keyTuple in $KEY_ARR) { - # Automattically say yes to overwrite with ssh-keygen - Write-Output "y" | ssh-keygen -t $keyTuple.Item1 -f $keyTuple.Item2 -P """" - } - - $SSH_GIT_URL = "git://github.com/anubhav929/flux-get-started.git" - $HTTP_GIT_URL = "https://github.com/Azure/arc-k8s-demo" - - $configDataRSA = [System.Tuple]::Create("rsa-config", $RSA_KEYPATH) - $configDataDSA = [System.Tuple]::Create("dsa-config", $DSA_KEYPATH) - $configDataECDSA = [System.Tuple]::Create("ecdsa-config", $ECDSA_KEYPATH) - $configDataED25519 = [System.Tuple]::Create("ed25519-config", $ED25519_KEYPATH) - - $CONFIG_ARR = $configDataRSA, $configDataDSA, $configDataECDSA, $configDataED25519 - } - - It 'Creates a configuration with each type of ssh private key' { - foreach($configData in $CONFIG_ARR) { - az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u $SSH_GIT_URL -n $configData.Item1 --scope cluster --operator-namespace $configData.Item1 --ssh-private-key-file $configData.Item2 - $? | Should -BeTrue - } - - # Loop and retry until the configuration installs and helm pod comes up - $n = 0 - do - { - $readyConfigs = 0 - foreach($configData in $CONFIG_ARR) { - # TODO: Change this to checking the success message after we merge in the bugfix into the agent - if (Get-PodStatus $configData.Item1 -Namespace $configData.Item1 -eq $POD_RUNNING) { - $readyConfigs += 1 - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le 30 -And $readyConfigs -ne 4) - $n | Should -BeLessOrEqual 30 - } - - It 'Fails when trying to create a configuration with ssh url and https auth values' { - az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u $HTTP_GIT_URL -n "config-should-fail" --scope cluster --operator-namespace "config-should-fail" --ssh-private-key-file $RSA_KEYPATH - $? | Should -BeFalse - } - - It "Lists the configurations on the cluster" { - $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $? | Should -BeTrue - - foreach ($configData in $CONFIG_ARR) { - $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configData.Item1 } - $configExists | Should -Not -BeNullOrEmpty - } - } - - It "Deletes the configuration from the cluster" { - foreach ($configData in $CONFIG_ARR) { - az k8s-configuration delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configData.Item1 - $? | Should -BeTrue - - # Configuration should be removed from the resource model - az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configData.Item1 - $? | Should -BeFalse - } - } - - It "Performs another list after the delete" { - $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $? | Should -BeTrue - - foreach ($configData in $CONFIG_ARR) { - $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configData.Item1 } - $configExists | Should -BeNullOrEmpty - } - } -} \ No newline at end of file diff --git a/testing/test/configurations/Configuration.Tests.ps1 b/testing/test/configurations/Configuration.Tests.ps1 deleted file mode 100644 index a85df42ed2e..00000000000 --- a/testing/test/configurations/Configuration.Tests.ps1 +++ /dev/null @@ -1,80 +0,0 @@ -Describe 'Basic Source Control Configuration Testing' { - BeforeAll { - $configurationName = "basic-config" - . $PSScriptRoot/Constants.ps1 - . $PSScriptRoot/Helper.ps1 - } - - It 'Creates a configuration and checks that it onboards correctly' { - az k8s-configuration create -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -u "https://github.com/Azure/arc-k8s-demo" -n $configurationName --scope cluster --enable-helm-operator=false --operator-namespace $configurationName - $? | Should -BeTrue - - # Loop and retry until the configuration installs - $n = 0 - do - { - if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { - break - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Performs a show on the configuration" { - $output = az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type "connectedClusters" -n $configurationName - $? | Should -BeTrue - $output | Should -Not -BeNullOrEmpty - } - - It "Runs an update on the configuration on the cluster" { - Set-ItResult -Skipped -Because "Update is not a valid scenario for now" - - az k8s-configuration update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName --enable-helm-operator - $? | Should -BeTrue - - $output = az k8s-configuration show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeTrue - - $helmOperatorEnabled = ($output | ConvertFrom-Json).enableHelmOperator - $helmOperatorEnabled.ToString() -eq "True" | Should -BeTrue - - # Loop and retry until the configuration updates - $n = 0 - do { - $helmOperatorEnabled = (Get-ConfigData $configurationName).spec.enableHelmOperator - if ($helmOperatorEnabled -And $helmOperatorEnabled.ToString() -eq "True") { - if (Get-ConfigStatus $configurationName -Match $SUCCESS_MESSAGE) { - break - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Lists the configurations on the cluster" { - $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $? | Should -BeTrue - - $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } - $configExists | Should -Not -BeNullOrEmpty - } - - It "Deletes the configuration from the cluster" { - az k8s-configuration delete -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeTrue - - # Configuration should be removed from the resource model - az k8s-configuration show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $configurationName - $? | Should -BeFalse - } - - It "Performs another list after the delete" { - $output = az k8s-configuration list -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters - $configExists = $output | ConvertFrom-Json | Where-Object { $_.id -Match $configurationName } - $configExists | Should -BeNullOrEmpty - } -} \ No newline at end of file diff --git a/testing/test/configurations/Constants.ps1 b/testing/test/configurations/Constants.ps1 deleted file mode 100644 index f1e8c6ffdc3..00000000000 --- a/testing/test/configurations/Constants.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -$ENVCONFIG = Get-Content -Path $PSScriptRoot\..\..\settings.json | ConvertFrom-Json -$SUCCESS_MESSAGE = "Successfully installed the operator" -$FAILED_MESSAGE = "Failed the install of the operator" -$TMP_DIRECTORY = "$PSScriptRoot\..\..\tmp" - -$POD_RUNNING = "Running" - -$MAX_RETRY_ATTEMPTS = 18 \ No newline at end of file diff --git a/testing/test/configurations/Helper.ps1 b/testing/test/configurations/Helper.ps1 deleted file mode 100644 index 842e2da84aa..00000000000 --- a/testing/test/configurations/Helper.ps1 +++ /dev/null @@ -1,45 +0,0 @@ -function Get-ConfigData { - param( - [string]$configName - ) - - $output = kubectl get gitconfigs -A -o json | ConvertFrom-Json - return $output.items | Where-Object { $_.metadata.name -eq $configurationName } -} - -function Get-ConfigStatus { - param( - [string]$configName - ) - - $configData = Get-ConfigData $configName - if ($configData -ne $null) { - return $configData.status.status - } - return $null -} - -function Get-PodStatus { - param( - [string]$podName, - [string]$Namespace - ) - - $allPodData = kubectl get pods -n $Namespace -o json | ConvertFrom-Json - $podData = $allPodData.items | Where-Object { $_.metadata.name -Match $podName } - return $podData.status.phase -} - -function Secret-Exists { - param( - [string]$secretName, - [string]$Namespace - ) - - $allSecretData = kubectl get secrets -n $Namespace -o json | ConvertFrom-Json - $secretData = $allSecretData.items | Where-Object { $_.metadata.name -Match $secretName } - if ($secretData.Length -ge 1) { - return $true - } - return $false -} \ No newline at end of file diff --git a/testing/test/extensions/data/azure_ml/test_cert.pem b/testing/test/extensions/data/azure_ml/test_cert.pem deleted file mode 100644 index e7529e3fdea..00000000000 --- a/testing/test/extensions/data/azure_ml/test_cert.pem +++ /dev/null @@ -1 +0,0 @@ -testcert \ No newline at end of file diff --git a/testing/test/extensions/data/azure_ml/test_key.pem b/testing/test/extensions/data/azure_ml/test_key.pem deleted file mode 100644 index 7ef00201c75..00000000000 --- a/testing/test/extensions/data/azure_ml/test_key.pem +++ /dev/null @@ -1 +0,0 @@ -testkey \ No newline at end of file diff --git a/testing/test/extensions/private-preview/AzurePolicy.Tests.ps1 b/testing/test/extensions/private-preview/AzurePolicy.Tests.ps1 deleted file mode 100644 index 2793a2e179d..00000000000 --- a/testing/test/extensions/private-preview/AzurePolicy.Tests.ps1 +++ /dev/null @@ -1,100 +0,0 @@ -Describe 'Azure Policy Testing' { - BeforeAll { - $extensionType = "microsoft.policyinsights" - $extensionName = "policy" - $extensionAgentName = "azure-policy" - $extensionAgentNamespace = "kube-system" - - . $PSScriptRoot/../../helper/Constants.ps1 - . $PSScriptRoot/../../helper/Helper.ps1 - } - - It 'Creates the extension and checks that it onboards correctly' { - Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue - - # Loop and retry until the extension installs - $n = 0 - do - { - if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { - if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { - break - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Performs a show on the extension" { - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - $output | Should -Not -BeNullOrEmpty - } - - It "Runs an update on the extension on the cluster" { - Set-ItResult -Skipped -Because "Update is not a valid scenario for now" - - # az $Env:K8sExtensionName update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName --auto-upgrade-minor-version false - # $? | Should -BeTrue - - # $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - # $? | Should -BeTrue - - # $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - # $isAutoUpgradeMinorVersion.ToString() -eq "False" | Should -BeTrue - - # # Loop and retry until the extension config updates - # $n = 0 - # do - # { - # $isAutoUpgradeMinorVersion = (Get-ExtensionData $extensionName).spec.autoUpgradeMinorVersion - # if (!$isAutoUpgradeMinorVersion) { #autoUpgradeMinorVersion doesn't exist in ExtensionConfig CRD if false - # if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { - # if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { - # break - # } - # } - # } - # Start-Sleep -Seconds 10 - # $n += 1 - # } while ($n -le $MAX_RETRY_ATTEMPTS) - # $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Lists the extensions on the cluster" { - $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $output | Should -Not -BeNullOrEmpty - $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } - $extensionExists | Should -Not -BeNullOrEmpty - } - - It "Deletes the extension from the cluster" { - $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - # Extension should not be found on the cluster - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -Not -BeNullOrEmpty - $output | Should -BeNullOrEmpty - } - - It "Performs another list after the delete" { - $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - $output | Should -Not -BeNullOrEmpty - - $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } - $extensionExists | Should -BeNullOrEmpty - } -} diff --git a/testing/test/extensions/public/AzureDefender.Tests.ps1 b/testing/test/extensions/public/AzureDefender.Tests.ps1 deleted file mode 100644 index e60d443f620..00000000000 --- a/testing/test/extensions/public/AzureDefender.Tests.ps1 +++ /dev/null @@ -1,98 +0,0 @@ -Describe 'Azure Defender Testing' { - BeforeAll { - $extensionType = "microsoft.azuredefender.kubernetes" - $extensionName = "microsoft.azuredefender.kubernetes" - $extensionAgentNamespace = "azuredefender" - - . $PSScriptRoot/../../helper/Constants.ps1 - . $PSScriptRoot/../../helper/Helper.ps1 - } - - It 'Creates the extension and checks that it onboards correctly' { - Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue - - # Loop and retry until the extension installs - $n = 0 - do - { - # Only check the extension config, not the pod since this doesn't bring up pods - if (Has-ExtensionData $extensionName) { - break - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Performs a show on the extension" { - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - $output | Should -Not -BeNullOrEmpty - } - - It "Runs an update on the extension on the cluster" { - Set-ItResult -Skipped -Because "Update is not a valid scenario for now" - - # az $Env:K8sExtensionName update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName --auto-upgrade-minor-version false - # $? | Should -BeTrue - - # $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - # $? | Should -BeTrue - - # $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - # $isAutoUpgradeMinorVersion.ToString() -eq "False" | Should -BeTrue - - # # Loop and retry until the extension config updates - # $n = 0 - # do - # { - # $isAutoUpgradeMinorVersion = (Get-ExtensionData $extensionName).spec.autoUpgradeMinorVersion - # if (!$isAutoUpgradeMinorVersion) { #autoUpgradeMinorVersion doesn't exist in ExtensionConfig CRD if false - # if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { - # if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { - # break - # } - # } - # } - # Start-Sleep -Seconds 10 - # $n += 1 - # } while ($n -le $MAX_RETRY_ATTEMPTS) - # $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Lists the extensions on the cluster" { - $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $output | Should -Not -BeNullOrEmpty - $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } - $extensionExists | Should -Not -BeNullOrEmpty - } - - It "Deletes the extension from the cluster" { - $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - # Extension should not be found on the cluster - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -Not -BeNullOrEmpty - $output | Should -BeNullOrEmpty - } - - It "Performs another list after the delete" { - $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - $output | Should -Not -BeNullOrEmpty - - $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } - $extensionExists | Should -BeNullOrEmpty - } -} diff --git a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 b/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 deleted file mode 100644 index 4625ff0016a..00000000000 --- a/testing/test/extensions/public/AzureMLKubernetes.Tests.ps1 +++ /dev/null @@ -1,201 +0,0 @@ -Describe 'AzureML Kubernetes Testing' { - BeforeAll { - $extensionType = "Microsoft.AzureML.Kubernetes" - $extensionName = "azureml-kubernetes-connector" - $extensionAgentNamespace = "azureml" - $relayResourceIDKey = "relayserver.hybridConnectionResourceID" - $serviceBusResourceIDKey = "servicebus.resourceID" - - . $PSScriptRoot/../../helper/Constants.ps1 - . $PSScriptRoot/../../helper/Helper.ps1 - } - - It 'Creates the extension and checks that it onboards correctly with inference and SSL enabled' { - $sslKeyPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_key.pem" - $sslCertPemFile = Join-Path (Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) "data") "azure_ml") "test_cert.pem" - Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net inferenceLoadBalancerHA=False --config-protected sslKeyPemFile=$sslKeyPemFile sslCertPemFile=$sslCertPemFile" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue - - # Loop and retry until the extension installs - $n = 0 - do - { - if (Has-ExtensionData $extensionName) { - break - } - Start-Sleep -Seconds 20 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - - # check if relay is populated - $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey - $relayResourceID | Should -Not -BeNullOrEmpty - } - - It "Runs an update on the extension on the cluster" { - Set-ItResult -Skipped -Because "Update is not a valid scenario for now" - az k8s-extension update --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName --auto-upgrade-minor-version false - $? | Should -BeTrue - - $output = az k8s-extension show --cluster-name $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters --name $extensionName - $? | Should -BeTrue - - $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - $isAutoUpgradeMinorVersion.ToString() -eq "False" | Should -BeTrue - - # Loop and retry until the extension config updates - $n = 0 - do - { - $isAutoUpgradeMinorVersion = (Get-ExtensionData $extensionName).spec.autoUpgradeMinorVersion - if (!$isAutoUpgradeMinorVersion) { #autoUpgradeMinorVersion doesn't exist in ExtensionConfig CRD if false - if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { - break - } - } - Start-Sleep -Seconds 20 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Performs a show on the extension" { - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - $output | Should -Not -BeNullOrEmpty - } - - - It "Lists the extensions on the cluster" { - $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $output | Should -Not -BeNullOrEmpty - $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } - $extensionExists | Should -Not -BeNullOrEmpty - } - - It "Deletes the extension from the cluster with inference enabled" { - # cleanup the relay and servicebus - $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey - $serviceBusResourceID = Get-ExtensionConfigurationSettings $extensionName $serviceBusResourceIDKey - $relayNamespaceName = $relayResourceID.split("/")[8] - $serviceBusNamespaceName = $serviceBusResourceID.split("/")[8] - az relay namespace delete --resource-group $ENVCONFIG.resourceGroup --name $relayNamespaceName - az servicebus namespace delete --resource-group $ENVCONFIG.resourceGroup --name $serviceBusNamespaceName - - $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - # Extension should not be found on the cluster - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -Not -BeNullOrEmpty - $output | Should -BeNullOrEmpty - } - - It "Performs another list after the delete" { - $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - $output | Should -Not -BeNullOrEmpty - - $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } - $extensionExists | Should -BeNullOrEmpty - } - - # It 'Creates the extension and checks that it onboards correctly with training enabled' { - # Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableTraining=true" -ErrorVariable badOut - # $badOut | Should -BeNullOrEmpty - - # $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - # $badOut | Should -BeNullOrEmpty - - # $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - # $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue - - # # Loop and retry until the extension installs - # $n = 0 - # do - # { - # if (Has-ExtensionData $extensionName) { - # break - # } - # Start-Sleep -Seconds 20 - # $n += 1 - # } while ($n -le $MAX_RETRY_ATTEMPTS) - # $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - - # # check if relay is populated - # $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey - # $relayResourceID | Should -Not -BeNullOrEmpty - # } - - # It "Deletes the extension from the cluster" { - # # cleanup the relay and servicebus - # $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey - # $serviceBusResourceID = Get-ExtensionConfigurationSettings $extensionName $serviceBusResourceIDKey - # $relayNamespaceName = $relayResourceID.split("/")[8] - # $serviceBusNamespaceName = $serviceBusResourceID.split("/")[8] - # az relay namespace delete --resource-group $ENVCONFIG.resourceGroup --name $relayNamespaceName - # az servicebus namespace delete --resource-group $ENVCONFIG.resourceGroup --name $serviceBusNamespaceName - - # $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - # $badOut | Should -BeNullOrEmpty - - # # Extension should not be found on the cluster - # $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - # $badOut | Should -Not -BeNullOrEmpty - # $output | Should -BeNullOrEmpty - # } - - # It 'Creates the extension and checks that it onboards correctly with inference enabled' { - # Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train staging --config enableInference=true identity.proxy.remoteEnabled=True identity.proxy.remoteHost=https://master.experiments.azureml-test.net allowInsecureConnections=True inferenceLoadBalancerHA=false" -ErrorVariable badOut - # $badOut | Should -BeNullOrEmpty - - # $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - # $badOut | Should -BeNullOrEmpty - - # $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - # $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue - - # # Loop and retry until the extension installs - # $n = 0 - # do - # { - # if (Has-ExtensionData $extensionName) { - # break - # } - # Start-Sleep -Seconds 20 - # $n += 1 - # } while ($n -le $MAX_RETRY_ATTEMPTS) - # $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - - # # check if relay is populated - # $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey - # $relayResourceID | Should -Not -BeNullOrEmpty - # } - - # It "Deletes the extension from the cluster with inference enabled" { - # # cleanup the relay and servicebus - # $relayResourceID = Get-ExtensionConfigurationSettings $extensionName $relayResourceIDKey - # $serviceBusResourceID = Get-ExtensionConfigurationSettings $extensionName $serviceBusResourceIDKey - # $relayNamespaceName = $relayResourceID.split("/")[8] - # $serviceBusNamespaceName = $serviceBusResourceID.split("/")[8] - # az relay namespace delete --resource-group $ENVCONFIG.resourceGroup --name $relayNamespaceName - # az servicebus namespace delete --resource-group $ENVCONFIG.resourceGroup --name $serviceBusNamespaceName - - # $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - # $badOut | Should -BeNullOrEmpty - - # # Extension should not be found on the cluster - # $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - # $badOut | Should -Not -BeNullOrEmpty - # $output | Should -BeNullOrEmpty - # } -} diff --git a/testing/test/extensions/public/AzureMonitor.Tests.ps1 b/testing/test/extensions/public/AzureMonitor.Tests.ps1 deleted file mode 100644 index bc7b1fedd8f..00000000000 --- a/testing/test/extensions/public/AzureMonitor.Tests.ps1 +++ /dev/null @@ -1,100 +0,0 @@ -Describe 'Azure Monitor Testing' { - BeforeAll { - $extensionType = "microsoft.azuremonitor.containers" - $extensionName = "azuremonitor-containers" - $extensionAgentName = "omsagent" - $extensionAgentNamespace = "kube-system" - - . $PSScriptRoot/../../helper/Constants.ps1 - . $PSScriptRoot/../../helper/Helper.ps1 - } - - It 'Creates the extension and checks that it onboards correctly' { - Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - $isAutoUpgradeMinorVersion.ToString() -eq "True" | Should -BeTrue - - # Loop and retry until the extension installs - $n = 0 - do - { - if (Has-ExtensionData $extensionName) { - if (Has-Identity-Provisioned) { - break - } - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Performs a show on the extension" { - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - $output | Should -Not -BeNullOrEmpty - } - - It "Runs an update on the extension on the cluster" { - Set-ItResult -Skipped -Because "Update is not a valid scenario for now" - - # az $Env:K8sExtensionName update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName --auto-upgrade-minor-version false - # $? | Should -BeTrue - - # $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - # $? | Should -BeTrue - - # $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - # $isAutoUpgradeMinorVersion.ToString() -eq "False" | Should -BeTrue - - # # Loop and retry until the extension config updates - # $n = 0 - # do - # { - # $isAutoUpgradeMinorVersion = (Get-ExtensionData $extensionName).spec.autoUpgradeMinorVersion - # if (!$isAutoUpgradeMinorVersion) { #autoUpgradeMinorVersion doesn't exist in ExtensionConfig CRD if false - # if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { - # if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { - # break - # } - # } - # } - # Start-Sleep -Seconds 10 - # $n += 1 - # } while ($n -le $MAX_RETRY_ATTEMPTS) - # $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Lists the extensions on the cluster" { - $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $output | Should -Not -BeNullOrEmpty - $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } - $extensionExists | Should -Not -BeNullOrEmpty - } - - It "Deletes the extension from the cluster" { - $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - # Extension should not be found on the cluster - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -Not -BeNullOrEmpty - $output | Should -BeNullOrEmpty - } - - It "Performs another list after the delete" { - $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - $output | Should -Not -BeNullOrEmpty - - $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } - $extensionExists | Should -BeNullOrEmpty - } -} diff --git a/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 b/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 deleted file mode 100644 index 3070d579731..00000000000 --- a/testing/test/extensions/public/OpenServiceMesh.Tests.ps1 +++ /dev/null @@ -1,102 +0,0 @@ -Describe 'Azure OpenServiceMesh Testing' { - BeforeAll { - $extensionType = "microsoft.openservicemesh" - $extensionName = "openservicemesh" - $extensionVersion = "0.9.1" - $extensionAgentName = "osm-controller" - $extensionAgentNamespace = "arc-osm-system" - $releaseTrain = "pilot" - - . $PSScriptRoot/../../helper/Constants.ps1 - . $PSScriptRoot/../../helper/Helper.ps1 - } - - # Should Not BeNullOrEmpty checks if the command returns JSON output - - It 'Creates the extension and checks that it onboards correctly' { - Invoke-Expression "az $Env:K8sExtensionName create -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters --extension-type $extensionType -n $extensionName --release-train $releaseTrain --version $extensionVersion" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - $isAutoUpgradeMinorVersion.ToString() -eq "False" | Should -BeTrue - - # Loop and retry until the extension installs - $n = 0 - do - { - if (Has-ExtensionData $extensionName) { - break - } - Start-Sleep -Seconds 10 - $n += 1 - } while ($n -le $MAX_RETRY_ATTEMPTS) - $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Performs a show on the extension" { - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - $output | Should -Not -BeNullOrEmpty - } - - It "Runs an update on the extension on the cluster" { - Set-ItResult -Skipped -Because "Update is not a valid scenario for now" - - # az $Env:K8sExtensionName update -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName --auto-upgrade-minor-version false - # $? | Should -BeTrue - - # $output = az $Env:K8sExtensionName show -c $ENVCONFIG.arcClusterName -g $ENVCONFIG.resourceGroup --cluster-type connectedClusters -n $extensionName - # $? | Should -BeTrue - - # $isAutoUpgradeMinorVersion = ($output | ConvertFrom-Json).autoUpgradeMinorVersion - # $isAutoUpgradeMinorVersion.ToString() -eq "False" | Should -BeTrue - - # # Loop and retry until the extension config updates - # $n = 0 - # do - # { - # $isAutoUpgradeMinorVersion = (Get-ExtensionData $extensionName).spec.autoUpgradeMinorVersion - # if (!$isAutoUpgradeMinorVersion) { #autoUpgradeMinorVersion doesn't exist in ExtensionConfig CRD if false - # if (Get-ExtensionStatus $extensionName -eq $SUCCESS_MESSAGE) { - # if (Get-PodStatus $extensionAgentName -Namespace $extensionAgentNamespace -eq $POD_RUNNING) { - # break - # } - # } - # } - # Start-Sleep -Seconds 10 - # $n += 1 - # } while ($n -le $MAX_RETRY_ATTEMPTS) - # $n | Should -BeLessOrEqual $MAX_RETRY_ATTEMPTS - } - - It "Lists the extensions on the cluster" { - $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - $output | Should -Not -BeNullOrEmpty - $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionType } - $extensionExists | Should -Not -BeNullOrEmpty - } - - It "Deletes the extension from the cluster" { - $output = Invoke-Expression "az $Env:K8sExtensionName delete -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - - # Extension should not be found on the cluster - $output = Invoke-Expression "az $Env:K8sExtensionName show -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters -n $extensionName" -ErrorVariable badOut - $badOut | Should -Not -BeNullOrEmpty - $output | Should -BeNullOrEmpty - } - - It "Performs another list after the delete" { - $output = Invoke-Expression "az $Env:K8sExtensionName list -c $($ENVCONFIG.arcClusterName) -g $($ENVCONFIG.resourceGroup) --cluster-type connectedClusters" -ErrorVariable badOut - $badOut | Should -BeNullOrEmpty - $output | Should -Not -BeNullOrEmpty - - $extensionExists = $output | ConvertFrom-Json | Where-Object { $_.extensionType -eq $extensionName } - $extensionExists | Should -BeNullOrEmpty - } -} diff --git a/testing/test/helper/Constants.ps1 b/testing/test/helper/Constants.ps1 deleted file mode 100644 index 3ecd3621bc5..00000000000 --- a/testing/test/helper/Constants.ps1 +++ /dev/null @@ -1,7 +0,0 @@ -$ENVCONFIG = Get-Content -Path $PSScriptRoot/../../settings.json | ConvertFrom-Json -$SUCCESS_MESSAGE = "Successfully installed the extension" -$FAILED_MESSAGE = "Failed to install the extension" - -$POD_RUNNING = "Running" - -$MAX_RETRY_ATTEMPTS = 10 \ No newline at end of file diff --git a/testing/test/helper/Helper.ps1 b/testing/test/helper/Helper.ps1 deleted file mode 100644 index 4ff949e7ab4..00000000000 --- a/testing/test/helper/Helper.ps1 +++ /dev/null @@ -1,72 +0,0 @@ -function Get-ExtensionData { - param( - [string]$extensionName - ) - - $output = kubectl get extensionconfigs -A -o json | ConvertFrom-Json - return $output.items | Where-Object { $_.metadata.name -eq $extensionName } -} - -function Has-ExtensionData { - param( - [string]$extensionName - ) - $extensionData = Get-ExtensionData $extensionName - if ($extensionData) { - return $true - } - return $false -} - - -function Has-Identity-Provisioned { - $output = kubectl get azureclusteridentityrequests -n azure-arc container-insights-clusteridentityrequest -o json | ConvertFrom-Json - return ($null -ne $output.status.expirationTime) -and ($null -ne $output.status.tokenReference.dataName) -and ($null -ne $output.status.tokenReference.secretName) -} - -function Get-ExtensionStatus { - param( - [string]$extensionName - ) - - $extensionData = Get-ExtensionData $extensionName - if ($extensionData) { - return $extensionData.status.status - } - return $null -} - -function Get-PodStatus { - param( - [string]$podName, - [string]$Namespace - ) - - $allPodData = kubectl get pods -n $Namespace -o json | ConvertFrom-Json - $podData = $allPodData.items | Where-Object { $_.metadata.name -Match $podName } - if ($podData.Length -gt 1) { - return $podData[0].status.phase - } - return $podData.status.phase -} - -function Get-ExtensionConfigurationSettings { - param( - [string]$extensionName, - [string]$configKey - ) - - $extensionData = Get-ExtensionData $extensionName - if ($extensionData) { - return $extensionData.spec.parameter."$configKey" - } - return $null -} - -function Check-Error { - param( - [string]$output - ) - $hasError = $output -CMatch "ERROR" - return $hasError -} \ No newline at end of file