From d041f5753072f1c746670f50f6f1f5e8c3d0654d Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Thu, 27 Jun 2019 13:54:51 -0700 Subject: [PATCH 1/2] feat: Updates to deployment process to enable rollback (Part 1) --- src/armTemplates/resources/storageAccount.ts | 22 ++++++- src/config.ts | 6 +- src/models/serverless.ts | 5 ++ src/plugins/deploy/azureDeployPlugin.ts | 4 +- src/plugins/login/loginPlugin.test.ts | 3 +- src/services/armService.ts | 7 +- src/services/azureBlobStorageService.ts | 20 +++++- src/services/baseService.ts | 68 ++++++++++++++++++-- src/services/functionAppService.ts | 30 ++++++--- src/services/loginService.test.ts | 3 +- src/services/loginService.ts | 17 +++-- src/services/packageService.ts | 6 +- src/shared/utils.test.ts | 16 ++++- src/shared/utils.ts | 15 ++++- src/test/mockFactory.ts | 13 +++- 15 files changed, 195 insertions(+), 40 deletions(-) diff --git a/src/armTemplates/resources/storageAccount.ts b/src/armTemplates/resources/storageAccount.ts index b786e983..0c59f8bc 100644 --- a/src/armTemplates/resources/storageAccount.ts +++ b/src/armTemplates/resources/storageAccount.ts @@ -1,11 +1,12 @@ import { ArmResourceTemplateGenerator, ArmResourceTemplate } from "../../models/armTemplates"; import { ServerlessAzureConfig, ResourceConfig } from "../../models/serverless"; +import { Utils } from "../../shared/utils"; export class StorageAccountResource implements ArmResourceTemplateGenerator { public static getResourceName(config: ServerlessAzureConfig) { return config.provider.storageAccount && config.provider.storageAccount.name ? config.provider.storageAccount.name - : `${config.provider.prefix}${config.provider.region.substr(0, 3)}${config.provider.stage.substr(0, 3)}sa`.replace("-", "").toLocaleLowerCase(); + : StorageAccountResource.getDefaultStorageAccountName(config) } public getTemplate(): ArmResourceTemplate { @@ -62,4 +63,23 @@ export class StorageAccountResource implements ArmResourceTemplateGenerator { storageAccoutSkuTier: resourceConfig.sku.tier, }; } + + /** + * Gets a default storage account name. + * Storage account names can have at most 24 characters and can have only alpha-numerics + * Default naming convention: + * + * "(first 3 of prefix)(first 3 of region)(first 3 of stage)(first 12 of service)sa" + * (Maximum of 23 characters) + * @param config Serverless Azure Config + */ + private static getDefaultStorageAccountName(config: ServerlessAzureConfig): string { + const prefix = Utils.appendSubstrings( + 3, + config.provider.prefix, + config.provider.region, + config.provider.stage, + ); + return `${prefix}${config.service.substr(0, 12)}sa`.replace("-", "").toLocaleLowerCase(); + } } \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index f2c36435..2f08214d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,5 +1,6 @@ -export const constants = { +export const configConstants = { bearer: "Bearer ", + deploymentArtifactContainer: "deployment-artifacts", functionAppApiPath: "/api/", functionAppDomain: ".azurewebsites.net", functionsAdminApiPath: "/admin/functions/", @@ -10,10 +11,11 @@ export const constants = { logStreamApiPath: "/api/logstream/application/functions/function/", masterKeyApiPath: "/api/functions/admin/masterkey", providerName: "azure", + rollbackEnabled: false, scmCommandApiPath: "/api/command", scmDomain: ".scm.azurewebsites.net", scmVfsPath: "/api/vfs/site/wwwroot/", scmZipDeployApiPath: "/api/zipdeploy" }; -export default constants; \ No newline at end of file +export default configConstants; \ No newline at end of file diff --git a/src/models/serverless.ts b/src/models/serverless.ts index b3f6d315..f50d0248 100644 --- a/src/models/serverless.ts +++ b/src/models/serverless.ts @@ -23,6 +23,11 @@ export interface FunctionAppConfig extends ResourceConfig { extensionVersion?; } +export interface DeploymentConfig { + rollback?: boolean; + container?: string; +} + export interface ServerlessAzureConfig { service: string; provider: { diff --git a/src/plugins/deploy/azureDeployPlugin.ts b/src/plugins/deploy/azureDeployPlugin.ts index 485e771a..00b19414 100644 --- a/src/plugins/deploy/azureDeployPlugin.ts +++ b/src/plugins/deploy/azureDeployPlugin.ts @@ -1,6 +1,6 @@ import Serverless from "serverless"; -import { ResourceService } from "../../services/resourceService"; import { FunctionAppService } from "../../services/functionAppService"; +import { ResourceService } from "../../services/resourceService"; export class AzureDeployPlugin { public hooks: { [eventName: string]: Promise }; @@ -48,9 +48,11 @@ export class AzureDeployPlugin { private async deploy() { const resourceService = new ResourceService(this.serverless, this.options); + await resourceService.deployResourceGroup(); const functionAppService = new FunctionAppService(this.serverless, this.options); + await functionAppService.initialize(); const functionApp = await functionAppService.deploy(); await functionAppService.uploadFunctions(functionApp); diff --git a/src/plugins/login/loginPlugin.test.ts b/src/plugins/login/loginPlugin.test.ts index 6bf138ed..fe1fa892 100644 --- a/src/plugins/login/loginPlugin.test.ts +++ b/src/plugins/login/loginPlugin.test.ts @@ -60,7 +60,8 @@ describe("Login Plugin", () => { expect(AzureLoginService.servicePrincipalLogin).toBeCalledWith( "azureServicePrincipalClientId", "azureServicePrincipalPassword", - "azureServicePrincipalTenantId" + "azureServicePrincipalTenantId", + undefined // would be options ) expect(AzureLoginService.interactiveLogin).not.toBeCalled(); expect(sls.variables["azureCredentials"]).toEqual(credentials); diff --git a/src/services/armService.ts b/src/services/armService.ts index ce13d0c3..90feef5c 100644 --- a/src/services/armService.ts +++ b/src/services/armService.ts @@ -111,9 +111,12 @@ export class ArmService extends BaseService { }; // Deploy ARM template - this.serverless.cli.log("-> Deploying ARM template..."); + this.log("-> Deploying ARM template..."); + this.log(`---> Resource Group: ${this.resourceGroup}`) + this.log(`---> Deployment Name: ${this.deploymentName}`) + const result = await this.resourceClient.deployments.createOrUpdate(this.resourceGroup, this.deploymentName, armDeployment); - this.serverless.cli.log("-> ARM deployment complete"); + this.log("-> ARM deployment complete"); return result; } diff --git a/src/services/azureBlobStorageService.ts b/src/services/azureBlobStorageService.ts index 9946cea5..8726dc06 100644 --- a/src/services/azureBlobStorageService.ts +++ b/src/services/azureBlobStorageService.ts @@ -1,7 +1,8 @@ -import { Aborter, BlockBlobURL, ContainerURL, ServiceURL, StorageURL, uploadFileToBlockBlob } from "@azure/storage-blob"; +import { Aborter, BlockBlobURL, ContainerURL, Credential, ServiceURL, StorageURL, TokenCredential, uploadFileToBlockBlob } from "@azure/storage-blob"; import Serverless from "serverless"; -import { BaseService } from "./baseService"; import { Guard } from "../shared/guard"; +import { BaseService } from "./baseService"; +import { AzureLoginService } from "./loginService"; /** * Wrapper for operations on Azure Blob Storage account @@ -12,12 +13,17 @@ export class AzureBlobStorageService extends BaseService { * Account URL for Azure Blob Storage account. Depends on `storageAccountName` being set in baseService */ private accountUrl: string; + private storageCredential: Credential; public constructor(serverless: Serverless, options: Serverless.Options) { super(serverless, options); this.accountUrl = `https://${this.storageAccountName}.blob.core.windows.net`; } + public async initialize() { + this.storageCredential = new TokenCredential(await this.getToken()); + } + /** * Upload a file to Azure Blob Storage * @param path Path of file to upload @@ -114,7 +120,7 @@ export class AzureBlobStorageService extends BaseService { * Get ServiceURL object for Azure Blob Storage Account */ private getServiceURL(): ServiceURL { - const pipeline = StorageURL.newPipeline(this.credentials); + const pipeline = StorageURL.newPipeline(this.storageCredential); const accountUrl = this.accountUrl; const serviceUrl = new ServiceURL( accountUrl, @@ -149,4 +155,12 @@ export class AzureBlobStorageService extends BaseService { blobName, ); } + + private async getToken(): Promise { + const authResponse = await AzureLoginService.login({ + tokenAudience: "https://storage.azure.com/" + }); + const token = await authResponse.credentials.getToken(); + return token.accessToken; + } } diff --git a/src/services/baseService.ts b/src/services/baseService.ts index c45a86a8..c89f1688 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -2,16 +2,20 @@ import axios from "axios"; import fs from "fs"; import request from "request"; import Serverless from "serverless"; +import { StorageAccountResource } from "../armTemplates/resources/storageAccount"; +import { configConstants } from "../config"; +import { DeploymentConfig, ServerlessAzureConfig } from "../models/serverless"; import { Guard } from "../shared/guard"; -import { ServerlessAzureConfig } from "../models/serverless"; +import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; export abstract class BaseService { protected baseUrl: string; protected serviceName: string; - protected credentials: any; + protected credentials: TokenCredentialsBase protected subscriptionId: string; protected resourceGroup: string; protected deploymentName: string; + protected deploymentConfig: DeploymentConfig protected deploymentContainerName: string; protected storageAccountName: string; protected config: ServerlessAzureConfig; @@ -26,12 +30,14 @@ export abstract class BaseService { this.setDefaultValues(); this.baseUrl = "https://management.azure.com"; + this.serviceName = this.getServiceName(); this.config = serverless.service as any; - this.serviceName = serverless.service["service"]; this.credentials = serverless.variables["azureCredentials"]; this.subscriptionId = serverless.variables["subscriptionId"]; this.resourceGroup = this.getResourceGroupName(); - this.deploymentName = serverless.service.provider["deploymentName"] || `${this.resourceGroup}-deployment`; + this.deploymentConfig = this.getDeploymentConfig(); + this.deploymentName = this.getDeploymentName(); + this.storageAccountName = StorageAccountResource.getResourceName(serverless.service as any) if (!this.credentials && authenticate) { throw new Error(`Azure Credentials has not been set in ${this.constructor.name}`); @@ -52,6 +58,41 @@ export abstract class BaseService { || `${this.config.provider.prefix}-${this.getRegion()}-${this.getStage()}-${this.serviceName}-rg`; } + public getDeploymentConfig(): DeploymentConfig { + const providedConfig = this.serverless["deploy"] as DeploymentConfig; + if (providedConfig && providedConfig.name && providedConfig.rollback) { + throw new Error("Cannot both specify a deployment name and enable rollback. " + + "In order for rollback to work, the name of deployment must follow the generated " + + "naming convention") + } + const config = providedConfig || { + rollback: configConstants.rollbackEnabled, + container: configConstants.deploymentArtifactContainer, + } + return config; + } + + public getDeploymentName(): string { + const name = this.serverless.service.provider["deploymentName"] || `${this.resourceGroup}-deployment` + return this.rollbackConfiguredName(name); + } + + public getServiceName(): string { + return this.serverless.service["service"]; + } + + /** + * Get rollback-configured artifact name. Contains `-t{timestamp}` + * if rollback is configured + */ + public getArtifactName(): string { + return this.rollbackConfiguredName(this.getServiceName()); + } + + protected getAccessToken(): string{ + return (this.credentials.tokenCache as any)._entries[0].accessToken; + } + /** * Sends an API request using axios HTTP library * @param method The HTTP method @@ -60,7 +101,7 @@ export abstract class BaseService { */ protected async sendApiRequest(method: string, relativeUrl: string, options: any = {}) { const defaultHeaders = { - Authorization: `Bearer ${this.credentials.tokenCache._entries[0].accessToken}`, + Authorization: `Bearer ${this.getAccessToken()}`, }; const allHeaders = { @@ -125,4 +166,21 @@ export abstract class BaseService { this.serverless.service.provider["prefix"] = "sls"; } } + + private rollbackConfiguredName(name: string) { + return (this.deploymentConfig.rollback) ? `${name}-t${this.getTimestamp()}` : name; + } + + /** + * Get timestamp from `packageTimestamp` serverless variable + * If not set, create timestamp, set variable and return timestamp + */ + private getTimestamp(): number { + let timestamp = +this.serverless.variables["packageTimestamp"]; + if (!timestamp) { + timestamp = Date.now(); + this.serverless.variables["packageTimestamp"] = timestamp; + } + return timestamp; + } } diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index e83fd6dc..cbf04cd8 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -1,14 +1,15 @@ +import { WebSiteManagementClient } from "@azure/arm-appservice"; +import { FunctionEnvelope, Site } from "@azure/arm-appservice/esm/models"; import fs from "fs"; import path from "path"; -import { WebSiteManagementClient } from "@azure/arm-appservice"; import Serverless from "serverless"; -import { BaseService } from "./baseService"; +import { FunctionAppResource } from "../armTemplates/resources/functionApp"; +import { ArmDeployment } from "../models/armTemplates"; import { FunctionAppHttpTriggerConfig } from "../models/functionApp"; -import { Site, FunctionEnvelope } from "@azure/arm-appservice/esm/models"; import { Guard } from "../shared/guard"; import { ArmService } from "./armService"; -import { ArmDeployment } from "../models/armTemplates"; -import { FunctionAppResource } from "../armTemplates/resources/functionApp"; +import { AzureBlobStorageService } from "./azureBlobStorageService"; +import { BaseService } from "./baseService"; export class FunctionAppService extends BaseService { private webClient: WebSiteManagementClient; @@ -42,6 +43,17 @@ export class FunctionAppService extends BaseService { return response.data.value; } + /** + * Initialize deployment artifact container if rollback is specified + */ + public async initialize() { + if (this.deploymentConfig.rollback) { + const blobService = new AzureBlobStorageService(this.serverless, this.options); + await blobService.initialize(); + blobService.createContainer(this.deploymentConfig.container); + } + } + public async deleteFunction(functionApp: Site, functionName: string) { Guard.null(functionApp); Guard.empty(functionName); @@ -109,7 +121,8 @@ export class FunctionAppService extends BaseService { public async uploadFunctions(functionApp: Site): Promise { Guard.null(functionApp, "functionApp"); - this.log("Deploying serverless functions..."); + this.log("Deploying serverless functions..."); + await this.zipDeploy(functionApp); } @@ -132,10 +145,9 @@ export class FunctionAppService extends BaseService { } private async zipDeploy(functionApp) { - const functionAppName = functionApp.name; const scmDomain = this.getScmDomain(functionApp); - this.serverless.cli.log(`Deploying zip file to function app: ${functionAppName}`); + this.serverless.cli.log(`Deploying zip file to function app: ${functionApp.name}`); // Upload function artifact if it exists, otherwise the full service is handled in 'uploadFunctions' method let functionZipFile = this.serverless.service["artifact"]; @@ -155,7 +167,7 @@ export class FunctionAppService extends BaseService { uri: `https://${scmDomain}/api/zipdeploy/`, json: true, headers: { - Authorization: `Bearer ${this.credentials.tokenCache._entries[0].accessToken}`, + Authorization: `Bearer ${this.getAccessToken()}`, Accept: "*/*", ContentType: "application/octet-stream", } diff --git a/src/services/loginService.test.ts b/src/services/loginService.test.ts index 2e8d23c9..a43e5114 100644 --- a/src/services/loginService.test.ts +++ b/src/services/loginService.test.ts @@ -30,7 +30,8 @@ describe("Login Service", () => { expect(loginWithServicePrincipalSecretWithAuthResponse).toBeCalledWith( "azureServicePrincipalClientId", "azureServicePrincipalPassword", - "azureServicePrincipalTenantId" + "azureServicePrincipalTenantId", + undefined // would be options ); }); }); \ No newline at end of file diff --git a/src/services/loginService.ts b/src/services/loginService.ts index b6afa60b..b934fc91 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -3,28 +3,31 @@ import { interactiveLoginWithAuthResponse, loginWithServicePrincipalSecretWithAuthResponse, AuthResponse, + AzureTokenCredentialsOptions, + InteractiveLoginOptions, } from "@azure/ms-rest-nodeauth"; export class AzureLoginService { - public static async login(): Promise { + public static async login(options?: AzureTokenCredentialsOptions|InteractiveLoginOptions): Promise { const subscriptionId = process.env.azureSubId; const clientId = process.env.azureServicePrincipalClientId; const secret = process.env.azureServicePrincipalPassword; const tenantId = process.env.azureServicePrincipalTenantId; if (subscriptionId && clientId && secret && tenantId) { - return await AzureLoginService.servicePrincipalLogin(clientId, secret, tenantId); + return await AzureLoginService.servicePrincipalLogin(clientId, secret, tenantId, options); } else { - return await AzureLoginService.interactiveLogin(); + return await AzureLoginService.interactiveLogin(options); } } - public static async interactiveLogin(): Promise { + public static async interactiveLogin(options: InteractiveLoginOptions): Promise { await open("https://microsoft.com/devicelogin"); - return await interactiveLoginWithAuthResponse(); + return await interactiveLoginWithAuthResponse(options) } - public static async servicePrincipalLogin(clientId: string, secret: string, tenantId: string): Promise { - return loginWithServicePrincipalSecretWithAuthResponse(clientId, secret, tenantId); + public static async servicePrincipalLogin(clientId: string, secret: string, tenantId: string, options: AzureTokenCredentialsOptions): Promise { + // return loginWithServicePrincipalSecretWithAuthResponse(clientId, secret, tenantId); + return loginWithServicePrincipalSecretWithAuthResponse(clientId, secret, tenantId, options) } } diff --git a/src/services/packageService.ts b/src/services/packageService.ts index 1bcd9431..89127a42 100644 --- a/src/services/packageService.ts +++ b/src/services/packageService.ts @@ -1,7 +1,7 @@ -import Serverless from "serverless"; -import path from "path"; import fs from "fs"; -import { Utils, FunctionMetadata } from "../shared/utils"; +import path from "path"; +import Serverless from "serverless"; +import { FunctionMetadata, Utils } from "../shared/utils"; /** * Adds service packing support diff --git a/src/shared/utils.test.ts b/src/shared/utils.test.ts index 3f37d702..8f319178 100644 --- a/src/shared/utils.test.ts +++ b/src/shared/utils.test.ts @@ -64,4 +64,18 @@ describe("utils", () => { expect(metadata).toEqual(expectedMetadata); }); -}); \ No newline at end of file + + it("should create string from substrings", () => { + expect( + Utils.appendSubstrings( + 2, + "abcde", + "fghij", + "klmno", + "pqrst", + "uvwxyz", + "ab", + ) + ).toEqual("abfgklpquvab"); + }); +}); diff --git a/src/shared/utils.ts b/src/shared/utils.ts index f8102a93..8777c344 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -1,5 +1,5 @@ -import Serverless from "serverless"; import { relative } from "path"; +import Serverless from "serverless"; import { BindingUtils } from "./bindings"; import { constants } from "./constants"; @@ -123,4 +123,17 @@ export class Utils { const vals = params.values(); return new Function(...names, `return \`${template}\`;`)(...vals); } + + /** + * Take the first `substringSize` characters from each string and return as one string + * @param substringSize Size of substring to take from beginning of each string + * @param args Strings to take substrings from + */ + public static appendSubstrings(substringSize: number, ...args: string[]): string { + let result = ""; + for (const s of args) { + result += (s.substr(0, substringSize)); + } + return result; + } } diff --git a/src/test/mockFactory.ts b/src/test/mockFactory.ts index 7962cd74..f6f4da95 100644 --- a/src/test/mockFactory.ts +++ b/src/test/mockFactory.ts @@ -1,14 +1,21 @@ +import { ApiContract, ApiManagementServiceResource } from "@azure/arm-apimanagement/esm/models"; +import { FunctionEnvelope, Site } from "@azure/arm-appservice/esm/models"; import { DeploymentsListByResourceGroupResponse } from "@azure/arm-resources/esm/models"; import { HttpHeaders, HttpOperationResponse, HttpResponse, WebResource } from "@azure/ms-rest-js"; -import { LinkedSubscription, TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; -import { TokenResponse } from "@azure/ms-rest-nodeauth/dist/lib/credentials/tokenClientCredentials"; +import { AuthResponse, LinkedSubscription, TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; +import { TokenClientCredentials, TokenResponse } from "@azure/ms-rest-nodeauth/dist/lib/credentials/tokenClientCredentials"; import { ServiceListContainersSegmentResponse } from "@azure/storage-blob/typings/lib/generated/lib/models"; -import { AxiosResponse } from "axios"; +import { AxiosRequestConfig, AxiosResponse } from "axios"; import yaml from "js-yaml"; import Serverless from "serverless"; import Service from "serverless/classes/Service"; import Utils from "serverless/classes/Utils"; import PluginManager from "serverless/lib/classes/PluginManager"; +import { ApiCorsPolicy, ApiManagementConfig } from "../models/apiManagement"; +import { ArmResourceTemplate } from "../models/armTemplates"; +import { AzureServiceProvider, ServicePrincipalEnvVariables } from "../models/azureProvider"; +import { Logger } from "../models/generic"; +import { ServerlessAzureConfig } from "../models/serverless"; function getAttribute(object: any, prop: string, defaultValue: any): any { if (object && object[prop]) { From 127866902139487b2067873a96d3efbee78f4654 Mon Sep 17 00:00:00 2001 From: Tanner Barlow Date: Fri, 28 Jun 2019 11:52:15 -0700 Subject: [PATCH 2/2] Cleanup and documentation --- docs/DEPLOY.md | 47 ++++++++- docs/ROLLBACK.md | 37 +++++++ docs/sequenceDiagrams/deployBlob.md | 17 ++++ docs/sequenceDiagrams/deployBlob.png | Bin 0 -> 40699 bytes docs/sequenceDiagrams/rollback.md | 20 ++++ docs/sequenceDiagrams/rollback.png | Bin 0 -> 48105 bytes package-lock.json | 96 +++++-------------- src/services/azureBlobStorageService.test.ts | 29 +++++- src/services/baseService.ts | 7 ++ src/services/functionAppService.ts | 23 ----- src/services/loginService.ts | 11 ++- 11 files changed, 183 insertions(+), 104 deletions(-) create mode 100644 docs/ROLLBACK.md create mode 100644 docs/sequenceDiagrams/deployBlob.md create mode 100644 docs/sequenceDiagrams/deployBlob.png create mode 100644 docs/sequenceDiagrams/rollback.md create mode 100644 docs/sequenceDiagrams/rollback.png diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 45fbd25f..ad19fd8d 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -1,4 +1,4 @@ -# Overview +# Deploy Overview Deploy usage guide and design decision. @@ -56,3 +56,48 @@ then user try to deploy 1. always using the right resource group 1. restrictive for user who have already defined their resources + +## Deployment Methodologies + +#### 1. Deployment to Function App (rollback disabled) +- Deploy resource group, upload packaged artifact directly to function app. Sets function app `RUN_FROM_PACKAGE` setting to `1`. + +#### 2. Deployment to Blob Storage (rollback enabled) +- Deploy resource group, upload packaged function app to blob storage with version name. Sets function app `RUN_FROM_PACKAGE` setting to path of zipped artifact in blob storage +- Default container name - `DEPLOYMENT_ARTIFACTS` (configurable in `serverless.yml`, see below) + +### Deployment configuration + +```yml +service: my-app +provider: + ... + +plugins: + - serverless-azure-functions + +package: + ... + +deploy: + # Rollback enabled, deploying to blob storage + # Default is true + # If false, deploys directly to function app + rollback: true + # Container in blob storage containing deployed packages + # Default is DEPLOYMENT_ARTIFACTS + container: MY_CONTAINER_NAME + +functions: + ... +``` + +If rollback is enabled, the name of the created package will include the UTC timestamp of its creation. This timestamp will also be included in the name of the Azure deployment so as to be able to link the two together. Both names will have `-t{timestamp}` appended to the end. + +##### Sequence diagram for deployment to blob storage + +![alt text](./sequenceDiagrams/deployBlob.png) + +##### Sub-Commands + +- `sls deploy list` - Logs list of deployments to configured resource group with relevant metadata (name, timestamp, etc.). Also logs versions of deployed function app code if available diff --git a/docs/ROLLBACK.md b/docs/ROLLBACK.md new file mode 100644 index 00000000..d9bdab13 --- /dev/null +++ b/docs/ROLLBACK.md @@ -0,0 +1,37 @@ +# Rollback + +##### `sls rollback` +- Description - Roll back deployment of Function App & Resource Group +- Options: + - `-t` or `--timestamp` - Timestamp associated with version to target + - Use `sls deploy list` to discover timestamps + - Defaults to previous deployment +- **Important to note that there is no option for rolling back an individual function. A function app is considered one unit and will be rolled back as such.** +- In order to roll back your function app, make sure your `deploy.rollback` is either set to `true` or not specified (defaults to `true`). The container in Azure Blob Storage which contains the packaged code artifacts can also be specified, defaults to `DEPLOYMENT_ARTIFACTS` (see [deploy documentation](./DEPLOY.md)) + +##### Example usage + +```bash +# List all deployments to know the timestamp for rollback +$ sls deploy list +Serverless: +----------- +Name: myFunctionApp-t1561479533 +Timestamp: 1561479533 +Datetime: 2019-06-25T16:18:53+00:00 +----------- +Name: myFunctionApp-t1561479506 +Timestamp: 1561479506 +Datetime: 2019-06-25T16:18:26+00:00 +----------- +Name: myFunctionApp-t1561479444 +Timestamp: 1561479444 +Datetime: 2019-06-25T16:17:24+00:00 +----------- + +$ sls rollback -t 1561479506 +``` + +##### Sequence diagram for rollback + +![alt text](./sequenceDiagrams/rollback.png) \ No newline at end of file diff --git a/docs/sequenceDiagrams/deployBlob.md b/docs/sequenceDiagrams/deployBlob.md new file mode 100644 index 00000000..a1e79d7f --- /dev/null +++ b/docs/sequenceDiagrams/deployBlob.md @@ -0,0 +1,17 @@ +```mermaid +sequenceDiagram + participant s as Serverless CLI + participant r as Resource Group + participant f as Function App + participant b as Blob Storage + + note right of s: `sls deploy` + s ->> r: Create resource group + s ->> r: Deploy ARM template + r ->> f: Included in ARM template + r ->> b: Included in ARM template + note right of s: Zip code + s ->> b: Deploy zip code with name {serviceName}-t{timestamp}.zip + s ->> f: Set package path in settings + note right of s: Log URLs +``` \ No newline at end of file diff --git a/docs/sequenceDiagrams/deployBlob.png b/docs/sequenceDiagrams/deployBlob.png new file mode 100644 index 0000000000000000000000000000000000000000..4593ee975bc9969c7349d9d1500eda03333be92c GIT binary patch literal 40699 zcmeFZ2UL?;+ct{hj1|N(3IT<&&{RMY0qJTKrAK;41VRl+3B5QYhEbYA07ErOP3XO& zp!6aol+YBUBoGiJfPui-nRn(LW!|&S`p>_p_wyK`d5hx! z&jB_zHV&2BH?-NbHf{cYXpMAY0xOv z&&40ORsZ$L@Z2TvOst>$+joU`{qgDUv)70p{`mCz%&+(#|M>La=F1$s|I$0I20a6_ zbtNyItZP;H-Qts8 z=+U|o%<@9j;vqj;iNSMOw8Q!v&cFvx}#`t}m>t&_Z&{R=CS#N42Jz;92vD6rcp zHz)1WwJR`ghrkoW$=?-UN|40HQ?YU#G`7<&j7I6&BVu)%%l)%_{vNpST1W@|r|V;> z>opTkE*QV^``leXeeNFc>r-h;(d`tK$)Bcp-iq`5+;Xt>m?yj+L1h?e_)MR_)tnHx z-oTF;DzYt8Ui$#E?M%EBz*upz_~nzvlkuWXS)yd+Uj9?W2$=z{eLm5Op)0t{=7`*D zn=h|Dt9~JGDc#4TdQ|(eaJ+<7V>uLiF4gYos0*h|7p;-o%>(aLmb)oO`1y(Z|Sge z)g{aNV9Ux+QEG)#YSH8hFjFxJF10dMwfmFzf zggv;Bx5O;YQdtr`$=uOrQ}BgN68p7L83$wT)`DjN-k%ijJ2_pBv5vhM+EDD0-KFZ= zWp|ys6-p8tONa`%W?XOklyQ>@*34Yk+?oQrO@#wjgMFuM$}KA!;W={p0HZoAj+pc5 z#H&}oUdHmsFMU|>G*da>g78$`(<2?dg4SHc=+uY!Tx6db%Hcj?1A1CCmnLFH*I3I|}>x z=#;+6`%z!DjKns;$kNF;6!UdCuFh1nx75{)QcG*7FO8mfP!JF_BR8H^+Uv{V9iuYb zos|?-T0+<6xk8XOPa&R-p|+nQQesV<_j`c>de6|D5|BF)(~;w%%IcNI#76Vcy%FKP z-T@hgPs(6Zjb0g7THhtph9_Fg=M>&hRR>BO;bhHWDY0lm!kT1E_d2a4Tze$0R54u3 zu`Gt<8`HZHN9xTAeT--E6EbaW#}`S{dfKPnP%EMv#uo)N4z-rb5Xn zs|hCJCBs1(r9+GnTYJXVFH_`fn^N;B^g{38{EE3w<(?h{`E}p^7LF}LeJMURvY_HK z9z#Og9z^v;>Dg>C-jlS_A}Hl`H^bXVn${;vvbh#hDl~v39dU_nrT!j?kM8mOWZiU% zUu!>Wt~$vNyvvB4nMgIeh=$e9O$g@*MO+XG-|d&QAU>c;;va}x5YHK=J|St~bW1g) zQGLEKI>QsF-t}TP{gnPWw5fq_wH2#7z~mO>O|Y-^V%1ec; zzGPGWVjZ&$vdBQcZm9O6;Xb&GRH5PH@!xyVCxVRoHCWxEr@}mu3n?_)?@gd#}8A3~i}& zO=?6rCk~ey$MI%Vbd7o_QMP)Y|K8^Dfw;FHLyhbDJ2#}tkeynPF28w2p4zOiL|kT!x%TG zpCjKTPPlfXE~0tH^|e^*!;g%qtXz>D(2S<)Z>&h0kL9AY&8i2N%NMh0HKh`Y3zewe z)d5o4RMOaPWjKg2HU&cz_oha7W6iuONP_D~f4GKDZnE>etJGEInKD%Ndbtyher?1v z$3P}jE?Tr~iy(&-bveW5cN|k_nr0;$8%uG2)1T&QDlIlSn8iDZUffShWxh*!yi{s$ zQfZ&ipB2{HXmihK1C^qmv6>XBAX>(tal6w8P!=y6AK@<#y?Ec!f43moZuc)SNpHRH zzN5d}_4H7=w}njFZV`@H@08I}m~(WF+HjJiLFJ;MzIo4f1vIN~uWN!^p_K#I3lOZp z6maE_q7rxlwc2 z7*e7WL^qyuM@4MF%%tPNB*-$Yp0SpQkL{f^Z$wtPI=7OJHNBqjulv1BqwVby|Aky| z!jFUA{MuxTw0Hkn2H`c8+@-E|lL~xsw0ox`lE4}eFGNimYWo|SRH71M)U4X5WzS3K zONuLXV9%*WEz}4`Y#Ub4)cRgSROKYl54D84etW57q_eOa{$ww%|J@seG@A~oTxQohm#_*@s!DYjWN{!N*oV>X@b22Z#Z5Eh0 z#7YTa`)d1E>li*7|1O* zb@qA8a~p0i)VBv-DbM$_)bEov6t>%YtuGRFrh2d5-WwY`w3VG5L;3ke5;==elH&0p ztC|iMyFVf8WTdGtHddE9Kq+Y22y1IBoYb6iWF-@Kc=l%XxRU;egYK@Wv3 zHRe3XPYRb}LqB?i4Xu|Jj`~jXAZ=(gnoWw11VU*-kOpmpYl>Wlo!3Tv<3R|}nrO>eAnoZ#w-GM#BUh*H( zMik{rU}bsUuT{xf)#g;s{Pc~u9!8pJUBboY?A$Bfs;u~!>PlQni15>qk45==J`u=D zSW1eR9N{iqHG+_c1rCz~*U2 z4~5l!vnoo|v0oY_Uc%Z{-%js!&z=Tfu+R#A;St{H1pCxF=#Wcq5Ql1iE{aEmSq)57 z3=bKIgFKxDau93v2&sb@H-8XKpWhX33b8&(gX-&K7_JKc51jfx1mDq7!s1o)h4x(s}BP;3CfDA2EcN(OQZ!iC$wq`zoRv za~nU`KRRgg%6IcVlRj{)uDRr5pk!5T$L7PoM#pb&n2>o1F$E3NK!O_OHV*;6wD}wU$Il1%i#)LSeSG;fLTVr8=xcGnMb$lh`osF8|6YgP z*1;?nwu!;*K3wi+HU0Ya10T31%H%I$IeM`8M3}ASdcW5pxZPckek0ow<+VY;m3{kc zx1%x3A+U@`GZx8_PyITcOLD;XvngEr`zpVI;1y&kyz7O*?c3n;o3VQ_h1UQXLlEANmDwEetNq zQ8wR4!9Twif9v9NSD5!l5Ye{l5kFkb`>vCZP%S{m&mKRj%~p4~Zt7RO(_yf-nFm25 zYrc8Z|Bo7JjbNX07dJZ`c*)((@ZMu3_TGrGTHmA#1DY9KgW{VsRsMrzV@oyjjCc4D zJ_VRRvZn6fUVd4Tsz6st&xMFX#{|_gx2F5oh~o-q%|ZLP@cNXkp^%1{0lqj7g6{+AK74TF?24wW+9_KfU~nb)?SF+#-;MhSZ@P0Woz#K>t9VUwp(8|op4KM zc8o~#FA!DARtWMDhm@&gqRfor5SW28-Nqbu7}k)sctw`76<(}x@g20v>v|L!xNm)nSWqC zfsnoBpu9@qn&ybAf#u4a7G+ZTd<_x3(Wud^yCyb@9rX`SLHyG5C1c+6%l}7JlBdDsdh! z33Y}(AnOO|rIbc;Oo^5SNM|$Fg7hF=E+>oNdY&_*io9E&&Td!vzJCglgJB&e&u@#A^?UGyQO}kYCqE(cUzF%!XeJ-iAk`p~JDt(AI$eI2* zVZ4*i;g!d-h@U>~=3GqHZ;R$S|X7=x)~|ddB_A+<2mTEEgrV9_w%R!&>(G?BlI@Z z&eu-MS0qD35`kwfW**6^&6V|N&ph)4Ja(5x z$2?s+glOqiYr5guL(#{R{uUbv(DECHujh`D&$cecVQFe22|Bhu1 z_*dGgSh7oNnvJHdl@72HvZrsLuOt`e;Nj=P-N)!ePRk&22!>WUu@lG2hZKk2lQabh~o zRk~6QWz)t%yn$bdkcNJHj8!=Ad6cJQ3QTTD(C8+Ca zBc?Ep=GN1mjD%@ze5`Fi(mU&B=SXhgc2_%7#RCL4+ZwkI^R3gf@orIJiDE^hnnYg| zMjNHh|KaRzUZr0pC9z3-t!Ep&N5h*qMdOMsaQ75k^-*DE@P?h+0mK6?LJ?B5o>-B4 zTGXYO5A_S-|2oxrgGP_h!0_FV?nHQ2nQ5+K^-)0}uIa-bGKLO!oS{pq0uwC9bu8aI!bj*PBpcqbe z{@S%HJk~@0po=U%9!*;6Lpf@1&~F$8`V|)ahp2k!NjqYiZxzM^h2u7GM=zW1n2U$m z)A++n@O=%w9(kE!Z#r^rE&g*4TzORh#a7m7$90JbE2qt?xsyeGek=OM=c39&T3F z!}U$p6AuN6jM9J8MDV|JlKQ{Vwy+Ys|4fosJ$k^5i=`Un&lk2~_8$`ro{FVz=4i9M=h!hWVQF}OAh*^7OvD{)>eh0}z9;ok(AMUCTFC%LM@vC6yOiu;nUxlM zzCUOnK>%0$0pH8B?gn8Ko9$0)c{*A4OT0jg~F4nB&A_ekl8Pp@` zOQ0Qx{N@X8m283LIf|Y#N%mW)$p47p#Q>3|G?op^!9ow;WWPWG%B!Rj9FT5|6-o*0 zsn>Xb*Yhi`_pWSjelbyNJi#vDy&Jy&1J~av@6&=ySeH7c=auyj4;z4-eUi`n!>|24 zozLIMO@_hhmzwx3%#Kzy@;C-tY)){Id^>!TJ<0Z#bdmvoCD)hKXw3t8)yt|SWAD?l z2Ta{H1|5hw$|Gg=E#>62Rr@4LBd<$#|NfW+d7tI?t8+PJ=H<(+7l@U^c&P>ONFOs_ zMI2DtT8bpnN?cHmb5$~XYWkP;^o?vXM?Qw|o0}=0`~t3^i!r6?$k4QCnu}UV0ClYT z8!Fk=%=QJAsKs4uqA5*3w^A;4ytC0&K9lTM53XwxAcB|Gyz(*l{@1BE1N4wjk41@D z+?^nbngyYCp>|_|G+&0`Pematvbw@NEzaBF056@dSWQV!s6QQYQafDBemr396pz8H zKRWn{8R)N+e?bIRe~DLrzCP$Z8Qvf@>;%J(cs&$YOW9hT-uLwel*%W0hiG$b*)LnR}xE3QLkOt-@i3qJ?qWy|M?AJ#BK+=Z8qdqRqL@)zv#NSXIQi4 zYXhg!h%tkY%k=&=8$lpoW?N!zXg!Vj`jm16`UD!aK`R^2#|fie8FdZnPPGS@4%wyz z5Zs{3jV3NN$0LBRA;12>ge}B)~>?${(?<_u_N+4ojUGhb~ zsAe|DuU$b~U#KN!`icE(pQ5$&F@fOv{Lri#p4Q4$$DnwTux^FIc$I#3>pEt}Dq$2_ zq@_UDb+q5trL^rocrR2`nOn7{e`~@Wh0f&c8fHOYr*QmGpi>l2~Gq2A&E31qY9n9s~-DlxA#cGZ)eV4_s;a<%nR zmI7I`$#S}J^(%|Oidj8&pT zkL?VO^@!Gs>IqUgR=9u%QeHx2xJJ;8;niepJ7pe5>?58A4LsZQU@Ro^` zDZ$Rt`Z{&fFz;rkxY9=(N$W;Y6-Qq zUz7{l0&VNZR7wbecE;mK?6m(hqq{r0FwHBoN3V|#XF|~}HD+S*o$*ueqZL2|>~Lt* zdc&`zcUkhs3MOf^Zcr56NQ|>;#!r@iolYRG&Mm8&{YIV>$XjweTl2O!mq`=zA#F*M z?Ot7SpKh<+$Qu>N^Sz3Y#<<-P^8vewSz|npYWTpgOI?jG%5eOC%LTULhksKW*4gZ7 z?Y}1zvO+p$@uFE-{wv+Lh(DT6cLh$o>_i+;$X7;Fc1cP2-Vf#baOPEdn`4J{Dn@xu zS5WFlPl$v6e&i_JGI%uya^25cGeQtM}{WVhh^%g6MqNHnnuXEkNacIKEi6Z!em2s=GZef0Cr*Ud2dWu6#q z!D}%8B`y8-WQD5p{0W!+UW6a@-)P}rI!_oo=IG&)2Om7g83M;Z!%Z$TU7V?rE4+Tg-H!?WWJn=uP93Cen0h^ zP{a$zm0F-@^UV1@-d}@j8PhVhgF7S1sezA!q;6@Jb_?`%B208KRX#Dt|M6egz$YaCW-hK*XuS>%J z4BZ6{0eXU`UZi@+!kg;3+$u~5J7tZ{YtS;pAqaZ7@}v$-n^E8o>F~y=b3S`Eq#Lj# z5EU<(1hrAMIYBOwNlQ9?Y?SX|pdfssH*nHT7Q;Pf!L+N3>79y`n&W+C6F;R>mpK9` zBc4Dy(Lyg^EFAxHe;`DRrWJ(Fa@;IBb??rGRwn;K`D6rM{H$&hAYx6KVp&uwc-)d} z0eCAt8TvEEc@)t+#l2Go-jwPV|A^#->_kv|Og7APN^*uLjZf|jEB%7Z>lrMUGW~ykcOhU*JFV)Kk2Z?HMb8HUqz7WPe!y>HGW= zP^u3s{^zi75^h2EslF_ohT#3jlfohIz#UF9jg{l`ld*nP5nUaYRUci zf~G_ET?%V}nqq%oUPtdY$q`i!(Pp0tgTU&aT*&1HMy(3!_DQ6$^srMAE6x5RjNLXc zL+g<&sB(~AAhhR|knY1PSlHnfe?V~w@`{%H(hb8PCcR1u%L4;b*5CW%BQ9gL)>MA@ zSCHoitBF;dyJ(pIq9he)4ByQx`&LBjr{?+8>doc0k1rk`Isgu}elF+b>`0kK{D$`j z&s@Jc?81NO%7+K{Kfl=<(jh%A@7U9hD0Q488CaM>==Qzoj{qDhGe%%eM3VKOOm0JmsIG2zl<<^86jVyc(0Wn;dM zHb3N*PdsVu69w5jX&eh^y4*0_OA(!P-9|~TA)?tr<$$^0qf=fZN8m$000#2a{=b*m z?Z+7H#QXL8W4bGutHUO0*U|guf8sJd+z*o7)^oGTZE#M7%kp2^VL3+^Pw#%gaH*uE z-)}x05}X!mOy~h9M(*O*To^W9Ql8$YLmZN&-HMCib8nY`Z*c@bY8gq+P%i~ZuMsj)41MTU?=5@-jWMWqWkWUvI$9AC1xlDzd3M4gWpK5ZK7qx zrF<1NF})Vyk?Y!&M7-{Ja?sXS%Zay-N#*Yu0NAC;HnFm6y~a{l+N9nZVIjZa5NS1D zV;lgM2GC6smtpeyJ0_+!0pAyvbo8rxmqMAZX!MJXA4{BiQdLMbDBDC=WkXa6sXjfy znJ3wonng%_ipN4?RPqapkR60ok>2GG4>|Rx+%Veo=9A6B&G~kvh-v+FH;`ZNtNYsF zLU5BR65f5xYFb^nAi5&Ia7lnbHt5VOg<+wNvVy0GVWv*W_uyH3w`L3V>8P*>*FU_Y zTlBnJE4QAsk0p7HK|oxNBP2lsJ-gDk9B>TPCOmTHxvq)NFF0I^EZm+TohQ33+vJ#Af%N|L?3S`q9=cQbPOYiz0?>+Ld-AjA#R`)9h);rDFaL|97dwwIHdt z3>7Wqwyd>ASYK!1<^CvN>rd9#J)AJO+Le#;$d?|b=pObY@+sb-K}73eR8fZ%sV2VP zfC0pb;@W8ZW!)6=E6@EM411*B2K1rGeCUJq{iOw`!`cX4eO{F#MsW(fEJPWh74!&| z!gGbDHhFf;z(JTFupJI3u1Ie7Z>>MiUUXtVkZ@CTIMX$K30c%(_0x_uY0f^?eTzRy z(gFGt^vF_LB3U{8l9a>sX%(a@@@nHRl2~NT&p*gSn{ zcl1IVUr)3-)KTz!U@Nhs)qW2mZgy)Rh-pAv*?0>UW*~#o5OHn#7Ay~1W*=01Ruapp zu!1l|lk`GxdJW_k0SDzyFxy)|a5fe_R%#0Stzf7MY?lqH7jiCNK9Oy@&~q1-CRLQg z)9RVpgoR-%SM*^x&x;b|kbX*a*zp~S0oFk{2HUo8HY+u+Zp&A4!z#|OQ3-dtIUu3fO!)k|Ub_f0%61oH!=SE*{G;?Bm~3ERkesJ!gB z9pC+!!$1?sqdm!W7M_K>+>x2KhmdsV2=st!jZ~W*kSQRHL8^xmnuhE&cgZ?;I$Bg2XUS=OX$YGwG z1oVpd?h{$~K4yBQbZ_9IE@LG_$&6kiW*HGFGMe{(yVNX++Sp~cBM)aq+_ehIX$jTO z=)9Ltuf1p5Q;wOr3sVLF9y@mLky%t_xtP!K_-ipsB=#g~3-jWwCuaS|UiR6v3sM4M%!fktuw(;hojvb#LS&7NpEkhP90fzf%Q&qe`1}rJT536r0uqlP^sF&1tKTxc zNi3!V@7uRmKfN;;BCDxTqT6(e_eCPUxXs*#Fz;sYY%Wq(`=N`VOu$6$`-kqA&kj-y zoqWnL+HF01*(ixk%JPy}DJgAu=9Tx{J@e^EnVX&GhQDg^uct1h7JOZb3`+C^^!0^r z=u1FfLv^9HmD^pZd)w_+Z!4=R(GDkEW4f#zpk@eTi@Q>k>f)dE77p6Q+dtLKXm!xn zOc#`rm)T8Pu!UQ5>m17-x?u(-1fahJYn9x=NCh6XKLit7y`QX!(SE-4B}IYK6~RAuG8xcaq^{pH58>o+G8-}+^rTKB@?vNkg1|J+O$qLnVRibgut$9e zvW|yjue{&tJR!X4l$R-*GdJQEeMqL1X_szFpUb!r%yjy_pR#O}Q#3B85k76Vb5wLN z+A4wPA)E?dzf32%@U6nvF$+e0oIP`H8j!sLpP@l4+@HUU6h_Ms1Q>w&Fy*&^n%KUu z-na7pIPNTPs-nV}sVd@i3b?9lsm3rugid+}FFY$iD@C!!p{S!1MBk?>rIq28Tg~{| z_bzt`5$lH5)*0$JZ90yOAOAsP!`T(0O4}Av`l>>A7jXzlevl9W9*hv_F+^x3bN9I1 zrg{m|AMkbjW|ERQ54LYZoCfLiq+Q!0@Z03G2$M$bxfQ>79+;KA;Lb3r!QI;3$exD# zy;~Whz2`g=ca-~7rNqGGPdiwcOIW7mnpnTW-MuH2xLeoW?8HVDcC+C(;>xK<&CCRS zKt@>B{~C0qKmR{26N=%(zS`R{%O|VwSt#(q;V`j>&ca_TViq7q^~XQkktML8*FlJCM}C5}SQ*)Yc*$#F~Y$W^0M z477iHoqyY^*_3MQu_MvEb7>Y2Wt_ShjQG+DX-Tw}S6abyKGM;YNrT3*eK`1c9)I|! zhP_JbZ=C9%96oOmv@8^)t?*(WoBucNmX~**%lo)p0UE*)7S!X>VRrK~@2qdsZEt)A z++^~9hGvz1!f1O&Zq)*h7_eUf`ftL3-+}D|=Qnp|J;Yng@>%|`&HIi)cFfj4z?-%3 zFs_;VA05vY_>VyY-Tgm@eTVQr#F=wU0Z!%w2p?stjG)Wjz${h|5-sj@%_OpMp*JFc zU_cian{LM)(>M2-0+5TdIQA1HJb;&_^jz6mpJai%dVsk2#sKBqAOA5k=X`W;l$scX zmD=AY+h>Gd$#bY`1vESKBS0-KqNiFw%E+q^P#$3(AdEp{YC-|`BJ=9ERw-bu!W1e2 z-0>HRYT$9qzQZKV0T2d=>pqtFbf|ap>z4+A&5UxCRtK#b!0kw6Y5-g$3E+&Jw6i!+ zK8L8##4LlG?29vXQ!&iHd(BY=w<0%29}+Iwu0JSV9DC9Ho50Rm9&Zq+|S;`Zj&_R$3kCVwn8Vo`&Ela^@fPRyeK)CDJOQ)27~iKp4z!MAr9;RB2c&koX4D%sinZ{~ z+%Sel6R_(EVQk{er2K}6VqPS63gD~4@L`}_^XjR;%wGFH#J#f=w&AlrlM7@t7Ok4k zpb`!8&{|_L2IdezQzsD;PZzyv6U!M{ZWHH^kePn|xgFM}I|l@Mv8*nLEKO^Jw53`Tj2^+$!6c~) z5RJLiTf<_Cn!H^vhg}Eu?-+>_8TdZ-81-IYYd%j2!bLwi$fZ9Od~{l2ZKO!`b)+mc zlj^218kZ65`=vuVul=eLM!RJb03I`3kxI0n-WnyzwGpB>Q46FkP8hoXM9e4Ld=;r^oU|9x(1!&;Ojh3v zO0?3oc*}IcfTFTZ8~S^GNK>3eU`5F)v(3!rGU{S649g-8Nhi#d{oWqA70l~gjwfg# zwbxu6Hj1^RMm>5z_vvVrIC$4>7mF!+In;%X0yS*N!QcKXVAK#vRuHMVXw&VkcuAtH zW_ZkAe*gUQalC%U8Bp1jBHOOqg=-|Pa}J@aK|n0l=Tz>oFuR^z$YtU0S`HRGf#wJC zO8Snh{9)b3l}A;e%N6QWWV_TJhD^cU21LzNj=&;*NCR@Zf4G4%&H z*^-;I*k?~V;&8VT=&%NCJP$L^W|aGNBEGLS1C$5P@46CigzfHTXc|^|v=GGtjzgi) zfQpbWgNk$KqVt9-XHtS{0sHeD-zU+!*}QKoU~M#~WD$@L{l<=ZL15KgM`v}eU!e{5 zDi*kNDZP;z2r~8pEQvweq&rv|c^Cj>E26fzN&}n|GtEl#Mh1H?ZUS{qCw%ewNQGKg z)vV_T`g?-)wRmVRBH3peM{k6N=9d(<9wV4bU%8n0;~QPQj0ZcW(aHS2HWuRw_|=g~KNC zc4)Da^YBR~ck2o|tl!8@yy%zSd=Q89iqeqz>hUPj#T2nODBGywZ08^ekl^RQ6jo)e zqvp$m=CF#18O|wEyPtQy<|)8mZm>ZQU(D(tl*|#I_xavEX?`|Z8juDg`W&ig;qx?fnZjR?uh?w-jdi-&XOukeJ#1@ zR2%e0XbU9XG>K9$U>Uc%P|(D8R{in0htZv@B%01F{-&pv1-Z#V%|9&u2&pT2_NalV z2@pz%+c&FfpLBZ?bWlemS2 z8v=`?L|5c9Mj}*FH)uBhoD5d>!oz0Egtl!dpkz_Eiu-hp6e`qqp(!?r4~U{rWs1!U z&!JR-HL0^F#tjTYa493b8pDw2=ZW|`DQOsC(2E{ML_TP0os9K48|R6TFMTwf%t#wS zk{6em#~nwCU#94i1lcB!p!LM|I_l!5)gx8`1R96gGMP4SI$B`ov*7=UVG&oIuJr3Y ztKx+ygHa+Oqsc)~PwRI*pR-=-|G7VbEQ8-*V4LSo%VXFh`4IQkil@|LwhWH)EA6|< z8Tm{moFlknB#m>QFW^H7xZ*1n12*yTW<3pvd-IlZstlXEv_efBdxs`Oyk<(94WdIKpAMZ0_9j#H{tEDt*EDOp&ZSufYfKFRK4V5}U zUA44Z)hc?h6p)3CqGQcrZ>~3;IhMXP;PW$UY({xhQ7L|GPfMlGCP!GxDP^7R(<042 zE+*)}5|um8dC4tQ{<de`Rl;o$D;Tku|Cune`-FT>dy~`TJ0qN|@bh_Pncq&5SU2-z768&;W zSm>%H=SLgj~-Bav>p$3VFuI6j1_?` z20y+P7LH_%k~l`Y$EJZChkQZrJ+)MmO5xV$&l)Es@8*=hpBc`e90~tPG_osc_0?1c za5KE`db|Q%ko_j5X4wnc40%=Y!NyuVF(N&l45FGDEkoQbT@`kfp>ky7jUVdO#unB; zW6$;UNtQhadIq{*D8=9Hxncn0lFsRD(bcQR1y^prW{<)jd2Sr04j&Y+&i8$< z>3@-DHweIi=&wbcNetterFh5Oti}g-ye=A#h2s0Z_SQs_BO6;Jnoh!;q$H#as!(JDAIy#r5)UzTwxca{(KMZRtx0oBXf% zi+@!_yj*7jfcI3x@W}P4xuC7CZO9hgV!PycH`|Aw*lqr0;0G|}-6BXakEW~Remi{r zs!Z>5Y8|s|`@|L_Z(Ek-nfM5(K-5hboA(cJqkmNg{IGuW4{1yIVdAgYt`SFkC z7puS2gN3ITBk--f_=jwTIFa=@@v?o-rDG8I&cquCV%Fzam}*C!v;#e|;MM;KVEqrg$!{Q)?m4uTxblJyc4$Cim}^el5yUHNn1>@xJ3ifI7B;JtB_?d4Te zHOOE;u1`jCg5rjKA^d^f-ayJSal{CvIuKhw6@&LXV1P0uo(P2Avz`iILdCHW){S8h_>?T96{u=w zffG=`^388QO;kA2aUfdl088`ZOgaRtjec@n5l zA91$HS^XK9LNo0Ym|Nq8RaQaO^^EMIG zkmxXKXn9HpVhez(Y(B=>$#sB*%ld1BDu`j}V28sj12~sMe%WRhd1d1F`wbk@z}2Wu z-O@m%thW56x9M{%uBG(9o6)4j6>QAgDw%|fizq;9FtE*%;UaXbrrzx3 z&u*P+Oa?y7&YAh4BBCFH{?lXfOXfI0ppvA0YBWHGgIZ5yTBLZ5AA(fq7G$Y zSO|yZbT^XOekmQI2VB!%;CotfjnX3QCxehUks$KY8H>e!qig~5`}Dq z|3+2kUH(f!)vkZek;hkn;!7OB@Qu9;DHpb9=`ju9-8B$Ftz^@UyIimM;aNk75`pC7 z$5lrpd66wOKxk{>mItg;j`pJEMiH(YkO4~B#XYKLBed*{lrT_KM#m*q(PNGP{rRuY zT0*U#C>315NmZowkOP*+>r(CdKH6wFyaqcWkv3Bk=eqa7-@E#6_~7TJzo7)r zXr|sYmMTL<1hU&OWR>SGR>T=SwI6gU>L3hh!wdyGN*!Xg;y#qZRE+h@>YmHOas4U_OnTy0WV{}C}IV0l(LBmu1%&3d_S$t4DM5@&KV?|ru}57^m>kC zsi=3p&+O8MV?jSxX6tc6dd5#yI_XX;ul-=W+5dkByb6a@Ev-(M0b76$%4Ifcz}a?& z8gaADedg@IcI78Y=Kh~~UjeC$YObfCpbyLPy`J65TLA>b;kNSKuMvNicCyIC>+11$ z5B|}5HygA?%26MM;HIZTy}MQClS>n`dbU+3CimiJKT1F|3VzMURjus07OAjmC>ec1 zXsuFRr%270TQy9<>6S_zd$w7>NHl&(WHK$z`~WcUqxxT`eq!%<3G{9SSK z>-b6vvY!f7f6lO&xzl$lG}g;exFld<*H!XZa#X^924QSF;uY*M@(eb$=~HXGOnwSS zxBAO1D*f8tQ@R~&0AwoIA%QD`l`X)PhVgp`Q}@&-$7ZEO{vPCe9V01q+}@;^lgS?3 zn!3Mk)HV}W@s3_}S_Y1lTm9M3*LkzA_asz|91?gzr;pZ)FoRw_e{B1$vV-uW&V<&X zv^-4sqsjno&qT)ct7uBb4S)mZdrU-Al!CV2y96~V7lG+l%>}BZ3N2}Bu=fl~#JYKp z2RkvcZ_%Oa*S&mK`jnX~zc>CC+ccxO(Y*}B51ZMU8@>>)OSNylAIQ#g@sAP9C!;T1 zz#`^UUO{h?HG!>=$XvPEscXOIwvxy_CGR|x$9PKAV&Bzf4?F$Nu;10|Ssp_OaZq=k zW2ge+=6CHTv)gkimhAT?Tn%h7sDvsdO; zAy%*oSwWl~zBIMTPU}^;xYu#16J#1Z(*^kQ>#5h|mJ00H6M^l7G}R^RMRhFM{$B%R zzInP8-&N1B?)U%h zFyNi@2Ncm^o-5&!f42IsV*blE!ZScA@*G7Q1n*I{O+xECdPJVud62zu&=q%M?`fs3 zTg^4d_IM0%z$Sk~1lW@iirdTn!6W~y%=5uN&|Hr4nfWeJbpW0Gq9%xVID{?_&9e96;0Z*7>4?99%~DZ*5B zymgfYy*_FP1qU&gZ3zq;PbE25dj5x~tdB!nAzjIY+El|*t>EwQlFMYKsbA{Wpu-@WzW zp@Xp1LrL3(YeP^O;5(}6d(aZrs>^r(Lo6IAo&lTj&moV0sLb~p3`kiLs4f6v3~9AD zuzV3=M43>_(A%@}NoVz>G3G?wKUxF)P?^@**7+-!_;4kr6_pTBSf}7@D1&Qeu5Mo+ zyxXAH;1Ve3Hz?Z+O0Kz92j|=^qdT@F51y++B%|f=H`u&$QmAfkZb`!NH)4;gkeX#hK#FMnkfOx3L>mi|#eZwK5|GSt8(=LMg65~8ur66fH`-ReDEzy-Hg)`g{@9c(do{_)}s_9%Su zhM=TemQkxI&JYZjnt5Ku)^J1X&{TG)FOaw3W-d&g_eJXcmx zbEJo^*0<@0`Am>&?&KOAtF|2G4{FN|+Ex&Jr?eO^PMP(`D`IbPjZ((dFGMGrP2YEA2E1%-3iRJ!20d(#(9Xs?DLCdIS=;lZj` zp1cZazx#i+cb-vAX7Ae9zcZE*L`9{8jozdP3IZyMNSEFb=^!8_fM8%|KoB9I5PFwR zXrU9DN{N8f5K2N|Bt#?_5FvmGfp>=~XUh3-)_Ti%*EuJj(52yd_EYxW*ZsS$Te$a% zdxnzw$lnk2zQ`=_!~Zkf1Mos1E;mN(Z?DM1kzb+qA|tPU_**!`|D(*6 z{|nad|8RN@n9+AWHZGi%+`Z`IG3+Tx9^R>cqdl%GjS9 z_CIR$|2=uW&<@}~+(J63mu~h;wLD}Ro!?%bux4lCB&Lfa#QF)_qkx*V5@gtMvt9dg zeNV~;t5!?x?OX)}MJbXOn6q0b>J<~5pcu_#rTsbxRNy-SP-yGFTVj(>P;K*;Y@ScB z2MTG0;Dvi>tcuf@u5C{HRS1&u6r}3M3_m=(5OTpFjSwV+*}^6Yxg;5miu%2h(rFHwxV3TucWE`G9U! zQc0{3-~jU-)k`;F#CSE;y#R4R&ORlk3-!|Duk+P_Zszd(7ci>33;YhPTWUvv*q-OW z-XULJKW_P}_?;C^6w=cKJnDv64|8|$1xR|P{p(ME0cUjFc`(zW?F>H)38+UWq`>)R z(+WlX`Gs`TPcaSK;|V)&4C2^+-$@tR!TqsmFhF};(m9 zR&J34dbmN3M_=`uBh$-`=>I%tQP9mA>AU{g;MhTDmP_yz7|#Hi0-{8)9Jb&H9(xSX z=X*?}?~AP67qP$ght$P*#r7t^S^bkNzs(s$f9V52i)90aTiJMXpJ*xmcVB~lY0@P* zKLGDMRX=vMKunwhWX?_T-WR{{4NsWmMCY-!X} z_H5I#ye;J=dgIyh&B%)srg7fP4hl({E^FPfee~vg8=Ldxpq?w zRWk(<22o2H)SZqBXeYKeM3^m6ErmV+wZ1Ga-|GpfT4u90hatfW^eWHBiTC5*7IFlH z^fJw}4p0JLZr2$2=wYX1ime4@o%kzi-0a*u%t!!} zls={yZ!;U{~@g$vmF5GDPU$zkO?m|E}+nSs#mYG96v0_rX7Q&@L$ zPm#^C3$(B!lIqv$ZPT7%TUoaGo#X^aIetui94yetRkoK|z+uZ&;ex>a%+h1Fv#;x; z7P`!jzNjCDDzjU7ln+xVqY1`Y@TdpR8VVFHnAp|1IUD!aEx)@E&+Efh(6i-MoTVV) z1D(>Ir&KBM z2%T`q!><*qj&YhW#EOwTn=sfpqwSr`rVE#5+6gWxd223=sbg3k0V^?{LvFlN9@Ju* zmghK<$=_o`u5NwzyG}{FEnvGE1&I@{Y(UUIll5X2o|5Xsj!fMTXMO7p zB1y_J1?y17B6^SDybr;Hik8zUza4Vf>QwUk3Q)tl(1RQd=f7XS zITK+5Um3Hu*pVP3rZgI10?X*B``==%V z)gfwd8t8I!omp=R71;62lj?US=xK2`!l+Mw9?V#FgCmXYrU8$F4ZYR4pzmM^EkTy= zm}QiEOFEBfWQCKGttx}>zg*srMaGh@)=`w90bJbPz{#hJ9Sr!CZBehBtn+F{eM})_ zSC=Rf$~L-+3u&d@!&_Ya^8N3n%yn(W$WhgI>-)vw$3Lmte7rIHl{@)X_ub-#kp{Pu zh4boklZ3L1qPmYFoH?jJRYu4N5ytsV6Vfxmo3BMn6Mkv^=d_9~`&$5`(%To&Cyt~z zJ}GCTQod?Sw~blSYz6v;KZ8lt4r91KV##yetb$%_DmG~Fn)*9^t+koAwa-~)$Oq+W~?sR zkDWbwL*CtT{WV(xB291H(Wc*qp7qO>zRVJ11yfn#SQ2NPZtxy}qp5DQT9IMv`2wue z=ANiz9(TmtC0KFLbV+Q_^NkbeyGjl4x_;gg6`75va3Gb-6zsfQ#C!#Gt;Z*GTQD6k zjh$wryElcDC51n?-S+V`Z(H5SdKN!$(vT;xwDcZV5-eUC+EG8@CV)%fTzrf~x)Li> z){4{g(i)k(X4O-iRJcgKd+m7hW(@LLR0FhlNJ@R>d!P4#F}SmxS==k~ai1YHe(%P@ zE}6rje`xffBZsP0c|&C2$;Uq(kiYFzKbTwm4aIJ$6P}yjb=uO->VA1pC!va7 z{3fR^I=Yt9nV^H8v0cLZrUftMj}33Six4A>MDAg}#U}RfLhv5W*Jqaneyv&+P--|e z=@ZGtbS=6y-PkU5N&$XN>;nw#d#t-l5%~DrlSM+Su#=`auF1xp)m&J(?S?ml1D#MU zeO`W9;$`yP7Zxvmk}cPN7}A|bvtigpaS@xmI(u}tLld(FpjUJIZHbP`iVeQz5@=$X z7mHi-oX)Cq37gaWL%WUdwU;w-bZc%56GVfA${!sc7?STs7fmMldr@boZ7a|!6S9gg z8sy>Omd1`xeI5H;gq-aERx6Wlp=ML&<}59^8F~*rry6yWg!=YbR^AHXS0a6MbSNTV zzWwzn@X}wpGR{J+0ELGAPyV1{z@p-pE2{(N)N|Qn<{h^A=e1GqA8)5m*Lmb{rJx#OKZHRJDq;XQezt*!>oe&E#GNPXadko5zOMi43WAC{F*>w-G34u5Dd^BJb1`|Wz7}ee^;wgH9 zy60Vdv4V$yr3*3>VN|38nIwIL(6Omljyu727-&`S>~O$pQwfjy>SI{+X5l7(8f(@e z!l6w5c;{v#Vv&r<1y_oJN}0arrEc`e75yWNfgqK&EY{f_V#V=R=N5*&xA}dMCQ$^+ zmp}9o1u;7*AsM4 zuODgb)2hLErz{OeH474GFZ4%3>(uz-T_@VToZsRE4{1n4hbvVjW9VfmYi3a=IpxkM z#Rtq7Hzp_#aJp@n9>h2gjH;f(>1TEw@vsY)$f9+A3J4ka&dn72KDFQw^xg5LH|jnu z)!BK$knQFtZ`mJ1k&G12^#%eA^hYAx&lh788#AC zBgLbYbKIlWI%X-s%s%C%yXkegM4t@2y|dw6-6v{}I_cBqH&62jb>fgEDXs2jG;0w! zmqzG)N{sYT`Oz*Bm9Xb_&=Ts;6n*{Rv8%NVa#Njk?QPl2)zw0%$mm5qSE!VaR^p@* zieQ^kvY6@d@zxUjIJaAiL|P19ik#3|H$)}fR1(J?eI7k6SasDAemSv}fq&>DP1ncs zJNT4IAN7Av?%2xKdbtsuuo7g78BQIC+L1j5BE@caE6VXF@+jV{ou?UUuIBg4SHuoh zsBjrM$m(rie9nQC4V&363XASPiJzsf$NLu;`hO;vP)i#fP??~WCJrBDOl0|?2j?4PX{y(PX7}Z-#33mja9OCmg za8g~j_nw4WEjlUDUQZMT2<&d5qn0yj*{EA@4&M}bg{xB8x{%WJDcnNf6NIfGI_ItE z*crhR5cljb35eA)igKR96uSuV(D{ObUYOt|%QM2?mFc6)Zk=mqxzsJyt1IyJ0h@Vs zh+GThfO3E*kDAC`IkVs|@qM_fdP6y2CisTv98Ag95vjdD8;dVQyWeTmf4PzOj*E{+ z&h31vHn*RDOIwn52j{)85(e@}jiVGuVk0fK8%wE%wxAXH9LQl=hbYpFshCq~QZ~=I z8_n1VBjs`YZ`t#uxz?S+ZJa0jcQ6gbt^tP73!Mcryu#S?6m}cIT$v}PUw`C=xk;Kq z6+-=LFVhn4)K>UhEA9=8b&ftYLsThHzi-`NJz-tV*f1NP|nq;4$GB`YLrDx@N0D-fjh-e1&o z`zNvW8zofQzHz5;*|VrqcT?zH8W>f$Ahy((cF^*OM&^_GlN=QYdM}IMNOOeQ-I$Fe z)zdY{v{jvA;qZdZ-AN+3URv;k62JYCq>VySaAs?q?#GJ6tCvSr;p-i7L+z(m3fbVF z-n|pEh*`pX(`7FSnpF~T_g>zvSH~}I5z`)IT*ZEsKcY)F_!L&Jul6|4d#k<;mL`8f zXYc8lk(xE8Q!xj~R)ly#O!7vWpq~%^sigj9q3`O(DhI~~`$5#^y@dw+>H{GeukiOj zlZS&07)l4R2>PZ!K)?8cr^0X>r#zzKly%E)Cly+mj@ak1W&g0=Bqfh2_ zS$%~Xg1S2n682_1bFv#Vxe?Yy8lxCEq|&K@xN4~wNu~*T#@C;Vv!Xvfnw2uCIT?~O zAQTvmv-k01#%VxT^HO!_-Qk?(v91XeGx8>0#+Ui~9CA8LG7 zw@6t`wZBJLWLujMlq%DPkPl#RNnSh-@$oAgcpQqTXfbzgF^n(1`qqu;0cOd&n(5#; zQhV|-L&{n)rNTmh8JJ+wp-EF?Kx=Jj*LzcO1t@>p_3#-oUH+%%L$f)c9i18V!Ug*RsE9v{dlhXkK?bj?`(4El@uI?=Mm&NI-ny9Swo8^%d2DER{?mRH zErX;}N3}oB{Wg(|s!jxr#nbTzZq=y?MiSyUJi>61OtT3$8|~`%^|u#n7A`Z8YhIf< z9wD&_4+x4}vu1pWOYHc@P)m&^`ADZ3;=GmXwqBZSp=Fh?Qqi>e#MQcsDp3RHyPd`& z)8t%^#aw6;UaZxwHAuIq5xzMA%R{yI)}!Z4eF{;ZTSb;1uzRgQvw9#pyox!y+Y3=D z2BH(v+zY|+hRTl5M>nlPvb-{q<%~6)FbaVoJMdd*(z+Q)4K@&1ObCX)$(NP&XI@ph z9+=P@s+NQi6Eq|0cyC{kk7E^eO*vq(s$kPj?!zyDME(skhKxZ=;z7iZU%aVZrunNE zLdW^~@I# zPrv_Z-k1Z8O81aW3{J3b+}yWuAo67T_00)#Z&^N{(Io{}Q^f3M#g%i|126pJ3zjQ* zA~uVJ7B_0ON&eD^Ww!x>RdW-N!GI$8Ct7wdqaOY=wu8T?FiIHG2m5zQ^-o!xBp4U< zvYIdc9>@MiNgd$T%wVKa7u87@sb7(XdzW-FuOMkYK@l&cMFXPkBAz_ox?LhcignbZ zRSp0F4Ez9K4ID*)*Otxsz`=7707N(kp8!%9zON+D4N$e`)q#c1gGmCjotxKT zLJj9~&WD~5)4tl7iUBg$302xid}J8CG0ejWvqv_(%$w)e+Lt2|+8lNAweWQK_XcOK zuDR02id9dygK&>1ki=6fru};y{U5H`TnR(==20ziR=Jll|7JG)-9Ov+Dlt0tj}-bp zPWzYN67&Dh0tf#kzKvIn8L-O4fsZ1}YqXfPd~^5G!nZ&{&2+C-p|K;?joU5Pncm)7Gb4da;uC!-P z3rwNv3eytHj6yTDgHpxZWWIBk4Hg#bMhR4(ZRF`xEdem-ic9ilFD z%UQRm!uS&WLK?u`Cjm$+F6XrFA);asI9(WT&w1@HX4~+1XB$(%1B$&ITm|4evz@6h zFX%_{0XX#BBq<|M>Kez+5}7$7WZ4)0ShkEY0L^l zX>DhHrNsh!h{)9mC5%@K#ndyUQ|cTD^ASe``+uV_jmZs!0Q@5`2z6R*@yp*bWu5w- zg{W4wETfp>eS=n8RG(h zstCxxMdYFUr`_+kPmdj}cLJ=NNpq5^rD!Hdkz)en7v4uZ|9tsCc|d77d0Y@ZbZt!s zaZ80l)9oJ(fRGil1hRxHVU1QUFC8KMS0-7icQoIQPggTW}4rCLNRJ zhH530toGN#J9V>6>nS>~!Bt|R!9=aV;jENd;55XcY1ZK}IO0yGQjoV+52X1BzzSB# z){f8KTrFsCSSwUj+y2to!p-vw&!6i@q+jw2tdAfPu%NPmK$(38n&eUM_HWLJE=~A6 zxm`=}E%3Akg7_zK;04Gu5NugxB$q8Pi!@9Rw3Eg%;Nrgkd$-w=p~N{m2iWDZf?%MI zCdn(872r<>o~td6upv1QW%E_GZQ#lXfcUgDF{R6l9ZbTaimno_fJ-1>{ysaG{k&}Q$1T6UlO(* z8|jp6;hdTWPa9J~C~ef0Z|nkRg9(Z3I^i9*vk+#hV%++b+PVkw4eF%-a?feMH0RkiZp!?u}0r_Y=ldb2L1`aPRNc&z~ZE|${ z%38#mKjm6}r+!uSMd{1^qY~)}5AH@&KuNFX5N*e+Ekp-=Mc?Fr@`%UX z&6J~-H?z}AfX}N&HV`$0!R2wCENXb6GN{agCpOVuJUof>D%`hpb=B& z1WA_sJX&q0Jfi{3Sh1v+)1YP*sX{eHS4A~-p|^f>g@ko=#`-^Iy1K5Gm)AeW)w!9p zu*ls=hn2~FcxBV!7NCQk^yoN}#+{Z2wuiDKK30DCg%UaY#m`N+VuyGavM(=?u}sv^ zwGd{p<+3tD_{`F^DNci&44E^|^F|k}1E&``mlw=W2|Q=-j>+TFNz=yKJ5hYse@(Kr z2C6dlD|7i%A$aN8UtFtb4y~Fb`5X{cl#~W9HU3$g-LZF_89d7+$i+^u<8nx0^#ajM zN&DHG9j`?R&NoNxmZ)guDyxW^g63%NCtZu~$!Tbf;qCFg_ur-VzV3Sg(GHGH_c5GJ zo4Vxt(WpqX){Yq=O~n~lq`kLRL)R@eI>cQqq9x=`gtAjWPXa#?bzz5z-hGx<6?UH? z?x~JHG=gf9d!}0}_MAge==fvIx91(xm)^tqKtAw=*k-=Qo|KWQywCm;&yRNBS^l~* z_5{blj#{^b&McSh|J%!u$RpUA{&ei&^MT`JS!FIME}*25&xuuXC)ghG`ahn3wVvVXB7&iTRV4@3plAWzp877Wk~1p` zu-V8m_0h+}@8>~Iv8w;joMhv)VVWMF3$BCgM{OHVgM;I^d^j1T) zWZ{v^14-W+rKRfx81>N3`+uEHpFG^CBCP0;6YjOlk2z!i2>aCeD|PqJBtChza&b%0|CyXjYToba2FadiAoUtT$Q>zpB76{>t=G*wA^PoxHNtT z5P5skGrIrd@`2A!0~j0BsxTqunqDE$26k#FRAEk4o=d7ChW(dm_1h8rKjcrT zr0KRq8S3w+_)is8M_hxJL_1^sqbvB!&l@>@6uexgRY&zV#@Zho1vyJI?4aA8z`#xN z$(b>OMO(wPlYIHv7sbkGa6*xLhP(hf9@>dtbxIv>p#FOpodGG*6 zQPkH@3nIZ3#g}p@LC|z2k)<|He^a#QQ?@3LADP{zGE22i({b>PU&rVvZe2?XS&#nf) zlR7mrlCJrCeMp-;60rAixAM zb)Dc+l!JUxiupP@If*eeiH}Vr*Uw6hsra8)RCkd}*jQt6K4~%-`u2ueb6$?+NGR4z zj^q4h?K$@-Dd{-YLKRPACyKDS|214ya-oJ?p-snPf;Gdd=u`dd7>8x0Lw!!42rx-7 zxxnf1!624RKM7exGa%-_8R9DhE-(8( z6l2D7E9rGRZ95%e5&E;$v~M*$VS44sg0ecB>&x|yWn`@MuAHNoQ%QO2$?2>Boai(N z_rKtblTSQ_@!c7C)~)tR`amF<8;$X3Ih*uv7&csgE&?ViU#9N_ zB2yg=2Gw@A^FsESbi_uyzyHL|GY;GW4r5X2BibEq*IdVqD9GZCWDpglw@!LW(}EVE zvRU9 zpGmk-OqX03$WZi;U!0R?0ssF~CTO$Ds7EOpZk}HnHG^ezQ=JZM{p}#TLHDSaEt|Tv zhKr0`#?N`{cL;4c3z>d;6m`V??Y|kmfu}iFVx9D+E1JFYeVSa9&DYGotQ7VljILX} zGw5-5k-GcmMq+zAH~4J;heDVAa*hHs8nsapzrZ?^(l>5Ms!oLA2zh*vtKEx09pb;8o84qjjF0pg%YyAxd5#d}{$Afho z_sw@{r&_wCZ<0h@IQ?kDQL3j@zrKu*0GFc+A!`?p%#DVAxFPsQF{r8>4Rm&Nzv z$T~6}D#46yWs?75+W(FV*;mLH)aQ9*U&{RHJbnY$f8Bxp!`J-hK&}6jINldUEF?fe zy#-s3$l=$Y%4-MRVav$BH=+R88=H&X(ZJoWcMdmC#9CmO)Fu0$&Qh1B@7GRpf9I$F z!_3`(v5s*sSV)CtynKL3JFW*<_y8z-@TU>EPM2|dOEn{oCjy%I43j}d>-DfD!~7L{um znfDNMR`?6^c$|gw$z7h+sgmX2`L3SYyjEsQI{px-)DuunFKwWM-fpJ{uPh;$RPD9$ z%=frJy=lqqf#?0EB!!)6@`vJ-Fu8{4C|JBYE|LG*|z_DrBZSn`X8q&SCzvI3{tijHFjkobZ_oUsG#1Hi1FhT3; zU!GO3dF8NbORPrOf8B4#DeFT+kOX$Vocxfh-cZ@MSS59tkr=iV}`rp@o`%TTwls$9SXKnY_F8b7vW8V&>6yHL2vECq80e8@3kxftX`1 z#2Yrcd|{mN)lBYfx(CCP+0@NAqV)N!tTp#QdcA~0j#k(jP7rPZ`%s?JBQ-LX{ z%y`kSgA$o4^>}trGZ(pvZ@c?M3g*>6j{D$7mGN>JI`HW3W798;}-+S<%q@Mm}Q_vi>9AfNa?^Xh|sh{xw7voMr^<@zy|C{*z%|8)5AzjgHc&qUP!GpEaR zc{qJ}^E9x`F{g^}|84onTugu%hN$7!ZsmVVvn&t=z&zuhd6@s-HrV~&=v~J2Dee^k z{*lY`|79!Tzn^J*L*&=lUB+x@Bj}V2=yFH-A8G8SgHHO1*i>PF&)XjG4rTf_C&)a>fc!gnt`P-lObF!musR#)cKplQlHD#!$r;)L zUt*7F)pQ7I?XHST0v)f<#@IoHU>p*mNg?yJ@6Jgl92zrE7XBPro@Wi>HUZ_%UZZ^d zU4|YI<$W$|mrj9$V12H4ZMBQ}CkL#|0_|VtyhmL|g3=3(EAuihOzd^pHp{N&sfqjw znA~HSV*Mf0xEG28sWKOsopCOs4&?xDP<;DKl-m1^?{Cf&Aj@S`Y==Wmuf`m_-6seP zs2FZSc>J8%)jNCrxx1^b*(=@`8F_tFJ^Fv5LVDa&HyI`-_^i=fNYH>Sm?c8rH){Oosi z<%#D?l<(*sRlllG!xle%C=LcIN_d~(9y8k1NSeMY7p{Ro~#|v z84yl5g-UTkBh-8{z>xP+!wOT>!UkAY?XQw`Q&y@s8f|E>)jDv8+Oy;bDKN}xcDPIc z-jWaC0&!c;mbN;gU)3~Ib&r5i8BN7ZBIvJ?KHOC+r?giCil(=gPu*#rtc6-#oAPKA zHn&+s1$b)W-S9n5hLE-PB3bT^wuXDp>7(Ptt8SEr8o+7<@74q>)%(al1y_@sjhh3q z>QytT)8Fl&#-Cw1<27AKobDTIJ2d~G1HXBc0qcj*r_P{_39A4Qi5d)Sfx^$kxBZ}Q zQTdSNSPgSr-#8$5Hi=sXRpPiPg#afWY96;iaJ21__n8Oyj^-Pnu;-Ko`B97=tg` zH7SAzeKz6j^L28#q*!!$dCtex0n>+D3bHPe8KrOz^^%`ARwR<2<41JypWDt?sHy0Q zQiSoQBxBqD@|`PEo-JGECU>lh{8WmPMAtf927@DAjEc?FwK~g(rtCjo1{T^Go_uQ2XCH+ z@)%nRii zYba!et$8E))bBS)50Xcm-)^yKu=~}i_z5p`P*$A{u1G#WkRoS0ql=e_9HrqrJ)Hbj zA3C961pkggckmRI`{yopxi#x$>gBfSH6jzO6mv0ePIY)G9-pI?v*H?z()1`}yGre- zUof&2MrrRK@&uU(n%7zqVs=<;NcUjHIQ(LpW_ zk3zcjVKKTanuK<{`3Bf&ffUJF)o1uhs+fz9OATq<4uUQx>2^6(fv^CCe|h()EL3U| zB_Frctj%eG~?)0IOf@kCf3+UbRjVm z&XwB@p1j)6**vrxJ!^g49$GNY)HbNjMD3UXuS4TN+eFu@fOsP`n)`~nn{biWm&JJt z1Aj4(KInbT)w3AX6;ATO`(-GF`GZzf` zH|R0s#+f(4sEk96&x!aPF9*@$peKz8VSZwBP@|_HBL^Wa>Co?hZA;kk_%NCk zw1Trd92D_ou{5JFDymXSa%G+W5;wS(qA*cdh5U?>#da}i4mmq8Str~4`>oK{4%{c{ za~nlVqdPSdZc}yEZmW|BASa9Qc_2quvwM69O|cHeUa=Ex!l~p2Ik(3xL*-sLXLNB5 z2##%fzGU3u=TtXMUh$8$m46gzSf<=-sI)~sXw-U*-`t1m?oyq!3PZG_m~g#@1UVo| za8abhreqlF6QYtG>1Ha;)C8&p;r>K}o0T7$50Sx8ILAVoEFq-U$neB3c|8!d_0mFH zcs+Cn77~gytgVS(+yP%eY^+ofEB?wX{no3hcp=m#1d7(peV^lJDq)MOcYS8-@CuAa zdhv~3c;s;~3D(DKnnQeYkz;PwczPsE7yB|!_b%RzKJ9mCbaDcPT9sVf_0O>8vf>vv zYwT@kq(T@I+95=u1oL%hqIggcrU+B!aoE81k-fPu7+$k&;UktRbz4el%+(O;BstU} z^m>Oz5-v>YkeLkycRHnCQu^KjHxWt%?!IOUI{fWHpH>OJeTzTFIzh^wa35ZLJt~XO zNvfC4`nkkN85jr?k6lY@41M>#o^_ODe{WwmJ!M@@QuO;RLuj7))I#`1F-6&{g{s}k z#>mNzLV|D)ovF!av{}}uc{S3_q0GzL>MBnLq3ab{b7GlptC#fr;OzVNV~@Kf-)8ODkWZqNmGp+YQeeY66xg#jSYSrwFo+ z*AR_!q9)=hYQ~Glg+VQTqT1IjEG`M)k7Y)!Xf1TnPL87Y%rIvK&%_xcpVmO`1s`5= zn5ZOXk#W8eMgfkehUtP(&BnVAKB_e5OXkUSL*e^P)Srh*89 zn}q(8lh&it5trbu$DQszdYJ4dCdnTHmi#b|UjN`YY$n(N+y;A~F`$SJFj2C%do1Le zln17@FrUws*8-L4bRO&($hMq)&dgG*J(IyCZM}%ix9M>wpX%oai3k6)o%(OF zooZ(#f%Y}*OgsKV(&5LLAo_KqFT1}SQhWF7ZhV?@2zU1S_NyBSzIRD*;`bzOop-r6 zzz!kfGMp(`T$z8NUXyfTUN?#_z}&p)k-djMo9cvJ81xFT9@avCdZ8#9dbtRFRLPXj zZ*Kz+JtJDoD=ji{HgqIc)3zGFHNf2$IP+MTH+1Vmk{lE`eQT6&l=!PK)-hN`2JJc% zOtj1o`NHf4iC~lGQ(lc}?VB@mczg|L7T7s6=!+$u2(1*=5V*Qn%9zV&(2Z3@X86|a z?9*tbRN%;3$n;OYWg>K6lB%8vf@aLDu%DTU2yvhn#Al_k4$!hZ=L^jH$%XCZzzz=> zc{MyP?N|9m97b5>{Z+szm^5Ko*pqk=LuW||MAjHP(%I(r8;lt6PbIas$GNqLJM@(* zsW&&?WiXRK$3^%%O4xj%;RcK6L!<5H&AOylW45(ss?3zQwf60Oe&huT;oismB2ZZO zKGm8)AKUxs{!7psWbdcf=YPukZttfbZ!=pl?tNmeAKq09u-{P)%`stR{>|OndbcWX I-v9Z30rwDdS^xk5 literal 0 HcmV?d00001 diff --git a/docs/sequenceDiagrams/rollback.md b/docs/sequenceDiagrams/rollback.md new file mode 100644 index 00000000..0dd1a328 --- /dev/null +++ b/docs/sequenceDiagrams/rollback.md @@ -0,0 +1,20 @@ + +```mermaid +sequenceDiagram + participant s as Serverless CLI + participant r as Resource Group + participant f as Function App + participant b as Blob Storage + + note right of s: `sls rollback` + s ->> r: Request deployments + r ->> s: Return deployments + note right of s: Select deployment + s ->> r: Deploy ARM template + s ->> b: Request names of previously deployed artifacts + b ->> s: Return names + note right of s: Select artifact + s ->> r: Re-deploy template + s ->> f: Update RUN_FROM_PACKAGE path (could be done with above step) + note right of s: Log URLs +``` \ No newline at end of file diff --git a/docs/sequenceDiagrams/rollback.png b/docs/sequenceDiagrams/rollback.png new file mode 100644 index 0000000000000000000000000000000000000000..938be68390daa1c1c7465233077224124f5b71bc GIT binary patch literal 48105 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 KyQCqBd4B;dpFku4 literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json index 1e278235..d8bbd4d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1784,7 +1784,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -1957,7 +1956,6 @@ "version": "4.10.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "optional": true, "requires": { "bn.js": "^4.0.0", "inherits": "^2.0.1", @@ -2427,8 +2425,7 @@ "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "optional": true + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" }, "body-parser": { "version": "1.19.0", @@ -2533,8 +2530,7 @@ "brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "optional": true + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, "browser-process-hrtime": { "version": "0.1.3", @@ -2563,7 +2559,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "optional": true, "requires": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", @@ -2600,7 +2595,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "optional": true, "requires": { "bn.js": "^4.1.0", "randombytes": "^2.0.1" @@ -2697,8 +2691,7 @@ "buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "optional": true + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" }, "builtin-modules": { "version": "1.1.1", @@ -2880,7 +2873,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "optional": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -3258,7 +3250,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "optional": true, "requires": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -3271,7 +3262,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "optional": true, "requires": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -3730,7 +3720,6 @@ "version": "6.4.1", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", - "optional": true, "requires": { "bn.js": "^4.4.0", "brorand": "^1.0.1", @@ -3792,7 +3781,6 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "optional": true, "requires": { "prr": "~1.0.1" } @@ -4172,7 +4160,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "optional": true, "requires": { "d": "1", "es5-ext": "~0.10.14" @@ -4187,7 +4174,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "optional": true, "requires": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" @@ -4717,8 +4703,7 @@ }, "ansi-regex": { "version": "2.1.1", - "bundled": true, - "optional": true + "bundled": true }, "aproba": { "version": "1.2.0", @@ -4736,13 +4721,11 @@ }, "balanced-match": { "version": "1.0.0", - "bundled": true, - "optional": true + "bundled": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4755,18 +4738,15 @@ }, "code-point-at": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "concat-map": { "version": "0.0.1", - "bundled": true, - "optional": true + "bundled": true }, "console-control-strings": { "version": "1.1.0", - "bundled": true, - "optional": true + "bundled": true }, "core-util-is": { "version": "1.0.2", @@ -4869,8 +4849,7 @@ }, "inherits": { "version": "2.0.3", - "bundled": true, - "optional": true + "bundled": true }, "ini": { "version": "1.3.5", @@ -4880,7 +4859,6 @@ "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4893,20 +4871,17 @@ "minimatch": { "version": "3.0.4", "bundled": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { "version": "0.0.8", - "bundled": true, - "optional": true + "bundled": true }, "minipass": { "version": "2.3.5", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4923,7 +4898,6 @@ "mkdirp": { "version": "0.5.1", "bundled": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -4996,8 +4970,7 @@ }, "number-is-nan": { "version": "1.0.1", - "bundled": true, - "optional": true + "bundled": true }, "object-assign": { "version": "4.1.1", @@ -5007,7 +4980,6 @@ "once": { "version": "1.4.0", "bundled": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5083,8 +5055,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -5114,7 +5085,6 @@ "string-width": { "version": "1.0.2", "bundled": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5132,7 +5102,6 @@ "strip-ansi": { "version": "3.0.1", "bundled": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5171,13 +5140,11 @@ }, "wrappy": { "version": "1.0.2", - "bundled": true, - "optional": true + "bundled": true }, "yallist": { "version": "3.0.3", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -5485,7 +5452,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "optional": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -5495,7 +5461,6 @@ "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "optional": true, "requires": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -5505,7 +5470,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "optional": true, "requires": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -5824,8 +5788,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "optional": true + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -7465,8 +7428,7 @@ "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "optional": true + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" }, "loose-envify": { "version": "1.4.0", @@ -7557,7 +7519,6 @@ "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "optional": true, "requires": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -7583,7 +7544,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "optional": true, "requires": { "errno": "^0.1.3", "readable-stream": "^2.0.1" @@ -7674,14 +7634,12 @@ "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==", - "optional": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "optional": true + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "minimatch": { "version": "3.0.4", @@ -8279,7 +8237,6 @@ "version": "5.1.4", "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.4.tgz", "integrity": "sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw==", - "optional": true, "requires": { "asn1.js": "^4.0.0", "browserify-aes": "^1.0.0", @@ -8388,7 +8345,6 @@ "version": "3.0.17", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", - "optional": true, "requires": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", @@ -8591,8 +8547,7 @@ "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "optional": true + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" }, "pseudomap": { "version": "1.0.2", @@ -8659,7 +8614,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "optional": true, "requires": { "safe-buffer": "^5.1.0" } @@ -9072,7 +9026,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "optional": true, "requires": { "hash-base": "^3.0.0", "inherits": "^2.0.1" @@ -9462,7 +9415,6 @@ "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "optional": true, "requires": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -9658,8 +9610,7 @@ "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "optional": true + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==" }, "source-map": { "version": "0.7.3", @@ -10216,8 +10167,7 @@ "tapable": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.9.tgz", - "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==", - "optional": true + "integrity": "sha512-2wsvQ+4GwBvLPLWsNfLCDYGsW6xb7aeC6utq2Qh0PFwgEy7K7dsma9Jsmb2zSQj7GvYAyUGSntLtsv++GmgL1A==" }, "tar-stream": { "version": "1.6.2", @@ -11067,7 +11017,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", "integrity": "sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA==", - "optional": true, "requires": { "source-list-map": "^2.0.0", "source-map": "~0.6.1" @@ -11076,8 +11025,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, diff --git a/src/services/azureBlobStorageService.test.ts b/src/services/azureBlobStorageService.test.ts index 9302e146..e8d10a2c 100644 --- a/src/services/azureBlobStorageService.test.ts +++ b/src/services/azureBlobStorageService.test.ts @@ -1,9 +1,13 @@ -import { MockFactory } from "../test/mockFactory" import mockFs from "mock-fs"; +import { MockFactory } from "../test/mockFactory"; +import { AzureBlobStorageService } from "./azureBlobStorageService"; jest.mock("@azure/storage-blob"); -import { BlockBlobURL, ContainerURL, ServiceURL, Aborter, uploadFileToBlockBlob } from "@azure/storage-blob"; -import { AzureBlobStorageService } from "./azureBlobStorageService"; +jest.genMockFromModule("@azure/storage-blob") +import { Aborter, BlockBlobURL, ContainerURL, ServiceURL, uploadFileToBlockBlob, TokenCredential } from "@azure/storage-blob"; + +jest.mock("./loginService"); +import { AzureLoginService } from "./loginService" describe("Azure Blob Storage Service", () => { @@ -18,9 +22,23 @@ describe("Azure Blob Storage Service", () => { const blockBlobUrl = MockFactory.createTestBlockBlobUrl(containerName, filePath); let service: AzureBlobStorageService; + const token = "myToken"; beforeAll(() => { + (TokenCredential as any).mockImplementation((token: string) => { + token + }); + BlockBlobURL.fromContainerURL = jest.fn(() => blockBlobUrl) as any; + AzureLoginService.login = jest.fn(() => Promise.resolve({ + credentials: { + getToken: jest.fn(() => { + return { + accessToken: token + } + }) + } + } as any)); }); beforeAll(() => { @@ -37,6 +55,11 @@ describe("Azure Blob Storage Service", () => { service = new AzureBlobStorageService(sls, options); }); + it("should initialize authentication", async () => { + await service.initialize(); + expect(TokenCredential).toBeCalledWith(token); + }); + it("should upload a file", async () => { uploadFileToBlockBlob.prototype = jest.fn(); ContainerURL.fromServiceURL = jest.fn((serviceUrl, containerName) => (containerName as any)); diff --git a/src/services/baseService.ts b/src/services/baseService.ts index 8dfdac23..642da6b0 100644 --- a/src/services/baseService.ts +++ b/src/services/baseService.ts @@ -90,6 +90,9 @@ export abstract class BaseService { return this.rollbackConfiguredName(this.getServiceName()); } + /** + * Get the access token from credentials token cache + */ protected getAccessToken(): string{ return (this.credentials.tokenCache as any)._entries[0].accessToken; } @@ -168,6 +171,10 @@ export abstract class BaseService { } } + /** + * Add `-t{timestamp}` if rollback is enabled + * @param name Original name + */ private rollbackConfiguredName(name: string) { return (this.deploymentConfig.rollback) ? `${name}-t${this.getTimestamp()}` : name; } diff --git a/src/services/functionAppService.ts b/src/services/functionAppService.ts index cbf04cd8..eb1a7176 100644 --- a/src/services/functionAppService.ts +++ b/src/services/functionAppService.ts @@ -213,29 +213,6 @@ export class FunctionAppService extends BaseService { }; } - private async runKuduCommand(functionApp: Site, command: string) { - this.serverless.cli.log(`-> Running Kudu command ${command}...`); - - const scmDomain = this.getScmDomain(functionApp); - const requestUrl = `https://${scmDomain}/api/command`; - - // TODO: There is a case where the body will contain an error, but it's - // not actually an error. These are warnings from npm install. - const response = await this.sendApiRequest("POST", requestUrl, { - data: { - command: command, - dir: "site\\wwwroot" - } - }); - - if (response.status !== 200) { - if (response.data && response.data.Error) { - throw new Error(response.data.Error); - } - throw new Error(`Error executing ${command} command, try again later.`); - } - } - /** * Gets a short lived admin token used to retrieve function keys */ diff --git a/src/services/loginService.ts b/src/services/loginService.ts index b934fc91..736671a4 100644 --- a/src/services/loginService.ts +++ b/src/services/loginService.ts @@ -8,6 +8,12 @@ import { } from "@azure/ms-rest-nodeauth"; export class AzureLoginService { + + /** + * Logs in via service principal login if environment variables are + * set or via interactive login if environment variables are not set + * @param options Options for different authentication methods + */ public static async login(options?: AzureTokenCredentialsOptions|InteractiveLoginOptions): Promise { const subscriptionId = process.env.azureSubId; const clientId = process.env.azureServicePrincipalClientId; @@ -23,11 +29,10 @@ export class AzureLoginService { public static async interactiveLogin(options: InteractiveLoginOptions): Promise { await open("https://microsoft.com/devicelogin"); - return await interactiveLoginWithAuthResponse(options) + return await interactiveLoginWithAuthResponse(options); } public static async servicePrincipalLogin(clientId: string, secret: string, tenantId: string, options: AzureTokenCredentialsOptions): Promise { - // return loginWithServicePrincipalSecretWithAuthResponse(clientId, secret, tenantId); - return loginWithServicePrincipalSecretWithAuthResponse(clientId, secret, tenantId, options) + return loginWithServicePrincipalSecretWithAuthResponse(clientId, secret, tenantId, options); } }