diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7cef5c307297..3a36af297b9a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -19,6 +19,8 @@ Tasks/ArchiveFilesV2/ @microsoft/akvelon-build-task-team # DRI Rotation for @microsoft/release-management-task-team : DRI-ReleaseManagement +Tasks/AzureAppConfigurationSnapshotV1/ @microsoft/azure-appconfig-team + Tasks/AzureAppServiceManageV0/ @microsoft/release-management-task-team @manolerazvan Tasks/AzureAppServiceSettingsV1/ @microsoft/release-management-task-team @manolerazvan diff --git a/Tasks/AzureAppConfigurationSnapshotV1/README.md b/Tasks/AzureAppConfigurationSnapshotV1/README.md new file mode 100644 index 000000000000..505d011a675b --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/README.md @@ -0,0 +1,35 @@ +# Azure AppConfiguration Snapshot + +### Overview + +This task is used for creating [snapshots](https://learn.microsoft.com/azure/azure-app-configuration/concept-snapshots) in a given [App Configuration store](https://learn.microsoft.com/en-us/azure/azure-app-configuration/quickstart-azure-app-configuration-create). A snapshot is a named, immutable subset of an App Configuration store's key-values. The task is node based and works on cross platform Azure Pipelines agents running Windows, Linux or Mac. + +## Contact Information + +Please report a problem to [AzureAppConfig@microsoft.com](AzureAppConfig@microsoft.com) if you are facing problems in making this task work. You can also share feedback about the task like, what more functionality should be added to the task, what other tasks you would like to have, at the same place. + +### Parameters of the task: + +The parameters of the task are described below. The parameters listed with a \* are required parameters for the task: + +* **Azure Subscription**\*: Select the AzureRM Subscription. If none exists, then click on the **Manage** link, to navigate to the Services tab in the Administrators panel. In the tab click on **New Service Connection** and select **Azure Resource Manager** from the dropdown. + +* **App Configuration Endpoint**\*: Select the endpoint of the App Configuration store to which the snapshot will be created. + +* **Snapshot name**\*: Provide the name of the snapshot + +* **Composition Type**\*: Select the **composition type**. + - **Key** composition type, if your store has identical keys with different labels, only the key-value specified in the last applicable filter is included in the snapshot. Identical key-values with other labels are left out of the snapshot. + + - **Key-Label** composition type, if your store has identical keys with different labels, all key-values with identical keys but different labels are included in the snapshot depending on the specified filters. + +* **Filters**\*: Provide snapshot filters that represent the key and label filters used to build an App Configuration snapshot. Filters should be of a valid JSON format. + Example + ```json + [{\"key\":\"abc*\", \"label\":\"1.0.0\"}] + +* **Retention Period**: Specify the days to retain an archived snapshot. Archived snapshots can be recovered during the retention period + +* **Tags**: Specify one or more tags that should be added to a snapshot. Tags should be of a valid JSON format. + + diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Strings/resources.resjson/en-US/resources.resjson b/Tasks/AzureAppConfigurationSnapshotV1/Strings/resources.resjson/en-US/resources.resjson new file mode 100644 index 000000000000..c5db2d90d854 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Strings/resources.resjson/en-US/resources.resjson @@ -0,0 +1,48 @@ +{ + "loc.friendlyName": "Azure App Configuration Snapshot", + "loc.helpMarkDown": "Email AzureAppConfig@microsoft.com for questions.", + "loc.description": "Create a snapshot in an Azure App Configuration instance", + "loc.instanceNameFormat": "Azure App Configuration Snapshot", + "loc.group.displayName.AppConfiguration": "AppConfiguration", + "loc.group.displayName.Options": "Options", + "loc.input.label.ConnectedServiceName": "Azure subscription", + "loc.input.help.ConnectedServiceName": "Select the Azure Subscription for the Azure App Configuration instance.", + "loc.input.label.AppConfigurationEndpoint": "App Configuration Endpoint", + "loc.input.help.AppConfigurationEndpoint": "Provide the endpoint of an existing [Azure App Configuration](https://docs.microsoft.com/en-us/azure/azure-app-configuration/concept-key-value).", + "loc.input.label.SnapshotName": "Snapshot Name", + "loc.input.help.SnapshotName": "Provide a name for the snapshot.", + "loc.input.label.CompositionType": "Composition Type", + "loc.input.help.CompositionType": "'Key': The filters are applied in order for this composition type. Each key-value in the snapshot is uniquely identified by the key only. If there are multiple key-values with the same key and multiple labels, only one key-value will be retained based on the last applicable filter. \n 'Key-Label': Filters will be applied and every key-value in resulting snapshot will be uniquely identified by the key and label together.", + "loc.input.label.Filters": "Filters for key-values", + "loc.input.help.Filters": "Specifies snapshot filters that represent the key and label filters used to build an App Configuration snapshot. Filters should be of a valid JSON format. Example [{\"key\":\"abc*\", \"label\":\"1.0.0\"}]. At least 1 filter and max of 3 filters can be applied.", + "loc.input.label.RetentionPeriod": "Days to retain archived snapshot", + "loc.input.help.RetentionPeriod": "Archived snapshots can be recovered during the retention period. Choose the number of days the snapshot will be retained after it is archived. The value cannot be changed after creation.", + "loc.input.label.Tags": "Tags", + "loc.input.help.Tags": "Specifies one or more tags that should be added to a snapshot. Tags should be of a valid JSON format and can span multiple lines. Example: {\"tag1\": \"value1\", \"tag2\": \"value2\"}", + "loc.messages.AccessDenied": "Access to the target App Configuration instance was denied. Please ensure the required assignment is made for the identity running this task.", + "loc.messages.SnapshotAlreadyExists": "Snapshot %s already exists.", + "loc.messages.MaxRetentionDaysforFreeStore": "The maximum retention period for snapshots after archival in free stores is 7 days.", + "loc.messages.InvalidCompositionTypeValue": "Invalid value for parameter 'CompositionType'. Expected '%s' or '%s', but got %s.", + "loc.messages.InvalidFilterFormatJSONObjectExpected": "Invalid format for parameter 'Filters'. Please provide an escaped JSON object.", + "loc.messages.InvalidFilterFormat": "Invalid format for parameter 'Filters'. Sample Filters: '[{\"key\":\"abc*\", \"label\":\"1.0.0\"}]'", + "loc.messages.InvalidFilterFormatKeyIsRequired": "Invalid format for parameter 'Filters', 'key' is a required property.", + "loc.messages.InvalidFilterFormatExpectedAllowedProperties": "Invalid format for parameter 'Filters'. Expected only allowed properties 'key' and 'label' but got %s.", + "loc.messages.MaxAndMinFiltersRequired": "At least one filter is required and a maximum of 3 filters are allowed.", + "loc.messages.RetentionPeriodNonNegativeIntegerValue": "Retention period value should be a non-negative integer value", + "loc.messages.MaxAndMinRetentionPeriodStandardStore": "Retention period must be between %s and %s days.", + "loc.messages.MinRetentionAfterArchiveSnapshot": "The snapshot will be retained for a minimum of one hour after it is archived.", + "loc.messages.InvalidTagFormatValidJSONStringExpected": "Invalid format for parameter 'Tags'. Please provide a valid JSON string as input.", + "loc.messages.InvalidTagFormat": "Invalid format for parameter 'Tags'. Sample 'Tags': '{\"name1\": \"value1\", \"name2\": \"value2\"}'.", + "loc.messages.InvalidTagFormatOnlyStringsSupported": "Invalid type in parameter 'Tags'. Only strings supported", + "loc.messages.SnapshotCreatedSuccessfully": "Snapshot created successfully. \nName: %s \nCreated On: %s \nItems Count: %s \nSize: %s bytes \nStatus: %s", + "loc.messages.SnapshotTaskIsStartingUp": "Azure App Configuration Snapshot Task is starting up...", + "loc.messages.AzureSubscriptionTitle": "Azure Subscription:", + "loc.messages.AzureAppConfigurationEndpointTitle": "Azure App Configuration Endpoint:", + "loc.messages.SnapshotNameTitle": "Snapshot Name:", + "loc.messages.CompositionTypeTitle": "Composition Type:", + "loc.messages.FiltersTitle": "Filters:", + "loc.messages.UnexpectedError": "An unexpected error occurred. %s", + "loc.messages.HttpError": "A HTTP error occurred \nName: %s \nCode: %s \nStatus code: %s \nUrl: %s \nError message: %s \nClientRequestId: %s", + "loc.messages.UnauthenticatedRestError": "\nStatus code: %s \nUrl: %s \nError message: %s \nWWW-Authenticate: %s \nClientRequestId: %s", + "loc.messages.AuthenticationError": "Error response: %s \nStatus code: %s \nError message: %s" +} \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/L0.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/L0.ts new file mode 100644 index 000000000000..c207c87d6b86 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/L0.ts @@ -0,0 +1,222 @@ +import * as assert from 'assert'; +import path = require('path'); +import { MockTestRunner } from 'azure-pipelines-task-lib/mock-test'; + +describe("Create Snapshot test", function () { + this.timeout(30000); + + before(async () => { + process.env["ENDPOINT_AUTH_PARAMETER_AzureRMSpn_SERVICEPRINCIPALID"] = "spId"; + process.env["ENDPOINT_AUTH_PARAMETER_AzureRMSpn_SERVICEPRINCIPALKEY"] = "spKey"; + process.env["ENDPOINT_AUTH_PARAMETER_AzureRMSpn_TENANTID"] = "tenant"; + process.env["ENDPOINT_AUTH_PARAMETER_AzureRMSpn_AUTHENTICATIONTYPE"] = "spnKey"; + process.env["ENDPOINT_DATA_AzureRMSpn_SUBSCRIPTIONNAME"] = "sName"; + process.env["ENDPOINT_DATA_AzureRMSpn_SUBSCRIPTIONID"] = "sId"; + process.env["ENDPOINT_DATA_AzureRMSpn_GRAPHURL"] = "https://graph.windows.net/"; + process.env["ENDPOINT_DATA_AzureRMSpn_ENVIRONMENT"] = "AzureCloud"; + process.env["ENDPOINT_URL_AzureRMSpn"] = "https://management.azure.com/"; + process.env["SYSTEM_DEFAULTWORKINGDIRECTORY"] = "C:\\a\\w\\"; + process.env["AGENT_TEMPDIRECTORY"] = process.cwd(); + }); + + function runValidations(validator: () => void, testRunner) { + try { + validator(); + } catch (error) { + console.log("STDERR", testRunner.stderr); + console.log("STDOUT", testRunner.stdout); + console.log("Error", error); + } + } + + it("Successfully create a snapshot", async () => { + const taskPath = path.join(__dirname, "createSnapshot.js"); + const tr = new MockTestRunner(taskPath); + + await tr.runAsync(); + runValidations(() => { + assert.strictEqual(tr.succeeded, true, "should have succeeded"); + assert.strictEqual(tr.warningIssues.length, 0, "should have no warnings"); + assert.strictEqual(tr.errorIssues.length, 0, "should have no errors"); + assert.strictEqual(tr.stdout.indexOf(`loc_mock_SnapshotCreatedSuccessfully`) >= 0, true, "should have printed snapshot created successfully"); + }, tr); + }); + + it("Handle create snapshot conflict request", async () => { + const taskPath = path.join(__dirname, "createSnapshotWithConflict.js"); + const tr = new MockTestRunner(taskPath); + + await tr.runAsync(); + + runValidations(() => { + assert.strictEqual(tr.succeeded, false, "should not succeeded"); + assert.strictEqual(tr.warningIssues.length, 0, "should have no warnings"); + assert.strictEqual(tr.errorIssues.length, 1, "should have one error"); + assert.strictEqual(tr.errorIssues[0], `loc_mock_SnapshotAlreadyExists TestSnapshot Status code: 409`); + }, tr); + }); + + it("Handle create snapshot forbidden request", async () => { + const taskPath = path.join(__dirname, "createSnapshotWithForbidden.js"); + const tr = new MockTestRunner(taskPath); + + await tr.runAsync(); + runValidations(() => { + assert.strictEqual(tr.succeeded, false, "should not succeeded"); + assert.strictEqual(tr.warningIssues.length, 0, "should have no warnings"); + assert.strictEqual(tr.errorIssues.length, 1, "should have one error"); + assert.strictEqual(tr.errorIssues[0], "loc_mock_AccessDenied Status code: 403", "Should have error message"); + }, tr); + }); + + it("Handle empty filter", async () => { + const taskPath = path.join(__dirname, "createSnapshotWithEmptyFilter.js"); + const tr = new MockTestRunner(taskPath); + + await tr.runAsync(); + + runValidations(() => { + assert.strictEqual(tr.succeeded, false, "should have failed"); + assert.strictEqual(tr.warningIssues.length, 0, "should have no warnings"); + assert.strictEqual(tr.errorIssues.length, 1, "should have one error"); + assert.strictEqual(tr.errorIssues[0], "loc_mock_MaxAndMinFiltersRequired"); + }, tr); + }); + + it("Handle invalid composition type", async () => { + const taskPath = path.join(__dirname, "createSnapshotWithInvalidCompositionType.js"); + const tr = new MockTestRunner(taskPath); + + await tr.runAsync(); + + runValidations(() => { + assert.strictEqual(tr.succeeded, false, "should have failed"); + assert.strictEqual(tr.warningIssues.length, 0, "should have no warnings"); + assert.strictEqual(tr.errorIssues.length, 1, "should have one error"); + assert.strictEqual(tr.errorIssues[0], "loc_mock_InvalidCompositionTypeValue key key_label invalidCompositionType"); + }, tr); + }); + + it("Handle invalid filter type", async () => { + const taskPath = path.join(__dirname, "createSnapshotWithInvalidFilter.js"); + const tr = new MockTestRunner(taskPath); + + await tr.runAsync(); + runValidations(() => { + assert.strictEqual(tr.succeeded, false, "should have failed"); + assert.strictEqual(tr.warningIssues.length, 0, "should have no warnings"); + assert.strictEqual(tr.errorIssues.length, 1, "should have one error"); + assert.strictEqual(tr.errorIssues[0], "loc_mock_InvalidFilterFormat"); + }, tr); + }); + + it("Handle invalid json filters", async () => { + const taskPath = path.join(__dirname, "createSnapshotWithInvalidJsonFilter.js"); + const tr = new MockTestRunner(taskPath); + + await tr.runAsync(); + runValidations(() => { + assert.strictEqual(tr.succeeded, false, "should have failed"); + assert.strictEqual(tr.warningIssues.length, 0, "should have no warnings"); + assert.strictEqual(tr.errorIssues.length, 1, "should have one error"); + assert.strictEqual(tr.errorIssues[0], "loc_mock_InvalidFilterFormatJSONObjectExpected"); + }, tr); + }); + + it("Handle invalid key filter property", async () => { + const taskPath = path.join(__dirname, "createSnapshotWithInvalidKeyFilter.js"); + const tr = new MockTestRunner(taskPath); + + await tr.runAsync(); + runValidations(() => { + assert.strictEqual(tr.succeeded, false, "should have failed"); + assert.strictEqual(tr.warningIssues.length, 0, "should have no warnings"); + assert.strictEqual(tr.errorIssues.length, 1, "should have one error"); + assert.strictEqual(tr.errorIssues[0], "loc_mock_InvalidFilterFormatKeyIsRequired"); + }, tr); + }); + + it("Handle invalid label filter property", async () => { + const taskPath = path.join(__dirname, "createSnapshotWithInvalidLabelFilter.js"); + const tr = new MockTestRunner(taskPath); + + await tr.runAsync(); + runValidations(() => { + assert.strictEqual(tr.succeeded, false, "should have failed"); + assert.strictEqual(tr.warningIssues.length, 0, "should have no warnings"); + assert.strictEqual(tr.errorIssues.length, 1, "should have one error"); + assert.strictEqual(tr.errorIssues[0],`loc_mock_InvalidFilterFormatExpectedAllowedProperties {"key":"*","label_filter":"2.0.0"}`); + }, tr); + + try { + + } catch (error) { + console.log("STDERR", tr.stderr); + console.log("STDOUT", tr.stdout); + console.log("Error", error); + } + }); + + it("Handle invalid retention period", async () => { + const taskPath = path.join(__dirname, "createSnapshotWithInvalidRetention.js"); + const tr = new MockTestRunner(taskPath); + + await tr.runAsync(); + }); + + it("Handle invalid tags", async () => { + const taskPath = path.join(__dirname, "createSnapshotWithInvalidTags.js"); + const tr = new MockTestRunner(taskPath); + + await tr.runAsync(); + runValidations(() => { + assert.strictEqual(tr.succeeded, false, "should have failed"); + assert.strictEqual(tr.warningIssues.length, 0, "should have no warnings"); + assert.strictEqual(tr.errorIssues.length, 1, "should have one error"); + assert.strictEqual(tr.errorIssues[0], "loc_mock_InvalidTagFormatValidJSONStringExpected"); + }, tr); + }); + + it("Handle invalid tag type", async () => { + const taskPath = path.join(__dirname, "createSnapshotWithInvalidTagType.js"); + const tr = new MockTestRunner(taskPath); + + await tr.runAsync(); + + runValidations(() => { + assert.strictEqual(tr.succeeded, false, "should have failed"); + assert.strictEqual(tr.warningIssues.length, 0, "should have no warnings"); + assert.strictEqual(tr.errorIssues.length, 1, "should have one error"); + assert.strictEqual(tr.errorIssues[0], "loc_mock_InvalidTagFormatOnlyStringsSupported"); + }, tr); + }); + + it("Handle max filters", async () => { + const taskPath = path.join(__dirname, "createSnapshotWithMaxFilters.js"); + const tr = new MockTestRunner(taskPath); + + await tr.runAsync(); + + runValidations(() => { + assert.strictEqual(tr.succeeded, false, "should have failed"); + assert.strictEqual(tr.warningIssues.length, 0, "should have no warnings"); + assert.strictEqual(tr.errorIssues.length, 1, "should have one error"); + assert.strictEqual(tr.errorIssues[0], "loc_mock_MaxAndMinFiltersRequired"); + }, tr); + }); + + it("Warn for minimum retention period", async () => { + const taskPath = path.join(__dirname, "createSnapshotWithMinRetention.js"); + const tr = new MockTestRunner(taskPath); + + await tr.runAsync(); + + runValidations(() => { + assert.strictEqual(tr.succeeded, true, "should have succeeded"); + assert.strictEqual(tr.warningIssues.length, 1, "should have one warning"); + assert.strictEqual(tr.errorIssues.length, 0, "should no error"); + assert.strictEqual(tr.warningIssues[0], "loc_mock_MinRetentionAfterArchiveSnapshot"); + }, tr); + }); + +}); diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshot.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshot.ts new file mode 100644 index 000000000000..dad3c0a10bd5 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshot.ts @@ -0,0 +1,17 @@ +import { TaskMockRunner } from "azure-pipelines-task-lib/mock-run"; +import * as path from "path"; + +let taskPath = path.join(__dirname, "..", "index.js"); +let taskRunner:TaskMockRunner = new TaskMockRunner(taskPath); + +taskRunner.setInput("AppConfigurationEndpoint", "https://Test.azconfig.io"); +taskRunner.setInput("SnapshotName", "TestSnapshot"); +taskRunner.setInput("CompositionType", "key"); +taskRunner.setInput("Filters", "[{\"key\": \"*\"}]"); +taskRunner.setInput("RetentionPeriod", "7"); +taskRunner.setInput("ConnectedServiceName","AzureRMSpn"); + +taskRunner.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); +taskRunner.registerMock('@azure/app-configuration', require('./mock_node_modules/app-configuration/appConfigurationClient')); + +taskRunner.run(); \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithConflict.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithConflict.ts new file mode 100644 index 000000000000..977a9efbf1cb --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithConflict.ts @@ -0,0 +1,17 @@ +import { TaskMockRunner } from "azure-pipelines-task-lib/mock-run"; +import * as path from "path"; + +let taskPath = path.join(__dirname, "..", "index.js"); +let taskRunner:TaskMockRunner = new TaskMockRunner(taskPath); + +taskRunner.setInput("AppConfigurationEndpoint", "https://Test.azconfig.io"); +taskRunner.setInput("SnapshotName", "TestSnapshot"); +taskRunner.setInput("CompositionType", "key"); +taskRunner.setInput("Filters", "[{\"key\": \"*\"}]"); +taskRunner.setInput("RetentionPeriod", "7"); +taskRunner.setInput("ConnectedServiceName","AzureRMSpn"); + +taskRunner.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); +taskRunner.registerMock('@azure/app-configuration', require('./mock_node_modules/app-configuration/conflictAppConfigurationClient')); + +taskRunner.run(); \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithEmptyFilter.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithEmptyFilter.ts new file mode 100644 index 000000000000..90fe91d7f5c6 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithEmptyFilter.ts @@ -0,0 +1,17 @@ +import { TaskMockRunner } from "azure-pipelines-task-lib/mock-run"; +import * as path from "path"; + +let taskPath = path.join(__dirname, "..", "index.js"); +let taskRunner:TaskMockRunner = new TaskMockRunner(taskPath); + +taskRunner.setInput("AppConfigurationEndpoint", "https://Test.azconfig.io"); +taskRunner.setInput("SnapshotName", "TestSnapshot"); +taskRunner.setInput("CompositionType", "key"); +taskRunner.setInput("Filters", "[]"); +taskRunner.setInput("RetentionPeriod", "7"); +taskRunner.setInput("ConnectedServiceName","AzureRMSpn"); + +taskRunner.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); +taskRunner.registerMock('@azure/app-configuration', require('./mock_node_modules/app-configuration/appConfigurationClient')); + +taskRunner.run(); diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithForbidden.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithForbidden.ts new file mode 100644 index 000000000000..17a97aecc5a0 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithForbidden.ts @@ -0,0 +1,17 @@ +import { TaskMockRunner } from "azure-pipelines-task-lib/mock-run"; +import * as path from "path"; + +let taskPath = path.join(__dirname, "..", "index.js"); +let taskRunner:TaskMockRunner = new TaskMockRunner(taskPath); + +taskRunner.setInput("AppConfigurationEndpoint", "https://Test.azconfig.io"); +taskRunner.setInput("SnapshotName", "TestSnapshot"); +taskRunner.setInput("CompositionType", "key"); +taskRunner.setInput("Filters", "[{\"key\": \"*\"}]"); +taskRunner.setInput("RetentionPeriod", "7"); +taskRunner.setInput("ConnectedServiceName","AzureRMSpn"); + +taskRunner.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); +taskRunner.registerMock('@azure/app-configuration', require('./mock_node_modules/app-configuration/forbiddenAppConfigurationClient')); + +taskRunner.run(); \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidCompositionType.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidCompositionType.ts new file mode 100644 index 000000000000..5f3189d30dbe --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidCompositionType.ts @@ -0,0 +1,17 @@ +import { TaskMockRunner } from "azure-pipelines-task-lib/mock-run"; +import * as path from "path"; + +let taskPath = path.join(__dirname, "..", "index.js"); +let taskRunner:TaskMockRunner = new TaskMockRunner(taskPath); + +taskRunner.setInput("AppConfigurationEndpoint", "https://Test.azconfig.io"); +taskRunner.setInput("SnapshotName", "TestSnapshot"); +taskRunner.setInput("CompositionType", "invalidCompositionType"); +taskRunner.setInput("Filters", "[{\"key\": \"*\"}]"); +taskRunner.setInput("RetentionPeriod", "7"); +taskRunner.setInput("ConnectedServiceName","AzureRMSpn"); + +taskRunner.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); +taskRunner.registerMock('@azure/app-configuration', require('./mock_node_modules/app-configuration/appConfigurationClient')); + +taskRunner.run(); \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidFilter.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidFilter.ts new file mode 100644 index 000000000000..35c3ba557977 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidFilter.ts @@ -0,0 +1,17 @@ +import { TaskMockRunner } from "azure-pipelines-task-lib/mock-run"; +import * as path from "path"; + +let taskPath = path.join(__dirname, "..", "index.js"); +let taskRunner:TaskMockRunner = new TaskMockRunner(taskPath); + +taskRunner.setInput("AppConfigurationEndpoint", "https://Test.azconfig.io"); +taskRunner.setInput("SnapshotName", "TestSnapshot"); +taskRunner.setInput("CompositionType", "key"); +taskRunner.setInput("Filters", "5"); +taskRunner.setInput("RetentionPeriod", "7"); +taskRunner.setInput("ConnectedServiceName","AzureRMSpn"); + +taskRunner.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); +taskRunner.registerMock('@azure/app-configuration', require('./mock_node_modules/app-configuration/appConfigurationClient')); + +taskRunner.run(); \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidJsonFilter.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidJsonFilter.ts new file mode 100644 index 000000000000..7f3b5e444193 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidJsonFilter.ts @@ -0,0 +1,17 @@ +import { TaskMockRunner } from "azure-pipelines-task-lib/mock-run"; +import * as path from "path"; + +let taskPath = path.join(__dirname, "..", "index.js"); +let taskRunner:TaskMockRunner = new TaskMockRunner(taskPath); + +taskRunner.setInput("AppConfigurationEndpoint", "https://Test.azconfig.io"); +taskRunner.setInput("SnapshotName", "TestSnapshot"); +taskRunner.setInput("CompositionType", "key"); +taskRunner.setInput("Filters", "abc"); +taskRunner.setInput("RetentionPeriod", "7"); +taskRunner.setInput("ConnectedServiceName","AzureRMSpn"); + +taskRunner.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); +taskRunner.registerMock('@azure/app-configuration', require('./mock_node_modules/app-configuration/appConfigurationClient')); + +taskRunner.run(); \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidKeyFilter.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidKeyFilter.ts new file mode 100644 index 000000000000..c447f8cbb4dc --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidKeyFilter.ts @@ -0,0 +1,17 @@ +import { TaskMockRunner } from "azure-pipelines-task-lib/mock-run"; +import * as path from "path"; + +let taskPath = path.join(__dirname, "..", "index.js"); +let taskRunner:TaskMockRunner = new TaskMockRunner(taskPath); + +taskRunner.setInput("AppConfigurationEndpoint", "https://Test.azconfig.io"); +taskRunner.setInput("SnapshotName", "TestSnapshot"); +taskRunner.setInput("CompositionType", "key"); +taskRunner.setInput("Filters", "[{\"invalidFilter\": \"*\"}]"); +taskRunner.setInput("RetentionPeriod", "7"); +taskRunner.setInput("ConnectedServiceName","AzureRMSpn"); + +taskRunner.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); +taskRunner.registerMock('@azure/app-configuration', require('./mock_node_modules/app-configuration/appConfigurationClient')); + +taskRunner.run(); \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidLabelFilter.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidLabelFilter.ts new file mode 100644 index 000000000000..a502497d867a --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidLabelFilter.ts @@ -0,0 +1,17 @@ +import { TaskMockRunner } from "azure-pipelines-task-lib/mock-run"; +import * as path from "path"; + +let taskPath = path.join(__dirname, "..", "index.js"); +let taskRunner:TaskMockRunner = new TaskMockRunner(taskPath); + +taskRunner.setInput("AppConfigurationEndpoint", "https://Test.azconfig.io"); +taskRunner.setInput("SnapshotName", "TestSnapshot"); +taskRunner.setInput("CompositionType", "key"); +taskRunner.setInput("Filters", "[{\"key\":\"abc*\",\"label\":\"1.0.0\"}, {\"key\":\"xyz\"}, {\"key\":\"*\", \"label_filter\":\"2.0.0\"}]"); +taskRunner.setInput("RetentionPeriod", "7"); +taskRunner.setInput("ConnectedServiceName","AzureRMSpn"); + +taskRunner.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); +taskRunner.registerMock('@azure/app-configuration', require('./mock_node_modules/app-configuration/appConfigurationClient')); + +taskRunner.run(); \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidRetention.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidRetention.ts new file mode 100644 index 000000000000..8a2b81cfab1e --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidRetention.ts @@ -0,0 +1,17 @@ +import { TaskMockRunner } from "azure-pipelines-task-lib/mock-run"; +import * as path from "path"; + +let taskPath = path.join(__dirname, "..", "index.js"); +let taskRunner:TaskMockRunner = new TaskMockRunner(taskPath); + +taskRunner.setInput("AppConfigurationEndpoint", "https://Test.azconfig.io"); +taskRunner.setInput("SnapshotName", "TestSnapshot"); +taskRunner.setInput("CompositionType", "key"); +taskRunner.setInput("Filters", "[{\"key\": \"*\"}]"); +taskRunner.setInput("RetentionPeriod", "-1"); +taskRunner.setInput("ConnectedServiceName","AzureRMSpn"); + +taskRunner.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); +taskRunner.registerMock('@azure/app-configuration', require('./mock_node_modules/app-configuration/appConfigurationClient')); + +taskRunner.run(); \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidTagType.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidTagType.ts new file mode 100644 index 000000000000..41e18cc52098 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidTagType.ts @@ -0,0 +1,18 @@ +import { TaskMockRunner } from "azure-pipelines-task-lib/mock-run"; +import * as path from "path"; + +let taskPath = path.join(__dirname, "..", "index.js"); +let taskRunner:TaskMockRunner = new TaskMockRunner(taskPath); + +taskRunner.setInput("AppConfigurationEndpoint", "https://Test.azconfig.io"); +taskRunner.setInput("SnapshotName", "TestSnapshot"); +taskRunner.setInput("CompositionType", "key"); +taskRunner.setInput("Filters", "[{\"key\": \"*\"}]"); +taskRunner.setInput("RetentionPeriod", "7"); +taskRunner.setInput("Tags", "{\"5\" : 5}"); +taskRunner.setInput("ConnectedServiceName","AzureRMSpn"); + +taskRunner.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); +taskRunner.registerMock('@azure/app-configuration', require('./mock_node_modules/app-configuration/appConfigurationClient')); + +taskRunner.run(); \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidTags.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidTags.ts new file mode 100644 index 000000000000..83b269a811c1 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithInvalidTags.ts @@ -0,0 +1,18 @@ +import { TaskMockRunner } from "azure-pipelines-task-lib/mock-run"; +import * as path from "path"; + +let taskPath = path.join(__dirname, "..", "index.js"); +let taskRunner:TaskMockRunner = new TaskMockRunner(taskPath); + +taskRunner.setInput("AppConfigurationEndpoint", "https://Test.azconfig.io"); +taskRunner.setInput("SnapshotName", "TestSnapshot"); +taskRunner.setInput("CompositionType", "key"); +taskRunner.setInput("Filters", "[{\"key\": \"*\"}]"); +taskRunner.setInput("RetentionPeriod", "7"); +taskRunner.setInput("Tags", "tags"); +taskRunner.setInput("ConnectedServiceName","AzureRMSpn"); + +taskRunner.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); +taskRunner.registerMock('@azure/app-configuration', require('./mock_node_modules/app-configuration/appConfigurationClient')); + +taskRunner.run(); \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithMaxFilters.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithMaxFilters.ts new file mode 100644 index 000000000000..e73ad70bf767 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithMaxFilters.ts @@ -0,0 +1,17 @@ +import { TaskMockRunner } from "azure-pipelines-task-lib/mock-run"; +import * as path from "path"; + +let taskPath = path.join(__dirname, "..", "index.js"); +let taskRunner:TaskMockRunner = new TaskMockRunner(taskPath); + +taskRunner.setInput("AppConfigurationEndpoint", "https://Test.azconfig.io"); +taskRunner.setInput("SnapshotName", "TestSnapshot"); +taskRunner.setInput("CompositionType", "key"); +taskRunner.setInput("Filters", "[{\"key\": \"*\", \"label\": \"1.0.0\"},{\"key\": \"abc*\", \"label\": \"2.0.0\"},{\"key\": \"xyz*\", \"label\": \"3.0.0*\"},{\"key\": \"test\", \"label\": \"4.0.0\"}]"); +taskRunner.setInput("RetentionPeriod", "7"); +taskRunner.setInput("ConnectedServiceName","AzureRMSpn"); + +taskRunner.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); +taskRunner.registerMock('@azure/app-configuration', require('./mock_node_modules/app-configuration/appConfigurationClient')); + +taskRunner.run(); \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithMinRetention.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithMinRetention.ts new file mode 100644 index 000000000000..4630859a28bf --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithMinRetention.ts @@ -0,0 +1,17 @@ +import { TaskMockRunner } from "azure-pipelines-task-lib/mock-run"; +import * as path from "path"; + +let taskPath = path.join(__dirname, "..", "index.js"); +let taskRunner:TaskMockRunner = new TaskMockRunner(taskPath); + +taskRunner.setInput("AppConfigurationEndpoint", "https://Test.azconfig.io"); +taskRunner.setInput("SnapshotName", "TestSnapshot"); +taskRunner.setInput("CompositionType", "key"); +taskRunner.setInput("Filters", "[{\"key\": \"*\"}]"); +taskRunner.setInput("RetentionPeriod", "0"); +taskRunner.setInput("ConnectedServiceName","AzureRMSpn"); + +taskRunner.registerMock('azure-pipelines-task-lib/toolrunner', require('azure-pipelines-task-lib/mock-toolrunner')); +taskRunner.registerMock('@azure/app-configuration', require('./mock_node_modules/app-configuration/appConfigurationClient')); + +taskRunner.run(); \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/mock_node_modules/app-configuration/appConfigurationClient.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/mock_node_modules/app-configuration/appConfigurationClient.ts new file mode 100644 index 000000000000..9c8c82a47e11 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/mock_node_modules/app-configuration/appConfigurationClient.ts @@ -0,0 +1,22 @@ +import { SnapshotInfo, CreateSnapshotOptions, CreateSnapshotResponse } from "@azure/app-configuration"; + +export class AppConfigurationClient { + + public async beginCreateSnapshotAndWait(snapshot: SnapshotInfo, options?: CreateSnapshotOptions): Promise { + return Promise.resolve({ + name: snapshot.name, + id: "id", + eTag: "etag", + lastModified: new Date(), + contentType: "application/json", + sizeInBytes: 1000, + createdOn: new Date(), + itemCount: 1, + status: "ready", + filters: snapshot.filters, + retentionPeriodInSeconds: snapshot.retentionPeriodInSeconds + }); + } +} + +export { KnownSnapshotComposition } from "@azure/app-configuration"; \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/mock_node_modules/app-configuration/conflictAppConfigurationClient.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/mock_node_modules/app-configuration/conflictAppConfigurationClient.ts new file mode 100644 index 000000000000..ae84cc9344d4 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/mock_node_modules/app-configuration/conflictAppConfigurationClient.ts @@ -0,0 +1,12 @@ +import { SnapshotInfo, CreateSnapshotOptions, CreateSnapshotResponse } from "@azure/app-configuration"; +import { RestError } from "@azure/core-rest-pipeline"; + +class ConflictAppConfigurationClient { + public async beginCreateSnapshotAndWait(snapshot: SnapshotInfo, options?: CreateSnapshotOptions): Promise { + return Promise.reject(new RestError("Snapshot already exists", {statusCode: 409})); + } +} + +export { KnownSnapshotComposition } from "@azure/app-configuration"; + +exports.AppConfigurationClient = ConflictAppConfigurationClient; \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/Tests/mock_node_modules/app-configuration/forbiddenAppConfigurationClient.ts b/Tasks/AzureAppConfigurationSnapshotV1/Tests/mock_node_modules/app-configuration/forbiddenAppConfigurationClient.ts new file mode 100644 index 000000000000..a39280e0aee5 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/Tests/mock_node_modules/app-configuration/forbiddenAppConfigurationClient.ts @@ -0,0 +1,16 @@ +/** + * Mock App config Client, that throws an 403 status code +*/ +import { RestError } from "@azure/core-rest-pipeline"; +import { SnapshotInfo, CreateSnapshotOptions, CreateSnapshotResponse } from "@azure/app-configuration"; + + class ForbiddenAppConfigurationClient { + + public async beginCreateSnapshotAndWait(snapshot: SnapshotInfo, options?: CreateSnapshotOptions): Promise { + + return Promise.reject(new RestError('', {statusCode: 403})); + } +} + +export { KnownSnapshotComposition } from "@azure/app-configuration"; +exports.AppConfigurationClient = ForbiddenAppConfigurationClient; \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/ThirdPartyNotices.txt b/Tasks/AzureAppConfigurationSnapshotV1/ThirdPartyNotices.txt new file mode 100644 index 000000000000..ad80d22edf14 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/ThirdPartyNotices.txt @@ -0,0 +1,115 @@ +THIRD-PARTY SOFTWARE NOTICES AND INFORMATION +Do Not Translate or Localize + +This Azure DevOps extension (AzureAppConfigurationSnapshot) is based on or incorporates material from the projects listed below (Third Party IP). The original copyright notice and the license under which Microsoft received such Third Party IP, are set forth below. Such licenses and notices are provided for informational purposes only. Microsoft licenses the Third Party IP to you under the licensing terms for the Azure DevOps extension. Microsoft reserves all other rights not expressly granted under this agreement, whether by implication, estoppel or otherwise. + +1. @types/mocha (git+https://github.com/DefinitelyTyped/DefinitelyTyped.git) +2. @types/node (git+https://github.com/DefinitelyTyped/DefinitelyTyped.git) +3. lodash (https://github.com/lodash/lodash) + + +%% @types/mocha NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE +========================================= +END OF @types/mocha NOTICES, INFORMATION, AND LICENSE + +%% @types/node NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE +========================================= +END OF @types/node NOTICES, INFORMATION, AND LICENSE + +%% lodash NOTICES, INFORMATION, AND LICENSE BEGIN HERE +========================================= +The MIT License + +Copyright JS Foundation and other contributors + +Based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/lodash/lodash + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code displayed within the prose of the +documentation. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +Files located in the node_modules and vendor directories are externally +maintained libraries used by this software which have their own +licenses; we recommend you read them, as their terms may differ from the +terms above. +========================================= +END OF lodash NOTICES, INFORMATION, AND LICENSE \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/connectedServiceCredential.ts b/Tasks/AzureAppConfigurationSnapshotV1/connectedServiceCredential.ts new file mode 100644 index 000000000000..cdcbf0386dea --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/connectedServiceCredential.ts @@ -0,0 +1,25 @@ +import { TokenCredential, AccessToken } from "@azure/identity"; +import { AzureEndpoint } from "azure-pipelines-tasks-azure-arm-rest/azureModels"; + +export class ConnectedServiceCredential implements TokenCredential { + + private _endpoint: AzureEndpoint; + private _audience: string; + + constructor(endpoint: AzureEndpoint, audience: string) { + + this._endpoint = endpoint; + + this._audience = audience; + } + + async getToken(): Promise { + this._endpoint.applicationTokenCredentials.activeDirectoryResourceId = this._audience; + + // https://learn.microsoft.com/en-us/entra/identity-platform/configurable-token-lifetimes + return { + token: await this._endpoint.applicationTokenCredentials.getToken(true), // force will result in a new token being fetched instead of cached token + expiresOnTimestamp: Date.now() + (3600 * 1000) + }; + } +} \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/errors.ts b/Tasks/AzureAppConfigurationSnapshotV1/errors.ts new file mode 100644 index 000000000000..6e71eb1593a3 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/errors.ts @@ -0,0 +1,74 @@ +import { RestError } from "@azure/core-rest-pipeline"; + +/** + * Custom error type used during input validation + */ +export class ArgumentError extends Error { +} + +/** + * Custom error type used for expected RestError ie: Conflict and Forbidden. + */ +export class AppConfigurationError extends Error { + public message: string; + + constructor(message: string) { + super(); + + this.message = message; + } +} + +/** + * Custom error type used when null arguments are passed + */ +export class ArgumentNullError extends Error { +} + +/** + * Custom error type used during JSON data parsing + */ +export class ParseError extends Error { + public message: string; + + constructor(message: string) { + super(); + + this.message = message; + } +} + +/** + * Obtains an error message to output to the log + * + * @param error Error object + * @param description Optional description to include in the error message + */ + +export function getErrorMessage(error: any, description?: string): string { + + const parts: string[] = [description]; + + // Include the error message from our custom Error types + if (error instanceof RestError) { + + // Include status code if present + const statusCode: string | number = error.statusCode || error.code; + + if (statusCode) { + + parts.push(`Status code: ${statusCode}`); + } + } + else if (error instanceof Error) { + + parts.push(error.message); + } + + // Remove null/undefined/empty values + const filteredParts: string[] = parts.filter((e: string) => e); + + return filteredParts.length === 0 ? + "An unknown error occurred." : + filteredParts.join(" "); +} \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/icon.png b/Tasks/AzureAppConfigurationSnapshotV1/icon.png new file mode 100644 index 000000000000..76abfe8ed395 Binary files /dev/null and b/Tasks/AzureAppConfigurationSnapshotV1/icon.png differ diff --git a/Tasks/AzureAppConfigurationSnapshotV1/icon.svg b/Tasks/AzureAppConfigurationSnapshotV1/icon.svg new file mode 100644 index 000000000000..8e2ed2aac8e5 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/index.ts b/Tasks/AzureAppConfigurationSnapshotV1/index.ts new file mode 100644 index 000000000000..d2e1ffc8f2c8 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/index.ts @@ -0,0 +1,41 @@ +import * as tl from "azure-pipelines-task-lib/task"; +import * as path from "path"; +import { AuthenticationError } from "@azure/identity"; +import { RestError } from "@azure/core-rest-pipeline"; +import { TaskParameters } from "./taskParameters"; +import { TaskController } from "./taskController"; +import { AppConfigurationError, ArgumentError, ParseError } from "./errors"; + +async function run(): Promise { + try { + const taskManifestPath: string = path.join(__dirname, "task.json"); + tl.setResourcePath(taskManifestPath); + + const taskParameters: TaskParameters = await TaskParameters.initialize(); + const taskController: TaskController = new TaskController(taskParameters); + + await taskController.createSnapshot(); + + tl.setResult(tl.TaskResult.Succeeded, "", true); + } + catch (error: any) { + if (error instanceof AppConfigurationError || error instanceof ArgumentError || error instanceof ParseError) { + tl.error(error.message); + } + else if (error instanceof AuthenticationError) { + tl.error(tl.loc("AuthenticationError", JSON.stringify(error.errorResponse), error.statusCode, error.message)); + } + else if (error instanceof RestError && error.statusCode == 401) { + tl.error(tl.loc("UnauthenticatedRestError", error.statusCode, error.request.url, error.message, error.response.headers.get("www-authenticate"), error.request.headers.get("x-ms-client-request-id"))); + } + else if (error instanceof RestError) { + tl.error(tl.loc("HttpError", error.name !== undefined ? error.name: "", error.code !== undefined ? error.code: "", error.statusCode, error.request.url, error.message, error.request.headers.get("x-ms-client-request-id"))); + } + else { + tl.error(tl.loc("UnexpectedError", error.message)); + } + tl.setResult(tl.TaskResult.Failed, "", true); + } +} + +run(); \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/make.json b/Tasks/AzureAppConfigurationSnapshotV1/make.json new file mode 100644 index 000000000000..4f24d646b822 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/make.json @@ -0,0 +1,12 @@ +{ + "rm": [ + { + "items": [ + "node_modules/https-proxy-agent/node_modules/agent-base", + "node_modules/azure-pipelines-tasks-azure-arm-rest/node_modules/agent-base", + "node_modules/azure-pipelines-tasks-azure-arm-rest/node_modules/azure-pipelines-task-lib" + ], + "options": "-Rf" + } + ] +} \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/models.ts b/Tasks/AzureAppConfigurationSnapshotV1/models.ts new file mode 100644 index 000000000000..8e10479152bc --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/models.ts @@ -0,0 +1,8 @@ +export interface Tags { + [propertyName: string]: string; +} + +export interface SnapshotFilter { + key: string; + label: string; +} \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/package-lock.json b/Tasks/AzureAppConfigurationSnapshotV1/package-lock.json new file mode 100644 index 000000000000..d93c7481e6a3 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/package-lock.json @@ -0,0 +1,1363 @@ +{ + "name": "vsts-azureappconfiguration-snapshot-task", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vsts-azureappconfiguration-snapshot-task", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@azure/app-configuration": "^1.5.0", + "@azure/identity": "^3.1.2", + "@types/mocha": "^5.2.7", + "@types/node": "^20.3.1", + "agent-base": "^6.0.2", + "azure-pipelines-task-lib": "^4.13.0", + "azure-pipelines-tasks-azure-arm-rest": "^3.242.1", + "lodash": "^4.17.21" + }, + "devDependencies": { + "typescript": "5.1.6" + } + }, + "node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/app-configuration": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@azure/app-configuration/-/app-configuration-1.6.1.tgz", + "integrity": "sha512-pk8zyG/8Nc6VN7uDA9QY19UFhTXneUbnB+5IcW9uuPyVDXU17TcXBI4xY1ZBm7hmhn0yh3CeZK4kOxa/tjsMqQ==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.5.0", + "@azure/core-http-compat": "^2.0.0", + "@azure/core-lro": "^2.5.1", + "@azure/core-paging": "^1.4.0", + "@azure/core-rest-pipeline": "^1.6.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.2.tgz", + "integrity": "sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.1.2.tgz", + "integrity": "sha512-5MnV1yqzZwgNLLjlizsU3QqOeQChkIXw781Fwh1xdAqJR5AA32IUaq6xv1BICJvfbHoa+JYcaij2HFkhLbNTJQ==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-client": "^1.3.0", + "@azure/core-rest-pipeline": "^1.3.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-http-compat/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-lro/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.2.tgz", + "integrity": "sha512-Hnhm/PG9/SQ07JJyLDv3l9Qr8V3xgAe1hFoBYzt6LaalMxfL/ZqFaZf/bz5VN3pMcleCPwl8ivlS2Fjxq/iC8Q==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.9.0", + "@azure/logger": "^1.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.1.2.tgz", + "integrity": "sha512-dawW9ifvWAWmUm9/h+/UQ2jrdvjCJ7VJEuCJ6XVNudzcOwm53BFZH4Q845vjfgoUAM8ZxokvVNxNxAITc502YA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.1.tgz", + "integrity": "sha512-OLsq0etbHO1MA7j6FouXFghuHrAFGk+5C1imcpQ2e+0oZhYF07WLA+NW2Vqs70R7d+zOAWiWM3tbE1sXcDN66g==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-3.4.2.tgz", + "integrity": "sha512-0q5DL4uyR0EZ4RXQKD8MadGH6zTIcloUoS/RVbCpNpej4pwte0xpqYxk8K97Py2RiuUvI7F4GXpoT4046VfufA==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-client": "^1.4.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^3.5.0", + "@azure/msal-node": "^2.5.1", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^8.0.0", + "stoppable": "^1.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.3.tgz", + "integrity": "sha512-J8/cIKNQB1Fc9fuYqBVnrppiUtW+5WWJPCj/tAokC5LdSTwkWWttN+jsRgw9BLYD7JDBx7PceiqOBxJJ1tQz3Q==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.20.0.tgz", + "integrity": "sha512-ErsxbfCGIwdqD8jipqdxpfAGiUEQS7MWUe39Rjhl0ZVPsb1JEe9bZCe2+0g23HDH6DGyCAtnTNN9scPtievrMQ==", + "dependencies": { + "@azure/msal-common": "14.14.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "14.14.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.14.0.tgz", + "integrity": "sha512-OxcOk9H1/1fktHh6//VCORgSNJc2dCQObTm6JNmL824Z6iZSO6eFo/Bttxe0hETn9B+cr7gDouTQtsRq3YPuSQ==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.12.0.tgz", + "integrity": "sha512-jmk5Im5KujRA2AcyCb0awA3buV8niSrwXZs+NBJWIvxOz76RvNlusGIqi43A0h45BPUy93Qb+CPdpJn82NFTIg==", + "dependencies": { + "@azure/msal-common": "14.14.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz", + "integrity": "sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mocha": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", + "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==" + }, + "node_modules/@types/node": { + "version": "20.14.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.13.tgz", + "integrity": "sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/q": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz", + "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==" + }, + "node_modules/adm-zip": { + "version": "0.5.14", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.14.tgz", + "integrity": "sha512-DnyqqifT4Jrcvb8USYjp6FHtBpEIz1mnXu6pTRHZ0RL69LbQYiO+0lDFg5+OKA7U29oWSs3a/i8fhn8ZcceIWg==", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/azure-devops-node-api": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-14.0.1.tgz", + "integrity": "sha512-oVnFfTNmergd3JU852EpGY64d1nAxW8lCyzZqFDPhfQVZkdApBeK/ZMN7yoFiq/C50Ru304X1L/+BFblh2SRJw==", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^2.0.1" + }, + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/azure-pipelines-task-lib": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/azure-pipelines-task-lib/-/azure-pipelines-task-lib-4.15.0.tgz", + "integrity": "sha512-Y72FjLTE2CAM9KrBXzc6vjelTBCpdYb2NkyFB0hwksTrhA3q8nsF680dofuTeXztQ94UTpkK27hpgSHnqYf5ZA==", + "dependencies": { + "adm-zip": "^0.5.10", + "minimatch": "3.0.5", + "nodejs-file-downloader": "^4.11.1", + "q": "^1.5.1", + "semver": "^5.1.0", + "shelljs": "^0.8.5", + "uuid": "^3.0.1" + } + }, + "node_modules/azure-pipelines-task-lib/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/azure-pipelines-tasks-azure-arm-rest": { + "version": "3.242.2", + "resolved": "https://registry.npmjs.org/azure-pipelines-tasks-azure-arm-rest/-/azure-pipelines-tasks-azure-arm-rest-3.242.2.tgz", + "integrity": "sha512-ljPHxC07BIMH9f3EP0+Rd86CCH+GX3TeDoULa1hCQUoMYrSfvx+Q5ywxoMp9riB78h9aUZf93CPuuVc79naUgg==", + "dependencies": { + "@types/jsonwebtoken": "^8.5.8", + "@types/mocha": "^5.2.7", + "@types/node": "^10.17.0", + "@types/q": "1.5.4", + "async-mutex": "^0.4.0", + "azure-devops-node-api": "^14.0.1", + "azure-pipelines-task-lib": "^4.11.0", + "https-proxy-agent": "^4.0.0", + "jsonwebtoken": "^9.0.0", + "msalv1": "npm:@azure/msal-node@^1.18.4", + "msalv2": "npm:@azure/msal-node@^2.7.0", + "node-fetch": "^2.6.7", + "q": "1.5.1", + "typed-rest-client": "^2.0.1", + "xml2js": "0.6.2" + } + }, + "node_modules/azure-pipelines-tasks-azure-arm-rest/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" + }, + "node_modules/azure-pipelines-tasks-azure-arm-rest/node_modules/agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/azure-pipelines-tasks-azure-arm-rest/node_modules/https-proxy-agent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "dependencies": { + "agent-base": "5", + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "engines": { + "node": ">=8" + } + }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-core-module": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/msalv1": { + "name": "@azure/msal-node", + "version": "1.18.4", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.18.4.tgz", + "integrity": "sha512-Kc/dRvhZ9Q4+1FSfsTFDME/v6+R2Y1fuMty/TfwqE5p9GTPw08BPbKgeWinE8JRHRp+LemjQbUZsn4Q4l6Lszg==", + "deprecated": "A newer major version of this library is available. Please upgrade to the latest available version.", + "dependencies": { + "@azure/msal-common": "13.3.1", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": "10 || 12 || 14 || 16 || 18" + } + }, + "node_modules/msalv1/node_modules/@azure/msal-common": { + "version": "13.3.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-13.3.1.tgz", + "integrity": "sha512-Lrk1ozoAtaP/cp53May3v6HtcFSVxdFrg2Pa/1xu5oIvsIwhxW6zSPibKefCOVgd5osgykMi5jjcZHv8XkzZEQ==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/msalv2": { + "name": "@azure/msal-node", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.12.0.tgz", + "integrity": "sha512-jmk5Im5KujRA2AcyCb0awA3buV8niSrwXZs+NBJWIvxOz76RvNlusGIqi43A0h45BPUy93Qb+CPdpJn82NFTIg==", + "dependencies": { + "@azure/msal-common": "14.14.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nodejs-file-downloader": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/nodejs-file-downloader/-/nodejs-file-downloader-4.13.0.tgz", + "integrity": "sha512-nI2fKnmJWWFZF6SgMPe1iBodKhfpztLKJTtCtNYGhm/9QXmWa/Pk9Sv00qHgzEvNLe1x7hjGDRor7gcm/ChaIQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "https-proxy-agent": "^5.0.0", + "mime-types": "^2.1.27", + "sanitize-filename": "^1.6.3" + } + }, + "node_modules/nodejs-file-downloader/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qs": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.3.tgz", + "integrity": "sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/typed-rest-client": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-2.0.2.tgz", + "integrity": "sha512-rmAQM2gZw/PQpK5+5aSs+I6ZBv4PFC2BT1o+0ADS1SgSejA+14EmbI2Lt8uXwkX7oeOMkwFmg0pHKwe8D9IT5A==", + "dependencies": { + "des.js": "^1.1.0", + "js-md4": "^0.3.2", + "qs": "^6.10.3", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + }, + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + } + } +} diff --git a/Tasks/AzureAppConfigurationSnapshotV1/package.json b/Tasks/AzureAppConfigurationSnapshotV1/package.json new file mode 100644 index 000000000000..fb014cc41127 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/package.json @@ -0,0 +1,29 @@ +{ + "name": "vsts-azureappconfiguration-snapshot-task", + "version": "1.0.0", + "description": "The project is a Azure DevOps extension used to create a snapshot in App Configuration store", + "repository": { + "type": "git", + "url": "git+https://github.com/Microsoft/azure-pipelines-tasks.git" + }, + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Microsoft Corporation", + "license": "MIT", + "homepage": "https://github.com/Microsoft/azure-pipelines-tasks#readme", + "dependencies": { + "@azure/app-configuration": "^1.5.0", + "@azure/identity": "^3.1.2", + "@types/node": "^20.3.1", + "@types/mocha": "^5.2.7", + "agent-base": "^6.0.2", + "azure-pipelines-tasks-azure-arm-rest": "^3.242.1", + "azure-pipelines-task-lib": "^4.13.0", + "lodash": "^4.17.21" + }, + "devDependencies": { + "typescript": "5.1.6" + } +} diff --git a/Tasks/AzureAppConfigurationSnapshotV1/task.json b/Tasks/AzureAppConfigurationSnapshotV1/task.json new file mode 100644 index 000000000000..e48c6bf7d8a5 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/task.json @@ -0,0 +1,168 @@ +{ + "id": "520c1ef0-be95-4931-9278-6e3ac79b81f2", + "name": "AzureAppConfigurationSnapshot", + "friendlyName": "Azure App Configuration Snapshot", + "description": "Create a snapshot in an Azure App Configuration instance", + "helpUrl": "https://learn.microsoft.com/en-us/azure/azure-app-configuration/concept-snapshots", + "helpMarkDown": "Email AzureAppConfig@microsoft.com for questions.", + "category": "Utility", + "author": "Microsoft Corporation", + "version": { + "Major": 1, + "Minor": 244, + "Patch": 0 + }, + "instanceNameFormat": "Azure App Configuration Snapshot", + "minimumAgentVersion": "2.144.0", + "inputs": [ + { + "name": "ConnectedServiceName", + "aliases": [ + "azureSubscription" + ], + "type": "connectedService:AzureRM", + "label": "Azure subscription", + "defaultValue": "", + "required": true, + "helpMarkDown": "Select the Azure Subscription for the Azure App Configuration instance.", + "groupName": "AppConfiguration" + }, + { + "name": "AppConfigurationEndpoint", + "type": "pickList", + "label": "App Configuration Endpoint", + "required": true, + "helpMarkDown": "Provide the endpoint of an existing [Azure App Configuration](https://docs.microsoft.com/en-us/azure/azure-app-configuration/concept-key-value).", + "groupName": "AppConfiguration", + "properties": { + "EditableOptions": "True" + }, + "validation": { + "expression": "isUrl(value)", + "message": "Provide a valid app configuration endpoint." + } + }, + { + "name": "SnapshotName", + "type": "string", + "label": "Snapshot Name", + "defaultValue": "", + "required": true, + "groupName": "Options", + "helpMarkDown": "Provide a name for the snapshot.", + "properties": { + "EditableOptions": "True" + } + }, + { + "name": "CompositionType", + "type": "pickList", + "label": "Composition Type", + "defaultValue": "key", + "required": true, + "groupName": "Options", + "helpMarkDown": "'Key': The filters are applied in order for this composition type. Each key-value in the snapshot is uniquely identified by the key only. If there are multiple key-values with the same key and multiple labels, only one key-value will be retained based on the last applicable filter. \n 'Key-Label': Filters will be applied and every key-value in resulting snapshot will be uniquely identified by the key and label together.", + "options": { + "key": "Key (default)", + "key_label": "Key-Label" + }, + "properties": { + "EditableOptions": "False" + } + }, + { + "name": "Filters", + "type": "multiLine", + "label": "Filters for key-values", + "defaultValue": "", + "required": true, + "groupName": "Options", + "helpMarkDown": "Specifies snapshot filters that represent the key and label filters used to build an App Configuration snapshot. Filters should be of a valid JSON format. Example [{\"key\":\"abc*\", \"label\":\"1.0.0\"}]. At least 1 filter and max of 3 filters can be applied." + }, + { + "name": "RetentionPeriod", + "type": "int", + "label": "Days to retain archived snapshot", + "defaultValue": "30", + "required": false, + "groupName": "Options", + "helpMarkDown": "Archived snapshots can be recovered during the retention period. Choose the number of days the snapshot will be retained after it is archived. The value cannot be changed after creation.", + "properties": { + "EditableOptions": "True" + }, + "validation": { + "expression": "isInRange(value, 0, 90)", + "message": "Allowed range for the retention period is from 0 days (minimum) to 90 days (maximum)" + } + }, + { + "name": "Tags", + "type": "multiLine", + "label": "Tags", + "defaultValue": "", + "required": false, + "helpMarkDown": "Specifies one or more tags that should be added to a snapshot. Tags should be of a valid JSON format and can span multiple lines. Example: {\"tag1\": \"value1\", \"tag2\": \"value2\"}", + "groupName": "Options", + "properties": { + "EditableOptions": "True" + } + } + ], + "groups": [ + { + "name": "AppConfiguration", + "displayName": "AppConfiguration", + "isExpanded": true + }, + { + "name": "Options", + "displayName": "Options", + "isExpanded": true + } + ], + "dataSourceBindings": [ + { + "target": "AppConfigurationEndpoint", + "endpointId": "$(ConnectedServiceName)", + "endpointUrl": "{{{endpoint.url}}}/subscriptions/{{{endpoint.subscriptionId}}}/providers/Microsoft.AppConfiguration/configurationStores?api-version=2020-06-01", + "resultSelector": "jsonpath:$.value[*]", + "resultTemplate": "{ \"Value\" : \"{{{properties.endpoint}}}\", \"DisplayValue\" : \"{{{properties.endpoint}}}\" }" + } + ], + "execution": { + "Node16": { + "target": "index.js" + }, + "Node20_1": { + "target": "index.js" + } + }, + "messages": { + "AccessDenied": "Access to the target App Configuration instance was denied. Please ensure the required assignment is made for the identity running this task.", + "SnapshotAlreadyExists": "Snapshot %s already exists.", + "MaxRetentionDaysforFreeStore": "The maximum retention period for snapshots after archival in free stores is 7 days.", + "InvalidCompositionTypeValue": "Invalid value for parameter 'CompositionType'. Expected '%s' or '%s', but got %s.", + "InvalidFilterFormatJSONObjectExpected": "Invalid format for parameter 'Filters'. Please provide an escaped JSON object.", + "InvalidFilterFormat": "Invalid format for parameter 'Filters'. Sample Filters: '[{\"key\":\"abc*\", \"label\":\"1.0.0\"}]'", + "InvalidFilterFormatKeyIsRequired": "Invalid format for parameter 'Filters', 'key' is a required property.", + "InvalidFilterFormatExpectedAllowedProperties": "Invalid format for parameter 'Filters'. Expected only allowed properties 'key' and 'label' but got %s.", + "MaxAndMinFiltersRequired": "At least one filter is required and a maximum of 3 filters are allowed.", + "RetentionPeriodNonNegativeIntegerValue": "Retention period value should be a non-negative integer value", + "MaxAndMinRetentionPeriodStandardStore": "Retention period must be between %s and %s days.", + "MinRetentionAfterArchiveSnapshot": "The snapshot will be retained for a minimum of one hour after it is archived.", + "InvalidTagFormatValidJSONStringExpected": "Invalid format for parameter 'Tags'. Please provide a valid JSON string as input.", + "InvalidTagFormat": "Invalid format for parameter 'Tags'. Sample 'Tags': '{\"name1\": \"value1\", \"name2\": \"value2\"}'.", + "InvalidTagFormatOnlyStringsSupported": "Invalid type in parameter 'Tags'. Only strings supported", + "SnapshotCreatedSuccessfully": "Snapshot created successfully. \nName: %s \nCreated On: %s \nItems Count: %s \nSize: %s bytes \nStatus: %s", + "SnapshotTaskIsStartingUp": "Azure App Configuration Snapshot Task is starting up...", + "AzureSubscriptionTitle": "Azure Subscription:", + "AzureAppConfigurationEndpointTitle": "Azure App Configuration Endpoint:", + "SnapshotNameTitle": "Snapshot Name:", + "CompositionTypeTitle": "Composition Type:", + "FiltersTitle": "Filters:", + "UnexpectedError": "An unexpected error occurred. %s", + "HttpError": "A HTTP error occurred \nName: %s \nCode: %s \nStatus code: %s \nUrl: %s \nError message: %s \nClientRequestId: %s", + "UnauthenticatedRestError": "\nStatus code: %s \nUrl: %s \nError message: %s \nWWW-Authenticate: %s \nClientRequestId: %s", + "AuthenticationError":"Error response: %s \nStatus code: %s \nError message: %s" + } +} \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/task.loc.json b/Tasks/AzureAppConfigurationSnapshotV1/task.loc.json new file mode 100644 index 000000000000..e6954281b192 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/task.loc.json @@ -0,0 +1,168 @@ +{ + "id": "520c1ef0-be95-4931-9278-6e3ac79b81f2", + "name": "AzureAppConfigurationSnapshot", + "friendlyName": "ms-resource:loc.friendlyName", + "description": "ms-resource:loc.description", + "helpUrl": "https://learn.microsoft.com/en-us/azure/azure-app-configuration/concept-snapshots", + "helpMarkDown": "ms-resource:loc.helpMarkDown", + "category": "Utility", + "author": "Microsoft Corporation", + "version": { + "Major": 1, + "Minor": 244, + "Patch": 0 + }, + "instanceNameFormat": "ms-resource:loc.instanceNameFormat", + "minimumAgentVersion": "2.144.0", + "inputs": [ + { + "name": "ConnectedServiceName", + "aliases": [ + "azureSubscription" + ], + "type": "connectedService:AzureRM", + "label": "ms-resource:loc.input.label.ConnectedServiceName", + "defaultValue": "", + "required": true, + "helpMarkDown": "ms-resource:loc.input.help.ConnectedServiceName", + "groupName": "AppConfiguration" + }, + { + "name": "AppConfigurationEndpoint", + "type": "pickList", + "label": "ms-resource:loc.input.label.AppConfigurationEndpoint", + "required": true, + "helpMarkDown": "ms-resource:loc.input.help.AppConfigurationEndpoint", + "groupName": "AppConfiguration", + "properties": { + "EditableOptions": "True" + }, + "validation": { + "expression": "isUrl(value)", + "message": "Provide a valid app configuration endpoint." + } + }, + { + "name": "SnapshotName", + "type": "string", + "label": "ms-resource:loc.input.label.SnapshotName", + "defaultValue": "", + "required": true, + "groupName": "Options", + "helpMarkDown": "ms-resource:loc.input.help.SnapshotName", + "properties": { + "EditableOptions": "True" + } + }, + { + "name": "CompositionType", + "type": "pickList", + "label": "ms-resource:loc.input.label.CompositionType", + "defaultValue": "key", + "required": true, + "groupName": "Options", + "helpMarkDown": "ms-resource:loc.input.help.CompositionType", + "options": { + "key": "Key (default)", + "key_label": "Key-Label" + }, + "properties": { + "EditableOptions": "False" + } + }, + { + "name": "Filters", + "type": "multiLine", + "label": "ms-resource:loc.input.label.Filters", + "defaultValue": "", + "required": true, + "groupName": "Options", + "helpMarkDown": "ms-resource:loc.input.help.Filters" + }, + { + "name": "RetentionPeriod", + "type": "int", + "label": "ms-resource:loc.input.label.RetentionPeriod", + "defaultValue": "30", + "required": false, + "groupName": "Options", + "helpMarkDown": "ms-resource:loc.input.help.RetentionPeriod", + "properties": { + "EditableOptions": "True" + }, + "validation": { + "expression": "isInRange(value, 0, 90)", + "message": "Allowed range for the retention period is from 0 days (minimum) to 90 days (maximum)" + } + }, + { + "name": "Tags", + "type": "multiLine", + "label": "ms-resource:loc.input.label.Tags", + "defaultValue": "", + "required": false, + "helpMarkDown": "ms-resource:loc.input.help.Tags", + "groupName": "Options", + "properties": { + "EditableOptions": "True" + } + } + ], + "groups": [ + { + "name": "AppConfiguration", + "displayName": "ms-resource:loc.group.displayName.AppConfiguration", + "isExpanded": true + }, + { + "name": "Options", + "displayName": "ms-resource:loc.group.displayName.Options", + "isExpanded": true + } + ], + "dataSourceBindings": [ + { + "target": "AppConfigurationEndpoint", + "endpointId": "$(ConnectedServiceName)", + "endpointUrl": "{{{endpoint.url}}}/subscriptions/{{{endpoint.subscriptionId}}}/providers/Microsoft.AppConfiguration/configurationStores?api-version=2020-06-01", + "resultSelector": "jsonpath:$.value[*]", + "resultTemplate": "{ \"Value\" : \"{{{properties.endpoint}}}\", \"DisplayValue\" : \"{{{properties.endpoint}}}\" }" + } + ], + "execution": { + "Node16": { + "target": "index.js" + }, + "Node20_1": { + "target": "index.js" + } + }, + "messages": { + "AccessDenied": "ms-resource:loc.messages.AccessDenied", + "SnapshotAlreadyExists": "ms-resource:loc.messages.SnapshotAlreadyExists", + "MaxRetentionDaysforFreeStore": "ms-resource:loc.messages.MaxRetentionDaysforFreeStore", + "InvalidCompositionTypeValue": "ms-resource:loc.messages.InvalidCompositionTypeValue", + "InvalidFilterFormatJSONObjectExpected": "ms-resource:loc.messages.InvalidFilterFormatJSONObjectExpected", + "InvalidFilterFormat": "ms-resource:loc.messages.InvalidFilterFormat", + "InvalidFilterFormatKeyIsRequired": "ms-resource:loc.messages.InvalidFilterFormatKeyIsRequired", + "InvalidFilterFormatExpectedAllowedProperties": "ms-resource:loc.messages.InvalidFilterFormatExpectedAllowedProperties", + "MaxAndMinFiltersRequired": "ms-resource:loc.messages.MaxAndMinFiltersRequired", + "RetentionPeriodNonNegativeIntegerValue": "ms-resource:loc.messages.RetentionPeriodNonNegativeIntegerValue", + "MaxAndMinRetentionPeriodStandardStore": "ms-resource:loc.messages.MaxAndMinRetentionPeriodStandardStore", + "MinRetentionAfterArchiveSnapshot": "ms-resource:loc.messages.MinRetentionAfterArchiveSnapshot", + "InvalidTagFormatValidJSONStringExpected": "ms-resource:loc.messages.InvalidTagFormatValidJSONStringExpected", + "InvalidTagFormat": "ms-resource:loc.messages.InvalidTagFormat", + "InvalidTagFormatOnlyStringsSupported": "ms-resource:loc.messages.InvalidTagFormatOnlyStringsSupported", + "SnapshotCreatedSuccessfully": "ms-resource:loc.messages.SnapshotCreatedSuccessfully", + "SnapshotTaskIsStartingUp": "ms-resource:loc.messages.SnapshotTaskIsStartingUp", + "AzureSubscriptionTitle": "ms-resource:loc.messages.AzureSubscriptionTitle", + "AzureAppConfigurationEndpointTitle": "ms-resource:loc.messages.AzureAppConfigurationEndpointTitle", + "SnapshotNameTitle": "ms-resource:loc.messages.SnapshotNameTitle", + "CompositionTypeTitle": "ms-resource:loc.messages.CompositionTypeTitle", + "FiltersTitle": "ms-resource:loc.messages.FiltersTitle", + "UnexpectedError": "ms-resource:loc.messages.UnexpectedError", + "HttpError": "ms-resource:loc.messages.HttpError", + "UnauthenticatedRestError": "ms-resource:loc.messages.UnauthenticatedRestError", + "AuthenticationError": "ms-resource:loc.messages.AuthenticationError" + } +} \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/taskController.ts b/Tasks/AzureAppConfigurationSnapshotV1/taskController.ts new file mode 100644 index 000000000000..b1c36c32ec36 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/taskController.ts @@ -0,0 +1,65 @@ +import * as tl from "azure-pipelines-task-lib/task"; +import { AppConfigurationClient, CreateSnapshotResponse } from '@azure/app-configuration'; +import { RestError } from "@azure/core-rest-pipeline"; +import { TaskParameters } from './taskParameters'; +import { AppConfigurationError, getErrorMessage } from "./errors"; +import { Utils } from './utils'; + +export class TaskController { + private _taskParameters: TaskParameters; + private _client: AppConfigurationClient; + + constructor(taskParameters: TaskParameters) { + this._taskParameters = taskParameters; + this._client = new AppConfigurationClient( + taskParameters.configStoreUrl, + taskParameters.credential, + { + userAgentOptions: { + userAgentPrefix: Utils.GenerateUserAgent() + } + } + ); + } + + public async createSnapshot(): Promise { + console.log(tl.loc("SnapshotTaskIsStartingUp")); + console.log(tl.loc("AzureSubscriptionTitle"), this._taskParameters.endpoint.subscriptionName); + console.log(tl.loc("AzureAppConfigurationEndpointTitle"), this._taskParameters.configStoreUrl); + console.log(tl.loc("SnapshotNameTitle"), this._taskParameters.snapshotName); + console.log(tl.loc("CompositionTypeTitle"), this._taskParameters.compositionType); + console.log(tl.loc("FiltersTitle"), this._taskParameters.filters); + + try { + const createdSnapshot: CreateSnapshotResponse = await this._client.beginCreateSnapshotAndWait({ + name: this._taskParameters.snapshotName, + compositionType: this._taskParameters.compositionType, + filters: this._taskParameters.filters, + retentionPeriodInSeconds: this._taskParameters.retentionPeriod, + tags: this._taskParameters.tags + }); + + console.log(tl.loc("SnapshotCreatedSuccessfully", createdSnapshot.name, new Date(createdSnapshot.createdOn), createdSnapshot.itemCount, createdSnapshot.sizeInBytes, createdSnapshot.status)); + } + catch (error: any) { + if (error instanceof RestError) { + if (error.statusCode == 403) { + tl.debug(`${error.message}`); + throw new AppConfigurationError(getErrorMessage(error, tl.loc("AccessDenied"))); + } + + else if (error.statusCode == 409) { + throw new AppConfigurationError(getErrorMessage(error, tl.loc("SnapshotAlreadyExists", this._taskParameters.snapshotName))); + } + else if (error.statusCode == 400) { + const errorMessage: any = JSON.parse(error.message); + + if (errorMessage["type"] == "https://azconfig.io/errors/invalid-argument" && errorMessage["name"] == "retention_period") { + throw new AppConfigurationError(getErrorMessage(error, tl.loc("MaxRetentionDaysforFreeStore"))); + } + } + } + throw error; + } + } +} \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/taskParameters.ts b/Tasks/AzureAppConfigurationSnapshotV1/taskParameters.ts new file mode 100644 index 000000000000..cd227df3f356 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/taskParameters.ts @@ -0,0 +1,150 @@ +import * as tl from "azure-pipelines-task-lib/task"; +import { isObject, isString } from "lodash"; +import { ConfigurationSettingsFilter, KnownSnapshotComposition } from "@azure/app-configuration"; +import { AzureRMEndpoint } from "azure-pipelines-tasks-azure-arm-rest/azure-arm-endpoint"; +import { AzureEndpoint } from "azure-pipelines-tasks-azure-arm-rest/azureModels"; +import { ConnectedServiceCredential } from "./connectedServiceCredential"; +import { ArgumentError, ParseError } from "./errors"; +import { SnapshotFilter, Tags } from "./models"; + +export class TaskParameters { + public configStoreUrl: string; + public snapshotName: string; + public compositionType: KnownSnapshotComposition; + public filters: ConfigurationSettingsFilter[]; + public retentionPeriod: number; + public tags: Tags; + public credential: ConnectedServiceCredential; + public endpoint: AzureEndpoint; + + public static async initialize(): Promise { + const taskParameters: TaskParameters = new TaskParameters(); + let compositionType: string; + let filters: string; + + try { + taskParameters.configStoreUrl = tl.getInput("AppConfigurationEndpoint", true); + taskParameters.snapshotName = tl.getInput("SnapshotName", true); + compositionType = tl.getInput("CompositionType", true); + filters = tl.getInput("Filters", true); + } + catch (error: any) { + throw new ArgumentError(`${error.message}`); + } + + if (compositionType && !(compositionType == KnownSnapshotComposition.Key || compositionType == KnownSnapshotComposition.KeyLabel)) { + throw new ArgumentError(tl.loc("InvalidCompositionTypeValue", KnownSnapshotComposition.Key, KnownSnapshotComposition.KeyLabel, compositionType)); + } + + taskParameters.compositionType = compositionType == KnownSnapshotComposition.KeyLabel ? KnownSnapshotComposition.KeyLabel : KnownSnapshotComposition.Key; + taskParameters.filters = taskParameters.getFilters(filters); + + taskParameters.tags = taskParameters.getTags(tl.getInput("Tags", false)); + taskParameters.endpoint = await new AzureRMEndpoint(tl.getInput("ConnectedServiceName", true)).getEndpoint(); + taskParameters.credential = new ConnectedServiceCredential(taskParameters.endpoint, taskParameters.configStoreUrl); + taskParameters.retentionPeriod = taskParameters.getRetentionPeriod(Number(tl.getInput("RetentionPeriod", false))); + + return taskParameters; + } + + private getFilters(filters: string): ConfigurationSettingsFilter[] { + let snapshotFilters: SnapshotFilter[] = []; + + try { + snapshotFilters = JSON.parse(filters); + } + catch (error: any) { + throw new ParseError(tl.loc("InvalidFilterFormatJSONObjectExpected")); + } + + if (snapshotFilters && (!isObject(snapshotFilters) || !Array.isArray(snapshotFilters))) { + + throw new ArgumentError(tl.loc("InvalidFilterFormat")); + } + + if (snapshotFilters.length == 0 || snapshotFilters.length > 3) { + + throw new ArgumentError(tl.loc("MaxAndMinFiltersRequired")); + } + + const configurationFilters: ConfigurationSettingsFilter[] = []; + + const allowedPropertyNames: string[] = ["key", "label"]; + + snapshotFilters.forEach((filter: SnapshotFilter) => { + const filterProperties: string[] = Object.keys(filter); + + if (!filterProperties.includes("key")) { + throw new ArgumentError(tl.loc("InvalidFilterFormatKeyIsRequired")); + } + + filterProperties.forEach((filterProperty: string) => { + if (!allowedPropertyNames.includes(filterProperty)) { + throw new ArgumentError(tl.loc("InvalidFilterFormatExpectedAllowedProperties", JSON.stringify(filter))); + } + }); + + configurationFilters.push({ keyFilter: filter.key, labelFilter: filter.label }); + + }); + + return configurationFilters; + } + + private getRetentionPeriod(retentionPeriod: number): number { + const secondsInADay: number = 86400; + const minDays: number = 0; + const maxDaysStandardSKU: number = 90; + + let retentionPeriodNumber: number; + + if (isNaN(retentionPeriod)) { + + throw new ArgumentError(tl.loc("RetentionPeriodNonNegativeIntegerValue")); + } + + if (retentionPeriod < minDays || retentionPeriod > maxDaysStandardSKU) { + + throw new ArgumentError(tl.loc("MaxAndMinRetentionPeriodStandardStore", minDays, maxDaysStandardSKU)); + } + else if (retentionPeriod == minDays) { + + retentionPeriodNumber = 3600; // minimum retention period is 1 hour + tl.warning(tl.loc("MinRetentionAfterArchiveSnapshot")); + } + else { + + retentionPeriodNumber = retentionPeriod * secondsInADay; + } + + return retentionPeriodNumber; + } + + private getTags(tags: string): Tags { + let tagsObject: Tags; + + try { + + tagsObject = tags ? JSON.parse(tags) : undefined; + } + catch { + + throw new ParseError(tl.loc("InvalidTagFormatValidJSONStringExpected")); + } + + if (tagsObject && (!isObject(tagsObject) || Array.isArray(tagsObject))) { + + throw new ParseError(tl.loc("InvalidTagFormat")); + } + + for (const tag in tagsObject) { + + if (Object.prototype.hasOwnProperty.call(tagsObject, tag) && !isString(tagsObject[tag])) { + + throw new ParseError(tl.loc("InvalidTagFormatOnlyStringsSupported")); + } + } + return tagsObject; + } + +} \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/tsconfig.json b/Tasks/AzureAppConfigurationSnapshotV1/tsconfig.json new file mode 100644 index 000000000000..223e04425630 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "commonjs", + "resolveJsonModule": true + }, + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/Tasks/AzureAppConfigurationSnapshotV1/utils.ts b/Tasks/AzureAppConfigurationSnapshotV1/utils.ts new file mode 100644 index 000000000000..aeca81536839 --- /dev/null +++ b/Tasks/AzureAppConfigurationSnapshotV1/utils.ts @@ -0,0 +1,12 @@ +import * as TaskManifestData from "./task.json"; +import * as os from "os"; + +export class Utils { + + public static GenerateUserAgent(): string { + const taskVersion: string = `${TaskManifestData.version.Major}.${TaskManifestData.version.Minor}.${TaskManifestData.version.Patch}`; + const userAgent: string = `AzurePipelines.AzureAppConfiguration.Snapshot/${taskVersion} Node/${process["version"]} OS/(${os.arch()}-${os.type()}-${os.release()})`; + + return userAgent; + } +} \ No newline at end of file diff --git a/make-options.json b/make-options.json index 0dda239be189..cbb648e7e6a1 100644 --- a/make-options.json +++ b/make-options.json @@ -8,6 +8,7 @@ "AppCenterDistributeV3", "AppCenterTestV1", "ArchiveFilesV2", + "AzureAppConfigurationSnapshotV1", "AzureAppServiceManageV0", "AzureAppServiceSettingsV1", "AzureCLIV1",