diff --git a/.gitignore b/.gitignore index 7a6353d6737e69a3a39e3e1f0b91656b0ce1c0e4..88858c3061ac7ef7bca255ec255518ca3be0a8ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,371 @@ -.envrc +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +.envrc* +*.output + +output + +.ssh + +# Go files +vendor/ +_vendor-*/ +*.exe +*.lock +*.toml + +# Terraform +.terraform/ +**/.terraform/ +**/terraform.tfstate.* +**/terraform.tfstate +**/.terraform.tfstate.lock.info +**/*.plan +**/output/ +**/*-aks-flux/ + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# VSCode Files +.vscode* + +# ENV files +**/.env + +# Exclude local build directory +build/ +# For Mac OS user files +**/.DS_Store +# For Mac OS user files +**/.DS_Store + +# Dep files +*.toml +*.lock diff --git a/infra/modules/magefile.go b/infra/modules/magefile.go new file mode 100644 index 0000000000000000000000000000000000000000..107193235bf3bfbc23b40047f00a834210a651ec --- /dev/null +++ b/infra/modules/magefile.go @@ -0,0 +1,118 @@ +//+build mage + +// osdu-infrastructure task runner. +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" +) + +// A build step that runs all tests. +func All() { + mg.Deps(TestModules) +} + +// Execute Module Tests and fail if a test fails. Only executes tests in 'test' directories. +func TestModules() error { + mg.Deps(Clean) + mg.Deps(Check) + fmt.Println("INFO: Running unit tests...") + return FindAndRunTests("testing") +} + +// Validate both Terraform code and Go code. +func Check() { + mg.Deps(LintTF) + mg.Deps(LintGO) +} + +// Lint check Go and fail if files are not not formatted properly. +func LintGO() error { + fmt.Println("INFO: Checking format for Go files...") + return verifyRunsQuietly("Run `go fmt ./...` to fix", "go", "fmt", "./...") +} + +// Lint check Terraform and fail if files are not formatted properly. +func LintTF() error { + fmt.Println("INFO: Checking format for Terraform files...") + return verifyRunsQuietly("Run `terraform fmt --check --recursive` to fix the offending files", "terraform", "fmt") +} + +// Remove temporary build and test files. +func Clean() error { + fmt.Println("INFO: Cleaning...") + return filepath.Walk("./", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && info.Name() == "vendor" { + return filepath.SkipDir + } + if info.IsDir() && info.Name() == ".terraform" { + os.RemoveAll(path) + fmt.Printf("Removed \"%v\"\n", path) + return filepath.SkipDir + } + if info.IsDir() && info.Name() == "terraform.tfstate.d" { + os.RemoveAll(path) + fmt.Printf("Removed \"%v\"\n", path) + return filepath.SkipDir + } + if !info.IsDir() && (info.Name() == "terraform.tfstate" || + info.Name() == "terraform.tfplan" || + info.Name() == "terraform.tfstate.backup") { + os.Remove(path) + fmt.Printf("Removed \"%v\"\n", path) + } + return nil + }) +} + +//------------------------------- +// GO UTILITY FUNCTIONS +//------------------------------- + +// runs a command and ensures that the exit code indicates success and that there is no output to stdout +func verifyRunsQuietly(instructionsToFix string, cmd string, args ...string) error { + output, err := sh.Output(cmd, args...) + + if err != nil { + return err + } + + if len(output) == 0 { + return nil + } + + return fmt.Errorf("ERROR: command '%s' with arguments %s failed. Output was: '%s'. %s", cmd, args, output, instructionsToFix) +} + +// FindAndRunTests finds all tests with a given path suffix and runs them using `go test` +func FindAndRunTests(pathSuffix string) error { + goModules, err := sh.Output("go", "list", "./...") + if err != nil { + return err + } + + testTargetModules := make([]string, 0) + for _, module := range strings.Fields(goModules) { + if strings.HasSuffix(module, pathSuffix) { + testTargetModules = append(testTargetModules, module) + } + } + + if len(testTargetModules) == 0 { + return fmt.Errorf("No modules found for testing prefix '%s'", pathSuffix) + } + + cmdArgs := []string{"test"} + cmdArgs = append(cmdArgs, testTargetModules...) + cmdArgs = append(cmdArgs, "-v", "-timeout", "7200s") + return sh.RunV("go", cmdArgs...) +} diff --git a/infra/modules/providers/azure/.gitignore b/infra/modules/providers/azure/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..75f51127c0b5870b950f95de7fd8d8f91231bf9e --- /dev/null +++ b/infra/modules/providers/azure/.gitignore @@ -0,0 +1 @@ +testing.tfvars diff --git a/infra/modules/providers/azure/README.md b/infra/modules/providers/azure/README.md new file mode 100755 index 0000000000000000000000000000000000000000..9f3165aa8abbbcf0ab742c8ea38f0c66ef8a2ad8 --- /dev/null +++ b/infra/modules/providers/azure/README.md @@ -0,0 +1,16 @@ + + +## License +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/infra/modules/providers/azure/ad-application/README.md b/infra/modules/providers/azure/ad-application/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7c9b1163a2bc22926b74d738e5c9f752bb727e57 --- /dev/null +++ b/infra/modules/providers/azure/ad-application/README.md @@ -0,0 +1,98 @@ +# Module ad-application + +Module for managing an Azure Active Directory Application with the following characteristics: + +- Create an application and optionally assign roles to it.. + +> __This module requires the Terraform Principal to have Azure Active Directory Graph - `Application.ReadWrite.OwnedBy` Permissions.__ + + +## Usage + +``` +locals { + name = "iac-osdu" +} + +resource "random_id" "main" { + keepers = { + name = local.name + } + + byte_length = 8 +} + + +module "ad-application" { + source = "https://github.com/azure/osdu-infrastructure/infra/modules/providers/azure/ad-application" + + name = format("${local.name}-%s-ad-app-management", random_id.main.hex) + + reply_urls = [ + "https://iac-osdu.com", + "https://iac-osdu.com/.auth/login/aad/callback" + ] + + api_permissions = [ + { + name = "Microsoft Graph" + oauth2_permissions = [ + "User.Read" + ] + } + ] + + app_roles = [ + { + name = "test" + description = "test" + member_types = [ + "Application" + ] + } + ] +} +``` + +## Inputs + +| Variable Name | Type | Description | +| ------------- | ---------- | ------------------------------------ | +| `name` | _string_ | The name of the application. | +| `homepage` | _string_ | The URL of the application's homepage. | +| `reply_urls` | _list_ | A list of URLs that user tokens are sent to for sign in, or the redirect URIs that OAuth 2.0 authorization codes and access tokens are sent to. Default: `[]` | +| `identifier_uris` | _string_ | A list of user-defined URI(s) that uniquely identify a Web application within it's Azure AD tenant Default: `null`. | +| `oauth2_allow_implicit_flow` | _bool_ | Does this ad application allow oauth2 implicit flow tokens? | +| `available_to_other_tenants` | _bool_ | Is this ad application available to other tenants? | +| `group_membership_claims` | _bool_ | Configures the groups claim issued in a user or OAuth 2.0 access token that the app expects. Default: `SecurityGroup` | +| `password` | _string_ | The application password (aka client secret). If empty, Terraform will generate a password. | +| `end_date` | _string_ | The date after which the password expire. This can either be relative duration or RFC3339 date. Default: `1Y`. | +| `api_permissions` | _list_ | List of API permissions. | +| `app_roles` | _list_ | List of App roles. | + + + +## Outputs + +Once the deployments are completed successfully, the output for the current module will be in the format mentioned below: + +- `name`: The name of the application. +- `id`: The name of the application. +- `object_id`: The object ID of the application. +- `roles`: The application roles. +- `password`: The password for the application. + +## License +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/infra/modules/providers/azure/ad-application/main.tf b/infra/modules/providers/azure/ad-application/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..9bdaa9b1fd72f428b7bbfbcbf51579f81691ceaf --- /dev/null +++ b/infra/modules/providers/azure/ad-application/main.tf @@ -0,0 +1,78 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +data "azuread_service_principal" "main" { + count = length(local.api_names) + display_name = local.api_names[count.index] +} + +resource "azuread_application" "main" { + name = var.name + homepage = coalesce(var.homepage, local.homepage) + identifier_uris = local.identifier_uris + reply_urls = var.reply_urls + available_to_other_tenants = var.available_to_other_tenants + public_client = local.public_client + oauth2_allow_implicit_flow = var.oauth2_allow_implicit_flow + group_membership_claims = var.group_membership_claims + type = local.type + + dynamic "required_resource_access" { + for_each = local.required_resource_access + + content { + resource_app_id = required_resource_access.value.resource_app_id + + dynamic "resource_access" { + for_each = required_resource_access.value.resource_access + + content { + id = resource_access.value.id + type = resource_access.value.type + } + } + } + } + + dynamic "app_role" { + for_each = local.app_roles + + content { + allowed_member_types = app_role.value.member_types + display_name = app_role.value.name + description = app_role.value.description + value = coalesce(app_role.value.value, app_role.value.name) + is_enabled = app_role.value.enabled + } + } +} + +resource "random_password" "main" { + count = var.password == "" ? 1 : 0 + length = 32 + special = false +} + +resource "azuread_application_password" "main" { + count = var.password != null ? 1 : 0 + application_object_id = azuread_application.main.id + + value = coalesce(var.password, random_password.main[0].result) + end_date = local.end_date + end_date_relative = local.end_date_relative + + lifecycle { + ignore_changes = all + } +} diff --git a/infra/modules/providers/azure/ad-application/output.tf b/infra/modules/providers/azure/ad-application/output.tf new file mode 100644 index 0000000000000000000000000000000000000000..95c7c46e60ab6aa21938b4e2d62b0b67175cb08f --- /dev/null +++ b/infra/modules/providers/azure/ad-application/output.tf @@ -0,0 +1,48 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +output "name" { + value = azuread_application.main.name + description = "The display name of the application." +} + +output "id" { + value = azuread_application.main.application_id + description = "The ID of the application." +} + +output "object_id" { + value = azuread_application.main.object_id + description = "The object ID of the application." +} + +output "roles" { + value = { + for r in azuread_application.main.app_role : + r.display_name => { + id = r.id + name = r.display_name + value = r.value + description = r.description + enabled = r.is_enabled + } + } + description = "The application roles." +} + +output "password" { + value = azuread_application_password.main.0.value + sensitive = true + description = "The password for the application." +} diff --git a/infra/modules/providers/azure/ad-application/sample/main.tf b/infra/modules/providers/azure/ad-application/sample/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..fcfaa73db6025353c24e5d7313258d7ee80a4a23 --- /dev/null +++ b/infra/modules/providers/azure/ad-application/sample/main.tf @@ -0,0 +1,62 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +provider "azuread" { + version = "=0.7.0" +} + +locals { + name = "iac-osdu" +} + +resource "random_id" "main" { + keepers = { + name = local.name + } + + byte_length = 8 +} + + +module "ad-application" { + source = "../" + + name = format("${local.name}-%s-ad-app-management", random_id.main.hex) + + group_membership_claims = "All" + + reply_urls = [ + "https://iac-osdu.com", + "https://iac-osdu.com/.auth/login/aad/callback" + ] + + api_permissions = [ + { + name = "Microsoft Graph" + oauth2_permissions = [ + "User.Read" + ] + } + ] + + app_roles = [ + { + name = "test" + description = "test" + member_types = [ + "Application" + ] + } + ] +} diff --git a/infra/modules/providers/azure/ad-application/sample/unit_test.go b/infra/modules/providers/azure/ad-application/sample/unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..860edbed662114a552e21ea5f2207623557b80b2 --- /dev/null +++ b/infra/modules/providers/azure/ad-application/sample/unit_test.go @@ -0,0 +1,61 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "encoding/json" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var name = "adapplication-" +var count = 4 + +var tfOptions = &terraform.Options{ + TerraformDir: "./", + Upgrade: true, +} + +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} + +func TestTemplate(t *testing.T) { + + expectedResult := asMap(t, `{ + "available_to_other_tenants": false, + "type": "webapp/api" + }`) + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tfOptions, + Workspace: name + random.UniqueId(), + PlanAssertions: nil, + ExpectedResourceCount: count, + ExpectedResourceAttributeValues: infratests.ResourceDescription{ + "module.ad-application.azuread_application.main": expectedResult, + }, + } + + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/modules/providers/azure/ad-application/variables.tf b/infra/modules/providers/azure/ad-application/variables.tf new file mode 100644 index 0000000000000000000000000000000000000000..344959a4bae71cf1ca85045dd6e0dee3c87f6807 --- /dev/null +++ b/infra/modules/providers/azure/ad-application/variables.tf @@ -0,0 +1,169 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +variable "name" { + type = string + description = "The display name of the application" +} + +variable "homepage" { + type = string + default = "" + description = "The URL of the application's homepage." +} + +variable "reply_urls" { + type = list(string) + default = [] + description = "List of URIs to which Azure AD will redirect in response to an OAuth 2.0 request." +} + +variable "identifier_uris" { + type = list(string) + default = [] + description = "List of unique URIs that Azure AD can use for the application." +} + +variable "available_to_other_tenants" { + type = bool + default = false + description = "Whether the application can be used from any Azure AD tenants." +} + +variable "oauth2_allow_implicit_flow" { + type = bool + default = false + description = "Whether to allow implicit grant flow for OAuth2." +} + +variable "group_membership_claims" { + type = string + default = "SecurityGroup" + description = "Configures the groups claim issued in a user or OAuth 2.0 access token that the app expects." +} + +variable "password" { + type = string + default = "" + description = "The application password (aka client secret)." +} + +variable "end_date" { + type = string + default = "2Y" + description = "The RFC3339 date after which credentials expire." +} + +variable "api_permissions" { + type = any + default = [] + description = "List of API permissions." +} + +variable "app_roles" { + type = any + default = [] + description = "List of App roles." +} + +variable "native" { + type = bool + default = false + description = "Whether the application can be installed on a user's device or computer." +} + +locals { + homepage = format("https://%s", var.name) + + type = var.native ? "native" : "webapp/api" + + public_client = var.native ? true : false + + default_identifier_uris = [format("http://%s", var.name)] + + identifier_uris = var.native ? [] : coalescelist(var.identifier_uris, local.default_identifier_uris) + + api_permissions = [ + for p in var.api_permissions : merge({ + id = "" + name = "" + app_roles = [] + oauth2_permissions = [] + }, p) + ] + + api_names = local.api_permissions[*].name + + service_principals = { + for s in data.azuread_service_principal.main : s.display_name => { + application_id = s.application_id + display_name = s.display_name + app_roles = { for p in s.app_roles : p.value => p.id } + oauth2_permissions = { for p in s.oauth2_permissions : p.value => p.id } + } + } + + required_resource_access = [ + for a in local.api_permissions : { + resource_app_id = local.service_principals[a.name].application_id + resource_access = concat( + [for p in a.oauth2_permissions : { + id = local.service_principals[a.name].oauth2_permissions[p] + type = "Scope" + }], + [for p in a.app_roles : { + id = local.service_principals[a.name].app_roles[p] + type = "Role" + }] + ) + } + ] + + app_roles = [ + for r in var.app_roles : merge({ + name = "" + description = "" + member_types = [] + enabled = true + value = "" + }, r) + ] + + date = regexall("^(?:(\\d{4})-(\\d{2})-(\\d{2}))[Tt]?(?:(\\d{2}):(\\d{2})(?::(\\d{2}))?(?:\\.(\\d+))?)?([Zz]|[\\+|\\-]\\d{2}:\\d{2})?$", var.end_date) + + duration = regexall("^(?:(\\d+)Y)?(?:(\\d+)M)?(?:(\\d+)W)?(?:(\\d+)D)?(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?$", var.end_date) + + end_date_relative = length(local.duration) > 0 ? format( + "%dh", + ( + (coalesce(local.duration[0][0], 0) * 24 * 365) + + (coalesce(local.duration[0][1], 0) * 24 * 30) + + (coalesce(local.duration[0][2], 0) * 24 * 7) + + (coalesce(local.duration[0][3], 0) * 24) + + coalesce(local.duration[0][4], 0) + ) + ) : null + + end_date = length(local.date) > 0 ? format( + "%02d-%02d-%02dT%02d:%02d:%02d.%02d%s", + local.date[0][0], + local.date[0][1], + local.date[0][2], + coalesce(local.date[0][3], "23"), + coalesce(local.date[0][4], "59"), + coalesce(local.date[0][5], "00"), + coalesce(local.date[0][6], "00"), + coalesce(local.date[0][7], "Z") + ) : null +} diff --git a/infra/modules/providers/azure/aks/kubeconfig.tf b/infra/modules/providers/azure/aks/kubeconfig.tf new file mode 100644 index 0000000000000000000000000000000000000000..21e7cd4fbe617e510652639bf0b37fef4b6ff3f1 --- /dev/null +++ b/infra/modules/providers/azure/aks/kubeconfig.tf @@ -0,0 +1,21 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +resource "local_file" "cluster_credentials" { + count = var.kubeconfig_to_disk ? 1 : 0 + sensitive_content = azurerm_kubernetes_cluster.main.kube_config_raw + filename = "${var.output_directory}/${var.kubeconfig_filename}" + + depends_on = [azurerm_kubernetes_cluster.main] +} \ No newline at end of file diff --git a/infra/modules/providers/azure/aks/main.tf b/infra/modules/providers/azure/aks/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..c75bec60b42dc5fbe93a82c63f6904c157bd29b7 --- /dev/null +++ b/infra/modules/providers/azure/aks/main.tf @@ -0,0 +1,146 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + + +// Need to be able to query the identityProfile to get kubelet client information. id, resourceid and client_id +locals { + msi_identity_type = "SystemAssigned" + log_analytics_id = var.log_analytics_id == "" ? azurerm_log_analytics_workspace.main.0.id : var.log_analytics_id +} + +data "azurerm_resource_group" "main" { + name = var.resource_group_name +} + +data "azurerm_subscription" "current" {} + +resource "random_id" "main" { + keepers = { + group_name = data.azurerm_resource_group.main.name + } + + byte_length = 8 +} + +resource "azurerm_log_analytics_workspace" "main" { + count = var.log_analytics_id == "" ? 1 : 0 + + name = lower("${var.name}") + resource_group_name = data.azurerm_resource_group.main.name + location = data.azurerm_resource_group.main.location + sku = "PerGB2018" +} + +resource "azurerm_log_analytics_solution" "main" { + count = var.log_analytics_id == "" ? 1 : 0 + + solution_name = "ContainerInsights" + resource_group_name = data.azurerm_resource_group.main.name + location = data.azurerm_resource_group.main.location + + workspace_resource_id = azurerm_log_analytics_workspace.main.0.id + workspace_name = azurerm_log_analytics_workspace.main.0.name + + plan { + publisher = "Microsoft" + product = "OMSGallery/ContainerInsights" + } +} + +resource "azurerm_kubernetes_cluster" "main" { + name = var.name + resource_group_name = data.azurerm_resource_group.main.name + location = data.azurerm_resource_group.main.location + + tags = var.resource_tags + + dns_prefix = var.dns_prefix + kubernetes_version = var.kubernetes_version + + linux_profile { + admin_username = var.admin_user + + ssh_key { + key_data = var.ssh_public_key + } + } + + default_node_pool { + name = "default" + node_count = var.agent_vm_count + vm_size = var.agent_vm_size + os_disk_size_gb = 30 + vnet_subnet_id = var.vnet_subnet_id + enable_auto_scaling = var.auto_scaling_default_node + max_pods = var.max_pods + max_count = var.auto_scaling_default_node == true ? var.max_node_count : null + min_count = var.auto_scaling_default_node == true ? var.agent_vm_count : null + } + + network_profile { + network_plugin = var.network_plugin + network_policy = var.network_policy + service_cidr = var.service_cidr + dns_service_ip = var.dns_ip + docker_bridge_cidr = var.docker_cidr + } + + role_based_access_control { + enabled = true + } + + dynamic "service_principal" { + for_each = ! var.msi_enabled && var.service_principal_id != "" ? [{ + client_id = var.service_principal_id + client_secret = var.service_principal_secret + }] : [] + content { + client_id = service_principal.value.client_id + client_secret = service_principal.value.client_secret + } + } + + # This dynamic block enables managed service identity for the cluster + # in the case that the following holds true: + # 1: the msi_enabled input variable is set to true + dynamic "identity" { + for_each = var.msi_enabled ? [local.msi_identity_type] : [] + content { + type = identity.value + } + } + + addon_profile { + + oms_agent { + enabled = var.oms_agent_enabled + log_analytics_workspace_id = local.log_analytics_id + } + + # adding this as a patch to disable azurerm provider from redeploying due to unset + # internal "optional value". To be removed when azurerm provider is fixed. + kube_dashboard { + enabled = var.enable_kube_dashboard + } + } + + lifecycle { + ignore_changes = [ + default_node_pool[0].node_count, + addon_profile[0].oms_agent[0].log_analytics_workspace_id + ] + } +} + diff --git a/infra/modules/providers/azure/aks/outputs.tf b/infra/modules/providers/azure/aks/outputs.tf new file mode 100644 index 0000000000000000000000000000000000000000..6279128a95431d40b40eca2fc3714a16417798f9 --- /dev/null +++ b/infra/modules/providers/azure/aks/outputs.tf @@ -0,0 +1,60 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +output "id" { + value = azurerm_kubernetes_cluster.main.id +} + +output "name" { + value = azurerm_kubernetes_cluster.main.name +} + +output "client_certificate" { + sensitive = true + value = azurerm_kubernetes_cluster.main.kube_config.0.client_certificate +} + +output "kube_config" { + sensitive = true + value = azurerm_kubernetes_cluster.main.kube_config_raw +} + +output "kube_config_block" { + sensitive = true + value = azurerm_kubernetes_cluster.main.kube_config +} + +output "kubeconfig_done" { + value = join("", local_file.cluster_credentials.*.id) +} + +output "principal_id" { + value = azurerm_kubernetes_cluster.main.identity.0.principal_id +} + +output "kubelet_identity_id" { + value = azurerm_kubernetes_cluster.main.kubelet_identity.0.user_assigned_identity_id +} + +output "kubelet_object_id" { + value = azurerm_kubernetes_cluster.main.kubelet_identity.0.object_id +} + +output "kubelet_client_id" { + value = azurerm_kubernetes_cluster.main.kubelet_identity.0.client_id +} + +output "node_resource_group" { + value = azurerm_kubernetes_cluster.main.node_resource_group +} \ No newline at end of file diff --git a/infra/modules/providers/azure/aks/testing/main.tf b/infra/modules/providers/azure/aks/testing/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..81dc072091e34c842171b37fcf46f72dfb523989 --- /dev/null +++ b/infra/modules/providers/azure/aks/testing/main.tf @@ -0,0 +1,73 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +provider "azurerm" { + features {} +} + +module "resource_group" { + source = "../../resource-group" + name = "osdu-module" + location = "eastus2" +} + +module "network" { + source = "../../network" + + name = format("osdu-module-vnet-%s", module.resource_group.random) + resource_group_name = module.resource_group.name + address_space = "10.10.0.0/16" + dns_servers = ["8.8.8.8"] + subnet_prefixes = ["10.10.1.0/24"] + subnet_names = ["Cluster-Subnet"] +} + +resource "tls_private_key" "key" { + algorithm = "RSA" +} + +resource "null_resource" "save-key" { + triggers = { + key = tls_private_key.key.private_key_pem + } + + provisioner "local-exec" { + command = < ${path.module}/.ssh/id_rsa + chmod 0600 ${path.module}/.ssh/id_rsa + EOF + } +} + +data "azurerm_client_config" "current" {} + +module "aks" { + source = "../" + + name = format("osdu-module-cluster-%s", module.resource_group.random) + resource_group_name = module.resource_group.name + dns_prefix = format("osdu-module-cluster-%s", module.resource_group.random) + ssh_public_key = "${trimspace(tls_private_key.key.public_key_openssh)} k8sadmin" + vnet_subnet_id = module.network.subnets.0 + + msi_enabled = true + kubeconfig_to_disk = false + oms_agent_enabled = true + enable_kube_dashboard = false + + resource_tags = { + osdu = "module" + } +} \ No newline at end of file diff --git a/infra/modules/providers/azure/aks/testing/unit_test.go b/infra/modules/providers/azure/aks/testing/unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..924915f4f3be886f81596eda7da9d29c833276ba --- /dev/null +++ b/infra/modules/providers/azure/aks/testing/unit_test.go @@ -0,0 +1,60 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package test + +import ( + "encoding/json" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var name = "cluster-" +var location = "eastus" +var count = 12 + +var tfOptions = &terraform.Options{ + TerraformDir: "./", + Upgrade: true, +} + +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} + +func TestTemplate(t *testing.T) { + + expectedResult := asMap(t, `{ + "kubernetes_version": "1.17.7" + }`) + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tfOptions, + Workspace: name + random.UniqueId(), + PlanAssertions: nil, + ExpectedResourceCount: count, + ExpectedResourceAttributeValues: infratests.ResourceDescription{ + "module.aks.azurerm_kubernetes_cluster.main": expectedResult, + }, + } + + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/modules/providers/azure/aks/variables.tf b/infra/modules/providers/azure/aks/variables.tf new file mode 100644 index 0000000000000000000000000000000000000000..afe73b112598681bb3d5ed98785062e5cbd3988f --- /dev/null +++ b/infra/modules/providers/azure/aks/variables.tf @@ -0,0 +1,158 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +variable "name" { + description = "The name of the Kubernetes Cluster." + type = string +} + +variable "resource_group_name" { + description = "The name of an existing resource group." + type = string +} + +variable "resource_tags" { + description = "Map of tags to apply to taggable resources in this module. By default the taggable resources are tagged with the name defined above and this map is merged in" + type = map(string) + default = {} +} + +variable "dns_prefix" { + type = string +} + +variable "agent_vm_count" { + type = string + default = "2" +} + +variable "max_node_count" { + type = string + default = "10" +} + +variable "agent_vm_size" { + type = string + default = "Standard_D2s_v3" +} + +variable "max_pods" { + type = string + default = 30 +} + +variable "kubernetes_version" { + type = string + default = "1.17.9" +} + +variable "admin_user" { + type = string + default = "k8sadmin" +} + +variable "ssh_public_key" { + type = string +} + +variable "output_directory" { + type = string + default = "./output" +} + +variable "vnet_subnet_id" { + type = string +} + +variable "enable_virtual_node_addon" { + type = string + default = "false" +} + +variable "kubeconfig_to_disk" { + description = "This disables or enables the kube config file from being written to disk." + type = string + default = "true" +} + +variable "kubeconfig_filename" { + description = "Name of the kube config file saved to disk." + type = string + default = "bedrock_kube_config" +} + +variable "service_cidr" { + default = "10.0.0.0/16" + description = "Used to assign internal services in the AKS cluster an IP address. This IP address range should be an address space that isn't in use elsewhere in your network environment. This includes any on-premises network ranges if you connect, or plan to connect, your Azure virtual networks using Express Route or a Site-to-Site VPN connections." + type = string +} + +variable "dns_ip" { + default = "10.0.0.10" + description = "should be the .10 address of your service IP address range" + type = string +} + +variable "docker_cidr" { + default = "172.17.0.1/16" + description = "IP address (in CIDR notation) used as the Docker bridge IP address on nodes. Default of 172.17.0.1/16." +} + +variable "network_plugin" { + default = "azure" + description = "Network plugin used by AKS. Either azure or kubenet." +} + +variable "network_policy" { + default = "azure" + description = "Network policy to be used with Azure CNI. Either azure or calico." +} + +variable "auto_scaling_default_node" { + description = "(Optional) Kubernetes Auto Scaler must be enabled for this main pool" + type = bool + default = false +} + +variable "oms_agent_enabled" { + default = "false" + description = "Enable Azure Monitoring for AKS" + type = string +} + +variable "log_analytics_id" { + description = "Id of the log analytics workspace" + type = string + default = "" +} + +variable "service_principal_id" { + type = string + default = "" +} + +variable "service_principal_secret" { + type = string + default = "" +} + +variable "msi_enabled" { + type = bool + default = true +} + +variable "enable_kube_dashboard" { + type = bool + default = true +} \ No newline at end of file diff --git a/infra/modules/providers/azure/aks/virtualnodeaddon.tf b/infra/modules/providers/azure/aks/virtualnodeaddon.tf new file mode 100644 index 0000000000000000000000000000000000000000..6928b90d0947e1be3c69f1cf70abe2cb51c4208f --- /dev/null +++ b/infra/modules/providers/azure/aks/virtualnodeaddon.tf @@ -0,0 +1,21 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +resource "null_resource" "enable_virtual_node_addon" { + count = var.enable_virtual_node_addon ? 1 : 0 + + provisioner "local-exec" { + command = "az aks enable-addons --resource-group ${var.resource_group_name} --name ${var.name} --addons virtual-node --subnet-name ${var.name}-virtual-node-subnet" + } +} \ No newline at end of file diff --git a/infra/modules/providers/azure/app-insights/README.md b/infra/modules/providers/azure/app-insights/README.md new file mode 100755 index 0000000000000000000000000000000000000000..995f51899ae766f0384155bb36fa48474a22e64e --- /dev/null +++ b/infra/modules/providers/azure/app-insights/README.md @@ -0,0 +1,167 @@ +# Azure Application Insights + +Application Insights is an extensible Application Performance Management (APM) service for web developers on multiple platforms. It is used to monitor live web applications and can automatically detect performance anomalies. It also includes powerful analytics tools to help diagnose issues and to understand what users actually do with an app. + +More information for Azure Application Insights can be found [here](https://docs.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview). + +This directory contains a terraform module in Cobalt to create a new instance of Application Insights. + +The following characteristics can be specified by the user: + + - name: Specifies the name of the Application Insights component. Changing this forces a new resource to be created. + - application_type: Specifies the type of Application Insights to create. Valid values are ios for iOS, java for Java web, MobileCenter for App Center, Node.JS for Node.js, other for General, phone for Windows Phone, store for Windows Store and web for ASP.NET. Please note these values are case sensitive; unmatched values are treated as ASP.NET by Azure. Changing this forces a new resource to be created. + - resource_group_name: Specifies the resource group where the Service Plan was deployed (so the Application Insights instance gets deployed to the same resource group) + - tags : A mapping of tags to assign to the resource. + +Please click the [link](https://www.terraform.io/docs/providers/azurerm/r/application_insights.html) to get additional details on settings in Terraform for Azure Application Insights. + +## Usage + +Use of this App Insights module assumes that a resource group has already been created, and that an App Service Plan has already been deployed to it. The Cobalt module to deploy an App Service Plan is located [here](infra/modules/providers/azure/service-plan). + +### Usage in a Cobalt Template + +Sample usage of the Service Plan and App Insights modules in a Cobalt template: + +``` +variable "resource_group_name" { + value = "test-rg" +} + +variable "service_plan_name" { + value = "test-svcplan" +} + +variable "appinsights_name" { + value = "test-app-insights" +} + +variable "application_type" { + value = "Node.JS" +} + +variable "resource_tags" { + value = "{}" +} + +module "service_plan" { + source = "" + resource_group_name = "${var.resource_group_name}" + resource_group_location = "${var.resource_group_location}" + service_plan_name = "${var.service_plan_name}" +} + +module "app_insights" { + source = "" + service_plan_resource_group_name = "${var.resource_group_name}" + appinsights_name = "${var.appinsights_name}" + application_type = "${var.application_type}" + resource_tags = "${var.resource_tags}" +} +``` + +### Manual Execution with Terraform + +Note: Terraform will prompt for any variable values which are not passed into the `plan` or `apply` command, and for which default values are not set. + +Sample manual execution of the module using Terraform from within the `infra/modules/providers/azure/app-insights` directory is shown below. + +Note: Descriptions for each value are located in the `variables.tf` file. + +``` +c:\Users\user\repos\cobalt\infra\modules\providers\azure\app-insights>terraform apply +var.appinsights_application_type + Type of the App Insights Application. Valid values are ios for iOS, java for Java web, MobileCenter for App Center, Node.JS for Node.js, other for General, phone for Windows Phone, store for Windows Store and web for ASP.NET. + + Enter a value: Node.JS + +var.appinsights_name + Name of the App Insights to create + + Enter a value: test-app-insights + +var.resource_tags + Map of tags to apply to taggable resources in this module (enter as a set of curly braces containing key-value pairs, as in: {"tag1" = "value1", "tag2" = "value2"}). By default the taggable resources are tagged with the name defined above and this map is merged in + + Enter a value: {"tag1" = "value1", "tag2" = "value2"} + +var.service_plan_resource_group_name + The name of the resource group in which the service plan was created. + + Enter a value: test-rg + ``` + +Terraform will output its execution plan and requires the user to approve the changes by typing "yes": + +``` +data.azurerm_resource_group.appinsights: Refreshing state... + +An execution plan has been generated and is shown below. +Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + + azurerm_application_insights.appinsights + id: + app_id: + application_type: "Node.JS" + instrumentation_key: + location: "eastus" + name: "test-app-insights" + resource_group_name: "test-rg" + tags.%: + + +Plan: 1 to add, 0 to change, 0 to destroy. + +Do you want to perform these actions? + Terraform will perform the actions described above. + Only 'yes' will be accepted to approve. + + Enter a value: yes +``` + +The module will then deploy the App Insights instance specified above: + +``` +azurerm_application_insights.appinsights: Creating... + app_id: "" => "" + application_type: "" => "Node.JS" + instrumentation_key: "" => "" + location: "" => "eastus" + name: "" => "test-app-insights" + resource_group_name: "" => "test-rg" + tags.%: "" => "" +azurerm_application_insights.appinsights: Creation complete after 8s (ID: /subscriptions/xxxxbca0-7axx-xxbd-b5xx-...sights/components/test-app-insights) + +Apply complete! Resources: 1 added, 0 changed, 0 destroyed. +``` + + +## Output + +Once the deployments are completed successfully, the output for the App Insights module will be in the format shown below: + +``` +Outputs: + +app_insights_app_id = xxxx5ba9-f5xx-xx94-93xx-xxxx0d40xxxx +app_insights_instrumentation_key = xxxx75785-xx5f-42xx-xx80-xxxxx94c93xx +``` + + +## License +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/infra/modules/providers/azure/app-insights/main.tf b/infra/modules/providers/azure/app-insights/main.tf new file mode 100755 index 0000000000000000000000000000000000000000..014c17474b898a0aeb7c27daefea1cca413d4baf --- /dev/null +++ b/infra/modules/providers/azure/app-insights/main.tf @@ -0,0 +1,26 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +data "azurerm_resource_group" "appinsights" { + name = var.service_plan_resource_group_name +} + +resource "azurerm_application_insights" "appinsights" { + name = var.appinsights_name + resource_group_name = data.azurerm_resource_group.appinsights.name + location = data.azurerm_resource_group.appinsights.location + application_type = var.appinsights_application_type + tags = var.resource_tags +} + diff --git a/infra/modules/providers/azure/app-insights/output.tf b/infra/modules/providers/azure/app-insights/output.tf new file mode 100755 index 0000000000000000000000000000000000000000..3221ccf580557b38da002a804ea59bafa0883e92 --- /dev/null +++ b/infra/modules/providers/azure/app-insights/output.tf @@ -0,0 +1,25 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +output "app_insights_app_id" { + description = "The App ID associated with this Application Insights component" + value = azurerm_application_insights.appinsights.app_id +} + +output "app_insights_instrumentation_key" { + description = "The Instrumentation Key for this Application Insights component." + value = azurerm_application_insights.appinsights.instrumentation_key + sensitive = true +} + diff --git a/infra/modules/providers/azure/app-insights/testing/main.tf b/infra/modules/providers/azure/app-insights/testing/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..eccde725b9b3fb03d3f3fc6b57e7cc91c39c3fa4 --- /dev/null +++ b/infra/modules/providers/azure/app-insights/testing/main.tf @@ -0,0 +1,36 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +provider "azurerm" { + features {} +} + +module "resource_group" { + source = "../../resource-group" + + name = "osdu-module" + location = "eastus2" +} + +module "app-insights" { + source = "../" + + appinsights_name = "osdu-module-app-insights-${module.resource_group.random}" + service_plan_resource_group_name = module.resource_group.name + appinsights_application_type = "java" + + resource_tags = { + osdu = "module" + } +} diff --git a/infra/modules/providers/azure/app-insights/testing/unit_test.go b/infra/modules/providers/azure/app-insights/testing/unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..df35af7e49cf15551594e4bec1dd7181dde1466b --- /dev/null +++ b/infra/modules/providers/azure/app-insights/testing/unit_test.go @@ -0,0 +1,48 @@ +package test + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var workspace = "osdu-services-" + strings.ToLower(random.UniqueId()) +var location = "eastus" +var count = 4 + +var tfOptions = &terraform.Options{ + TerraformDir: "./", + Upgrade: false, +} + +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} + +func TestTemplate(t *testing.T) { + + expectedResult := asMap(t, `{ + "application_type" : "java" + }`) + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tfOptions, + Workspace: workspace, + PlanAssertions: nil, + ExpectedResourceCount: count, + ExpectedResourceAttributeValues: infratests.ResourceDescription{ + "module.app-insights.azurerm_application_insights.appinsights": expectedResult, + }, + } + + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/modules/providers/azure/app-insights/variables.tf b/infra/modules/providers/azure/app-insights/variables.tf new file mode 100755 index 0000000000000000000000000000000000000000..a9f9947f27825c417417f9d8807bc14d7ffbe6ce --- /dev/null +++ b/infra/modules/providers/azure/app-insights/variables.tf @@ -0,0 +1,35 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +variable "service_plan_resource_group_name" { + description = "The name of the resource group in which the service plan was created." + type = string +} + +variable "resource_tags" { + description = "Map of tags to apply to taggable resources in this module (enter as a set of curly braces containing key-value pairs, as in: {\"tag1\" = \"value1\", \"tag2\" = \"value2\"}). By default the taggable resources are tagged with the name defined above and this map is merged in" + type = map(string) + default = {} +} + +variable "appinsights_name" { + description = "Name of the App Insights to create" + type = string +} + +variable "appinsights_application_type" { + description = "Type of the App Insights Application. Valid values are ios for iOS, java for Java web, MobileCenter for App Center, Node.JS for Node.js, other for General, phone for Windows Phone, store for Windows Store and web for ASP.NET." + type = string +} + diff --git a/infra/modules/providers/azure/app-insights/versions.tf b/infra/modules/providers/azure/app-insights/versions.tf new file mode 100755 index 0000000000000000000000000000000000000000..b2d3f76baf77aa01523d08c2c19ced85c0848b77 --- /dev/null +++ b/infra/modules/providers/azure/app-insights/versions.tf @@ -0,0 +1,18 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +terraform { + required_version = ">= 0.12" +} diff --git a/infra/modules/providers/azure/app-monitoring/README.md b/infra/modules/providers/azure/app-monitoring/README.md new file mode 100755 index 0000000000000000000000000000000000000000..ca5b96064b5ba20d54cf41905f7af26e1ff3997a --- /dev/null +++ b/infra/modules/providers/azure/app-monitoring/README.md @@ -0,0 +1,147 @@ +# Azure Monitor + +This Terraform based `app-monitoring` module grants templates the ability to define and assign `alert criteria` to "Azure Resources" using Microsoft's _**Azure Monitor**_ service. `alert criteria` is conditional logic that targets a list of one or more "Azure Resources" for the purposes of monitoring their behavior. `alert criteria` comes in two forms, `metrics` and `logs`. This module introduces `metrics` based monitoring only. As a result, `alert criteria` in the context of this module is refered to as `metric alert criteria`. The way `metric alert criteria` is configured depends entirely on the `metrics` options offered by the "Azure Resources" selected for monitoring. (ex. The App Service Plan resource offers 'CpuMonitoring' as a configurable `metric`.) + +In addition to the `metric alert criteria`, this module introduces a configurable `action group` that when paired with `metric alert criteria` can be configured to trigger events like e-mail notifications. + +#### _More on Azure Monitoring_ + +> "Azure Monitor maximizes the availability and performance of your applications by delivering a comprehensive solution for collecting, analyzing, and acting on telemetry from your cloud and on-premises environments. +> All data collected by Azure Monitor fits into one of two fundamental types, metrics and logs. Metrics are numerical values that describe some aspect of a system at a particular point in time. They are lightweight and capable of supporting near real-time scenarios. Logs contain different kinds of data organized into records with different sets of properties for each type." - Source: Microsoft's [Azure Monitor Service Overview](https://docs.microsoft.com/en-us/azure/azure-monitor/overview) + +This module deploys the _**Azure Monitor**_ service in order to offer visibility into the behavior of your deployed "Azure Resources". This module is recommended for `metrics` based monitoring of any "Azure Resource" deployed alongside App Services (ex. App Service Plan, App Gateway, VM, etc.). The list of `metrics` available for configuration depends entirely on the Azure Resource chosen to be monitored. For more information on how `metrics` work, please visit Microsoft's [alerts-metric-overview](https://docs.microsoft.com/en-us/azure/azure-monitor/platform/alerts-metric-overview) documentation. + +For monitoring of an App Service, the [app-insights](../app-insights) module is instead recommended. The [app-insights](../app-insights) module leverages an _**Azure Monitor**_ service called [application insights](https://www.terraform.io/docs/providers/azurerm/r/application_insights.html). + +## Characteristics + +An instance of the `app-monitoring` module deploys the _**Azure Monitor**_ service in order to provide templates with the following: + +- Ability to deploy Azure Monitor within a single resource group. + +- Ability to deploy Azure Monitor with a single set of configurable `metric alert criteria` targeting one or more Azure resources. + +- Ability to deploy Azure Monitor with a single set of configurable `metric alert criteria` tied to a single `action group`. + +## Definition + +App Monitoring definition example: + +```terraform +resource "azurerm_monitor_action_group" "appmonitoring" { + count = "${var.action_group_email_receiver == "" ? 0 : 1}" + name = "${var.action_group_name}" + resource_group_name = "${var.resource_group_name}" +} + +resource "azurerm_monitor_metric_alert" "appmonitoring" { + count = "${var.action_group_email_receiver == "" ? 0 : 1}" + name = "${var.metric_alert_name}" + resource_group_name = "${azurerm_monitor_action_group.appmonitoring.resource_group_name}" + scopes = ["${var.resource_ids}"] + + criteria { + metric_namespace = "${var.metric_alert_criteria_namespace}" + metric_name = "${var.metric_alert_criteria_name}" + aggregation = "${var.metric_alert_criteria_aggregation}" + operator = "${var.metric_alert_criteria_operator}" + threshold = "${var.metric_alert_criteria_threshold}" + } + + action { + action_group_id = "${azurerm_monitor_action_group.appmonitoring.id}" + } +} +``` + +Terraform resources used to define the `app-monitoring` module include the following: + +- [azurerm_monitor_metric_alert](https://www.terraform.io/docs/providers/azurerm/r/monitor_metric_alert.html) + +- [azurerm_monitor_action_group](https://www.terraform.io/docs/providers/azurerm/r/monitor_action_group.html) + +## Usage + +App Monitoring usage example: + +```terraform +variable "resource_group_name" { + value = "test-rg" +} + +variable "service_plan_name" { + value = "test-svcplan" +} + +module "service_plan" { + resource_group_name = "${var.resource_group_name}" + resource_group_location = "${var.resource_group_location}" + service_plan_name = "${var.service_plan_name}" +} + +module "app_monitoring" { + source = "../../modules/providers/azure/app-monitoring" + resource_group_name = "${module.service_plan.resource_group_name}" + resource_ids = ["${module.service_plan.app_service_plan_id}"] + action_group_name = "${var.action_group_name}" + action_group_email_receiver = "${var.action_group_email_receiver}" + metric_alert_name = "${var.metric_alert_name}" + metric_alert_frequency = "${var.metric_alert_frequency}" + metric_alert_period = "${var.metric_alert_period}" + metric_alert_criteria_namespace = "${var.metric_alert_criteria_namespace}" + metric_alert_criteria_name = "${var.metric_alert_criteria_name}" + metric_alert_criteria_aggregation = "${var.metric_alert_criteria_aggregation}" + metric_alert_criteria_operator = "${var.metric_alert_criteria_operator}" + metric_alert_criteria_threshold = "${var.metric_alert_criteria_threshold}" + monitoring_dimension_values = "${var.monitoring_dimension_values}" +} +``` + +#### Configuring `resource_ids` + +Please visit _**Azure Monitor's**_ [monitoring at scale](https://docs.microsoft.com/en-us/azure/azure-monitor/platform/alerts-metric-overview#monitoring-at-scale-using-metric-alerts-in-azure-monitor.) page for more information on choosing multiple "Azure Resources". + +- Resource IDs can either be a list of VM IDs or a single Azure Resource ID. + +#### Configuring `metric alert criteria` + +Please visit _**Azure Monitor's**_ [Supported Metrics](https://docs.microsoft.com/en-us/azure/azure-monitor/platform/metrics-supported) page for a complete list of supported metrics per "Azure Resource". + +1. View the list of 'metrics' offered by the namespace of your chosen "Azure Resource". +2. Choose a metric. +3. Define the metric's criteria (i.e. Conditional logic being applied to an "Azure Resource".) +4. Apply that criteria to your chosen or newly created template. + +#### Configuring `action group` + +Please visit _**Azure Monitor's**_ [action-groups](https://docs.microsoft.com/en-us/azure/azure-monitor/platform/action-groups) page for more information on e-mail action groups. + +1. Choose an appropriate e-mail address to receive alert notifications. +2. Choose an appropriate name for the 'action group' that will hold the e-mail address. (ex. "E-mail Alert Group") +3. Apply the e-mail address and name to your chosen or newly created template. + +## Argument Reference + +Supported arguments for this module are available in [variables.tf](variables.tf). + +## Attributes Reference + +The following attributes are exported: + +- `rule_resource_id`: The ID of the metric alert. + + +## License +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/infra/modules/providers/azure/app-monitoring/main.tf b/infra/modules/providers/azure/app-monitoring/main.tf new file mode 100755 index 0000000000000000000000000000000000000000..8e177f646be4beadc460690eabd3345b1113b78e --- /dev/null +++ b/infra/modules/providers/azure/app-monitoring/main.tf @@ -0,0 +1,58 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +locals { + scaling_name = "Instance" + scaling_operator = "Include" +} + +resource "azurerm_monitor_action_group" "appmonitoring" { + count = var.action_group_email_receiver == "" ? 0 : 1 + name = var.action_group_name + resource_group_name = var.resource_group_name + short_name = var.action_group_short_name + + email_receiver { + name = var.action_group_email_receiver_name + email_address = var.action_group_email_receiver + } +} + +resource "azurerm_monitor_metric_alert" "appmonitoring" { + count = var.action_group_email_receiver == "" ? 0 : 1 + name = var.metric_alert_name + resource_group_name = azurerm_monitor_action_group.appmonitoring[0].resource_group_name + scopes = var.resource_ids + frequency = var.metric_alert_frequency + window_size = var.metric_alert_period + + criteria { + metric_namespace = var.metric_alert_criteria_namespace + metric_name = var.metric_alert_criteria_name + aggregation = var.metric_alert_criteria_aggregation + operator = var.metric_alert_criteria_operator + threshold = var.metric_alert_criteria_threshold + + dimension { + name = local.scaling_name + operator = local.scaling_operator + values = var.monitoring_dimension_values + } + } + + action { + action_group_id = azurerm_monitor_action_group.appmonitoring[0].id + } +} + diff --git a/infra/modules/providers/azure/app-monitoring/output.tf b/infra/modules/providers/azure/app-monitoring/output.tf new file mode 100755 index 0000000000000000000000000000000000000000..640b4cea4f3186d26e0e1227d11a0a4020f12cec --- /dev/null +++ b/infra/modules/providers/azure/app-monitoring/output.tf @@ -0,0 +1,19 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +output "rule_resource_id" { + description = "The id of a metric alert rule." + value = azurerm_monitor_metric_alert.appmonitoring.*.id +} + diff --git a/infra/modules/providers/azure/app-monitoring/testing/main.tf b/infra/modules/providers/azure/app-monitoring/testing/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..a36cba7b52f673f7a50502c1c8806a2db142699a --- /dev/null +++ b/infra/modules/providers/azure/app-monitoring/testing/main.tf @@ -0,0 +1,41 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +provider "azurerm" { + features {} +} + +module "resource_group" { + source = "../../resource-group" + name = "osdu-module" + location = "eastus2" +} + +module "app_monitoring" { + source = "../" + + resource_group_name = module.resource_group.name + action_group_name = var.action_group_name + + # action_group_email_receiver = "${var.action_group_email_receiver}" + # metric_alert_name = "${var.metric_alert_name}" + # metric_alert_frequency = "${var.metric_alert_frequency}" + # metric_alert_period = "${var.metric_alert_period}" + # metric_alert_criteria_namespace = "${var.metric_alert_criteria_namespace}" + # metric_alert_criteria_name = "${var.metric_alert_criteria_name}" + # metric_alert_criteria_aggregation = "${var.metric_alert_criteria_aggregation}" + # metric_alert_criteria_operator = "${var.metric_alert_criteria_operator}" + # metric_alert_criteria_threshold = "${var.metric_alert_criteria_threshold}" + # monitoring_dimension_values = "${var.monitoring_dimension_values}" +} \ No newline at end of file diff --git a/infra/modules/providers/azure/app-monitoring/variables.tf b/infra/modules/providers/azure/app-monitoring/variables.tf new file mode 100755 index 0000000000000000000000000000000000000000..e79e3028364e0878374215dc2bd23531952ee8b2 --- /dev/null +++ b/infra/modules/providers/azure/app-monitoring/variables.tf @@ -0,0 +1,97 @@ +// Copyright � Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +variable "resource_group_name" { + description = "The name of the resource group." + type = string +} + +# action group attributes +variable "action_group_name" { + description = "The name of the action group." + type = string +} + +variable "action_group_email_receiver" { + description = "The e-mail receiver for an alert rule resource." + type = string + default = "" +} + +variable "action_group_email_receiver_name" { + description = "The e-mail receiver name for an alert group." + type = string + default = "E-mail Receiver" +} + +variable "action_group_short_name" { + description = "The abbreviated name of the action group." + type = string + default = "Notify" +} + +# metric alert attributes +variable "resource_ids" { + description = "Resource Ids to be monitored." + type = list(string) + default = [] +} + +variable "metric_alert_name" { + description = "The display name of a group of metric alert criteria." + type = string +} + +variable "metric_alert_criteria_name" { + description = "A predefined Azure resource alert monitoring rule name." + type = string +} + +variable "metric_alert_criteria_namespace" { + description = "A monitored resource namespace that holds metric alert criteria." + type = string +} + +variable "metric_alert_criteria_aggregation" { + description = "The calculation used for building metric alert criteria." + type = string +} + +variable "metric_alert_criteria_operator" { + description = "A logical operator used for building metric alert criteria." + type = string +} + +variable "metric_alert_criteria_threshold" { + description = "The criteria threshold value that activates the metric alert." + type = string +} + +variable "monitoring_dimension_values" { + description = "Dimensions used to determine service plan scaling." + type = list(string) +} + +variable "metric_alert_frequency" { + description = "The frequency with which the metric alert checks if the conditions are met." + type = string + default = "PT1M" +} + +variable "metric_alert_period" { + description = "The look back window over which metric values are checked." + type = string + default = "PT5M" +} + diff --git a/infra/modules/providers/azure/app-monitoring/versions.tf b/infra/modules/providers/azure/app-monitoring/versions.tf new file mode 100755 index 0000000000000000000000000000000000000000..b2d3f76baf77aa01523d08c2c19ced85c0848b77 --- /dev/null +++ b/infra/modules/providers/azure/app-monitoring/versions.tf @@ -0,0 +1,18 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +terraform { + required_version = ">= 0.12" +} diff --git a/infra/modules/providers/azure/appgw/main.tf b/infra/modules/providers/azure/appgw/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..9bfdb99ad1a3eacd54eb9bbd7baf584ac48e3ff3 --- /dev/null +++ b/infra/modules/providers/azure/appgw/main.tf @@ -0,0 +1,198 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +data "azurerm_client_config" "current" {} + +data "azurerm_resource_group" "main" { + name = var.resource_group_name +} + +locals { + public_ip_name = format("%s-ip", var.name) + gateway_ip_configuration_name = format("%s-config", var.vnet_name) + backend_address_pool_name = format("%s-beap", var.vnet_name) + frontend_port_name = format("%s-feport", var.vnet_name) + frontend_ip_configuration_name = format("%s-feip", var.vnet_name) + backend_http_settings = format("%s-be-htst", var.vnet_name) + listener_name = format("%s-httplstn", var.vnet_name) + request_routing_rule_name = format("%s-rqrt", var.vnet_name) + identity_name = format("%s-identity", var.name) +} + +resource "azurerm_public_ip" "main" { + name = local.public_ip_name + resource_group_name = data.azurerm_resource_group.main.name + location = data.azurerm_resource_group.main.location + + allocation_method = "Static" + sku = "Standard" + domain_name_label = var.name + + tags = var.resource_tags +} + +// This Identity is used for accessing Key Vault to retrieve SSL Certificate +resource "azurerm_user_assigned_identity" "main" { + name = local.identity_name + resource_group_name = data.azurerm_resource_group.main.name + location = data.azurerm_resource_group.main.location + + tags = var.resource_tags +} + + +module "app_gw_keyvault_access_policy" { + source = "../keyvault-policy" + vault_id = var.keyvault_id + tenant_id = data.azurerm_client_config.current.tenant_id + object_ids = [ + azurerm_user_assigned_identity.main.principal_id + ] + key_permissions = [] + secret_permissions = ["get"] + certificate_permissions = ["get"] +} + + +## Reference Configuration: https://docs.microsoft.com/en-us/azure/application-gateway/configuration-overview +resource "azurerm_application_gateway" "main" { + name = var.name + resource_group_name = data.azurerm_resource_group.main.name + location = data.azurerm_resource_group.main.location + tags = var.resource_tags + + sku { + name = "WAF_v2" + tier = "WAF_v2" + } + + waf_configuration { + enabled = var.tier == "WAF" || var.tier == "WAF_v2" ? true : false + firewall_mode = var.waf_config_firewall_mode + rule_set_type = "OWASP" + rule_set_version = "3.1" + } + + identity { + identity_ids = [azurerm_user_assigned_identity.main.id] + } + + autoscale_configuration { + min_capacity = 2 + } + + gateway_ip_configuration { + name = local.gateway_ip_configuration_name + subnet_id = var.vnet_subnet_id + } + + frontend_ip_configuration { + name = local.frontend_ip_configuration_name + public_ip_address_id = azurerm_public_ip.main.id + } + + ######## + ### Listener 1 http://mygateway.com + ######## + + http_listener { + name = format("http-%s", local.listener_name) + frontend_ip_configuration_name = local.frontend_ip_configuration_name + frontend_port_name = format("http-%s", local.frontend_port_name) + protocol = "Http" + } + + frontend_port { + name = format("http-%s", local.frontend_port_name) + port = 80 + } + + request_routing_rule { + name = format("http-%s", local.request_routing_rule_name) + rule_type = "Basic" + http_listener_name = format("http-%s", local.listener_name) + backend_address_pool_name = format("http-%s", local.backend_address_pool_name) + backend_http_settings_name = format("http-%s", local.backend_http_settings) + } + + backend_http_settings { + name = format("http-%s", local.backend_http_settings) + cookie_based_affinity = "Disabled" + port = 80 + protocol = "Http" + request_timeout = 1 + } + + backend_address_pool { + name = format("http-%s", local.backend_address_pool_name) + } + + ######## + ### Listener 2 https://mygateway.com + ######## + + http_listener { + name = format("https-%s", local.listener_name) + frontend_ip_configuration_name = local.frontend_ip_configuration_name + frontend_port_name = format("https-%s", local.frontend_port_name) + protocol = "Https" + ssl_certificate_name = var.ssl_certificate_name + } + + frontend_port { + name = format("https-%s", local.frontend_port_name) + port = 443 + } + + ssl_certificate { + name = var.ssl_certificate_name + key_vault_secret_id = var.keyvault_secret_id + } + + request_routing_rule { + name = format("https-%s", local.request_routing_rule_name) + rule_type = "Basic" + http_listener_name = format("https-%s", local.listener_name) + backend_address_pool_name = format("https-%s", local.backend_address_pool_name) + backend_http_settings_name = format("https-%s", local.backend_http_settings) + } + + backend_http_settings { + name = format("https-%s", local.backend_http_settings) + cookie_based_affinity = "Disabled" + port = 443 + protocol = "Https" + request_timeout = 1 + } + + backend_address_pool { + name = format("https-%s", local.backend_address_pool_name) + } + + lifecycle { + ignore_changes = [ + ssl_certificate, + request_routing_rule, + http_listener, + backend_http_settings, + backend_address_pool, + probe, + tags, + frontend_port, + redirect_configuration, + url_path_map + ] + } +} \ No newline at end of file diff --git a/infra/modules/providers/azure/appgw/outputs.tf b/infra/modules/providers/azure/appgw/outputs.tf new file mode 100644 index 0000000000000000000000000000000000000000..ffe43624c29915e0bdf7eed8c1346ec859537b76 --- /dev/null +++ b/infra/modules/providers/azure/appgw/outputs.tf @@ -0,0 +1,44 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +output "name" { + description = "The name of the Application Gateway created" + value = azurerm_application_gateway.main.name +} + +output "id" { + description = "The resource id of the Application Gateway created" + value = azurerm_application_gateway.main.id +} + +output "ipconfig" { + description = "The Application Gateway IP Configuration" + value = azurerm_application_gateway.main.gateway_ip_configuration +} + +output "frontend_ip_configuration" { + description = "The Application Gateway Frontend IP Configuration" + value = azurerm_application_gateway.main.frontend_ip_configuration +} + +output "managed_identity_resource_id" { + description = "The resource id of the managed user identity" + value = azurerm_user_assigned_identity.main.id +} + +output "managed_identity_principal_id" { + description = "The resource id of the managed user identity" + value = azurerm_user_assigned_identity.main.principal_id +} diff --git a/infra/modules/providers/azure/appgw/testing/main.tf b/infra/modules/providers/azure/appgw/testing/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..297e1f879d119fc39fa6ff9196cf34a21e5ab2e6 --- /dev/null +++ b/infra/modules/providers/azure/appgw/testing/main.tf @@ -0,0 +1,126 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +provider "azurerm" { + features {} +} + +locals { + ssl_cert_name = "test-ssl" + location = "eastus2" +} + + +module "resource_group" { + source = "../../resource-group" + + name = "osdu-module" + location = local.location +} + +module "keyvault" { + source = "../../keyvault" + + keyvault_name = substr("osdu-module-kv-${module.resource_group.random}", 0, 24) + resource_group_name = module.resource_group.name +} + +resource "azurerm_key_vault_certificate" "test" { + name = local.ssl_cert_name + key_vault_id = module.keyvault.keyvault_id + + certificate_policy { + issuer_parameters { + name = "Self" + } + + key_properties { + exportable = true + key_size = 2048 + key_type = "RSA" + reuse_key = true + } + + lifetime_action { + action { + action_type = "AutoRenew" + } + + trigger { + days_before_expiry = 30 + } + } + + secret_properties { + content_type = "application/x-pkcs12" + } + + x509_certificate_properties { + # Server Authentication = 1.3.6.1.5.5.7.3.1 + # Client Authentication = 1.3.6.1.5.5.7.3.2 + extended_key_usage = ["1.3.6.1.5.5.7.3.1"] + + key_usage = [ + "cRLSign", + "dataEncipherment", + "digitalSignature", + "keyAgreement", + "keyCertSign", + "keyEncipherment", + ] + + subject_alternative_names { + dns_names = ["internal.contoso.com", "osdu-module-gw-${module.resource_group.random}.${local.location}.cloudapp.azure.com"] + } + + subject = "CN=*.contoso.com" + validity_in_months = 12 + } + } +} + + +module "network" { + source = "../../network" + + name = "osdu-module-vnet-${module.resource_group.random}" + resource_group_name = module.resource_group.name + + address_space = "10.10.0.0/16" + dns_servers = ["8.8.8.8"] + subnet_prefixes = ["10.10.1.0/24", "10.10.2.0/24"] + subnet_names = ["frontend", "backend"] + + # Tags + resource_tags = { + osdu = "module" + } +} + +module "appgateway" { + source = "../" + + name = "osdu-module-gw-${module.resource_group.random}" + resource_group_name = module.resource_group.name + vnet_name = module.network.name + vnet_subnet_id = module.network.subnets[1] + keyvault_id = module.keyvault.keyvault_id + keyvault_secret_id = azurerm_key_vault_certificate.test.secret_id + ssl_certificate_name = local.ssl_cert_name + + # Tags + resource_tags = { + osdu = "module" + } +} \ No newline at end of file diff --git a/infra/modules/providers/azure/appgw/testing/unit_test.go b/infra/modules/providers/azure/appgw/testing/unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2e4a90e5240c6ae50e18817a318ccdefe919ff86 --- /dev/null +++ b/infra/modules/providers/azure/appgw/testing/unit_test.go @@ -0,0 +1,75 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "encoding/json" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var name = "cluster-" +var location = "eastus" +var count = 15 + +var tfOptions = &terraform.Options{ + TerraformDir: "./", + Upgrade: true, +} + +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} + +func TestTemplate(t *testing.T) { + + ipExpectedResult := asMap(t, `{ + "sku": "Standard", + "allocation_method": "Static" + }`) + + gatewayExpectedResult := asMap(t, `{ + "autoscale_configuration": [{ + "min_capacity": 2 + }], + "identity": [{ + "type": "UserAssigned" + }], + "request_routing_rule": [{ + "rule_type": "Basic" + }] + }`) + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tfOptions, + Workspace: name + random.UniqueId(), + PlanAssertions: nil, + ExpectedResourceCount: count, + ExpectedResourceAttributeValues: infratests.ResourceDescription{ + "module.appgateway.azurerm_public_ip.main": ipExpectedResult, + "module.appgateway.azurerm_application_gateway.main": gatewayExpectedResult, + }, + } + + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/modules/providers/azure/appgw/tests/integration/appgateway.go b/infra/modules/providers/azure/appgw/tests/integration/appgateway.go new file mode 100644 index 0000000000000000000000000000000000000000..85448c941c22a40a70c45d490e74aae5d60d7847 --- /dev/null +++ b/infra/modules/providers/azure/appgw/tests/integration/appgateway.go @@ -0,0 +1,68 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package integration + +import ( + "os" + "testing" + + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-02-01/network" + "github.com/microsoft/cobalt/test-harness/infratests" + "github.com/microsoft/cobalt/test-harness/terratest-extensions/modules/azure" + "github.com/stretchr/testify/require" +) + +var subscription = os.Getenv("ARM_SUBSCRIPTION_ID") + +func checkMinCapactiy(t *testing.T, appGatewayProperties *network.ApplicationGatewayPropertiesFormat) { + minCapacity := appGatewayProperties.AutoscaleConfiguration.MinCapacity + require.Equal(t, int32(2), *minCapacity) +} + +func checkOWASPRuleset(t *testing.T, appGatewayProperties *network.ApplicationGatewayPropertiesFormat) { + firewallRulesetType := appGatewayProperties.WebApplicationFirewallConfiguration.RuleSetType + firewallRulesetVersion := appGatewayProperties.WebApplicationFirewallConfiguration.RuleSetVersion + require.Equal(t, "OWASP", *firewallRulesetType, "Firewall ruleset type is incorrect") + require.Equal(t, "3.1", *firewallRulesetVersion, "Firewall ruleset version is incorrect") +} + +func checkAvailablePorts(t *testing.T, appGatewayProperties *network.ApplicationGatewayPropertiesFormat) { + foundPort := false + frontendPorts := appGatewayProperties.FrontendPorts + for _, frontend := range *frontendPorts { + if *frontend.Port == int32(443) { + foundPort = true + } + } + require.True(t, foundPort, "Failed to find a frontendPort with port 443") +} + +// InspectAppGateway - Runs a suite of test assertions to validate properties for an application gateway +func InspectAppGateway(resourceGroupNameOutput string, appGatewayNameOutput string, keyvaultIDOutput string) func(t *testing.T, output infratests.TerraformOutput) { + return func(t *testing.T, output infratests.TerraformOutput) { + appGatewayName := output[appGatewayNameOutput].(string) + resourceGroupName := output[resourceGroupNameOutput].(string) + + appGateway, err := azure.GetAppGatewayProperties(subscription, resourceGroupName, appGatewayName) + if err != nil { + t.Fatal(err) + } + + appGatewayProperties := appGateway.ApplicationGatewayPropertiesFormat + + checkMinCapactiy(t, appGatewayProperties) + checkOWASPRuleset(t, appGatewayProperties) + } +} diff --git a/infra/modules/providers/azure/appgw/variables.tf b/infra/modules/providers/azure/appgw/variables.tf new file mode 100644 index 0000000000000000000000000000000000000000..ec6f046c3ff26615acfa67f4e9e453fe1c4526ff --- /dev/null +++ b/infra/modules/providers/azure/appgw/variables.tf @@ -0,0 +1,74 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +variable "name" { + description = "The name of the application gateway." + type = string +} + +variable "resource_group_name" { + description = "Resource group name that the app gateway will be created in." + type = string +} + +variable "resource_tags" { + description = "Map of tags to apply to taggable resources in this module. By default the taggable resources are tagged with the name defined above and this map is merged in" + type = map(string) + default = {} +} + +variable "sku_name" { + description = "The SKU for the Appication Gateway to be created" + type = string + default = "WAF_v2" +} + +variable "tier" { + description = "The tier of the application gateway. Small/Medium/Large. More details can be found at https://azure.microsoft.com/en-us/pricing/details/application-gateway/" + type = string + default = "WAF_v2" +} + +variable "waf_config_firewall_mode" { + description = "The firewall mode on the waf gateway" + type = string + default = "Detection" +} + +variable "vnet_name" { + description = "Virtual Network name that the app gateway will be created in." + type = string +} + +variable "vnet_subnet_id" { + description = "Subnet id that the app gateway will be created in." + type = string +} + +variable "keyvault_id" { + description = "Key Vault ID holding the ssl certificate used for enabling tls termination." + type = string +} + +variable "keyvault_secret_id" { + description = "Key Vault secret ID holding the ssl certificate used for enabling tls termination." + type = string +} + +variable "ssl_certificate_name" { + description = "The Name of the SSL certificate that is unique within this Application Gateway" + type = string + default = "ssl-cert" +} \ No newline at end of file diff --git a/infra/modules/providers/azure/container-registry/README.md b/infra/modules/providers/azure/container-registry/README.md new file mode 100755 index 0000000000000000000000000000000000000000..3b652da26ec2001d5fa017fb27d3d98047dba125 --- /dev/null +++ b/infra/modules/providers/azure/container-registry/README.md @@ -0,0 +1,63 @@ +# Module Azure Container Registry + +Simplify container development by easily storing and managing container images for Azure deployments in a central registry. Azure Container Registry allows you to build, store, and manage images for all types of container deployments. + +More information for Azure Container Registry can be found [here](https://azure.microsoft.com/en-us/services/container-registry/) + +A terraform module in Cobalt to provide the Container Registry with the following characteristics: + +- Ability to specify resource group name in which the Container Registry is deployed. +- Ability to specify resource group location in which the Azure Container Registry is deployed. +- Also gives ability to specify the following for Azure Container Registry based on the requirements: + - name : (Required) Specifies the name of the Container Registry. Changing this forces a new resource to be created. + - resource_group_name : (Required) The name of the resource group in which to create the Container Registry. Changing this forces a new resource to be created. + - location : (Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created. + - admin_enabled : (Optional) Specifies whether the admin user is enabled. Defaults to false. + - sku : (Optional) The SKU name of the the container registry. Possible values are Basic, Standard and Premium. + - tags : (Optional) A mapping of tags to assign to the resource. + +Please click the [link](https://www.terraform.io/docs/providers/azurerm/r/container_registry.html) to get additional details on settings in Terraform for Azure Container Registry. + +## Usage + +### Module Definitions + +- Container Registry Module : infra/modules/providers/azure/container-registry + +``` +module "container_registry" { + source = "github.com/Microsoft/cobalt/infra/modules/providers/azure/container-registry" + container_registry_name = "test-container_registry-name" + resource_group_name = ${azurerm_resource_group.container_registry.name} + container_registry_sku = "Basic" | "Standard" | "Premium" + container_registry_admin_enabled = true | false + container_registry_tags = {test:test} +} +``` +## Outputs + +Once the deployments are completed successfully, the output for the current module will be in the format mentioned below: + +```hcl +Outputs: + +container_registry_id = +container_registry_login_server = +container_registry_admin_username = +``` + + +## License +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/infra/modules/providers/azure/container-registry/main.tf b/infra/modules/providers/azure/container-registry/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..0e34df50ad6f8b4cfb69a065b8c5ae2766283269 --- /dev/null +++ b/infra/modules/providers/azure/container-registry/main.tf @@ -0,0 +1,71 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +data "azurerm_resource_group" "container_registry" { + name = var.resource_group_name +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_container_registry" "container_registry" { + name = var.container_registry_name + resource_group_name = data.azurerm_resource_group.container_registry.name + location = data.azurerm_resource_group.container_registry.location + sku = var.container_registry_sku + admin_enabled = var.container_registry_admin_enabled + tags = var.resource_tags + + # This dynamic block configures a default DENY action to all incoming traffic + # in the case that one of the following hold true: + # 1: IP whitelist has been configured + # 2: Subnet whitelist has been configured + dynamic "network_rule_set" { + for_each = length(concat(var.resource_ip_whitelist, var.subnet_id_whitelist)) == 0 ? [] : [var.resource_ip_whitelist] + content { + default_action = "Deny" + # This dynamic block configures "Allow" action to all of the whitelisted IPs. It is only + # stamped out in the case that there are IPs configured for whitelist + dynamic "ip_rule" { + for_each = var.resource_ip_whitelist + content { + action = "Allow" + ip_range = ip_rule.value + } + } + } + } +} + +# Configures access from the subnets that should have access +resource "null_resource" "acr_acr_subnet_access_rule" { + count = length(var.subnet_id_whitelist) + triggers = { + acr_id = azurerm_container_registry.container_registry.id + subnets = join(",", var.subnet_id_whitelist) + } + provisioner "local-exec" { + command = < v.id } + description = "A mapping of secret names and URIs." +} + +output "references" { + value = { + for k, v in azurerm_key_vault_secret.secret : + v.name => format("@Microsoft.KeyVault(SecretUri=%s)", v.id) + } + description = "A mapping of Key Vault references for App Service and Azure Functions." +} \ No newline at end of file diff --git a/infra/modules/providers/azure/keyvault-secret/testing/main.tf b/infra/modules/providers/azure/keyvault-secret/testing/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..2ec2b7aea2d50c2b169a5bc8ef1f80b3e292854c --- /dev/null +++ b/infra/modules/providers/azure/keyvault-secret/testing/main.tf @@ -0,0 +1,39 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +provider "azurerm" { + features {} +} + +module "resource_group" { + source = "../../resource-group" + + name = "osdu-module" + location = "eastus2" +} + +module "keyvault" { + source = "../../keyvault" + + resource_group_name = module.resource_group.name +} + +module "keyvault-secret" { + source = "../" + + keyvault_id = module.keyvault.keyvault_id + secrets = { + test = "test" + } +} \ No newline at end of file diff --git a/infra/modules/providers/azure/keyvault-secret/testing/unit_test.go b/infra/modules/providers/azure/keyvault-secret/testing/unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b1b08f29113eeac7aadd40fce5c14fd4dc9e9749 --- /dev/null +++ b/infra/modules/providers/azure/keyvault-secret/testing/unit_test.go @@ -0,0 +1,62 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var workspace = "osdu-services-" + strings.ToLower(random.UniqueId()) +var location = "eastus" +var count = 7 + +var tfOptions = &terraform.Options{ + TerraformDir: "./", + Upgrade: true, +} + +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} + +func TestTemplate(t *testing.T) { + + expectedResult := asMap(t, `{ + "name" : "test" + }`) + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tfOptions, + Workspace: workspace, + PlanAssertions: nil, + ExpectedResourceCount: count, + ExpectedResourceAttributeValues: infratests.ResourceDescription{ + "module.keyvault-secret.azurerm_key_vault_secret.secret[0]": expectedResult, + }, + } + + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/modules/providers/azure/keyvault-secret/variables.tf b/infra/modules/providers/azure/keyvault-secret/variables.tf new file mode 100644 index 0000000000000000000000000000000000000000..c04d9178297fe4808c490756ef30434121f98512 --- /dev/null +++ b/infra/modules/providers/azure/keyvault-secret/variables.tf @@ -0,0 +1,23 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +variable "keyvault_id" { + type = string + description = "Default resource group name that the network will be created in." +} + +variable "secrets" { + description = "Key/value pair of keyvault secret names and corresponding secret value." + type = map(string) +} diff --git a/infra/modules/providers/azure/keyvault-secret/versions.tf b/infra/modules/providers/azure/keyvault-secret/versions.tf new file mode 100644 index 0000000000000000000000000000000000000000..de64496c65ffb9beea697445862f04e4d1c00f2c --- /dev/null +++ b/infra/modules/providers/azure/keyvault-secret/versions.tf @@ -0,0 +1,18 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +terraform { + required_version = ">= 0.12" +} diff --git a/infra/modules/providers/azure/keyvault/README.md b/infra/modules/providers/azure/keyvault/README.md new file mode 100755 index 0000000000000000000000000000000000000000..c97bba33a123281d45b124a77279c1cdbb54deba --- /dev/null +++ b/infra/modules/providers/azure/keyvault/README.md @@ -0,0 +1,50 @@ +# keyvault + +A terraform module to provide key vaults in Azure with the following characteristics: + +- Generates or updates a target key vault resource in azure: `keyvault_name`. +- The key vault is created in a specified resource group: `resource_group_name`. +- An access policy is created in the vault based on the deployment's service principal and tenant: environment variables `ARM_TENANT_ID` `ARM_CLIENT_SECRET` `ARM_CLIENT_ID`. +- Key Vault SKU is configurable: `keyvault_sku`. Defaults to `standard`. +- Access policy permissions for the deployment's service principal are configurable: `keyvault_key_permissions`, `keyvault_secret_permissions` and `keyvault_certificate_permissions`. +- Specified resource tags are updated to the targeted vault: `resource_tags`. + +## Usage + +Key Vault usage example: + +```hcl + +module "keyvault" { + source = "../../modules/providers/azure/keyvault" + keyvault_name = "${local.kv_name}" + resource_group_name = "${azurerm_resource_group.svcplan.name}" +} +``` + +## Attributes Reference + +The following attributes are exported: + +- `keyvault_id`: The id of the Keyvault. +- `keyvault_uri`: The uri of the keyvault. +- `keyvault_name`: The name of the Keyvault. + +## Argument Reference + +Supported arguments for this module are available in [variables.tf](./variables.tf). + +## License +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/infra/modules/providers/azure/keyvault/main.tf b/infra/modules/providers/azure/keyvault/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..7ecafb4465b89e9b337d991333018e66529f4f2a --- /dev/null +++ b/infra/modules/providers/azure/keyvault/main.tf @@ -0,0 +1,69 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +data "azurerm_resource_group" "kv" { + name = var.resource_group_name +} + +data "azurerm_client_config" "current" { +} + +# Note: Any access policies needed for the keyvault should be created using +# the `keyvault-policy` module. More information on why can be found here: +# https://www.terraform.io/docs/providers/azurerm/r/key_vault.html#access_policy +resource "azurerm_key_vault" "keyvault" { + name = var.keyvault_name + location = data.azurerm_resource_group.kv.location + resource_group_name = data.azurerm_resource_group.kv.name + tenant_id = data.azurerm_client_config.current.tenant_id + + soft_delete_enabled = true + soft_delete_retention_days = 90 + purge_protection_enabled = false + + sku_name = var.keyvault_sku + + # This block configures VNET integration if a subnet whitelist is specified + dynamic "network_acls" { + # this block allows the loop to run 1 or 0 times based on if the resource ip whitelist or subnet id whitelist is provided. + for_each = length(concat(var.resource_ip_whitelist, var.subnet_id_whitelist)) == 0 ? [] : [""] + content { + bypass = "None" + default_action = "Deny" + virtual_network_subnet_ids = var.subnet_id_whitelist + ip_rules = var.resource_ip_whitelist + } + } + + tags = var.resource_tags +} + +resource "azurerm_key_vault_secret" "keyvault" { + for_each = var.secrets + name = each.key + value = each.value + key_vault_id = azurerm_key_vault.keyvault.id + + depends_on = [module.deployment_service_principal_keyvault_access_policies] +} + +module "deployment_service_principal_keyvault_access_policies" { + source = "../keyvault-policy" + vault_id = azurerm_key_vault.keyvault.id + tenant_id = data.azurerm_client_config.current.tenant_id + object_ids = [data.azurerm_client_config.current.object_id] + key_permissions = var.keyvault_key_permissions + secret_permissions = var.keyvault_secret_permissions + certificate_permissions = var.keyvault_certificate_permissions +} diff --git a/infra/modules/providers/azure/keyvault/output.tf b/infra/modules/providers/azure/keyvault/output.tf new file mode 100644 index 0000000000000000000000000000000000000000..8472906893bdebadfaab392dcef76b3bf81691dc --- /dev/null +++ b/infra/modules/providers/azure/keyvault/output.tf @@ -0,0 +1,47 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +# The dependency of these values on the keyvault access policy is required in +# order to create an explicit dependency between the access policy that +# allows the service principal executing the deployment and the keyvault +# ID. This ensures that the access policy is always configured prior to +# managing entitites within the keyvault. +# +# More documentation on this stanza can be found here: +# https://www.terraform.io/docs/configuration/outputs.html#depends_on-explicit-output-dependencies + +output "keyvault_id" { + description = "The id of the Keyvault" + value = azurerm_key_vault.keyvault.id + depends_on = [ + module.deployment_service_principal_keyvault_access_policies + ] +} + +output "keyvault_uri" { + description = "The uri of the keyvault" + value = azurerm_key_vault.keyvault.vault_uri + depends_on = [ + module.deployment_service_principal_keyvault_access_policies + ] +} + +output "keyvault_name" { + description = "The name of the Keyvault" + value = azurerm_key_vault.keyvault.name + depends_on = [ + module.deployment_service_principal_keyvault_access_policies + ] +} + diff --git a/infra/modules/providers/azure/keyvault/testing/main.tf b/infra/modules/providers/azure/keyvault/testing/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..a5d6d42376623f19d3d10863769db0271ad0c271 --- /dev/null +++ b/infra/modules/providers/azure/keyvault/testing/main.tf @@ -0,0 +1,33 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +provider "azurerm" { + features {} +} + +module "resource_group" { + source = "../../resource-group" + + name = "osdu-module" + location = "eastus2" +} + +module "keyvault" { + source = "../" + + resource_group_name = module.resource_group.name + resource_tags = { + osdu = "module" + } +} \ No newline at end of file diff --git a/infra/modules/providers/azure/keyvault/testing/unit_test.go b/infra/modules/providers/azure/keyvault/testing/unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c2058d31975db729bc557c17604deb366400d2d0 --- /dev/null +++ b/infra/modules/providers/azure/keyvault/testing/unit_test.go @@ -0,0 +1,66 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "encoding/json" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var name = "keyvault-" +var location = "eastus" +var count = 5 + +var tfOptions = &terraform.Options{ + TerraformDir: "./", + Upgrade: true, +} + +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} + +func TestTemplate(t *testing.T) { + + expectedKeyVault := asMap(t, `{ + "name" : "spkeyvault", + "resource_group_name" : "osdu-module", + "sku_name" : "standard", + "tags" : { + "osdu" : "module" + } + }`) + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tfOptions, + Workspace: name + random.UniqueId(), + PlanAssertions: nil, + ExpectedResourceCount: count, + ExpectedResourceAttributeValues: infratests.ResourceDescription{ + "module.keyvault.azurerm_key_vault.keyvault": expectedKeyVault, + }, + } + + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/modules/providers/azure/keyvault/variables.tf b/infra/modules/providers/azure/keyvault/variables.tf new file mode 100644 index 0000000000000000000000000000000000000000..fbf3be42012b837f3692befd32ee017106f05ac0 --- /dev/null +++ b/infra/modules/providers/azure/keyvault/variables.tf @@ -0,0 +1,72 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +variable "keyvault_name" { + description = "Name of the keyvault to create" + type = string + default = "spkeyvault" +} + +variable "keyvault_sku" { + description = "SKU of the keyvault to create" + type = string + default = "standard" +} + +variable "resource_group_name" { + type = string + description = "Default resource group name that the network will be created in." +} + +variable "keyvault_key_permissions" { + description = "Permissions that the service principal has for accessing keys from KeyVault" + type = list(string) + default = ["create", "delete", "get"] +} + +variable "keyvault_secret_permissions" { + description = "Permissions that the service principal has for accessing secrets from KeyVault" + type = list(string) + default = ["set", "delete", "get", "list"] +} + +variable "keyvault_certificate_permissions" { + description = "Permissions that the service principal has for accessing certificates from KeyVault" + type = list(string) + default = ["create", "delete", "get", "list"] +} + +variable "resource_tags" { + description = "Map of tags to apply to taggable resources in this module. By default the taggable resources are tagged with the name defined above and this map is merged in" + type = map(string) + default = {} +} + +variable "subnet_id_whitelist" { + description = "If supplied this represents the subnet IDs that should be allowed to access this resource" + type = list(string) + default = [] +} + +variable "resource_ip_whitelist" { + description = "A list of IPs and/or IP ranges that should have access to the provisioned keyvault" + type = list(string) + default = [] +} + +variable "secrets" { + type = map(string) + description = "A map of secrets for the Key Vault." + default = {} +} diff --git a/infra/modules/providers/azure/keyvault/versions.tf b/infra/modules/providers/azure/keyvault/versions.tf new file mode 100755 index 0000000000000000000000000000000000000000..de64496c65ffb9beea697445862f04e4d1c00f2c --- /dev/null +++ b/infra/modules/providers/azure/keyvault/versions.tf @@ -0,0 +1,18 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +terraform { + required_version = ">= 0.12" +} diff --git a/infra/modules/providers/azure/log-analytics/README.md b/infra/modules/providers/azure/log-analytics/README.md new file mode 100644 index 0000000000000000000000000000000000000000..4bafb18417f5e76b649452398656f6c3dfbad338 --- /dev/null +++ b/infra/modules/providers/azure/log-analytics/README.md @@ -0,0 +1,58 @@ +# Module Azure Log Analytics + +Module for creating and managing a Log Analytics Workspace. + +## Usage + +``` +module "resource_group" { + source = "../../resource-group" + + name = "osdu-module" + location = "eastus2" +} + +module "log_analytics" { + source = "../" + + name = "osdu-module-logs-${module.resource_group.random}" + resource_group_name = module.resource_group.name + + solutions = [ + { + solution_name = "ContainerInsights", + publisher = "Microsoft", + product = "OMSGallery/ContainerInsights", + } + ] + + # Tags + resource_tags = { + osdu = "module" + } +} +``` + +### Input Variables + +Please refer to [variables.tf](./variables.tf). + +### Output Variables + +Please refer to [output.tf](./output.tf). + + +## License +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/infra/modules/providers/azure/log-analytics/main.tf b/infra/modules/providers/azure/log-analytics/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..4164e91fa56ef6bf9e80e1a6940ac3cda172a5d6 --- /dev/null +++ b/infra/modules/providers/azure/log-analytics/main.tf @@ -0,0 +1,49 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +data "azurerm_resource_group" "main" { + name = var.resource_group_name +} + +resource "azurerm_log_analytics_workspace" "main" { + name = var.name + resource_group_name = data.azurerm_resource_group.main.name + location = data.azurerm_resource_group.main.location + sku = var.sku + retention_in_days = var.retention_in_days + + tags = var.resource_tags +} + +resource "azurerm_security_center_workspace" "main" { + count = length(var.security_center_subscription) + + scope = "/subscriptions/${element(var.security_center_subscription, count.index)}" + workspace_id = azurerm_log_analytics_workspace.main.id +} + +resource "azurerm_log_analytics_solution" "main" { + count = length(var.solutions) + + solution_name = var.solutions[count.index].solution_name + resource_group_name = data.azurerm_resource_group.main.name + location = data.azurerm_resource_group.main.location + workspace_resource_id = azurerm_log_analytics_workspace.main.id + workspace_name = azurerm_log_analytics_workspace.main.name + + plan { + publisher = var.solutions[count.index].publisher + product = var.solutions[count.index].product + } +} \ No newline at end of file diff --git a/infra/modules/providers/azure/log-analytics/outputs.tf b/infra/modules/providers/azure/log-analytics/outputs.tf new file mode 100644 index 0000000000000000000000000000000000000000..b80df36a5723887a220ffff55f2475b44d2e88ff --- /dev/null +++ b/infra/modules/providers/azure/log-analytics/outputs.tf @@ -0,0 +1,31 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +output "id" { + description = "The Log Analytics Workspace Id" + value = azurerm_log_analytics_workspace.main.id +} + +output "name" { + description = "The Log Analytics Workspace Name" + value = azurerm_log_analytics_workspace.main.name +} + +output "log_workspace_id" { + value = azurerm_log_analytics_workspace.main.workspace_id +} + +output "log_workspace_key" { + value = azurerm_log_analytics_workspace.main.primary_shared_key +} \ No newline at end of file diff --git a/infra/modules/providers/azure/log-analytics/testing/main.tf b/infra/modules/providers/azure/log-analytics/testing/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..398cf912bd2bb6c701ebbdabdaa0665e8fa64dae --- /dev/null +++ b/infra/modules/providers/azure/log-analytics/testing/main.tf @@ -0,0 +1,44 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +provider "azurerm" { + features {} +} + +module "resource_group" { + source = "../../resource-group" + + name = "osdu-module" + location = "eastus2" +} + +module "log_analytics" { + source = "../" + + name = "osdu-module-logs-${module.resource_group.random}" + resource_group_name = module.resource_group.name + + solutions = [ + { + solution_name = "ContainerInsights", + publisher = "Microsoft", + product = "OMSGallery/ContainerInsights", + } + ] + + # Tags + resource_tags = { + osdu = "module" + } +} \ No newline at end of file diff --git a/infra/modules/providers/azure/log-analytics/testing/unit_test.go b/infra/modules/providers/azure/log-analytics/testing/unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..06b09a3a71f40827a4a3425511c2e447850b0f32 --- /dev/null +++ b/infra/modules/providers/azure/log-analytics/testing/unit_test.go @@ -0,0 +1,61 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "encoding/json" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var name = "logs-" +var location = "eastus" +var count = 5 + +var tfOptions = &terraform.Options{ + TerraformDir: "./", + Upgrade: true, +} + +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} + +func TestTemplate(t *testing.T) { + + expectedResult := asMap(t, `{ + "retention_in_days": 30 + }`) + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tfOptions, + Workspace: name + random.UniqueId(), + PlanAssertions: nil, + ExpectedResourceCount: count, + ExpectedResourceAttributeValues: infratests.ResourceDescription{ + "module.log_analytics.azurerm_log_analytics_workspace.main": expectedResult, + }, + } + + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/modules/providers/azure/log-analytics/variables.tf b/infra/modules/providers/azure/log-analytics/variables.tf new file mode 100644 index 0000000000000000000000000000000000000000..b3d641f0070ab1207fec735acb4c96c4d707f903 --- /dev/null +++ b/infra/modules/providers/azure/log-analytics/variables.tf @@ -0,0 +1,56 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +############################################################## +# This module allows the creation of a Virtual Network +############################################################## + +variable "name" { + description = "Name of Log Analystics Workspace." + type = string +} + +variable "resource_group_name" { + description = "The name of the resource group the resource will be created in" + type = string +} + +variable "resource_tags" { + description = "Map of tags to apply to taggable resources in this module. By default the taggable resources are tagged with the name defined above and this map is merged in" + type = map(string) + default = {} +} + +variable "sku" { + description = "Sku of the Log Analytics Workspace." + type = string + default = "PerGB2018" +} + +variable "retention_in_days" { + description = "The workspace data retention in days. Between 30 and 730." + default = 30 +} + +variable "security_center_subscription" { + description = "List of subscriptions this log analytics should collect data for." + type = list(string) + default = [] +} + +variable "solutions" { + description = "A list of solutions to add to the workspace." + type = list(object({ solution_name = string, publisher = string, product = string })) + default = [] +} \ No newline at end of file diff --git a/infra/modules/providers/azure/network/README.md b/infra/modules/providers/azure/network/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f9b3e5a6d65dfb9eb0ffd3435d62dbfa1f0c6d2d --- /dev/null +++ b/infra/modules/providers/azure/network/README.md @@ -0,0 +1,72 @@ +# Module network + +A terraform module that provisions networks with the following characteristics: + +- Vnet and Subnets with DNS Prefix + + +## Usage + +``` +module "resource_group" { + source = "github.com/azure/osdu-infrastructure/modules/resource-group" + + name = "osdu-module" + location = "eastus2" +} + + +module "network" { + source = "github.com/azure/osdu-infrastructure/modules/network" + + name = "osdu-module-vnet-${module.resource_group.random}" + resource_group_name = module.resource_group.name + address_space = "10.0.1.0/24" + dns_servers = ["8.8.8.8"] + subnet_prefixes = ["10.0.1.0/26", "10.0.1.64/26", "10.0.1.128/26", "10.0.1.192/27", "10.0.1.224/28"] + subnet_names = ["Web-Tier", "App-Tier", "Data-Tier", "Mgmt-Tier", "GatewaySubnet"] + + # Tags + resource_tags = { + osdu = "module" + } +} +``` + +## Inputs + +| Variable Name | Type | Description | +| --------------------------------- | ---------- | ------------------------------------ | +| `name` | _string_ | The name of the web app service. | +| `resource_group_name` | _string_ | The name of an existing resource group. | +| `resource_tags` | _list_ | Map of tags to apply to taggable resources in this module. | +| `address_space` | _string_ | The address space that is used by the virtual network. Default: `10.0.0.0/16` | +| `dns_servers` | _list_ | The DNS servers to be used with vNet. | +| `subnet_prefixes` | _list_ | The address prefix to use for the subnet. Default: `["10.0.1.0/24"]` +| `subnet_names` | _list_ | A list of public subnets inside the vNet. Default: `["subnet1"]` + + +## Outputs + +Once the deployments are completed successfully, the output for the current module will be in the format mentioned below: + +- `id`: The virtual network Id. +- `name`: The Application Insights Instrumentation Key. +- `address_space`: The address space of the virtual network. +- `subnets`: The ids of subnets created inside the virtual network. + + +## License +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/infra/modules/providers/azure/network/main.tf b/infra/modules/providers/azure/network/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..959913db60a627ba85f3098ab8d9fa671066876c --- /dev/null +++ b/infra/modules/providers/azure/network/main.tf @@ -0,0 +1,37 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +data "azurerm_resource_group" "main" { + name = var.resource_group_name +} + +resource "azurerm_virtual_network" "main" { + name = var.name + resource_group_name = data.azurerm_resource_group.main.name + location = data.azurerm_resource_group.main.location + address_space = [var.address_space] + dns_servers = var.dns_servers + tags = var.resource_tags +} + +resource "azurerm_subnet" "main" { + count = length(var.subnet_names) + + name = var.subnet_names[count.index] + virtual_network_name = azurerm_virtual_network.main.name + resource_group_name = data.azurerm_resource_group.main.name + address_prefixes = [var.subnet_prefixes[count.index]] + service_endpoints = lookup(var.subnet_service_endpoints, var.subnet_names[count.index], null) +} + diff --git a/infra/modules/providers/azure/network/output.tf b/infra/modules/providers/azure/network/output.tf new file mode 100644 index 0000000000000000000000000000000000000000..6b718efd61402ecb9a5bc5a95bf300ec362714a6 --- /dev/null +++ b/infra/modules/providers/azure/network/output.tf @@ -0,0 +1,33 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +output "id" { + description = "The virtual network Id" + value = azurerm_virtual_network.main.id +} + +output "name" { + description = "The virtual network name" + value = azurerm_virtual_network.main.name +} + +output "address_space" { + description = "The address space of the virtual network." + value = azurerm_virtual_network.main.address_space +} + +output "subnets" { + description = "The ids of subnets created inside the virtual network." + value = azurerm_subnet.main.*.id +} \ No newline at end of file diff --git a/infra/modules/providers/azure/network/testing/main.tf b/infra/modules/providers/azure/network/testing/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..3b305fc42c9b29f582ccb51ff8f64de25e1a0aa2 --- /dev/null +++ b/infra/modules/providers/azure/network/testing/main.tf @@ -0,0 +1,42 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +provider "azurerm" { + features {} +} + +module "resource_group" { + source = "../../resource-group" + + name = "osdu-module" + location = "eastus2" +} + + +module "network" { + source = "../" + + name = "osdu-module-vnet-${module.resource_group.random}" + resource_group_name = module.resource_group.name + address_space = "10.0.1.0/24" + dns_servers = ["8.8.8.8"] + subnet_prefixes = ["10.0.1.0/26", "10.0.1.64/26", "10.0.1.128/26", "10.0.1.192/27", "10.0.1.224/28"] + subnet_names = ["Web-Tier", "App-Tier", "Data-Tier", "Mgmt-Tier", "GatewaySubnet"] + + # Tags + resource_tags = { + osdu = "module" + } + +} \ No newline at end of file diff --git a/infra/modules/providers/azure/network/testing/unit_test.go b/infra/modules/providers/azure/network/testing/unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7926a5f16cab88b568140297d10af329d5780529 --- /dev/null +++ b/infra/modules/providers/azure/network/testing/unit_test.go @@ -0,0 +1,99 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "encoding/json" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var name = "network-" +var location = "eastus" +var count = 9 + +var tfOptions = &terraform.Options{ + TerraformDir: "./", + Upgrade: true, +} + +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} + +func TestTemplate(t *testing.T) { + + expectedResult := asMap(t, `{ + "address_space": ["10.0.1.0/24"], + "dns_servers": ["8.8.8.8"] + }`) + + expectedSubnet0 := asMap(t, `{ + "name": "Web-Tier", + "address_prefixes": [ + "10.0.1.0/26" + ] + }`) + + expectedSubnet1 := asMap(t, `{ + "address_prefixes": [ + "10.0.1.64/26" + ] + }`) + + expectedSubnet2 := asMap(t, `{ + "address_prefixes": [ + "10.0.1.128/26" + ] + }`) + + expectedSubnet3 := asMap(t, `{ + "address_prefixes": [ + "10.0.1.192/27" + ] + }`) + + expectedSubnet4 := asMap(t, `{ + "name": "GatewaySubnet", + "address_prefixes": [ + "10.0.1.224/28" + ] + }`) + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tfOptions, + Workspace: name + random.UniqueId(), + PlanAssertions: nil, + ExpectedResourceCount: count, + ExpectedResourceAttributeValues: infratests.ResourceDescription{ + "module.network.azurerm_virtual_network.main": expectedResult, + "module.network.azurerm_subnet.main[0]": expectedSubnet0, + "module.network.azurerm_subnet.main[1]": expectedSubnet1, + "module.network.azurerm_subnet.main[2]": expectedSubnet2, + "module.network.azurerm_subnet.main[3]": expectedSubnet3, + "module.network.azurerm_subnet.main[4]": expectedSubnet4, + }, + } + + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/modules/providers/azure/network/variables.tf b/infra/modules/providers/azure/network/variables.tf new file mode 100644 index 0000000000000000000000000000000000000000..92365af90179866058970ba72ec1816ef55c267c --- /dev/null +++ b/infra/modules/providers/azure/network/variables.tf @@ -0,0 +1,60 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +############################################################## +# This module allows the creation of a Virtual Network +############################################################## + +variable "name" { + description = "Name of the vnet to create" + default = "acctvnet" +} + +variable "resource_group_name" { + description = "Default resource group name that the network will be created in." + default = "myapp-rg" +} + +variable "resource_tags" { + description = "Map of tags to apply to taggable resources in this module. By default the taggable resources are tagged with the name defined above and this map is merged in" + type = map(string) + default = {} +} + +variable "address_space" { + description = "The address space that is used by the virtual network." + default = "10.0.0.0/16" +} + +# If no values specified, this defaults to Azure DNS +variable "dns_servers" { + description = "The DNS servers to be used with vNet." + default = [] +} + +variable "subnet_prefixes" { + description = "The address prefix to use for the subnet." + default = ["10.0.1.0/24"] +} + +variable "subnet_names" { + description = "A list of public subnets inside the vNet." + default = ["subnet1"] +} + +variable "subnet_service_endpoints" { + description = "A map of subnet name to service endpoints to add to the subnet." + type = map(any) + default = {} +} \ No newline at end of file diff --git a/infra/modules/providers/azure/postgreSQL/README.md b/infra/modules/providers/azure/postgreSQL/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9ef1afcd1801b7a4a8fb5d98149efac2bb04f6cf --- /dev/null +++ b/infra/modules/providers/azure/postgreSQL/README.md @@ -0,0 +1,112 @@ +# Module network + +A terraform module that provisions PostgreSQL databases with the following characteristics: + +## Usage + +``` +module "resource_group" { + source = "../../resource-group" + + name = "osdu-module" + location = "eastus2" +} + +module "network" { + source = "../../network" + + name = "osdu-module-vnet-${module.resource_group.random}" + resource_group_name = module.resource_group.name + address_space = "10.0.1.0/24" + dns_servers = ["8.8.8.8"] + subnet_prefixes = ["10.0.1.0/26"] + subnet_names = ["Web-Tier"] + + # Tags + resource_tags = { + osdu = "module" + } + +} + +module "postgreSQL" { + source = "../" + + resource_group_name = module.resource_group.name + name = "osdu-module-db-${module.resource_group.random}" + databases = [ "osdu-module-database" ] + admin_user = "test" + admin_password = "AzurePassword@123" + + # Tags + resource_tags = { + osdu = "module" + } + + firewall_rules = [{ + start_ip = "10.0.0.2" + end_ip = "10.0.0.8" + }] + + vnet_rules = [{ + subnet_id = module.network.subnets[0] + }] + + postgresql_configurations = { + config = "test" + } +} +``` + +## Inputs + +| Variable Name | Type | Description | +| --------------------------------- | ---------- | ------------------------------------ | +| `name` | _string_ | The name of the postgreSQL db. | +| `resource_group_name` | _string_ | The name of an existing resource group. | +| `resource_tags` | _list_ | Map of tags to apply to taggable resources in this module. | +| `databases` | _list_ | The list of names of the PostgreSQL Database, which needs to be a valid PostgreSQL identifier. Changing this forces a new resource to be created. | +| `admin_user` | _string_ | The Administrator Login for the PostgreSQL Server. Changing this forces a new resource to be created. | +| `admin_password` | _string_ | The Password associated with the administrator_login for the PostgreSQL Server. | +| `sku` | _string_ | Name of the server's SKU. Default: "GP_Gen5_4". | +| `storage_mb` | _int_ | Max storage allowed for a server. Possible values are between 5120 MB(5GB) and 1048576 MB(1TB) for the Basic SKU and between 5120 MB(5GB) and 4194304 MB(4TB). Default: 5120. | +| `server_version` | _string_ | Specifies the version of PostgreSQL to use. Valid values are 9.5, 9.6, and 10.0. Changing this forces a new resource to be created. Default: "10.0". | +| `backup_retention_days` | _int_ | Number of days to retain backup data. Default: 7. | +| `geo_redundant_backup_enabled` | _bool_ | Enable geo-redundancy. Default: true. | +| `auto_grow_enabled` | _bool_ | Enable auto grow. Default: true. | +| `ssl_enforcement_enabled` | _bool_ | Enable SSL enforcement. Default: true. | +| `public_network_access` | _bool_ | Enable or Disable public network access to the VM. Default: true. | +| `db_charset` | _string_ | Specifies the Charset for the PostgreSQL Database, which needs to be a valid PostgreSQL Charset. Changing this forces a new resource to be created. Default: "UTF8". | +| `db_collation` | _string_ | Specifies the Collation for the PostgreSQL Database, which needs to be a valid PostgreSQL Collation. Note that Microsoft uses different notation - en-US instead of en_US. Changing this forces a new resource to be created. Default: "English_United States.1252". | +| `firewall_rule_prefix` | _string_ | Specifies prefix for firewall rule names. Default: "firewall-" +| `firewall_rules` | _list_ | The list of maps, describing firewall rules. Valid map items: name, start_ip, end_ip. Default: [] | +| `vnet_rule_name_prefix` | _string_ | Specifies prefix for vnet rule names. Default: "postgresql-vnet-rule-". | +| `vnet_rules` | _list_ | The list of maps, describing vnet rules. Valud map items: name, subnet_id. Default: [] +| `postgresql_configurations` | _map(_string_)_ | A map with PostgreSQL configurations to enable. Default: {} | + + +## Outputs + +Once the deployments are completed successfully, the output for the current module will be in the format mentioned below: + +- `db_names` : List of db names. +- `db_ids`: List of db ids. +- `server_name`: The server name. +- `server_id`: The server id. +- `server_fqdn`: The server FQDN. + + +## License +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/infra/modules/providers/azure/postgreSQL/main.tf b/infra/modules/providers/azure/postgreSQL/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..0985f82fe237393ed495492c30dc0d65fc312e4e --- /dev/null +++ b/infra/modules/providers/azure/postgreSQL/main.tf @@ -0,0 +1,85 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +data "azurerm_resource_group" "main" { + name = var.resource_group_name +} + +/* data "azurerm_virtual_network" "main" { + name = var.virtual_network_name + resource_group_name = data.azurerm_resource_group.main.name +} + +data "azurerm_subnet" "main" { + name = var.subnet_name + resource_group_name = data.azurerm_resource_group.main.name + virtual_network_name = data.azurerm_virtual_network.main.name +} */ + +resource "azurerm_postgresql_server" "main" { + name = var.name + location = data.azurerm_resource_group.main.location + resource_group_name = data.azurerm_resource_group.main.name + tags = var.resource_tags + + administrator_login = var.admin_user + administrator_login_password = var.admin_password + + sku_name = var.sku + storage_mb = var.storage_mb + backup_retention_days = var.backup_retention_days + geo_redundant_backup_enabled = var.geo_redundant_backup_enabled + auto_grow_enabled = var.auto_grow_enabled + version = var.server_version + ssl_enforcement_enabled = var.ssl_enforcement_enabled + + public_network_access_enabled = var.public_network_access +} + +resource "azurerm_postgresql_database" "main" { + depends_on = [azurerm_postgresql_server.main] + count = length(var.databases) + name = var.databases[count.index] + server_name = azurerm_postgresql_server.main.name + resource_group_name = data.azurerm_resource_group.main.name + charset = var.db_charset + collation = var.db_collation +} + +resource "azurerm_postgresql_firewall_rule" "main" { + count = length(var.firewall_rules) + name = format("%s%s", var.firewall_rule_prefix, lookup(var.firewall_rules[count.index], "name", count.index)) + resource_group_name = var.resource_group_name + server_name = azurerm_postgresql_server.main.name + start_ip_address = var.firewall_rules[count.index]["start_ip"] + end_ip_address = var.firewall_rules[count.index]["end_ip"] +} + +resource "azurerm_postgresql_virtual_network_rule" "main" { + count = length(var.vnet_rules) + name = format("%s%s", var.vnet_rule_name_prefix, lookup(var.vnet_rules[count.index], "name", count.index)) + resource_group_name = var.resource_group_name + server_name = azurerm_postgresql_server.main.name + subnet_id = var.vnet_rules[count.index]["subnet_id"] +} + +resource "azurerm_postgresql_configuration" "main" { + count = length(keys(var.postgresql_configurations)) + resource_group_name = var.resource_group_name + server_name = azurerm_postgresql_server.main.name + + name = element(keys(var.postgresql_configurations), count.index) + value = element(values(var.postgresql_configurations), count.index) +} \ No newline at end of file diff --git a/infra/modules/providers/azure/postgreSQL/output.tf b/infra/modules/providers/azure/postgreSQL/output.tf new file mode 100644 index 0000000000000000000000000000000000000000..44d000f866724c4202c37a822bcbb912f8fc1abd --- /dev/null +++ b/infra/modules/providers/azure/postgreSQL/output.tf @@ -0,0 +1,38 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +output "db_names" { + description = "The db names as an ordered list" + value = azurerm_postgresql_database.main.*.name +} + +output "server_name" { + description = "The server name" + value = azurerm_postgresql_server.main.name +} + +output "db_ids" { + description = "The db ids as an ordered list" + value = azurerm_postgresql_database.main.*.id +} + +output "server_id" { + description = "The server id." + value = azurerm_postgresql_server.main.id +} + +output "server_fqdn" { + description = "The server FQDN" + value = azurerm_postgresql_server.main.fqdn +} \ No newline at end of file diff --git a/infra/modules/providers/azure/postgreSQL/testing/main.tf b/infra/modules/providers/azure/postgreSQL/testing/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..fe607e54b4f639ca468465d848a6dd9d3036486f --- /dev/null +++ b/infra/modules/providers/azure/postgreSQL/testing/main.tf @@ -0,0 +1,69 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +provider "azurerm" { + features {} +} + +module "resource_group" { + source = "../../resource-group" + + name = "osdu-module" + location = "eastus2" +} + +module "network" { + source = "../../network" + + name = "osdu-module-vnet-${module.resource_group.random}" + resource_group_name = module.resource_group.name + address_space = "10.0.1.0/24" + dns_servers = ["8.8.8.8"] + subnet_prefixes = ["10.0.1.0/26"] + subnet_names = ["Web-Tier"] + + # Tags + resource_tags = { + osdu = "module" + } + +} + +module "postgreSQL" { + source = "../" + + resource_group_name = module.resource_group.name + name = "osdu-module-db-${module.resource_group.random}" + databases = ["osdu-module-database"] + admin_user = "test" + admin_password = "AzurePassword@123" + + # Tags + resource_tags = { + osdu = "module" + } + + firewall_rules = [{ + start_ip = "10.0.0.2" + end_ip = "10.0.0.8" + }] + + vnet_rules = [{ + subnet_id = module.network.subnets[0] + }] + + postgresql_configurations = { + config = "test" + } +} \ No newline at end of file diff --git a/infra/modules/providers/azure/postgreSQL/testing/unit_test.go b/infra/modules/providers/azure/postgreSQL/testing/unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0c26682e29fef094ec9bdd24e3eac2ab81c6acc7 --- /dev/null +++ b/infra/modules/providers/azure/postgreSQL/testing/unit_test.go @@ -0,0 +1,82 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "encoding/json" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var name = "postgreSQL-" +var location = "eastus2" +var count = 11 + +var tfOptions = &terraform.Options{ + TerraformDir: "./", + Upgrade: true, +} + +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} + +func TestTemplate(t *testing.T) { + + expectedResult := asMap(t, `{ + "administrator_login" : "test", + "sku_name" : "GP_Gen5_4", + "auto_grow_enabled" : true, + "backup_retention_days" : 7, + "geo_redundant_backup_enabled" : true, + "public_network_access_enabled" : false, + "ssl_enforcement_enabled" : true, + "ssl_minimal_tls_version_enforced" : "TLSEnforcementDisabled", + "version" : "10.0", + "storage_mb" : 5120 + }`) + + expectedFirewallRule := asMap(t, `{ + "start_ip_address" : "10.0.0.2", + "end_ip_address" : "10.0.0.8" + }`) + + expectedConfig := asMap(t, `{ + "name" : "config", + "value" : "test" + }`) + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tfOptions, + Workspace: name + random.UniqueId(), + PlanAssertions: nil, + ExpectedResourceCount: count, + ExpectedResourceAttributeValues: infratests.ResourceDescription{ + "module.postgreSQL.azurerm_postgresql_server.main": expectedResult, + "module.postgreSQL.azurerm_postgresql_configuration.main[0]": expectedConfig, + "module.postgreSQL.azurerm_postgresql_firewall_rule.main[0]": expectedFirewallRule, + }, + } + + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/modules/providers/azure/postgreSQL/variables.tf b/infra/modules/providers/azure/postgreSQL/variables.tf new file mode 100644 index 0000000000000000000000000000000000000000..2f79821f052dd282cf776cc3fc10f88064086df0 --- /dev/null +++ b/infra/modules/providers/azure/postgreSQL/variables.tf @@ -0,0 +1,131 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +# Naming Conventions (required) + +variable "name" { + description = "The name of the postgresql db being created" + type = string +} + +variable "resource_group_name" { + description = "The name of the resource group postgreSQL VM will be created in" + type = string +} + +variable "resource_tags" { + description = "Map of tags to apply to taggable resources in this module. By default the taggable resources are tagged with the name defined above and this map is merged in" + type = map(string) + default = {} +} + +variable "databases" { + description = "The list of names of the PostgreSQL Database, which needs to be a valid PostgreSQL identifier. Changing this forces a new resource to be created." + default = [] +} + +variable "admin_user" { + description = "The Administrator Login for the PostgreSQL Server. Changing this forces a new resource to be created." + type = string +} + +variable "admin_password" { + description = "The Password associated with the administrator_login for the PostgreSQL Server." + type = string +} + +# Configuration Variables (Optional) +variable "sku" { + description = "Name of the sku" + type = string + default = "GP_Gen5_4" +} + +variable "storage_mb" { + description = "Max storage allowed for a server. Possible values are between 5120 MB(5GB) and 1048576 MB(1TB) for the Basic SKU and between 5120 MB(5GB) and 4194304 MB(4TB)." + type = number + default = 5120 +} + +variable "server_version" { + description = "Specifies the version of PostgreSQL to use. Valid values are 9.5, 9.6, and 10.0. Changing this forces a new resource to be created." + type = string + default = "10.0" +} + +variable "backup_retention_days" { + description = "Number of days to retain backup data" + type = number + default = 7 +} + +variable "geo_redundant_backup_enabled" { + description = "Enable geo-redundancy" + type = bool + default = true +} + +variable "auto_grow_enabled" { + description = "Enable auto grow" + type = bool + default = true +} + +variable "ssl_enforcement_enabled" { + description = "Enable ssl enforcement" + type = bool + default = true +} + +variable "public_network_access" { + description = "Enable or Disable public network access to the VM" + type = bool + default = false +} + +variable "db_charset" { + description = "Specifies the Charset for the PostgreSQL Database, which needs to be a valid PostgreSQL Charset. Changing this forces a new resource to be created." + default = "UTF8" +} + +variable "db_collation" { + description = "Specifies the Collation for the PostgreSQL Database, which needs to be a valid PostgreSQL Collation. Note that Microsoft uses different notation - en-US instead of en_US. Changing this forces a new resource to be created." + default = "English_United States.1252" +} + +variable "firewall_rule_prefix" { + description = "Specifies prefix for firewall rule names." + default = "firewall-" +} + +variable "firewall_rules" { + description = "The list of maps, describing firewall rules. Valid map items: name, start_ip, end_ip." + default = [] +} + +variable "vnet_rule_name_prefix" { + description = "Specifies prefix for vnet rule names." + default = "postgresql-vnet-rule-" +} + +variable "vnet_rules" { + description = "The list of maps, describing vnet rules. Valud map items: name, subnet_id." + default = [] +} + +variable "postgresql_configurations" { + description = "A map with PostgreSQL configurations to enable." + type = map(string) + default = {} +} \ No newline at end of file diff --git a/infra/modules/providers/azure/postgreSQL/versions.tf b/infra/modules/providers/azure/postgreSQL/versions.tf new file mode 100644 index 0000000000000000000000000000000000000000..b91745b95c79c4a1e00cfc8003a6b544097399d8 --- /dev/null +++ b/infra/modules/providers/azure/postgreSQL/versions.tf @@ -0,0 +1,18 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +terraform { + required_version = ">= 0.12" +} diff --git a/infra/modules/providers/azure/redis-cache/.env.testing.template b/infra/modules/providers/azure/redis-cache/.env.testing.template new file mode 100644 index 0000000000000000000000000000000000000000..5ec86b895661a0b47f0dca48377ed85a6eb1eebe --- /dev/null +++ b/infra/modules/providers/azure/redis-cache/.env.testing.template @@ -0,0 +1,2 @@ +CACHE_NAME="..." +RESOURCE_GROUP_NAME="..." \ No newline at end of file diff --git a/infra/modules/providers/azure/redis-cache/README.md b/infra/modules/providers/azure/redis-cache/README.md new file mode 100644 index 0000000000000000000000000000000000000000..aac2ede24d36c60ec7b0bf9406c60cf3fe420958 --- /dev/null +++ b/infra/modules/providers/azure/redis-cache/README.md @@ -0,0 +1,138 @@ +# Azure Cache for Redis + +The `redis-cache` module simplifies provisioning **[Azure Cache for Redis](https://docs.microsoft.com/en-us/azure/azure-cache-for-redis/cache-overview)** for our Terraform based Cobalt Infrastructure Templates (CITs). It grants CITs the ability to configure **Azure Cache for Redis** for all available pricing tiers offered by Microsoft Azure. + +## What is Azure Redis for Cache? + +From the official [Documentation](hhttps://docs.microsoft.com/en-us/azure/azure-cache-for-redis/cache-overview): + +> "Azure Cache for Redis is based on the popular software Redis. It is typically used as a cache to improve the performance and scalability of systems that rely heavily on backend data-stores. Performance is improved by temporarily copying frequently accessed data to fast storage located close to the application. With Azure Cache for Redis, this fast storage is located in-memory with Azure Cache for Redis instead of being loaded from disk by a database." + +## Current Features + +An instance of the `redis-cache` module deploys the _**Azure Redis for Cache**_ service in order to provide templates with the following: + +- Ability to deploy Azure Redis for Cache using SSL certificates and https within a single resource group. + +- Ability to deploy Azure Redis for Cache premium and standard tier with a single set of configurable `memory_features` properties for managing in-memory server performance. + +- Ability to deploy Azure Redis for Cache premium with or without clustering. + +> **Excluded features:** VNET, Subnet, Keyvault, MI, High-availability zone customization (still in preview), Multi-count module (module that deploys multiple redis instances), Shared storage account, Append Only File (AOF) persistence, and notify keyspace events. + +### Module Usage + +```json +locals { + ws_name = replace(trimspace(lower(terraform.workspace)), "_", "-") + app_rg_Name = "${local.ws_name}-cblt-redis-rg" + region = "eastus" +} + +resource "azurerm_resource_group" "main" { + name = local.app_rg_name + location = local.region +} + +module "redis_cache" { + # Required inputs + source = "../../modules/providers/azure/redis-cache" + name = "cblt-deployment" + resource_group_name = azurerm_resource_group.main.name + + # Basic, Standard and Premimum Tier Inputs (optional) + capacity = 0 + sku_name = "Basic" + ssl_tls_version = "1.2" + + # Standard & Premium Tier Inputs for In Memory Management (optional) + memory_features = { + maxmemory_reserved = 50 + maxmemory_delta = 50 + maxmemory_policy = "volatile-lru" + maxfragmentationmemory_reserved = 50 + } + + # Distinct Premium Tier Inputs (optional) + premium_tier_config = { + server_patch_day = "Friday" + server_patch_hour = 7 + cache_shard_count = 0 + } +} +``` + +> NOTE: This example is configured for the basic tier and is overly verbose for demonstration purposes. All optional inputs above display sensible default values that don't need to be provided here if relying on defaults. + +### Resources + +| Resource | Terraform Link | Description | +|---|---|---| +| `azurerm_redis_cache` | [redis cache](https://www.terraform.io/docs/providers/azurerm/r/redis_cache.html) | This resource will be declared within the module. | + +### Input Variables + +Please refer to [variables.tf](./variables.tf). + +### Output Variables + +Please refer to [output.tf](./output.tf). + +### Automated Tests + +#### Run Integration Tests + +This module's integration tests validate a provisioned Terraform workspace. Follow these steps to setup your local workspace and apply the execution plan in Azure. + +```hcl +cp .env.testing.template .env +## NOTE: You'll need to fill out the values for CACHE_NAME and RESOURCE_GROUP_NAME in .env +export $(cat .env | xargs) + +terraform init +terraform workspace new redis-ws +terraform plan -var name=$CACHE_NAME -var resource_group_name=$RESOURCE_GROUP_NAME +terraform apply + +go test -v $(go list ./... | grep "integration") +``` + +### Configuring Inputs + +Visit the [azure cache for redis best practices](https://docs.microsoft.com/en-us/azure/azure-cache-for-redis/cache-best-practices) page for more guidance on configuring our `redis-cache` module. + +#### Configuring `capacity` + +Please visit _**Azure Cache for Redis's**_ [Cache Pricing Details](https://azure.microsoft.com/en-us/pricing/details/cache/) page for more information on choosing the right cache for your scenarios. + +#### Configuring `memory_features` + +Please visit _**Redis's**_ [Eviction Policies](https://redis.io/topics/lru-cache#eviction-policies) for more information on the trade-offs between different maximum memory policy threshold configurations in [Azure Cache for Redis Setting](https://docs.microsoft.com/en-us/azure/azure-cache-for-redis/cache-configure#settings). + +#### Configuring `premium_tier_config` + +Please visit _**Azure Cache for Redis's**_ [Premium Tier Intro](https://docs.microsoft.com/en-us/azure/azure-cache-for-redis/cache-premium-tier-intro) for more information on premium configurations. + +#### Configuring `cache_shard_count` of `premium_tier_config` + +Please visit _**Azure Cache for Redis's**_ [Premium Clustering](https://docs.microsoft.com/en-us/azure/azure-cache-for-redis/cache-how-to-premium-clustering) page for more information on choosing whether or not clustering is right for you. + +### Observability Support + +For support on extending this module for observability, please visit [Redis Cache Metrics with Azure Monitoring](https://docs.microsoft.com/en-us/azure/azure-monitor/platform/metrics-supported#microsoftcacheredis). + + +## License +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/infra/modules/providers/azure/redis-cache/main.tf b/infra/modules/providers/azure/redis-cache/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..6ae839262c5f151848a388893dd51ce9284f28d8 --- /dev/null +++ b/infra/modules/providers/azure/redis-cache/main.tf @@ -0,0 +1,41 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +data "azurerm_resource_group" "arc" { + name = var.resource_group_name +} + +resource "azurerm_redis_cache" "arc" { + name = var.name + location = data.azurerm_resource_group.arc.location + resource_group_name = var.resource_group_name + capacity = var.capacity + sku_name = var.sku_name + family = var.sku_name == "Premium" ? "P" : "C" + shard_count = var.premium_tier_config.cache_shard_count + minimum_tls_version = var.minimum_tls_version + tags = var.resource_tags + + redis_configuration { + maxmemory_reserved = var.memory_features.maxmemory_reserved + maxmemory_delta = var.memory_features.maxmemory_delta + maxmemory_policy = var.memory_features.maxmemory_policy + maxfragmentationmemory_reserved = var.memory_features.maxfragmentationmemory_reserved + } + + patch_schedule { + day_of_week = var.premium_tier_config.server_patch_day + start_hour_utc = var.premium_tier_config.server_patch_hour + } +} \ No newline at end of file diff --git a/infra/modules/providers/azure/redis-cache/output.tf b/infra/modules/providers/azure/redis-cache/output.tf new file mode 100644 index 0000000000000000000000000000000000000000..202729ede5ee4fa86a800d6a27b0e217896e5adc --- /dev/null +++ b/infra/modules/providers/azure/redis-cache/output.tf @@ -0,0 +1,53 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +output "id" { + description = "The azure redis cache ID." + value = azurerm_redis_cache.arc.id +} + +output "hostname" { + description = "The URL of the azure redis cache created." + value = azurerm_redis_cache.arc.hostname +} + +output "ssl_port" { + description = "The SSL Port of the Redis Instance." + value = azurerm_redis_cache.arc.ssl_port +} + +output "name" { + description = "The name of the Redis instance." + value = azurerm_redis_cache.arc.name +} + +output "resource_group_name" { + description = "The resource group name of the Redis instance." + value = azurerm_redis_cache.arc.resource_group_name +} + +output "max_clients" { + description = "Returns maximum number of allowed connected clients at same time based on current configuration." + value = azurerm_redis_cache.arc.redis_configuration[0].maxclients +} + +output "maximum_cache_capacity" { + description = "The maximum capacity of the cache." + value = azurerm_redis_cache.arc.capacity * var.premium_tier_config.cache_shard_count +} + +output "primary_access_key" { + description = "The Primary Access Key for the Redis Instance." + value = azurerm_redis_cache.arc.primary_access_key +} diff --git a/infra/modules/providers/azure/redis-cache/testing/main.tf b/infra/modules/providers/azure/redis-cache/testing/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..7fa72a505ea8d05da9e66e180ecb5be58049a39f --- /dev/null +++ b/infra/modules/providers/azure/redis-cache/testing/main.tf @@ -0,0 +1,36 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +provider "azurerm" { + features {} +} + +module "resource_group" { + source = "../../resource-group" + + name = "osdu-module" + location = "eastus2" +} + +module "redis-cache" { + source = "../" + + name = "osdu-module-redis-cache-${module.resource_group.random}" + resource_group_name = module.resource_group.name + + resource_tags = { + osdu = "module" + } + +} \ No newline at end of file diff --git a/infra/modules/providers/azure/redis-cache/testing/unit_test.go b/infra/modules/providers/azure/redis-cache/testing/unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0fae77cd4f4164baa5b16d24e31bffb6cc1f1b64 --- /dev/null +++ b/infra/modules/providers/azure/redis-cache/testing/unit_test.go @@ -0,0 +1,74 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var workspace = "osdu-services-" + strings.ToLower(random.UniqueId()) +var location = "eastus" +var count = 4 + +var tfOptions = &terraform.Options{ + TerraformDir: "./", + Upgrade: true, +} + +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} + +func TestTemplate(t *testing.T) { + + expectedResult := asMap(t, `{ + "capacity" : 1, + "enable_non_ssl_port" : false, + "family" : "C", + "minimum_tls_version" : "1.2", + "shard_count" : 0, + "sku_name" : "Standard", + "redis_configuration" : [{ + "enable_authentication" : true, + "maxfragmentationmemory_reserved" : 50, + "maxmemory_delta" : 50, + "maxmemory_policy" : "volatile-lru", + "maxmemory_reserved" : 50 + }] + }`) + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tfOptions, + Workspace: workspace, + PlanAssertions: nil, + ExpectedResourceCount: count, + ExpectedResourceAttributeValues: infratests.ResourceDescription{ + "module.redis-cache.azurerm_redis_cache.arc": expectedResult, + }, + } + + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/modules/providers/azure/redis-cache/tests/integration/redis.go b/infra/modules/providers/azure/redis-cache/tests/integration/redis.go new file mode 100644 index 0000000000000000000000000000000000000000..4ee328075d1037a867545ef291c3824890286fff --- /dev/null +++ b/infra/modules/providers/azure/redis-cache/tests/integration/redis.go @@ -0,0 +1,95 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package integration + +import ( + "fmt" + "github.com/microsoft/cobalt/test-harness/infratests" + "github.com/microsoft/cobalt/test-harness/terratest-extensions/modules/azure" + "github.com/stretchr/testify/require" + "math/rand" + "os" + "strconv" + "testing" + "time" +) + +var subscription = os.Getenv("ARM_SUBSCRIPTION_ID") + +// redisHealthCheck - Asserts that the deployment was succesful. +func redisHealthCheck(t *testing.T, provionState string) { + require.Equal(t, string(provionState), "Succeeded", "The redis deployment hasn't succeeded") +} + +// validateNonSSLPort - Asserts that non SSL ports are disabled +func validateNonSSLPort(t *testing.T, NonSSLPort *bool) { + require.False(t, *NonSSLPort, "There's a non SSL port opened on the redis cluster") +} + +// validateMinTLSVersion - Validate that the min TLS version isn't nil and >= 1.0 +func validateMinTLSVersion(t *testing.T, minTLSVersion string) { + minTLSVersionFloat, err := strconv.ParseFloat(minTLSVersion, 32) + if err != nil { + t.Fatal(err) + } + + require.True(t, minTLSVersionFloat >= 1, "Min TLS version should be >= 1.0") +} + +// validateResourceGroupCaches - Validate the caches within the resource group +func validateResourceGroupCacheCount(t *testing.T, caches []string, expectedCacheName string) { + expectedResourceGroupCaches := []string{expectedCacheName} + + require.Equal(t, expectedResourceGroupCaches, caches, "The provisioned caches in the RG don't match the expected result") +} + +// InspectProvisionedCache - Runs a suite of test assertions to validate that a provisioned redis cache +// is operational. +func InspectProvisionedCache(cacheOutputName string, resourceGroupOutputName string) func(t *testing.T, output infratests.TerraformOutput) { + return func(t *testing.T, output infratests.TerraformOutput) { + cacheName := output[cacheOutputName].(string) + resourceGroup := output[resourceGroupOutputName].(string) + results := azure.GetCache(t, subscription, resourceGroup, cacheName) + cacheNameList := []string{} + + for _, resourceType := range *azure.ListCachesByResourceGroup(t, subscription, resourceGroup) { + cacheNameList = append(cacheNameList, string(*resourceType.Name)) + } + + validateResourceGroupCacheCount(t, cacheNameList, cacheName) + redisHealthCheck(t, string(results.ProvisioningState)) + validateNonSSLPort(t, results.EnableNonSslPort) + validateMinTLSVersion(t, string(results.MinimumTLSVersion)) + } +} + +// CheckRedisWriteOperations - Runs a suite of test assertions to validate that entries can be written and read from an redis cluster +func CheckRedisWriteOperations(hostnameOutputName string, primaryKeyOutputName string, hostPortOutputName string) func(t *testing.T, output infratests.TerraformOutput) { + return func(t *testing.T, output infratests.TerraformOutput) { + primaryKey := output[primaryKeyOutputName].(string) + hostname := output[hostnameOutputName].(string) + hostPort := output[hostPortOutputName].(float64) + address := fmt.Sprintf("%s:%d", hostname, int(hostPort)) + rand.Seed(time.Now().UnixNano()) + entryIdentifier := rand.Int() + keyName := fmt.Sprintf("key-%d", entryIdentifier) + client := azure.RedisClient(t, address, primaryKey) + keyValue := "entryTestValue" + + require.Equal(t, azure.SetRedisCacheEntry(t, client, keyName, keyValue, 0), "OK", "Redis cache key set operation result doesn't match the expected result") + require.Equal(t, azure.GetRedisCacheEntryValueStr(t, client, keyName), keyValue, "Redis cache key get operation result doesn't match the expected result") + require.Equal(t, azure.RemoveRedisCacheEntry(t, client, keyName), int64(1), "Redis cache key removal operation result doesn't match the expected result") + } +} diff --git a/infra/modules/providers/azure/redis-cache/tests/integration/redis_test.go b/infra/modules/providers/azure/redis-cache/tests/integration/redis_test.go new file mode 100644 index 0000000000000000000000000000000000000000..518d74f757f5d565a40f021bd8086969695ea400 --- /dev/null +++ b/infra/modules/providers/azure/redis-cache/tests/integration/redis_test.go @@ -0,0 +1,44 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package integration + +import ( + "fmt" + "testing" + + "github.com/microsoft/cobalt/infra/modules/providers/azure/redis-cache/tests" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +func TestRedisDeployment(t *testing.T) { + if tests.RedisName == "" { + t.Fatal(fmt.Errorf("tests.RedisName was not specified. Are all the required environment variables set?")) + } + + if tests.ResourceGroupName == "" { + t.Fatal(fmt.Errorf("tests.ResourceGroupName was not specified. Are all the required environment variables set?")) + } + + testFixture := infratests.IntegrationTestFixture{ + GoTest: t, + TfOptions: tests.RedisOptions, + ExpectedTfOutputCount: 6, + TfOutputAssertions: []infratests.TerraformOutputValidation{ + InspectProvisionedCache("name", "resource_group_name"), + CheckRedisWriteOperations("hostname", "primary_access_key", "ssl_port"), + }, + } + infratests.RunIntegrationTests(&testFixture) +} diff --git a/infra/modules/providers/azure/redis-cache/tests/tf_options.go b/infra/modules/providers/azure/redis-cache/tests/tf_options.go new file mode 100644 index 0000000000000000000000000000000000000000..f2a04d1ae26620ede9be490827e12637917c10ae --- /dev/null +++ b/infra/modules/providers/azure/redis-cache/tests/tf_options.go @@ -0,0 +1,37 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "os" + + "github.com/gruntwork-io/terratest/modules/terraform" +) + +// RedisName the redis cache name +var RedisName = os.Getenv("CACHE_NAME") + +// ResourceGroupName the name of the resource group +var ResourceGroupName = os.Getenv("RESOURCE_GROUP_NAME") + +// RedisOptions terraform options used for redis integration testing +var RedisOptions = &terraform.Options{ + TerraformDir: "../../", + Upgrade: true, + Vars: map[string]interface{}{ + "name": RedisName, + "resource_group_name": ResourceGroupName, + }, +} diff --git a/infra/modules/providers/azure/redis-cache/variables.tf b/infra/modules/providers/azure/redis-cache/variables.tf new file mode 100644 index 0000000000000000000000000000000000000000..3e38393b658b7a123615c2270f832ba19e84f1bd --- /dev/null +++ b/infra/modules/providers/azure/redis-cache/variables.tf @@ -0,0 +1,86 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +# Required inputs + +variable "name" { + description = "The name of the Redis instance. Changing this forces a new resource to be created as this should be a globally unique name." + type = string +} + +variable "resource_group_name" { + description = "The name of the resource group. TIP: Redis performs better when this is located in the same region as the app service intended for redis." + type = string +} + +# Basic, Standard and Premimum Tier Inputs (optional) + +variable "capacity" { + description = "The size of the Redis cache to deploy. When premium account is enabled with clusters, the true capacity of the account cache is capacity * cache_shard_count" + type = number + default = 1 +} + +variable "sku_name" { + description = "The Azure Cache for Redis pricing tier. Possible values are Basic, Standard and Premium. Azure currently charges by the minute for all pricing tiers." + type = string + default = "Standard" +} + +variable "minimum_tls_version" { + description = "The minimum TLS version." + type = string + default = "1.2" +} + +# Standard & Premium Tier Inputs (optional) + +variable "memory_features" { + description = "Configures memory management for standard & premium tier accounts. All number values are in megabytes. maxmemory_policy_cfg property controls how Redis will select what to remove when maxmemory is reached." + type = object({ + maxmemory_reserved = number + maxmemory_delta = number + maxmemory_policy = string + maxfragmentationmemory_reserved = number + }) + default = { + maxmemory_reserved = 50 + maxmemory_delta = 50 + maxmemory_policy = "volatile-lru" + maxfragmentationmemory_reserved = 50 + } +} + +# Distinct Premium Tier Inputs (optional) + +# We want to be explicit when enabling clustering due to its impact on pricing. +variable "premium_tier_config" { + description = "Configures the weekly schedule for server patching (Patch Window lasts for 5 hours). Also enables a single cluster for premium tier and when enabled, the true cache capacity of a redis cluster is capacity * cache_shard_count. 10 is the maximum number of shards/nodes allowed." + type = object({ + server_patch_day = string + server_patch_hour = number + cache_shard_count = number + }) + default = { + server_patch_day = "Friday" + server_patch_hour = 17 + cache_shard_count = 0 + } +} + +variable "resource_tags" { + description = "Map of tags to apply to taggable resources in this module. By default the taggable resources are tagged with the name defined above and this map is merged in" + type = map(string) + default = {} +} \ No newline at end of file diff --git a/infra/modules/providers/azure/resource-group/README.md b/infra/modules/providers/azure/resource-group/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ca9ff40df03e4b1e47af5cad19c94d653f9be979 --- /dev/null +++ b/infra/modules/providers/azure/resource-group/README.md @@ -0,0 +1,52 @@ +# Module Azure Resource Group + +Module for creating and managing Azure Resource Groups. + +## Usage + +``` +module "resource_group" { + source = "github.com/azure/osdu-infrastructure/modules/resource-group" + + name = "osdu-module" + location = "eastus2" + + resource_tags = { + environment = "test-environment" + } +} +``` + +## Inputs + +| Variable Name | Type | Description | +| --------------------------------- | ---------- | ------------------------------------ | +| `name` | _string_ | The name of the web app service. | +| `location` | _string_ | The location of the resource group. | +| `resource_tags` | _list_ | Map of tags to apply to taggable resources in this module. | + + +## Outputs + +Once the deployments are completed successfully, the output for the current module will be in the format mentioned below: + +- `name`: The name of the Resource Group. +- `location`: The location of the Resource Group. +- `id`: The id of the Resource Group. +- `random`: A random string derived from the Resource Group. + + +## License +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/infra/modules/providers/azure/resource-group/main.tf b/infra/modules/providers/azure/resource-group/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..4cfa691ac1127ff8bb1c3a9be14682488e25a515 --- /dev/null +++ b/infra/modules/providers/azure/resource-group/main.tf @@ -0,0 +1,39 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +resource "azurerm_resource_group" "main" { + name = var.name + location = var.location + tags = var.resource_tags +} + +resource "random_id" "main" { + keepers = { + # Generate a new ID only when a new resource group is defined + resource_group = azurerm_resource_group.main.name + } + + byte_length = 8 +} + +resource "azurerm_management_lock" "main" { + count = var.isLocked ? 1 : 0 + name = "${azurerm_resource_group.main.name}-delete-lock" + scope = azurerm_resource_group.main.id + lock_level = "CanNotDelete" + + lifecycle { + prevent_destroy = true + } +} \ No newline at end of file diff --git a/infra/modules/providers/azure/resource-group/outputs.tf b/infra/modules/providers/azure/resource-group/outputs.tf new file mode 100644 index 0000000000000000000000000000000000000000..ffc38af41c1d7e6ec2a3e6555d0156ce2c9e61e6 --- /dev/null +++ b/infra/modules/providers/azure/resource-group/outputs.tf @@ -0,0 +1,33 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +output "name" { + description = "The name of the Resource Group." + value = "${azurerm_resource_group.main.name}" +} + +output "location" { + description = "The location of the Resource Group." + value = "${azurerm_resource_group.main.location}" +} + +output "id" { + description = "The id of the Resource Group." + value = "${azurerm_resource_group.main.id}" +} + +output "random" { + description = "A random string derived from the Resource Group." + value = "${random_id.main.hex}" +} \ No newline at end of file diff --git a/infra/modules/providers/azure/resource-group/testing/main.tf b/infra/modules/providers/azure/resource-group/testing/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..fdebf47e0c53650714f43df07088b10af7da190c --- /dev/null +++ b/infra/modules/providers/azure/resource-group/testing/main.tf @@ -0,0 +1,28 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +provider "azurerm" { + features {} +} + +module "resource_group" { + source = "../" + + name = "osdu-module" + location = "eastus2" + + resource_tags = { + environment = "test-environment" + } +} \ No newline at end of file diff --git a/infra/modules/providers/azure/resource-group/testing/unit_test.go b/infra/modules/providers/azure/resource-group/testing/unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b554f7dc692391df7b326121daa6a0c7727056a7 --- /dev/null +++ b/infra/modules/providers/azure/resource-group/testing/unit_test.go @@ -0,0 +1,61 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "encoding/json" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var name = "resourcegroup-" +var location = "eastus2" +var count = 2 + +var tfOptions = &terraform.Options{ + TerraformDir: "./", + Upgrade: true, +} + +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} + +func TestTemplate(t *testing.T) { + + expectedResult := asMap(t, `{ + "location": "`+location+`" + }`) + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tfOptions, + Workspace: name + random.UniqueId(), + PlanAssertions: nil, + ExpectedResourceCount: count, + ExpectedResourceAttributeValues: infratests.ResourceDescription{ + "module.resource_group.azurerm_resource_group.main": expectedResult, + }, + } + + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/modules/providers/azure/resource-group/variables.tf b/infra/modules/providers/azure/resource-group/variables.tf new file mode 100644 index 0000000000000000000000000000000000000000..11cfb4ba8dd5628e7f395273b4558b5f11161564 --- /dev/null +++ b/infra/modules/providers/azure/resource-group/variables.tf @@ -0,0 +1,41 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +variable "name" { + description = "The name of the Resource Group." + type = string +} + +variable "location" { + description = "The location of the Resource Group." + type = string +} + +variable "resource_tags" { + description = "Map of tags to apply to taggable resources in this module. By default the taggable resources are tagged with the name defined above and this map is merged in" + type = map(string) + default = {} +} + +variable "environment" { + description = "The environment tag for the Resource Group." + type = string + default = "dev" +} + +variable "isLocked" { + description = "Does the Resource Group prevent deletion?" + type = bool + default = false +} \ No newline at end of file diff --git a/infra/modules/providers/azure/service-bus/README.md b/infra/modules/providers/azure/service-bus/README.md new file mode 100644 index 0000000000000000000000000000000000000000..c77bd3d50c54cd14e129ae622ffa669f527bb2ed --- /dev/null +++ b/infra/modules/providers/azure/service-bus/README.md @@ -0,0 +1,180 @@ +# Service Bus + +This Terrafom based service-bus module grants templates the ability to create service bus main functionality and Advanced features as well. + +## _More on Service Bus_ + +Microsoft Azure Service Bus is a fully managed enterprise integration message broker. Service Bus can decouple applications and services. + +Data is transferred between different applications and services using messages. A message is in binary format and can contain JSON, XML, or just text. + +### namespaces + +A namespace is a container for all messaging components. Multiple queues and topics can be in a single namespace, and namespaces often serve as application containers. + +### Topics + +You can also use topics to send and receive messages. While a queue is often used for point-to-point communication, topics are useful in publish/subscribe scenarios. + +Topics can have multiple, independent subscriptions. A subscriber to a topic can receive a copy of each message sent to that topic. Subscriptions are named entities. Subscriptions persist, but can expire or autodelete. + +For more information, Please check Azure Service Bus [documentation](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-messaging-overview). + +## Characteristics + +An instance of the `service-bus` module deploys the _**Service Bus**_ in order to provide templates with the following: + +- Ability to provision a single Service Bus namespace +- Ability to provision a configurable list of topic(s) and corresponding subscription(s) +- Ability to configure message TTLs +- Ability to enable dead lettering +- Ability to allow subscription sessions to guarantee ordered message processing +- Ability to allow message topic forwarding +- Ability to support azure resource tags + +## Out Of Scope + +The following are not support in the time being + +- Terraform support for Service Bus vnet integration is not available [https://github.com/terraform-providers/terraform-provider-azurerm/issues/3930](https://github.com/terraform-providers/terraform-provider-azurerm/issues/3930). However, this still can be implemented in other ways.' +- MI can be integrated with service bus using Terraform. However, MSI is not available in Germany and China 21Vianet yet. + +## Definition + +Terraform resources used to define the `service-bus` module include the following: + +- [azurerm_servicebus_namespace](https://www.terraform.io/docs/providers/azurerm/r/servicebus_namespace.html) +- [azurerm_servicebus_namespace_authorization_rule](https://www.terraform.io/docs/providers/azurerm/r/servicebus_namespace_authorization_rule.html) +- [azurerm_servicebus_topic](https://www.terraform.io/docs/providers/azurerm/r/servicebus_topic.html) +- [azurerm_servicebus_subscription](https://www.terraform.io/docs/providers/azurerm/r/servicebus_subscription.html) +- [azurerm_servicebus_subscription_rule](https://www.terraform.io/docs/providers/azurerm/r/servicebus_subscription_rule.html) + +## Usage + +Service Bus usage example: + +```terraform +module "service_bus" { + source = "../../modules/providers/azure/service-bus" + namespace_name = "sb-namespace" + resource_group_name = "rg" + sku = "Standard" + tags = { source = "terraform" } + + topics = [ + { + name = "topic_test" + enable_partitioning = true + authorization_rules = [ + { + name = "example" + rights = ["listen", "send"] + } + ] + subscriptions = [ + { + name = "sub_test" + max_delivery_count = 1 + lock_duration = "PT5M" //ISO 8601 format + forward_to = "" + } + ] + } + ] +} +``` + +## Outputs + +The value will have the following schema: + +```terraform +output "namespace_name" { + value = azurerm_servicebus_namespace.servicebus.name + description = "The namespace name." +} + +output "resource_group" { + value = data.azurerm_resource_group.resourcegroup.name +} + +output "namespace_id" { + value = azurerm_servicebus_namespace.servicebus.id + description = "The namespace ID." +} + +output "namespace_authorization_rules"{ + value = { + for auth in azurerm_servicebus_namespace_authorization_rule.sbnamespaceauth : + auth.name => { + listen = auth.listen + send = auth.send + manage = auth.manage + } + } + description = "List of namespace authorization rules." +} + +output "service_bus_namespace_default_primary_key" { + value = azurerm_servicebus_namespace.servicebus.default_primary_key + description = "The primary access key for the authorization rule RootManageSharedAccessKey." +} + +output "service_bus_namespace_default_connection_string" { + value = azurerm_servicebus_namespace.servicebus.default_primary_connection_string + description = "The primary connection string for the authorization rule RootManageSharedAccessKey which is created automatically by Azure." +} + +output "topics" { + value = { + for topic in azurerm_servicebus_topic.sptopic : + topic.name => { + id = topic.id + name = topic.name + authorization_rules = { + for auth in azurerm_servicebus_topic_authorization_rule.topicaauth : + auth.name => { + listen = auth.listen + send = auth.send + manage = auth.manage + } if topic.name == auth.topic_name + } + subscriptions = { + for subscription in azurerm_servicebus_subscription.subscription : + subscription.name => { + name = subscription.name + } if topic.name == subscription.topic_name + } + } + } + description = "All topics with the corresponding subscriptions" +} + +``` + +## Argument Reference + +Supported arguments for this module are available in [variables.tf](variables.tf) + +## Attributes Reference + +- `name` : The namespace name. +- `id` : The namespace ID. +- `namespace_authorization_rules` : The authorization rules for the namespace +- `topics` : All topics with the corresponding subscriptions + + +## License +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/infra/modules/providers/azure/service-bus/main.tf b/infra/modules/providers/azure/service-bus/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..1dc78ff9140b0e5d9ebf82112c39836b0079e0a7 --- /dev/null +++ b/infra/modules/providers/azure/service-bus/main.tf @@ -0,0 +1,257 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +locals { + authorization_rules = [ + for rule in var.authorization_rules : merge({ + name = "" + rights = [] + }, rule) + ] + + default_authorization_rule = { + name = "RootManageSharedAccessKey" + primary_connection_string = azurerm_servicebus_namespace.main.default_primary_connection_string + secondary_connection_string = azurerm_servicebus_namespace.main.default_secondary_connection_string + primary_key = azurerm_servicebus_namespace.main.default_primary_key + secondary_key = azurerm_servicebus_namespace.main.default_secondary_key + } + + topics = [ + for topic in var.topics : merge({ + name = "" + status = "Active" + auto_delete_on_idle = null + default_message_ttl = null + enable_batched_operations = null + enable_express = null + enable_partitioning = null + max_size = null + enable_duplicate_detection = null + enable_ordering = null + authorization_rules = [] + subscriptions = [] + + duplicate_detection_history_time_window = null + }, topic) + ] + + topic_authorization_rules = flatten([ + for topic in local.topics : [ + for rule in topic.authorization_rules : merge({ + name = "" + rights = [] + }, rule, { + topic_name = topic.name + }) + ] + ]) + + topic_subscriptions = flatten([ + for topic in local.topics : [ + for subscription in topic.subscriptions : + merge({ + name = "" + auto_delete_on_idle = null + default_message_ttl = null + lock_duration = null + enable_batched_operations = null + max_delivery_count = null + enable_session = null + forward_to = null + rules = [] + + enable_dead_lettering_on_message_expiration = null + }, subscription, { + topic_name = topic.name + }) + ] + ]) + + topic_subscription_rules = flatten([ + for subscription in local.topic_subscriptions : [ + for rule in subscription.rules : merge({ + name = "" + sql_filter = "" + action = "" + }, rule, { + topic_name = subscription.topic_name + subscription_name = subscription.name + }) + ] + ]) + + queues = [ + for queue in var.queues : merge({ + name = "" + auto_delete_on_idle = null + default_message_ttl = null + enable_express = false + enable_partitioning = false + lock_duration = null + max_size = null + enable_duplicate_detection = false + enable_session = false + max_delivery_count = 10 + authorization_rules = [] + + enable_dead_lettering_on_message_expiration = false + duplicate_detection_history_time_window = null + }, queue) + ] + + queue_authorization_rules = flatten([ + for queue in local.queues : [ + for rule in queue.authorization_rules : merge({ + name = "" + rights = [] + }, rule, { + queue_name = queue.name + }) + ] + ]) +} + +## define resource group +data "azurerm_resource_group" "main" { + name = var.resource_group_name +} + +resource "azurerm_servicebus_namespace" "main" { + name = var.name + location = data.azurerm_resource_group.main.location + resource_group_name = data.azurerm_resource_group.main.name + sku = var.sku + capacity = var.capacity + tags = var.resource_tags +} + +resource "azurerm_servicebus_namespace_authorization_rule" "main" { + count = length(local.authorization_rules) + + name = local.authorization_rules[count.index].name + namespace_name = azurerm_servicebus_namespace.main.name + resource_group_name = data.azurerm_resource_group.main.name + + listen = contains(local.authorization_rules[count.index].rights, "listen") ? true : false + send = contains(local.authorization_rules[count.index].rights, "send") ? true : false + manage = contains(local.authorization_rules[count.index].rights, "manage") ? true : false +} + +resource "azurerm_servicebus_topic" "main" { + count = length(local.topics) + + name = local.topics[count.index].name + resource_group_name = data.azurerm_resource_group.main.name + namespace_name = azurerm_servicebus_namespace.main.name + + status = local.topics[count.index].status + auto_delete_on_idle = local.topics[count.index].auto_delete_on_idle + default_message_ttl = local.topics[count.index].default_message_ttl + enable_batched_operations = local.topics[count.index].enable_batched_operations + enable_express = local.topics[count.index].enable_express + enable_partitioning = local.topics[count.index].enable_partitioning + max_size_in_megabytes = local.topics[count.index].max_size + requires_duplicate_detection = local.topics[count.index].enable_duplicate_detection + support_ordering = local.topics[count.index].enable_ordering + + duplicate_detection_history_time_window = local.topics[count.index].duplicate_detection_history_time_window +} + +resource "azurerm_servicebus_topic_authorization_rule" "main" { + count = length(local.topic_authorization_rules) + + name = local.topic_authorization_rules[count.index].name + resource_group_name = data.azurerm_resource_group.main.name + namespace_name = azurerm_servicebus_namespace.main.name + topic_name = local.topic_authorization_rules[count.index].topic_name + + listen = contains(local.topic_authorization_rules[count.index].rights, "listen") ? true : false + send = contains(local.topic_authorization_rules[count.index].rights, "send") ? true : false + manage = contains(local.topic_authorization_rules[count.index].rights, "manage") ? true : false + + depends_on = [azurerm_servicebus_topic.main] +} + +resource "azurerm_servicebus_subscription" "main" { + count = length(local.topic_subscriptions) + + name = local.topic_subscriptions[count.index].name + resource_group_name = data.azurerm_resource_group.main.name + namespace_name = azurerm_servicebus_namespace.main.name + topic_name = local.topic_subscriptions[count.index].topic_name + + max_delivery_count = local.topic_subscriptions[count.index].max_delivery_count + auto_delete_on_idle = local.topic_subscriptions[count.index].auto_delete_on_idle + default_message_ttl = local.topic_subscriptions[count.index].default_message_ttl + lock_duration = local.topic_subscriptions[count.index].lock_duration + enable_batched_operations = local.topic_subscriptions[count.index].enable_batched_operations + requires_session = local.topic_subscriptions[count.index].enable_session + forward_to = local.topic_subscriptions[count.index].forward_to + + dead_lettering_on_message_expiration = local.topic_subscriptions[count.index].enable_dead_lettering_on_message_expiration + + depends_on = [azurerm_servicebus_topic.main] +} + +resource "azurerm_servicebus_subscription_rule" "main" { + count = length(local.topic_subscription_rules) + + name = local.topic_subscription_rules[count.index].name + resource_group_name = data.azurerm_resource_group.main.name + namespace_name = azurerm_servicebus_namespace.main.name + topic_name = local.topic_subscription_rules[count.index].topic_name + subscription_name = local.topic_subscription_rules[count.index].subscription_name + filter_type = local.topic_subscription_rules[count.index].sql_filter != "" ? "SqlFilter" : null + sql_filter = local.topic_subscription_rules[count.index].sql_filter + action = local.topic_subscription_rules[count.index].action + + depends_on = [azurerm_servicebus_subscription.main] +} + +resource "azurerm_servicebus_queue" "main" { + count = length(local.queues) + + name = local.queues[count.index].name + resource_group_name = data.azurerm_resource_group.main.name + namespace_name = azurerm_servicebus_namespace.main.name + + auto_delete_on_idle = local.queues[count.index].auto_delete_on_idle + default_message_ttl = local.queues[count.index].default_message_ttl + enable_express = local.queues[count.index].enable_express + enable_partitioning = local.queues[count.index].enable_partitioning + lock_duration = local.queues[count.index].lock_duration + max_size_in_megabytes = local.queues[count.index].max_size + requires_duplicate_detection = local.queues[count.index].enable_duplicate_detection + requires_session = local.queues[count.index].enable_session + dead_lettering_on_message_expiration = local.queues[count.index].enable_dead_lettering_on_message_expiration + max_delivery_count = local.queues[count.index].max_delivery_count + + duplicate_detection_history_time_window = local.queues[count.index].duplicate_detection_history_time_window +} + +resource "azurerm_servicebus_queue_authorization_rule" "main" { + count = length(local.queue_authorization_rules) + + name = local.queue_authorization_rules[count.index].name + resource_group_name = data.azurerm_resource_group.main.name + namespace_name = azurerm_servicebus_namespace.main.name + queue_name = local.queue_authorization_rules[count.index].queue_name + + listen = contains(local.queue_authorization_rules[count.index].rights, "listen") ? true : false + send = contains(local.queue_authorization_rules[count.index].rights, "send") ? true : false + manage = contains(local.queue_authorization_rules[count.index].rights, "manage") ? true : false + + depends_on = [azurerm_servicebus_queue.main] +} \ No newline at end of file diff --git a/infra/modules/providers/azure/service-bus/output.tf b/infra/modules/providers/azure/service-bus/output.tf new file mode 100644 index 0000000000000000000000000000000000000000..ad40ce61bf0cccf0c31d328c8fc8e9b7faa93f8a --- /dev/null +++ b/infra/modules/providers/azure/service-bus/output.tf @@ -0,0 +1,73 @@ +output "name" { + value = azurerm_servicebus_namespace.main.name + description = "The namespace name." +} + +output "id" { + value = azurerm_servicebus_namespace.main.id + description = "The namespace ID." +} + +output "default_connection_string" { + description = "The primary connection string for the authorization rule RootManageSharedAccessKey which is created automatically by Azure." + value = azurerm_servicebus_namespace.main.default_primary_connection_string +} + +output "authorization_rules" { + value = merge({ + for rule in azurerm_servicebus_namespace_authorization_rule.main : + rule.name => { + name = rule.name + primary_key = rule.primary_key + primary_connection_string = rule.primary_connection_string + secondary_key = rule.secondary_key + secondary_connection_string = rule.secondary_connection_string + } + }, { + default = local.default_authorization_rule + }) + description = "Map of authorization rules." + sensitive = true +} + +output "topics" { + value = { + for topic in azurerm_servicebus_topic.main : + topic.name => { + id = topic.id + name = topic.name + authorization_rules = { + for rule in azurerm_servicebus_topic_authorization_rule.main : + rule.name => { + name = rule.name + primary_key = rule.primary_key + primary_connection_string = rule.primary_connection_string + secondary_key = rule.secondary_key + secondary_connection_string = rule.secondary_connection_string + } if topic.name == rule.topic_name + } + } + } + description = "Map of topics." +} + +output "queues" { + value = { + for queue in azurerm_servicebus_queue.main : + queue.name => { + id = queue.id + name = queue.name + authorization_rules = { + for rule in azurerm_servicebus_queue_authorization_rule.main : + rule.name => { + name = rule.name + primary_key = rule.primary_key + primary_connection_string = rule.primary_connection_string + secondary_key = rule.secondary_key + secondary_connection_string = rule.secondary_connection_string + } if queue.name == rule.queue_name + } + } + } + description = "Map of queues." +} \ No newline at end of file diff --git a/infra/modules/providers/azure/service-bus/testing/main.tf b/infra/modules/providers/azure/service-bus/testing/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..b97a9b946042983e13a2ec319e16adba666efe97 --- /dev/null +++ b/infra/modules/providers/azure/service-bus/testing/main.tf @@ -0,0 +1,50 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +provider "azurerm" { + features {} +} + +module "resource_group" { + source = "../../resource-group" + + name = "osdu-module" + location = "eastus2" +} + +module "service_bus" { + source = "../" + name = "osdu-module-service-bus-${module.resource_group.random}" + resource_group_name = module.resource_group.name + sku = "Standard" + + topics = [ + { + name = "topic_test" + enable_partitioning = true + subscriptions = [ + { + name = "sub_test" + max_delivery_count = 1 + lock_duration = "PT5M" + forward_to = "" + } + ] + } + ] + + resource_tags = { + source = "terraform", + } +} diff --git a/infra/modules/providers/azure/service-bus/testing/unit_test.go b/infra/modules/providers/azure/service-bus/testing/unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1a4e33168ce0274eaab31fd6bd712f5d9c897489 --- /dev/null +++ b/infra/modules/providers/azure/service-bus/testing/unit_test.go @@ -0,0 +1,108 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package unit + +//might be package test +import ( + "encoding/json" + "strings" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var workspace = "osdu-services-" + strings.ToLower(random.UniqueId()) +var count = 14 + +var tfOptions = &terraform.Options{ + TerraformDir: "./", + Upgrade: false, +} + +// helper function to parse blocks of JSON into a generic Go map +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} + +func TestTemplate(t *testing.T) { + + expectedSBNamespace := map[string]interface{}{ + "capacity": 0.0, + "sku": "Standard", + "tags": map[string]interface{}{ + "source": "terraform", + }, + } + + expectedNamespaceAuth := map[string]interface{}{ + "name": "policy", + "listen": true, + "send": true, + "manage": false, + } + + expectedSubscription := map[string]interface{}{ + "name": "sub_test", + "max_delivery_count": 1.0, + "lock_duration": "PT5M", + "forward_to": "", + "dead_lettering_on_message_expiration": true, + } + + expectedTopic := map[string]interface{}{ + "name": "topic_test", + "default_message_ttl": "PT30M", + "enable_partitioning": true, + "support_ordering": true, + "requires_duplicate_detection": true, + } + + expectedTopicAuth := map[string]interface{}{ + "name": "policy", + "listen": true, + "send": true, + "manage": false, + } + + expectedSubRules := map[string]interface{}{ + "name": "sub_test", + "filter_type": "SqlFilter", + "sql_filter": "color = 'red'", + "action": "", + } + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tfOptions, + Workspace: workspace, + PlanAssertions: nil, + ExpectedResourceCount: count, + ExpectedResourceAttributeValues: infratests.ResourceDescription{ + "module.service-bus.azurerm_servicebus_namespace.servicebus": expectedSBNamespace, + "module.service-bus.azurerm_servicebus_namespace_authorization_rule.sbnamespaceauth[0]": expectedNamespaceAuth, + "module.service-bus.azurerm_servicebus_topic.sptopic[0]": expectedTopic, + "module.service-bus.azurerm_servicebus_subscription.subscription[0]": expectedSubscription, + "module.service-bus.azurerm_servicebus_topic_authorization_rule.topicaauth[0]": expectedTopicAuth, + "module.service-bus.azurerm_servicebus_subscription_rule.subrules[0]": expectedSubRules, + }, + } + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/modules/providers/azure/service-bus/variables.tf b/infra/modules/providers/azure/service-bus/variables.tf new file mode 100644 index 0000000000000000000000000000000000000000..a2c5dbe6a4e2553226f498540f01d289d87f4e07 --- /dev/null +++ b/infra/modules/providers/azure/service-bus/variables.tf @@ -0,0 +1,59 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +variable "name" { + description = "The name of the namespace." + type = string +} + +variable "resource_group_name" { + description = "The name of an existing resource group that service bus will be provisioned" + type = string +} + +variable "sku" { + description = "The SKU of the namespace. The options are: `Basic`, `Standard`, `Premium`." + type = string + default = "Standard" +} + +variable "resource_tags" { + description = " A mapping of tags to assign to the resource." + type = map(string) + default = {} +} + +variable "capacity" { + description = "The number of message units." + type = number + default = 0 +} + +variable "topics" { + type = any + default = [] + description = "List of topics." +} + +variable "authorization_rules" { + type = any + default = [] + description = "List of namespace authorization rules." +} + +variable "queues" { + type = any + default = [] + description = "List of queues." +} \ No newline at end of file diff --git a/infra/modules/providers/azure/service-principal/README.md b/infra/modules/providers/azure/service-principal/README.md new file mode 100755 index 0000000000000000000000000000000000000000..741aa9ff00b072aeb5c52b2e733a1c3843f5716c --- /dev/null +++ b/infra/modules/providers/azure/service-principal/README.md @@ -0,0 +1,167 @@ +# Module service-principal + +Module for managing a service principal for Azure Active Directory with the following characteristics: + +- Create a Principal and Assign to a role. +- Use an existing Principal and Assign to a role. + +> __This module requires the Terraform Principal to have Azure Active Directory Graph - `Application.ReadWrite.OwnedBy` Permissions if creating a principal.__ + + +## Usage + +__Sample 1:__ Create a Service Principal + +_terraform_ +``` +locals { + name = "iac-osdu" + location = "southcentralus" +} + +resource "random_id" "main" { + keepers = { + name = local.name + } + + byte_length = 8 +} + +resource "azurerm_resource_group" "main" { + name = format("${local.name}-%s", random_id.main.hex) + location = local.location +} + +module "service_principal" { + source = "https://github.com/azure/osdu-infrastructure/infra/modules/providers/azure/service-principal" + + name = format("${local.name}-%s-ad-app-management", random_id.main.hex) + + role = "Contributor" + scopes = [azurerm_resource_group.main.id] + + api_permissions = [ + { + name = "Microsoft Graph" + oauth2_permissions = [ + "User.Read.All", + "Directory.Read.All" + ] + } + ] + + end_date = "1W" +} +``` + + +__Sample 2:__ Bring your own Service Principal + +_cli commands_ +```bash +UNIQUE=$(echo $((RANDOM%999+100))) +NAME="iac-osdu-$UNIQUE-ad-app-management" + +# Create a Service Principal +SECRET=$(az ad sp create-for-rbac --name $NAME --skip-assignment --query password -otsv) + +# Retrieve the Principal Metadata Information +az ad sp list --display-name $NAME --query [].'{objectId:objectId, appId:appId, name:displayName}' -ojson + +# Result +[ + { + "appId": "2357b068-2541-4244-8866-27e23aa0a112", + "name": "iac-osdu-246-ad-app-management", + "objectId": "1586d1ed-dd0b-45ce-a698-f155a7becc8b" + } +] + +# Retrieve the AD Application Metadata Information +az ad app list --display-name $NAME --query [].'{object_id:objectId, name:displayName, appId:appId}' -ojson + +# Result +[ + { + "appId": "2357b068-2541-4244-8866-27e23aa0a112", + "name": "iac-osdu-246-ad-app-management", + "object_id": "32f1438a-6b3a-47d8-9c71-bf7fc8efbdfd" + } +] + + +# Assign any API Permissions Desired +# Microsoft Graph -- Application Permissions -- Directory.Read.All ** GRANT ADMIN-CONSENT +adObjectId=$(az ad app list --display-name $NAME --query [].objectId -otsv) +graphId=$(az ad sp list --query "[?appDisplayName=='Microsoft Graph'].appId | [0]" --all -otsv) +directoryReadAll=$(az ad sp show --id $graphId --query "appRoles[?value=='Directory.Read.All'].id | [0]" -otsv)=Role + +az ad app permission add --id $adObjectId --api $graphId --api-permissions $directoryReadAll + +# Grant Admin Consent +# ** REQUIRES ADMIN AD ACCESS ** +az ad app permission admin-consent --id $appId +``` + +_terraform_ +```hcl +locals { + name = "iac-osdu" + location = "southcentralus" +} + +resource "random_id" "main" { + keepers = { + name = local.name + } + + byte_length = 8 +} + +resource "azurerm_resource_group" "main" { + name = format("${local.name}-%s", random_id.main.hex) + location = local.location +} + +module "service_principal" { + source = "../" + + name = "iac-osdu-246-ad-app-management" + + scopes = [azurerm_resource_group.main.id] + role = "Contributor" + + create_for_rbac = false + object_id = "1586d1ed-dd0b-45ce-a698-f155a7becc8b" + + principal = { + name = "iac-osdu-246-ad-app-management" + appId = "2357b068-2541-4244-8866-27e23aa0a112" + password = "******************************" + } +} +``` + +### Input Variables + +Please refer to [variables.tf](./variables.tf). + +### Output Variables + +Please refer to [output.tf](./output.tf). + + +## License +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/infra/modules/providers/azure/service-principal/main.tf b/infra/modules/providers/azure/service-principal/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..99e873d0c5142e5271d935312dd9503973579cb9 --- /dev/null +++ b/infra/modules/providers/azure/service-principal/main.tf @@ -0,0 +1,73 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +resource "random_password" "main" { + count = local.create_count != 0 && var.password != null ? 1 : 0 + length = 32 + special = false +} + +data "azuread_service_principal" "main" { + count = length(local.api_names) + display_name = local.api_names[count.index] +} + + +resource "azuread_application" "main" { + count = local.create_count + name = var.name + available_to_other_tenants = false + + dynamic "required_resource_access" { + for_each = local.required_resource_access + iterator = resource + content { + resource_app_id = resource.value.resource_app_id + + dynamic "resource_access" { + for_each = resource.value.resource_access + iterator = access + content { + id = access.value.id + type = access.value.type + } + } + } + } +} + +resource "azuread_service_principal" "main" { + count = local.create_count + application_id = azuread_application.main[0].application_id +} + +resource "azurerm_role_assignment" "main" { + count = length(var.scopes) + role_definition_name = var.role + principal_id = var.create_for_rbac == true ? azuread_service_principal.main[0].object_id : var.object_id + scope = var.scopes[count.index] +} + +resource "azuread_service_principal_password" "main" { + count = local.create_count != 0 && var.password != null ? 1 : 0 + service_principal_id = azuread_service_principal.main[0].id + + value = coalesce(var.password, random_password.main[0].result) + end_date = local.end_date + end_date_relative = local.end_date_relative + + lifecycle { + ignore_changes = all + } +} diff --git a/infra/modules/providers/azure/service-principal/output.tf b/infra/modules/providers/azure/service-principal/output.tf new file mode 100755 index 0000000000000000000000000000000000000000..e848a59a74d928f4b2279cd7b8e58437cf65779b --- /dev/null +++ b/infra/modules/providers/azure/service-principal/output.tf @@ -0,0 +1,34 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +output "id" { + description = "The ID of the Azure AD Service Principal" + value = var.create_for_rbac == true ? azuread_service_principal.main[0].object_id : var.object_id +} + +output "name" { + description = "The Display Name of the Azure AD Application associated with this Service Principal" + value = var.create_for_rbac == true ? azuread_service_principal.main[0].display_name : var.principal.name +} + +output "client_id" { + description = "The ID of the Azure AD Application" + value = var.create_for_rbac == true ? azuread_service_principal.main[0].application_id : var.principal.appId +} + +output "client_secret" { + description = "The password of the generated service principal. This is only exported when create_for_rbac is true." + value = var.create_for_rbac == true ? azuread_service_principal_password.main[0].value : var.principal.password + sensitive = true +} diff --git a/infra/modules/providers/azure/service-principal/sample/main.tf b/infra/modules/providers/azure/service-principal/sample/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..a6571ece4ca198a7e8cb56f77237dd8cee7d1430 --- /dev/null +++ b/infra/modules/providers/azure/service-principal/sample/main.tf @@ -0,0 +1,99 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +provider "azurerm" { + version = "=1.44.0" + # features {} +} + +provider "azuread" { + version = "=0.7.0" +} + + +locals { + name = "iac-osdu" + location = "southcentralus" +} + +resource "random_id" "main" { + keepers = { + name = local.name + } + + byte_length = 8 +} + +resource "azurerm_resource_group" "main" { + name = format("${local.name}-%s", random_id.main.hex) + location = local.location +} + +# This Example Creates a Service Principal +# module "service_principal" { +# source = "../" + +# name = format("${local.name}-%s-ad-app-management", random_id.main.hex) +# role = "Contributor" +# scopes = [azurerm_resource_group.main.id] +# end_date = "1W" + +# api_permissions = [ +# { +# name = "Microsoft Graph" +# app_roles = [ +# "User.Read.All", +# "Directory.Read.All" +# ] +# } +# ] +# } + +# This Example Uses an Existing Service Principal +module "service_principal" { + source = "../" + + name = "iac-osdu-246-ad-app-management" + scopes = [azurerm_resource_group.main.id] + role = "Contributor" + + create_for_rbac = false + object_id = "1586d1ed-dd0b-45ce-a698-f155a7becc8b" + + principal = { + name = "iac-osdu-246-ad-app-management" + appId = "2357b068-2541-4244-8866-27e23aa0a112" + password = "******************************" + } +} + +output "id" { + description = "The ID of the Azure AD Service Principal" + value = module.service_principal.id +} + +output "name" { + description = "The Display Name of the Azure AD Application associated with this Service Principal" + value = module.service_principal.name +} + +output "client_id" { + description = "The ID of the Azure AD Application" + value = module.service_principal.client_id +} + +output "client_secret" { + description = "The password of the generated service principal. This is only exported when create_for_rbac is true." + value = module.service_principal.client_secret +} diff --git a/infra/modules/providers/azure/service-principal/sample/unit_test.go b/infra/modules/providers/azure/service-principal/sample/unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d2493c656eea7e832d287d3ca6bda3413ed94e5e --- /dev/null +++ b/infra/modules/providers/azure/service-principal/sample/unit_test.go @@ -0,0 +1,71 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "encoding/json" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var name = "serviceprincipal-" +var count = 7 + +var tfOptions = &terraform.Options{ + TerraformDir: "./", + Upgrade: true, +} + +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} + +func TestTemplate(t *testing.T) { + + expectedResult := asMap(t, `{ + "available_to_other_tenants": false, + "type": "webapp/api", + "required_resource_access": [{ + "resource_app_id": "00000003-0000-0000-c000-000000000000", + "resource_access": [{ + "id": "df021288-bdef-4463-88db-98f22de89214", + "type": "Role" + }, { + "id": "7ab1d382-f21e-4acd-a863-ba3e13f7da61", + "type": "Role" + }] + }] + }`) + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tfOptions, + Workspace: name + random.UniqueId(), + PlanAssertions: nil, + ExpectedResourceCount: count, + ExpectedResourceAttributeValues: infratests.ResourceDescription{ + "module.service_principal.azuread_application.main[0]": expectedResult, + }, + } + + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/modules/providers/azure/service-principal/variables.tf b/infra/modules/providers/azure/service-principal/variables.tf new file mode 100755 index 0000000000000000000000000000000000000000..cb9b298fe074bdfcd883542b3a9a1712c102a4ee --- /dev/null +++ b/infra/modules/providers/azure/service-principal/variables.tf @@ -0,0 +1,132 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +variable "name" { + type = string + description = "The name of the service principal." +} + +variable "password" { + type = string + description = "A password for the service principal. (Optional)" + default = "" +} + +variable "end_date" { + type = string + description = "The relative duration or RFC3339 date after which the password expire." + default = "2Y" +} + +variable "role" { + type = string + description = "The name of a role for the service principal." + default = "" +} + +variable "scopes" { + type = list(string) + description = "List of scopes the role assignment applies to." + default = [] +} + +variable "create_for_rbac" { + description = "Create a new Service Principle" + type = bool + default = true +} + +variable "object_id" { + description = "Object Id of an existing AD service principal to be assigned to a role." + type = string + default = "" +} + +variable "principal" { + description = "Bring your own Principal metainformation. Optional: {name, appId, password}" + type = map(string) + default = {} +} + + +variable "api_permissions" { + type = any + default = [] + description = "List of API permissions." +} + + + +locals { + create_count = var.create_for_rbac == true ? 1 : 0 + + date = regexall("^(?:(\\d{4})-(\\d{2})-(\\d{2}))[Tt]?(?:(\\d{2}):(\\d{2})(?::(\\d{2}))?(?:\\.(\\d+))?)?([Zz]|[\\+|\\-]\\d{2}:\\d{2})?$", var.end_date) + + duration = regexall("^(?:(\\d+)Y)?(?:(\\d+)M)?(?:(\\d+)W)?(?:(\\d+)D)?(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)s)?$", var.end_date) + + service_principals = { + for s in data.azuread_service_principal.main : s.display_name => { + application_id = s.application_id + display_name = s.display_name + app_roles = { for p in s.app_roles : p.value => p.id } + oauth2_permissions = { for p in s.oauth2_permissions : p.value => p.id } + } + } + + api_permissions = [ + for p in var.api_permissions : merge({ + id = "" + name = "" + app_roles = [] + oauth2_permissions = [] + }, p) + ] + + api_names = local.api_permissions[*].name + + required_resource_access = [ + for a in local.api_permissions : { + resource_app_id = local.service_principals[a.name].application_id + resource_access = concat( + [for p in a.app_roles : { + id = local.service_principals[a.name].app_roles[p] + type = "Role" + }] + ) + } + ] + + end_date_relative = length(local.duration) > 0 ? format( + "%dh", + ( + (coalesce(local.duration[0][0], 0) * 24 * 365) + + (coalesce(local.duration[0][1], 0) * 24 * 30) + + (coalesce(local.duration[0][2], 0) * 24 * 7) + + (coalesce(local.duration[0][3], 0) * 24) + + coalesce(local.duration[0][4], 0) + ) + ) : null + + end_date = length(local.date) > 0 ? format( + "%02d-%02d-%02dT%02d:%02d:%02d.%02d%s", + local.date[0][0], + local.date[0][1], + local.date[0][2], + coalesce(local.date[0][3], "23"), + coalesce(local.date[0][4], "59"), + coalesce(local.date[0][5], "00"), + coalesce(local.date[0][6], "00"), + coalesce(local.date[0][7], "Z") + ) : null +} diff --git a/infra/modules/providers/azure/storage-account/README.md b/infra/modules/providers/azure/storage-account/README.md new file mode 100755 index 0000000000000000000000000000000000000000..e030679464484383fa5b43ab11f9941e36728dd9 --- /dev/null +++ b/infra/modules/providers/azure/storage-account/README.md @@ -0,0 +1,120 @@ +# Azure Storage + +This Terraform based `storage-account` module grants templates the ability to configure and deploy cloud `storage containers` along with a `storage account` using Microsoft's _**Azure Storage**_ service. + +In addition, this module offers both authentication and authorization features: + +- For authentication, this module automatically enrolls the deployed `storage account` into Microsoft's _**Managed Identities**_ service. + + +#### What is Azure Storage? + +From the official [Documentation](https://docs.microsoft.com/en-us/azure/storage/common/storage-introduction): + +> "A storage account provides a unique namespace in Azure for your data. Every object that you store in Azure Storage has an address that includes your unique account name. A container organizes a set of blobs, similar to a directory in a file system. A storage account can include an unlimited number of containers, and a container can store an unlimited number of blobs." - Source: Microsoft's [Introduction to Azure Blob Storage](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blobs-introduction) + +This module deploys a `storage account` along with `storage containers` in order to satisfy blob storage scenarios which are optimized for storing massive amounts of unstructured data, such as text or binary data. + +## Current Features + +An instance of the `storage-account` module deploys the _**Azure Storage**_ service in order to provide templates with the following: + +- Ability to deploy Storage Containers alongside deploying a Storage Account. + + +## Module Usage + +Azure Storage usage example: + +```terraform +resource "azurerm_resource_group" "sample" { + name = var.prefix + location = var.resource_group_location +} + +module "storage_account" { + source = "../../modules/providers/azure/storage-account" + + name = "mystorageaccount" + container_names = ["test"] + share_names = ["share"] + queue_names = ["queue"] + resource_group_name = azurerm_resource_group.sample.name +} +``` + +### Resources + +| Resource | Terraform Link | Description | +|---|---|---| +| `azurerm_storage_account` | [storage account](https://www.terraform.io/docs/providers/azurerm/r/storage_account.html) | This resource will be declared within the module. | +| `azurerm_storage_container` | [storage container](https://www.terraform.io/docs/providers/azurerm/r/storage_container.html) | This resource will be declared within the module. | +| `azurerm_storage_share` | [storage share](https://www.terraform.io/docs/providers/azurerm/r/storage_share.html) | This resource will be declared within the module. | +| `azurerm_storage_queue` | [storage queue](https://www.terraform.io/docs/providers/azurerm/r/storage_queue.html) | This resource will be declared within the module. | + +### Input Variables + +Please refer to [variables.tf](./variables.tf). + +### Output Variables + +Please refer to [output.tf](./output.tf). + +### Automated Tests + + +_Setup the Environment._ +```bash +# Copy the environment template and populate required values for RESOURCE_GROUP_NAME STORAGE_ACCOUNT_NAME and CONTAINER_NAME in .env +cp ./tests/.env.testing.template .env + +# Export the environment variables. +export $(cat .env | xargs) + +# Create a resource group if one does not exist. (OPTIONAL) +az group create --name $RESOURCE_GROUP_NAME --location eastus2 + +## Create a tfvars file from the environment values. +cat > testing.tfvars << EOF +name = "$STORAGE_ACCOUNT_NAME" + +container_names = [ + "$CONTAINER_NAME" +] + +resource_group_name = "$RESOURCE_GROUP_NAME" +EOF +``` + +>This module's tests validate a provisioned Terraform workspace. + +```bash +terraform init +terraform plan --var-file=testing.tfvars +terraform apply --var-file=testing.tfvars +``` + +__Execute Unit Tests__ + +`go test -v $(go list ./... | grep "unit")` + +__Execute Integration Tests__ + +`go test -v $(go list ./... | grep "integration")` + + + +## License +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/infra/modules/providers/azure/storage-account/main.tf b/infra/modules/providers/azure/storage-account/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..35f2eb59d3bbb885f4ac2f3e2528d65fb85a1844 --- /dev/null +++ b/infra/modules/providers/azure/storage-account/main.tf @@ -0,0 +1,56 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +data "azurerm_resource_group" "main" { + name = var.resource_group_name +} + +resource "azurerm_storage_account" "main" { + # required + name = lower(var.name) + resource_group_name = data.azurerm_resource_group.main.name + location = data.azurerm_resource_group.main.location + account_tier = var.performance_tier + account_replication_type = var.replication_type + + # optional + account_kind = var.kind + enable_https_traffic_only = var.https + tags = var.resource_tags + + # enrolls storage account into azure 'managed identities' authentication + identity { + type = "SystemAssigned" + } +} + +resource "azurerm_storage_container" "main" { + count = length(var.container_names) + name = var.container_names[count.index] + storage_account_name = azurerm_storage_account.main.name + container_access_type = "private" +} + +resource "azurerm_storage_share" "main" { + count = length(var.share_names) + name = var.share_names[count.index] + storage_account_name = azurerm_storage_account.main.name + quota = 50 +} + +resource "azurerm_storage_queue" "main" { + count = length(var.queue_names) + name = var.queue_names[count.index] + storage_account_name = azurerm_storage_account.main.name +} diff --git a/infra/modules/providers/azure/storage-account/output.tf b/infra/modules/providers/azure/storage-account/output.tf new file mode 100755 index 0000000000000000000000000000000000000000..88204378e96674de3bbd4cd2a2c6d5555206d6d3 --- /dev/null +++ b/infra/modules/providers/azure/storage-account/output.tf @@ -0,0 +1,88 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +output "id" { + description = "The ID of the storage account." + value = azurerm_storage_account.main.id +} + +output "name" { + description = "The name of the storage account." + value = azurerm_storage_account.main.name +} + +output "primary_access_key" { + description = "The primary access key for the storage account." + value = azurerm_storage_account.main.primary_access_key + sensitive = true +} + +output "containers" { + description = "Map of containers." + value = { + for c in azurerm_storage_container.main : + c.name => { + id = c.id + name = c.name + } + } +} + +output "shares" { + description = "Map of shares." + value = { + for c in azurerm_storage_share.main : + c.name => { + id = c.id + name = c.name + } + } +} + +output "queues" { + description = "Map of queues." + value = { + for c in azurerm_storage_queue.main : + c.name => { + id = c.id + name = c.name + } + } +} + +output "properties" { + description = "Properties of the deployed Storage Account." + value = { + id = azurerm_storage_account.main.id + name = azurerm_storage_account.main.name + primary_access_key = azurerm_storage_account.main.primary_access_key + } + sensitive = true +} + +output "tenant_id" { + description = "The tenant ID for the Service Principal of this storage account." + value = azurerm_storage_account.main.identity.0.tenant_id +} + +output "managed_identities_id" { + description = "The principal ID generated from enabling a Managed Identity with this storage account." + value = azurerm_storage_account.main.identity.0.principal_id +} + +# This output is required for proper integration testing. +output "resource_group_name" { + description = "The resource group name for the Storage Account." + value = data.azurerm_resource_group.main.name +} diff --git a/infra/modules/providers/azure/storage-account/testing/main.tf b/infra/modules/providers/azure/storage-account/testing/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..ca1f9125009f9e61cf32ae8ca2d0a54cc74b424b --- /dev/null +++ b/infra/modules/providers/azure/storage-account/testing/main.tf @@ -0,0 +1,31 @@ +provider "azurerm" { + features {} +} + +module "resource_group" { + source = "../../resource-group" + + name = "osdu-module" + location = "eastus2" +} + +module "storage_account" { + source = "../" + + resource_group_name = module.resource_group.name + name = substr("osdumodule${module.resource_group.random}", 0, 23) + replication_type = "GZRS" + container_names = [ + "osdu-container" + ] + share_names = [ + "osdu-share" + ] + queue_names = [ + "osdu-queue" + ] + + resource_tags = { + environment = "test-environment" + } +} \ No newline at end of file diff --git a/infra/modules/providers/azure/storage-account/testing/unit_test.go b/infra/modules/providers/azure/storage-account/testing/unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9261ed423016100954f400b1b2859b6c228c56a5 --- /dev/null +++ b/infra/modules/providers/azure/storage-account/testing/unit_test.go @@ -0,0 +1,65 @@ +package test + +import ( + "encoding/json" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var name = "storage-" +var count = 7 + +var tfOptions = &terraform.Options{ + TerraformDir: "./", + Upgrade: true, +} + +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} + +func TestTemplate(t *testing.T) { + + expectedResult := asMap(t, `{ + "account_kind" : "StorageV2", + "account_replication_type": "LRS", + "account_tier": "Standard" + }`) + + expectedContainer := asMap(t, `{ + "name" : "osdu-container", + "container_access_type": "private" + }`) + + expectedShare := asMap(t, `{ + "name" : "osdu-share", + "quota": 50 + }`) + + expectedQueue := asMap(t, `{ + "name" : "osdu-queue" + }`) + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tfOptions, + Workspace: name + random.UniqueId(), + PlanAssertions: nil, + ExpectedResourceCount: count, + ExpectedResourceAttributeValues: infratests.ResourceDescription{ + "module.storage_account.azurerm_storage_account.main": expectedResult, + "module.storage_account.azurerm_storage_container.main[0]": expectedContainer, + "module.storage_account.azurerm_storage_share.main[0]": expectedShare, + "module.storage_account.azurerm_storage_queue.main[0]": expectedQueue, + }, + } + + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/modules/providers/azure/storage-account/tests/.env.testing.template b/infra/modules/providers/azure/storage-account/tests/.env.testing.template new file mode 100644 index 0000000000000000000000000000000000000000..ffa9dc5cc1206e37e664c118a94e9917c6b95ccb --- /dev/null +++ b/infra/modules/providers/azure/storage-account/tests/.env.testing.template @@ -0,0 +1,3 @@ +RESOURCE_GROUP_NAME="..." +STORAGE_ACCOUNT_NAME="..." +CONTAINER_NAME="..." diff --git a/infra/modules/providers/azure/storage-account/tests/integration/storage.go b/infra/modules/providers/azure/storage-account/tests/integration/storage.go new file mode 100644 index 0000000000000000000000000000000000000000..484741df1bcabe286ab9fd928b0d9599f3104584 --- /dev/null +++ b/infra/modules/providers/azure/storage-account/tests/integration/storage.go @@ -0,0 +1,47 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package integration + +import ( + "os" + "testing" + + "github.com/microsoft/cobalt/test-harness/infratests" + "github.com/microsoft/cobalt/test-harness/terratest-extensions/modules/azure" + "github.com/stretchr/testify/assert" +) + +var subscription = os.Getenv("ARM_SUBSCRIPTION_ID") + +// InspectStorageAccount - Runs a suite of test assertions to validate the list of containers. +func InspectStorageAccount(storageAccountOutputName string, storageContainerOutputName string, resourceGroupOutputName string) func(t *testing.T, output infratests.TerraformOutput) { + return func(t *testing.T, output infratests.TerraformOutput) { + resourceGroupName := output[resourceGroupOutputName].(string) + storageAccountName := output[storageAccountOutputName].(string) + containerList := output[storageContainerOutputName].(map[string]interface{}) + + expectedContainerList := []string{} + for name := range containerList { + expectedContainerList = append(expectedContainerList, string(name)) + } + + actualContainerList := []string{} + for _, container := range *azure.ListAccountContainers(t, subscription, resourceGroupName, storageAccountName) { + actualContainerList = append(actualContainerList, string(*container.Name)) + } + + assert.ElementsMatch(t, expectedContainerList, actualContainerList, "Container does not exist in the Storage Account") + } +} diff --git a/infra/modules/providers/azure/storage-account/tests/integration/storage_test.go b/infra/modules/providers/azure/storage-account/tests/integration/storage_test.go new file mode 100644 index 0000000000000000000000000000000000000000..64992406b4ad3286e8a259a7d9ad41625c78aa79 --- /dev/null +++ b/infra/modules/providers/azure/storage-account/tests/integration/storage_test.go @@ -0,0 +1,45 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package integration + +import ( + "fmt" + "testing" + + "github.com/microsoft/cobalt/infra/modules/providers/azure/storage-account/tests" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var outputVariableCount = 7 + +func TestServiceDeployment(t *testing.T) { + if tests.ResourceGroupName == "" { + t.Fatal(fmt.Errorf("tests.ResourceGroupName was not specified. Are all the required environment variables set?")) + } + + if tests.ContainerName == "" { + t.Fatal(fmt.Errorf("Container Name was not specified. Are all the required environment variables set?")) + } + + testFixture := infratests.IntegrationTestFixture{ + GoTest: t, + TfOptions: tests.StorageTFOptions, + ExpectedTfOutputCount: outputVariableCount, + TfOutputAssertions: []infratests.TerraformOutputValidation{ + InspectStorageAccount("name", "containers", "resource_group_name"), + }, + } + infratests.RunIntegrationTests(&testFixture) +} diff --git a/infra/modules/providers/azure/storage-account/tests/tf_options.go b/infra/modules/providers/azure/storage-account/tests/tf_options.go new file mode 100644 index 0000000000000000000000000000000000000000..26a5690bf8f4a184d6fd3722fc8194c2f874ce73 --- /dev/null +++ b/infra/modules/providers/azure/storage-account/tests/tf_options.go @@ -0,0 +1,42 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tests + +import ( + "os" + + "github.com/gruntwork-io/terratest/modules/terraform" +) + +// StorageAccount - The Storage Account Name +var StorageAccount = os.Getenv("STORAGE_ACCOUNT_NAME") + +// ContainerName - The Container Name +var ContainerName = os.Getenv("CONTAINER_NAME") + +// ResourceGroupName - The Resource Group Name +var ResourceGroupName = os.Getenv("RESOURCE_GROUP_NAME") + +// StorageTFOptions common terraform options used for unit and integration testing +var StorageTFOptions = &terraform.Options{ + TerraformDir: "../../", + Vars: map[string]interface{}{ + "resource_group_name": ResourceGroupName, + "name": StorageAccount, + "container_names": []interface{}{ + ContainerName, + }, + }, +} diff --git a/infra/modules/providers/azure/storage-account/tests/unit/storage_deployment_unit_test.go b/infra/modules/providers/azure/storage-account/tests/unit/storage_deployment_unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7d0d0bcb4db12c64b8996137df509962302262dc --- /dev/null +++ b/infra/modules/providers/azure/storage-account/tests/unit/storage_deployment_unit_test.go @@ -0,0 +1,60 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package unit + +import ( + "encoding/json" + "testing" + + "github.com/microsoft/cobalt/infra/modules/providers/azure/storage-account/tests" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var resourceCount = 2 + +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} + +func TestStorageDeployment_Unit(t *testing.T) { + + expectedResult := asMap(t, `{ + "account_kind": "StorageV2", + "account_replication_type": "LRS", + "account_tier": "Standard", + "account_encryption_source": "Microsoft.Storage" + }`) + + expectedContainerResult := asMap(t, `{ + "container_access_type": "private", + "name": "`+tests.ContainerName+`" + }`) + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tests.StorageTFOptions, + ExpectedResourceCount: resourceCount, + ExpectedResourceAttributeValues: infratests.ResourceDescription{ + "azurerm_storage_account.main": expectedResult, + "azurerm_storage_container.main[0]": expectedContainerResult, + }, + } + + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/modules/providers/azure/storage-account/variables.tf b/infra/modules/providers/azure/storage-account/variables.tf new file mode 100755 index 0000000000000000000000000000000000000000..eabe6555be2f88f5152f5c369ff9518f95f051ab --- /dev/null +++ b/infra/modules/providers/azure/storage-account/variables.tf @@ -0,0 +1,81 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +# Naming Items (required) + +variable "name" { + description = "The name of the storage account service." + type = string +} + +variable "container_names" { + description = "The list of storage container names to create. Names must be unique per storage account." + type = list(string) +} + +variable "share_names" { + description = "The list of storage file share names to create. Names must be unique per storage account." + type = list(string) + default = [] +} + +variable "queue_names" { + description = "The list of storage queue names to create. Names must be unique per storage account." + type = list(string) + default = [] +} + +variable "resource_group_name" { + description = "The name of the resource group." + type = string +} + + +# Tier Items (optional) + +variable "performance_tier" { + description = "Determines the level of performance required." + type = string + default = "Standard" +} + +variable "kind" { + description = "Storage account types that determine available features and pricing of Azure Storage. Use StorageV2 when possible." + type = string + default = "StorageV2" +} + +variable "replication_type" { + description = "Defines the type of replication to use for this storage account. Valid options are LRS*, GRS, RAGRS and ZRS." + type = string + default = "LRS" +} + + +# Configuration Items (optional) + +variable "https" { + description = "Boolean flag which forces HTTPS in order to ensure secure connections." + type = bool + default = true +} + + +# General Items (optional) + +variable "resource_tags" { + description = "Map of tags to apply to taggable resources in this module. By default the taggable resources are tagged with the name defined above and this map is merged in" + type = map(string) + default = {} +} diff --git a/infra/templates/osdu-r3-mvp/README.md b/infra/templates/osdu-r3-mvp/README.md new file mode 100644 index 0000000000000000000000000000000000000000..7a582e9064e43df2fa5a6ee0264dc8a96938f78d --- /dev/null +++ b/infra/templates/osdu-r3-mvp/README.md @@ -0,0 +1,144 @@ +# Azure OSDU R3 MVP Architecture + +The `osdu` - R3 MVP Architecture solution template is intended to provision Managed Kubernetes resources like AKS and other core OSDU cloud managed services like Cosmos, Blob Storage and Keyvault. + +## Cloud Resource Architecture + +![Architecture](./docs/images/architecture.png "Architecture") + + +## Cost + +Azure environment cost ballpark [estimate](). This is subject to change and is driven from the resource pricing tiers configured when the template is deployed. + + +## Prerequisites + +1. Azure Subscription +1. An available Service Principal capable of creating resources. +1. Terraform and Go are locally installed. +1. Requires the use of [direnv](https://direnv.net/). +1. Install the required common tools (kubectl, helm, and terraform). Note: Currently uses [Terraform 0.12.29](https://releases.hashicorp.com/terraform/0.12.29/). + + +### Install the required tooling + +This document assumes one is running a current version of Ubuntu. Windows users can install the Ubuntu Terminal from the Microsoft Store. The Ubuntu Terminal enables Linux command-line utilities, including bash, ssh, and git that will be useful for the following deployment. _Note: You will need the Windows Subsystem for Linux installed to use the Ubuntu Terminal on Windows_. + + +### Install the Azure CLI + +For information specific to your operating system, see the [Azure CLI install guide](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest). You can also use [this script](https://github.com/microsoft/bedrock/blob/master/tools/prereqs/setup_azure_cli.sh) if running on a Unix based machine. + + +## Provision the Common Resources + +The script ./common_prepare.sh will conveniently setup the common things that are necessary to install infrastructure. +- Run the script with your subscription ID as the first argument and an optional unique string as the second argument. +- Note the files (azure-aks-gitops-ssh-key and azure-aks-node-ssh-key.pub) that have appeared in the .ssh directory. +You will need these in a later step. +- Note the output of this script creates a .envrc file used with `direnv` to automatically setup the required environment for installation. + +> UNIQUE is used to generate uniqueness across all of azure. _(3-5 characters)_ + +```bash +export ARM_SUBSCRIPTION_ID= +export UNIQUE= + +./common_prepare.sh +``` + +This results in 2 service principals being created that need an AD Admin to `grant admin consent` on. + +1. osdu-mvp-{UNIQUE}-terraform +2. osdu-mvp-{UNIQUE}-principal + +__Installed Common Resources__ + +1. Resource Group +2. Storage Account +3. Key Vault +4. A principal to be used for Terraform _(Requires Grant Admin Approval)_ +5. A principal to be used for the OSDU environment. +6. An application to be used for the OSDU environment. _(future)_ +7. An application to be used for negative integration testing. + +>Note: 2 Users are required to be created in AD for integration testing purposes manually and values stored in this Common Key Vault. + + +## Elastic Search Setup + +Infrastructure assumes bring your own Elastic Search Instance at a version of 6.8.x and access information must be stored in the Common KeyVault. + +```bash +ENDPOINT="" +USERNAME="" +PASSWORD="" +az keyvault secret set --vault-name $COMMON_VAULT --name "elastic-endpoint-ado-demo" --value $ENDPOINT +az keyvault secret set --vault-name $COMMON_VAULT --name "elastic-username-ado-demo" --value $USERNAME +az keyvault secret set --vault-name $COMMON_VAULT --name "elastic-password-ado-demo" --value $PASSWORD + +cat >> .envrc_${UNIQUE} << EOF + +# https://cloud.elastic.co +# ------------------------------------------------------------------------------------------------------ +export TF_VAR_elasticsearch_endpoint="$(az keyvault secret show --vault-name $COMMON_VAULT --id https://$COMMON_VAULT.vault.azure.net/secrets/elastic-endpoint-ado-demo --query value -otsv)" +export TF_VAR_elasticsearch_username="$(az keyvault secret show --vault-name $COMMON_VAULT --id https://$COMMON_VAULT.vault.azure.net/secrets/elastic-username-ado-demo --query value -otsv)" +export TF_VAR_elasticsearch_password="$(az keyvault secret show --vault-name $COMMON_VAULT --id https://$COMMON_VAULT.vault.azure.net/secrets/elastic-password-ado-demo --query value -otsv)" + +EOF + +cp .envrc_${UNIQUE} .envrc +``` + + +## Create the Flux Manifest Repository + +[Create an empty git repository](https://docs.microsoft.com/en-us/azure/devops/repos/git/create-new-repo?view=azure-devops) with a name that clearly signals that the repo is used for the Flux manifests. For example `k8-gitops-manifests`. + +Flux requires that the git repository have at least one commit. Initialize the repo with an empty commit. + +```bash +git commit --allow-empty -m "Initializing the Flux Manifest Repository" +``` + + +## Configure Key Access in Manifest Repository + +The public key of the [RSA key pair](#create-an-rsa-key-pair-for-a-deploy-key-for-the-flux-repository) previously created needs to be added as a deploy key. Note: _If you do not own the repository, you will have to fork it before proceeding_. + +Use the contents of the Secret as shown above. + +Next, in your Azure DevOPS Project, follow these [steps](https://docs.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops&tabs=current-page#step-2--add-the-public-key-to-azure-devops-servicestfs) to add your public SSH key to your ADO environment. + + +## Manual Deployment Process +Follow these steps if you wish to deploy manually without pipeline support. + +__Deploy Central Resources__ + +Follow the directions in the [`central_resources`](./central_resources/README.md) environment. + +__Deploy Data Resources__ + +Follow the directions in the [`data_resources`](./data_partition/README.md) environment. + +__Deploy Service Resources__ + +Follow the directions in the [`service_resources`](./service_resources/README.md) environment. + +## License + +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/infra/templates/osdu-r3-mvp/central_resources/README.md b/infra/templates/osdu-r3-mvp/central_resources/README.md new file mode 100644 index 0000000000000000000000000000000000000000..a2a4bc432705b946517979676f1841b0b911474d --- /dev/null +++ b/infra/templates/osdu-r3-mvp/central_resources/README.md @@ -0,0 +1,146 @@ +# Azure OSDU MVC - Central Resources Configuration + +The `osdu` - `central_resources` environment template is intended to provision to Azure resources for OSDU which are typically central to the architecture and can't be removed without destroying the entire OSDU deployment. + +__PreRequisites__ + +> These are typically performed by the `common_prepare.sh` scripts. + +Requires the use of [direnv](https://direnv.net/) for environment variable management. + +Requires a preexisting Service Principal to be created to be used for this OSDU Environment. + +```bash +ENV=$USER # This is helpful to set to your expected OSDU environment name. +NAME="osdu-mvp-$ENV-principal" + +# Create a Service Principal +az ad sp create-for-rbac --name $NAME --skip-assignment -ojson + +# Result +{ + "appId": "", # -> Use this for TF_VAR_principal_appId + "displayName": "", # -> Use this for TF_VAR_principal_name + "name": "http://", + "password": "****************", # -> Use this for TF_VAR_principal_password + "tenant": "" +} + +# Retrieve the AD Service Pricipal ID +az ad sp list --display-name $NAME --query [].objectId -ojson + +# Result +[ + "" # -> Use this for TF_VAR_principal_objectId +] + + +# Assign API Permissions +# Microsoft Graph -- Application Permissions -- Directory.Read.All ** GRANT ADMIN-CONSENT +adObjectId=$(az ad app list --display-name $NAME --query [].objectId -otsv) +graphId=$(az ad sp list --query "[?appDisplayName=='Microsoft Graph'].appId | [0]" --all -otsv) +directoryReadAll=$(az ad sp show --id $graphId --query "appRoles[?value=='Directory.Read.All'].id | [0]" -otsv)=Role + +az ad app permission add --id $adObjectId --api $graphId --api-permissions $directoryReadAll + +# Grant Admin Consent +# ** REQUIRES ADMIN AD ACCESS ** +az ad app permission admin-consent --id $appId +``` + +Set up your local environment variables + +*Note: environment variables are automatically sourced by direnv* + +Required Environment Variables (.envrc) +```bash +export ARM_TENANT_ID="" +export ARM_SUBSCRIPTION_ID="" + +# Terraform-Principal +export ARM_CLIENT_ID="" +export ARM_CLIENT_SECRET="" + +# Terraform State Storage Account Key +export TF_VAR_remote_state_account="" +export TF_VAR_remote_state_container="" +export ARM_ACCESS_KEY="" + +# Instance Variables +export TF_VAR_resource_group_location="centralus" +``` + +Navigate to the `terraform.tfvars` terraform file. Here's a sample of the terraform.tfvars file for this template. + +```HCL +prefix = "osdu-mvp" + +resource_tags = { + contact = "" +} +``` + +__Manually Provision__ + +Execute the following commands to set up your terraform workspace. + +```bash +# This configures terraform to leverage a remote backend that will help you and your +# team keep consistent state +terraform init -backend-config "storage_account_name=${TF_VAR_remote_state_account}" -backend-config "container_name=${TF_VAR_remote_state_container}" + +# This command configures terraform to use a workspace unique to you. This allows you to work +# without stepping over your teammate's deployments +TF_WORKSPACE="${UNIQUE}-cr" +terraform workspace new $TF_WORKSPACE || terraform workspace select $TF_WORKSPACE +``` + +Execute the following commands to orchestrate a deployment. + +```bash +# See what terraform will try to deploy without actually deploying +terraform plan + +# Execute a deployment +terraform apply +``` + +Optionally execute the following command to teardown your deployment and delete your resources. + +```bash +# Destroy resources and tear down deployment. Only do this if you want to destroy your deployment. +terraform destroy +``` + +## Testing + +Please confirm that you've completed the `terraform apply` step before running the integration tests as we're validating the active terraform workspace. + +Unit tests can be run using the following command: + +``` +go test -v $(go list ./... | grep "unit") +``` + +Integration tests can be run using the following command: + +``` +go test -v $(go list ./... | grep "integration") +``` + + +## License + +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/infra/templates/osdu-r3-mvp/central_resources/diagnostics.tf b/infra/templates/osdu-r3-mvp/central_resources/diagnostics.tf new file mode 100644 index 0000000000000000000000000000000000000000..23ea5242a38bb9480d704a4caa18bab25e0807a0 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/central_resources/diagnostics.tf @@ -0,0 +1,86 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +/* +.Synopsis + Terraform Diagnostics Control +.DESCRIPTION + This file holds diagnostics settings. +*/ + +#------------------------------- +# Key Vault +#------------------------------- +resource "azurerm_monitor_diagnostic_setting" "kv_diagnostics" { + name = "kv_diagnostics" + target_resource_id = module.keyvault.keyvault_id + log_analytics_workspace_id = module.log_analytics.id + + log { + category = "AuditEvent" + + retention_policy { + enabled = false + } + } + + metric { + category = "AllMetrics" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } +} + + +#------------------------------- +# Container Registry +#------------------------------- +resource "azurerm_monitor_diagnostic_setting" "acr_diagnostics" { + name = "acr_diagnostics" + target_resource_id = module.container_registry.container_registry_id + log_analytics_workspace_id = module.log_analytics.id + + log { + category = "ContainerRegistryRepositoryEvents" + enabled = true + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + log { + category = "ContainerRegistryLoginEvents" + enabled = true + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + metric { + category = "AllMetrics" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } +} \ No newline at end of file diff --git a/infra/templates/osdu-r3-mvp/central_resources/main.tf b/infra/templates/osdu-r3-mvp/central_resources/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..7250ebeca946aa258aa127aaa550b8aeb0c727ac --- /dev/null +++ b/infra/templates/osdu-r3-mvp/central_resources/main.tf @@ -0,0 +1,333 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +/* +.Synopsis + Terraform Main Control +.DESCRIPTION + This file holds the main control. +*/ + +// *** WARNING **** +// This template includes locks and won't delete by destroy if locks aren't removed first. +// Lock: KeyVault +// Lock: Container Registry +// *** WARNING **** + +terraform { + required_version = ">= 0.12" + backend "azurerm" { + key = "terraform.tfstate" + } +} + +#------------------------------- +# Providers +#------------------------------- +provider "azurerm" { + version = "=2.29.0" + features {} +} + +provider "azuread" { + version = "=1.0.0" +} + +provider "random" { + version = "~>2.2" +} + + + +#------------------------------- +# Private Variables +#------------------------------- +locals { + // sanitize names + prefix = replace(trimspace(lower(var.prefix)), "_", "-") + workspace = replace(trimspace(lower(terraform.workspace)), "-", "") + suffix = var.randomization_level > 0 ? "-${random_string.workspace_scope.result}" : "" + + // base prefix for resources, prefix constraints documented here: https://docs.microsoft.com/en-us/azure/architecture/best-practices/naming-conventions + base_name = length(local.prefix) > 0 ? "${local.prefix}-${local.workspace}${local.suffix}" : "${local.workspace}${local.suffix}" + base_name_21 = length(local.base_name) < 22 ? local.base_name : "${substr(local.base_name, 0, 21 - length(local.suffix))}${local.suffix}" + base_name_46 = length(local.base_name) < 47 ? local.base_name : "${substr(local.base_name, 0, 46 - length(local.suffix))}${local.suffix}" + base_name_60 = length(local.base_name) < 61 ? local.base_name : "${substr(local.base_name, 0, 60 - length(local.suffix))}${local.suffix}" + base_name_76 = length(local.base_name) < 77 ? local.base_name : "${substr(local.base_name, 0, 76 - length(local.suffix))}${local.suffix}" + base_name_83 = length(local.base_name) < 84 ? local.base_name : "${substr(local.base_name, 0, 83 - length(local.suffix))}${local.suffix}" + + resource_group_name = format("%s-%s-%s-rg", var.prefix, local.workspace, random_string.workspace_scope.result) + retention_policy = var.log_retention_days == 0 ? false : true + + kv_name = "${local.base_name_21}-kv" + storage_name = "${replace(local.base_name_21, "-", "")}tbl" + container_registry_name = "${replace(local.base_name_21, "-", "")}cr" + osdupod_identity_name = "${local.base_name}-osdu-identity" + ai_name = "${local.base_name}-ai" + logs_name = "${local.base_name}-logs" + ad_app_name = "${local.base_name}-app" + + rbac_contributor_scopes = concat( + [module.container_registry.container_registry_id], + [module.keyvault.keyvault_id] + ) + role = "Contributor" + rbac_principals = [ + azurerm_user_assigned_identity.osduidentity.principal_id, + module.service_principal.id + ] +} + + + +#------------------------------- +# Common Resources +#------------------------------- +data "azurerm_client_config" "current" {} + +resource "random_string" "workspace_scope" { + keepers = { + # Generate a new id each time we switch to a new workspace or app id + ws_name = replace(trimspace(lower(terraform.workspace)), "-", "") + prefix = replace(trimspace(lower(var.prefix)), "_", "-") + } + + length = max(1, var.randomization_level) // error for zero-length + special = false + upper = false +} + +#------------------------------- +# Resource Group +#------------------------------- +resource "azurerm_resource_group" "main" { + name = local.resource_group_name + location = var.resource_group_location + tags = var.resource_tags + + lifecycle { + ignore_changes = [tags] + } +} + + + +#------------------------------- +# Key Vault +#------------------------------- +module "keyvault" { + source = "../../../modules/providers/azure/keyvault" + + keyvault_name = local.kv_name + resource_group_name = azurerm_resource_group.main.name + secrets = { + app-dev-sp-tenant-id = data.azurerm_client_config.current.tenant_id + } + + resource_tags = var.resource_tags +} + +module "keyvault_policy" { + source = "../../../modules/providers/azure/keyvault-policy" + vault_id = module.keyvault.keyvault_id + tenant_id = data.azurerm_client_config.current.tenant_id + object_ids = [ + azurerm_user_assigned_identity.osduidentity.principal_id, + module.service_principal.id + ] + key_permissions = ["get"] + certificate_permissions = ["get"] + secret_permissions = ["get"] +} + +resource "azurerm_role_assignment" "kv_roles" { + count = length(local.rbac_principals) + + role_definition_name = "Reader" + principal_id = local.rbac_principals[count.index] + scope = module.keyvault.keyvault_id +} + +#------------------------------- +# Storage +#------------------------------- +module "storage_account" { + source = "../../../modules/providers/azure/storage-account" + + name = local.storage_name + resource_group_name = azurerm_resource_group.main.name + container_names = [] + kind = "StorageV2" + replication_type = var.storage_replication_type + + resource_tags = var.resource_tags +} + +// Add Access Control to Principal +resource "azurerm_role_assignment" "storage_access" { + count = length(local.rbac_principals) + + role_definition_name = local.role + principal_id = local.rbac_principals[count.index] + scope = module.storage_account.id +} + + + +#------------------------------- +# Container Registry +#------------------------------- +module "container_registry" { + source = "../../../modules/providers/azure/container-registry" + + container_registry_name = local.container_registry_name + resource_group_name = azurerm_resource_group.main.name + + container_registry_sku = var.container_registry_sku + container_registry_admin_enabled = false + + resource_tags = var.resource_tags +} + + + +#------------------------------- +# Application Insights +#------------------------------- +module "app_insights" { + source = "../../../modules/providers/azure/app-insights" + + appinsights_name = local.ai_name + service_plan_resource_group_name = azurerm_resource_group.main.name + appinsights_application_type = "other" + + resource_tags = var.resource_tags +} + + + +#------------------------------- +# Log Analytics +#------------------------------- +module "log_analytics" { + source = "../../../modules/providers/azure/log-analytics" + + name = local.logs_name + resource_group_name = azurerm_resource_group.main.name + + solutions = [ + { + solution_name = "ContainerInsights", + publisher = "Microsoft", + product = "OMSGallery/ContainerInsights", + }, + { + solution_name = "KeyVaultAnalytics", + publisher = "Microsoft", + product = "OMSGallery/KeyVaultAnalytics", + }, + { + solution_name = "AzureAppGatewayAnalytics", + publisher = "Microsoft", + product = "OMSGallery/AzureAppGatewayAnalytics", + } + ] + + resource_tags = var.resource_tags +} + + + +#------------------------------- +# AD Principal and Applications +#------------------------------- +module "service_principal" { + source = "../../../modules/providers/azure/service-principal" + + name = var.principal_name + scopes = local.rbac_contributor_scopes + role = "Contributor" + + create_for_rbac = false + object_id = var.principal_objectId + + principal = { + name = var.principal_name + appId = var.principal_appId + password = var.principal_password + } +} + + +module "ad_application" { + source = "../../../modules/providers/azure/ad-application" + name = local.ad_app_name + oauth2_allow_implicit_flow = true + + reply_urls = [ + "http://localhost:8080", + "http://localhost:8080/auth/callback" + ] + + api_permissions = [ + { + name = "Microsoft Graph" + oauth2_permissions = [ + "User.Read" + ] + } + ] +} + + + +#------------------------------- +# OSDU Identity +#------------------------------- +// Identity for OSDU Pod Identity +resource "azurerm_user_assigned_identity" "osduidentity" { + name = local.osdupod_identity_name + resource_group_name = azurerm_resource_group.main.name + location = azurerm_resource_group.main.location + + tags = var.resource_tags +} + + + +#------------------------------- +# Locks +#------------------------------- + +// Lock the KV +resource "azurerm_management_lock" "kv_lock" { + name = "osdu_cr_kv_lock" + scope = module.keyvault.keyvault_id + lock_level = "CanNotDelete" +} + +// Lock the Storage +resource "azurerm_management_lock" "sa_lock" { + name = "osdu_tbl_sa_lock" + scope = module.storage_account.id + lock_level = "CanNotDelete" +} + +// Lock the Container Registry +resource "azurerm_management_lock" "acr_lock" { + name = "osdu_acr_lock" + scope = module.container_registry.container_registry_id + lock_level = "CanNotDelete" +} diff --git a/infra/templates/osdu-r3-mvp/central_resources/output.tf b/infra/templates/osdu-r3-mvp/central_resources/output.tf new file mode 100644 index 0000000000000000000000000000000000000000..3a2b0dbe15e192956e796adc7660c0868113682e --- /dev/null +++ b/infra/templates/osdu-r3-mvp/central_resources/output.tf @@ -0,0 +1,72 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +.Synopsis + Terraform Output Configuration +.DESCRIPTION + This file holds the Output Configuration +*/ + +#------------------------------- +# Output Variables +#------------------------------- +output "central_resource_group_name" { + value = azurerm_resource_group.main.name +} + +output "container_registry_id" { + description = "The resource identifier of the container registry." + value = module.container_registry.container_registry_id +} + +output "container_registry_name" { + description = "The name of the container registry." + value = module.container_registry.container_registry_name +} + +output "keyvault_id" { + description = "The resource id for Key Vault" + value = module.keyvault.keyvault_id +} + +output "keyvault_name" { + description = "The name for Key Vault" + value = module.keyvault.keyvault_name +} + +output "log_analytics_id" { + description = "The resource id for Log Analytics" + value = module.log_analytics.id +} + +output "osdu_identity_id" { + description = "The resource id for the User Assigned Identity" + value = azurerm_user_assigned_identity.osduidentity.id +} + +output "osdu_identity_principal_id" { + description = "The principal id for the User Assigned Identity" + value = azurerm_user_assigned_identity.osduidentity.principal_id +} + +output "osdu_identity_client_id" { + description = "The client id for the User Assigned Identity" + value = azurerm_user_assigned_identity.osduidentity.client_id +} + +output "principal_objectId" { + description = "The service principal application object id" + value = var.principal_objectId +} diff --git a/infra/templates/osdu-r3-mvp/central_resources/secrets.tf b/infra/templates/osdu-r3-mvp/central_resources/secrets.tf new file mode 100644 index 0000000000000000000000000000000000000000..757cd58c0b3ad819cea39d59ad7c28b1f50e8e08 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/central_resources/secrets.tf @@ -0,0 +1,150 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +/* +.Synopsis + Terraform Security Control +.DESCRIPTION + This file holds security settings. +*/ + + +#------------------------------- +# Private Variables +#------------------------------- +locals { + storage_account_name = format("tbl-storage") + storage_key_name = format("%s-key", local.storage_account_name) + + logs_id_name = "log-workspace-id" + logs_key_name = "log-workspace-key" +} + + +#------------------------------- +# Misc +#------------------------------- +resource "azurerm_key_vault_secret" "base_name_cr" { + name = "base-name-cr" + value = local.base_name_60 + key_vault_id = module.keyvault.keyvault_id +} + +resource "azurerm_key_vault_secret" "tenant_id" { + name = "tenant-id" + value = data.azurerm_client_config.current.tenant_id + key_vault_id = module.keyvault.keyvault_id +} + +resource "azurerm_key_vault_secret" "subscription_id" { + name = "subscription-id" + value = data.azurerm_client_config.current.subscription_id + key_vault_id = module.keyvault.keyvault_id +} + + +#------------------------------- +# Container Registry +#------------------------------- +resource "azurerm_key_vault_secret" "container_registry_name" { + name = "container-registry" + value = module.container_registry.container_registry_name + key_vault_id = module.keyvault.keyvault_id +} + + +#------------------------------- +# Storage +#------------------------------- +resource "azurerm_key_vault_secret" "storage_name" { + name = local.storage_account_name + value = module.storage_account.name + key_vault_id = module.keyvault.keyvault_id +} + +resource "azurerm_key_vault_secret" "storage_key" { + name = local.storage_key_name + value = module.storage_account.primary_access_key + key_vault_id = module.keyvault.keyvault_id +} + + + +#------------------------------- +# Application Insights +#------------------------------- +resource "azurerm_key_vault_secret" "insights" { + name = "appinsights-key" + value = module.app_insights.app_insights_instrumentation_key + key_vault_id = module.keyvault.keyvault_id +} + + + +#------------------------------- +# Log Analytics +#------------------------------- +resource "azurerm_key_vault_secret" "workspace_id" { + name = local.logs_id_name + value = module.log_analytics.log_workspace_id + key_vault_id = module.keyvault.keyvault_id +} + +resource "azurerm_key_vault_secret" "workspace_key" { + name = local.logs_key_name + value = module.log_analytics.log_workspace_key + key_vault_id = module.keyvault.keyvault_id +} + + +#------------------------------- +# AD Principal and Applications +#------------------------------- +resource "azurerm_key_vault_secret" "principal_id" { + name = "app-dev-sp-username" + value = module.service_principal.client_id + key_vault_id = module.keyvault.keyvault_id +} + +resource "azurerm_key_vault_secret" "principal_secret" { + name = "app-dev-sp-password" + value = module.service_principal.client_secret + key_vault_id = module.keyvault.keyvault_id +} + +resource "azurerm_key_vault_secret" "principal_object_id" { + name = "app-dev-sp-id" + value = module.service_principal.id + key_vault_id = module.keyvault.keyvault_id +} + +// Add Application Information to KV +resource "azurerm_key_vault_secret" "application_id" { + name = "aad-client-id" + value = module.ad_application.id + key_vault_id = module.keyvault.keyvault_id +} + + +#------------------------------- +# OSDU Identity +#------------------------------- + +// Add Application Information to KV +resource "azurerm_key_vault_secret" "identity_id" { + name = "osdu-identity-id" + value = azurerm_user_assigned_identity.osduidentity.client_id + key_vault_id = module.keyvault.keyvault_id +} \ No newline at end of file diff --git a/infra/templates/osdu-r3-mvp/central_resources/terraform.tfvars b/infra/templates/osdu-r3-mvp/central_resources/terraform.tfvars new file mode 100644 index 0000000000000000000000000000000000000000..4eff444b4295a75174ace3ddd201ad47eb13354e --- /dev/null +++ b/infra/templates/osdu-r3-mvp/central_resources/terraform.tfvars @@ -0,0 +1,29 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +.Synopsis + Terraform Variable Configuration +.DESCRIPTION + This file holds the Default Variable Configuration +*/ + +prefix = "osdu-mvp" + +resource_tags = { + contact = "pipeline" +} + +# Storage Settings +storage_replication_type = "LRS" diff --git a/infra/templates/osdu-r3-mvp/central_resources/tests/integration/integration_test.go b/infra/templates/osdu-r3-mvp/central_resources/tests/integration/integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..496e4635e4a0fc968afa5c48ac992f35b65094a8 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/central_resources/tests/integration/integration_test.go @@ -0,0 +1,47 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "os" + "testing" + + "github.com/gruntwork-io/terratest/modules/terraform" + containerRegistryIntegTests "github.com/microsoft/cobalt/infra/modules/providers/azure/container-registry/tests/integration" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var subscription = os.Getenv("ARM_SUBSCRIPTION_ID") +var tfOptions = &terraform.Options{ + TerraformDir: "../../", + BackendConfig: map[string]interface{}{ + "storage_account_name": os.Getenv("TF_VAR_remote_state_account"), + "container_name": os.Getenv("TF_VAR_remote_state_container"), + }, +} + +// Runs a suite of test assertions to validate that a provisioned data source environment +// is fully functional. +func TestDataEnvironment(t *testing.T) { + testFixture := infratests.IntegrationTestFixture{ + GoTest: t, + TfOptions: tfOptions, + ExpectedTfOutputCount: 10, + TfOutputAssertions: []infratests.TerraformOutputValidation{ + containerRegistryIntegTests.InspectContainerRegistryOutputs(subscription, "central_resource_group_name", "container_registry_name"), + }, + } + infratests.RunIntegrationTests(&testFixture) +} diff --git a/infra/templates/osdu-r3-mvp/central_resources/tests/unit/common.go b/infra/templates/osdu-r3-mvp/central_resources/tests/unit/common.go new file mode 100644 index 0000000000000000000000000000000000000000..1904c24e3301d66572e43a03a7ec973c48821c81 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/central_resources/tests/unit/common.go @@ -0,0 +1,36 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "encoding/json" + "github.com/gruntwork-io/terratest/modules/random" + "strings" + "testing" +) + +// these are useful values used in many test +var region = "centralus" +var prefix = "osdu-testing" + strings.ToLower(random.UniqueId()) +var workspace = "osdu-testing-" + strings.ToLower(random.UniqueId()) + +// helper function to parse blocks of JSON into a generic Go map +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} diff --git a/infra/templates/osdu-r3-mvp/central_resources/tests/unit/keyvault.go b/infra/templates/osdu-r3-mvp/central_resources/tests/unit/keyvault.go new file mode 100644 index 0000000000000000000000000000000000000000..22e9cd4d6199c3a155a02c397a75d21b83a4fe1b --- /dev/null +++ b/infra/templates/osdu-r3-mvp/central_resources/tests/unit/keyvault.go @@ -0,0 +1,77 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "fmt" + "sort" + "testing" + + "github.com/microsoft/cobalt/test-harness/infratests" +) + +func appendKeyVaultTests(t *testing.T, description infratests.ResourceDescription) { + kvBasicExpectations(t, description) + kvAccessPolicyExpectations(t, description) + kvSecretExpectations(t, description) +} + +func kvBasicExpectations(t *testing.T, description infratests.ResourceDescription) { + k1 := "module.keyvault.azurerm_key_vault.keyvault" + e1 := asMap(t, `{ + "sku_name": "standard" + }`) + description[k1] = e1 +} + +func kvSecretExpectations(t *testing.T, description infratests.ResourceDescription) { + expectedKeys := []string{ + "appinsights-key", + } + + // The unit test fixture will expect these secrets to match ordinals returned by terraform, which sorts by name + sort.Strings(expectedKeys) + for index, value := range expectedKeys { + key := fmt.Sprintf("module.keyvault_secrets.azurerm_key_vault_secret.secret[%v]", index) + val := asMap(t, fmt.Sprintf(`{"name": "%s"}`, value)) + description[key] = val + } +} + +func kvAccessPolicyExpectations(t *testing.T, description infratests.ResourceDescription) { + e1 := asMap(t, `{ + "certificate_permissions": ["update", "delete", "get", "list"], + "secret_permissions": ["set", "delete", "get", "list"], + "key_permissions": ["update", "delete", "get", "list"] + }`) + k1 := "module.app_management_service_principal_keyvault_access_policy.azurerm_key_vault_access_policy.keyvault[0]" + description[k1] = e1 + + e2 := asMap(t, `{ + "certificate_permissions": ["create", "delete", "get", "list"], + "secret_permissions": ["set", "delete", "get", "list"], + "key_permissions": ["create", "delete", "get"] + }`) + k2 := "module.keyvault.module.deployment_service_principal_keyvault_access_policies.azurerm_key_vault_access_policy.keyvault[0]" + description[k2] = e2 + + e3 := asMap(t, `{ + "certificate_permissions": ["get", "list"], + "secret_permissions": ["get", "list"], + "key_permissions": ["get", "list"] + }`) + k3 := "module.authn_app_service_keyvault_access_policy.azurerm_key_vault_access_policy.keyvault[0]" + description[k3] = e3 +} diff --git a/infra/templates/osdu-r3-mvp/central_resources/tests/unit/unit_test.go b/infra/templates/osdu-r3-mvp/central_resources/tests/unit/unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..400407b62d19993539df652e5bfbc303441df581 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/central_resources/tests/unit/unit_test.go @@ -0,0 +1,57 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "os" + "testing" + + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var tfOptions = &terraform.Options{ + TerraformDir: "../../", + Upgrade: true, + Vars: map[string]interface{}{ + "resource_group_location": region, + "prefix": prefix, + }, + BackendConfig: map[string]interface{}{ + "storage_account_name": os.Getenv("TF_VAR_remote_state_account"), + "container_name": os.Getenv("TF_VAR_remote_state_container"), + }, +} + +func TestTemplate(t *testing.T) { + expectedAppDevResourceGroup := asMap(t, `{ + "location": "`+region+`" + }`) + + resourceDescription := infratests.ResourceDescription{ + "azurerm_resource_group.main": expectedAppDevResourceGroup, + } + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tfOptions, + Workspace: workspace, + PlanAssertions: nil, + ExpectedResourceCount: 48, + ExpectedResourceAttributeValues: resourceDescription, + } + + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/templates/osdu-r3-mvp/central_resources/variables.tf b/infra/templates/osdu-r3-mvp/central_resources/variables.tf new file mode 100644 index 0000000000000000000000000000000000000000..5db8e9ee6e3d08b7ab6a7720ec923eb71159fd4b --- /dev/null +++ b/infra/templates/osdu-r3-mvp/central_resources/variables.tf @@ -0,0 +1,84 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +.Synopsis + Terraform Variable Configuration +.DESCRIPTION + This file holds the Variable Configuration +*/ + + +#------------------------------- +# Application Variables +#------------------------------- +variable "prefix" { + description = "(Required) An identifier used to construct the names of all resources in this template." + type = string +} + +variable "randomization_level" { + description = "Number of additional random characters to include in resource names to insulate against unexpected resource name collisions." + type = number + default = 4 +} + +variable "resource_group_location" { + description = "The Azure region where container registry resources in this template should be created." + type = string +} + +variable "log_retention_days" { + description = "Number of days to retain logs." + type = number + default = 30 +} + +variable "container_registry_sku" { + description = "(Optional) The SKU name of the the container registry. Possible values are Basic, Standard and Premium." + type = string + default = "Standard" +} + +variable "resource_tags" { + description = "Map of tags to apply to this template." + type = map(string) + default = {} +} + +variable "principal_name" { + description = "Existing Service Principal Name." + type = string +} + +variable "principal_password" { + description = "Existing Service Principal Password." + type = string +} + +variable "principal_appId" { + description = "Existing Service Principal AppId." + type = string +} + +variable "principal_objectId" { + description = "Existing Service Principal ObjectId." + type = string +} + +variable "storage_replication_type" { + description = "Defines the type of replication to use for this storage account. Valid options are LRS*, GRS, RAGRS and ZRS." + type = string + default = "LRS" +} \ No newline at end of file diff --git a/infra/templates/osdu-r3-mvp/common_keys.sh b/infra/templates/osdu-r3-mvp/common_keys.sh new file mode 100755 index 0000000000000000000000000000000000000000..f07165b624e412b17d4305cc1f73977ec88d13f4 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/common_keys.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# +# Purpose: Show all the Keys in a Key Vault. +# Usage: +# common_keys.sh + + +############################### +## ARGUMENT INPUT ## +############################### + +if [ ! -z $1 ]; then COMMON_VAULT=$1; fi +if [ -z $COMMON_VAULT ]; then + tput setaf 1; echo 'ERROR: COMMON_VAULT not provided' ; tput sgr0 + usage; +fi + +############################### +## EXECUTE ## +############################### + +tput setaf 2; echo 'Key Vault Dump...' ; tput sgr0 +tput setaf 3; echo "------------------------------------" ; tput sgr0 +for i in `az keyvault secret list --vault-name $COMMON_VAULT --query [].id -otsv` +do + echo "${i##*/}=\"$(az keyvault secret show --vault-name $COMMON_VAULT --id $i --query value -otsv)\"" +done diff --git a/infra/templates/osdu-r3-mvp/common_prepare.sh b/infra/templates/osdu-r3-mvp/common_prepare.sh new file mode 100755 index 0000000000000000000000000000000000000000..e77518d995db178ca7d48acc6c386923fb4d0b3a --- /dev/null +++ b/infra/templates/osdu-r3-mvp/common_prepare.sh @@ -0,0 +1,506 @@ +#!/usr/bin/env bash +# +# Purpose: Initialize the common resources necessary for building infrastructure. +# Usage: +# common_prepare.sh + +############################### +## ARGUMENT INPUT ## +############################### +usage() { echo "Usage: common_prepare.sh " 1>&2; exit 1; } + +if [ ! -z $1 ]; then ARM_SUBSCRIPTION_ID=$1; fi +if [ -z $ARM_SUBSCRIPTION_ID ]; then + tput setaf 1; echo 'ERROR: ARM_SUBSCRIPTION_ID not provided' ; tput sgr0 + usage; +fi + +if [ ! -z $2 ]; then UNIQUE=$2; fi +if [ -z $UNIQUE ]; then + UNIQUE=$(echo $((RANDOM%999+100))) + echo "export UNIQUE=${UNIQUE}" > .envrc_${UNIQUE} +fi + +if [ -z $AZURE_LOCATION ]; then + AZURE_LOCATION="centralus" +fi + +if [ -z $AZURE_PAIR_LOCATION ]; then + AZURE_PAIR_LOCATION="eastus" +fi + +if [ -z $AZURE_GROUP ]; then + AZURE_GROUP="osdu-common-${UNIQUE}" +fi + +if [ -z $AZURE_STORAGE ]; then + AZURE_STORAGE="osducommon${UNIQUE}" +fi + +if [ -z $AZURE_VAULT ]; then + AZURE_VAULT="osducommon${UNIQUE}-kv" +fi + +if [ -z $REMOTE_STATE_CONTAINER ]; then + REMOTE_STATE_CONTAINER="remote-state-container" +fi + +if [ -z $AZURE_AKS_USER ]; then + AZURE_AKS_USER="osdu.${UNIQUE}" +fi + + +if [ -z $GIT_REPO ]; then + GIT_REPO="git@ssh.dev.azure.com:v3///k8-gitops-manifests" +fi + + +############################### +## FUNCTIONS ## +############################### +function CreateResourceGroup() { + # Required Argument $1 = RESOURCE_GROUP + # Required Argument $2 = LOCATION + + if [ -z $1 ]; then + tput setaf 1; echo 'ERROR: Argument $1 (RESOURCE_GROUP) not received'; tput sgr0 + exit 1; + fi + if [ -z $2 ]; then + tput setaf 1; echo 'ERROR: Argument $2 (LOCATION) not received'; tput sgr0 + exit 1; + fi + + local _result=$(az group show --name $1) + if [ "$_result" == "" ] + then + OUTPUT=$(az group create --name $1 \ + --location $2 \ + -ojsonc) + LOCK=$(az group lock create --name "OSDU-PROTECTED" \ + --resource-group $1 \ + --lock-type CanNotDelete \ + -ojsonc) + else + tput setaf 3; echo "Resource Group $1 already exists."; tput sgr0 + fi +} +function CreateTfPrincipal() { + # Required Argument $1 = PRINCIPAL_NAME + # Required Argument $2 = VAULT_NAME + # Required Argument $3 = true/false (Add Scope) + + if [ -z $1 ]; then + tput setaf 1; echo 'ERROR: Argument $1 (PRINCIPAL_NAME) not received'; tput sgr0 + exit 1; + fi + + local _result=$(az ad sp list --display-name $1 --query [].appId -otsv) + if [ "$_result" == "" ] + then + + PRINCIPAL_SECRET=$(az ad sp create-for-rbac \ + --name $1 \ + --role owner \ + --scopes /subscriptions/${ARM_SUBSCRIPTION_ID} \ + --query password -otsv) + + PRINCIPAL_ID=$(az ad sp list \ + --display-name $1 \ + --query [].appId -otsv) + + AD_GRAPH_API_GUID="00000002-0000-0000-c000-000000000000" + + # Azure AD Graph API Access Application.ReadWrite.OwnedBy + AD_GRAPH_API=$(az ad app permission add \ + --id $PRINCIPAL_ID \ + --api $AD_GRAPH_API_GUID \ + --api-permissions 824c81eb-e3f8-4ee6-8f6d-de7f50d565b7=Role \ + -ojsonc) + + tput setaf 2; echo "Adding Information to Vault..." ; tput sgr0 + AddKeyToVault $2 "${1}-id" $PRINCIPAL_ID + AddKeyToVault $2 "${1}-key" $PRINCIPAL_SECRET + + else + tput setaf 3; echo "Service Principal $1 already exists."; tput sgr0 + fi +} +function CreatePrincipal() { + # Required Argument $1 = PRINCIPAL_NAME + # Required Argument $2 = VAULT_NAME + # Required Argument $3 = true/false (Add Scope) + + if [ -z $1 ]; then + tput setaf 1; echo 'ERROR: Argument $1 (PRINCIPAL_NAME) not received'; tput sgr0 + exit 1; + fi + + local _result=$(az ad sp list --display-name $1 --query [].appId -otsv) + if [ "$_result" == "" ] + then + + PRINCIPAL_SECRET=$(az ad sp create-for-rbac \ + --name $1 \ + --skip-assignment \ + --role owner \ + --scopes subscription/${ARM_SUBSCRIPTION_ID} \ + --query password -otsv) + + PRINCIPAL_ID=$(az ad sp list \ + --display-name $1 \ + --query [].appId -otsv) + + PRINCIPAL_OID=$(az ad sp list \ + --display-name $1 \ + --query [].objectId -otsv) + + MS_GRAPH_API_GUID="00000003-0000-0000-c000-000000000000" + + # MS Graph API Directory.Read.All + PERMISSION_1=$(az ad app permission add \ + --id $PRINCIPAL_ID \ + --api $MS_GRAPH_API_GUID \ + --api-permissions 7ab1d382-f21e-4acd-a863-ba3e13f7da61=Role \ + -ojsonc) + + tput setaf 2; echo "Adding Information to Vault..." ; tput sgr0 + AddKeyToVault $2 "${1}-id" $PRINCIPAL_ID + AddKeyToVault $2 "${1}-key" $PRINCIPAL_SECRET + AddKeyToVault $2 "${1}-oid" $PRINCIPAL_OID + + else + tput setaf 3; echo "Service Principal $1 already exists."; tput sgr0 + fi +} +function CreateADApplication() { + # Required Argument $1 = APPLICATION_NAME + # Required Argument $2 = VAULT_NAME + + if [ -z $1 ]; then + tput setaf 1; echo 'ERROR: Argument $1 (APPLICATION_NAME) not received'; tput sgr0 + exit 1; + fi + + local _result=$(az ad sp list --display-name $1 --query [].appId -otsv) + if [ "$_result" == "" ] + then + + APP_SECRET=$(az ad sp create-for-rbac \ + --name $1 \ + --skip-assignment \ + --query password -otsv) + + APP_ID=$(az ad sp list \ + --display-name $1 \ + --query [].appId -otsv) + + tput setaf 2; echo "Adding AD Application to Vault..." ; tput sgr0 + AddKeyToVault $2 "${1}-clientid" $APP_ID + AddKeyToVault $2 "${1}-secret" $APP_SECRET + + else + tput setaf 3; echo "AD Application $1 already exists."; tput sgr0 + fi +} +function CreateSSHKeys() { + # Required Argument $1 = SSH_USER + # Required Argument $2 = KEY_NAME + + if [ -z $1 ]; then + tput setaf 1; echo 'ERROR: Argument $1 (SSH_USER) not received'; tput sgr0 + exit 1; + fi + + if [ -z $2 ]; then + tput setaf 1; echo 'ERROR: Argument $2 (KEY_NAME) not received'; tput sgr0 + exit 1; + fi + + if [ ! -d ./.ssh ] + then + mkdir .ssh + fi + + if [ -f ./.ssh/$2.passphrase ]; then + tput setaf 3; echo "SSH Keys already exist."; tput sgr0 + PASSPHRASE=`cat ./.ssh/${2}.passphrase` + else + cd .ssh + + PASSPHRASE=$(echo $((RANDOM%20000000000000000000+100000000000000000000))) + echo "$PASSPHRASE" >> "$2.passphrase" + ssh-keygen -t rsa -b 2048 -C $1 -f $2 -N $PASSPHRASE && cd .. + fi + + AddKeyToVault $AZURE_VAULT "${2}" ".ssh/${2}" "file" + AddKeyToVault $AZURE_VAULT "${2}-pub" ".ssh/${2}.pub" "file" + AddKeyToVault $AZURE_VAULT "${2}-passphrase" $PASSPHRASE + + _result=`cat ./.ssh/${2}.pub` + echo $_result +} + +function CreateKeyVault() { + # Required Argument $1 = KV_NAME + # Required Argument $2 = RESOURCE_GROUP + # Required Argument $3 = LOCATION + + if [ -z $1 ]; then + tput setaf 1; echo 'ERROR: Argument $1 (KV_NAME) not received' ; tput sgr0 + exit 1; + fi + if [ -z $2 ]; then + tput setaf 1; echo 'ERROR: Argument $2 (RESOURCE_GROUP) not received' ; tput sgr0 + exit 1; + fi + if [ -z $3 ]; then + tput setaf 1; echo 'ERROR: Argument $3 (LOCATION) not received' ; tput sgr0 + exit 1; + fi + + local _vault=$(az keyvault list --resource-group $2 --query [].name -otsv) + if [ "$_vault" == "" ] + then + OUTPUT=$(az keyvault create --name $1 --resource-group $2 --location $3 --query [].name -otsv) + else + tput setaf 3; echo "Key Vault $1 already exists."; tput sgr0 + fi +} +function CreateStorageAccount() { + # Required Argument $1 = STORAGE_ACCOUNT + # Required Argument $2 = RESOURCE_GROUP + # Required Argument $3 = LOCATION + + if [ -z $1 ]; then + tput setaf 1; echo 'ERROR: Argument $1 (STORAGE_ACCOUNT) not received' ; tput sgr0 + exit 1; + fi + if [ -z $2 ]; then + tput setaf 1; echo 'ERROR: Argument $2 (RESOURCE_GROUP) not received' ; tput sgr0 + exit 1; + fi + if [ -z $3 ]; then + tput setaf 1; echo 'ERROR: Argument $3 (LOCATION) not received' ; tput sgr0 + exit 1; + fi + + local _storage=$(az storage account show --name $1 --resource-group $2 --query name -otsv) + if [ "$_storage" == "" ] + then + OUTPUT=$(az storage account create \ + --name $1 \ + --resource-group $2 \ + --location $3 \ + --sku Standard_LRS \ + --kind StorageV2 \ + --encryption-services blob \ + --query name -otsv) + else + tput setaf 3; echo "Storage Account $1 already exists."; tput sgr0 + fi +} +function GetStorageAccountKey() { + # Required Argument $1 = STORAGE_ACCOUNT + # Required Argument $2 = RESOURCE_GROUP + + if [ -z $1 ]; then + tput setaf 1; echo 'ERROR: Argument $1 (STORAGE_ACCOUNT) not received'; tput sgr0 + exit 1; + fi + if [ -z $2 ]; then + tput setaf 1; echo 'ERROR: Argument $2 (RESOURCE_GROUP) not received'; tput sgr0 + exit 1; + fi + + local _result=$(az storage account keys list \ + --account-name $1 \ + --resource-group $2 \ + --query '[0].value' \ + --output tsv) + echo ${_result} +} +function CreateBlobContainer() { + # Required Argument $1 = CONTAINER_NAME + # Required Argument $2 = STORAGE_ACCOUNT + # Required Argument $3 = STORAGE_KEY + + if [ -z $1 ]; then + tput setaf 1; echo 'ERROR: Argument $1 (CONTAINER_NAME) not received' ; tput sgr0 + exit 1; + fi + + if [ -z $2 ]; then + tput setaf 1; echo 'ERROR: Argument $2 (STORAGE_ACCOUNT) not received' ; tput sgr0 + exit 1; + fi + + if [ -z $3 ]; then + tput setaf 1; echo 'ERROR: Argument $3 (STORAGE_KEY) not received' ; tput sgr0 + exit 1; + fi + + local _container=$(az storage container show --name $1 --account-name $2 --account-key $3 --query name -otsv) + if [ "$_container" == "" ] + then + OUTPUT=$(az storage container create \ + --name $1 \ + --account-name $2 \ + --account-key $3 -otsv) + if [ $OUTPUT == True ]; then + tput setaf 3; echo "Storage Container $1 created."; tput sgr0 + else + tput setaf 1; echo "Storage Container $1 not created."; tput sgr0 + fi + else + tput setaf 3; echo "Storage Container $1 already exists."; tput sgr0 + fi +} +function AddKeyToVault() { + # Required Argument $1 = KEY_VAULT + # Required Argument $2 = SECRET_NAME + # Required Argument $3 = SECRET_VALUE + # Optional Argument $4 = isFile (bool) + + if [ -z $1 ]; then + tput setaf 1; echo 'ERROR: Argument $1 (KEY_VAULT) not received' ; tput sgr0 + exit 1; + fi + + if [ -z $2 ]; then + tput setaf 1; echo 'ERROR: Argument $2 (SECRET_NAME) not received' ; tput sgr0 + exit 1; + fi + + if [ -z $3 ]; then + tput setaf 1; echo 'ERROR: Argument $3 (SECRET_VALUE) not received' ; tput sgr0 + exit 1; + fi + + if [ "$4" == "file" ]; then + local _secret=$(az keyvault secret set --vault-name $1 --name $2 --file $3) + else + local _secret=$(az keyvault secret set --vault-name $1 --name $2 --value $3) + fi +} + +function CreateADUser() { + # Required Argument $1 = FIRST_NAME + # Required Argument $2 = LAST_NAME + + + if [ -z $1 ]; then + tput setaf 1; echo 'ERROR: Argument $1 (FIRST_NAME) not received' ; tput sgr0 + exit 1; + fi + + if [ -z $2 ]; then + tput setaf 1; echo 'ERROR: Argument $2 (LAST_NAME) not received' ; tput sgr0 + exit 1; + fi + + local _result=$(az ad user list --display-name $1 --query [].objectId -otsv) + if [ "$_result" == "" ] + then + USER_PASSWORD=$(echo $((RANDOM%200000000000000+1000000000000000))TESTER\!) + TENANT_NAME=$(az ad signed-in-user show -otsv --query 'userPrincipalName' | cut -d '@' -f 2 | sed 's/\"//') + EMAIL="${1}.${2}@${TENANT_NAME}" + + OBJECT_ID=$(az ad user create \ + --display-name "${1} ${2}" \ + --password $USER_PASSWORD \ + --user-principal-name $EMAIL \ + --query objectId + ) + + AddKeyToVault $AZURE_VAULT "ad-user-email" $EMAIL + AddKeyToVault $AZURE_VAULT "ad-user-oid" $OBJECT_ID + else + tput setaf 3; echo "User $1 already exists."; tput sgr0 + fi +} + + +############################### +## EXECUTION ## +############################### +printf "\n" +tput setaf 2; echo "Creating OSDU Common Resources" ; tput sgr0 +tput setaf 3; echo "------------------------------------" ; tput sgr0 + +tput setaf 2; echo 'Logging in and setting subscription...' ; tput sgr0 +az account set --subscription ${ARM_SUBSCRIPTION_ID} + +tput setaf 2; echo 'Creating the Common Resource Group...' ; tput sgr0 +CreateResourceGroup $AZURE_GROUP $AZURE_LOCATION + +tput setaf 2; echo "Creating a Common Key Vault..." ; tput sgr0 +CreateKeyVault $AZURE_VAULT $AZURE_GROUP $AZURE_LOCATION + +tput setaf 2; echo "Creating a Common Storage Account..." ; tput sgr0 +CreateStorageAccount $AZURE_STORAGE $AZURE_GROUP $AZURE_LOCATION + +tput setaf 2; echo "Retrieving the Storage Account Key..." ; tput sgr0 +STORAGE_KEY=$(GetStorageAccountKey $AZURE_STORAGE $AZURE_GROUP) + +tput setaf 2; echo "Creating a Storage Account Container..." ; tput sgr0 +CreateBlobContainer $REMOTE_STATE_CONTAINER $AZURE_STORAGE $STORAGE_KEY + +tput setaf 2; echo "Adding Storage Account Secrets to Vault..." ; tput sgr0 +AddKeyToVault $AZURE_VAULT "${AZURE_STORAGE}-storage" $AZURE_STORAGE +AddKeyToVault $AZURE_VAULT "${AZURE_STORAGE}-storage-key" $STORAGE_KEY + +tput setaf 2; echo 'Creating required Service Principals...' ; tput sgr0 +CreateTfPrincipal "osdu-mvp-${UNIQUE}-terraform" $AZURE_VAULT +CreatePrincipal "osdu-mvp-${UNIQUE}-principal" $AZURE_VAULT + +tput setaf 2; echo 'Creating required AD Application...' ; tput sgr0 +CreateADApplication "osdu-mvp-${UNIQUE}-application" $AZURE_VAULT +CreateADApplication "osdu-mvp-${UNIQUE}-noaccess" $AZURE_VAULT + +tput setaf 2; echo 'Creating SSH Keys...' ; tput sgr0 +GITOPS_KEY="azure-aks-gitops-ssh-key" +CreateSSHKeys $AZURE_AKS_USER $GITOPS_KEY +AddKeyToVault $AZURE_VAULT "azure-aks-gitops-ssh-key" ".ssh/${GITOPS_KEY}" "file" + +CreateSSHKeys $AZURE_AKS_USER "azure-aks-node-ssh-key" + +# tput setaf 2; echo 'Creating AD User...' ; tput sgr0 +# CreateADUser "Integration" "Test" + +tput setaf 2; echo '# OSDU ENVIRONMENT ${UNIQUE}' ; tput sgr0 +tput setaf 3; echo "------------------------------------" ; tput sgr0 +cat > .envrc_${UNIQUE} << EOF + +# OSDU ENVIRONMENT ${UNIQUE} +# ------------------------------------------------------------------------------------------------------ +export UNIQUE=${UNIQUE} +export COMMON_VAULT="${AZURE_VAULT}" +export ARM_TENANT_ID="$(az account show -ojson --query tenantId -otsv)" +export ARM_SUBSCRIPTION_ID="${ARM_SUBSCRIPTION_ID}" +export ARM_CLIENT_ID="$(az keyvault secret show --vault-name $AZURE_VAULT --id https://$AZURE_VAULT.vault.azure.net/secrets/osdu-mvp-${UNIQUE}-terraform-id --query value -otsv)" +export ARM_CLIENT_SECRET="$(az keyvault secret show --vault-name $AZURE_VAULT --id https://$AZURE_VAULT.vault.azure.net/secrets/osdu-mvp-${UNIQUE}-terraform-key --query value -otsv)" +export ARM_ACCESS_KEY="$(az keyvault secret show --vault-name $AZURE_VAULT --id https://$AZURE_VAULT.vault.azure.net/secrets/osducommon${UNIQUE}-storage-key --query value -otsv)" + +export TF_VAR_remote_state_account="$(az keyvault secret show --vault-name $AZURE_VAULT --id https://$AZURE_VAULT.vault.azure.net/secrets/osducommon${UNIQUE}-storage --query value -otsv)" +export TF_VAR_remote_state_container="remote-state-container" + +export TF_VAR_resource_group_location="${AZURE_LOCATION}" +export TF_VAR_cosmosdb_replica_location="${AZURE_PAIR_LOCATION}" + +export TF_VAR_central_resources_workspace_name="${UNIQUE}-cr" + +export TF_VAR_principal_appId="$(az keyvault secret show --vault-name $AZURE_VAULT --id https://$AZURE_VAULT.vault.azure.net/secrets/osdu-mvp-${UNIQUE}-principal-id --query value -otsv)" +export TF_VAR_principal_name="osdu-mvp-${UNIQUE}-principal" +export TF_VAR_principal_password="$(az keyvault secret show --vault-name $AZURE_VAULT --id https://$AZURE_VAULT.vault.azure.net/secrets/osdu-mvp-${UNIQUE}-principal-key --query value -otsv)" +export TF_VAR_principal_objectId="$(az keyvault secret show --vault-name $AZURE_VAULT --id https://$AZURE_VAULT.vault.azure.net/secrets/osdu-mvp-${UNIQUE}-principal-oid --query value -otsv)" + +export TF_VAR_application_clientid="$(az keyvault secret show --vault-name $AZURE_VAULT --id https://$AZURE_VAULT.vault.azure.net/secrets/osdu-mvp-${UNIQUE}-application-clientid --query value -otsv)" +export TF_VAR_application_secret="$(az keyvault secret show --vault-name $AZURE_VAULT --id https://$AZURE_VAULT.vault.azure.net/secrets/osdu-mvp-${UNIQUE}-application-secret --query value -otsv)" + +export TF_VAR_ssh_public_key_file=.ssh/node-ssh-key.pub +export TF_VAR_gitops_ssh_key_file=.ssh/gitops-ssh-key +export TF_VAR_gitops_ssh_url="git@ssh.dev.azure.com:v3/osdu-demo/OSDU_Rx/k8-gitops-manifests" + +EOF +cp .envrc_${UNIQUE} .envrc diff --git a/infra/templates/osdu-r3-mvp/data_partition/README.md b/infra/templates/osdu-r3-mvp/data_partition/README.md new file mode 100644 index 0000000000000000000000000000000000000000..443ad8a8299facd58cf29183440dd5cb2254906b --- /dev/null +++ b/infra/templates/osdu-r3-mvp/data_partition/README.md @@ -0,0 +1,114 @@ +# Azure OSDU MVC - Data Partition Configuration + +The `osdu` - `data_partition` template is intended to provision to Azure resources for an OSDU Data Partition. + + +__PreRequisites__ + +> These are typically performed by the `common_prepare.sh` scripts. + +Requires the use of [direnv](https://direnv.net/) for environment variable management. + + +Set up your local environment variables + +*Note: environment variables are automatically sourced by direnv* + +Required Environment Variables (.envrc) +```bash +export ARM_TENANT_ID="" +export ARM_SUBSCRIPTION_ID="" + +# Terraform-Principal +export ARM_CLIENT_ID="" +export ARM_CLIENT_SECRET="" + +# Terraform State Storage Account Key +export TF_VAR_remote_state_account="" +export TF_VAR_remote_state_container="" +export ARM_ACCESS_KEY="" + +# Instance Variables +export TF_VAR_resource_group_location="centralus" +``` + +__Configure__ + +Navigate to the `terraform.tfvars` terraform file. Here's a sample of the terraform.tfvars file for this template. + +```HCL +prefix = "osdu-mvp" + +resource_tags = { + contact = "" +} + +# Storage Settings +storage_shares = [ "airflowdags" ] +storage_queues = [ "airflowlogqueue" ] +``` + +__Provision__ + +Execute the following commands to set up your terraform workspace. + +```bash +# This configures terraform to leverage a remote backend that will help you and your +# team keep consistent state +terraform init -backend-config "storage_account_name=${TF_VAR_remote_state_account}" -backend-config "container_name=${TF_VAR_remote_state_container}" + +# This command configures terraform to use a workspace unique to you. This allows you to work +# without stepping over your teammate's deployments +TF_WORKSPACE="${UNIQUE}-dp" +terraform workspace new $TF_WORKSPACE || terraform workspace select $TF_WORKSPACE +``` + +Execute the following commands to orchestrate a deployment. + +```bash +# See what terraform will try to deploy without actually deploying +terraform plan + +# Execute a deployment +terraform apply +``` + +Optionally execute the following command to teardown your deployment and delete your resources. + +```bash +# Destroy resources and tear down deployment. Only do this if you want to destroy your deployment. +terraform destroy +``` + +## Testing + +Please confirm that you've completed the `terraform apply` step before running the integration tests as we're validating the active terraform workspace. + +Unit tests can be run using the following command: + +``` +go test -v $(go list ./... | grep "unit") +``` + +Integration tests can be run using the following command: + +``` +go test -v $(go list ./... | grep "integration") +``` + + +## License + +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/infra/templates/osdu-r3-mvp/data_partition/diagnostics.tf b/infra/templates/osdu-r3-mvp/data_partition/diagnostics.tf new file mode 100644 index 0000000000000000000000000000000000000000..620436c865a29425368419da1ea48ba6dd2de216 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/data_partition/diagnostics.tf @@ -0,0 +1,187 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +/* +.Synopsis + Terraform Diagnostics Control +.DESCRIPTION + This file holds diagnostics settings. +*/ + + +#------------------------------- +# CosmosDB +#------------------------------- +resource "azurerm_monitor_diagnostic_setting" "db_diagnostics" { + name = "db_diagnostics" + target_resource_id = module.cosmosdb_account.account_id + log_analytics_workspace_id = data.terraform_remote_state.central_resources.outputs.log_analytics_id + + // This one always off. + log { + category = "CassandraRequests" + enabled = false + + retention_policy { + days = 0 + enabled = false + } + } + + log { + category = "ControlPlaneRequests" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + log { + category = "DataPlaneRequests" + enabled = true + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + // This one always off. + log { + category = "GremlinRequests" + enabled = false + + retention_policy { + days = 0 + enabled = false + } + } + + // This one always off. + log { + category = "MongoRequests" + enabled = false + + retention_policy { + days = 0 + enabled = false + } + } + + log { + category = "PartitionKeyRUConsumption" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + log { + category = "PartitionKeyStatistics" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + log { + category = "QueryRuntimeStatistics" + enabled = true + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + metric { + category = "Requests" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } +} + + + +#------------------------------- +# Azure Service Bus +#------------------------------- +resource "azurerm_monitor_diagnostic_setting" "sb_diagnostics" { + name = "sb_diagnostics" + target_resource_id = module.service_bus.id + log_analytics_workspace_id = data.terraform_remote_state.central_resources.outputs.log_analytics_id + + log { + category = "OperationalLogs" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + metric { + category = "AllMetrics" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } +} + + + +#------------------------------- +# Azure Event Grid +#------------------------------- +resource "azurerm_monitor_diagnostic_setting" "eg_diagnostics" { + name = "eg_diagnostics" + target_resource_id = module.event_grid.id + log_analytics_workspace_id = data.terraform_remote_state.central_resources.outputs.log_analytics_id + + log { + category = "DeliveryFailures" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + log { + category = "PublishFailures" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + metric { + category = "AllMetrics" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } +} diff --git a/infra/templates/osdu-r3-mvp/data_partition/main.tf b/infra/templates/osdu-r3-mvp/data_partition/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..5ca2db87cec4cb65864508fbc7af8eff31898826 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/data_partition/main.tf @@ -0,0 +1,311 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +/* +.Synopsis + Terraform Main Control +.DESCRIPTION + This file holds the main control. +*/ + + +// *** WARNING **** +// This template includes locks and won't fully delete if locks aren't removed first. +// Lock: Storage +// Lock: CosmosDb +// *** WARNING **** + +// *** WARNING **** +// This template makes changes into the Central Resources and the locks in Central have to be removed to delete. +// Lock: Key Vault +// *** WARNING **** + +terraform { + required_version = ">= 0.12" + backend "azurerm" { + key = "terraform.tfstate" + } +} + +#------------------------------- +# Providers +#------------------------------- +provider "azurerm" { + version = "=2.29.0" + features {} +} + +provider "azuread" { + version = "=1.0.0" +} + +provider "random" { + version = "~>2.2" +} + +provider "external" { + version = "~> 1.0" +} + +provider "null" { + version = "~>2.1.0" +} + + + +#------------------------------- +# Private Variables +#------------------------------- +locals { + // sanitize names + prefix = replace(trimspace(lower(var.prefix)), "_", "-") + workspace = replace(trimspace(lower(terraform.workspace)), "-", "") + suffix = var.randomization_level > 0 ? "-${random_string.workspace_scope.result}" : "" + partition = split("-", trimspace(lower(terraform.workspace)))[0] + + // base prefix for resources, prefix constraints documented here: https://docs.microsoft.com/en-us/azure/architecture/best-practices/naming-conventions + base_name = length(local.prefix) > 0 ? "${local.prefix}-${local.workspace}${local.suffix}" : "${local.workspace}${local.suffix}" + base_name_21 = length(local.base_name) < 22 ? local.base_name : "${substr(local.base_name, 0, 21 - length(local.suffix))}${local.suffix}" + base_name_46 = length(local.base_name) < 47 ? local.base_name : "${substr(local.base_name, 0, 46 - length(local.suffix))}${local.suffix}" + base_name_60 = length(local.base_name) < 61 ? local.base_name : "${substr(local.base_name, 0, 60 - length(local.suffix))}${local.suffix}" + base_name_76 = length(local.base_name) < 77 ? local.base_name : "${substr(local.base_name, 0, 76 - length(local.suffix))}${local.suffix}" + base_name_83 = length(local.base_name) < 84 ? local.base_name : "${substr(local.base_name, 0, 83 - length(local.suffix))}${local.suffix}" + + resource_group_name = format("%s-%s-%s-rg", var.prefix, local.workspace, random_string.workspace_scope.result) + retention_policy = var.log_retention_days == 0 ? false : true + + storage_name = "${replace(local.base_name_21, "-", "")}data" + sdms_storage_name = "${replace(local.base_name_21, "-", "")}sdms" + cosmosdb_name = "${local.base_name}-db" + sb_namespace = "${local.base_name_21}-bus" + eventgrid_name = "${local.base_name_21}-grid" + eventgrid_records_topic = format("%s-recordstopic", local.eventgrid_name) + + rbac_principals = [ + data.terraform_remote_state.central_resources.outputs.osdu_identity_principal_id, + data.terraform_remote_state.central_resources.outputs.principal_objectId + ] +} + + + +#------------------------------- +# Common Resources +#------------------------------- +data "azurerm_client_config" "current" {} +data "azurerm_subscription" "current" {} + +data "terraform_remote_state" "central_resources" { + backend = "azurerm" + + config = { + storage_account_name = var.remote_state_account + container_name = var.remote_state_container + key = format("terraform.tfstateenv:%s", var.central_resources_workspace_name) + } +} + +resource "random_string" "workspace_scope" { + keepers = { + # Generate a new id each time we switch to a new workspace or app id + ws_name = replace(trimspace(lower(terraform.workspace)), "-", "") + prefix = replace(trimspace(lower(var.prefix)), "_", "-") + } + + length = max(1, var.randomization_level) // error for zero-length + special = false + upper = false +} + + + +#------------------------------- +# Resource Group +#------------------------------- +resource "azurerm_resource_group" "main" { + name = local.resource_group_name + location = var.resource_group_location + + tags = var.resource_tags + lifecycle { ignore_changes = [tags] } +} + + + +#------------------------------- +# Storage +#------------------------------- +module "storage_account" { + source = "../../../modules/providers/azure/storage-account" + + name = local.storage_name + resource_group_name = azurerm_resource_group.main.name + container_names = var.storage_containers + kind = "StorageV2" + replication_type = var.storage_replication_type + + resource_tags = var.resource_tags +} + +// Add Access Control to Principal +resource "azurerm_role_assignment" "storage_access" { + count = length(local.rbac_principals) + + role_definition_name = "Contributor" + principal_id = local.rbac_principals[count.index] + scope = module.storage_account.id +} + +// Add Data Contributor Role to Principal +resource "azurerm_role_assignment" "storage_data_contributor" { + count = length(local.rbac_principals) + + role_definition_name = "Storage Blob Data Contributor" + principal_id = local.rbac_principals[count.index] + scope = module.storage_account.id +} + +module "sdms_storage_account" { + source = "../../../modules/providers/azure/storage-account" + + name = local.sdms_storage_name + resource_group_name = azurerm_resource_group.main.name + container_names = [] + kind = "StorageV2" + replication_type = var.storage_replication_type + + resource_tags = var.resource_tags +} + +// Add Access Control to Principal +resource "azurerm_role_assignment" "sdms_storage_access" { + count = length(local.rbac_principals) + + role_definition_name = "Contributor" + principal_id = local.rbac_principals[count.index] + scope = module.sdms_storage_account.id +} + +// Add Data Contributor Role to Principal +resource "azurerm_role_assignment" "sdms_storage_data_contributor" { + count = length(local.rbac_principals) + + role_definition_name = "Storage Blob Data Contributor" + principal_id = local.rbac_principals[count.index] + scope = module.sdms_storage_account.id +} + +#------------------------------- +# CosmosDB +#------------------------------- +module "cosmosdb_account" { + source = "../../../modules/providers/azure/cosmosdb" + + name = local.cosmosdb_name + resource_group_name = azurerm_resource_group.main.name + primary_replica_location = var.cosmosdb_replica_location + automatic_failover = var.cosmosdb_automatic_failover + consistency_level = var.cosmosdb_consistency_level + databases = var.cosmos_databases + sql_collections = var.cosmos_sql_collections + + resource_tags = var.resource_tags +} + +// Add Access Control to Principal +resource "azurerm_role_assignment" "cosmos_access" { + count = length(local.rbac_principals) + + role_definition_name = "Contributor" + principal_id = local.rbac_principals[count.index] + scope = module.cosmosdb_account.account_id +} + + + +#------------------------------- +# Azure Service Bus +#------------------------------- +module "service_bus" { + source = "../../../modules/providers/azure/service-bus" + + name = local.sb_namespace + resource_group_name = azurerm_resource_group.main.name + sku = var.sb_sku + topics = var.sb_topics + + resource_tags = var.resource_tags +} + + +// Add Access Control to Principal +resource "azurerm_role_assignment" "sb_access" { + count = length(local.rbac_principals) + + role_definition_name = "Azure Service Bus Data Sender" + principal_id = local.rbac_principals[count.index] + scope = module.service_bus.id +} + + +#------------------------------- +# Azure Event Grid +#------------------------------- +module "event_grid" { + source = "../../../modules/providers/azure/event-grid" + + name = local.eventgrid_name + resource_group_name = azurerm_resource_group.main.name + + topics = [ + { + name = local.eventgrid_records_topic + } + ] + + resource_tags = var.resource_tags +} + +// Add Access Control to Principal +resource "azurerm_role_assignment" "eventgrid_access" { + count = length(local.rbac_principals) + + role_definition_name = "Contributor" + principal_id = local.rbac_principals[count.index] + scope = module.event_grid.id +} + + + +#------------------------------- +# Locks +#------------------------------- +resource "azurerm_management_lock" "sa_lock" { + name = "osdu_ds_sa_lock" + scope = module.storage_account.id + lock_level = "CanNotDelete" +} + +resource "azurerm_management_lock" "sdms_sa_lock" { + name = "osdu_sdms_sa_lock" + scope = module.sdms_storage_account.id + lock_level = "CanNotDelete" +} + +resource "azurerm_management_lock" "db_lock" { + name = "osdu_ds_db_lock" + scope = module.cosmosdb_account.properties.cosmosdb.id + lock_level = "CanNotDelete" +} diff --git a/infra/templates/osdu-r3-mvp/data_partition/output.tf b/infra/templates/osdu-r3-mvp/data_partition/output.tf new file mode 100644 index 0000000000000000000000000000000000000000..d1479117bbdf0fac8049e913495c59e87499703f --- /dev/null +++ b/infra/templates/osdu-r3-mvp/data_partition/output.tf @@ -0,0 +1,58 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +.Synopsis + Terraform Output Configuration +.DESCRIPTION + This file holds the Output Configuration +*/ + +#------------------------------- +# Output Variables +#------------------------------- +output "data_partition_group_name" { + description = "The name of the resource group" + value = azurerm_resource_group.main.name +} + +output "data_partition_group_id" { + description = "The resource id for the provisioned resource group" + value = azurerm_resource_group.main.id +} + +output "storage_account" { + description = "The name of the storage account." + value = module.storage_account.name +} + +output "storage_account_id" { + description = "The resource id of the storage account instance" + value = module.storage_account.id +} + +output "storage_containers" { + description = "Map of storage account containers." + value = module.storage_account.containers +} + +output "cosmosdb_account_name" { + description = "The name of the CosmosDB account." + value = module.cosmosdb_account.account_name +} + +output "cosmosdb_properties" { + description = "Properties of the deployed CosmosDB account." + value = module.cosmosdb_account.properties +} diff --git a/infra/templates/osdu-r3-mvp/data_partition/secrets.tf b/infra/templates/osdu-r3-mvp/data_partition/secrets.tf new file mode 100644 index 0000000000000000000000000000000000000000..5e4af435fe7bd341d67ada737ea042a5669f2966 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/data_partition/secrets.tf @@ -0,0 +1,175 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +/* +.Synopsis + Terraform Secrets Control +.DESCRIPTION + This file holds KV Secrets. +*/ + + +#------------------------------- +# Private Variables +#------------------------------- +locals { + partition_id = format("%s-id", var.data_partition_name) + + storage_account_name = format("%s-storage", var.data_partition_name) + storage_key_name = format("%s-key", local.storage_account_name) + + sdms_storage_account_name = format("%s-sdms-storage", var.data_partition_name) + sdms_storage_key_name = format("%s-key", local.sdms_storage_account_name) + + cosmos_connection = format("%s-cosmos-connection", var.data_partition_name) + cosmos_endpoint = format("%s-cosmos-endpoint", var.data_partition_name) + cosmos_primary_key = format("%s-cosmos-primary-key", var.data_partition_name) + + sb_namespace_name = format("%s-sb-namespace", var.data_partition_name) + sb_connection = format("%s-sb-connection", var.data_partition_name) + + eventgrid_domain_name = format("%s-eventgrid", var.data_partition_name) + eventgrid_domain_key_name = format("%s-key", local.eventgrid_domain_name) + eventgrid_records_topic_name = format("%s-recordstopic", local.eventgrid_domain_name) + eventgrid_records_topic_endpoint = format("https://%s.%s-1.eventgrid.azure.net/api/events", local.eventgrid_records_topic, var.resource_group_location) + + elastic_endpoint = format("%s-elastic-endpoint", var.data_partition_name) + elastic_username = format("%s-elastic-username", var.data_partition_name) + elastic_password = format("%s-elastic-password", var.data_partition_name) +} + + +#------------------------------- +# Partition +#------------------------------- +resource "azurerm_key_vault_secret" "partition_id" { + name = local.partition_id + value = var.data_partition_name + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + + + +#------------------------------- +# Storage +#------------------------------- +resource "azurerm_key_vault_secret" "storage_name" { + name = local.storage_account_name + value = module.storage_account.name + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + +resource "azurerm_key_vault_secret" "storage_key" { + name = local.storage_key_name + value = module.storage_account.primary_access_key + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + +resource "azurerm_key_vault_secret" "sdms_storage_name" { + name = local.sdms_storage_account_name + value = module.sdms_storage_account.name + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + +resource "azurerm_key_vault_secret" "sdms_storage_key" { + name = local.sdms_storage_key_name + value = module.sdms_storage_account.primary_access_key + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + + + +#------------------------------- +# CosmosDB +#------------------------------- +resource "azurerm_key_vault_secret" "cosmos_connection" { + name = local.cosmos_connection + value = module.cosmosdb_account.properties.cosmosdb.connection_strings[0] + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + +resource "azurerm_key_vault_secret" "cosmos_endpoint" { + name = local.cosmos_endpoint + value = module.cosmosdb_account.properties.cosmosdb.endpoint + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + +resource "azurerm_key_vault_secret" "cosmos_key" { + name = local.cosmos_primary_key + value = module.cosmosdb_account.properties.cosmosdb.primary_master_key + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + + + +#------------------------------- +# Azure Service Bus +#------------------------------- +resource "azurerm_key_vault_secret" "sb_namespace" { + name = local.sb_namespace_name + value = module.service_bus.name + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + +resource "azurerm_key_vault_secret" "sb_connection" { + name = local.sb_connection + value = module.service_bus.default_connection_string + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + + + +#------------------------------- +# Azure Event Grid +#------------------------------- +resource "azurerm_key_vault_secret" "eventgrid_name" { + name = local.eventgrid_domain_name + value = module.event_grid.name + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + +resource "azurerm_key_vault_secret" "eventgrid_key" { + name = local.eventgrid_domain_key_name + value = module.event_grid.primary_access_key + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + +resource "azurerm_key_vault_secret" "recordstopic_name" { + name = local.eventgrid_records_topic_name + value = local.eventgrid_records_topic_endpoint + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + + +#------------------------------- +# Elastic +#------------------------------- +resource "azurerm_key_vault_secret" "elastic_endpoint" { + name = local.elastic_endpoint + value = var.elasticsearch_endpoint + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + +resource "azurerm_key_vault_secret" "elastic_username" { + name = local.elastic_username + value = var.elasticsearch_username + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + +resource "azurerm_key_vault_secret" "elastic_password" { + name = local.elastic_password + value = var.elasticsearch_password + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} \ No newline at end of file diff --git a/infra/templates/osdu-r3-mvp/data_partition/terraform.tfvars b/infra/templates/osdu-r3-mvp/data_partition/terraform.tfvars new file mode 100644 index 0000000000000000000000000000000000000000..b79c20a52cc7223efac9cd36401cf0b3066603b5 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/data_partition/terraform.tfvars @@ -0,0 +1,191 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +.Synopsis + Terraform Variable Configuration +.DESCRIPTION + This file holds the Default Variable Configuration +*/ + +prefix = "osdu-mvp" + +resource_tags = { + contact = "pipeline" +} + +# Storage Settings +storage_replication_type = "GZRS" +storage_containers = [ + "legal-service-azure-configuration", + "opendes", + "osdu-wks-mappings" +] + + +# Database Settings +cosmosdb_consistency_level = "Session" +cosmos_databases = [ + { + name = "osdu-db" + throughput = 4000 + } +] +cosmos_sql_collections = [ + { + name = "LegalTag" + database_name = "osdu-db" + partition_key_path = "/id" + }, + { + name = "StorageRecord" + database_name = "osdu-db" + partition_key_path = "/id" + }, + { + name = "StorageSchema" + database_name = "osdu-db" + partition_key_path = "/kind" + }, + { + name = "TenantInfo" + database_name = "osdu-db" + partition_key_path = "/id" + }, + { + name = "UserInfo" + database_name = "osdu-db" + partition_key_path = "/id" + }, + { + name = "Authority" + database_name = "osdu-db" + partition_key_path = "/dataPartitionId" + }, + { + name = "EntityType" + database_name = "osdu-db" + partition_key_path = "/dataPartitionId" + }, + { + name = "SchemaInfo" + database_name = "osdu-db" + partition_key_path = "/dataPartitionId" + }, + { + name = "Source" + database_name = "osdu-db" + partition_key_path = "/dataPartitionId" + }, + { + name = "RegisterAction" + database_name = "osdu-db" + partition_key_path = "/dataPartitionId" + }, + { + name = "RegisterDdms" + database_name = "osdu-db" + partition_key_path = "/dataPartitionId" + }, + { + name = "RegisterSubscription" + database_name = "osdu-db" + partition_key_path = "/dataPartitionId" + }, + { + name = "IngestionStrategy" + database_name = "osdu-db" + partition_key_path = "/workflowType" + }, + { + name = "WorkflowStatus" + database_name = "osdu-db" + partition_key_path = "/workflowId" + }, + { + name = "Workflow" + database_name = "osdu-db" + partition_key_path = "/workflowId" + }, + { + name = "WorkflowRun" + database_name = "osdu-db" + partition_key_path = "/workflowId" + } +] + + +# Service Bus Settings +sb_topics = [ + { + name = "indexing-progress" + enable_partitioning = true + subscriptions = [ + { + name = "indexing-progresssubscription" + max_delivery_count = 5 + lock_duration = "PT5M" + forward_to = "" + } + ] + }, + { + name = "legaltags" + enable_partitioning = true + subscriptions = [ + { + name = "compliance-change--integration-test" + max_delivery_count = 1 + lock_duration = "PT5M" + forward_to = "" + }, + { + name = "legaltagsubscription" + max_delivery_count = 5 + lock_duration = "PT5M" + forward_to = "" + } + ] + }, + { + name = "recordstopic" + enable_partitioning = true + subscriptions = [ + { + name = "recordstopicsubscription" + max_delivery_count = 5 + lock_duration = "PT5M" + forward_to = "" + }, + { + name = "wkssubscription" + max_delivery_count = 5 + lock_duration = "PT5M" + forward_to = "" + } + ] + }, + { + name = "recordstopicdownstream" + enable_partitioning = true + subscriptions = [ + { + name = "downstreamsub" + max_delivery_count = 5 + lock_duration = "PT5M" + forward_to = "" + } + ] + } +] \ No newline at end of file diff --git a/infra/templates/osdu-r3-mvp/data_partition/tests/integration/integration_test.go b/infra/templates/osdu-r3-mvp/data_partition/tests/integration/integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..df81890b0dec6265f3486372e97c35a89c748f22 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/data_partition/tests/integration/integration_test.go @@ -0,0 +1,49 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "os" + "testing" + + "github.com/gruntwork-io/terratest/modules/terraform" + cosmosIntegTests "github.com/microsoft/cobalt/infra/modules/providers/azure/cosmosdb/tests/integration" + storageIntegTests "github.com/microsoft/cobalt/infra/modules/providers/azure/storage-account/tests/integration" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var subscription = os.Getenv("ARM_SUBSCRIPTION_ID") +var tfOptions = &terraform.Options{ + TerraformDir: "../../", + BackendConfig: map[string]interface{}{ + "storage_account_name": os.Getenv("TF_VAR_remote_state_account"), + "container_name": os.Getenv("TF_VAR_remote_state_container"), + }, +} + +// Runs a suite of test assertions to validate that a provisioned data source environment +// is fully functional. +func TestDataEnvironment(t *testing.T) { + testFixture := infratests.IntegrationTestFixture{ + GoTest: t, + TfOptions: tfOptions, + ExpectedTfOutputCount: 7, + TfOutputAssertions: []infratests.TerraformOutputValidation{ + storageIntegTests.InspectStorageAccount("storage_account", "storage_containers", "data_partition_group_name"), + cosmosIntegTests.InspectProvisionedCosmosDBAccount("data_partition_group_name", "cosmosdb_account_name", "cosmosdb_properties"), + }, + } + infratests.RunIntegrationTests(&testFixture) +} diff --git a/infra/templates/osdu-r3-mvp/data_partition/tests/unit/common.go b/infra/templates/osdu-r3-mvp/data_partition/tests/unit/common.go new file mode 100644 index 0000000000000000000000000000000000000000..1904c24e3301d66572e43a03a7ec973c48821c81 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/data_partition/tests/unit/common.go @@ -0,0 +1,36 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "encoding/json" + "github.com/gruntwork-io/terratest/modules/random" + "strings" + "testing" +) + +// these are useful values used in many test +var region = "centralus" +var prefix = "osdu-testing" + strings.ToLower(random.UniqueId()) +var workspace = "osdu-testing-" + strings.ToLower(random.UniqueId()) + +// helper function to parse blocks of JSON into a generic Go map +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} diff --git a/infra/templates/osdu-r3-mvp/data_partition/tests/unit/unit_test.go b/infra/templates/osdu-r3-mvp/data_partition/tests/unit/unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..eda5fdb881576ca9083821f86f64d2551054d503 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/data_partition/tests/unit/unit_test.go @@ -0,0 +1,57 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "os" + "testing" + + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var tfOptions = &terraform.Options{ + TerraformDir: "../../", + Upgrade: true, + Vars: map[string]interface{}{ + "resource_group_location": region, + "prefix": prefix, + }, + BackendConfig: map[string]interface{}{ + "storage_account_name": os.Getenv("TF_VAR_remote_state_account"), + "container_name": os.Getenv("TF_VAR_remote_state_container"), + }, +} + +func TestTemplate(t *testing.T) { + expectedAppDevResourceGroup := asMap(t, `{ + "location": "`+region+`" + }`) + + resourceDescription := infratests.ResourceDescription{ + "azurerm_resource_group.main": expectedAppDevResourceGroup, + } + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tfOptions, + Workspace: workspace, + PlanAssertions: nil, + ExpectedResourceCount: 79, + ExpectedResourceAttributeValues: resourceDescription, + } + + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/templates/osdu-r3-mvp/data_partition/variables.tf b/infra/templates/osdu-r3-mvp/data_partition/variables.tf new file mode 100644 index 0000000000000000000000000000000000000000..f3d6f61006d31791d65d0d8d1d0f8f7eba5b6c67 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/data_partition/variables.tf @@ -0,0 +1,168 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +.Synopsis + Terraform Variable Configuration +.DESCRIPTION + This file holds the Variable Configuration +*/ + + +#------------------------------- +# Application Variables +#------------------------------- +variable "prefix" { + description = "The workspace prefix defining the project area for this terraform deployment." + type = string +} + +variable "randomization_level" { + description = "Number of additional random characters to include in resource names to insulate against unexpected resource name collisions." + type = number + default = 4 +} + +variable "remote_state_account" { + description = "Remote Terraform State Azure storage account name. This is typically set as an environment variable and used for the initial terraform init." + type = string +} + +variable "remote_state_container" { + description = "Remote Terraform State Azure storage container name. This is typically set as an environment variable and used for the initial terraform init." + type = string +} + +variable "central_resources_workspace_name" { + description = "(Required) The workspace name for the central_resources repository terraform environment / template to reference for this template." + type = string +} + +variable "resource_tags" { + description = "Map of tags to apply to this template." + type = map(string) + default = {} +} + +variable "resource_group_location" { + description = "The Azure region where data storage resources in this template should be created." + type = string +} + +variable "data_partition_name" { + description = "The OSDU data Partition Name." + type = string + default = "opendes" +} + +variable "log_retention_days" { + description = "Number of days to retain logs." + type = number + default = 30 +} + +variable "storage_replication_type" { + description = "Defines the type of replication to use for this storage account. Valid options are LRS*, GRS, RAGRS and ZRS." + type = string + default = "GZRS" +} + +variable "storage_containers" { + description = "The list of storage container names to create. Names must be unique per storage account." + type = list(string) +} + +variable "cosmosdb_replica_location" { + description = "The name of the Azure region to host replicated data. i.e. 'East US' 'East US 2'. More locations can be found at https://azure.microsoft.com/en-us/global-infrastructure/locations/" + type = string +} + +variable "cosmosdb_consistency_level" { + description = "The level of consistency backed by SLAs for Cosmos database. Developers can chose from five well-defined consistency levels on the consistency spectrum." + type = string + default = "Session" +} + +variable "cosmosdb_automatic_failover" { + description = "Determines if automatic failover is enabled for CosmosDB." + type = bool + default = true +} + +variable "cosmos_databases" { + description = "The list of Cosmos DB SQL Databases." + type = list(object({ + name = string + throughput = number + })) + default = [] +} + +variable "cosmos_sql_collections" { + description = "The list of cosmos collection names to create. Names must be unique per cosmos instance." + type = list(object({ + name = string + database_name = string + partition_key_path = string + })) + default = [] +} + +variable "sb_sku" { + description = "The SKU of the namespace. The options are: `Basic`, `Standard`, `Premium`." + type = string + default = "Standard" +} + +variable "sb_topics" { + type = list(object({ + name = string + enable_partitioning = bool + subscriptions = list(object({ + name = string + max_delivery_count = number + lock_duration = string + forward_to = string + })) + })) + default = [ + { + name = "topic_test" + enable_partitioning = true + subscriptions = [ + { + name = "sub_test" + max_delivery_count = 1 + lock_duration = "PT5M" + forward_to = "" + } + ] + } + ] +} + +variable "elasticsearch_endpoint" { + type = string + description = "endpoint for elasticsearch cluster" +} + +variable "elasticsearch_username" { + type = string + description = "username for elasticsearch cluster" +} + +variable "elasticsearch_password" { + type = string + description = "password for elasticsearch cluster" +} \ No newline at end of file diff --git a/infra/templates/osdu-r3-mvp/docs/aks-environment.md b/infra/templates/osdu-r3-mvp/docs/aks-environment.md new file mode 100644 index 0000000000000000000000000000000000000000..25886b6f6b6b6b4802b63cdabbec37255cdd70c1 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/docs/aks-environment.md @@ -0,0 +1,146 @@ +# Deploying OSDU services with Kubernetes + Elastic Cloud + +v0.1 - 2/6/2020 +v0.2 - 7/6/2020 +v0.3 - 10/4/2020 + +## Introduction + +Some of OSDU's enterprise customers have a small number of microservices they'd like to deploy and host on [AKS](https://docs.microsoft.com/en-us/azure/aks/). Geospatial documents are indexed in Elastic Search to accomodate bounding box and radius distance querying scenarios. This template provisions resources required to run OSDU Services in AKS and uses an instance of a fully managed PaaS Elasticsearch hosted in [EC](https://www.elastic.co/cloud/). + +This document outlines how Cobalt has been extended to meet the use cases of these customers. The intended audience of this document is the development and product teams working on OSDU Infratructure on Azure and related projects. + +## In Scope + +- Diagram the architecture solution. +- Identify deployment topology needed +- Data Partition Integration +- Airflow Integration +- Bring your own Service Principal + +## Out of scope + +- Traffic Manager Integration +- APIM integration +- Elastic Search Service + +## Key Terms +- **RG**: Abbreviation for “Resource Group†+- **Sub**: Abbreviation for “Subscription†+- **Persona**: An archetype of a Cobalt customer +- **Stage**: An application deployment stage (dev, qa, pre-prod, prod, etc...) +- **Region**: A location in which an application is deployed + + +## Customers +- **Admin**: This persona represents an administrator of Azure. This persona does not implement the line of business applications but will help other teams deliver them. +- **App Developer Team**: This persona is responsible for creating and maintaining the line of business applications + +## Solution Architecture + +This drawing shows the intended Azure Solution Architecture necessary. + +![Architecture](images/architecture.png "Architecture") + +## Deployment Topology + +This graphic shows the targeted deployment topology needed by our enterprise customers. The deployment is deployed to a single tenant and subscription. The resources are partitioned to align with the different personas within the customer. + +![Deployment Topology](images/topology.png "Deployment Topology") + +## Template Topology + +The graphic below outlines the topology of the terraform templates that will deploy the topology called out above. + +![Template Topology](images/template.png "Template Topology") + +## Terraform Template Environment Dependencies + +``` +└── configurations + ├── central_resources + │ ├── main.tf + │ └── terraform.tfvars + ├── data_partition + │ ├── main.tf + │ ├── terraform.tfvars + │ └── variables.tf + ├── service_resources + │ ├── agic.tf + │ ├── aks.tf + │ ├── backend.tf + │ ├── commons.tf + │ ├── keyvault.tf + │ ├── networking.tf + │ ├── outputs.tf + │ ├── pod_identity.tf + │ ├── security.tf + │ ├── terraform.tfvars + │ └── variables.tf +``` + +### central_resources + +The [central_resources](../configurations/central_resources/main.tf) configuation is the resources that are central to an OSDU architecture. These resources need to exist first before any other configuration and can never be destroyed without impacting the entire data platform. + +Items here incude things such as: +- Centralized Logging +- Key Vault +- User Assigned Identity + + +### service_resources + +The [service_resources](../configurations/service_resources/main.tf) configuration relies on the resources from the [central_resources](../configurations/central_resources/main.tf) configuration and has the responsibility to store any sensitive information it creates to the central keyvault. Additionally it is required to setup any maintain any roles it requires. + +Items here include things such as: +- AKS Clusters +- Application Gateway +- Airflow Configuration Components + + +### data_partition + +The [data_partition](../configurations/data_partition/main.tf) configuration relies on the resources from the [central_resources](../configuration/data_partition/main.tf) configuration and has the responsibility to store any sensitive information it creates to the central keyvault. Additionally it is required to setup and maintain any roles it requires. + +Items here include things such as: +- CosmosDB +- Storage Account +- Message Bus +- Event Grid + + +### Credential Management + +The AKS cluster will be configured with a `SystemAssigned` identity to enable MSI integration with resources like Service Bus, ADLS Gen 2 and Keyvault. + +MSI is enabled through the [identity block](https://www.terraform.io/docs/providers/azurerm/r/kubernetes_cluster.html#type-2) of the `azurerm_kubernetes_cluster` Terraform provider. + +You can reference the AKS MI [docs](https://docs.microsoft.com/en-us/azure/aks/use-managed-identity) for manual setup instructions. + +## Security + +Here is an overview of the security for the deployment strategy and templates discussed above: + +The service principal running the deployment will have to be an owner in the target subscription. + +This template will **not** create the OSDU environment service principal but one will have to be provided to the central resources configuration. This will eliminate the need to have an elevated service principal to deploy the solution. The service principalcreated will need to have Microsoft Graph Directory.Read.All access granted. + + + + + +## License +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/infra/templates/osdu-r3-mvp/docs/images/architecture.png b/infra/templates/osdu-r3-mvp/docs/images/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..f3430e9bfbadb38d4ac52adafaec873ee59ffff2 Binary files /dev/null and b/infra/templates/osdu-r3-mvp/docs/images/architecture.png differ diff --git a/infra/templates/osdu-r3-mvp/magefile.go b/infra/templates/osdu-r3-mvp/magefile.go new file mode 100644 index 0000000000000000000000000000000000000000..d9f92e0110d36116dda9c4efcf93e33c0f573c2e --- /dev/null +++ b/infra/templates/osdu-r3-mvp/magefile.go @@ -0,0 +1,172 @@ +//+build mage + +// osdu-infrastructure task runner. +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" +) + +// A build step that runs all tests. +func All() { + mg.Deps(CentralTests) + mg.Deps(ServiceTests) + mg.Deps(PartitionTests) +} + +// Execute Unit Tests for OSDU MVP Central Resources. +func CentralUnitTest() error { + mg.Deps(Check) + fmt.Println("INFO: Running unit tests...") + return FindAndRunTests("central_resources/tests/unit") +} + +// Execute Integration Tests for OSDU MVP Central Resources. +func CentralIntegrationTest() error { + mg.Deps(Check) + fmt.Println("INFO: Running integration tests...") + return FindAndRunTests("central_resources/tests/integration") +} + +// Execute Tests for OSDU MVP Central Resources. +func CentralTests() { + mg.Deps(CentralUnitTest) + mg.Deps(CentralIntegrationTest) +} + +// Execute Unit Tests for OSDU MVP Service Resources. +func ServiceUnitTest() error { + mg.Deps(Check) + fmt.Println("INFO: Running unit tests...") + return FindAndRunTests("service_resources/tests/unit") +} + +// Execute Integration Tests for OSDU MVP Service Resources. +func ServiceIntegrationTest() error { + mg.Deps(Check) + fmt.Println("INFO: Running integration tests...") + return FindAndRunTests("service_resources/tests/integration") +} + +// Execute Tests for OSDU MVP Service Resources. +func ServiceTests() { + mg.Deps(ServiceUnitTest) + mg.Deps(ServiceIntegrationTest) +} + +// Execute Unit Tests for OSDU MVP Partition Resources. +func PartitionUnitTest() error { + mg.Deps(Check) + fmt.Println("INFO: Running unit tests...") + return FindAndRunTests("data_partition/tests/unit") +} + +// Execute Integration Tests for OSDU MVP Partition Resources. +func PartitionIntegrationTest() error { + mg.Deps(Check) + fmt.Println("INFO: Running integration tests...") + return FindAndRunTests("data_partition/tests/integration") +} + +// Execute Tests for OSDU MVP Partition Resources. +func PartitionTests() { + mg.Deps(PartitionUnitTest) + mg.Deps(PartitionIntegrationTest) +} + +// Validate both Terraform code and Go code. +func Check() { + mg.Deps(LintTF) + mg.Deps(LintGO) +} + +// Lint check Go and fail if files are not not formatted properly. +func LintGO() error { + fmt.Println("INFO: Checking format for Go files...") + return verifyRunsQuietly("Run `go fmt ./...` to fix", "go", "fmt", "./...") +} + +// Lint check Terraform and fail if files are not formatted properly. +func LintTF() error { + fmt.Println("INFO: Checking format for Terraform files...") + return verifyRunsQuietly("Run `terraform fmt --check --recursive` to fix the offending files", "terraform", "fmt") +} + +// Remove temporary build and test files. +func Clean() error { + fmt.Println("INFO: Cleaning...") + return filepath.Walk("./infra/modules", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && info.Name() == "vendor" { + return filepath.SkipDir + } + if info.IsDir() && info.Name() == ".terraform" { + os.RemoveAll(path) + fmt.Printf("Removed \"%v\"\n", path) + return filepath.SkipDir + } + if info.IsDir() && info.Name() == "terraform.tfstate.d" { + os.RemoveAll(path) + fmt.Printf("Removed \"%v\"\n", path) + return filepath.SkipDir + } + if !info.IsDir() && (info.Name() == "terraform.tfstate" || + info.Name() == "terraform.tfplan" || + info.Name() == "terraform.tfstate.backup") { + os.Remove(path) + fmt.Printf("Removed \"%v\"\n", path) + } + return nil + }) +} + +//------------------------------- +// GO UTILITY FUNCTIONS +//------------------------------- + +// runs a command and ensures that the exit code indicates success and that there is no output to stdout +func verifyRunsQuietly(instructionsToFix string, cmd string, args ...string) error { + output, err := sh.Output(cmd, args...) + + if err != nil { + return err + } + + if len(output) == 0 { + return nil + } + + return fmt.Errorf("ERROR: command '%s' with arguments %s failed. Output was: '%s'. %s", cmd, args, output, instructionsToFix) +} + +// FindAndRunTests finds all tests with a given path suffix and runs them using `go test` +func FindAndRunTests(pathSuffix string) error { + goModules, err := sh.Output("go", "list", "./...") + if err != nil { + return err + } + + testTargetModules := make([]string, 0) + for _, module := range strings.Fields(goModules) { + if strings.HasSuffix(module, pathSuffix) { + testTargetModules = append(testTargetModules, module) + } + } + + if len(testTargetModules) == 0 { + return fmt.Errorf("No modules found for testing prefix '%s'", pathSuffix) + } + + cmdArgs := []string{"test"} + cmdArgs = append(cmdArgs, testTargetModules...) + cmdArgs = append(cmdArgs, "-v", "-timeout", "7200s") + return sh.RunV("go", cmdArgs...) +} diff --git a/infra/templates/osdu-r3-mvp/service_resources/README.md b/infra/templates/osdu-r3-mvp/service_resources/README.md new file mode 100644 index 0000000000000000000000000000000000000000..701fdc1d7a591a3aa162e0ffa5c93d1ac9cb2d7c --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/README.md @@ -0,0 +1,113 @@ +# Azure OSDU MVC - Service Resources Configuration + +The `osdu` - `service_resources` environment template is intended to provision to Azure resources for OSDU which are specifically used for the AKS cluster and configuration of the cluster. + +__PreRequisites__ + +> These are typically performed by the `common_prepare.sh` scripts. + + +Requires the use of [direnv](https://direnv.net/) for environment variable management. + +Set up your local environment variables + +*Note: environment variables are automatically sourced by direnv* + +Required Environment Variables (.envrc) +```bash +export ARM_TENANT_ID="" +export ARM_SUBSCRIPTION_ID="" + +# Terraform-Principal +export ARM_CLIENT_ID="" +export ARM_CLIENT_SECRET="" + +# Terraform State Storage Account Key +export TF_VAR_remote_state_account="" +export TF_VAR_remote_state_container="" +export ARM_ACCESS_KEY="" + +# Instance Variables +export TF_VAR_resource_group_location="centralus" +``` + +__Configure__ + +Navigate to the `terraform.tfvars` terraform file. Here's a sample of the terraform.tfvars file for this template. + +```HCL +prefix = "osdu-mvp" + +resource_tags = { + contact = "" +} + +# Storage Settings +storage_shares = [ "airflowdags" ] +storage_queues = [ "airflowlogqueue" ] +``` + +__Provision__ + +Execute the following commands to set up your terraform workspace. + +```bash +# This configures terraform to leverage a remote backend that will help you and your +# team keep consistent state +terraform init -backend-config "storage_account_name=${TF_VAR_remote_state_account}" -backend-config "container_name=${TF_VAR_remote_state_container}" + +# This command configures terraform to use a workspace unique to you. This allows you to work +# without stepping over your teammate's deployments +TF_WORKSPACE="${UNIQUE}-sr" +terraform workspace new $TF_WORKSPACE || terraform workspace select $TF_WORKSPACE +``` + +Execute the following commands to orchestrate a deployment. + +```bash +# See what terraform will try to deploy without actually deploying +terraform plan + +# Execute a deployment +terraform apply +``` + +Optionally execute the following command to teardown your deployment and delete your resources. + +```bash +# Destroy resources and tear down deployment. Only do this if you want to destroy your deployment. +terraform destroy +``` + +## Testing + +Please confirm that you've completed the `terraform apply` step before running the integration tests as we're validating the active terraform workspace. + +Unit tests can be run using the following command: + +``` +go test -v $(go list ./... | grep "unit") +``` + +Integration tests can be run using the following command: + +``` +go test -v $(go list ./... | grep "integration") +``` + + +## License + +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/infra/templates/osdu-r3-mvp/service_resources/airflow.tf b/infra/templates/osdu-r3-mvp/service_resources/airflow.tf new file mode 100644 index 0000000000000000000000000000000000000000..10eb2fd045083337acafd499d208f1c6beb9d130 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/airflow.tf @@ -0,0 +1,91 @@ +# // Copyright © Microsoft Corporation +# // +# // Licensed under the Apache License, Version 2.0 (the "License"); +# // you may not use this file except in compliance with the License. +# // You may obtain a copy of the License at +# // +# // http://www.apache.org/licenses/LICENSE-2.0 +# // +# // Unless required by applicable law or agreed to in writing, software +# // distributed under the License is distributed on an "AS IS" BASIS, +# // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# // See the License for the specific language governing permissions and +# // limitations under the License. + + +# /* +# .Synopsis +# Terraform Security Control +# .DESCRIPTION +# This file holds airflow specific settings. +# */ + + +#------------------------------- +# Airflow +#------------------------------- +locals { + airflow_admin_password = coalesce(var.airflow_admin_password, random_password.airflow_admin_password[0].result) +} + +resource "random_password" "airflow_admin_password" { + count = var.airflow_admin_password == "" ? 1 : 0 + + length = 8 + special = true + override_special = "_%@" + min_upper = 1 + min_lower = 1 + min_numeric = 1 + min_special = 1 +} + +resource "random_string" "airflow_fernete_key_rnd" { + keepers = { + postgresql_name = local.postgresql_name + } + length = 32 + special = true + min_upper = 1 + min_lower = 1 + min_numeric = 1 + min_special = 1 +} + +// Add the Fernet Key to the Vault +resource "azurerm_key_vault_secret" "airflow_fernet_key_secret" { + name = "airflow-fernet-key" + value = base64encode(random_string.airflow_fernete_key_rnd.result) + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + +// Add the Airflow Admin to the Vault +resource "azurerm_key_vault_secret" "airflow_admin_password" { + name = "airflow-admin-password" + value = local.airflow_admin_password + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + +// Add the Airflow Log Connection to the Vault +resource "azurerm_key_vault_secret" "airflow_remote_log_connection" { + name = "airflow-remote-log-connection" + value = format("wasb://%s:%s@", module.storage_account.name, urlencode(module.storage_account.primary_access_key)) + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + +// Add the Subscription to the Queue +resource "azurerm_eventgrid_event_subscription" "airflow_log_event_subscription" { + name = "airflowlogeventsubscription" + scope = module.storage_account.id + + storage_queue_endpoint { + storage_account_id = module.storage_account.id + queue_name = "airflowlogqueue" + } + + included_event_types = ["Microsoft.Storage.BlobCreated"] + + subject_filter { + subject_begins_with = "/blobServices/default/containers/airflow-logs/blobs" + } +} diff --git a/infra/templates/osdu-r3-mvp/service_resources/config_map.tf b/infra/templates/osdu-r3-mvp/service_resources/config_map.tf new file mode 100644 index 0000000000000000000000000000000000000000..e5839d073bf0aa3820716cbc795e9586c5d2de2c --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/config_map.tf @@ -0,0 +1,52 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +#------------------------------- +# Kubernetes Config Map +#------------------------------- +locals { + osdu_ns = "osdu" +} + +resource "kubernetes_namespace" "osdu" { + metadata { + name = local.osdu_ns + labels = { + "istio-injection" = "enabled" + } + } + + depends_on = [module.aks] +} + + +resource "kubernetes_config_map" "osduconfigmap" { + metadata { + name = "osdu-svc-properties" + namespace = local.osdu_ns + } + + data = { + ENV_TENANT_ID = data.azurerm_client_config.current.tenant_id + ENV_SUBSCRIPTION_NAME = data.azurerm_subscription.current.display_name + ENV_REGISTRY = data.terraform_remote_state.central_resources.outputs.container_registry_name + ENV_KEYVAULT = format("https://%s.vault.azure.net/", data.terraform_remote_state.central_resources.outputs.keyvault_name) + ENV_LOG_WORKSPACE_ID = data.terraform_remote_state.central_resources.outputs.log_analytics_id + ENV_POSTGRES_USERNAME = var.postgres_username + ENV_POSTGRES_HOSTNAME = module.postgreSQL.server_fqdn + } + + depends_on = [kubernetes_namespace.osdu] +} diff --git a/infra/templates/osdu-r3-mvp/service_resources/diagnostics.tf b/infra/templates/osdu-r3-mvp/service_resources/diagnostics.tf new file mode 100644 index 0000000000000000000000000000000000000000..a00adaaae8ccade4a4e5b4c917a3b2fef84e0570 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/diagnostics.tf @@ -0,0 +1,246 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +/* +.Synopsis + Terraform Diagnostics Control +.DESCRIPTION + This file holds diagnostics settings. +*/ + + + +#------------------------------- +# Network +#------------------------------- +resource "azurerm_monitor_diagnostic_setting" "vnet_diagnostics" { + name = "vnet_diagnostics" + target_resource_id = module.network.id + log_analytics_workspace_id = data.terraform_remote_state.central_resources.outputs.log_analytics_id + + log { + category = "VMProtectionAlerts" + enabled = false + + retention_policy { + days = 0 + enabled = false + } + } + + + metric { + category = "AllMetrics" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } +} + +resource "azurerm_monitor_diagnostic_setting" "gw_diagnostics" { + name = "gw_diagnostics" + target_resource_id = module.appgateway.id + log_analytics_workspace_id = data.terraform_remote_state.central_resources.outputs.log_analytics_id + + + log { + category = "ApplicationGatewayAccessLog" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + log { + category = "ApplicationGatewayPerformanceLog" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + log { + category = "ApplicationGatewayFirewallLog" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + metric { + category = "AllMetrics" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } +} + + + +#------------------------------- +# Azure AKS +#------------------------------- +resource "azurerm_monitor_diagnostic_setting" "aks_diagnostics" { + name = "aks_diagnostics" + target_resource_id = module.aks.id + log_analytics_workspace_id = data.terraform_remote_state.central_resources.outputs.log_analytics_id + + log { + category = "cluster-autoscaler" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + log { + category = "guard" + enabled = false + + retention_policy { + days = 0 + enabled = false + } + } + + log { + category = "kube-apiserver" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + log { + category = "kube-audit" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + log { + category = "kube-audit-admin" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + log { + category = "kube-controller-manager" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + log { + category = "kube-scheduler" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + metric { + category = "AllMetrics" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } +} + + + +#------------------------------- +# PostgreSQL +#------------------------------- +resource "azurerm_monitor_diagnostic_setting" "postgres_diagnostics" { + name = "postgres_diagnostics" + target_resource_id = module.postgreSQL.server_id + log_analytics_workspace_id = data.terraform_remote_state.central_resources.outputs.log_analytics_id + + log { + category = "PostgreSQLLogs" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } + + log { + category = "QueryStoreRuntimeStatistics" + + retention_policy { + enabled = false + } + } + + log { + category = "QueryStoreWaitStatistics" + + retention_policy { + enabled = false + } + } + + + metric { + category = "AllMetrics" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } +} + + + +#------------------------------- +# Azure Redis Cache +#------------------------------- +resource "azurerm_monitor_diagnostic_setting" "redis_diagnostics" { + name = "redis_diagnostics" + target_resource_id = module.redis_cache.id + log_analytics_workspace_id = data.terraform_remote_state.central_resources.outputs.log_analytics_id + + + metric { + category = "AllMetrics" + + retention_policy { + days = var.log_retention_days + enabled = local.retention_policy + } + } +} diff --git a/infra/templates/osdu-r3-mvp/service_resources/helm_agic.tf b/infra/templates/osdu-r3-mvp/service_resources/helm_agic.tf new file mode 100644 index 0000000000000000000000000000000000000000..d6a961724dc8b5b45f5ea3c25ffef58f2b07ebcf --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/helm_agic.tf @@ -0,0 +1,94 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +#------------------------------- +# Application Gateway Ingress Controller +#------------------------------- +locals { + helm_agic_name = "agic" + helm_agic_ns = "agic" + helm_agic_repo = "https://appgwingress.blob.core.windows.net/ingress-azure-helm-package/" + helm_agic_version = "1.2.0" +} + + +resource "kubernetes_namespace" "agic" { + metadata { + name = local.helm_agic_ns + } + + depends_on = [module.aks] +} + +resource "helm_release" "agic" { + name = local.helm_agic_name + repository = local.helm_agic_repo + chart = "ingress-azure" + version = local.helm_agic_version + namespace = kubernetes_namespace.agic.metadata.0.name + + + set { + name = "appgw.subscriptionId" + value = data.azurerm_client_config.current.subscription_id + } + + set { + name = "appgw.resourceGroup" + value = azurerm_resource_group.main.name + } + + set { + name = "appgw.name" + value = module.appgateway.name + } + + set { + name = "armAuth.identityResourceID" + value = azurerm_user_assigned_identity.agicidentity.id + } + + set { + name = "armAuth.identityClientID" + value = azurerm_user_assigned_identity.agicidentity.client_id + } + + set { + name = "armAuth.type" + value = "aadPodIdentity" + } + + set { + name = "appgw.shared" + value = false + } + + set { + name = "appgw.usePrivateIP" + value = false + } + + set { + name = "rbac.enabled" + value = true + } + + set { + name = "verbosityLevel" + value = 5 + } + + depends_on = [helm_release.aad_pod_id] +} diff --git a/infra/templates/osdu-r3-mvp/service_resources/helm_certs.tf b/infra/templates/osdu-r3-mvp/service_resources/helm_certs.tf new file mode 100644 index 0000000000000000000000000000000000000000..976f38e1a2b828bef6bbba48bf6a06c23bc3c963 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/helm_certs.tf @@ -0,0 +1,49 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +#------------------------------- +# Certificate Manager +#------------------------------- +locals { + helm_certs_name = "jetstack" + helm_certs_ns = "cert-manager" + helm_certs_repo = "https://charts.jetstack.io" + helm_certs_version = "v0.16.1" +} + +resource "kubernetes_namespace" "certs" { + metadata { + name = local.helm_certs_ns + labels = { + "cert-manager.io/disable-validation" = "true" + } + } + + depends_on = [module.aks] +} + +resource "helm_release" "certmgr" { + name = local.helm_certs_name + repository = local.helm_certs_repo + chart = "cert-manager" + version = local.helm_certs_version + namespace = local.helm_certs_ns + depends_on = [kubernetes_namespace.certs] + + set { + name = "installCRDs" + value = true + } +} diff --git a/infra/templates/osdu-r3-mvp/service_resources/helm_flux.tf b/infra/templates/osdu-r3-mvp/service_resources/helm_flux.tf new file mode 100644 index 0000000000000000000000000000000000000000..c975069c5720ecc8af29a5c6ec3205c7d9e38335 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/helm_flux.tf @@ -0,0 +1,103 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +#------------------------------- +# Flux +#------------------------------- +locals { + helm_flux_name = "flux" + helm_flux_ns = "flux" + helm_flux_repo = "https://charts.fluxcd.io" + helm_flux_version = "1.5.0" + helm_flux_secret = "flux-git-deploy" +} + +resource "kubernetes_namespace" "flux" { + metadata { + name = local.helm_flux_ns + } + + depends_on = [module.aks] +} + +resource "kubernetes_secret" "flux_ssh" { + metadata { + name = local.helm_flux_secret + namespace = local.helm_flux_ns + } + + type = "Opaque" + + data = { + identity = file(var.gitops_ssh_key_file) + } + + depends_on = [kubernetes_namespace.flux] +} + +resource "helm_release" "flux" { + name = local.helm_flux_name + repository = local.helm_flux_repo + chart = "flux" + version = local.helm_flux_version + namespace = local.helm_flux_ns + + set { + name = "git.url" + value = var.gitops_ssh_url + } + + set { + name = "git.branch" + value = var.gitops_branch + } + + set { + name = "git.secretName" + value = local.helm_flux_secret + } + + set { + name = "git.path" + value = var.gitops_path + } + + set { + name = "git.pollInterval" + value = "5m" + } + + set { + name = "git.label" + value = "flux-sync" + } + + set { + name = "registry.acr.enabled" + value = "true" + } + + set { + name = "syncGarbageCollection.enabled" + value = "true" + } + + set { + name = "serviceAccount.name" + value = "flux" + } + + depends_on = [kubernetes_namespace.flux, kubernetes_secret.flux_ssh] +} \ No newline at end of file diff --git a/infra/templates/osdu-r3-mvp/service_resources/helm_keda.tf b/infra/templates/osdu-r3-mvp/service_resources/helm_keda.tf new file mode 100644 index 0000000000000000000000000000000000000000..367426b45c5c15046560991930c61b03a919e13b --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/helm_keda.tf @@ -0,0 +1,42 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +#------------------------------- +# Keda +#------------------------------- +locals { + helm_keda_name = "keda" + helm_keda_ns = "keda" + helm_keda_repo = "https://kedacore.github.io/charts" + helm_keda_version = "1.4" +} + +resource "kubernetes_namespace" "keda" { + metadata { + name = local.helm_keda_ns + } + + depends_on = [module.aks] +} + +resource "helm_release" "keda" { + name = local.helm_keda_name + repository = local.helm_keda_repo + chart = "keda" + version = local.helm_keda_version + namespace = local.helm_keda_ns + + depends_on = [kubernetes_namespace.keda] +} \ No newline at end of file diff --git a/infra/templates/osdu-r3-mvp/service_resources/helm_kv_csi.tf b/infra/templates/osdu-r3-mvp/service_resources/helm_kv_csi.tf new file mode 100644 index 0000000000000000000000000000000000000000..343521aa0f660a06d9a55cd1c8ace34dc14f2127 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/helm_kv_csi.tf @@ -0,0 +1,47 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +#------------------------------- +# KeyVault Secret Driver +#------------------------------- +locals { + helm_kv_csi_name = "kvsecrets" + helm_kv_csi_ns = "kvsecrets" + helm_kv_csi_repo = "https://raw.githubusercontent.com/Azure/secrets-store-csi-driver-provider-azure/master/charts" + helm_kv_csi_version = "0.0.9" +} + +resource "kubernetes_namespace" "kvsecrets" { + metadata { + name = local.helm_kv_csi_ns + } + + depends_on = [module.aks] +} + +resource "helm_release" "kvsecrets" { + name = local.helm_kv_csi_name + repository = local.helm_kv_csi_repo + chart = "csi-secrets-store-provider-azure" + version = local.helm_kv_csi_version + namespace = local.helm_kv_csi_ns + + set { + name = "secrets-store-csi-driver.linux.metricsAddr" + value = ":8081" + } + + depends_on = [kubernetes_namespace.kvsecrets] +} \ No newline at end of file diff --git a/infra/templates/osdu-r3-mvp/service_resources/helm_pod_identity.tf b/infra/templates/osdu-r3-mvp/service_resources/helm_pod_identity.tf new file mode 100644 index 0000000000000000000000000000000000000000..fc4302eddbc00a43f4598803701be3327e1bb0c0 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/helm_pod_identity.tf @@ -0,0 +1,85 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +#------------------------------- +# Pod Identity +#------------------------------- +locals { + pod_identity_name = "${local.aks_cluster_name}-pod-identity" + helm_pod_identity_name = "aad-pod-identity" + helm_pod_identity_ns = "podidentity" + helm_pod_identity_repo = "https://raw.githubusercontent.com/Azure/aad-pod-identity/master/charts" + helm_pod_identity_version = "2.0.0" +} + +resource "kubernetes_namespace" "pod_identity" { + metadata { + name = local.helm_pod_identity_ns + } + + depends_on = [module.aks] +} + +resource "helm_release" "aad_pod_id" { + name = local.helm_pod_identity_name + repository = local.helm_pod_identity_repo + chart = "aad-pod-identity" + version = local.helm_pod_identity_version + namespace = kubernetes_namespace.pod_identity.metadata.0.name + + + set { + name = "azureIdentities[0].enabled" + value = true + } + + set { + name = "azureIdentities[0].type" + value = 0 + } + + set { + name = "azureIdentities[0].namespace" + value = kubernetes_namespace.pod_identity.metadata.0.name + } + + set { + name = "azureIdentities[0].name" + value = "podidentity" + } + + set { + name = "azureIdentities[0].resourceID" + value = azurerm_user_assigned_identity.podidentity.id + } + + set { + name = "azureIdentities[0].clientID" + value = azurerm_user_assigned_identity.podidentity.principal_id + } + + + set { + name = "azureIdentities[0].binding.selector" + value = "podidentity" + } + + set { + name = "azureIdentities[0].binding.name" + value = "podidentitybinding" + } + + depends_on = [kubernetes_namespace.pod_identity, azurerm_user_assigned_identity.podidentity] +} \ No newline at end of file diff --git a/infra/templates/osdu-r3-mvp/service_resources/main.tf b/infra/templates/osdu-r3-mvp/service_resources/main.tf new file mode 100644 index 0000000000000000000000000000000000000000..1cd29804e0a7c5f0f2f5ea5042a1ef3efeed2665 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/main.tf @@ -0,0 +1,466 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +/* +.Synopsis + Terraform Main Control +.DESCRIPTION + This file holds the main control and resoures for bootstraping an OSDU Azure Devops Project. +*/ + +// *** WARNING **** +// This template makes changes into the Central Resources and the locks in Central have to be removed to delete. +// Lock: Key Vault +// Lock: Container Registry +// *** WARNING **** + +terraform { + required_version = ">= 0.12" + backend "azurerm" { + key = "terraform.tfstate" + } +} + + +#------------------------------- +# Providers +#------------------------------- +provider "azurerm" { + version = "=2.29.0" + features {} +} + +provider "azuread" { + version = "=1.0.0" +} + +provider "random" { + version = "~>2.2" +} + +provider "external" { + version = "~> 1.0" +} + +provider "local" { + version = "~> 1.4" +} + +provider "null" { + version = "~>2.1.0" +} + +// Hook-up kubectl Provider for Terraform +provider "kubernetes" { + version = "~> 1.11.3" + load_config_file = false + host = module.aks.kube_config_block.0.host + username = module.aks.kube_config_block.0.username + password = module.aks.kube_config_block.0.password + client_certificate = base64decode(module.aks.kube_config_block.0.client_certificate) + client_key = base64decode(module.aks.kube_config_block.0.client_key) + cluster_ca_certificate = base64decode(module.aks.kube_config_block.0.cluster_ca_certificate) +} + +// Hook-up helm Provider for Terraform +provider "helm" { + version = "~> 1.2.3" + + kubernetes { + load_config_file = false + host = module.aks.kube_config_block.0.host + username = module.aks.kube_config_block.0.username + password = module.aks.kube_config_block.0.password + client_certificate = base64decode(module.aks.kube_config_block.0.client_certificate) + client_key = base64decode(module.aks.kube_config_block.0.client_key) + cluster_ca_certificate = base64decode(module.aks.kube_config_block.0.cluster_ca_certificate) + } +} + + + +#------------------------------- +# Private Variables +#------------------------------- +locals { + // sanitize names + prefix = replace(trimspace(lower(var.prefix)), "_", "-") + workspace = replace(trimspace(lower(terraform.workspace)), "-", "") + suffix = var.randomization_level > 0 ? "-${random_string.workspace_scope.result}" : "" + + // base prefix for resources, prefix constraints documented here: https://docs.microsoft.com/en-us/azure/architecture/best-practices/naming-conventions + base_name = length(local.prefix) > 0 ? "${local.prefix}-${local.workspace}${local.suffix}" : "${local.workspace}${local.suffix}" + base_name_21 = length(local.base_name) < 22 ? local.base_name : "${substr(local.base_name, 0, 21 - length(local.suffix))}${local.suffix}" + base_name_46 = length(local.base_name) < 47 ? local.base_name : "${substr(local.base_name, 0, 46 - length(local.suffix))}${local.suffix}" + base_name_60 = length(local.base_name) < 61 ? local.base_name : "${substr(local.base_name, 0, 60 - length(local.suffix))}${local.suffix}" + base_name_76 = length(local.base_name) < 77 ? local.base_name : "${substr(local.base_name, 0, 76 - length(local.suffix))}${local.suffix}" + base_name_83 = length(local.base_name) < 84 ? local.base_name : "${substr(local.base_name, 0, 83 - length(local.suffix))}${local.suffix}" + + tenant_id = data.azurerm_client_config.current.tenant_id + resource_group_name = format("%s-%s-%s-rg", var.prefix, local.workspace, random_string.workspace_scope.result) + retention_policy = var.log_retention_days == 0 ? false : true + + storage_name = "${replace(local.base_name_21, "-", "")}config" + + redis_cache_name = "${local.base_name}-cache" + postgresql_name = "${local.base_name}-pg" + + vnet_name = "${local.base_name_60}-vnet" + fe_subnet_name = "${local.base_name_21}-fe-subnet" + aks_subnet_name = "${local.base_name_21}-aks-subnet" + be_subnet_name = "${local.base_name_21}-be-subnet" + app_gw_name = "${local.base_name_60}-gw" + appgw_identity_name = format("%s-agic-identity", local.app_gw_name) + + + aks_cluster_name = "${local.base_name_60}-aks" + aks_identity_name = format("%s-pod-identity", local.aks_cluster_name) + aks_dns_prefix = local.base_name_60 + + role = "Contributor" + rbac_principals = [ + // OSDU Identity + data.terraform_remote_state.central_resources.outputs.osdu_identity_principal_id, + + // Service Principal + data.terraform_remote_state.central_resources.outputs.principal_objectId + ] +} + + + +#------------------------------- +# Common Resources +#------------------------------- +data "azurerm_client_config" "current" {} +data "azurerm_subscription" "current" {} + +data "terraform_remote_state" "central_resources" { + backend = "azurerm" + + config = { + storage_account_name = var.remote_state_account + container_name = var.remote_state_container + key = format("terraform.tfstateenv:%s", var.central_resources_workspace_name) + } +} + +resource "random_string" "workspace_scope" { + keepers = { + # Generate a new id each time we switch to a new workspace or app id + ws_name = replace(trimspace(lower(terraform.workspace)), "_", "-") + cluster_id = replace(trimspace(lower(var.prefix)), "_", "-") + } + + length = max(1, var.randomization_level) // error for zero-length + special = false + upper = false +} + + + +#------------------------------- +# Resource Group +#------------------------------- +resource "azurerm_resource_group" "main" { + name = local.resource_group_name + location = var.resource_group_location + + tags = var.resource_tags + + lifecycle { + ignore_changes = [tags] + } +} + + +#------------------------------- +# User Assigned Identities +#------------------------------- + +// Create an Identity for Pod Identity +resource "azurerm_user_assigned_identity" "podidentity" { + name = local.aks_identity_name + resource_group_name = azurerm_resource_group.main.name + location = azurerm_resource_group.main.location +} + +// Create and Identity for AGIC +resource "azurerm_user_assigned_identity" "agicidentity" { + name = local.appgw_identity_name + resource_group_name = azurerm_resource_group.main.name + location = azurerm_resource_group.main.location +} + + + +#------------------------------- +# Storage +#------------------------------- +module "storage_account" { + source = "../../../modules/providers/azure/storage-account" + + name = local.storage_name + resource_group_name = azurerm_resource_group.main.name + container_names = var.storage_containers + share_names = var.storage_shares + queue_names = var.storage_queues + kind = "StorageV2" + replication_type = var.storage_replication_type + + resource_tags = var.resource_tags +} + +// Add Contributor Role Access +resource "azurerm_role_assignment" "storage_access" { + count = length(local.rbac_principals) + + role_definition_name = local.role + principal_id = local.rbac_principals[count.index] + scope = module.storage_account.id +} + +// Add Storage Queue Data Reader Role Access +resource "azurerm_role_assignment" "queue_reader" { + count = length(local.rbac_principals) + + role_definition_name = "Storage Queue Data Reader" + principal_id = local.rbac_principals[count.index] + scope = module.storage_account.id +} + +// Add Storage Queue Data Message Processor Role Access +resource "azurerm_role_assignment" "airflow_log_queue_processor_roles" { + count = length(local.rbac_principals) + + role_definition_name = "Storage Queue Data Message Processor" + principal_id = local.rbac_principals[count.index] + scope = module.storage_account.id +} + + + +#------------------------------- +# Network +#------------------------------- +module "network" { + source = "../../../modules/providers/azure/network" + + name = local.vnet_name + resource_group_name = azurerm_resource_group.main.name + address_space = var.address_space + subnet_prefixes = [var.subnet_fe_prefix, var.subnet_aks_prefix] + subnet_names = [local.fe_subnet_name, local.aks_subnet_name] + subnet_service_endpoints = { + (local.aks_subnet_name) = ["Microsoft.Storage", + "Microsoft.Sql", + "Microsoft.AzureCosmosDB", + "Microsoft.KeyVault", + "Microsoft.ServiceBus", + "Microsoft.EventHub"] + } + + resource_tags = var.resource_tags +} + +module "appgateway" { + source = "../../../modules/providers/azure/appgw" + + name = local.app_gw_name + resource_group_name = azurerm_resource_group.main.name + + vnet_name = module.network.name + vnet_subnet_id = module.network.subnets.0 + keyvault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id + keyvault_secret_id = azurerm_key_vault_certificate.default.0.secret_id + ssl_certificate_name = local.ssl_cert_name + + resource_tags = var.resource_tags +} + +// Give AGIC Identity Access rights to Change the Application Gateway +resource "azurerm_role_assignment" "appgwcontributor" { + principal_id = azurerm_user_assigned_identity.agicidentity.principal_id + scope = module.appgateway.id + role_definition_name = "Contributor" +} + +// Give AGIC Identity the rights to look at the Resource Group +resource "azurerm_role_assignment" "agic_resourcegroup_reader" { + principal_id = azurerm_user_assigned_identity.agicidentity.principal_id + scope = azurerm_resource_group.main.id + role_definition_name = "Reader" +} + +// Give AGIC Identity rights to Operate the Gateway Identity +resource "azurerm_role_assignment" "agic_app_gw_mi" { + principal_id = azurerm_user_assigned_identity.agicidentity.principal_id + scope = module.appgateway.managed_identity_resource_id + role_definition_name = "Managed Identity Operator" +} + + + +#------------------------------- +# Azure AKS +#------------------------------- +module "aks" { + source = "../../../modules/providers/azure/aks" + + name = local.aks_cluster_name + resource_group_name = azurerm_resource_group.main.name + + dns_prefix = local.aks_dns_prefix + agent_vm_count = var.aks_agent_vm_count + agent_vm_size = var.aks_agent_vm_size + vnet_subnet_id = module.network.subnets.1 + ssh_public_key = file(var.ssh_public_key_file) + kubernetes_version = var.kubernetes_version + log_analytics_id = data.terraform_remote_state.central_resources.outputs.log_analytics_id + + msi_enabled = true + oms_agent_enabled = true + auto_scaling_default_node = true + kubeconfig_to_disk = false + enable_kube_dashboard = false + + resource_tags = var.resource_tags +} + +data "azurerm_resource_group" "aks_node_resource_group" { + name = module.aks.node_resource_group +} + +// Give AKS Access rights to Operate the Node Resource Group +resource "azurerm_role_assignment" "all_mi_operator" { + principal_id = module.aks.kubelet_object_id + scope = data.azurerm_resource_group.aks_node_resource_group.id + role_definition_name = "Managed Identity Operator" +} + +// Give AKS Access to Create and Remove VM's in Node Resource Group +resource "azurerm_role_assignment" "vm_contributor" { + principal_id = module.aks.kubelet_object_id + scope = data.azurerm_resource_group.aks_node_resource_group.id + role_definition_name = "Virtual Machine Contributor" +} + +// Give AKS Access to Pull from ACR +resource "azurerm_role_assignment" "acr_reader" { + principal_id = module.aks.kubelet_object_id + scope = data.terraform_remote_state.central_resources.outputs.container_registry_id + role_definition_name = "AcrPull" +} + +// Give AKS Rights to operate the AGIC Identity +resource "azurerm_role_assignment" "mi_ag_operator" { + principal_id = module.aks.kubelet_object_id + scope = azurerm_user_assigned_identity.agicidentity.id + role_definition_name = "Managed Identity Operator" +} + +// Give AKS Access Rights to operate the Pod Identity +resource "azurerm_role_assignment" "mi_operator" { + principal_id = module.aks.kubelet_object_id + scope = azurerm_user_assigned_identity.podidentity.id + role_definition_name = "Managed Identity Operator" +} + +// Give AKS Access Rights to operate the OSDU Identity +resource "azurerm_role_assignment" "osdu_identity_mi_operator" { + principal_id = module.aks.kubelet_object_id + scope = data.terraform_remote_state.central_resources.outputs.osdu_identity_id + role_definition_name = "Managed Identity Operator" +} + + + +#------------------------------- +# PostgreSQL +#------------------------------- +resource "random_password" "postgres" { + count = var.postgres_password == "" ? 1 : 0 + + length = 8 + special = true + override_special = "_%@" + min_upper = 1 + min_lower = 1 + min_numeric = 1 + min_special = 1 +} + +module "postgreSQL" { + source = "../../../modules/providers/azure/postgreSQL" + + resource_group_name = azurerm_resource_group.main.name + name = local.postgresql_name + databases = var.postgres_databases + admin_user = var.postgres_username + admin_password = local.postgres_password + sku = var.postgres_sku + postgresql_configurations = var.postgres_configurations + + storage_mb = 5120 + server_version = "10.0" + backup_retention_days = 7 + geo_redundant_backup_enabled = true + auto_grow_enabled = true + ssl_enforcement_enabled = true + + public_network_access = true + firewall_rules = [{ + start_ip = "0.0.0.0" + end_ip = "0.0.0.0" + }] + + resource_tags = var.resource_tags +} + +// Add Contributor Role Access +resource "azurerm_role_assignment" "postgres_access" { + count = length(local.rbac_principals) + + role_definition_name = local.role + principal_id = local.rbac_principals[count.index] + scope = module.postgreSQL.server_id +} + + + +#------------------------------- +# Azure Redis Cache +#------------------------------- +module "redis_cache" { + source = "../../../modules/providers/azure/redis-cache" + + name = local.redis_cache_name + resource_group_name = azurerm_resource_group.main.name + capacity = var.redis_capacity + + memory_features = var.redis_config_memory + premium_tier_config = var.redis_config_schedule + + resource_tags = var.resource_tags +} + +// Add Contributor Role Access +resource "azurerm_role_assignment" "redis_cache" { + count = length(local.rbac_principals) + + role_definition_name = local.role + principal_id = local.rbac_principals[count.index] + scope = module.redis_cache.id +} diff --git a/infra/templates/osdu-r3-mvp/service_resources/output.tf b/infra/templates/osdu-r3-mvp/service_resources/output.tf new file mode 100644 index 0000000000000000000000000000000000000000..d56df744dc2910c5c4df2a74af5da84844d71809 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/output.tf @@ -0,0 +1,89 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +.Synopsis + Terraform Output Configuration +.DESCRIPTION + This file holds the Output Configuration +*/ + + +#------------------------------- +# Output Variables +#------------------------------- +output "services_resource_group_name" { + description = "The name of the resource group containing the data specific resources" + value = azurerm_resource_group.main.name +} + +output "services_resource_group_id" { + description = "The resource id for the provisioned resource group" + value = azurerm_resource_group.main.id +} + +output "storage_account" { + description = "The name of the storage account." + value = module.storage_account.name +} + +output "storage_account_id" { + description = "The resource id of the storage account instance" + value = module.storage_account.id +} + +output "storage_containers" { + description = "Map of storage account containers." + value = module.storage_account.containers +} + +output "storage_shares" { + description = "Map of storage account shares." + value = module.storage_account.shares +} + +output "storage_queues" { + description = "Map of storage account queues." + value = module.storage_account.queues +} + +// Network Output Items for Integration Tests +output "appgw_name" { + description = "Application gateway's name" + value = module.appgateway.name +} + +output "keyvault_secret_id" { + description = "The keyvault certificate keyvault resource id used to setup ssl termination on the app gateway." + value = azurerm_key_vault_certificate.default.0.secret_id +} + + +// Redis Output Items for Integration Tests +output "redis_name" { + description = "The name of the redis_cache" + value = module.redis_cache.name +} + +output "redis_hostname" { + value = module.redis_cache.hostname +} + +output "redis_primary_access_key" { + value = module.redis_cache.primary_access_key +} + +output "redis_ssl_port" { + value = module.redis_cache.ssl_port +} \ No newline at end of file diff --git a/infra/templates/osdu-r3-mvp/service_resources/secrets.tf b/infra/templates/osdu-r3-mvp/service_resources/secrets.tf new file mode 100644 index 0000000000000000000000000000000000000000..e368e2677c0880ec93e3e3ad4b7be6e51c260828 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/secrets.tf @@ -0,0 +1,153 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +/* +.Synopsis + Terraform Secrets Control +.DESCRIPTION + This file holds Key Vault Secrets. +*/ + +#------------------------------- +# Misc +#------------------------------- +resource "azurerm_key_vault_secret" "base_name_sr" { + name = "base-name-sr" + value = local.base_name_60 + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + + +#------------------------------- +# Storage +#------------------------------- +locals { + storage_account_name = "airflow-storage" + storage_key_name = "${local.storage_account_name}-key" + storage_connection_name = "${local.storage_account_name}-connection" +} + +resource "azurerm_key_vault_secret" "storage_name" { + name = local.storage_account_name + value = module.storage_account.name + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + +resource "azurerm_key_vault_secret" "storage_key" { + name = local.storage_key_name + value = module.storage_account.primary_access_key + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + +resource "azurerm_key_vault_secret" "storage_connection" { + name = local.storage_connection_name + value = format("DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=%s;EndpointSuffix=core.windows.net", local.storage_account_name, module.storage_account.primary_access_key) + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + + + +#------------------------------- +# Network +#------------------------------- +locals { + ssl_cert_name = "appgw-ssl-cert" +} + +resource "azurerm_key_vault_certificate" "default" { + count = var.ssl_certificate_file == "" ? 1 : 0 + + name = local.ssl_cert_name + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id + + certificate_policy { + issuer_parameters { + name = "Self" + } + + key_properties { + exportable = true + key_size = 2048 + key_type = "RSA" + reuse_key = true + } + + lifetime_action { + action { + action_type = "AutoRenew" + } + + trigger { + days_before_expiry = 30 + } + } + + secret_properties { + content_type = "application/x-pkcs12" + } + + x509_certificate_properties { + # Server Authentication = 1.3.6.1.5.5.7.3.1 + # Client Authentication = 1.3.6.1.5.5.7.3.2 + extended_key_usage = ["1.3.6.1.5.5.7.3.1"] + + key_usage = [ + "cRLSign", + "dataEncipherment", + "digitalSignature", + "keyAgreement", + "keyCertSign", + "keyEncipherment", + ] + + subject_alternative_names { + dns_names = [var.dns_name, "${local.base_name}-gw.${azurerm_resource_group.main.location}.cloudapp.azure.com"] + } + + subject = "CN=*.contoso.com" + validity_in_months = 12 + } + } +} + + +#------------------------------- +# PostgreSQL +#------------------------------- +locals { + postgres_password_name = "postgres-password" + postgres_password = coalesce(var.postgres_password, random_password.postgres[0].result) +} + +resource "azurerm_key_vault_secret" "postgres_password" { + name = local.postgres_password_name + value = local.postgres_password + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} + + + +#------------------------------- +# Azure Redis Cache +#------------------------------- +locals { + redis_password_name = "redis-password" +} + +resource "azurerm_key_vault_secret" "redis_password" { + name = local.redis_password_name + value = module.redis_cache.primary_access_key + key_vault_id = data.terraform_remote_state.central_resources.outputs.keyvault_id +} \ No newline at end of file diff --git a/infra/templates/osdu-r3-mvp/service_resources/terraform.tfvars b/infra/templates/osdu-r3-mvp/service_resources/terraform.tfvars new file mode 100644 index 0000000000000000000000000000000000000000..c0445a0c797159aa05037199727a987d503a7a8a --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/terraform.tfvars @@ -0,0 +1,39 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +.Synopsis + Terraform Variable Configuration +.DESCRIPTION + This file holds the Default Variable Configuration +*/ + +prefix = "osdu-mvp" + +resource_tags = { + contact = "pipeline" +} + +# Storage Settings +storage_replication_type = "LRS" +storage_containers = [ + "azure-webjobs-hosts", + "airflow-logs" +] +storage_shares = [ + "airflowdags" +] +storage_queues = [ + "airflowlogqueue" +] \ No newline at end of file diff --git a/infra/templates/osdu-r3-mvp/service_resources/tests/integration/integration_test.go b/infra/templates/osdu-r3-mvp/service_resources/tests/integration/integration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c67d450cdb3766da8e4afc9f6be08fdcb2bf3836 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/tests/integration/integration_test.go @@ -0,0 +1,52 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "os" + "testing" + + "github.com/gruntwork-io/terratest/modules/terraform" + appGatewayIntegTests "github.com/microsoft/cobalt/infra/modules/providers/azure/aks-appgw/tests/integration" + redisIntegTests "github.com/microsoft/cobalt/infra/modules/providers/azure/redis-cache/tests/integration" + storageIntegTests "github.com/microsoft/cobalt/infra/modules/providers/azure/storage-account/tests/integration" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var subscription = os.Getenv("ARM_SUBSCRIPTION_ID") +var tfOptions = &terraform.Options{ + TerraformDir: "../../", + BackendConfig: map[string]interface{}{ + "storage_account_name": os.Getenv("TF_VAR_remote_state_account"), + "container_name": os.Getenv("TF_VAR_remote_state_container"), + }, +} + +// Runs a suite of test assertions to validate that a provisioned data source environment +// is fully functional. +func TestDataEnvironment(t *testing.T) { + testFixture := infratests.IntegrationTestFixture{ + GoTest: t, + TfOptions: tfOptions, + ExpectedTfOutputCount: 13, + TfOutputAssertions: []infratests.TerraformOutputValidation{ + storageIntegTests.InspectStorageAccount("storage_account", "storage_containers", "services_resource_group_name"), + redisIntegTests.InspectProvisionedCache("redis_name", "services_resource_group_name"), + redisIntegTests.CheckRedisWriteOperations("redis_hostname", "redis_primary_access_key", "redis_ssl_port"), + appGatewayIntegTests.InspectAppGateway("services_resource_group_name", "appgw_name", "keyvault_secret_id"), + }, + } + infratests.RunIntegrationTests(&testFixture) +} diff --git a/infra/templates/osdu-r3-mvp/service_resources/tests/unit/common.go b/infra/templates/osdu-r3-mvp/service_resources/tests/unit/common.go new file mode 100644 index 0000000000000000000000000000000000000000..1904c24e3301d66572e43a03a7ec973c48821c81 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/tests/unit/common.go @@ -0,0 +1,36 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "encoding/json" + "github.com/gruntwork-io/terratest/modules/random" + "strings" + "testing" +) + +// these are useful values used in many test +var region = "centralus" +var prefix = "osdu-testing" + strings.ToLower(random.UniqueId()) +var workspace = "osdu-testing-" + strings.ToLower(random.UniqueId()) + +// helper function to parse blocks of JSON into a generic Go map +func asMap(t *testing.T, jsonString string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonString), &theMap); err != nil { + t.Fatal(err) + } + return theMap +} diff --git a/infra/templates/osdu-r3-mvp/service_resources/tests/unit/unit_test.go b/infra/templates/osdu-r3-mvp/service_resources/tests/unit/unit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e778b9072a213b6224cc8ddcec70645f5facd42f --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/tests/unit/unit_test.go @@ -0,0 +1,57 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package test + +import ( + "os" + "testing" + + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var tfOptions = &terraform.Options{ + TerraformDir: "../../", + Upgrade: true, + Vars: map[string]interface{}{ + "resource_group_location": region, + "prefix": prefix, + }, + BackendConfig: map[string]interface{}{ + "storage_account_name": os.Getenv("TF_VAR_remote_state_account"), + "container_name": os.Getenv("TF_VAR_remote_state_container"), + }, +} + +func TestTemplate(t *testing.T) { + expectedAppDevResourceGroup := asMap(t, `{ + "location": "`+region+`" + }`) + + resourceDescription := infratests.ResourceDescription{ + "azurerm_resource_group.main": expectedAppDevResourceGroup, + } + + testFixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tfOptions, + Workspace: workspace, + PlanAssertions: nil, + ExpectedResourceCount: 82, + ExpectedResourceAttributeValues: resourceDescription, + } + + infratests.RunUnitTests(&testFixture) +} diff --git a/infra/templates/osdu-r3-mvp/service_resources/variables.tf b/infra/templates/osdu-r3-mvp/service_resources/variables.tf new file mode 100644 index 0000000000000000000000000000000000000000..4cdaadefd4efb0e2567aacb7ae044c35df57ea79 --- /dev/null +++ b/infra/templates/osdu-r3-mvp/service_resources/variables.tf @@ -0,0 +1,247 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +.Synopsis + Terraform Variable Configuration +.DESCRIPTION + This file holds the Variable Configuration +*/ + + +#------------------------------- +# Application Variables +#------------------------------- +variable "prefix" { + description = "(Required) An identifier used to construct the names of all resources in this template." + type = string +} + +variable "randomization_level" { + description = "Number of additional random characters to include in resource names to insulate against unexpected resource name collisions." + type = number + default = 4 +} + +variable "remote_state_account" { + description = "Remote Terraform State Azure storage account name. This is typically set as an environment variable and used for the initial terraform init." + type = string +} + +variable "remote_state_container" { + description = "Remote Terraform State Azure storage container name. This is typically set as an environment variable and used for the initial terraform init." + type = string +} + +variable "central_resources_workspace_name" { + description = "(Required) The workspace name for the central_resources repository terraform environment / template to reference for this template." + type = string +} + +variable "resource_group_location" { + description = "(Required) The Azure region where all resources in this template should be created." + type = string +} + +variable "resource_tags" { + description = "Map of tags to apply to this template." + type = map(string) + default = {} +} + +variable "log_retention_days" { + description = "Number of days to retain logs." + type = number + default = 100 +} + +variable "storage_replication_type" { + description = "Defines the type of replication to use for this storage account. Valid options are LRS*, GRS, RAGRS and ZRS." + type = string + default = "LRS" +} + +variable "storage_containers" { + description = "The list of storage containers names to create. Names must be unique per storage account." + type = list(string) +} + +variable "storage_shares" { + description = "The list of storage share names to create. Names must be unique per storage account." + type = list(string) +} + +variable "storage_queues" { + description = "The list of storage queue names to create. Names must be unique per storage account." + type = list(string) +} + +variable "redis_config_schedule" { + description = "Configures the weekly schedule for server patching (Patch Window lasts for 5 hours). Also enables a single cluster for premium tier and when enabled, the true cache capacity of a redis cluster is capacity * cache_shard_count. 10 is the maximum number of shards/nodes allowed." + type = object({ + server_patch_day = string + server_patch_hour = number + cache_shard_count = number + }) + default = { + server_patch_day = "Friday" + server_patch_hour = 17 + cache_shard_count = 0 + } +} + +variable "redis_config_memory" { + description = "Configures memory management for standard & premium tier accounts. All number values are in megabytes. maxmemory_policy_cfg property controls how Redis will select what to remove when maxmemory is reached." + type = object({ + maxmemory_reserved = number + maxmemory_delta = number + maxmemory_policy = string + maxfragmentationmemory_reserved = number + }) + default = { + maxmemory_reserved = 50 + maxmemory_delta = 50 + maxmemory_policy = "volatile-lru" + maxfragmentationmemory_reserved = 50 + } +} + +variable "redis_capacity" { + description = "The size of the Redis cache to deploy. When premium account is enabled with clusters, the true capacity of the account cache is capacity * cache_shard_count" + type = number + default = 1 +} + +variable "postgres_databases" { + description = "The list of names of the PostgreSQL Database, which needs to be a valid PostgreSQL identifier. Changing this forces a new resource to be created." + default = [ + "airflow" + ] +} + +variable "postgres_username" { + description = "The Administrator Login for the PostgreSQL Server. Changing this forces a new resource to be created." + type = string + default = "osdu_admin" +} + +variable "postgres_password" { + description = "The Password associated with the administrator_login for the PostgreSQL Server." + type = string + default = "" +} + +variable "postgres_sku" { + description = "Name of the sku" + type = string + default = "GP_Gen5_4" +} + +variable "postgres_configurations" { + description = "A map with PostgreSQL configurations to enable." + type = map(string) + default = {} +} + +variable "airflow_admin_password" { + description = "Airflow admin password" + type = string + default = "" +} + +variable "dns_name" { + description = "Default DNS Name for the Public IP" + type = string + default = "osdu.contoso.com" +} + +variable "address_space" { + description = "The address space that is used by the virtual network." + type = string + default = "10.10.0.0/16" +} + +variable "subnet_fe_prefix" { + description = "The address prefix to use for the frontend subnet." + type = string + default = "10.10.1.0/26" +} + +variable "subnet_aks_prefix" { + description = "The address prefix to use for the aks subnet." + type = string + default = "10.10.2.0/24" +} + +variable "subnet_be_prefix" { + description = "The address prefix to use for the backend subnet." + type = string + default = "10.10.3.0/28" +} + +variable "ssl_certificate_file" { + type = string + description = "(Required) The x509-based SSL certificate used to setup ssl termination on the app gateway." + default = "" +} + +variable "aks_agent_vm_count" { + description = "The initial number of agent pools / nodes allocated to the AKS cluster" + type = string + default = "3" +} + +variable "aks_agent_vm_size" { + type = string + description = "The size of each VM in the Agent Pool (e.g. Standard_F1). Changing this forces a new resource to be created." + default = "Standard_D2s_v3" +} + +variable "kubernetes_version" { + type = string + default = "1.17.11" +} + +variable "ssh_public_key_file" { + type = string + description = "(Required) The SSH public key used to setup log-in credentials on the nodes in the AKS cluster." +} + +variable "flux_recreate" { + description = "Make any change to this value to trigger the recreation of the flux execution script." + type = string + default = "false" +} + +variable "gitops_ssh_url" { + type = string + description = "(Required) ssh git clone repository URL with Kubernetes manifests including services which runs in the cluster. Flux monitors this repo for Kubernetes manifest additions/changes periodically and apply them in the cluster." +} + +variable "gitops_ssh_key_file" { + type = string + description = "(Required) SSH key used to establish a connection to a private git repo containing the HLD manifest." +} + +variable "gitops_branch" { + type = string + description = "(Optional) The branch for flux to watch" + default = "master" +} + +variable "gitops_path" { + type = string + description = "(Optional) The path for flux to watch" + default = "providers/azure/hld-registry" +} \ No newline at end of file diff --git a/test-harness/Dockerfile b/test-harness/Dockerfile new file mode 100755 index 0000000000000000000000000000000000000000..51fc4870190dcf32476aabdd13dad6f0281cdca5 --- /dev/null +++ b/test-harness/Dockerfile @@ -0,0 +1,29 @@ +# Copyright © Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +ARG base_image + +FROM $base_image + +ARG build_directory +RUN echo "INFO: copying $build_directory" +# Copy the recently modified terraform templates +ADD $build_directory *.go ./ +RUN find ./ -type f -iname "*.sh" -exec chmod +x {} \; + +RUN echo "INFO: copying test-harness" +ADD test-harness/ ./test-harness + +# Run a fresh clean/format/test run +CMD ["go", "run", "magefile.go"] \ No newline at end of file diff --git a/test-harness/README.md b/test-harness/README.md new file mode 100644 index 0000000000000000000000000000000000000000..bac19e461afa165c369ee4d3b4e0409568f10bd2 --- /dev/null +++ b/test-harness/README.md @@ -0,0 +1,295 @@ +# Resource Deployment Testing in Cobalt + +## Summary to build integration and validation tests for your cobalt deployment environments using docker and the terratest modules. + +Terratest is a Go library that makes it easier to write automated tests for your infrastructure code. It provides a variety of helper functions and patterns for common infrastructure testing tasks. + +In addition, the cobalt test suite allows for better collaboration with embedding into CI/CD tools such as Travis or Azure DevOps Pipelines. + +This test harness runs automated tests for only the deployment templates that have changed by comparing the changes in your git log versus upstream master. + +## Writing tests against Terraform + +This module includes a library that simplifies writing unit and integration [Note: integration test support is *pending*] tests against templates. It aims to extract out the most painful pieces of this process and provide common-sense implementations that can be shared across any template. Care is taken to provide hooks for more in-depth testing if it is needed by the template maintainer. + +### Sample Unit Test Usage + +The below example shows how easy it is to write a unit test that automatically coordinates the following: + +- Run `terraform init`, `terraform workspace select`, `terraform plan` and parse the plan output into a [Terraform Plan](https://github.com/hashicorp/terraform/blob/master/terraform/plan.go) +- Validate that running the test would only create and not update/delete resources. (Note: This should always be true, otherwise the test is not running in isolation. Not running the test in isolation can be very dangerous and may cause resources to be deleted) +- Validate that the resource <--> attribute <--> attribute value mappings match those supplied via the `ExpectedResourceAttributeValues` parameter. This only asserts that the supplied mappings exist and match the terraform plan. If there are more resources or attributes, the test will not fail. +- Validate that the correct number of resources are created + +Also note that the harness provides a hook that allows a list of user-defined functions that accept a handle to the GoTest and Terraform Plan objects. Users can supply custom test logic via this hook by supplying a non-nil `PlanAssertions` argument to `infratests.UnitTestFixture`. This feature is not used in the example below. + +```go +package test + +import ( + "fmt" + "os" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var prefix = fmt.Sprintf("cobalt-%s", random.UniqueId()) +var datacenter = os.Getenv("DATACENTER_LOCATION") + +var tf_options = &terraform.Options{ + TerraformDir: "../../", + Upgrade: true, + Vars: map[string]interface{}{ + "prefix": prefix, + "location": datacenter, + }, +} + +func TestAzureSimple(t *testing.T) { + test_fixture := infratests.UnitTestFixture{ + GoTest: t, + TfOptions: tf_options, + ExpectedResourceCount: 3, + PlanAssertions: nil, + ExpectedResourceAttributeValues: infratests.ResourceAttributeValueMapping{ + "azurerm_app_service.main": map[string]string{ + "resource_group_name": prefix, + "location": datacenter, + "site_config.0.linux_fx_version": "DOCKER|appsvcsample/static-site:latest", + }, + "azurerm_app_service_plan.main": map[string]string{ + "kind": "Linux", + "location": datacenter, + "reserved": true, + "sku.0.size": "S1", + "sku.0.tier": "Standard", + }, + "azurerm_resource_group.main": map[string]string{ + "location": datacenter, + "name": prefix, + }, + }, + } + + infratests.RunUnitTests(&test_fixture) +} +``` + +### Sample Integration Testing Usage + +The below example shows how easy it is to write an integration test that automatically coordinates the following: + +- Run `terraform init`, `terraform workspace select`, `terraform apply` and parse the template outputs into a Go struct +- Validate that the terraform outputs are correct by asserting that the correct number exist and that any user-supplied key-value mappings are reflected in that output. +- Pass terraform output to user-defined test functions for use-case specific tests. In this case, we simply validate that the application endpoint responds as expected + +```go +package test + +import ( + "fmt" + "os" + "strings" + "testing" + "time" + + httpClient "github.com/gruntwork-io/terratest/modules/http-helper" + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/microsoft/cobalt/test-harness/infratests" +) + +var prefix = fmt.Sprintf("cobalt-%s", random.UniqueId()) +var datacenter = os.Getenv("DATACENTER_LOCATION") + +var tfOptions = &terraform.Options{ + TerraformDir: "../../", + Upgrade: true, + Vars: map[string]interface{}{ + "prefix": prefix, + "location": datacenter, + }, + BackendConfig: map[string]interface{}{ + "storage_account_name": os.Getenv("TF_VAR_remote_state_account"), + "container_name": os.Getenv("TF_VAR_remote_state_container"), + }, +} + +// Validates that the service responds with HTTP 200 status code. A retry strategy +// is used because it may take some time for the application to finish standing up. +func httpGetRespondsWith200(goTest *testing.T, output infratests.TerraformOutput) { + hostname := output["app_service_default_hostname"].(string) + maxRetries := 20 + timeBetweenRetries := 2 * time.Second + + httpClient.HttpGetWithRetryWithCustomValidationE( + goTest, + hostname, + maxRetries, + timeBetweenRetries, + func(status int, content string) bool { + return status == 200 && strings.Contains(content, "Hello App Service!") + }, + ) +} + +func TestAzureSimple(t *testing.T) { + testFixture := infratests.IntegrationTestFixture{ + GoTest: t, + TfOptions: tfOptions, + ExpectedTfOutputCount: 2, + ExpectedTfOutput: infratests.TerraformOutput{ + "app_service_name": fmt.Sprintf("%s-appservice", prefix), + "app_service_default_hostname": strings.ToLower(fmt.Sprintf("https://%s-appservice.azurewebsites.net", prefix)), + }, + TfOutputAssertions: []infratests.TerraformOutputValidation{ + httpGetRespondsWith200, + }, + } + infratests.RunIntegrationTests(&testFixture) +} +``` + +## Test Setup Locally + +### Local Environment Setup + +- You'll need to define a `.env` file in the root of the project. You can use our environment template file to start. `cp .env.template .env` +- Provide values for the environment values in `.env` which are required to authenticate Terraform to provision resources within your subscription. + +```bash +ARM_SUBSCRIPTION_ID="" +ARM_CLIENT_ID="" +ARM_CLIENT_SECRET="" +ARM_TENANT_ID="" +ARM_ACCESS_KEY="" +TF_VAR_remote_state_account="" +TF_VAR_remote_state_container="" +``` + +## Local Test Runner Options + +### Option 1: Docker + +The benefit with running the test harness through docker is that developers don't need to worry about setting up their local environment. We strongly recommend running `local-run.sh` before submitting a PR as our devops pipeline runs the dockerized version of the test harness. + +#### Prerequisites + +- [Docker](https://docs.docker.com/install/) 18.09 or later +- An Azure subscription +- A [service principal](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal) +- An azure storage account for tracking terraform remote backend state. You can use our backend state setup [template](../infra/templates/backend-state-setup/README.md) to provision the storage resources. +- [git](https://www.atlassian.com/git/tutorials/install-git) + +#### Base Image Setup + +Our test harness uses a base docker image to pre-package dependencies like Terraform, Go, Azure CLI, Terratest vendor packages, etc. + +- **Optional Step** - Cobalt uses the public [msftcse/cobalt-test-base](https://hub.docker.com/r/msftcse/cobalt-test-base) base image by default. We also provide a utility script to generate a new base image. +- Rebuilding a new base image is as simple as running + +```script +./test-harness/build-base-image.sh -g "" -t "" +``` + +##### Script Arguments + +- `-g` | `--go_version`: Golang version specification. This argument drives the version of the `golang` stretch base image. **Defaults** to `1.12.5`. +- `-t` | `--tf_version`: Terraform version specification. This argument drives which terraform version release this image will use.. **Defaults** to `0.12.2` + +Keep in mind that the terraform version should align with the version from the provider [module](/infra/modules/providers/azure/provider/main.tf#L6) + +- The base image will be tagged as: + +```script +msftcse/cobalt-test-base:g${GO_VERSION}t${TERRAFORM_VERSION} +``` + +#### Local Run Script + +Run the test runner by calling the below script from the project's root directory. This is one of two options. + +```script +./test-harness/local-run.sh +``` + +##### Script Arguments + +- `-t` | `--template_name_override`: The template folder to include for the test harness run(i.e. -t "azure-hello-world"). When set, the git log will be ignored. **Defaults** to the git log. +- `-b` | `--docker_base_image_name`: The base image to use for the test harness continer. **Defaults** to `msftcse/cobalt-test-base:g${GO_VERSION}t${TF_VERSION}`. + +### Option 2: Manual Setup + +The benefit with setting up the test harness manually is that runtimes are quicker as we're not rebuilding the test harness image on each run. + +The clear downside here is that you'll need to set up all cobalt base software packages and responsible for managing version dependency upgrades over time. Our central base image in docker hub is supported by CSE as well as version dependency upgrades. + +The other downside is that you'll need to install this project within your `GOPATH` and pull down all `dep` vendor dependency packages. + +#### Prerequisites + +- An Azure subscription +- A [service principal](https://docs.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal) +- An azure storage account for tracking terraform remote backend state. You can use our backend state setup [template](../infra/templates/backend-state-setup/README.md) to provision the storage resources. +- [git](https://www.atlassian.com/git/tutorials/install-git) +- Follow [these instructions](https://golang.org/doc/install#download) to download the Go Distribution. +- Follow these [instructions](https://golang.org/doc/install#testing) to test your golang install. +- Ensure that your repository is checked out into the following directory that does not live inside `$GOPATH`. Example: + + ```script + $ echo $GOPATH + /home/workspace/go + $ pwd + /home/workspace/oss/cobalt + ``` + +- Install [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) +- Install golang's dep package manager via Git Bash. + + ```script + curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh + dep version + dep: + version : v0.5.0 + build date : 2018-07-26 + git hash : 224a564 + go version : go1.10.3 + go compiler : gc + platform : windows/amd64 + features : ImportDuringSolve=false + ``` + +- Install [Terraform](https://learn.hashicorp.com/terraform/getting-started/install.html) + +#### Local Run Script (No-Docker Version) + +Run the test runner by calling the below script from the project's root directory. + +```script +./test-harness/local-run-wo-docker.sh +``` + +##### Script Arguments (No-Docker Version) + +- `-t` | `--template_name_override`: The template folder to include for the test harness run(i.e. -t "azure-hello-world"). When set, the git log will be ignored. **Defaults** to the git log. +- `-c` | `--tf_state_container`: The storage container name responsible for tracking remote state for terraform deployments. **Defaults** to `cobaltfstate-remote-state-container` +- `-a` | `--tf_state_storage_acct`: The storage account name responsible for tracking remote state for terraform deployments. **Defaults** to `cobaltfstate`. + + +## License +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/test-harness/build-base-image.sh b/test-harness/build-base-image.sh new file mode 100755 index 0000000000000000000000000000000000000000..ce7437d73c90652315654626bb2f9737f187f8b7 --- /dev/null +++ b/test-harness/build-base-image.sh @@ -0,0 +1,134 @@ +# Copyright © Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#!/usr/bin/env bash +#---------- see https://github.com/joelong01/BashWizard ---------------- +# bashWizard version 1.0.0 +# this will make the error text stand out in red - if you are looking at these errors/warnings in the log file +# you can use cat to see the text in color. + +function echoError() { + RED=$(tput setaf 1) + NORMAL=$(tput sgr0) + echo "${RED}${1}${NORMAL}" +} +function echoWarning() { + YELLOW=$(tput setaf 3) + NORMAL=$(tput sgr0) + echo "${YELLOW}${1}${NORMAL}" +} +function echoInfo() { + GREEN=$(tput setaf 2) + NORMAL=$(tput sgr0) + echo "${GREEN}${1}${NORMAL}" +} +function echoIfVerbose() { + if [[ "$verbose" == true ]]; then + echo "${@}" + fi +} + +# make sure this version of *nix supports the right getopt +! getopt --test 2>/dev/null +if [[ ${PIPESTATUS[0]} -ne 4 ]]; then + echoError "'getopt --test' failed in this environment. please install getopt." + read -r -p "install getopt using brew? [y,n]" response + if [[ $response == 'y' ]] || [[ $response == 'Y' ]]; then + ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" < /dev/null 2> /dev/null + brew install gnu-getopt + #shellcheck disable=SC2016 + echo 'export PATH="/usr/local/opt/gnu-getopt/bin:$PATH"' >> ~/.bash_profile + exec bash -l -i -- $0 "${@}" + fi + echo "exiting..." + exit 1 +fi + +function usage() { + + echo "Builds the docker test harness base image. This image comes pre-installed with Azure CLI, GO, Dep, GCC, git, unzip, wget, terraform This base image also pre-installs the golang vendor packages." + echo "" + echo "Usage: $0 -g|--go_version -t|--tf_version " 1>&2 + echo "" + echo " -g | --go_version Optional GOLang version" + echo " -t | --tf_version Optional Terraform version" + echo "" + exit 1 +} +function echoInput() { + echo "build-base-image.sh:" + echo -n " go_version.... " + echoInfo "$go_version" + echo -n " tf_version.... " + echoInfo "$tf_version" +} + +function parseInput() { + local OPTIONS=g:t: + local LONGOPTS=go_version:,tf_version: + + # -use ! and PIPESTATUS to get exit code with errexit set + # -temporarily store output to be able to check for errors + # -activate quoting/enhanced mode (e.g. by writing out "--options") + # -pass arguments only via -- "$@" to separate them correctly + ! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@") + if [[ ${PIPESTATUS[0]} -ne 0 ]]; then + # e.g. return value is 1 + # then getopt has complained about wrong arguments to stdout + usage + exit 2 + fi + # read getopt's output this way to handle the quoting right: + eval set -- "$PARSED" + while true; do + case "$1" in + -g | --go_version) + go_version=$2 + shift 2 + ;; + -t | --tf_version) + tf_version=$2 + shift 2 + ;; + --) + shift + break + ;; + *) + echoError "Invalid option $1 $2" + exit 3 + ;; + esac + done +} + +function build_image(){ + echoInfo "INFO: Building base image" + echoInput + declare docker_tag="g${go_version}t${tf_version}" + echoInfo "$docker_img - $docker_file" + docker build -f $docker_file \ + -t $docker_img:$docker_tag . \ + --build-arg gover=$go_version \ + --build-arg tfver=$tf_version +} + +declare go_version="1.12.5" +declare tf_version="0.12.2" + +parseInput "$@" +declare docker_img="msftcse/cobalt-test-base" +declare docker_file="test-harness/docker/base-images/Dockerfile" + +build_image \ No newline at end of file diff --git a/test-harness/docker/base-images/Dockerfile b/test-harness/docker/base-images/Dockerfile new file mode 100755 index 0000000000000000000000000000000000000000..fc31a1c8b45ec063f68a64bb472bb09b7ba1fe9b --- /dev/null +++ b/test-harness/docker/base-images/Dockerfile @@ -0,0 +1,64 @@ +# Copyright © Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Description: Base image used for running the golang terratest pipeline. +# Usage: +# build- +# docker build --rm -f "test-harness/Dockerfile" \ +# -t msftcse/cobalt-test-base:1 . \ +# --build-arg build_directory="$BUILD_TEMPLATE_DIRS" \ +# --build-arg base_img_tag="$TEST_HARNESS_BASE_IMAGE_TAG" +# +# Base image- +# ARG base_img_tag +# FROM msftcse/cobalt-test-base:$base_img_tag + +ARG gover +FROM golang:${gover}-stretch as build +RUN apt-get update && \ + apt-get install -y git curl apt-transport-https lsb-release gpg jq + + + +# Setup Azure CLI +RUN curl -sL https://packages.microsoft.com/keys/microsoft.asc | \ + gpg --dearmor | \ + tee /etc/apt/trusted.gpg.d/microsoft.asc.gpg > /dev/null + +RUN echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ stretch main" | \ + tee /etc/apt/sources.list.d/azure-cli.list + +# Install Azure CLI + core compilers like gcc which are required for dep +RUN apt-get update && apt-get install -y build-essential wget unzip azure-cli +ENV GOLANG_VERSION=$gover + +ENV PATH /usr/local/go/bin:/usr/local/go:$PATH +ENV GOPATH $HOME/go +ENV GOBIN /usr/local/go + +# Install Terraform +ARG tfver=0.12.2 +ENV TF_VERSION=$tfver +RUN wget --quiet https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip \ + && unzip terraform_${TF_VERSION}_linux_amd64.zip \ + && mv terraform /usr/bin \ + && rm terraform_${TF_VERSION}_linux_amd64.zip + +# setup project workspace +WORKDIR $HOME/app/ + +ADD go.mod go.sum ./ +RUN ["go", "mod", "download"] + +CMD bash \ No newline at end of file diff --git a/test-harness/docker/base-images/README.md b/test-harness/docker/base-images/README.md new file mode 100755 index 0000000000000000000000000000000000000000..c4d93ca65051e0fe2362d90b5f9f79d7d631408b --- /dev/null +++ b/test-harness/docker/base-images/README.md @@ -0,0 +1,62 @@ +# Cobalt Test Base Image + +This is the base image used for running terratest based unit and integration GO tests. This image comes pre-packaged with the following dependencies: +* Go programming language: Terraform test cases are written in [Go](https://golang.org/dl/). +* dep: [dep](https://github.com/golang/dep#installation) is a dependency management tool for Go. +* Azure CLI: The [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest) is a command-line tool you can use to manage Azure resources. (Terraform supports authenticating to Azure through a service principal or via the Azure CLI.) +* mage: We use the mage go [module](https://github.com/magefile/mage#installation) to show you how to simplify running Terratest cases. + +## Getting Started + +You can build this image locally using a different golang version following the example below. + +### Prerequisities + +In order to run this container you'll need docker installed. + +* [Windows](https://docs.docker.com/windows/started) +* [OS X](https://docs.docker.com/mac/started/) +* [Linux](https://docs.docker.com/linux/started/) + +### Usage + +#### Image Build Parameters + +**govver** + +Golang version specification. This argument drives the version of the `golang` stretch base image. + +**tfver** + +Terraform version specification. This argument drives which terraform version release this image will use. + +```shell +docker build -f "test-harness\docker\base-images\Dockerfile" -t msftcse/cobalt-test-base:1.12.5 . --build-arg gover=1.12.5 tfver=0.12.2 +``` +## Contributing + +Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. + +## Authors + +* [@erikschlegel](https://github.com/erikschlegel) + +## License + +This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. + + +## License +Copyright © Microsoft Corporation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/test-harness/infratests/integration.go b/test-harness/infratests/integration.go new file mode 100644 index 0000000000000000000000000000000000000000..90205731ac432cde3564f12253defda96d1946e5 --- /dev/null +++ b/test-harness/infratests/integration.go @@ -0,0 +1,119 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Package infratests This file provides abstractions that simplify the process of integration-testing terraform templates. The goal +is to minimize the boiler plate code required to effectively test terraform templates in order to reduce +the effort required to write robust template integration-tests. +*/ +package infratests + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/gruntwork-io/terratest/modules/terraform" +) + +// TerraformOutput Models terraform output key values +type TerraformOutput map[string]interface{} + +// TerraformOutputValidation A function that can validate terraform output +type TerraformOutputValidation func(goTest *testing.T, output TerraformOutput) + +// IntegrationTestFixture Holds metadata required to execute an integration test against a test against a terraform template +type IntegrationTestFixture struct { + GoTest *testing.T // Go test harness + TfOptions *terraform.Options // Terraform options + ExpectedTfOutputCount int // Expected # of resources that Terraform should create + ExpectedTfOutput TerraformOutput // Expected Terraform Output + TfOutputAssertions []TerraformOutputValidation // user-defined plan assertions +} + +// RunIntegrationTests Executes terraform lifecycle events and verifies the correctness of the resulting resources. +// The following actions are coordinated: +// - Run `terraform init` +// - Run `terraform output` +// - Validate outputs +// - Run user-supplied validation of outputs +func RunIntegrationTests(fixture *IntegrationTestFixture) { + terraform.Init(fixture.GoTest, fixture.TfOptions) + output := terraform.OutputAll(fixture.GoTest, fixture.TfOptions) + validateTerraformOutput(fixture, TerraformOutput(output)) +} + +// Coordinates the following validations of a terraform output: +// - The output contains the correct number of items +// - The output values match any user-supplied key-value mappings. This only validates +// that any user-supplied key-value mappings are correct, and will not fail if the +// output has more mappings +// - The output has the correct number of items +// - Run any user-supplied assertions over the output +func validateTerraformOutput(fixture *IntegrationTestFixture, output TerraformOutput) { + validateTerraformOutputCount(fixture, output) + validateTerraformOutputKeyValues(fixture, output) + + // run user-provided assertions over the TF output + for _, outputAssertion := range fixture.TfOutputAssertions { + outputAssertion(fixture.GoTest, output) + } +} + +// Validates that the terraform output contains the expected number of items +func validateTerraformOutputCount(fixture *IntegrationTestFixture, output TerraformOutput) { + if len(output) != fixture.ExpectedTfOutputCount { + fixture.GoTest.Fatal(fmt.Errorf( + "Output unexpectedly had %d entries instead of %d", + len(output), + fixture.ExpectedTfOutputCount, + )) + } +} + +// Validates that any outputs that the user supplies match the actual terraform outputs. +// Note: the comparison is done by converting the expected and actual values into JSON and +// doing a string comparison. This solves a number of complexities, such as: +// - Handles comparison of generic data types automatically +// - Handles differences in key ordering for maps +// - Handles all handling of generics, which is tricky in Go +func validateTerraformOutputKeyValues(fixture *IntegrationTestFixture, output TerraformOutput) { + for expectedKey, expectedValue := range fixture.ExpectedTfOutput { + actualValue, isFound := output[expectedKey] + if !isFound { + fixture.GoTest.Fatal(fmt.Errorf("Output unexpectedly did not contain key %s", expectedKey)) + } + + expectedAsJSON := jsonOrFail(fixture, expectedValue) + actualAsJSON := jsonOrFail(fixture, actualValue) + + if expectedAsJSON != actualAsJSON { + fixture.GoTest.Fatal(fmt.Errorf( + "Output value for '%s' was expected to be '%s' but was '%s'", + expectedKey, + expectedAsJSON, + actualAsJSON, + )) + } + } +} + +// parse data to JSON or fail if an error was encountered +func jsonOrFail(fixture *IntegrationTestFixture, value interface{}) string { + asJSON, err := json.Marshal(value) + if err != nil { + fixture.GoTest.Fatal(err) + } + return string(asJSON) +} diff --git a/test-harness/infratests/plan.go b/test-harness/infratests/plan.go new file mode 100755 index 0000000000000000000000000000000000000000..cb9023a7470fed9c4cf6952c4f56c3aef413b06a --- /dev/null +++ b/test-harness/infratests/plan.go @@ -0,0 +1,31 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Package infratests this file provides a model for the JSON representation of a terraform plan. It describes +a minimal set of metadata produced by the plan and can be expanded to support other attributes +if needed +*/ +package infratests + +// TerraformPlan a JSON schema for the output of `terraform plan ` +type TerraformPlan struct { + ResourceChanges []struct { + Address string `json:"address"` + Change struct { + Actions []string `json:"actions"` + After map[string]interface{} `json:"after"` + } `json:"change"` + } `json:"resource_changes"` +} diff --git a/test-harness/infratests/unit.go b/test-harness/infratests/unit.go new file mode 100755 index 0000000000000000000000000000000000000000..1c4127232f780779317527d5348d8f5ae13538d2 --- /dev/null +++ b/test-harness/infratests/unit.go @@ -0,0 +1,186 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Package infratests This file provides abstractions that simplify the process of unit-testing terraform templates. The goal +is to minimize the boiler plate code required to effectively test terraform templates in order to reduce +the effort required to write robust template unit-tests. +*/ +package infratests + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" +) + +// ResourceDescription Identifies mappings between resources and attributes +type ResourceDescription map[string]map[string]interface{} + +// TerraformPlanValidation A function that can run an assertion over a terraform plan +type TerraformPlanValidation func(goTest *testing.T, plan TerraformPlan) + +// UnitTestFixture Holds metadata required to execute a unit test against a test against a terraform template +type UnitTestFixture struct { + GoTest *testing.T // Go test harness + TfOptions *terraform.Options // Terraform options + Workspace string + ExpectedResourceCount int // Expected # of resources that Terraform should create + // map of maps specifying resource <--> attribute <--> attribute value mappings + ExpectedResourceAttributeValues ResourceDescription + PlanAssertions []TerraformPlanValidation // user-defined plan assertions +} + +// RunUnitTests Executes terraform lifecycle events and verifies the correctness of the resulting terraform. +// The following actions are coordinated: +// - Run `terraform init` +// - Create new terraform workspace. This helps prevent accidentally deleting resources +// - Run `terraform plan` +// - Validate terraform plan file. +func RunUnitTests(fixture *UnitTestFixture) { + terraform.Init(fixture.GoTest, fixture.TfOptions) + + workspaceName := fixture.Workspace + if workspaceName == "" { + workspaceName = "default-unit-testing" + } + + startingWorkspaceName := terraform.RunTerraformCommand( + fixture.GoTest, + fixture.TfOptions, + terraform.FormatArgs(fixture.TfOptions, "workspace", "show")...) + + terraform.WorkspaceSelectOrNew(fixture.GoTest, fixture.TfOptions, workspaceName) + defer terraform.RunTerraformCommand(fixture.GoTest, fixture.TfOptions, "workspace", "delete", workspaceName) + defer terraform.WorkspaceSelectOrNew(fixture.GoTest, fixture.TfOptions, startingWorkspaceName) + + tfPlanFilePath := filepath.FromSlash(fmt.Sprintf("%s/%s.plan", os.TempDir(), random.UniqueId())) + terraform.RunTerraformCommand( + fixture.GoTest, + fixture.TfOptions, + terraform.FormatArgs(fixture.TfOptions, "plan", "-input=false", "-out", tfPlanFilePath)...) + defer os.Remove(tfPlanFilePath) + + validateTerraformPlanFile(fixture, tfPlanFilePath) +} + +// Validates a terraform plan file based on the test fixture. The following validations are made: +// - The plan is only creating resources, and the number of resources created should match the +// parameters from the test fixture. The plan should only create resources because it should +// be brand new infrastructure on each PR cycle. +// - The resource <--> attribute <--> attribute value mappings match the parameters from the test fixture +// - The plan passes any user-defined assertions +func validateTerraformPlanFile(fixture *UnitTestFixture, tfPlanFilePath string) { + plan := parseTerraformPlan(fixture, tfPlanFilePath) + validatePlanCreateProperties(fixture, plan) + validatePlanResourceKeyValues(fixture, plan) + + // run user-provided assertions + if fixture.PlanAssertions != nil { + for _, planAssertion := range fixture.PlanAssertions { + planAssertion(fixture.GoTest, plan) + } + } +} + +func parseTerraformPlan(fixture *UnitTestFixture, filePath string) TerraformPlan { + // Note: when the PR linked below is merged and the new build is used by this codebase, + // we can leverage Terratest to run this for us. Currently with large plan outputs, + // a buffer overflow will happen in Terratest because the default max character limit + // may be exceeded for large plan files: + // + // - Issue: https://github.com/gruntwork-io/terratest/issues/203 + // - PR: https://github.com/gruntwork-io/terratest/pull/317 + // + // jsonBytes := []bytes(terraform.RunTerraformCommand( + // fixture.GoTest, + // fixture.TfOptions, + // terraform.FormatArgs(&terraform.Options{}, "show", "-json", filePath)...)) + cmd := exec.Command("terraform", "show", "-json", filePath) + cmd.Dir = fixture.TfOptions.TerraformDir + jsonBytes, cmdErr := cmd.Output() + if cmdErr != nil { + fixture.GoTest.Fatal(cmdErr) + } + + fmt.Println("Got terraform plan...", string(jsonBytes)) + var plan TerraformPlan + jsonErr := json.Unmarshal(jsonBytes, &plan) + if jsonErr != nil { + fixture.GoTest.Fatal(jsonErr) + } + return plan +} + +// Validates high level attributes of a terraform plan creat properties. This includes: +// - The plan is not empty +// - The plan contains the correct number of resources +// - The plan is not executing any destructive actions +func validatePlanCreateProperties(fixture *UnitTestFixture, plan TerraformPlan) { + if len(plan.ResourceChanges) == 0 { + fixture.GoTest.Fatal(errors.New("Plan diff was unexpectedly empty")) + } + + if len(plan.ResourceChanges) != fixture.ExpectedResourceCount { + fixture.GoTest.Fatal(fmt.Errorf( + "Plan unexpectedly had %d resources instead of %d", len(plan.ResourceChanges), fixture.ExpectedResourceCount)) + } + + // a unit test should never create a destructive action like deleting a resource + allowedActions := map[string]bool{"create": true, "read": true} + for _, resource := range plan.ResourceChanges { + for _, action := range resource.Change.Actions { + if !allowedActions[action] { + fixture.GoTest.Fatal( + fmt.Errorf("Plan unexpectedly actions other than `create`: %s", resource.Change.Actions)) + } + } + } +} + +// verifies that the attribute value mappings for each resource specified by the client exist +// as a subset of the actual values defined in the terraform plan. +func validatePlanResourceKeyValues(fixture *UnitTestFixture, plan TerraformPlan) { + theRealPlanAsMap := planToMap(plan) + theExpectedPlanAsMap := resourceDescriptionToMap(fixture.ExpectedResourceAttributeValues) + + if err := verifyTargetsExistInMap(theRealPlanAsMap, theExpectedPlanAsMap, ""); err != nil { + fixture.GoTest.Fatal(err) + } +} + +// transforms the output of `terraform show -json ` into a generic map +func planToMap(plan TerraformPlan) map[string]interface{} { + mp := make(map[string]interface{}) + for _, resource := range plan.ResourceChanges { + mp[resource.Address] = resource.Change.After + } + return mp +} + +// transforms a resource description into a generic map +func resourceDescriptionToMap(resources ResourceDescription) map[string]interface{} { + mp := make(map[string]interface{}) + for key, value := range resources { + mp[key] = value + } + return mp +} diff --git a/test-harness/infratests/validate.go b/test-harness/infratests/validate.go new file mode 100755 index 0000000000000000000000000000000000000000..a7ab481e7eca92f7da0422abdd323cedb0ff6de6 --- /dev/null +++ b/test-harness/infratests/validate.go @@ -0,0 +1,150 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Package infratests This file provides validation utilities that can be used by the core testing constructs +*/ +package infratests + +import ( + "fmt" + "reflect" +) + +// This function validates that a set of search targets exist in a map. The intended use case is to allow +// a user of this library to provide a set of *expected values* that should exist within a larger set of +// *actual values.* In other words, this is a map equality check that does not care about ordering in keys/lists +// and will not fail if the *expected values* are a subset of the *actual values.* +// +// In the case that the *expected values* are found in the *actual values,* nil will be returned. Otherwise an +// error describing the mismatch will be returned. +// +// Some examples (in pseudo-code): +// - Exact Match: +// a := {"key1":[{"key2" : "foo"}, {"key2" : "bar"}]} +// verifyTargetsExistInMap(a, a) --> nil +// +// - Subset Match #1: +// a := {"key1":[{"key2" : "foo"}, {"key2" : "bar"}]} +// b := {"key1":[{"key2" : "foo"}]} +// verifyTargetsExistInMap(a, b) --> nil +// +// - Subset Match #2: +// a := {"key1":[{"key2" : "foo"}, {"key2" : "bar"}]} +// b := {"key1":[]} +// verifyTargetsExistInMap(a, b) --> nil +// +// - Subset Match #3: +// a := {"key1":{"key2": "foo", "key3", "bar"}} +// b := {"key1":{"key3", "bar"}} +// verifyTargetsExistInMap(a, b) --> nil +// +// - Mismatch #1: +// a := {"key1":{"key2": "foo", "key3", "bar"}} +// b := {"key1":[]} +// verifyTargetsExistInMap(a, b) --> ERROR: wrong type for key `key1` +// +// - Mismatch #2: +// a := {"key1":{"key2": "foo", "key3", "bar"}} +// b := {"key1":{"key4": "foo"}} +// verifyTargetsExistInMap(a, b) --> ERROR: `key4` not found +// +// - Mismatch #3: +// a := {"key1":{"key2": "foo", "key3", "bar"}} +// b := {"key1":{"key2": "bar"}} +// verifyTargetsExistInMap(a, b) --> ERROR: wrong value for `key2` +// +// The algorithm used to do the equality check is to execute a DFS search in parallel for both maps. In the case +// that a mismatch (the values do not match, or the shape of the maps is different in a way that indicates a mismatch) +// is identified, the search will be terminated. If no mismatches are found then `nil` is returned +func verifyTargetsExistInMap(dataSource map[string]interface{}, searchTargets map[string]interface{}, traversalPath string) error { + for targetKey := range searchTargets { + // assemble current traversal path + currentTraversalPath := traversalPath + if currentTraversalPath != "" { + currentTraversalPath = currentTraversalPath + "." + } + currentTraversalPath = currentTraversalPath + targetKey + + candidateMatch, candidateExists := dataSource[targetKey] + target, targetExists := searchTargets[targetKey] + + // both maps should contain the target search key + if !candidateExists || !targetExists { + return fmt.Errorf("Unexpectedly could not find key '%s' at node '%s'", targetKey, currentTraversalPath) + } + + // the values for the key should be the same type + if !isSameType(candidateMatch, target) { + return fmt.Errorf("Unexpectedly found type '%T' instead of '%T' at node %s. Data source reference was %v", candidateMatch, target, currentTraversalPath, dataSource) + } + + // the key is found and both values are of the same type. time to look for a subset match + switch typedTarget := target.(type) { + case bool, float32, float64, int, string: + if typedTarget != candidateMatch { + return fmt.Errorf("Expected %s but got %s at node %s", typedTarget, candidateMatch, currentTraversalPath) + } + case []interface{}: + if err := verifyTargetsExistInList(candidateMatch.([]interface{}), typedTarget, currentTraversalPath); err != nil { + return err + } + case map[string]interface{}: + if err := verifyTargetsExistInMap(candidateMatch.(map[string]interface{}), typedTarget, currentTraversalPath); err != nil { + return err + } + default: + return fmt.Errorf("Comparison for type '%T' in a map not implemented", typedTarget) + } + + } + + return nil +} + +// This function has the same semantics as `verifyTargetsExistInMap` (so the documentation will not be repeated) +// except that it works for lists. +func verifyTargetsExistInList(dataSource []interface{}, searchTargets []interface{}, traversalPath string) error { + for i, target := range searchTargets { + currentTraversalPath := fmt.Sprintf("%s[%d]", traversalPath, i) + matchFound := false + + switch typedTarget := target.(type) { + case bool, float32, float64, int, string: + for _, candidateMatch := range dataSource { + matchFound = matchFound || typedTarget == candidateMatch + } + case map[string]interface{}: + for _, candidateMatch := range dataSource { + if isSameType(candidateMatch, typedTarget) { + err := verifyTargetsExistInMap(candidateMatch.(map[string]interface{}), typedTarget, currentTraversalPath) + matchFound = matchFound || err == nil + } + } + default: + return fmt.Errorf("Comparison for type '%T' in a list not yet implemented (at node %s)", typedTarget, currentTraversalPath) + } + + if !matchFound { + return fmt.Errorf("Unexpectedly did not find '%s' in '%s' at node %s", target, dataSource, currentTraversalPath) + } + } + + return nil +} + +// return true if the values have the same type, false otherwise +func isSameType(a interface{}, b interface{}) bool { + return reflect.TypeOf(a) == reflect.TypeOf(b) +} diff --git a/test-harness/infratests/validate_test.go b/test-harness/infratests/validate_test.go new file mode 100755 index 0000000000000000000000000000000000000000..60c6a1c4f89b804cb6c8e37bd0ad989f15a09f6f --- /dev/null +++ b/test-harness/infratests/validate_test.go @@ -0,0 +1,103 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package infratests + +import ( + "encoding/json" + "testing" +) + +var tests = []struct { + dataSourceJSON string + searchTargetsJSON string + shouldPass bool +}{ + { + `{"key1":[{"key2" : "foo"}, {"key2" : "bar"}]}`, + `{"key1":[{"key2" : "foo"}, {"key2" : "bar"}]}`, + true, + }, { + `{"key1":[{"key2" : "foo"}, {"key2" : "bar"}]}`, + `{"key1":[{"key2" : "foo"}]}`, + true, + }, { + `{"key1":[{"key2" : "foo"}, {"key2" : "bar"}]}`, + `{"key1":[]}`, + true, + }, { + `{"key1":{"key2": "foo", "key3": "bar"}}`, + `{"key1":{"key3": "bar"}}`, + true, + }, { + `{"key1":[{"key2" : "foo"}, {"key2" : "bar"}]}`, + `{}`, + true, + }, { + `{"key1":{"key2": "foo", "key3": "bar"}}`, + `{"key1":[]}`, + false, // key has wrong type + }, { + `{"key1":{"key2": "foo", "key3": "bar"}}`, + `{"key1":{"key4": "foo"}}`, + false, // key does not exist in data source + }, { + `{"key1":{"key2": "foo", "key3": "bar"}}`, + `{"key1":{"key2": "bar"}}`, + false, // key has wrong value + }, { + `{"key1":[1, 2, 3]}`, + `{"key1":[4]}`, + false, // list value does not exist in parent + }, { + `{"key1":[{"key2": "foo"}]}`, + `{"key1":[{"key3": "foo"}]}`, + false, // list value does not exist in parent + }, { + `{"key1":[{"key2": "foo"}]}`, + `{"key1":[{"key2": "bar"}]}`, + false, // list value does not exist in parent + }, +} + +func TestVerifyTargets(t *testing.T) { + for _, test := range tests { + err := verifyTargetsExistInMap( + jsonToMap(t, test.dataSourceJSON), + jsonToMap(t, test.searchTargetsJSON), "") + + // the test should pass but there was an error + if test.shouldPass && err != nil { + t.Errorf("Search Targets `%s` were unexpectedly not found in Data Source `%s`. %s", + test.searchTargetsJSON, + test.dataSourceJSON, + err) + } + + // the test should not pass but there was no error + if !test.shouldPass && err == nil { + t.Errorf("Search Targets `%s` were unexpectedly found in Data Source `%s`", + test.searchTargetsJSON, + test.dataSourceJSON) + } + } +} + +func jsonToMap(t *testing.T, jsonStr string) map[string]interface{} { + var theMap map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &theMap); err != nil { + t.Errorf("Unable to parse JSON `%s`. Error = `%s`", jsonStr, err) + } + return theMap +} diff --git a/test-harness/init.sh b/test-harness/init.sh new file mode 100755 index 0000000000000000000000000000000000000000..fb26f94e78994dc9df3b7b608427feadd81e85fe --- /dev/null +++ b/test-harness/init.sh @@ -0,0 +1,189 @@ +# Copyright © Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# NAME: init.sh +# Notable exported functions: +# 1. template_build_targets: compares the source and upstream branch to determines which terraform template directories were modified. +# 2. check_required_env_variables: verifies that required environment variables are defined. +# USAGE: template_build_targets $BUILD_UPSTREAMBRANCH $BUILD_SOURCEBRANCHNAME +# check_required_env_variables + +#!/usr/bin/env bash +set -euo pipefail + +declare -A TEST_RUN_MAP=() +declare BUILD_TEMPLATE_DIRS="build" +declare BUILD_TEST_RUN_IMAGE="infra-test-harness" +declare readonly TEMPLATE_DIR="infra/templates" +declare readonly GIT_DIFF_EXTENSION_WHITE_LIST="*.go|*.tf|*.sh|Dockerfile*|*.tfvars" + +function echoError() { + RED=$(tput setaf 1) + NORMAL=$(tput sgr0) + echo "${RED}${1}${NORMAL}" +} +function echoWarning() { + YELLOW=$(tput setaf 3) + NORMAL=$(tput sgr0) + echo "${YELLOW}${1}${NORMAL}" +} +function echoInfo() { + GREEN=$(tput setaf 2) + NORMAL=$(tput sgr0) + echo "${GREEN}${1}${NORMAL}" +} +function echoIfVerbose() { + if [[ "$verbose" == true ]]; then + echo "${@}" + fi +} +function dotenv() { + set -a + [ -f .env ] && . .env + set +a +} + +function rebuild_test_image() { + declare base_image=$1 + echoInfo "INFO: Using base image tag $base_image" + docker build --rm -f "test-harness/Dockerfile" \ + --build-arg build_directory="$BUILD_TEMPLATE_DIRS" \ + -t ${BUILD_TEST_RUN_IMAGE}:${BUILD_BUILDID} \ + --build-arg base_image=$base_image . +} + +function remove_build_directory() { + if [ -d "$BUILD_TEMPLATE_DIRS" ]; then rm -Rf $BUILD_TEMPLATE_DIRS; fi +} + +function check_required_env_variables() { + echoInfo "INFO: Checking required environment variables" + for var in BUILD_BUILDID ARM_SUBSCRIPTION_ID ARM_CLIENT_ID ARM_CLIENT_SECRET ARM_TENANT_ID ARM_ACCESS_KEY TF_VAR_remote_state_container TF_VAR_remote_state_account ; do + if [[ ! -v ${var} ]] ; then + echoError "ERROR: $var is not set in the environment" + return 0 + fi + done + echoInfo "INFO: passed environment variable check" +} + +function default_to_all_template_paths() { + echoInfo "INFO: Terraform module file(s) changed. Running all tests" + declare -a ALL_TEMPLATE_DIRS=(`find $TEMPLATE_DIR/* -maxdepth 0 -type d`) + shopt -s nullglob + for folder in "${ALL_TEMPLATE_DIRS[@]}" + do + IFS='/' read -a folder_array <<< "${folder}" + TEST_RUN_MAP[${folder_array[2]}]=$folder + done +} + +function add_template_if_not_exists() { + declare readonly template_name=$1 + declare readonly template_directory="$TEMPLATE_DIR/$template_name" + if [[ -z ${TEST_RUN_MAP[$template_name]+unset} && -d "$template_directory" ]]; then + TEST_RUN_MAP[$template_name]="$template_directory" + fi; +} + +function load_build_directory() { + template_dirs=$( IFS=$' '; echo "${TEST_RUN_MAP[*]}" ) + echoInfo "INFO: Running local build for templates: $template_dirs" + mkdir $BUILD_TEMPLATE_DIRS + mkdir $BUILD_TEMPLATE_DIRS/infra + mkdir $BUILD_TEMPLATE_DIRS/modules + cp -r $template_dirs $BUILD_TEMPLATE_DIRS/infra + cp -r infra/modules $BUILD_TEMPLATE_DIRS/ + cp test-harness/*.go $BUILD_TEMPLATE_DIRS +} + +# Builds the test harness off the template changes from the git log +function build_test_harness() { + GIT_DIFF_UPSTREAMBRANCH=$1 + GIT_DIFF_SOURCEBRANCH=$2 + BASE_IMAGE=$3 + echoInfo "INFO: verified that environment is fully defined" + template_build_targets $GIT_DIFF_UPSTREAMBRANCH $GIT_DIFF_SOURCEBRANCH + echoInfo "INFO: Building test harness image" + rebuild_test_image $BASE_IMAGE + if [ -d "$BUILD_TEMPLATE_DIRS" ]; then rm -Rf $BUILD_TEMPLATE_DIRS; fi +} + +# Builds the test harness based on the template name provided from the user +function build_test_harness_from_template() { + BASE_IMAGE=$1 + TEMPLATE_NAME=$2 + add_template_if_not_exists $TEMPLATE_NAME + echoInfo "INFO: moving terraform files for template $TEMPLATE_NAME to ${BUILD_TEMPLATE_DIRS}/" + load_build_directory + echoInfo "INFO: Building test harness image" + rebuild_test_image $BASE_IMAGE + if [ -d "$BUILD_TEMPLATE_DIRS" ]; then rm -Rf $BUILD_TEMPLATE_DIRS; fi +} + +function template_build_targets() { + GIT_DIFF_UPSTREAMBRANCH=$1 + GIT_DIFF_SOURCEBRANCH=$2 + [[ -z $GIT_DIFF_UPSTREAMBRANCH ]] && echoError "ERROR: GIT_DIFF_UPSTREAMBRANCH wasn't provided" && return 1 + + [[ -z $GIT_DIFF_SOURCEBRANCH ]] && echoError "ERROR: GIT_DIFF_SOURCEBRANCH wasn't provided" && return 1 + + declare -a files=() + echoInfo "INFO: Running git diff from branch ${GIT_DIFF_SOURCEBRANCH}" + files=(`git diff ${GIT_DIFF_UPSTREAMBRANCH} ${GIT_DIFF_SOURCEBRANCH} --name-only|grep -E ${GIT_DIFF_EXTENSION_WHITE_LIST}||true`) + + # covers the case where no files were determined to have changed. Without this + # the build will break for things like docs-only changes. + # + # note: the check ${files[@]:-} is safe for unset variables + if [ -z ${files[@]:-} ]; then + echoWarning "INFO: No templates to process. Exiting build steppp" + exit 0 + fi + + for file in "${files[@]}" + do + IFS='/' read -a folder_array <<< "${file}" + + if [ ${#folder_array[@]} -lt 1 ]; then + continue + fi + + if [ ${folder_array[0]}=='test-harness' ]; then + default_to_all_template_paths + break + fi + + if [ ${#folder_array[@]} -lt 3 ]; then + continue + fi + + case ${folder_array[1]} in + 'modules') + default_to_all_template_paths + break + ;; + 'templates') declare readonly template_name=${folder_array[2]} + add_template_if_not_exists $template_name + ;; + esac + done + + if [ ${#TEST_RUN_MAP[@]} -eq 0 ]; then + echoWarning "INFO: No templates to process. Exiting build step" + exit 0 + fi + + load_build_directory +} \ No newline at end of file diff --git a/test-harness/local-run-wo-docker.sh b/test-harness/local-run-wo-docker.sh new file mode 100755 index 0000000000000000000000000000000000000000..3d4b8baf6eb3e3c4a425df4681e73ccb2f370937 --- /dev/null +++ b/test-harness/local-run-wo-docker.sh @@ -0,0 +1,140 @@ +# Copyright © Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#!/usr/bin/env bash +#---------- see https://github.com/joelong01/BashWizard ---------------- +# bashWizard version 1.0.0 +# this will make the error text stand out in red - if you are looking at these errors/warnings in the log file +# you can use cat to see the text in color. + +. ./test-harness/init.sh --source-only + +# make sure this version of *nix supports the right getopt +! getopt --test 2>/dev/null +if [[ ${PIPESTATUS[0]} -ne 4 ]]; then + echoError "'getopt --test' failed in this environment. please install getopt." + read -r -p "install getopt using brew? [y,n]" response + if [[ $response == 'y' ]] || [[ $response == 'Y' ]]; then + ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" < /dev/null 2> /dev/null + brew install gnu-getopt + #shellcheck disable=SC2016 + echo 'export PATH="/usr/local/opt/gnu-getopt/bin:$PATH"' >> ~/.bash_profile + exec bash -l -i -- $0 "${@}" + fi + echo "exiting..." + exit 1 +fi + +function usage() { + + echo "Builds and runs the test harness container. This container runs all build target tasks on the host machine. These targets include mage clean, format, unit and integration tests. This base image also pre-installs the golang vendor. " + echo "" + echo "Usage: $0 -t|--template_name_override " 1>&2 + echo "" + echo " -t | --template_name_override Optional " + echo "" + exit 1 +} +function echoInput() { + echo "local-run-wo-docker.sh:" + echo -n " template_name_override.... " + echoInfo "$template_name_override" + +} + +function parseInput() { + + local OPTIONS=t: + local LONGOPTS=template_name_override: + + # -use ! and PIPESTATUS to get exit code with errexit set + # -temporarily store output to be able to check for errors + # -activate quoting/enhanced mode (e.g. by writing out "--options") + # -pass arguments only via -- "$@" to separate them correctly + ! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@") + if [[ ${PIPESTATUS[0]} -ne 0 ]]; then + # e.g. return value is 1 + # then getopt has complained about wrong arguments to stdout + usage + exit 2 + fi + # read getopt's output this way to handle the quoting right: + eval set -- "$PARSED" + while true; do + case "$1" in + -t | --template_name_override) + template_name_override=$2 + shift 2 + ;; + --) + shift + break + ;; + *) + echoError "Invalid option $1 $2" + exit 3 + ;; + esac + done +} + +# Bind environment from .env +dotenv + +# input variables +declare template_name_override="" + +# Parse user input arguments +parseInput "$@" + +readonly BUILD_SOURCEBRANCHNAME=`git branch | sed -n '/\* /s///p'` +readonly BUILD_UPSTREAMBRANCH="master" +readonly GO_MOD_FILE="go.mod" + +function move_target_template_to_build_dir() { + add_template_if_not_exists $template_name_override + echoInfo "INFO: moving terraform files for template $template_name_override to ${BUILD_TEMPLATE_DIRS}/" + load_build_directory +} + +function setup_manifest_dependencies_if_not_exists() { + if [ ! -f $GO_MOD_FILE ]; then + echoInfo "INFO: Setting up go module" + go mod init github.com/microsoft/cobalt + fi +} + +function run_test_harness() { + remove_build_directory + echoInfo "INFO: loading environment" + check_required_env_variables + echoInput + echoInfo "INFO: verified that environment is fully defined" + setup_manifest_dependencies_if_not_exists + echoInfo "INFO: Identifying and installing dependencies" + # This command will look for local packages and will inflate go.mod and + # go.sum along the way. It will also pull down any missing dependencies. + go list ./... + + case "$template_name_override" in + "") template_build_targets $BUILD_UPSTREAMBRANCH $BUILD_SOURCEBRANCHNAME ;; + *) move_target_template_to_build_dir ;; + esac + echoInfo "INFO: Running automated test harness" + cd $BUILD_TEMPLATE_DIRS && go run magefile.go && cd - + remove_build_directory +} + + +run_test_harness \ No newline at end of file diff --git a/test-harness/local-run.sh b/test-harness/local-run.sh new file mode 100755 index 0000000000000000000000000000000000000000..b5c4a5155f3aa50e6be3fde24928c24316ecc2c3 --- /dev/null +++ b/test-harness/local-run.sh @@ -0,0 +1,136 @@ +# Copyright © Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#!/usr/bin/env bash +#---------- see https://github.com/joelong01/BashWizard ---------------- +# bashWizard version 1.0.0 +# this will make the error text stand out in red - if you are looking at these errors/warnings in the log file +# you can use cat to see the text in color. + +. ./test-harness/init.sh --source-only + +# make sure this version of *nix supports the right getopt +! getopt --test 2>/dev/null +if [[ ${PIPESTATUS[0]} -ne 4 ]]; then + echoError "'getopt --test' failed in this environment. please install getopt." + read -r -p "install getopt using brew? [y,n]" response + if [[ $response == 'y' ]] || [[ $response == 'Y' ]]; then + ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" < /dev/null 2> /dev/null + brew install gnu-getopt + #shellcheck disable=SC2016 + echo 'export PATH="/usr/local/opt/gnu-getopt/bin:$PATH"' >> ~/.bash_profile + exec bash -l -i -- $0 "${@}" + fi + echo "exiting..." + exit 1 +fi + +function usage() { + + echo "Builds and runs the test harness container. This container runs all build target tasks on the host machine. These targets include mage clean, format, unit and integration tests. This base image also pre-installs the golang vendor. " + echo "" + echo "Usage: $0 -b|--docker_base_image_name -a|--template_name_override " 1>&2 + echo "" + echo " -b | --docker_base_image_name Optional " + echo " -t | --template_name_override Optional " + echo "" + exit 1 +} +function echoInput() { + echo "local-run.sh:" + echo -n " docker_base_image_name...................... " + echoInfo "$docker_base_image_name" + echo -n " template_name_override.... " + echoInfo "$template_name_override" + +} + +function parseInput() { + + local OPTIONS=b:t: + local LONGOPTS=docker_base_image_name:,template_name_override: + + # -use ! and PIPESTATUS to get exit code with errexit set + # -temporarily store output to be able to check for errors + # -activate quoting/enhanced mode (e.g. by writing out "--options") + # -pass arguments only via -- "$@" to separate them correctly + ! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@") + if [[ ${PIPESTATUS[0]} -ne 0 ]]; then + # e.g. return value is 1 + # then getopt has complained about wrong arguments to stdout + usage + exit 2 + fi + # read getopt's output this way to handle the quoting right: + eval set -- "$PARSED" + while true; do + case "$1" in + -b | --docker_base_image_name) + docker_base_image_name=$2 + shift 2 + ;; + -t | --template_name_override) + template_name_override=$2 + shift 2 + ;; + --) + shift + break + ;; + *) + echoError "Invalid option $1 $2" + exit 3 + ;; + esac + done +} + +# Bind environment from .env +dotenv + +# input variables +declare docker_base_image_tag="g${GO_VERSION}t${TF_VERSION}" +declare docker_base_image_name="msftcse/cobalt-test-base:$docker_base_image_tag" +declare template_name_override="" + +# Parse user input arguments +parseInput "$@" + +readonly BUILD_SOURCEBRANCHNAME=`git branch | sed -n '/\* /s///p'` +readonly BUILD_UPSTREAMBRANCH="master" + +function run_test_harness() { + echoInfo "INFO: loading environment" + check_required_env_variables + echoInput + echoInfo "INFO: verified that environment is fully defined" + remove_build_directory + case "$template_name_override" in + "") build_test_harness $BUILD_UPSTREAMBRANCH \ + $BUILD_SOURCEBRANCHNAME \ + $docker_base_image_name ;; + *) build_test_harness_from_template $docker_base_image_name \ + $template_name_override ;; + esac + + run_test_image +} + +function run_test_image() { + echoInfo "INFO: Running test harness container" + docker run --env-file .env --rm $BUILD_TEST_RUN_IMAGE:$BUILD_BUILDID + echoInfo "INFO: Completed test run" +} + +run_test_harness \ No newline at end of file diff --git a/test-harness/magefile.go b/test-harness/magefile.go new file mode 100755 index 0000000000000000000000000000000000000000..3c2c72bad2eb0b40c50bb588e7996030054f0300 --- /dev/null +++ b/test-harness/magefile.go @@ -0,0 +1,136 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Build a script to format and run tests of a Terraform module project +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" +) + +// Default The default target when the command executes `mage` in Cloud Shell +var Default = RunAllTargets + +func main() { + Default() +} + +// RunAllTargets A build step that runs Clean, Format, Unit and Integration in sequence +func RunAllTargets() { + mg.Deps(CleanAll) + mg.Deps(LintCheckGo) + mg.Deps(LintCheckTerraform) + mg.Deps(RunTestHarnessUnitTests) + mg.Deps(RunUnitTests) + mg.Deps(RunIntegrationTests) +} + +// RunTestHarnessUnitTests run unit test for the test harness itself +func RunTestHarnessUnitTests() error { + return sh.RunV("go", "test", "github.com/microsoft/cobalt/test-harness/infratests") +} + +// RunUnitTests A build step that runs unit tests +func RunUnitTests() error { + fmt.Println("INFO: Running unit tests...") + return FindAndRunTests("unit") +} + +// RunIntegrationTests A build step that runs integration tests +func RunIntegrationTests() error { + fmt.Println("INFO: Running integration tests...") + return FindAndRunTests("integration") +} + +// FindAndRunTests finds all tests with a given path suffix and runs them using `go test` +func FindAndRunTests(pathSuffix string) error { + goModules, err := sh.Output("go", "list", "./...") + if err != nil { + return err + } + + testTargetModules := make([]string, 0) + for _, module := range strings.Fields(goModules) { + if strings.HasSuffix(module, pathSuffix) { + testTargetModules = append(testTargetModules, module) + } + } + + if len(testTargetModules) == 0 { + return fmt.Errorf("No modules found for testing prefix '%s'", pathSuffix) + } + + cmdArgs := []string{"test"} + cmdArgs = append(cmdArgs, testTargetModules...) + cmdArgs = append(cmdArgs, "-v", "-timeout", "7200s") + return sh.RunV("go", cmdArgs...) +} + +// LintCheckGo A build step that fails if go code is not formatted properly +func LintCheckGo() error { + fmt.Println("INFO: Checking format for Go files...") + return verifyRunsQuietly("Run `go fmt ./...` to fix", "go", "fmt", "./...") +} + +// LintCheckTerraform a build step that fails if terraform files are not formatted properly +func LintCheckTerraform() error { + fmt.Println("INFO: Checking format for Terraform files...") + return verifyRunsQuietly("Run `terraform fmt` to fix the offending files", "terraform", "fmt") +} + +// runs a command and ensures that the exit code indicates success and that there is no output to stdout +func verifyRunsQuietly(instructionsToFix string, cmd string, args ...string) error { + output, err := sh.Output(cmd, args...) + + if err != nil { + return err + } + + if len(output) == 0 { + return nil + } + + return fmt.Errorf("ERROR: command '%s' with arguments %s failed. Output was: '%s'. %s", cmd, args, output, instructionsToFix) +} + +// CleanAll A build step that removes temporary build and test files +func CleanAll() error { + fmt.Println("INFO: Cleaning...") + return filepath.Walk(".", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && info.Name() == "vendor" { + return filepath.SkipDir + } + if info.IsDir() && info.Name() == ".terraform" { + os.RemoveAll(path) + fmt.Printf("Removed \"%v\"\n", path) + return filepath.SkipDir + } + if !info.IsDir() && (info.Name() == "terraform.tfstate" || + info.Name() == "terraform.tfplan" || + info.Name() == "terraform.tfstate.backup") { + os.Remove(path) + fmt.Printf("Removed \"%v\"\n", path) + } + return nil + }) +} diff --git a/test-harness/terratest-extensions/modules/azure/acr.go b/test-harness/terratest-extensions/modules/azure/acr.go new file mode 100755 index 0000000000000000000000000000000000000000..4e55494f6da9789aac37f1d44ef7c4386a054966 --- /dev/null +++ b/test-harness/terratest-extensions/modules/azure/acr.go @@ -0,0 +1,131 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "context" + "testing" + + "github.com/Azure/azure-sdk-for-go/services/containerregistry/mgmt/2017-10-01/containerregistry" +) + +func registriesClientE(subscriptionID string) (*containerregistry.RegistriesClient, error) { + authorizer, err := DeploymentServicePrincipalAuthorizer() + if err != nil { + return nil, err + } + client := containerregistry.NewRegistriesClient(subscriptionID) + client.Authorizer = authorizer + return &client, nil +} + +func webhookClientE(subscriptionID string) (*containerregistry.WebhooksClient, error) { + authorizer, err := DeploymentServicePrincipalAuthorizer() + if err != nil { + return nil, err + } + client := containerregistry.NewWebhooksClient(subscriptionID) + client.Authorizer = authorizer + return &client, nil +} + +// ACRNetworkAclsE - Return the newtwork ACLs for an ACR instance +func ACRNetworkAclsE(subscriptionID string, resourceGroupName string, acrName string) (*containerregistry.NetworkRuleSet, error) { + + client, err := registriesClientE(subscriptionID) + if err != nil { + return nil, err + } + + acr, err := client.Get(context.Background(), resourceGroupName, acrName) + if err != nil { + return nil, err + } + + return acr.NetworkRuleSet, nil +} + +// ACRNetworkAcls - Like ACRNetworkAclsE but fails in the case an error is returned +func ACRNetworkAcls(t *testing.T, subscriptionID string, resourceGroupName string, acrName string) *containerregistry.NetworkRuleSet { + acls, err := ACRNetworkAclsE(subscriptionID, resourceGroupName, acrName) + if err != nil { + t.Fatal(err) + } + return acls +} + +// ACRWebHookE - Return ACR Webhook definition +func ACRWebHookE(subscriptionID string, resourceGroupName string, acrName string, webhookName string) (*containerregistry.Webhook, error) { + client, err := webhookClientE(subscriptionID) + if err != nil { + return nil, err + } + + webhook, err := client.Get(context.Background(), resourceGroupName, acrName, webhookName) + if err != nil { + return nil, err + } + + return &webhook, nil +} + +// ACRWebHook - Like ACRWebHookE but fails in the case an error is returned +func ACRWebHook(t *testing.T, subscriptionID string, resourceGroupName string, acrName string, webhookName string) *containerregistry.Webhook { + webhooks, err := ACRWebHookE(subscriptionID, resourceGroupName, acrName, webhookName) + if err != nil { + t.Fatal(err) + } + return webhooks +} + +// ACRWebHookCallbackE - Get callback config for a webhook +func ACRWebHookCallbackE(subscriptionID string, resourceGroupName string, acrName string, webhookName string) (*containerregistry.CallbackConfig, error) { + client, err := webhookClientE(subscriptionID) + if err != nil { + return nil, err + } + + webhookCallback, err := client.GetCallbackConfig(context.Background(), resourceGroupName, acrName, webhookName) + if err != nil { + return nil, err + } + + return &webhookCallback, nil +} + +// ACRWebHookCallback - Like ACRWebHookCallbackE but fails in the case an error is returned +func ACRWebHookCallback(t *testing.T, subscriptionID string, resourceGroupName string, acrName string, webhookName string) *containerregistry.CallbackConfig { + webhookCallback, err := ACRWebHookCallbackE(subscriptionID, resourceGroupName, acrName, webhookName) + if err != nil { + t.Fatal(err) + } + return webhookCallback +} + +// ACRRegistryE - Return the Registry structure for the given ACR +func ACRRegistryE(subscriptionID string, resourceGroupName string, acrName string) (*containerregistry.Registry, error) { + + client, err := registriesClientE(subscriptionID) + if err != nil { + return nil, err + } + + acr, err := client.Get(context.Background(), resourceGroupName, acrName) + if err != nil { + return nil, err + } + + return &acr, nil +} diff --git a/test-harness/terratest-extensions/modules/azure/appgateway.go b/test-harness/terratest-extensions/modules/azure/appgateway.go new file mode 100644 index 0000000000000000000000000000000000000000..7e07616dcb63f47d2a35116b42256f878490e57f --- /dev/null +++ b/test-harness/terratest-extensions/modules/azure/appgateway.go @@ -0,0 +1,58 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-02-01/network" +) + +func applicationGatewaysClientE(subscriptionID string) (*network.ApplicationGatewaysClient, error) { + authorizer, err := DeploymentServicePrincipalAuthorizer() + if err != nil { + return nil, err + } + + client := network.NewApplicationGatewaysClient(subscriptionID) + client.Authorizer = authorizer + return &client, nil +} + +func getAppGatewayPropertiesE(client *network.ApplicationGatewaysClient, resourceGroupName string, applicationGatewayName string) (*network.ApplicationGateway, error) { //? + ctx := context.Background() + + applicationGateway, err := client.Get(ctx, resourceGroupName, applicationGatewayName) + if err != nil { + return nil, err + } + + return &applicationGateway, nil +} + +// GetAppGatewayProperties - Get properties for an app gateway +func GetAppGatewayProperties(subscription string, resourceGroupName string, appGatewayName string) (*network.ApplicationGateway, error) { + client, err := applicationGatewaysClientE(subscription) + if err != nil { + return nil, err + } + + appGateway, err := getAppGatewayPropertiesE(client, resourceGroupName, appGatewayName) + if err != nil { + return nil, err + } + + return appGateway, nil +} diff --git a/test-harness/terratest-extensions/modules/azure/authorizer.go b/test-harness/terratest-extensions/modules/azure/authorizer.go new file mode 100755 index 0000000000000000000000000000000000000000..f6da3b16428707b1de235aea8074067fdcdc8277 --- /dev/null +++ b/test-harness/terratest-extensions/modules/azure/authorizer.go @@ -0,0 +1,57 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/adal" + az "github.com/Azure/go-autorest/autorest/azure" + "os" + "strings" +) + +// DeploymentServicePrincipalAuthorizer - Returns an authorizer configured with the service principal +// used to execute the terraform commands +func DeploymentServicePrincipalAuthorizer() (autorest.Authorizer, error) { + return ServicePrincipalAuthorizer( + os.Getenv("ARM_CLIENT_ID"), + os.Getenv("ARM_CLIENT_SECRET"), + az.PublicCloud.ResourceManagerEndpoint) +} + +// KeyvaultServicePrincipalAuthorizer - gets an OAuthTokenAuthorizer for use with Key Vault +// keys and secrets. Note that Key Vault *Vaults* are managed by Azure Resource +// Manager. +func KeyvaultServicePrincipalAuthorizer() (autorest.Authorizer, error) { + return ServicePrincipalAuthorizer( + os.Getenv("ARM_CLIENT_ID"), + os.Getenv("ARM_CLIENT_SECRET"), + strings.TrimSuffix(az.PublicCloud.KeyVaultEndpoint, "/")) +} + +// ServicePrincipalAuthorizer - Configures a service principal authorizer that can be used to create bearer tokens +func ServicePrincipalAuthorizer(clientID string, clientSecret string, resource string) (autorest.Authorizer, error) { + oauthConfig, err := adal.NewOAuthConfig(az.PublicCloud.ActiveDirectoryEndpoint, os.Getenv("ARM_TENANT_ID")) + if err != nil { + return nil, err + } + + token, err := adal.NewServicePrincipalToken(*oauthConfig, clientID, clientSecret, resource) + if err != nil { + return nil, err + } + + return autorest.NewBearerAuthorizer(token), nil +} diff --git a/test-harness/terratest-extensions/modules/azure/azure.go b/test-harness/terratest-extensions/modules/azure/azure.go new file mode 100755 index 0000000000000000000000000000000000000000..aa6d1be86ef09aa9170e77472b7958ec04a1e3cc --- /dev/null +++ b/test-harness/terratest-extensions/modules/azure/azure.go @@ -0,0 +1,19 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +// azure - This package is intended to be the start of the azure module for Terratest. +// The package contains functions that are useful for inspecting Azure resources. +// Hopefully one day we can merge it upstream to Terratest itself. diff --git a/test-harness/terratest-extensions/modules/azure/cli.go b/test-harness/terratest-extensions/modules/azure/cli.go new file mode 100755 index 0000000000000000000000000000000000000000..10da86550bea782102245db583397a9bdeedfea5 --- /dev/null +++ b/test-harness/terratest-extensions/modules/azure/cli.go @@ -0,0 +1,42 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "github.com/gruntwork-io/terratest/modules/shell" + "os" + "testing" +) + +// CliServicePrincipalLoginE - Log into the local Azure CLI instance +func CliServicePrincipalLoginE(t *testing.T) error { + return shell.RunCommandE(t, shell.Command{ + Command: "az", + Args: []string{ + "login", "--service-principal", + "-u", os.Getenv("ARM_CLIENT_ID"), + "-p", os.Getenv("ARM_CLIENT_SECRET"), + "--tenant", os.Getenv("ARM_TENANT_ID"), + }, + }) +} + +// CliServicePrincipalLogin - Like CliServicePrincipalLoginE but fails in the case an error is returned +func CliServicePrincipalLogin(t *testing.T) { + err := CliServicePrincipalLoginE(t) + if err != nil { + t.Fatal(err) + } +} diff --git a/test-harness/terratest-extensions/modules/azure/cosmos.go b/test-harness/terratest-extensions/modules/azure/cosmos.go new file mode 100644 index 0000000000000000000000000000000000000000..76be481c2e8322d169c94f7a045c6c5e1cf32332 --- /dev/null +++ b/test-harness/terratest-extensions/modules/azure/cosmos.go @@ -0,0 +1,67 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "context" + + "testing" + + "github.com/Azure/azure-sdk-for-go/services/cosmos-db/mgmt/2015-04-08/documentdb" +) + +// cosmosClientE - Connects to the cosmos client +func cosmosClientE(subscriptionID string) (*documentdb.DatabaseAccountsClient, error) { + authorizer, err := DeploymentServicePrincipalAuthorizer() + if err != nil { + return nil, err + } + + client := documentdb.NewDatabaseAccountsClient(subscriptionID) + + client.Authorizer = authorizer + // Appends given user agent value to header for all future http calls to cosmos server for duration of integ tests. + client.AddToUserAgent("integration-test-harness") + return &client, err +} + +func getCosmosDBAccountE(subscriptionID string, resourceGroupName string, accountName string) (*documentdb.DatabaseAccount, error) { + client, err := cosmosClientE(subscriptionID) + + if err != nil { + return nil, err + } + + ctx := context.Background() + + response, err := client.Get(ctx, resourceGroupName, accountName) + + if err != nil { + return nil, err + } + + return &response, nil +} + +// GetCosmosDBAccount - Retrieves the properties of a CosmosDB Database Account. +func GetCosmosDBAccount(t *testing.T, subscriptionID string, resourceGroupName string, accountName string) *documentdb.DatabaseAccount { + resource, err := getCosmosDBAccountE(subscriptionID, resourceGroupName, accountName) + + if err != nil { + t.Fatal(err) + } + + return resource +} diff --git a/test-harness/terratest-extensions/modules/azure/keyvault.go b/test-harness/terratest-extensions/modules/azure/keyvault.go new file mode 100755 index 0000000000000000000000000000000000000000..9539d05f2adff7d90ff3406c7541876db81a6b4d --- /dev/null +++ b/test-harness/terratest-extensions/modules/azure/keyvault.go @@ -0,0 +1,84 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "context" + keyvaultSecret "github.com/Azure/azure-sdk-for-go/services/keyvault/2016-10-01/keyvault" + "github.com/Azure/azure-sdk-for-go/services/keyvault/mgmt/2018-02-14/keyvault" + "testing" +) + +func keyVaultClientE(subscriptionID string) (*keyvault.VaultsClient, error) { + authorizer, err := DeploymentServicePrincipalAuthorizer() + if err != nil { + return nil, err + } + + client := keyvault.NewVaultsClient(subscriptionID) + client.Authorizer = authorizer + return &client, err +} + +func keyVaultSecretClientE() (*keyvaultSecret.BaseClient, error) { + authorizer, err := KeyvaultServicePrincipalAuthorizer() + if err != nil { + return nil, err + } + + client := keyvaultSecret.New() + client.Authorizer = authorizer + return &client, err +} + +// KeyVaultNetworkAclsE - Return the newtwork ACLs for a KeyVault instance +func KeyVaultNetworkAclsE(subscriptionID string, resourceGroupName string, keyVaultName string) (*keyvault.NetworkRuleSet, error) { + + client, err := keyVaultClientE(subscriptionID) + if err != nil { + return nil, err + } + + vault, err := client.Get(context.Background(), resourceGroupName, keyVaultName) + if err != nil { + return nil, err + } + + return vault.Properties.NetworkAcls, nil +} + +// KeyVaultNetworkAcls - Like KeyVaultNetworkAclsE but fails in the case an error is returned +func KeyVaultNetworkAcls(t *testing.T, subscriptionID string, resourceGroupName string, keyVaultName string) *keyvault.NetworkRuleSet { + acls, err := KeyVaultNetworkAclsE(subscriptionID, resourceGroupName, keyVaultName) + if err != nil { + t.Fatal(err) + } + return acls +} + +// GetKeyVaultSecretValue - Returns the keyvault secret value +func GetKeyVaultSecretValue(t *testing.T, vaultURI string, secretName string, secretVersion string) *string { + client, err := keyVaultSecretClientE() + if err != nil { + t.Fatal(err) + } + secret, err := client.GetSecret(context.Background(), vaultURI, secretName, secretVersion) + + if err != nil { + t.Fatal(err) + } + + return secret.Value +} diff --git a/test-harness/terratest-extensions/modules/azure/network.go b/test-harness/terratest-extensions/modules/azure/network.go new file mode 100755 index 0000000000000000000000000000000000000000..1e1af629fc1574354fea9df636ede351d91a2992 --- /dev/null +++ b/test-harness/terratest-extensions/modules/azure/network.go @@ -0,0 +1,62 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "context" + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2018-12-01/network" + "testing" +) + +func vnetClient(subscriptionID string) (*network.VirtualNetworksClient, error) { + authorizer, err := DeploymentServicePrincipalAuthorizer() + if err != nil { + return nil, err + } + + client := network.NewVirtualNetworksClient(subscriptionID) + client.Authorizer = authorizer + return &client, err +} + +// VnetSubnetsListE - Return the subnets that exist wihin a given VNET +func VnetSubnetsListE(subscriptionID string, resourceGroupName string, vnetName string) ([]string, error) { + + client, err := vnetClient(subscriptionID) + if err != nil { + return nil, err + } + + vnet, err := client.Get(context.Background(), resourceGroupName, vnetName, "") + if err != nil { + return nil, err + } + + subnets := make([]string, len(*vnet.VirtualNetworkPropertiesFormat.Subnets)) + for index, subnet := range *vnet.VirtualNetworkPropertiesFormat.Subnets { + subnets[index] = *subnet.ID + } + + return subnets, nil +} + +// VnetSubnetsList - Like VnetSubnetsListE but fails in the case an error is returned +func VnetSubnetsList(t *testing.T, subscriptionID string, resourceGroupName string, vnetName string) []string { + subnets, err := VnetSubnetsListE(subscriptionID, resourceGroupName, vnetName) + if err != nil { + t.Fatal(err) + } + return subnets +} diff --git a/test-harness/terratest-extensions/modules/azure/redis.go b/test-harness/terratest-extensions/modules/azure/redis.go new file mode 100644 index 0000000000000000000000000000000000000000..58529378d7974e412914128fef726db5df6f52d7 --- /dev/null +++ b/test-harness/terratest-extensions/modules/azure/redis.go @@ -0,0 +1,174 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "context" + "crypto/tls" + "fmt" + "github.com/Azure/azure-sdk-for-go/services/redis/mgmt/2018-03-01/redis" + redis7Api "github.com/go-redis/redis/v7" + "testing" + "time" +) + +func redisAzureClientE(subscriptionID string) (*redis.Client, error) { + authorizer, err := DeploymentServicePrincipalAuthorizer() + if err != nil { + return nil, err + } + + client := redis.NewClient(subscriptionID) + client.Authorizer = authorizer + return &client, err +} + +func redisClientE(hostname string, accessKey string) (*redis7Api.Client, error) { + client := redis7Api.NewClient(&redis7Api.Options{ + Addr: hostname, + Password: accessKey, + DB: 0, + TLSConfig: &tls.Config{}, + DialTimeout: 10000000000, + }) + + healthCheck := client.Ping() + pingStatus, err := healthCheck.Result() + + if err != nil { + return nil, err + } + + // A redis cluster that returns a PONG is fully functional and works as expected + if pingStatus != "PONG" { + err = fmt.Errorf("REDIS ping status failed with result - %s", pingStatus) + } + + return client, err +} + +// RedisClient - Instantiate a new client from redis's collection pool +func RedisClient(t *testing.T, hostname string, accessKey string) *redis7Api.Client { + client, err := redisClientE(hostname, accessKey) + + if err != nil { + t.Fatal(fmt.Errorf("Failed to create Redis API client: %v", err)) + } + + return client +} + +// SetRedisCacheEntry - Sets a cache entry on the target redis cluster +func SetRedisCacheEntry(t *testing.T, client *redis7Api.Client, cacheKey string, cacheValue interface{}, expiration time.Duration) string { + setCmdResponse := client.Set(cacheKey, cacheValue, expiration) + result, err := setCmdResponse.Result() + + if err != nil { + t.Fatal(err) + } + + return result +} + +// GetRedisCacheEntryValueStr - Retrieves a cache entry from the target redis cluster +func GetRedisCacheEntryValueStr(t *testing.T, client *redis7Api.Client, cacheKey string) string { + getCmdResponse := client.Get(cacheKey) + result, err := getCmdResponse.Result() + if err != nil { + t.Fatal(err) + } + + return result +} + +// RemoveRedisCacheEntry - Removes a cache entry from the target redis cluster +func RemoveRedisCacheEntry(t *testing.T, client *redis7Api.Client, cacheKey string) int64 { + deleteCmdResponse := client.Del(cacheKey) + result, err := deleteCmdResponse.Result() + + if err != nil { + t.Fatal(err) + } + + return result +} + +// ListCachesByResourceGroup - Lists the caches by resource group +func ListCachesByResourceGroup(t *testing.T, subscriptionID string, resourceGroupName string) *[]redis.ResourceType { + caches, err := listCachesByResourceGroupE(subscriptionID, resourceGroupName) + + if err != nil { + t.Fatal(err) + } + + return caches +} + +func listCachesByResourceGroupE(subscriptionID string, resourceGroupName string) (*[]redis.ResourceType, error) { + client, err := redisAzureClientE(subscriptionID) + + if err != nil { + return nil, err + } + + ctx := context.Background() + results := []redis.ResourceType{} + + paginatedResponse, err := client.ListByResourceGroup(ctx, resourceGroupName) + + if err != nil { + return nil, err + } + + for paginatedResponse.NotDone() { + results = append(results, paginatedResponse.Values()...) + err = paginatedResponse.Next() + if err != nil { + return nil, err + } + } + + return &results, nil +} + +// GetCacheE - Retrieves the properties of a cache. +func GetCacheE(subscriptionID string, resourceGroupName string, cacheName string) (*redis.ResourceType, error) { + client, err := redisAzureClientE(subscriptionID) + + if err != nil { + return nil, err + } + + ctx := context.Background() + + resourceType, err := client.Get(ctx, resourceGroupName, cacheName) + + if err != nil { + return nil, err + } + + return &resourceType, nil +} + +// GetCache - Retrieves the properties of a cache. +func GetCache(t *testing.T, subscriptionID string, resourceGroupName string, cacheName string) *redis.ResourceType { + resourceType, err := GetCacheE(subscriptionID, resourceGroupName, cacheName) + + if err != nil { + t.Fatal(err) + } + + return resourceType +} diff --git a/test-harness/terratest-extensions/modules/azure/roles.go b/test-harness/terratest-extensions/modules/azure/roles.go new file mode 100644 index 0000000000000000000000000000000000000000..18c5ec1d1850cb24e6254ca15afc1bbdb887d7e0 --- /dev/null +++ b/test-harness/terratest-extensions/modules/azure/roles.go @@ -0,0 +1,101 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "context" + "fmt" + "testing" + + "github.com/Azure/azure-sdk-for-go/services/authorization/mgmt/2015-07-01/authorization" +) + +type roleClients struct { + DefinitionClient authorization.RoleDefinitionsClient + AssignmentsClient authorization.RoleAssignmentsClient +} + +func getRoleClients(subscriptionID string) (*roleClients, error) { + authorizer, err := DeploymentServicePrincipalAuthorizer() + if err != nil { + return nil, err + } + + clients := roleClients{ + DefinitionClient: authorization.NewRoleDefinitionsClient(subscriptionID), + AssignmentsClient: authorization.NewRoleAssignmentsClient(subscriptionID), + } + + clients.DefinitionClient.Authorizer = authorizer + clients.AssignmentsClient.Authorizer = authorizer + return &clients, nil +} + +// ListRoleAssignmentsE - Return the role assignments for an object ID +func ListRoleAssignmentsE(subscriptionID, objectID string) (*[]authorization.RoleAssignment, error) { + clients, err := getRoleClients(subscriptionID) + if err != nil { + return nil, err + } + + paginatedResponse, err := clients.AssignmentsClient.List(context.Background(), fmt.Sprintf("principalId eq '%s'", objectID)) + results := []authorization.RoleAssignment{} + if err != nil { + return nil, err + } + + for paginatedResponse.NotDone() { + results = append(results, paginatedResponse.Values()...) + err = paginatedResponse.Next() + if err != nil { + return nil, err + } + } + + return &results, nil +} + +// ListRoleAssignments - Like ListRoleAssignmentsE but fails in the case an error is returned +func ListRoleAssignments(t *testing.T, subscriptionID, objectID string) *[]authorization.RoleAssignment { + roleAssignments, err := ListRoleAssignmentsE(subscriptionID, objectID) + if err != nil { + t.Fatal(err) + } + return roleAssignments +} + +// RoleNameE - Get the name of a role +func RoleNameE(subscriptionID, roleID string) (string, error) { + clients, err := getRoleClients(subscriptionID) + if err != nil { + return "", err + } + + role, err := clients.DefinitionClient.GetByID(context.Background(), roleID) + if err != nil { + return "", err + } + + return *role.Properties.RoleName, nil +} + +// RoleName - Like RoleNameE but fails in the case an error is returned +func RoleName(t *testing.T, subscriptionID, roleID string) string { + name, err := RoleNameE(subscriptionID, roleID) + if err != nil { + t.Fatal(err) + } + return name +} diff --git a/test-harness/terratest-extensions/modules/azure/servicebus.go b/test-harness/terratest-extensions/modules/azure/servicebus.go new file mode 100644 index 0000000000000000000000000000000000000000..5e5fb2a5fee027ae66ff6bbeedabe90090c96549 --- /dev/null +++ b/test-harness/terratest-extensions/modules/azure/servicebus.go @@ -0,0 +1,424 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "context" + "testing" + + "github.com/Azure/azure-sdk-for-go/services/servicebus/mgmt/2017-04-01/servicebus" +) + +func serviceBusClientE(subscriptionID string) (*servicebus.BaseClient, error) { + authorizer, err := DeploymentServicePrincipalAuthorizer() + if err != nil { + return nil, err + } + + client := servicebus.New(subscriptionID) + client.Authorizer = authorizer + return &client, nil +} + +func serviceBusNamespaceClientE(subscriptionID string) (*servicebus.NamespacesClient, error) { + authorizer, err := DeploymentServicePrincipalAuthorizer() + if err != nil { + return nil, err + } + + nsClient := servicebus.NewNamespacesClient(subscriptionID) + nsClient.Authorizer = authorizer + return &nsClient, nil +} + +func serviceBusTopicClientE(subscriptionID string) (*servicebus.TopicsClient, error) { + authorizer, err := DeploymentServicePrincipalAuthorizer() + if err != nil { + return nil, err + } + + tClient := servicebus.NewTopicsClient(subscriptionID) + tClient.Authorizer = authorizer + return &tClient, nil +} + +func serviceBusSubscriptionsClientE(subscriptionID string) (*servicebus.SubscriptionsClient, error) { + authorizer, err := DeploymentServicePrincipalAuthorizer() + if err != nil { + return nil, err + } + + sClient := servicebus.NewSubscriptionsClient(subscriptionID) + sClient.Authorizer = authorizer + return &sClient, nil +} + +// ListServiceBusNamespaceE list all SB namespaces in all resource groups in the given subscription ID +func ListServiceBusNamespaceE(subscriptionID string) (*[]servicebus.SBNamespace, error) { + nsClient, err := serviceBusNamespaceClientE(subscriptionID) + if err != nil { + return nil, err + } + + iteratorSBNamespace, err := nsClient.ListComplete(context.Background()) + if err != nil { + return nil, err + } + + results := make([]servicebus.SBNamespace, 0) + for iteratorSBNamespace.NotDone() { + results = append(results, iteratorSBNamespace.Value()) + err = iteratorSBNamespace.Next() + if err != nil { + return nil, err + } + } + + return &results, nil +} + +// ListServiceBusNamespace - like ListServiceBusNamespaceE but fails in the case an error is returned +func ListServiceBusNamespace(t *testing.T, subscriptionID string) *[]servicebus.SBNamespace { + results, err := ListServiceBusNamespaceE(subscriptionID) + if err != nil { + t.Fatal(err) + } + + return results +} + +// ListServiceBusNamespaceNamesE list names of all SB namespaces in all resource groups in the given subscription ID +func ListServiceBusNamespaceNamesE(subscriptionID string) (*[]string, error) { + sbNamespace, err := ListServiceBusNamespaceE(subscriptionID) + + if err != nil { + return nil, err + } + + results := make([]string, 0) + for _, namespace := range *sbNamespace { + results = append(results, *namespace.Name) + if err != nil { + return nil, err + } + } + + return &results, nil +} + +// ListServiceBusNamespaceNames like ListServiceBusNamespaceNamesE but fails in the case an error is returned +func ListServiceBusNamespaceNames(t *testing.T, subscriptionID string) *[]string { + results, err := ListServiceBusNamespaceNamesE(subscriptionID) + if err != nil { + t.Fatal(err) + } + + return results +} + +// ListServiceBusNamespaceIDsE list IDs of all SB namespaces in all resource groups in the given subscription ID +func ListServiceBusNamespaceIDsE(subscriptionID string) (*[]string, error) { + sbNamespace, err := ListServiceBusNamespaceE(subscriptionID) + + if err != nil { + return nil, err + } + + results := make([]string, 0) + for _, namespace := range *sbNamespace { + results = append(results, *namespace.ID) + if err != nil { + return nil, err + } + } + + return &results, nil +} + +// ListServiceBusNamespaceIDs like ListServiceBusNamespaceIDsE but fails in the case an error is returned +func ListServiceBusNamespaceIDs(t *testing.T, subscriptionID string) *[]string { + results, err := ListServiceBusNamespaceIDsE(subscriptionID) + if err != nil { + t.Fatal(err) + } + + return results +} + +// ListServiceBusNamespaceByResourceGroupE list all SB namespaces in the given resource group +func ListServiceBusNamespaceByResourceGroupE(subscriptionID string, resourceGroup string) (*[]servicebus.SBNamespace, error) { + nsClient, err := serviceBusNamespaceClientE(subscriptionID) + if err != nil { + return nil, err + } + + iteratorSBNamespace, err := nsClient.ListByResourceGroupComplete(context.Background(), resourceGroup) + if err != nil { + return nil, err + } + + results := make([]servicebus.SBNamespace, 0) + + for iteratorSBNamespace.NotDone() { + results = append(results, iteratorSBNamespace.Value()) + err = iteratorSBNamespace.Next() + if err != nil { + return nil, err + } + } + + return &results, nil +} + +// ListServiceBusNamespaceByResourceGroup like ListServiceBusNamespaceByResourceGroupE but fails in the case an error is returned +func ListServiceBusNamespaceByResourceGroup(t *testing.T, subscriptionID string, resourceGroup string) *[]servicebus.SBNamespace { + results, err := ListServiceBusNamespaceByResourceGroupE(subscriptionID, resourceGroup) + if err != nil { + t.Fatal(err) + } + + return results +} + +// ListServiceBusNamespaceNamesByResourceGroupE list names of all SB namespaces in the given resource group +func ListServiceBusNamespaceNamesByResourceGroupE(subscriptionID string, resourceGroup string) (*[]string, error) { + sbNamespace, err := ListServiceBusNamespaceByResourceGroupE(subscriptionID, resourceGroup) + + if err != nil { + return nil, err + } + + results := make([]string, 0) + for _, namespace := range *sbNamespace { + results = append(results, *namespace.Name) + if err != nil { + return nil, err + } + } + + return &results, nil +} + +// ListServiceBusNamespaceNamesByResourceGroup like ListServiceBusNamespaceNamesByResourceGroupE but fails in the case an error is returned +func ListServiceBusNamespaceNamesByResourceGroup(t *testing.T, subscriptionID string, resourceGroup string) *[]string { + results, err := ListServiceBusNamespaceNamesByResourceGroupE(subscriptionID, resourceGroup) + if err != nil { + t.Fatal(err) + } + + return results +} + +// ListServiceBusNamespaceIDsByResourceGroupE list IDs of all SB namespaces in the given resource group +func ListServiceBusNamespaceIDsByResourceGroupE(subscriptionID string, resourceGroup string) (*[]string, error) { + sbNamespace, err := ListServiceBusNamespaceByResourceGroupE(subscriptionID, resourceGroup) + + if err != nil { + return nil, err + } + + results := make([]string, 0) + for _, namespace := range *sbNamespace { + results = append(results, *namespace.ID) + if err != nil { + return nil, err + } + } + + return &results, nil +} + +// ListServiceBusNamespaceIDsByResourceGroup like ListServiceBusNamespaceIDsByResourceGroupE but fails in the case an error is returned +func ListServiceBusNamespaceIDsByResourceGroup(t *testing.T, subscriptionID string, resourceGroup string) *[]string { + results, err := ListServiceBusNamespaceIDsByResourceGroupE(subscriptionID, resourceGroup) + if err != nil { + t.Fatal(err) + } + + return results +} + +// ListNamespaceAuthRulesE - authenticate namespace client and enumerates all values to get list of authorization rules for the given namespace name, +// automatically crossing page boundaries as required. +func ListNamespaceAuthRulesE(subscriptionID string, namespace string, resourceGroup string) (*[]string, error) { + nsClient, err := serviceBusNamespaceClientE(subscriptionID) + if err != nil { + return nil, err + } + iteratorNamespaceRules, err := nsClient.ListAuthorizationRulesComplete( + context.Background(), resourceGroup, namespace) + + if err != nil { + return nil, err + } + + results := make([]string, 0) + for iteratorNamespaceRules.NotDone() { + results = append(results, *(iteratorNamespaceRules.Value()).Name) + err = iteratorNamespaceRules.Next() + if err != nil { + return nil, err + } + } + return &results, nil +} + +// ListNamespaceAuthRules - like ListNamespaceAuthRulesE but fails in the case an error is returned +func ListNamespaceAuthRules(t *testing.T, subscriptionID string, namespace string, resourceGroup string) *[]string { + results, err := ListNamespaceAuthRulesE(subscriptionID, namespace, resourceGroup) + if err != nil { + t.Fatal(err) + } + + return results +} + +// ListNamespaceTopicsE - authenticate topic client and enumerates all values, automatically crossing page boundaries as required. +func ListNamespaceTopicsE(subscriptionID string, namespace string, resourceGroup string) (*[]servicebus.SBTopic, error) { + tClient, err := serviceBusTopicClientE(subscriptionID) + if err != nil { + return nil, err + } + + iteratorTopics, err := tClient.ListByNamespaceComplete(context.Background(), resourceGroup, namespace, nil, nil) + if err != nil { + return nil, err + } + + results := make([]servicebus.SBTopic, 0) + + for iteratorTopics.NotDone() { + results = append(results, iteratorTopics.Value()) + err = iteratorTopics.Next() + if err != nil { + return nil, err + } + } + + return &results, nil +} + +// ListNamespaceTopics - like ListNamespaceTopicsE but fails in the case an error is returned +func ListNamespaceTopics(t *testing.T, subscriptionID string, namespace string, resourceGroup string) *[]servicebus.SBTopic { + results, err := ListNamespaceTopicsE(subscriptionID, namespace, resourceGroup) + if err != nil { + t.Fatal(err) + } + + return results +} + +// ListTopicSubscriptionsE - authenticate subscriptions client and enumerates all values, automatically crossing page boundaries as required. +func ListTopicSubscriptionsE(subscriptionID string, namespace string, resourceGroup string, topicName string) ([]servicebus.SBSubscription, error) { + sClient, err := serviceBusSubscriptionsClientE(subscriptionID) + if err != nil { + return nil, err + } + iteratorSubscription, err := sClient.ListByTopicComplete(context.Background(), resourceGroup, namespace, topicName, nil, nil) + + if err != nil { + return nil, err + } + + results := make([]servicebus.SBSubscription, 0) + + for iteratorSubscription.NotDone() { + results = append(results, iteratorSubscription.Value()) + err = iteratorSubscription.Next() + if err != nil { + return nil, err + } + } + return results, nil +} + +// ListTopicSubscriptions - like ListTopicSubscriptionsE but fails in the case an error is returned +func ListTopicSubscriptions(t *testing.T, subscriptionID string, namespace string, resourceGroup string, topicName string) *[]servicebus.SBSubscription { + results, err := ListTopicSubscriptionsE(subscriptionID, namespace, resourceGroup, topicName) + if err != nil { + t.Fatal(err) + } + + return &results +} + +// ListTopicSubscriptionsNameE - authenticate subscriptions client and enumerates all values to get list of subscriptions for the given topic name, +// automatically crossing page boundaries as required. +func ListTopicSubscriptionsNameE(subscriptionID string, namespace string, resourceGroup string, topicName string) (*[]string, error) { + sClient, err := serviceBusSubscriptionsClientE(subscriptionID) + if err != nil { + return nil, err + } + iteratorSubscription, err := sClient.ListByTopicComplete(context.Background(), resourceGroup, namespace, topicName, nil, nil) + + if err != nil { + return nil, err + } + + results := make([]string, 0) + for iteratorSubscription.NotDone() { + results = append(results, *(iteratorSubscription.Value()).Name) + err = iteratorSubscription.Next() + if err != nil { + return nil, err + } + } + return &results, nil +} + +// ListTopicSubscriptionsName - like ListTopicSubscriptionsNameE but fails in the case an error is returned +func ListTopicSubscriptionsName(t *testing.T, subscriptionID string, namespace string, resourceGroup string, topicName string) *[]string { + results, err := ListTopicSubscriptionsNameE(subscriptionID, namespace, resourceGroup, topicName) + if err != nil { + t.Fatal(err) + } + + return results +} + +// ListTopicAuthRulesE - authenticate topic client and enumerates all values to get list of authorization rules for the given topic name, +// automatically crossing page boundaries as required. +func ListTopicAuthRulesE(subscriptionID string, namespace string, resourceGroup string, topicName string) (*[]string, error) { + tClient, err := serviceBusTopicClientE(subscriptionID) + if err != nil { + return nil, err + } + iteratorTopicsRules, err := tClient.ListAuthorizationRulesComplete( + context.Background(), resourceGroup, namespace, topicName) + + if err != nil { + return nil, err + } + + results := make([]string, 0) + for iteratorTopicsRules.NotDone() { + results = append(results, *(iteratorTopicsRules.Value()).Name) + err = iteratorTopicsRules.Next() + if err != nil { + return nil, err + } + } + return &results, nil +} + +// ListTopicAuthRules - like ListTopicAuthRulesE but fails in the case an error is returned +func ListTopicAuthRules(t *testing.T, subscriptionID string, namespace string, resourceGroup string, topicName string) *[]string { + results, err := ListTopicAuthRulesE(subscriptionID, namespace, resourceGroup, topicName) + if err != nil { + t.Fatal(err) + } + + return results +} diff --git a/test-harness/terratest-extensions/modules/azure/storage.go b/test-harness/terratest-extensions/modules/azure/storage.go new file mode 100644 index 0000000000000000000000000000000000000000..c4c411917603ef01f78e0c30e53c32f5c13a43c4 --- /dev/null +++ b/test-harness/terratest-extensions/modules/azure/storage.go @@ -0,0 +1,72 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "context" + "testing" + + "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-04-01/storage" +) + +func storageClientE(subscriptionID string) (*storage.BlobContainersClient, error) { + authorizer, err := DeploymentServicePrincipalAuthorizer() + if err != nil { + return nil, err + } + + client := storage.NewBlobContainersClient(subscriptionID) + client.Authorizer = authorizer + return &client, err +} + +func listAccountContainers(client *storage.BlobContainersClient, resourceGroupName string, accountName string) (*[]storage.ListContainerItem, error) { + MaxContainerPageSize := "10" + paginatedResponse, err := client.List(context.Background(), resourceGroupName, accountName, "", MaxContainerPageSize, "") + + if err != nil { + return nil, err + } + + return paginatedResponse.Value, nil + + // results := []storage.ListContainerItem{} + + // for paginatedResponse.NotDone() { + // results = append(results, paginatedResponse.Values()...) + // err = paginatedResponse.Next() + // if err != nil { + // return nil, err + // } + // } + + // return &results, nil +} + +// ListAccountContainers - Lists the containers for a target storage account +func ListAccountContainers(t *testing.T, subscriptionID string, resourceGroupName string, accountName string) *[]storage.ListContainerItem { + client, err := storageClientE(subscriptionID) + if err != nil { + t.Fatal(err) + } + + containers, err := listAccountContainers(client, resourceGroupName, accountName) + + if err != nil { + t.Fatal(err) + } + + return containers +} diff --git a/test-harness/terratest-extensions/modules/azure/webapp.go b/test-harness/terratest-extensions/modules/azure/webapp.go new file mode 100755 index 0000000000000000000000000000000000000000..16e61ea98f1643f24a43e7396e62dec6c971ba38 --- /dev/null +++ b/test-harness/terratest-extensions/modules/azure/webapp.go @@ -0,0 +1,132 @@ +// Copyright © Microsoft Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "context" + "encoding/json" + "fmt" + "github.com/Azure/azure-sdk-for-go/services/web/mgmt/2018-02-01/web" + "testing" +) + +func webAppClient(subscriptionID string) (*web.AppsClient, error) { + authorizer, err := DeploymentServicePrincipalAuthorizer() + if err != nil { + return nil, err + } + + client := web.NewAppsClient(subscriptionID) + client.Authorizer = authorizer + return &client, nil +} + +// WebAppCDUriE - Return the CD URL that can be used to trigger an ACR pull and redeploy +func WebAppCDUriE(subscriptionID string, resourceGroupName string, webAppName string) (string, error) { + + client, err := webAppClient(subscriptionID) + if err != nil { + return "", err + } + + ctx := context.Background() + httpResponse, err := client.ListPublishingCredentials(ctx, resourceGroupName, webAppName) + if err != nil { + return "", err + } + + err = httpResponse.WaitForCompletion(ctx, client.Client) + if err != nil { + return "", err + } + + var jsonResponse map[string]interface{} + err = json.NewDecoder(httpResponse.Response().Body).Decode(&jsonResponse) + if err != nil { + return "", err + } + + properties, propertiesExist := jsonResponse["properties"] + if !propertiesExist { + return "", fmt.Errorf("`properties` attribute missing from response of ListPublishingCredentials()") + } + + propertiesMap := properties.(map[string]interface{}) + scmURI, scmURIExists := propertiesMap["scmUri"] + if !scmURIExists { + return "", fmt.Errorf("`properties.scmUri` attribute missing from response of ListPublishingCredentials()") + } + + return scmURI.(string) + "/docker/hook", nil +} + +// WebAppCDUri - Like WebAppCDUriE but fails in the case an error is returned +func WebAppCDUri(t *testing.T, subscriptionID string, resourceGroupName string, webAppName string) string { + cdURI, err := WebAppCDUriE(subscriptionID, resourceGroupName, webAppName) + if err != nil { + t.Fatal(err) + } + return cdURI +} + +// WebAppSiteConfigurationE - Return the configuration for a webapp +func WebAppSiteConfigurationE(subscriptionID string, resourceGroupName string, webAppName string) (*web.SiteConfig, error) { + + client, err := webAppClient(subscriptionID) + if err != nil { + return nil, err + } + + appConfiguration, err := client.GetConfiguration(context.Background(), resourceGroupName, webAppName) + if err != nil { + return nil, err + } + + return appConfiguration.SiteConfig, nil +} + +// WebAppAuthSettingsClientIDE - Return the authn/authz settings for a webapp +func WebAppAuthSettingsClientIDE(subscriptionID string, resourceGroupName string, webAppName string) (*string, error) { + + client, err := webAppClient(subscriptionID) + if err != nil { + return nil, err + } + + authConfiguration, err := client.GetAuthSettings(context.Background(), resourceGroupName, webAppName) + if err != nil { + return nil, err + } + + return authConfiguration.SiteAuthSettingsProperties.ClientID, nil +} + +// WebAppSiteConfiguration - Like WebAppSiteConfigurationE but fails in the case an error is returned +func WebAppSiteConfiguration(t *testing.T, subscriptionID string, resourceGroupName string, webAppName string) *web.SiteConfig { + appConfiguration, err := WebAppSiteConfigurationE(subscriptionID, resourceGroupName, webAppName) + if err != nil { + t.Fatal(err) + } + return appConfiguration +} + +// WebAppEasyAuthClientID - Like WebAppAuthSettingsClientIDE but fails in the case an error is returned +func WebAppEasyAuthClientID(t *testing.T, subscriptionID string, resourceGroupName string, webAppName string) *string { + clientID, err := WebAppAuthSettingsClientIDE(subscriptionID, resourceGroupName, webAppName) + if err != nil { + t.Fatal(err) + } + return clientID +}