From f324d40e8cb35caed5d9e251375df0d82914b4af Mon Sep 17 00:00:00 2001 From: xzf0587 Date: Tue, 24 Aug 2021 17:19:44 +0800 Subject: [PATCH] chore(identity,sql): support bicep (#2018) * chore(sql): add bicep --- packages/fx-core/src/common/tools.ts | 2 + .../src/plugins/resource/function/plugin.ts | 74 +++---- .../plugins/resource/identity/constants.ts | 15 ++ .../src/plugins/resource/identity/index.ts | 98 ++++++++- .../src/plugins/resource/sql/constants.ts | 19 ++ .../fx-core/src/plugins/resource/sql/index.ts | 21 +- .../src/plugins/resource/sql/plugin.ts | 199 ++++++++++++++---- .../function/bicep/function.template.bicep | 17 +- .../function/bicep/module.template.bicep | 3 +- .../identity/bicep/module.template.bicep | 7 + .../identity/bicep/output.template.bicep | 4 + .../identity/bicep/param.template.bicep | 3 + .../bicep/userAssignedIdentity.template.bicep | 10 + .../resource/sql/bicep/module.template.bicep | 10 + .../resource/sql/bicep/output.template.bicep | 3 + .../resource/sql/bicep/param.template.bicep | 6 + .../resource/sql/bicep/parameters.json | 8 + .../resource/sql/bicep/sql.template.bicep | 35 +++ .../unit/expectedBicepFiles/function.bicep | 6 +- .../unit/expectedBicepFiles/module.bicep | 2 +- .../unit/expectedBicepFiles/module.bicep | 7 + .../unit/expectedBicepFiles/output.bicep | 4 + .../unit/expectedBicepFiles/param.bicep | 3 + .../userAssignedIdentity.template.bicep | 10 + .../unit/generateArmTemplates.test.ts | 94 +++++++++ .../sql/unit/expectedBicepFiles/module.bicep | 10 + .../sql/unit/expectedBicepFiles/output.bicep | 3 + .../sql/unit/expectedBicepFiles/param.bicep | 6 + .../unit/expectedBicepFiles/parameters.json | 8 + .../expectedBicepFiles/sql.template.bicep | 35 +++ .../sql/unit/generateArmTemplates.test.ts | 94 +++++++++ 31 files changed, 730 insertions(+), 86 deletions(-) create mode 100644 packages/fx-core/templates/plugins/resource/identity/bicep/module.template.bicep create mode 100644 packages/fx-core/templates/plugins/resource/identity/bicep/output.template.bicep create mode 100644 packages/fx-core/templates/plugins/resource/identity/bicep/param.template.bicep create mode 100644 packages/fx-core/templates/plugins/resource/identity/bicep/userAssignedIdentity.template.bicep create mode 100644 packages/fx-core/templates/plugins/resource/sql/bicep/module.template.bicep create mode 100644 packages/fx-core/templates/plugins/resource/sql/bicep/output.template.bicep create mode 100644 packages/fx-core/templates/plugins/resource/sql/bicep/param.template.bicep create mode 100644 packages/fx-core/templates/plugins/resource/sql/bicep/parameters.json create mode 100644 packages/fx-core/templates/plugins/resource/sql/bicep/sql.template.bicep create mode 100644 packages/fx-core/tests/plugins/resource/identity/unit/expectedBicepFiles/module.bicep create mode 100644 packages/fx-core/tests/plugins/resource/identity/unit/expectedBicepFiles/output.bicep create mode 100644 packages/fx-core/tests/plugins/resource/identity/unit/expectedBicepFiles/param.bicep create mode 100644 packages/fx-core/tests/plugins/resource/identity/unit/expectedBicepFiles/userAssignedIdentity.template.bicep create mode 100644 packages/fx-core/tests/plugins/resource/identity/unit/generateArmTemplates.test.ts create mode 100644 packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/module.bicep create mode 100644 packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/output.bicep create mode 100644 packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/param.bicep create mode 100644 packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/parameters.json create mode 100644 packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/sql.template.bicep create mode 100644 packages/fx-core/tests/plugins/resource/sql/unit/generateArmTemplates.test.ts diff --git a/packages/fx-core/src/common/tools.ts b/packages/fx-core/src/common/tools.ts index f2d38a3d2a..2dce1275e2 100644 --- a/packages/fx-core/src/common/tools.ts +++ b/packages/fx-core/src/common/tools.ts @@ -138,6 +138,7 @@ const SecretDataMatchers = [ "fx-resource-bot.bots", "fx-resource-bot.composeExtensions", "fx-resource-apim.apimClientAADClientSecret", + "fx-resource-azure-sql.adminPassword", ]; export const CryptoDataMatchers = new Set([ @@ -147,6 +148,7 @@ export const CryptoDataMatchers = new Set([ "fx-resource-bot.botPassword", "fx-resource-bot.localBotPassword", "fx-resource-apim.apimClientAADClientSecret", + "fx-resource-azure-sql.adminPassword", ]); /** diff --git a/packages/fx-core/src/plugins/resource/function/plugin.ts b/packages/fx-core/src/plugins/resource/function/plugin.ts index 9c861b9214..e9b15087d7 100644 --- a/packages/fx-core/src/plugins/resource/function/plugin.ts +++ b/packages/fx-core/src/plugins/resource/function/plugin.ts @@ -906,47 +906,47 @@ export class FunctionPluginImpl { FunctionProvision.updateFunctionSettingsForFrontend(site, frontendEndpoint); } - } - - const sqlConfig: ReadonlyPluginConfig | undefined = ctx.configOfOtherPlugins.get( - DependentPluginInfo.sqlPluginName - ); - const identityConfig: ReadonlyPluginConfig | undefined = ctx.configOfOtherPlugins.get( - DependentPluginInfo.identityPluginName - ); - if ( - this.isPluginEnabled(ctx, DependentPluginInfo.sqlPluginName) && - this.isPluginEnabled(ctx, DependentPluginInfo.identityPluginName) && - sqlConfig && - identityConfig - ) { - Logger.info(InfoMessages.dependPluginDetected(DependentPluginInfo.sqlPluginName)); - Logger.info(InfoMessages.dependPluginDetected(DependentPluginInfo.identityPluginName)); - const identityId: string = this.checkAndGet( - identityConfig.get(DependentPluginInfo.identityId) as string, - "identity Id" - ); - const databaseName: string = this.checkAndGet( - sqlConfig.get(DependentPluginInfo.databaseName) as string, - "database name" - ); - const sqlEndpoint: string = this.checkAndGet( - sqlConfig.get(DependentPluginInfo.sqlEndpoint) as string, - "sql endpoint" + const sqlConfig: ReadonlyPluginConfig | undefined = ctx.configOfOtherPlugins.get( + DependentPluginInfo.sqlPluginName ); - const identityName: string = this.checkAndGet( - identityConfig.get(DependentPluginInfo.identityName) as string, - "identity name" + const identityConfig: ReadonlyPluginConfig | undefined = ctx.configOfOtherPlugins.get( + DependentPluginInfo.identityPluginName ); + if ( + this.isPluginEnabled(ctx, DependentPluginInfo.sqlPluginName) && + this.isPluginEnabled(ctx, DependentPluginInfo.identityPluginName) && + sqlConfig && + identityConfig + ) { + Logger.info(InfoMessages.dependPluginDetected(DependentPluginInfo.sqlPluginName)); + Logger.info(InfoMessages.dependPluginDetected(DependentPluginInfo.identityPluginName)); + + const identityId: string = this.checkAndGet( + identityConfig.get(DependentPluginInfo.identityId) as string, + "identity Id" + ); + const databaseName: string = this.checkAndGet( + sqlConfig.get(DependentPluginInfo.databaseName) as string, + "database name" + ); + const sqlEndpoint: string = this.checkAndGet( + sqlConfig.get(DependentPluginInfo.sqlEndpoint) as string, + "sql endpoint" + ); + const identityName: string = this.checkAndGet( + identityConfig.get(DependentPluginInfo.identityName) as string, + "identity name" + ); - FunctionProvision.updateFunctionSettingsForSQL( - site, - identityId, - databaseName, - sqlEndpoint, - identityName - ); + FunctionProvision.updateFunctionSettingsForSQL( + site, + identityId, + databaseName, + sqlEndpoint, + identityName + ); + } } const apimConfig: ReadonlyPluginConfig | undefined = ctx.configOfOtherPlugins.get( diff --git a/packages/fx-core/src/plugins/resource/identity/constants.ts b/packages/fx-core/src/plugins/resource/identity/constants.ts index 93d1b73daf..0d1cbb6282 100644 --- a/packages/fx-core/src/plugins/resource/identity/constants.ts +++ b/packages/fx-core/src/plugins/resource/identity/constants.ts @@ -41,3 +41,18 @@ export class Telemetry { appid: "appid", }; } +export class IdentityBicep { + static readonly identityName: string = "userAssignedIdentityProvision.outputs.identityName"; + static readonly identityId: string = "userAssignedIdentityProvision.outputs.identityId"; + static readonly identity: string = "userAssignedIdentityProvision.outputs.identity"; +} + +export class IdentityArmOutput { + static readonly identityName: string = "identity_identityName"; + static readonly identityId: string = "identity_identityId"; + static readonly identity: string = "identity_identity"; +} + +export class IdentityBicepFile { + static readonly moduleTemplateFileName: string = "userAssignedIdentity.template.bicep"; +} diff --git a/packages/fx-core/src/plugins/resource/identity/index.ts b/packages/fx-core/src/plugins/resource/identity/index.ts index 50c800ac0f..2162b0200d 100644 --- a/packages/fx-core/src/plugins/resource/identity/index.ts +++ b/packages/fx-core/src/plugins/resource/identity/index.ts @@ -12,17 +12,27 @@ import { } from "@microsoft/teamsfx-api"; import { IdentityConfig } from "./config"; -import { Constants, Telemetry } from "./constants"; +import { + Constants, + IdentityArmOutput, + IdentityBicep, + IdentityBicepFile, + Telemetry, +} from "./constants"; import { ContextUtils } from "./utils/contextUtils"; import { ResultFactory, Result } from "./results"; import { Message } from "./utils/messages"; import { TelemetryUtils } from "./utils/telemetryUtil"; import { formatEndpoint } from "./utils/commonUtils"; -import { getTemplatesFolder } from "../../.."; +import { generateBicepFiles, getTemplatesFolder } from "../../.."; import { AzureResourceSQL } from "../../solution/fx-solution/question"; import { Service } from "typedi"; import { ResourcePlugins } from "../../solution/fx-solution/ResourcePluginContainer"; import { Providers, ResourceManagementClientContext } from "@azure/arm-resources"; +import { Bicep, ConstantString } from "../../../common/constants"; +import { ScaffoldArmTemplateResult } from "../../../common/armInterface"; +import { isArmSupportEnabled } from "../../../common"; +import { getArmOutput } from "../utils4v2"; @Service(ResourcePlugins.IdentityPlugin) export class IdentityPlugin implements Plugin { @@ -48,6 +58,21 @@ export class IdentityPlugin implements Plugin { config: IdentityConfig = new IdentityConfig(); async provision(ctx: PluginContext): Promise { + if (!isArmSupportEnabled()) { + return this.provisionImplement(ctx); + } else { + return ok(undefined); + } + } + + async postProvision(ctx: PluginContext): Promise { + if (isArmSupportEnabled()) { + this.syncArmOutput(ctx); + } + return ok(undefined); + } + + async provisionImplement(ctx: PluginContext): Promise { ctx.logProvider?.info(Message.startProvision); TelemetryUtils.init(ctx); TelemetryUtils.sendEvent(Telemetry.stage.provision + Telemetry.startSuffix); @@ -108,6 +133,69 @@ export class IdentityPlugin implements Plugin { return ok(undefined); } + public async generateArmTemplates(ctx: PluginContext): Promise { + const selectedPlugins = (ctx.projectSettings?.solutionSettings as AzureSolutionSettings) + .activeResourcePlugins; + const context = { + Plugins: selectedPlugins, + }; + + const bicepTemplateDirectory = path.join( + getTemplatesFolder(), + "plugins", + "resource", + "identity", + "bicep" + ); + + const moduleTemplateFilePath = path.join( + bicepTemplateDirectory, + IdentityBicepFile.moduleTemplateFileName + ); + const moduleContentResult = await generateBicepFiles(moduleTemplateFilePath, context); + if (moduleContentResult.isErr()) { + throw moduleContentResult.error; + } + + const parameterTemplateFilePath = path.join( + bicepTemplateDirectory, + Bicep.ParameterOrchestrationFileName + ); + const moduleOrchestrationFilePath = path.join( + bicepTemplateDirectory, + Bicep.ModuleOrchestrationFileName + ); + const outputTemplateFilePath = path.join( + bicepTemplateDirectory, + Bicep.OutputOrchestrationFileName + ); + + const result: ScaffoldArmTemplateResult = { + Modules: { + userAssignedIdentityProvision: { + Content: moduleContentResult.value, + }, + }, + Orchestration: { + ParameterTemplate: { + Content: await fs.readFile(parameterTemplateFilePath, ConstantString.UTF8Encoding), + }, + ModuleTemplate: { + Content: await fs.readFile(moduleOrchestrationFilePath, ConstantString.UTF8Encoding), + Outputs: { + identityName: IdentityBicep.identityName, + identityId: IdentityBicep.identityId, + identity: IdentityBicep.identity, + }, + }, + OutputTemplate: { + Content: await fs.readFile(outputTemplateFilePath, ConstantString.UTF8Encoding), + }, + }, + }; + return ok(result); + } + async loadArmTemplate(ctx: PluginContext) { try { const templatesFolder = path.resolve(getTemplatesFolder(), "plugins", "resource", "identity"); @@ -163,6 +251,12 @@ export class IdentityPlugin implements Plugin { throw error; } } + + private syncArmOutput(ctx: PluginContext) { + ctx.config.set(Constants.identityName, getArmOutput(ctx, IdentityArmOutput.identityName)); + ctx.config.set(Constants.identityId, getArmOutput(ctx, IdentityArmOutput.identityId)); + ctx.config.set(Constants.identity, getArmOutput(ctx, IdentityArmOutput.identity)); + } } export default new IdentityPlugin(); diff --git a/packages/fx-core/src/plugins/resource/sql/constants.ts b/packages/fx-core/src/plugins/resource/sql/constants.ts index 7312cb42a1..eb8e2e0e6a 100644 --- a/packages/fx-core/src/plugins/resource/sql/constants.ts +++ b/packages/fx-core/src/plugins/resource/sql/constants.ts @@ -29,6 +29,10 @@ export class Constants { public static readonly sqlEndpoint: string = "sqlEndpoint"; public static readonly databaseName: string = "databaseName"; public static readonly skipAddingUser: string = "skipAddingUser"; + public static readonly admin: string = "admin"; + public static readonly adminPassword: string = "adminPassword"; + public static readonly aadAdmin: string = "aadAdmin"; + public static readonly aadAdminObjectId: string = "aadAdminObjectId"; public static readonly solution: string = "solution"; public static readonly solutionPluginFullName = "fx-solution-azure"; @@ -75,6 +79,7 @@ export class Telemetry { provision: "provision", postProvision: "post-provision", getQuestion: "get-question", + generateArmTemplates: "generate-arm-templates", }; static readonly properties = { @@ -91,3 +96,17 @@ export class Telemetry { export class HelpLinks { static readonly default = "https://aka.ms/teamsfx-sql-help"; } + +export class AzureSqlBicep { + static readonly sqlEndpoint: string = "azureSqlProvision.outputs.sqlEndpoint"; + static readonly databaseName: string = "azureSqlProvision.outputs.databaseName"; +} + +export class AzureSqlArmOutput { + static readonly sqlEndpoint: string = "azureSql_sqlEndpoint"; + static readonly databaseName: string = "azureSql_databaseName"; +} + +export class AzureSqlBicepFile { + static readonly moduleTemplateFileName: string = "sql.template.bicep"; +} diff --git a/packages/fx-core/src/plugins/resource/sql/index.ts b/packages/fx-core/src/plugins/resource/sql/index.ts index 82840cfb6e..994e4645d4 100644 --- a/packages/fx-core/src/plugins/resource/sql/index.ts +++ b/packages/fx-core/src/plugins/resource/sql/index.ts @@ -2,8 +2,8 @@ // Licensed under the MIT license. import { AzureSolutionSettings, + ok, err, - Func, FxError, Plugin, PluginContext, @@ -14,6 +14,7 @@ import { UserError, } from "@microsoft/teamsfx-api"; import { Service } from "typedi"; +import { isArmSupportEnabled } from "../../.."; import { AzureResourceSQL, HostTypeOptionAzure, @@ -50,7 +51,15 @@ export class SqlPlugin implements Plugin { } public async provision(ctx: PluginContext): Promise { - return this.runWithSqlError(Telemetry.stage.provision, () => this.sqlImpl.provision(ctx), ctx); + if (!isArmSupportEnabled()) { + return this.runWithSqlError( + Telemetry.stage.provision, + () => this.sqlImpl.provision(ctx), + ctx + ); + } else { + return ok(undefined); + } } public async postProvision(ctx: PluginContext): Promise { @@ -61,6 +70,14 @@ export class SqlPlugin implements Plugin { ); } + public async generateArmTemplates(ctx: PluginContext): Promise { + return this.runWithSqlError( + Telemetry.stage.generateArmTemplates, + () => this.sqlImpl.generateArmTemplates(ctx), + ctx + ); + } + public async getQuestions( stage: Stage, ctx: PluginContext diff --git a/packages/fx-core/src/plugins/resource/sql/plugin.ts b/packages/fx-core/src/plugins/resource/sql/plugin.ts index 39743343db..b8f8ef4229 100644 --- a/packages/fx-core/src/plugins/resource/sql/plugin.ts +++ b/packages/fx-core/src/plugins/resource/sql/plugin.ts @@ -8,7 +8,7 @@ import { Stage, QTreeNode, Platform, - Func, + AzureSolutionSettings, } from "@microsoft/teamsfx-api"; import { ManagementClient } from "./managementClient"; import { ErrorMessage } from "./errors"; @@ -23,11 +23,26 @@ import { SqlConfig } from "./config"; import { SqlClient } from "./sqlClient"; import { ContextUtils } from "./utils/contextUtils"; import { formatEndpoint, parseToken, UserType } from "./utils/commonUtils"; -import { Constants, HelpLinks, Telemetry } from "./constants"; +import { + AzureSqlArmOutput, + AzureSqlBicep, + AzureSqlBicepFile, + Constants, + HelpLinks, + Telemetry, +} from "./constants"; import { Message } from "./utils/message"; import { TelemetryUtils } from "./utils/telemetryUtils"; import { adminNameQuestion, adminPasswordQuestion, confirmPasswordQuestion } from "./questions"; import { Providers, ResourceManagementClientContext } from "@azure/arm-resources"; +import path from "path"; +import { generateBicepFiles, getTemplatesFolder } from "../../.."; +import { Bicep, ConstantString } from "../../../common/constants"; +import { ScaffoldArmTemplateResult } from "../../../common/armInterface"; +import * as fs from "fs-extra"; +import { getArmOutput } from "../utils4v2"; +import { isArmSupportEnabled } from "../../../common"; +import { IdentityArmOutput } from "../identity/constants"; export class SqlPluginImpl { config: SqlConfig = new SqlConfig(); @@ -74,21 +89,24 @@ export class SqlPluginImpl { type: "group", }); if (ctx.answers?.platform === Platform.CLI_HELP) { - sqlNode.addChild(new QTreeNode(adminNameQuestion)); - sqlNode.addChild(new QTreeNode(adminPasswordQuestion)); - sqlNode.addChild(new QTreeNode(confirmPasswordQuestion)); + this.buildQuestionNode(sqlNode); return ok(sqlNode); } + await this.init(ctx); - if (this.config.azureSubscriptionId) { + if (isArmSupportEnabled()) { + this.config.admin = ctx.config.get(Constants.admin) as string; + this.config.adminPassword = ctx.config.get(Constants.adminPassword) as string; + if (this.config.admin) { + this.config.existSql = true; + } + } else if (this.config.azureSubscriptionId) { const managementClient: ManagementClient = await ManagementClient.create(ctx, this.config); this.config.existSql = await managementClient.existAzureSQL(); } if (!this.config.existSql) { - sqlNode.addChild(new QTreeNode(adminNameQuestion)); - sqlNode.addChild(new QTreeNode(adminPasswordQuestion)); - sqlNode.addChild(new QTreeNode(confirmPasswordQuestion)); + this.buildQuestionNode(sqlNode); } return ok(sqlNode); } @@ -123,9 +141,6 @@ export class SqlPluginImpl { this.config.skipAddingUser = skipAddingUser as boolean; } - // sql server name - ctx.logProvider?.debug(Message.endpoint(this.config.sqlEndpoint)); - if (!this.config.existSql) { this.config.admin = ctx.answers![Constants.questionKey.adminName] as string; this.config.adminPassword = ctx.answers![Constants.questionKey.adminPassword] as string; @@ -138,11 +153,10 @@ export class SqlPluginImpl { ctx.logProvider?.error(ErrorMessage.SqlInputError.message()); throw error; } + ctx.config.set(Constants.admin, this.config.admin); + ctx.config.set(Constants.adminPassword, this.config.adminPassword); } - ctx.config.set(Constants.sqlEndpoint, this.config.sqlEndpoint); - ctx.config.set(Constants.databaseName, this.config.databaseName); - // get login user info to set aad admin in sql try { const credential = await ctx.azureAccountProvider!.getAccountCredentialAsync(); @@ -162,6 +176,10 @@ export class SqlPluginImpl { ); throw error; } + + if (isArmSupportEnabled()) { + this.setContext(ctx); + } TelemetryUtils.sendEvent(Telemetry.stage.preProvision, true); ctx.logProvider?.info(Message.endPreProvision); return ok(undefined); @@ -229,34 +247,24 @@ export class SqlPluginImpl { : Telemetry.valueNo, }); + if (isArmSupportEnabled()) { + this.syncArmOutput(ctx); + } + + ctx.config.set(Constants.sqlEndpoint, this.config.sqlEndpoint); + ctx.config.set(Constants.databaseName, this.config.databaseName); + ctx.config.delete(Constants.adminPassword); + const managementClient: ManagementClient = await ManagementClient.create(ctx, this.config); ctx.logProvider?.info(Message.addFirewall); - await managementClient.addLocalFirewallRule(); - await managementClient.addAzureFirewallRule(); + await this.AddFireWallRules(managementClient); await DialogUtils.progressBar?.start(); await DialogUtils.progressBar?.next(ConfigureMessage.postProvisionAddAadmin); - let existAdmin = false; - ctx.logProvider?.info(Message.checkAadAdmin); - existAdmin = await managementClient.existAadAdmin(); - if (!existAdmin) { - ctx.logProvider?.info(Message.addSqlAadAdmin); - await managementClient.addAADadmin(); - } else { - ctx.logProvider?.info(Message.skipAddAadAdmin); - } + await this.CheckAndSetAadAdmin(ctx, managementClient); - const identityConfig = ctx.configOfOtherPlugins.get(Constants.identityPlugin); - this.config.identity = identityConfig!.get(Constants.identity) as string; - if (!this.config.identity) { - const error = SqlResultFactory.SystemError( - ErrorMessage.SqlGetConfigError.name, - ErrorMessage.SqlGetConfigError.message(Constants.identityPlugin, Constants.identity) - ); - ctx.logProvider?.error(error.message); - throw error; - } + this.getIdentity(ctx); if (!this.config.skipAddingUser) { await DialogUtils.progressBar?.next(ConfigureMessage.postProvisionAddUser); @@ -301,4 +309,123 @@ export class SqlPluginImpl { await DialogUtils.progressBar?.end(true); return ok(undefined); } + + public async generateArmTemplates(ctx: PluginContext): Promise> { + const selectedPlugins = (ctx.projectSettings?.solutionSettings as AzureSolutionSettings) + .activeResourcePlugins; + const context = { + Plugins: selectedPlugins, + }; + + const bicepTemplateDirectory = path.join( + getTemplatesFolder(), + "plugins", + "resource", + "sql", + "bicep" + ); + + const moduleTemplateFilePath = path.join( + bicepTemplateDirectory, + AzureSqlBicepFile.moduleTemplateFileName + ); + const moduleContentResult = await generateBicepFiles(moduleTemplateFilePath, context); + if (moduleContentResult.isErr()) { + throw moduleContentResult.error; + } + + const parameterTemplateFilePath = path.join( + bicepTemplateDirectory, + Bicep.ParameterOrchestrationFileName + ); + const moduleOrchestrationFilePath = path.join( + bicepTemplateDirectory, + Bicep.ModuleOrchestrationFileName + ); + const outputTemplateFilePath = path.join( + bicepTemplateDirectory, + Bicep.OutputOrchestrationFileName + ); + const parameterFilePath = path.join(bicepTemplateDirectory, Bicep.ParameterFileName); + + const result: ScaffoldArmTemplateResult = { + Modules: { + azureSqlProvision: { + Content: moduleContentResult.value, + }, + }, + Orchestration: { + ParameterTemplate: { + Content: await fs.readFile(parameterTemplateFilePath, ConstantString.UTF8Encoding), + ParameterJson: JSON.parse( + await fs.readFile(parameterFilePath, ConstantString.UTF8Encoding) + ), + }, + ModuleTemplate: { + Content: await fs.readFile(moduleOrchestrationFilePath, ConstantString.UTF8Encoding), + Outputs: { + sqlEndpoint: AzureSqlBicep.sqlEndpoint, + databaseName: AzureSqlBicep.databaseName, + }, + }, + OutputTemplate: { + Content: await fs.readFile(outputTemplateFilePath, ConstantString.UTF8Encoding), + }, + }, + }; + return ok(result); + } + + private setContext(ctx: PluginContext) { + ctx.config.set(Constants.admin, this.config.admin); + ctx.config.set(Constants.adminPassword, this.config.adminPassword); + } + + private syncArmOutput(ctx: PluginContext) { + this.config.sqlEndpoint = getArmOutput(ctx, AzureSqlArmOutput.sqlEndpoint)!; + this.config.databaseName = getArmOutput(ctx, AzureSqlArmOutput.databaseName)!; + this.config.sqlServer = this.config.sqlEndpoint.split(".")[0]; + } + + private buildQuestionNode(sqlNode: QTreeNode) { + sqlNode.addChild(new QTreeNode(adminNameQuestion)); + sqlNode.addChild(new QTreeNode(adminPasswordQuestion)); + sqlNode.addChild(new QTreeNode(confirmPasswordQuestion)); + } + + private async AddFireWallRules(client: ManagementClient) { + await client.addLocalFirewallRule(); + if (!isArmSupportEnabled()) { + await client.addAzureFirewallRule(); + } + } + + private async CheckAndSetAadAdmin(ctx: PluginContext, client: ManagementClient) { + let existAdmin = false; + ctx.logProvider?.info(Message.checkAadAdmin); + existAdmin = await client.existAadAdmin(); + if (!existAdmin) { + ctx.logProvider?.info(Message.addSqlAadAdmin); + await client.addAADadmin(); + } else { + ctx.logProvider?.info(Message.skipAddAadAdmin); + } + } + + private getIdentity(ctx: PluginContext) { + if (isArmSupportEnabled()) { + this.config.identity = getArmOutput(ctx, IdentityArmOutput.identity)!; + } else { + const identityConfig = ctx.configOfOtherPlugins.get(Constants.identityPlugin); + this.config.identity = identityConfig!.get(Constants.identity) as string; + if (!this.config.identity) { + const error = SqlResultFactory.SystemError( + ErrorMessage.SqlGetConfigError.name, + ErrorMessage.SqlGetConfigError.message(Constants.identityPlugin, Constants.identity) + ); + ctx.logProvider?.error(error.message); + throw error; + } + } + } } diff --git a/packages/fx-core/templates/plugins/resource/function/bicep/function.template.bicep b/packages/fx-core/templates/plugins/resource/function/bicep/function.template.bicep index e08f303bb6..257563f73e 100644 --- a/packages/fx-core/templates/plugins/resource/function/bicep/function.template.bicep +++ b/packages/fx-core/templates/plugins/resource/function/bicep/function.template.bicep @@ -5,7 +5,7 @@ param m365ClientId string @secure() param m365ClientSecret string param m365TenantId string -param applicationIdUri string +param m365ApplicationIdUri string param m365OauthAuthorityHost string {{#contains 'fx-resource-frontend-hosting' Plugins}} @@ -17,6 +17,7 @@ param sqlEndpoint string {{/contains}} {{#contains 'fx-resource-identity' Plugins}} param identityId string +param identityName string {{/contains}} var teamsMobileOrDesktopAppClientId = '1fec8e78-bce4-4aaf-ab1b-5451cc387264' @@ -48,6 +49,14 @@ resource functionApp 'Microsoft.Web/sites@2020-06-01' = { numberOfWorkers: 1 } } + {{#contains 'fx-resource-identity' Plugins}} + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${identityName}':{} + } + } + {{/contains}} } resource functionStorage 'Microsoft.Storage/storageAccounts@2021-04-01' = { @@ -75,7 +84,7 @@ resource functionAppAppSettings 'Microsoft.Web/sites/config@2018-02-01' = { AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' FUNCTIONS_EXTENSION_VERSION: '~3' FUNCTIONS_WORKER_RUNTIME: 'node' - M365_APPLICATION_ID_URI: applicationIdUri + M365_APPLICATION_ID_URI: m365ApplicationIdUri M365_CLIENT_ID: m365ClientId M365_CLIENT_SECRET: m365ClientSecret M365_TENANT_ID: m365TenantId @@ -83,7 +92,7 @@ resource functionAppAppSettings 'Microsoft.Web/sites/config@2018-02-01' = { WEBSITE_CONTENTAZUREFILECONNECTIONSTRING: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' WEBSITE_RUN_FROM_PACKAGE: '1' WEBSITE_CONTENTSHARE: toLower(functionAppName) - {{#contains 'fx-resource-azure-sql' Plugins}} + {{#contains 'fx-resource-identity' Plugins}} IDENTITY_ID: identityId {{/contains}} {{#contains 'fx-resource-azure-sql' Plugins}} @@ -103,7 +112,7 @@ resource functionAppAuthSettings 'Microsoft.Web/sites/config@2018-02-01' = { issuer: '${oauthAuthority}/v2.0' allowedAudiences: [ m365ClientId - applicationIdUri + m365ApplicationIdUri ] } } diff --git a/packages/fx-core/templates/plugins/resource/function/bicep/module.template.bicep b/packages/fx-core/templates/plugins/resource/function/bicep/module.template.bicep index 7974a317b6..31e56695ad 100644 --- a/packages/fx-core/templates/plugins/resource/function/bicep/module.template.bicep +++ b/packages/fx-core/templates/plugins/resource/function/bicep/module.template.bicep @@ -8,7 +8,7 @@ module functionProvision '{{PluginOutput.fx-resource-function.Modules.functionPr m365ClientId: m365ClientId m365ClientSecret: m365ClientSecret m365TenantId: m365TenantId - applicationIdUri: applicationIdUri + m365ApplicationIdUri: m365ApplicationIdUri m365OauthAuthorityHost: m365OauthAuthorityHost {{#contains 'fx-resource-frontend-hosting' Plugins}} frontendHostingStorageEndpoint: {{../PluginOutput.fx-resource-frontend-hosting.Outputs.endpoint}} @@ -19,6 +19,7 @@ module functionProvision '{{PluginOutput.fx-resource-function.Modules.functionPr {{/contains}} {{#contains 'fx-resource-identity' Plugins}} identityId: {{../PluginOutput.fx-resource-identity.Outputs.identityId}} + identityName: {{../PluginOutput.fx-resource-identity.Outputs.identityName}} {{/contains}} } } diff --git a/packages/fx-core/templates/plugins/resource/identity/bicep/module.template.bicep b/packages/fx-core/templates/plugins/resource/identity/bicep/module.template.bicep new file mode 100644 index 0000000000..4e1d65e11f --- /dev/null +++ b/packages/fx-core/templates/plugins/resource/identity/bicep/module.template.bicep @@ -0,0 +1,7 @@ + +module userAssignedIdentityProvision '{{PluginOutput.fx-resource-identity.Modules.userAssignedIdentityProvision.Path}}' = { + name: 'userAssignedIdentityProvision' + params: { + managedIdentityName: identity_managedIdentityName + } +} diff --git a/packages/fx-core/templates/plugins/resource/identity/bicep/output.template.bicep b/packages/fx-core/templates/plugins/resource/identity/bicep/output.template.bicep new file mode 100644 index 0000000000..a64bf752a4 --- /dev/null +++ b/packages/fx-core/templates/plugins/resource/identity/bicep/output.template.bicep @@ -0,0 +1,4 @@ + +output identity_identityName string = userAssignedIdentityProvision.outputs.identityName +output identity_identityId string = userAssignedIdentityProvision.outputs.identityId +output identity_identity string = userAssignedIdentityProvision.outputs.identity diff --git a/packages/fx-core/templates/plugins/resource/identity/bicep/param.template.bicep b/packages/fx-core/templates/plugins/resource/identity/bicep/param.template.bicep new file mode 100644 index 0000000000..78cd534d81 --- /dev/null +++ b/packages/fx-core/templates/plugins/resource/identity/bicep/param.template.bicep @@ -0,0 +1,3 @@ + +param identity_managedIdentityName string = '${resourceBaseName}-managedIdentity' + diff --git a/packages/fx-core/templates/plugins/resource/identity/bicep/userAssignedIdentity.template.bicep b/packages/fx-core/templates/plugins/resource/identity/bicep/userAssignedIdentity.template.bicep new file mode 100644 index 0000000000..be2483e99b --- /dev/null +++ b/packages/fx-core/templates/plugins/resource/identity/bicep/userAssignedIdentity.template.bicep @@ -0,0 +1,10 @@ +param managedIdentityName string + +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = { + name: managedIdentityName + location: resourceGroup().location +} + +output identityName string = managedIdentity.id +output identityId string = managedIdentity.properties.clientId +output identity string = managedIdentityName diff --git a/packages/fx-core/templates/plugins/resource/sql/bicep/module.template.bicep b/packages/fx-core/templates/plugins/resource/sql/bicep/module.template.bicep new file mode 100644 index 0000000000..c7f9965718 --- /dev/null +++ b/packages/fx-core/templates/plugins/resource/sql/bicep/module.template.bicep @@ -0,0 +1,10 @@ + +module azureSqlProvision '{{PluginOutput.fx-resource-azure-sql.Modules.azureSqlProvision.Path}}' = { + name: 'azureSqlProvision' + params: { + sqlServerName: azureSql_serverName + sqlDatabaseName: azureSql_databaseName + administratorLogin: azureSql_admin + administratorLoginPassword: azureSql_adminPassword + } +} diff --git a/packages/fx-core/templates/plugins/resource/sql/bicep/output.template.bicep b/packages/fx-core/templates/plugins/resource/sql/bicep/output.template.bicep new file mode 100644 index 0000000000..456193fa61 --- /dev/null +++ b/packages/fx-core/templates/plugins/resource/sql/bicep/output.template.bicep @@ -0,0 +1,3 @@ + +output azureSql_sqlEndpoint string = azureSqlProvision.outputs.sqlEndpoint +output azureSql_databaseName string = azureSqlProvision.outputs.databaseName diff --git a/packages/fx-core/templates/plugins/resource/sql/bicep/param.template.bicep b/packages/fx-core/templates/plugins/resource/sql/bicep/param.template.bicep new file mode 100644 index 0000000000..b2300e2552 --- /dev/null +++ b/packages/fx-core/templates/plugins/resource/sql/bicep/param.template.bicep @@ -0,0 +1,6 @@ + +param azureSql_admin string +@secure() +param azureSql_adminPassword string +param azureSql_serverName string = '${resourceBaseName}-sql-server' +param azureSql_databaseName string = '${resourceBaseName}-database' diff --git a/packages/fx-core/templates/plugins/resource/sql/bicep/parameters.json b/packages/fx-core/templates/plugins/resource/sql/bicep/parameters.json new file mode 100644 index 0000000000..4d238b4810 --- /dev/null +++ b/packages/fx-core/templates/plugins/resource/sql/bicep/parameters.json @@ -0,0 +1,8 @@ +{ + "azureSql_admin": { + "value": "{{FX_RESOURCE_AZURE_SQL__ADMIN}}" + }, + "azureSql_adminPassword": { + "value": "{{FX_RESOURCE_AZURE_SQL__ADMINPASSWORD}}" + } +} diff --git a/packages/fx-core/templates/plugins/resource/sql/bicep/sql.template.bicep b/packages/fx-core/templates/plugins/resource/sql/bicep/sql.template.bicep new file mode 100644 index 0000000000..e449365cbb --- /dev/null +++ b/packages/fx-core/templates/plugins/resource/sql/bicep/sql.template.bicep @@ -0,0 +1,35 @@ +param sqlServerName string +param sqlDatabaseName string +param administratorLogin string +@secure() +param administratorLoginPassword string + +resource sqlServer 'Microsoft.Sql/servers@2021-02-01-preview' = { + location: resourceGroup().location + name: sqlServerName + properties: { + administratorLogin: administratorLogin + administratorLoginPassword: administratorLoginPassword + } +} + +resource sqlDatabase 'Microsoft.Sql/servers/databases@2021-02-01-preview' = { + parent: sqlServer + location: resourceGroup().location + name: sqlDatabaseName + sku: { + name: 'Basic' + } +} + +resource sqlFirewallRules 'Microsoft.Sql/servers/firewallRules@2021-02-01-preview' = { + parent: sqlServer + name: 'AllowAzure' + properties: { + endIpAddress: '0.0.0.0' + startIpAddress: '0.0.0.0' + } +} + +output sqlEndpoint string = sqlServer.properties.fullyQualifiedDomainName +output databaseName string = sqlDatabaseName diff --git a/packages/fx-core/tests/plugins/resource/function/unit/expectedBicepFiles/function.bicep b/packages/fx-core/tests/plugins/resource/function/unit/expectedBicepFiles/function.bicep index c6d952172f..4d14b23c2b 100644 --- a/packages/fx-core/tests/plugins/resource/function/unit/expectedBicepFiles/function.bicep +++ b/packages/fx-core/tests/plugins/resource/function/unit/expectedBicepFiles/function.bicep @@ -5,7 +5,7 @@ param m365ClientId string @secure() param m365ClientSecret string param m365TenantId string -param applicationIdUri string +param m365ApplicationIdUri string param m365OauthAuthorityHost string param frontendHostingStorageEndpoint string @@ -66,7 +66,7 @@ resource functionAppAppSettings 'Microsoft.Web/sites/config@2018-02-01' = { AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${functionStorage.name};AccountKey=${listKeys(functionStorage.id, functionStorage.apiVersion).keys[0].value};EndpointSuffix=${environment().suffixes.storage}' FUNCTIONS_EXTENSION_VERSION: '~3' FUNCTIONS_WORKER_RUNTIME: 'node' - M365_APPLICATION_ID_URI: applicationIdUri + M365_APPLICATION_ID_URI: m365ApplicationIdUri M365_CLIENT_ID: m365ClientId M365_CLIENT_SECRET: m365ClientSecret M365_TENANT_ID: m365TenantId @@ -87,7 +87,7 @@ resource functionAppAuthSettings 'Microsoft.Web/sites/config@2018-02-01' = { issuer: '${oauthAuthority}/v2.0' allowedAudiences: [ m365ClientId - applicationIdUri + m365ApplicationIdUri ] } } diff --git a/packages/fx-core/tests/plugins/resource/function/unit/expectedBicepFiles/module.bicep b/packages/fx-core/tests/plugins/resource/function/unit/expectedBicepFiles/module.bicep index 6e1eae2cb4..f8d9201524 100644 --- a/packages/fx-core/tests/plugins/resource/function/unit/expectedBicepFiles/module.bicep +++ b/packages/fx-core/tests/plugins/resource/function/unit/expectedBicepFiles/module.bicep @@ -8,7 +8,7 @@ module functionProvision './function.bicep' = { m365ClientId: m365ClientId m365ClientSecret: m365ClientSecret m365TenantId: m365TenantId - applicationIdUri: applicationIdUri + m365ApplicationIdUri: m365ApplicationIdUri m365OauthAuthorityHost: m365OauthAuthorityHost frontendHostingStorageEndpoint: frontend_hosting_test_endpoint } diff --git a/packages/fx-core/tests/plugins/resource/identity/unit/expectedBicepFiles/module.bicep b/packages/fx-core/tests/plugins/resource/identity/unit/expectedBicepFiles/module.bicep new file mode 100644 index 0000000000..2f9f0cd126 --- /dev/null +++ b/packages/fx-core/tests/plugins/resource/identity/unit/expectedBicepFiles/module.bicep @@ -0,0 +1,7 @@ + +module userAssignedIdentityProvision './userAssignedIdentity.template.bicep' = { + name: 'userAssignedIdentityProvision' + params: { + managedIdentityName: identity_managedIdentityName + } +} diff --git a/packages/fx-core/tests/plugins/resource/identity/unit/expectedBicepFiles/output.bicep b/packages/fx-core/tests/plugins/resource/identity/unit/expectedBicepFiles/output.bicep new file mode 100644 index 0000000000..a64bf752a4 --- /dev/null +++ b/packages/fx-core/tests/plugins/resource/identity/unit/expectedBicepFiles/output.bicep @@ -0,0 +1,4 @@ + +output identity_identityName string = userAssignedIdentityProvision.outputs.identityName +output identity_identityId string = userAssignedIdentityProvision.outputs.identityId +output identity_identity string = userAssignedIdentityProvision.outputs.identity diff --git a/packages/fx-core/tests/plugins/resource/identity/unit/expectedBicepFiles/param.bicep b/packages/fx-core/tests/plugins/resource/identity/unit/expectedBicepFiles/param.bicep new file mode 100644 index 0000000000..78cd534d81 --- /dev/null +++ b/packages/fx-core/tests/plugins/resource/identity/unit/expectedBicepFiles/param.bicep @@ -0,0 +1,3 @@ + +param identity_managedIdentityName string = '${resourceBaseName}-managedIdentity' + diff --git a/packages/fx-core/tests/plugins/resource/identity/unit/expectedBicepFiles/userAssignedIdentity.template.bicep b/packages/fx-core/tests/plugins/resource/identity/unit/expectedBicepFiles/userAssignedIdentity.template.bicep new file mode 100644 index 0000000000..be2483e99b --- /dev/null +++ b/packages/fx-core/tests/plugins/resource/identity/unit/expectedBicepFiles/userAssignedIdentity.template.bicep @@ -0,0 +1,10 @@ +param managedIdentityName string + +resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' = { + name: managedIdentityName + location: resourceGroup().location +} + +output identityName string = managedIdentity.id +output identityId string = managedIdentity.properties.clientId +output identity string = managedIdentityName diff --git a/packages/fx-core/tests/plugins/resource/identity/unit/generateArmTemplates.test.ts b/packages/fx-core/tests/plugins/resource/identity/unit/generateArmTemplates.test.ts new file mode 100644 index 0000000000..07dfa291b6 --- /dev/null +++ b/packages/fx-core/tests/plugins/resource/identity/unit/generateArmTemplates.test.ts @@ -0,0 +1,94 @@ +import "mocha"; +import * as chai from "chai"; +import { TestHelper } from "../helper"; +import { IdentityPlugin } from "../../../../../src/plugins/resource/identity"; +import * as dotenv from "dotenv"; +import chaiAsPromised from "chai-as-promised"; +import { AzureSolutionSettings, PluginContext } from "@microsoft/teamsfx-api"; +import * as msRestNodeAuth from "@azure/ms-rest-nodeauth"; +import * as faker from "faker"; +import * as sinon from "sinon"; +import fs from "fs-extra"; +import * as path from "path"; +import { ConstantString, mockSolutionUpdateArmTemplates, ResourcePlugins } from "../../util"; +chai.use(chaiAsPromised); + +dotenv.config(); + +describe("identityPlugin", () => { + let identityPlugin: IdentityPlugin; + let pluginContext: PluginContext; + let credentials: msRestNodeAuth.TokenCredentialsBase; + + before(async () => { + credentials = new msRestNodeAuth.ApplicationTokenCredentials( + faker.datatype.uuid(), + faker.internet.url(), + faker.internet.password() + ); + }); + + beforeEach(async () => { + identityPlugin = new IdentityPlugin(); + pluginContext = await TestHelper.pluginContext(credentials); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("generate arm templates", async function () { + const activeResourcePlugins = [ResourcePlugins.Identity]; + pluginContext.projectSettings!.solutionSettings = { + name: "test_solution", + version: "1.0.0", + activeResourcePlugins: activeResourcePlugins, + } as AzureSolutionSettings; + const result = await identityPlugin.generateArmTemplates(pluginContext); + + // Assert + const testModuleFileName = "userAssignedIdentity.template.bicep"; + const mockedSolutionDataContext = { + Plugins: activeResourcePlugins, + PluginOutput: { + "fx-resource-identity": { + Modules: { + userAssignedIdentityProvision: { + Path: `./${testModuleFileName}`, + }, + }, + }, + }, + }; + chai.assert.isTrue(result.isOk()); + if (result.isOk()) { + const expectedResult = mockSolutionUpdateArmTemplates( + mockedSolutionDataContext, + result.value + ); + + const expectedBicepFileDirectory = path.join(__dirname, "expectedBicepFiles"); + const expectedModuleFilePath = path.join(expectedBicepFileDirectory, testModuleFileName); + chai.assert.strictEqual( + expectedResult.Modules!.userAssignedIdentityProvision.Content, + fs.readFileSync(expectedModuleFilePath, ConstantString.UTF8Encoding) + ); + const expectedModuleSnippetFilePath = path.join(expectedBicepFileDirectory, "module.bicep"); + chai.assert.strictEqual( + expectedResult.Orchestration.ModuleTemplate!.Content, + fs.readFileSync(expectedModuleSnippetFilePath, ConstantString.UTF8Encoding) + ); + const expectedParameterFilePath = path.join(expectedBicepFileDirectory, "param.bicep"); + chai.assert.strictEqual( + expectedResult.Orchestration.ParameterTemplate!.Content, + fs.readFileSync(expectedParameterFilePath, ConstantString.UTF8Encoding) + ); + const expectedOutputFilePath = path.join(expectedBicepFileDirectory, "output.bicep"); + chai.assert.strictEqual( + expectedResult.Orchestration.OutputTemplate!.Content, + fs.readFileSync(expectedOutputFilePath, ConstantString.UTF8Encoding) + ); + chai.assert.isUndefined(expectedResult.Orchestration.VariableTemplate); + } + }); +}); diff --git a/packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/module.bicep b/packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/module.bicep new file mode 100644 index 0000000000..5106ef9a61 --- /dev/null +++ b/packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/module.bicep @@ -0,0 +1,10 @@ + +module azureSqlProvision './sql.template.bicep' = { + name: 'azureSqlProvision' + params: { + sqlServerName: azureSql_serverName + sqlDatabaseName: azureSql_databaseName + administratorLogin: azureSql_admin + administratorLoginPassword: azureSql_adminPassword + } +} diff --git a/packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/output.bicep b/packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/output.bicep new file mode 100644 index 0000000000..456193fa61 --- /dev/null +++ b/packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/output.bicep @@ -0,0 +1,3 @@ + +output azureSql_sqlEndpoint string = azureSqlProvision.outputs.sqlEndpoint +output azureSql_databaseName string = azureSqlProvision.outputs.databaseName diff --git a/packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/param.bicep b/packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/param.bicep new file mode 100644 index 0000000000..b2300e2552 --- /dev/null +++ b/packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/param.bicep @@ -0,0 +1,6 @@ + +param azureSql_admin string +@secure() +param azureSql_adminPassword string +param azureSql_serverName string = '${resourceBaseName}-sql-server' +param azureSql_databaseName string = '${resourceBaseName}-database' diff --git a/packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/parameters.json b/packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/parameters.json new file mode 100644 index 0000000000..4d238b4810 --- /dev/null +++ b/packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/parameters.json @@ -0,0 +1,8 @@ +{ + "azureSql_admin": { + "value": "{{FX_RESOURCE_AZURE_SQL__ADMIN}}" + }, + "azureSql_adminPassword": { + "value": "{{FX_RESOURCE_AZURE_SQL__ADMINPASSWORD}}" + } +} diff --git a/packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/sql.template.bicep b/packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/sql.template.bicep new file mode 100644 index 0000000000..e449365cbb --- /dev/null +++ b/packages/fx-core/tests/plugins/resource/sql/unit/expectedBicepFiles/sql.template.bicep @@ -0,0 +1,35 @@ +param sqlServerName string +param sqlDatabaseName string +param administratorLogin string +@secure() +param administratorLoginPassword string + +resource sqlServer 'Microsoft.Sql/servers@2021-02-01-preview' = { + location: resourceGroup().location + name: sqlServerName + properties: { + administratorLogin: administratorLogin + administratorLoginPassword: administratorLoginPassword + } +} + +resource sqlDatabase 'Microsoft.Sql/servers/databases@2021-02-01-preview' = { + parent: sqlServer + location: resourceGroup().location + name: sqlDatabaseName + sku: { + name: 'Basic' + } +} + +resource sqlFirewallRules 'Microsoft.Sql/servers/firewallRules@2021-02-01-preview' = { + parent: sqlServer + name: 'AllowAzure' + properties: { + endIpAddress: '0.0.0.0' + startIpAddress: '0.0.0.0' + } +} + +output sqlEndpoint string = sqlServer.properties.fullyQualifiedDomainName +output databaseName string = sqlDatabaseName diff --git a/packages/fx-core/tests/plugins/resource/sql/unit/generateArmTemplates.test.ts b/packages/fx-core/tests/plugins/resource/sql/unit/generateArmTemplates.test.ts new file mode 100644 index 0000000000..9215a13fcd --- /dev/null +++ b/packages/fx-core/tests/plugins/resource/sql/unit/generateArmTemplates.test.ts @@ -0,0 +1,94 @@ +import "mocha"; +import * as chai from "chai"; +import chaiAsPromised from "chai-as-promised"; +import { TestHelper } from "../helper"; +import { SqlPlugin } from "../../../../../src/plugins/resource/sql"; +import * as dotenv from "dotenv"; +import { AzureSolutionSettings, Platform, PluginContext } from "@microsoft/teamsfx-api"; +import * as msRestNodeAuth from "@azure/ms-rest-nodeauth"; +import * as faker from "faker"; +import * as sinon from "sinon"; +import fs from "fs-extra"; +import * as path from "path"; +import { ConstantString, mockSolutionUpdateArmTemplates, ResourcePlugins } from "../../util"; +chai.use(chaiAsPromised); + +dotenv.config(); + +describe("generateArmTemplates", () => { + let sqlPlugin: SqlPlugin; + let pluginContext: PluginContext; + let credentials: msRestNodeAuth.TokenCredentialsBase; + + before(async () => { + credentials = new msRestNodeAuth.ApplicationTokenCredentials( + faker.datatype.uuid(), + faker.internet.url(), + faker.internet.password() + ); + }); + + beforeEach(async () => { + sqlPlugin = new SqlPlugin(); + pluginContext = await TestHelper.pluginContext(credentials); + }); + + afterEach(() => { + sinon.restore(); + }); + + it("generate arm templates", async function () { + const activeResourcePlugins = [ResourcePlugins.AzureSQL]; + pluginContext.projectSettings!.solutionSettings = { + name: "test_solution", + version: "1.0.0", + activeResourcePlugins: activeResourcePlugins, + } as AzureSolutionSettings; + const result = await sqlPlugin.generateArmTemplates(pluginContext); + + // Assert + const testModuleFileName = "sql.template.bicep"; + const mockedSolutionDataContext = { + Plugins: activeResourcePlugins, + PluginOutput: { + "fx-resource-azure-sql": { + Modules: { + azureSqlProvision: { + Path: `./${testModuleFileName}`, + }, + }, + }, + }, + }; + chai.assert.isTrue(result.isOk()); + if (result.isOk()) { + const expectedResult = mockSolutionUpdateArmTemplates( + mockedSolutionDataContext, + result.value + ); + + const expectedBicepFileDirectory = path.join(__dirname, "expectedBicepFiles"); + const expectedModuleFilePath = path.join(expectedBicepFileDirectory, testModuleFileName); + chai.assert.strictEqual( + expectedResult.Modules!.azureSqlProvision.Content, + fs.readFileSync(expectedModuleFilePath, ConstantString.UTF8Encoding) + ); + const expectedModuleSnippetFilePath = path.join(expectedBicepFileDirectory, "module.bicep"); + chai.assert.strictEqual( + expectedResult.Orchestration.ModuleTemplate!.Content, + fs.readFileSync(expectedModuleSnippetFilePath, ConstantString.UTF8Encoding) + ); + const expectedParameterFilePath = path.join(expectedBicepFileDirectory, "param.bicep"); + chai.assert.strictEqual( + expectedResult.Orchestration.ParameterTemplate!.Content, + fs.readFileSync(expectedParameterFilePath, ConstantString.UTF8Encoding) + ); + const expectedOutputFilePath = path.join(expectedBicepFileDirectory, "output.bicep"); + chai.assert.strictEqual( + expectedResult.Orchestration.OutputTemplate!.Content, + fs.readFileSync(expectedOutputFilePath, ConstantString.UTF8Encoding) + ); + chai.assert.isUndefined(expectedResult.Orchestration.VariableTemplate); + } + }); +});