-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge remote-tracking branch 'origin/master' into Localization
- Loading branch information
Showing
109 changed files
with
11,376 additions
and
4,594 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
|
||
|
48 changes: 48 additions & 0 deletions
48
Tasks/AzureAppConfigurationSnapshotV1/Strings/resources.resjson/en-US/resources.resjson
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
|
||
}); |
17 changes: 17 additions & 0 deletions
17
Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshot.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); |
17 changes: 17 additions & 0 deletions
17
Tasks/AzureAppConfigurationSnapshotV1/Tests/createSnapshotWithConflict.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); |
Oops, something went wrong.