diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 46ccf7612f..ce7bfdcc34 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -75,7 +75,7 @@ COPY ["e2e_tests/requirements.txt", "/tmp/pip-tmp/e2e_tests/"] RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt && rm -rf /tmp/pip-tmp # Install azure-cli -ARG AZURE_CLI_VERSION=2.29.2-1~buster +ARG AZURE_CLI_VERSION=2.36.0-1~buster COPY .devcontainer/scripts/azure-cli.sh /tmp/ RUN export AZURE_CLI_VERSION=${AZURE_CLI_VERSION} \ && /tmp/azure-cli.sh diff --git a/.github/linters/.tflint.hcl b/.github/linters/.tflint.hcl new file mode 100644 index 0000000000..9ecdd2c68a --- /dev/null +++ b/.github/linters/.tflint.hcl @@ -0,0 +1,8 @@ +config { + module = true + force = false +} + +plugin "azurerm" { + enabled = true +} diff --git a/.github/scripts/build.js b/.github/scripts/build.js index dae470d6dd..84ccf90333 100644 --- a/.github/scripts/build.js +++ b/.github/scripts/build.js @@ -17,19 +17,6 @@ async function getCommandFromComment({ core, context, github }) { const runId = context.runId; const prAuthorUsername = context.payload.issue.user.login; - // only allow actions for users with write access - if (!await userHasWriteAccessToRepo({ core, github }, commentUsername, repoOwner, repoName)) { - core.notice("Command: none - user doesn't have write permission]"); - await github.rest.issues.createComment({ - owner: repoOwner, - repo: repoName, - issue_number: prNumber, - body: `Sorry, @${commentUsername}, only users with write access to the repo can run pr-bot commands.` - }); - logAndSetOutput(core, "command", "none"); - return "none"; - } - // Determine PR SHA etc const ciGitRef = getRefForPr(prNumber); logAndSetOutput(core, "ciGitRef", ciGitRef); @@ -65,7 +52,20 @@ async function getCommandFromComment({ core, context, github }) { let command = "none"; const trimmedFirstLine = commentFirstLine.trim(); if (trimmedFirstLine[0] === "/") { - const parts = trimmedFirstLine.split(' ').filter(p=>p !== ''); + // only allow actions for users with write access + if (!await userHasWriteAccessToRepo({ core, github }, commentUsername, repoOwner, repoName)) { + core.notice("Command: none - user doesn't have write permission]"); + await github.rest.issues.createComment({ + owner: repoOwner, + repo: repoName, + issue_number: prNumber, + body: `Sorry, @${commentUsername}, only users with write access to the repo can run pr-bot commands.` + }); + logAndSetOutput(core, "command", "none"); + return "none"; + } + + const parts = trimmedFirstLine.split(' ').filter(p => p !== ''); const commandText = parts[0]; switch (commandText) { case "/test": diff --git a/.github/scripts/build.test.js b/.github/scripts/build.test.js index 403e783a6e..057aaa9e56 100644 --- a/.github/scripts/build.test.js +++ b/.github/scripts/build.test.js @@ -53,27 +53,49 @@ describe('getCommandFromComment', () => { } describe('with non-contributor', () => { - test(`for '/test' should return 'none'`, async () => { - const context = createCommentContext({ - username: 'non-contributor', - body: '/test', + describe(`for '/test`, () => { + test(`should return 'none'`, async () => { + const context = createCommentContext({ + username: 'non-contributor', + body: '/test', + }); + const command = await getCommandFromComment({ core, context, github }); + expect(outputFor(mockCoreSetOutput, 'command')).toBe('none'); }); - const command = await getCommandFromComment({ core, context, github }); - expect(outputFor(mockCoreSetOutput, 'command')).toBe('none'); - }); - test(`should add a comment indicating that the user cannot run commands`, async () => { - const context = createCommentContext({ - username: 'non-contributor', - body: '/test', - pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES, + test(`should add a comment indicating that the user cannot run commands`, async () => { + const context = createCommentContext({ + username: 'non-contributor', + body: '/test', + pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES, + }); + await getCommandFromComment({ core, context, github }); + expect(mockGithubRestIssuesCreateComment).toHaveComment({ + owner: 'someOwner', + repo: 'someRepo', + issue_number: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES, + bodyMatcher: /Sorry, @non-contributor, only users with write access to the repo can run pr-bot commands./ + }); }); - await getCommandFromComment({ core, context, github }); - expect(mockGithubRestIssuesCreateComment).toHaveComment({ - owner: 'someOwner', - repo: 'someRepo', - issue_number: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES, - bodyMatcher: /Sorry, @non-contributor, only users with write access to the repo can run pr-bot commands./ + }); + describe(`for 'non-command`, () => { + test(`should return 'none'`, async () => { + const context = createCommentContext({ + username: 'non-contributor', + body: 'non-command', + }); + const command = await getCommandFromComment({ core, context, github }); + expect(outputFor(mockCoreSetOutput, 'command')).toBe('none'); + }); + + test(`should not add a comment`, async () => { + const context = createCommentContext({ + username: 'non-contributor', + body: 'non-command', + pullRequestNumber: PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES, + }); + await getCommandFromComment({ core, context, github }); + expect(mockGithubRestIssuesCreateComment).not.toHaveBeenCalled(); }); }); diff --git a/.github/workflows/build_validation_develop.yml b/.github/workflows/build_validation_develop.yml index f01e02f34a..20bc0445f6 100644 --- a/.github/workflows/build_validation_develop.yml +++ b/.github/workflows/build_validation_develop.yml @@ -42,14 +42,17 @@ jobs: if: ${{ steps.filter.outputs.terraform == 'true' }} run: | find . -type d -name 'terraform' -not -path '*cnab*' -print0 \ - | xargs -0 -I{} sh -c 'echo "***** Validating: {} *****"; \ + | xargs -0 -I{} sh -c 'echo "***** Validating: {} *****"; \https://github.com/github/super-linter/issues/2433 terraform -chdir={} init -backend=false; terraform -chdir={} validate' - name: Lint code base # the slim image is 2GB smaller and we don't use the extra stuff # Moved this after the Terraform checks above due something similar to this issue: https://github.com/github/super-linter/issues/2433 - uses: github/super-linter/slim@v4 + uses: github/super-linter/slim@v4.9.3 env: + # Until https://github.com/github/super-linter/commit/ec0662756da93f1e3aad4df049712df7d764d143 is released + # we need to set the correct plugin directory (which is incorrectly set to github/home/.tflint.d/plugins by default) + TFLINT_PLUGIN_DIR: "/root/.tflint.d/plugins" VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: main GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -61,6 +64,7 @@ jobs: JAVA_FILE_NAME: checkstyle.xml VALIDATE_BASH: true VALIDATE_BASH_EXEC: true + VALIDATE_DOCKERFILE_HADOLINT: true # https://github.com/microsoft/AzureTRE/issues/1723 tracks re-instating VALIDATE_GITHUB_ACTIONS # Note: in the meantime, the `.github/scripts/run-test.sh` script includes the `actionlint` checks) # VALIDATE_GITHUB_ACTIONS: true diff --git a/Makefile b/Makefile index 01d3b0db68..f16b3a812a 100644 --- a/Makefile +++ b/Makefile @@ -241,6 +241,7 @@ lint: -e VALIDATE_BASH=true \ -e VALIDATE_BASH_EXEC=true \ -e VALIDATE_GITHUB_ACTIONS=true \ + -e VALIDATE_DOCKERFILE_HADOLINT=true \ -v $${LOCAL_WORKSPACE_FOLDER}:/tmp/lint \ github/super-linter:slim-v4 @@ -319,6 +320,11 @@ shared_service_bundle = $(MAKE) bundle-build DIR=./templates/shared_services/$(1 && $(MAKE) bundle-publish DIR=./templates/shared_services/$(1)/ \ && $(MAKE) bundle-register DIR="./templates/shared_services/$(1)" BUNDLE_TYPE=shared_service +user_resource_bundle = $(MAKE) bundle-build DIR=./templates/workspace_services/$(1)/user_resources/$(2)/ \ + && $(MAKE) bundle-publish DIR=./templates/workspace_services/$(1)/user_resources/$(2) \ + && $(MAKE) bundle-register DIR="./templates/workspace_services/$(1)/user_resources/$(2)" BUNDLE_TYPE=user_resource WORKSPACE_SERVICE_NAME=tre-service-$(1) + + deploy-shared-service: @# NOTE: ACR_NAME below comes from the env files, so needs the double '$$'. Others are set on command execution and don't $(call target_title, "Deploying ${DIR} shared service") \ @@ -352,7 +358,8 @@ prepare-for-e2e: && $(call workspace_service_bundle,gitea) \ && $(call workspace_service_bundle,innereye) \ && $(call shared_service_bundle,sonatype-nexus) \ - && $(call shared_service_bundle,gitea) + && $(call shared_service_bundle,gitea) \ + && $(call user_resource_bundle,guacamole,guacamole-dev-vm) test-e2e-smoke: $(call target_title, "Running E2E smoke tests") && \ diff --git a/api_app/_version.py b/api_app/_version.py index be2d7c2ffe..8879c6c772 100644 --- a/api_app/_version.py +++ b/api_app/_version.py @@ -1 +1 @@ -__version__ = "0.2.27" +__version__ = "0.3.7" diff --git a/api_app/api/routes/api.py b/api_app/api/routes/api.py index a7e5cae8d2..de4a683701 100644 --- a/api_app/api/routes/api.py +++ b/api_app/api/routes/api.py @@ -95,7 +95,7 @@ async def swagger_ui_redirect(): def get_scope(workspace) -> str: # Cope with the fact that scope id can have api:// at the front. - return f"api://{workspace['properties']['scope_id'].lstrip('api://')}/user_impersonation" + return f"api://{workspace.properties['scope_id'].lstrip('api://')}/user_impersonation" @workspace_swagger_router.get("/workspaces/{workspace_id}/openapi.json", include_in_schema=False, name="openapi_definitions") diff --git a/api_app/api/routes/resource_helpers.py b/api_app/api/routes/resource_helpers.py index e776c48b91..b1fc8c5f74 100644 --- a/api_app/api/routes/resource_helpers.py +++ b/api_app/api/routes/resource_helpers.py @@ -91,11 +91,6 @@ async def send_custom_action_message(resource: Resource, resource_repo: Resource raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=strings.SERVICE_BUS_GENERAL_ERROR_MESSAGE) -def check_for_etag(etag: str): - if etag is None or len(etag) == 0: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=strings.ETAG_REQUIRED) - - def get_current_template_by_name(template_name: str, template_repo: ResourceTemplateRepository, resource_type: ResourceType, parent_service_template_name: str = "", is_update: bool = False) -> dict: try: template = template_repo.get_current_template(template_name, resource_type, parent_service_template_name) diff --git a/api_app/api/routes/shared_services.py b/api_app/api/routes/shared_services.py index 3383d78349..6c883a929c 100644 --- a/api_app/api/routes/shared_services.py +++ b/api_app/api/routes/shared_services.py @@ -14,7 +14,7 @@ from models.schemas.shared_service import SharedServiceInCreate, SharedServicesInList, SharedServiceInResponse from models.schemas.resource import ResourcePatch from resources import strings -from .workspaces import save_and_deploy_resource, check_for_etag, construct_location_header +from .workspaces import save_and_deploy_resource, construct_location_header from azure.cosmos.exceptions import CosmosAccessConditionFailedError from .resource_helpers import send_custom_action_message, send_uninstall_message, send_resource_request_message from services.authentication import get_current_admin_user, get_current_tre_user_or_tre_admin @@ -63,8 +63,7 @@ async def create_shared_service(response: Response, shared_service_input: Shared response_model=OperationInResponse, name=strings.API_UPDATE_SHARED_SERVICE, dependencies=[Depends(get_current_admin_user), Depends(get_shared_service_by_id_from_path)]) -async def patch_shared_service(shared_service_patch: ResourcePatch, response: Response, user=Depends(get_current_admin_user), shared_service_repo=Depends(get_repository(SharedServiceRepository)), shared_service=Depends(get_shared_service_by_id_from_path), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), operations_repo=Depends(get_repository(OperationRepository)), etag: str = Header(None)) -> SharedServiceInResponse: - check_for_etag(etag) +async def patch_shared_service(shared_service_patch: ResourcePatch, response: Response, user=Depends(get_current_admin_user), shared_service_repo=Depends(get_repository(SharedServiceRepository)), shared_service=Depends(get_shared_service_by_id_from_path), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), operations_repo=Depends(get_repository(OperationRepository)), etag: str = Header(...)) -> SharedServiceInResponse: try: patched_shared_service, resource_template = shared_service_repo.patch_shared_service(shared_service, shared_service_patch, etag, resource_template_repo, user) operation = await send_resource_request_message( diff --git a/api_app/api/routes/workspaces.py b/api_app/api/routes/workspaces.py index 383370e74d..627f76fe5d 100644 --- a/api_app/api/routes/workspaces.py +++ b/api_app/api/routes/workspaces.py @@ -25,7 +25,7 @@ from services.authentication import extract_auth_information from services.azure_resource_status import get_azure_resource_status from azure.cosmos.exceptions import CosmosAccessConditionFailedError -from .resource_helpers import get_user_role_assignments, save_and_deploy_resource, construct_location_header, send_uninstall_message, check_for_etag, \ +from .resource_helpers import get_user_role_assignments, save_and_deploy_resource, construct_location_header, send_uninstall_message, \ send_custom_action_message, send_resource_request_message from models.domain.request_action import RequestAction @@ -73,7 +73,7 @@ async def create_workspace(workspace_create: WorkspaceInCreate, response: Respon try: # TODO: This requires Directory.ReadAll ( Application.Read.All ) to be enabled in the Azure AD application to enable a users workspaces to be listed. This should be made optional. auth_info = extract_auth_information(workspace_create.properties) - workspace, resource_template = workspace_repo.create_workspace_item(workspace_create, auth_info) + workspace, resource_template = workspace_repo.create_workspace_item(workspace_create, auth_info, user.id) except (ValidationError, ValueError) as e: logging.error(f"Failed to create workspace model instance: {e}") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @@ -91,8 +91,7 @@ async def create_workspace(workspace_create: WorkspaceInCreate, response: Respon @workspaces_core_router.patch("/workspaces/{workspace_id}", status_code=status.HTTP_202_ACCEPTED, response_model=OperationInResponse, name=strings.API_UPDATE_WORKSPACE, dependencies=[Depends(get_current_admin_user)]) -async def patch_workspace(workspace_patch: ResourcePatch, response: Response, user=Depends(get_current_admin_user), workspace=Depends(get_workspace_by_id_from_path), workspace_repo=Depends(get_repository(WorkspaceRepository)), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), operations_repo=Depends(get_repository(OperationRepository)), etag: str = Header(None)) -> OperationInResponse: - check_for_etag(etag) +async def patch_workspace(workspace_patch: ResourcePatch, response: Response, user=Depends(get_current_admin_user), workspace=Depends(get_workspace_by_id_from_path), workspace_repo=Depends(get_repository(WorkspaceRepository)), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), operations_repo=Depends(get_repository(OperationRepository)), etag: str = Header(...)) -> OperationInResponse: try: patched_workspace, resource_template = workspace_repo.patch_workspace(workspace, workspace_patch, etag, resource_template_repo, user) operation = await send_resource_request_message( @@ -194,8 +193,7 @@ async def create_workspace_service(response: Response, workspace_service_input: @workspace_services_workspace_router.patch("/workspaces/{workspace_id}/workspace-services/{service_id}", status_code=status.HTTP_202_ACCEPTED, response_model=OperationInResponse, name=strings.API_UPDATE_WORKSPACE_SERVICE, dependencies=[Depends(get_current_workspace_owner_or_researcher_user), Depends(get_workspace_by_id_from_path)]) -async def patch_workspace_service(workspace_service_patch: ResourcePatch, response: Response, user=Depends(get_current_workspace_owner_or_researcher_user), workspace_service_repo=Depends(get_repository(WorkspaceServiceRepository)), workspace_service=Depends(get_workspace_service_by_id_from_path), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), operations_repo=Depends(get_repository(OperationRepository)), etag: str = Header(None)) -> OperationInResponse: - check_for_etag(etag) +async def patch_workspace_service(workspace_service_patch: ResourcePatch, response: Response, user=Depends(get_current_workspace_owner_or_researcher_user), workspace_service_repo=Depends(get_repository(WorkspaceServiceRepository)), workspace_service=Depends(get_workspace_service_by_id_from_path), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), operations_repo=Depends(get_repository(OperationRepository)), etag: str = Header(...)) -> OperationInResponse: try: patched_workspace_service, resource_template = workspace_service_repo.patch_workspace_service(workspace_service, workspace_service_patch, etag, resource_template_repo, user) operation = await send_resource_request_message( @@ -337,8 +335,7 @@ async def delete_user_resource(response: Response, user=Depends(get_current_work @user_resources_workspace_router.patch("/workspaces/{workspace_id}/workspace-services/{service_id}/user-resources/{resource_id}", status_code=status.HTTP_202_ACCEPTED, response_model=OperationInResponse, name=strings.API_UPDATE_USER_RESOURCE, dependencies=[Depends(get_workspace_by_id_from_path), Depends(get_workspace_service_by_id_from_path)]) -async def patch_user_resource(user_resource_patch: ResourcePatch, response: Response, user=Depends(get_current_workspace_owner_or_researcher_user), user_resource=Depends(get_user_resource_by_id_from_path), workspace_service=Depends(get_workspace_service_by_id_from_path), user_resource_repo=Depends(get_repository(UserResourceRepository)), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), operations_repo=Depends(get_repository(OperationRepository)), etag: str = Header(None)) -> OperationInResponse: - check_for_etag(etag) +async def patch_user_resource(user_resource_patch: ResourcePatch, response: Response, user=Depends(get_current_workspace_owner_or_researcher_user), user_resource=Depends(get_user_resource_by_id_from_path), workspace_service=Depends(get_workspace_service_by_id_from_path), user_resource_repo=Depends(get_repository(UserResourceRepository)), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), operations_repo=Depends(get_repository(OperationRepository)), etag: str = Header(...)) -> OperationInResponse: validate_user_is_workspace_owner_or_resource_owner(user, user_resource) try: @@ -362,10 +359,11 @@ async def patch_user_resource(user_resource_patch: ResourcePatch, response: Resp # user resource actions @user_resources_workspace_router.post("/workspaces/{workspace_id}/workspace-services/{service_id}/user-resources/{resource_id}/invoke-action", status_code=status.HTTP_202_ACCEPTED, response_model=OperationInResponse, name=strings.API_INVOKE_ACTION_ON_USER_RESOURCE) -async def invoke_action_on_user_resource(response: Response, action: str, user_resource=Depends(get_user_resource_by_id_from_path), workspace_service=Depends(get_workspace_service_by_id_from_path), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), operations_repo=Depends(get_repository(OperationRepository)), user=Depends(get_current_workspace_owner_or_researcher_user)) -> OperationInResponse: +async def invoke_action_on_user_resource(response: Response, action: str, user_resource=Depends(get_user_resource_by_id_from_path), workspace_service=Depends(get_workspace_service_by_id_from_path), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), user_resource_repo=Depends(get_repository(UserResourceRepository)), operations_repo=Depends(get_repository(OperationRepository)), user=Depends(get_current_workspace_owner_or_researcher_user)) -> OperationInResponse: validate_user_is_workspace_owner_or_resource_owner(user, user_resource) operation = await send_custom_action_message( resource=user_resource, + resource_repo=user_resource_repo, custom_action=action, resource_type=ResourceType.UserResource, operations_repo=operations_repo, diff --git a/api_app/db/repositories/workspaces.py b/api_app/db/repositories/workspaces.py index 6d3e2192b7..0b62056a05 100644 --- a/api_app/db/repositories/workspaces.py +++ b/api_app/db/repositories/workspaces.py @@ -37,6 +37,11 @@ def workspaces_query_string(): def active_workspaces_query_string(): return f'SELECT * FROM c WHERE c.resourceType = "{ResourceType.Workspace}" AND {IS_ACTIVE_CLAUSE}' + def get_workspaces(self) -> List[Workspace]: + query = WorkspaceRepository.workspaces_query_string() + workspaces = self.query(query=query) + return parse_obj_as(List[Workspace], workspaces) + def get_active_workspaces(self) -> List[Workspace]: query = WorkspaceRepository.active_workspaces_query_string() workspaces = self.query(query=query) @@ -57,19 +62,21 @@ def get_workspace_by_id(self, workspace_id: str) -> Workspace: raise EntityDoesNotExist return parse_obj_as(Workspace, workspaces[0]) - def create_workspace_item(self, workspace_input: WorkspaceInCreate, auth_info: dict) -> Tuple[Workspace, ResourceTemplate]: + def create_workspace_item(self, workspace_input: WorkspaceInCreate, auth_info: dict, workspace_owner_object_id: str) -> Tuple[Workspace, ResourceTemplate]: full_workspace_id = str(uuid.uuid4()) template = self.validate_input_against_template(workspace_input.templateName, workspace_input, ResourceType.Workspace) address_space_param = {"address_space": self.get_address_space_based_on_size(workspace_input.properties)} auto_app_registration_param = {"register_aad_application": self.automatically_create_application_registration(workspace_input.properties)} + workspace_owner_param = {"workspace_owner_object_id": self.get_workspace_owner(workspace_input.properties, workspace_owner_object_id)} # we don't want something in the input to overwrite the system parameters, # so dict.update can't work. Priorities from right to left. resource_spec_parameters = {**workspace_input.properties, **address_space_param, **auto_app_registration_param, + **workspace_owner_param, **auth_info, **self.get_workspace_spec_params(full_workspace_id)} @@ -84,8 +91,14 @@ def create_workspace_item(self, workspace_input: WorkspaceInCreate, auth_info: d return workspace, template - def automatically_create_application_registration(self, properties: dict) -> bool: - return True if properties["client_id"] == "auto_create" else False + def get_workspace_owner(self, workspace_properties: dict, workspace_owner_object_id: str) -> str: + # Add the objectId of the user that will become the workspace owner. If it is not present in + # the request, we can assume the logged in user will be WorkspaceOwner + user_defined_workspace_owner_object_id = workspace_properties.get("workspace_owner_object_id") + return workspace_owner_object_id if user_defined_workspace_owner_object_id is None else user_defined_workspace_owner_object_id + + def automatically_create_application_registration(self, workspace_properties: dict) -> bool: + return True if workspace_properties["client_id"] == "auto_create" else False def get_address_space_based_on_size(self, workspace_properties: dict): # Default the address space to 'small' if not supplied. @@ -107,11 +120,11 @@ def validate_address_space(self, address_space): if (address_space is None): raise InvalidInput("Missing 'address_space' from properties.") - allocated_networks = [x.properties["address_space"] for x in self.get_active_workspaces()] + allocated_networks = [x.properties["address_space"] for x in self.get_workspaces()] return is_network_available(allocated_networks, address_space) def get_new_address_space(self, cidr_netmask: int = 24): - networks = [x.properties["address_space"] for x in self.get_active_workspaces()] + networks = [x.properties["address_space"] for x in self.get_workspaces()] new_address_space = generate_new_cidr(networks, cidr_netmask) return new_address_space diff --git a/api_app/models/schemas/workspace.py b/api_app/models/schemas/workspace.py index 6e045b2a1f..95b2c70038 100644 --- a/api_app/models/schemas/workspace.py +++ b/api_app/models/schemas/workspace.py @@ -72,7 +72,8 @@ class Config: "properties": { "display_name": "the workspace display name", "description": "workspace description", - "client_id": "9d52b04f-89cf-47b4-868a-e12be7133b36", + "client_id": "", + "client_secret": "", "address_space_size": "small" } } diff --git a/api_app/models/schemas/workspace_template.py b/api_app/models/schemas/workspace_template.py index 051c79ef15..847e521e96 100644 --- a/api_app/models/schemas/workspace_template.py +++ b/api_app/models/schemas/workspace_template.py @@ -18,6 +18,7 @@ def get_sample_workspace_template_object(template_name: str = "tre-workspace-bas "display_name": Property(type="string"), "description": Property(type="string"), "client_id": Property(type="string"), + "client_secret": Property(type="string"), "address_space_size": Property( type="string", default="small", diff --git a/api_app/requirements-dev.txt b/api_app/requirements-dev.txt index 3751ffdabf..af970a1db8 100644 --- a/api_app/requirements-dev.txt +++ b/api_app/requirements-dev.txt @@ -1,6 +1,6 @@ # Dev requirements pytest-asyncio asgi-lifespan~=1.0.1 -httpx~=0.22.0 +httpx~=0.23.0 mock==4.0.3 pytest==7.1.1 diff --git a/api_app/requirements.txt b/api_app/requirements.txt index dc176a1ba1..d190ebdfbd 100644 --- a/api_app/requirements.txt +++ b/api_app/requirements.txt @@ -13,6 +13,6 @@ jsonschema[format_nongpl]==4.4.0 msal==1.17.0 opencensus-ext-azure==1.1.3 opencensus-ext-logging==0.1.1 -PyJWT==2.3.0 +PyJWT==2.4.0 uvicorn[standard]==0.15.0 semantic-version==2.9.0 diff --git a/api_app/resources/strings.py b/api_app/resources/strings.py index b098e99a36..92c479d132 100644 --- a/api_app/resources/strings.py +++ b/api_app/resources/strings.py @@ -112,7 +112,6 @@ SHARED_SERVICE_TEMPLATE_DOES_NOT_EXIST = "Could not retrieve the workspace service template specified" SHARED_SERVICE_TEMPLATE_VERSION_EXISTS = "A template with this version already exists" -ETAG_REQUIRED = "A valid etag must be supplied in the header of this request" ETAG_CONFLICT = "This document has been modified by another user or process since you last retrieved it. Please get the document again and retry." # Resource Status diff --git a/api_app/schemas/azuread.json b/api_app/schemas/azuread.json index 146fa69f34..7f642074db 100644 --- a/api_app/schemas/azuread.json +++ b/api_app/schemas/azuread.json @@ -12,6 +12,11 @@ "type": "string", "title": "Application (Client) ID", "description": "AAD App registration of the workspace" - } + }, + "client_secret": { + "type": "string", + "title": "Application (Client) Secret", + "description": "AAD App registration of the workspace. This value will be copied into Key Vault." + } } } diff --git a/api_app/services/aad_authentication.py b/api_app/services/aad_authentication.py index f7a6af5f7e..c57de07b20 100644 --- a/api_app/services/aad_authentication.py +++ b/api_app/services/aad_authentication.py @@ -187,7 +187,7 @@ def _get_app_sp_graph_data(self, client_id: str) -> dict: return graph_data # This method is called when you create a workspace and you already have an AAD App Registration - # to link it to. You pass in teh client_id and go and get the extra information you need from AAD + # to link it to. You pass in the client_id and go and get the extra information you need from AAD # If the client_id is `auto_create`, then these values will be written by Terraform. def _get_app_auth_info(self, client_id: str) -> dict: graph_data = self._get_app_sp_graph_data(client_id) diff --git a/api_app/tests_ma/test_api/test_routes/test_workspaces.py b/api_app/tests_ma/test_api/test_routes/test_workspaces.py index 16e9ddebb8..5933b99edc 100644 --- a/api_app/tests_ma/test_api/test_routes/test_workspaces.py +++ b/api_app/tests_ma/test_api/test_routes/test_workspaces.py @@ -358,12 +358,12 @@ async def test_post_workspaces_returns_400_if_template_does_not_exist(self, _, a # [PATCH] /workspaces/{workspace_id} @ patch("api.dependencies.workspaces.WorkspaceRepository.get_workspace_by_id") @ patch("api.routes.workspaces.WorkspaceRepository.patch_workspace", return_value=None) - async def test_patch_workspaces_400_when_etag_not_present(self, patch_workspace_mock, get_workspace_mock, app, client): + async def test_patch_workspaces_422_when_etag_not_present(self, patch_workspace_mock, get_workspace_mock, app, client): workspace_patch = {"isEnabled": True} response = await client.patch(app.url_path_for(strings.API_UPDATE_WORKSPACE, workspace_id=WORKSPACE_ID), json=workspace_patch) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.text == strings.ETAG_REQUIRED + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY + assert ("header -> etag" in response.text and "field required" in response.text) # [PATCH] /workspaces/{workspace_id} @ patch("api.dependencies.workspaces.WorkspaceRepository.get_workspace_by_id", side_effect=EntityDoesNotExist) diff --git a/api_app/tests_ma/test_db/test_repositories/test_workpaces_repository.py b/api_app/tests_ma/test_db/test_repositories/test_workpaces_repository.py index f428098f5e..0e7de5d733 100644 --- a/api_app/tests_ma/test_db/test_repositories/test_workpaces_repository.py +++ b/api_app/tests_ma/test_db/test_repositories/test_workpaces_repository.py @@ -40,6 +40,14 @@ def workspace(): return workspace +def test_get_workspaces_queries_db(workspace_repo): + workspace_repo.container.query_items = MagicMock() + expected_query = workspace_repo.workspaces_query_string() + + workspace_repo.get_workspaces() + workspace_repo.container.query_items.assert_called_once_with(query=expected_query, enable_cross_partition_query=True) + + def test_get_active_workspaces_queries_db(workspace_repo): workspace_repo.container.query_items = MagicMock() expected_query = workspace_repo.active_workspaces_query_string() @@ -81,18 +89,19 @@ def test_get_workspace_by_id_queries_db(workspace_repo, workspace): @patch('core.config.TRE_ID', "9876") def test_create_workspace_item_creates_a_workspace_with_the_right_values(validate_input_mock, new_cidr_mock, workspace_repo, basic_workspace_request, basic_resource_template): workspace_to_create = basic_workspace_request - # make sure the input doesn't include an address_space so that one will be generated + # make sure the input has 'None' for values that we expect to be set workspace_to_create.properties.pop("address_space", None) + workspace_to_create.properties.pop("workspace_owner_object_id", None) validate_input_mock.return_value = basic_resource_template new_cidr_mock.return_value = "1.2.3.4/24" - workspace, _ = workspace_repo.create_workspace_item(workspace_to_create, {}) + workspace, _ = workspace_repo.create_workspace_item(workspace_to_create, {}, "test_object_id") assert workspace.templateName == workspace_to_create.templateName assert workspace.resourceType == ResourceType.Workspace - for key in ["display_name", "description", "azure_location", "workspace_id", "tre_id", "address_space"]: + for key in ["display_name", "description", "azure_location", "workspace_id", "tre_id", "address_space", "workspace_owner_object_id"]: assert key in workspace.properties assert len(workspace.properties[key]) > 0 @@ -100,6 +109,7 @@ def test_create_workspace_item_creates_a_workspace_with_the_right_values(validat assert workspace.properties["tre_id"] != workspace_to_create.properties["tre_id"] # a new CIDR was allocated assert workspace.properties["address_space"] == "1.2.3.4/24" + assert workspace.properties["workspace_owner_object_id"] == "test_object_id" @patch('core.config.RESOURCE_LOCATION', "useast2") @@ -145,7 +155,7 @@ def test_create_workspace_item_creates_a_workspace_with_custom_address_space(val workspace_to_create.properties["address_space"] = "10.2.4.0/24" validate_input_mock.return_value = basic_resource_template - workspace, _ = workspace_repo.create_workspace_item(workspace_to_create, {}) + workspace, _ = workspace_repo.create_workspace_item(workspace_to_create, {}, "test_object_id") assert workspace.properties["address_space"] == workspace_to_create.properties["address_space"] @@ -162,7 +172,7 @@ def test_create_workspace_item_throws_exception_with_bad_custom_address_space(va validate_input_mock.return_value = basic_resource_template with pytest.raises(InvalidInput): - workspace_repo.create_workspace_item(workspace_to_create, {}) + workspace_repo.create_workspace_item(workspace_to_create, {}, "test_object_id") def test_get_address_space_based_on_size_with_custom_address_space_and_missing_address(workspace_repo, basic_workspace_request): @@ -180,7 +190,7 @@ def test_create_workspace_item_raises_value_error_if_template_is_invalid(validat validate_input_mock.side_effect = ValueError with pytest.raises(ValueError): - workspace_repo.create_workspace_item(workspace_input, {}) + workspace_repo.create_workspace_item(workspace_input, {}, "test_object_id") def test_automatically_create_application_registration_returns_true(workspace_repo): @@ -193,3 +203,17 @@ def test_automatically_create_application_registration_returns_false(workspace_r dictToTest = {"client_id": "12345"} assert workspace_repo.automatically_create_application_registration(dictToTest) is False + + +def test_workspace_owner_is_set_if_not_present_in_workspace_properties(workspace_repo): + dictToTest = {} + expected_object_id = "Expected" + + assert workspace_repo.get_workspace_owner(dictToTest, expected_object_id) is expected_object_id + + +def test_workspace_owner_is_not_overwritten_if_present_in_workspace_properties(workspace_repo): + dictToTest = {"workspace_owner_object_id": "Expected"} + not_expected_object_id = "Not Expected" + + assert workspace_repo.get_workspace_owner(dictToTest, not_expected_object_id) == "Expected" diff --git a/devops/.env.sample b/devops/.env.sample index 4acb7723bc..21a4c29649 100644 --- a/devops/.env.sample +++ b/devops/.env.sample @@ -9,6 +9,7 @@ ACR_NAME=__CHANGE_ME__ ARM_SUBSCRIPTION_ID=__CHANGE_ME__ # If you want to override the currently signed in credentials +# You would do this if running commands like `make terraform-install DIR=./templates/workspaces/base` # ARM_TENANT_ID=__CHANGE_ME__ # ARM_CLIENT_ID=__CHANGE_ME__ # ARM_CLIENT_SECRET=__CHANGE_ME__ diff --git a/devops/scripts/control_tre.sh b/devops/scripts/control_tre.sh index 71d1a210e9..5268285b12 100755 --- a/devops/scripts/control_tre.sh +++ b/devops/scripts/control_tre.sh @@ -17,6 +17,8 @@ if [[ $(az group list --output json --query "[?name=='rg-${TRE_ID}'] | length(@) fi az config set extension.use_dynamic_install=yes_without_prompt +az extension add --name azure-firewall +az --version if [[ "$1" == *"start"* ]]; then if [[ $(az network firewall list --output json --query "[?resourceGroup=='rg-${TRE_ID}'&&name=='fw-${TRE_ID}'] | length(@)") != 0 ]]; then diff --git a/devops/terraform/.terraform.lock.hcl b/devops/terraform/.terraform.lock.hcl index 003eefa9f7..d566aa988e 100644 --- a/devops/terraform/.terraform.lock.hcl +++ b/devops/terraform/.terraform.lock.hcl @@ -2,20 +2,21 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/azurerm" { - version = "2.97.0" - constraints = "2.97.0" + version = "3.5.0" + constraints = "3.5.0" hashes = [ - "h1:XxT+XM/leTXa21aTnJjPBfNBQ8cLE4gYDg01WEZsV1U=", - "zh:0aac80e6d2b8ddf33d558ac893d52688e8abf8a0b995cfc3c35eb84afbf432a3", - "zh:11191068cb732208ebc8662651782f63db329a25f7ea1cd50cd91622a2c247b7", - "zh:36c8334194e7d605682053c7c70fbb2a650d9b0a7bcc44d5cdda4f205818438a", - "zh:3a5e01276added995e875b42ecc6b36ff73d267f0c096c87195bd2b1fff4f5b2", - "zh:557e38371657e6ed8aae9192d01480c4cca7c0f7ade6022f1aec247a6384922b", - "zh:67b913c280c5858549477a4b05e77078b1a5234de77c7bddd4ee1e8e237d5665", - "zh:7aeca864ce45b295db734cd968f7596ff12cd7c522ee89d53f432dae7c2b5d18", - "zh:b6127d7a796eaf9756dd212667eb48f79c0e78729589ec8ccf68e0b36ebb4e54", - "zh:bed448238740f897d1b399e5123b3a9eba256b981846f9ee92b71493446ca684", - "zh:c351a1bba34c3bd06fff75e4c15e4db0456268479463c2471598068ea1c5c884", - "zh:d073c24d0a4756e79b39f41f552d526800f9fb0ad0a74f742ac8de61b6416a3a", + "h1:T4XsCHDT839VehWKdxbVsLn0ECjcQaUTzbSGW055pgM=", + "zh:0d8ae6d6e87f44ed4a178be03d6466339b0bb578ab54c2677e365a8281b0bb7d", + "zh:29d250d1a18d49652b28f234ecd17687b36c875dc47877a678e587d5d136b054", + "zh:2e69ba373cf009e8a60b36d04f3dbc4638708d1bf88be9f96b3e52cbf8f47f31", + "zh:53d525dd84ac63b5f749bfbc6b70a202dacf29597664d2ab1165efea6f24f630", + "zh:a25024d574ccd5ae6c2962f3bb71d510f62899f493b1ed096f2f7f0e2b18f975", + "zh:aabc64fe64319b95aaba1d1866f87abc7b10adae37d2eafa2f85f37317fdd49f", + "zh:acc6a977814897cb23d3b3753213281334238f8bce6d2b21e9f04fc4087ee980", + "zh:b24987e9416c39cd59c0fa41c139a97406b9955f0607fcafbf3315014456338a", + "zh:c550eae45fd32acdbe32b4e5c450ae95df6cb18903ac7216b1b07b23a16ce045", + "zh:c8f83b763b643893dcb6933a6bcee824cb514e06e7e5c5f5ac4ba187e66d7e22", + "zh:dcdac07e7ea18464dea729717870c275de9453775243c231e1fb305cad0ee597", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } diff --git a/devops/terraform/main.tf b/devops/terraform/main.tf index c1f22ad7d9..9a0145c027 100644 --- a/devops/terraform/main.tf +++ b/devops/terraform/main.tf @@ -14,13 +14,13 @@ resource "azurerm_resource_group" "mgmt" { # Holds Terraform shared state (already exists, created by bootstrap.sh) resource "azurerm_storage_account" "state_storage" { - name = var.mgmt_storage_account_name - resource_group_name = azurerm_resource_group.mgmt.name - location = azurerm_resource_group.mgmt.location - account_tier = "Standard" - account_kind = "StorageV2" - account_replication_type = "LRS" - allow_blob_public_access = false + name = var.mgmt_storage_account_name + resource_group_name = azurerm_resource_group.mgmt.name + location = azurerm_resource_group.mgmt.location + account_tier = "Standard" + account_kind = "StorageV2" + account_replication_type = "LRS" + allow_nested_items_to_be_public = false lifecycle { ignore_changes = [tags] } } diff --git a/devops/terraform/terraform.tf b/devops/terraform/terraform.tf index d7b45396e5..18da1c89d9 100644 --- a/devops/terraform/terraform.tf +++ b/devops/terraform/terraform.tf @@ -4,7 +4,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "2.97.0" + version = "3.5.0" } } } diff --git a/docs/assets/rp_kv_access_policy.png b/docs/assets/rp_kv_access_policy.png new file mode 100644 index 0000000000..2c5a079b0c Binary files /dev/null and b/docs/assets/rp_kv_access_policy.png differ diff --git a/docs/assets/rp_local_debugging_vscode_screenshot.png b/docs/assets/rp_local_debugging_vscode_screenshot.png new file mode 100644 index 0000000000..3cf61d1d49 Binary files /dev/null and b/docs/assets/rp_local_debugging_vscode_screenshot.png differ diff --git a/docs/assets/rp_stop_vm_scale_set.png b/docs/assets/rp_stop_vm_scale_set.png new file mode 100644 index 0000000000..b9d337dd6c Binary files /dev/null and b/docs/assets/rp_stop_vm_scale_set.png differ diff --git a/docs/tre-admins/setup-instructions/configuring-shared-services.md b/docs/tre-admins/setup-instructions/configuring-shared-services.md index a3813263ec..19b2973e39 100644 --- a/docs/tre-admins/setup-instructions/configuring-shared-services.md +++ b/docs/tre-admins/setup-instructions/configuring-shared-services.md @@ -1,15 +1,109 @@ # Configuring Shared Services -Complete the configuration of the shared services (Nexus and Gitea) from inside of the TRE environment. +## Deploy/configure Nexus -Make sure you run the following command using git bash and set your current directory as C:/AzureTRE +If you're deploying a brand new environment you should deploy the VM-based (V2) service (read section `A`). If you wish to migrate from an existing App Service Nexus service (V1) to the VM-based service, first deploy the new service (section `A`) then proceed to section `B`. -## Configure Nexus repository +!!! info + The Makefile commands for deploying shared services temporarily target the V1 service so that existing environments won't have a new V2 Nexus service deployed automatically by CICD and introduce breaking changes. The V2 Nexus service will need to be deployed manually using the steps below. -1. Run the Nexus configuration script to reset the password and setup a PyPI proxy on Nexus: -```./templates/shared_services/sonatype-nexus/scripts/configure_nexus.sh -t ``` +### A. Deploy & configure V2 Nexus service (hosted on VM) -## Configure Gitea repository +!!! caution + Before deploying the V2 Nexus service, you will need workspaces of version `0.3.2` or above due to a dependency on a DNS zone link for the workspace(s) to connect to the Nexus VM. + +Before deploying the Nexus shared service, you need to make sure that it will have access to a certificate to configure serving secure proxies. By default, the Nexus service will serve proxies from `https://nexus-{TRE_ID}.{LOCATION}.cloudapp.azure.com/`, and thus it requires a certificate that validates ownership of this domain to use for SSL. + +You can use the Certs Shared Service to set one up by following these steps: + +1. Run the below commands in your terminal to build, publish and register the certs bundle: + + ```cmd + make bundle-build DIR=./templates/shared_services/certs + make bundle-publish DIR=./templates/shared_services/certs + make bundle-register DIR=./templates/shared_services/certs BUNDLE_TYPE=shared_service + ``` + +2. Navigate to the Swagger UI for your TRE API at `https:///api/docs`, and authenticate if you haven't already by clicking `Authorize`. + +3. Click `Try it out` on the `POST` `/api/shared-services` operation, and paste the following to deploy the certs service: + + ```json + { + "templateName": "tre-shared-service-certs", + "properties": { + "display_name": "Nexus cert", + "description": "Generate/renew ssl cert for Nexus shared service", + "domain_prefix": "nexus", + "cert_name": "nexus-ssl" + } + } + ``` + +!!! caution + If you have KeyVault Purge Protection enabled and are re-deploying your environment using the same `cert_name`, you may encounter this: `Status=409 Code=\"Conflict\" Message=\"Certificate nexus-ssl is currently in a deleted but recoverable state`. You need to either manually recover the certificate or purge it before redeploying. + +1. Once the shared service has been deployed (which you can check by querying the `/api/shared-services/operations` method), copy its `resource_id`, then find the `POST` operation for `/api/shared-services/{shared_service_id}/invoke_action`, click `Try it out` and paste in the resource id into the `shared_service_id` field, and enter `generate` into the `action` field, then click `Execute`. + +This will invoke the certs service to use Letsencrypt to generate a certificate for the specified domain prefix followed by `-{TRE_ID}.{LOCATION}.cloudapp.azure.com`, so in our case, having entered `nexus`, this will be `nexus-{TRE_ID}.{LOCATION}.cloudapp.azure.com`, which will be the public domain for our Nexus service. + +Once this has completed, you can verify its success either from the operation output, or by navigating to your core keyvault (`kv-{TRE_ID}`) and looking for a certificate called `nexus-ssl` (or whatever you called it). + +After verifying the certificate has been generated, you can deploy Nexus: + +1. Run the below commands in your terminal to build, publish and register the Nexus shared service bundle: + + ```cmd + make bundle-build DIR=./templates/shared_services/sonatype-nexus-vm + make bundle-publish DIR=./templates/shared_services/sonatype-nexus-vm + make bundle-register DIR=./templates/shared_services/sonatype-nexus-vm BUNDLE_TYPE=shared_service + ``` + +1. Navigate to the Swagger UI for your TRE API at `https:///api/docs`, and authenticate if you haven't already by clicking `Authorize`. + +1. Click `Try it out` on the `POST` `/api/shared-services` operation, and paste the following to deploy the Nexus shared service: + + ```json + { + "templateName": "tre-shared-service-sonatype-nexus", + "properties": { + "display_name": "Nexus", + "description": "Proxy public repositories with Nexus", + "ssl_cert_name": "nexus-ssl" + } + } + ``` + +!!! tip + If you called your cert something different in the certs shared service step, make sure that is reflected above. + +This will deploy the infrastructure required for Nexus, then start the service and configure it with the repository configurations located in the `./templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config` folder. It will also set up HTTPS using the certificate you generated in the previous section, so proxies can be served at `https://nexus-{TRE_ID}.{LOCATION}.cloudapp.azure.com`. + +You can optionally go to the Nexus web interface by visiting `https://nexus-{TRE_ID}.{LOCATION}.cloudapp.azure.com/` in the jumpbox and signing in with the username `admin` and the password secret located in your core keyvault, with the key `nexus-admin-password`. Here you should be able to see all of the configured repositories and you can use the UI to manage settings etc. + +Just bear in mind that if this service is redeployed any changes in the UI won't be persisted. If you wish to add new repositories or alter existing ones, use the JSON files within the `./nexus_repos_config` directory. + +### B. Migrate from an existing V1 Nexus service (hosted on App Service) + +Once you've created the new V2 (VM-based) Nexus service by following section `A`, you can migrate from the V1 Nexus service by following these steps: + +1. Identify any existing Guacamole user resources that are using the old proxy URL (`https://nexus-{TRE_ID}.azurewebsites.net/`). These will be any VMs with bundle versions < `0.3.2`. + +1. These will need to be either **re-deployed** with the new template versions `0.3.2` or later and specifying an additional template parameter `"nexus_version"` with the value of `"V2"`, or manually have their proxy URLs updated by remoting into the VMs and updating the various configuration files of required package managers with the new URL (`https://nexus-{TRE_ID}.{LOCATION}.cloudapp.azure.com/`). + + 1. For example, pip will need the `index`, `index-url` and `trusted-host` values in the global `pip.conf` file to be modified to use the new URL. + +2. Once you've confirmed there are no dependencies on the old Nexus shared service, you can delete it using the API. + +### Upgrade notes + +The new V2 Nexus shared service can be located in the `./templates/shared_services/sonatype-nexus-vm` directory, with the bundle name `tre-shared-service-sonatype-nexus`, which is now hosted using a VM to enable additional configuration required for proxying certain repositories. + +This has been created as a separate service as the domain name exposed for proxies will be different to the one used by the original Nexus service and thus will break any user resources configured with the old proxy URL. + +The original Nexus service that runs on App Service (located in `./templates/shared_services/sonatype-nexus`) has the bundle name `tre-shared-service-nexus` so can co-exist with the new VM-based shared service to enable smoother upgrading of existing resources. + +## Configure Gitea repositories Note : This is a Gitea *shared service* which will be accessible from all workspaces intended for mirroring external Git repositories. A Gitea *workspace service* can also be deployed per workspace to enable Gitea to be used within a specific workspace. @@ -17,6 +111,7 @@ By default, this Gitea instance does not have any repositories configured. You c ### Command Line +Make sure you run the following commands using git bash and set your current directory as C:/AzureTRE. 1. On the jumbox, run: ```./scripts/gitea_migrate_repo.sh -t -g ``` diff --git a/docs/tre-admins/setup-instructions/installing-base-workspace.md b/docs/tre-admins/setup-instructions/installing-base-workspace.md index 82f74b1001..3a03433347 100644 --- a/docs/tre-admins/setup-instructions/installing-base-workspace.md +++ b/docs/tre-admins/setup-instructions/installing-base-workspace.md @@ -56,7 +56,8 @@ Go to `https:///api/docs` and use POST `/api/workspaces` with th "properties": { "display_name": "manual-from-swagger", "description": "workspace for team X", - "client_id": "WORKSPACE_API_CLIENT_ID", + "client_id":"", + "client_secret":"", "address_space_size": "medium" } } @@ -70,4 +71,5 @@ Workspace level operations can now be carried out using the workspace API, at `/ ## Next steps -* [Installing a workspace service](./installing-workspace-service-and-user-resource.md) +* [Configuring shared services](./configuring-shared-services.md) +* [Installing a workspace service & user resources](./installing-workspace-service-and-user-resource.md) diff --git a/docs/tre-admins/setup-instructions/installing-workspace-service-and-user-resource.md b/docs/tre-admins/setup-instructions/installing-workspace-service-and-user-resource.md index ab390b7ebc..cb54cfd9ab 100644 --- a/docs/tre-admins/setup-instructions/installing-workspace-service-and-user-resource.md +++ b/docs/tre-admins/setup-instructions/installing-workspace-service-and-user-resource.md @@ -66,19 +66,18 @@ Now that we have published and registered both workspace service and user resour 4. Enter the workspace_id in the `workspace_id` field. -5. Paste the following payload json into the `Request body` field, update `` with the client ID of the app registration for the base workspace you created previously, then click `Execute`. Review the server response. +5. Paste the following payload json into the `Request body` field. Update `` with the Workspace AAD Application URI for the base workspace you created previously, this will be in the format `api://` or `api://_ws_`. Then click `Execute`. Review the server response. ```json { - "templateName":"tre-service-guacamole", + "templateName": "tre-service-guacamole", "properties": { - "display_name":"Virtual Desktop", - "description":"Create virtual desktops for running research workloads", - "ws_client_id":"", - "ws_client_secret":"", - "is_exposed_externally":true, - "guac_disable_copy":true, - "guac_disable_paste":true + "display_name": "Virtual Desktop", + "description": "Create virtual desktops for running research workloads", + "workspace_identifier_uri": "", + "is_exposed_externally": true, + "guac_disable_copy": true, + "guac_disable_paste": true } } ``` @@ -90,12 +89,15 @@ You can also follow the progress in Azure portal as various resources come up. !!! info There is currently a bug where the redirect URI isn't automatically set up correctly in the Workspace API app registration. Until this is fixed, you need to head to the app registration in the Azure portal, click on **Add a redirect URI** > **Add a platform** > **Web** > then paste in the Guacamole URI in the redirect URI box. - You can find this in the Guacamole app service properties and append `/guacamole/` to the end - it should look like this: `https://guacamole-{TRE_ID}-ws-XXXX-svc-XXXX.azurewebsites.net/guacamole/`). Finally, make sure you check the **ID tokens** checkbox and click **Configure**. + You can find this in the Guacamole app service properties and append `/oauth2/callback` to the end - it should look like this: `https://guacamole-{TRE_ID}-ws-XXXX-svc-XXXX.azurewebsites.net/oauth2/callback/`). Finally, make sure you check the **ID tokens** checkbox and click **Configure**. ## Creating a user resource Once the workspace service has been created, we can use the workspace API to create a user resource in our workspace. +!!! caution + Before deploying Guacamole user resources, you will want to make sure you have a Nexus shared service deployed in the workspace so that your VMs can access package repositories through a proxy (as they can't access public repositories directly). See [Configuring shared services](./configuring-shared-services.md). + 1. Navigate to the Swagger UI at `https:///api/workspaces//docs` . Where `` is the workspace ID of your workspace. 1. Click `Try it out` on the `POST` `/api/workspaces//workspace-services//user_resources` operation. Where `` and `` are the workspace ID of your workspace and workspace service ID of your workspace service. @@ -110,12 +112,13 @@ Once the workspace service has been created, we can use the workspace API to cre "properties": { "display_name": "My VM", "description": "Will be using this VM for my research", - "os_image": "Server 2019 Data Science VM" + "os_image": "Server 2019 Data Science VM", + "nexus_version": "V2" } } ``` - > Note: You can also specify "Windows 10" for a standard Windows 10 image + > Note: You can also specify "Windows 10" in "os_image" for a standard Windows 10 image. The "nexus_version" property also accepts "V1" if you have a V1 Nexus shared service deployed instead of the V2 service described in [Configuring shared services](./configuring-shared-services.md). The API will return an `operation` object with a `Location` header to query the operation status, as well as the `resourceId` and `resourcePath` properties to query the resource under creation. diff --git a/docs/tre-developers/api.md b/docs/tre-developers/api.md index 797d183dd5..8ceb2fb883 100644 --- a/docs/tre-developers/api.md +++ b/docs/tre-developers/api.md @@ -67,17 +67,7 @@ Now, you should be able to open Swagger UI for your local instance on [http://lo On Azure Portal, find an App Service instance named `app-${TRE_ID}`. -### API logs using deployment center - -Check that the version you are debugging/troubleshooting is the same one deployed on the App Service. - -You can check this in Deployment Center, or follow the logs as generated by the container in the logs tabs. - -![Deployment Center](../assets/api_deployment_center.png) - -### API logs using in App Insights - -You can also access logs in App Insights or LogAnalytics. +### API logs in LogAnalytics To find logs in LogAnalytics, go to your resource group, then to LogAnalytics instance, which is named like `log-${TRE_ID}`. @@ -89,6 +79,14 @@ AppTraces | order by TimeGenerated desc ``` +### API logs using deployment center + +Check that the version you are debugging/troubleshooting is the same one deployed on the App Service. + +You can check this in Deployment Center, or follow the logs as generated by the container in the logs tabs. + +![Deployment Center](../assets/api_deployment_center.png) + ### Deploying a cloud instance To deploy a new version of the API to your TRE deployment, do this: diff --git a/docs/tre-developers/letsencrypt.md b/docs/tre-developers/letsencrypt.md new file mode 100644 index 0000000000..1949202aca --- /dev/null +++ b/docs/tre-developers/letsencrypt.md @@ -0,0 +1,52 @@ +# Letsencrypt + +Certain components of the TRE require the aquisition of a certificate via Letsencrypt to ensure secure HTTPS connections. + +In order to aquire these certificates, there must be a public facing endpoint which can be reached by Letsencrypt. + +As TREs are secured environments with very few publicly facing points, additional resources are required to ensure the certificate can be provisioned for the correct domain. + +The additional resources are as followed: + +1. Public IP provisioned in the same location as the web app that the certificate is intended for; this will also have a domain label which matches the web app name. +1. Storage Account with a static web app. +1. Application gateway to route traffic from the Public IP to the static web app + +The following diagram illustrated the flow of data between the resources: + +```mermaid +flowchart RL + subgraph .dev Container + direction TB + A(letsencrypt process runs) + end + subgraph External + direction TB + B[letsencrypt authority] + end + subgraph TRE + subgraph Core VNet + C[Public IP
Domain Label: < web-app-name >
Endpoint: < web-app-name >.< location >.cloudapp.net] + subgraph Storage Account + D[SA Static Site] + end + end + subgraph VNet + E[Key Vault
kv-< tre_id >] + subgraph VM + F[Web App] + end + G[Private DNS Zone < web-app-name >.< location >.cloudapp.net] + end + end + + A --> |1. Request to | B + B --> |2. Attempts to hit | C + C --> |3. App Gateway routes | D + D --> |4. Responds | C + C --> |5. Responds | B + B --> |6. Acquires certificate | A + A --> |7. Stores Certificate | E + F --> |8. Pulls Certificate | E + + ``` diff --git a/docs/tre-developers/resource-processor.md b/docs/tre-developers/resource-processor.md index 271d62b84a..7e571b2e67 100644 --- a/docs/tre-developers/resource-processor.md +++ b/docs/tre-developers/resource-processor.md @@ -2,76 +2,137 @@ Resource Processor is the Azure TRE component automating [Porter](https://porter.sh) bundle deployments. It hosts Porter and its dependencies. -## Build and run the container +This page is a guide for a developer looking to make a change to the Resource Processor and debug it. -1. Navigate to `resource_processor/` folder and run `docker build` command: +## Overview - ```cmd - docker build -t resource-processor-vm-porter -f ./vmss_porter/Dockerfile . - ``` +The logic in Resource Processor is written in Python. The Resource Processor implementation is located in [`resource_processor` folder](https://github.com/microsoft/AzureTRE/blob/main/resource_processor/) of the repository. -1. Run the image: +Read [how a workspace is provisioned using Porter](./../azure-tre-overview/architecture.md#provisioning-a-workspace) - ```cmd - docker run -it -v /var/run/docker.sock:/var/run/docker.sock --env-file .env resource-processor-vm-porter - ``` +## Local debugging -## Local development +To set up local debugging, first run, if you haven't done so already (make sure `ENABLE_LOCAL_DEBUGGING` is set to `true` in your `.env` file): -To work locally in Visual Studio Code within the Dev Container you can use the `Resource Processor` debug profile to run the app. +```cmd +az login +make setup-local-debugging +``` + +This will allowlist your local IP against Azure resources and create a Service Principal for the Resource Processor. + +Next, disable the existing Resource Processor from running in your deployment. The easiest way to do this is to stop the VM scale set: + +[![Stop RP VM scale set](../assets/rp_stop_vm_scale_set.png)](../assets/rp_stop_vm_scale_set.png) + +Now, go to "Run and Debug" panel in VSCode, and select Resource Processor. + +[![VSCode RP debugging screenshot](../assets/rp_local_debugging_vscode_screenshot.png)](../assets/rp_local_debugging_vscode_screenshot.png) + +!!! info + If you get a credential error when trying to connect to Service Bus, make sure you've authenticated in the AZ CLI first as it uses your local credentials. + +!!! info + If you get an error similar to `Environment variable 'ARM_CLIENT_ID' is not set correctly`, make sure you have ran `make setup-local-debugging` -Before using this, you'll need to run `make setup-local-debugging` to whitelist your local IP against your resources and create a Service Principal for the Resource Processor to access and create Service Bus messages and deploy resources. +You can use an API instance deployed in your environment to create deployment requests, and debug your locally running Resource Processor. -Once the above is set up you can simulate receiving messages from Service Bus by going to Service Bus explorer on the portal and using a message payload for `SERVICE_BUS_RESOURCE_REQUEST_QUEUE` as follows: +For more information on how to use API, refer to [API documentation](./api.md#using-swagger-ui). -```json -{"action": "install", "id": "a8911125-50b4-491b-9e7c-ed8ff42220f9", "name": "tre-workspace-base", "version": "0.1.0", "parameters": {"azure_location": "westeurope", "workspace_id": "20f9", "tre_id": "myfavtre", "address_space": "192.168.3.0/24"}} +## Cloud instance + +On Azure Portal, find an Virtual VM scale set with a name `vmss-rp-porter-${TRE_ID}`. + +### Resource Processor logs in LogAnalytics + +To find logs in LogAnalytics, go to your resource group, then to LogAnalytics instance, which is named like `log-${TRE_ID}`. + +There, you can run a query like + +```cmd +AppTraces +| where AppRoleName == "runner.py" +| order by TimeGenerated desc ``` -This will trigger receiving of messages, and you can freely debug the code by setting breakpoints as desired. +### SSH-ing to the instance + +The processor runs in a VNET, and you cannot connect to it directly. +To SSH to this instance, use Bastion. + +1. Find a keyvault with a name `kv-${TRE_ID}` in your resource group. +1. Copy a secret named `resource-processor-vmss-password` + + ![VMSS Password](../assets/vmss_password.png) -> If you get a credential error when trying to connect to Service Bus, make sure you've authenticated in the AZ CLI first as it uses your local credentials. + If you don't have permissions to see the secret, add yourself to the Access Policy of this keyvault with a permission to read secrets: -## Porter Azure plugin + [![Keyvault access policy](../assets/rp_kv_access_policy.png)](../assets/rp_kv_access_policy.png) +1. Connect to the instance using Bastion. Use the username `adminuser` and the password you just copied. -Resource Processor uses [Porter Azure plugin](https://github.com/getporter/azure-plugins) to store Porter data in TRE management storage account. The storage container, named `porter`, is created during the bootstrapping phase of TRE deployment. The `/resource_processor/run.sh` script generates a `config.toml` file in Porter home folder to enable the Azure plugin when the image is started. + ![Bastion](../assets/bastion.png "Bastion") -## Debugging the deployed processor on Azure +### Getting container logs -See the [debugging and troubleshooting guide](../tre-admins/troubleshooting-guide.md). +1. SSH into the Resource Processor VM as described above +1. Check the status of the container using `sudo docker ps` -## Network requirements + If you see nothing (and the container was pulled) then the processor has either not started yet or it has crashed. -The Resource Processor needs to access the following resources outside the Azure TRE VNET via explicit allowed [Service Tags](https://docs.microsoft.com/en-us/azure/virtual-network/service-tags-overview) or URLs. +1. Get the logs from the container using `sudo docker logs ` command. -| Service Tag | Justification | -| --- | --- | -| AzureActiveDirectory | Authenticate with the User Assigned identity to access Azure Resource Manager and Azure Service Bus. | -| AzureResourceManager | Access the Azure control plane to deploy and manage Azure resources. | -| AzureContainerRegistry | Pull the Resource Processor container image, as it is located in Azure Container Registry. | -| Storage | The Porter bundles stores state between executions in an Azure Storage Account. | -| AzureKeyVault | The Porter bundles might need to create an Azure Key Vault inside of the Workspace. To verify the creation, before a private link connection is created, Terraform needs to reach Key Vault over public network | +### Starting container manually -To install Docker, Porter and related packages ([script](/templates/core/terraform/resource_processor/vmss_porter/cloud-config.yaml)) on the Resource Processor, the VM must have access to download from the following URLs: +1. Find the **runner_image:tag** by running ``docker ps`` +1. Execute the following command from the root (/) of the file system -* packages.microsoft.com -* keyserver.ubuntu.com -* api.snapcraft.io -* azure.archive.ubuntu.com -* security.ubuntu.com -* entropy.ubuntu.com -* download.docker.com -* registry-1.docker.io -* auth.docker.io -* registry.terraform.io -* releases.hashicorp.com + ```cmd + sudo docker run -v /var/run/docker.sock:/var/run/docker.sock --env-file .env --name resource_processor_vmss_porter_debug [runner_image:tag] + ``` -## Challenges +!!! info + If you start a container manually you will probably want to install software, for example, an editor. However, the firewall blocks all ingress traffic, so you cannot run `sudo apt update`. You need to add an override rule in the firewall to allow the traffic. -The notable challenges that needed to be solved included Porter automation, namely hosting environment, managing workspace (deployment) states and concurrency. +!!! caution + Remember to remove this rule when debugging is done. + +## Troubleshooting + +### No container logs on the instance + +1. If you don't see container logs, you should check the status of **cloud-init** which is used to bootstrap the machine with docker and start the processor. Log files for cloud init are: + + - `/var/log/cloud-init.log` + - `/var/log/cloud-init-output.log` + + If the Docker container is pulled as shown in logs then the resource processor should start. +1. Check the status of all Docker processes using `docker ps -a` which should show you if the container terminated prematurely. + +## Implementation details + +### Porter + +Azure TRE needed a solution for implementing and deploying workspaces and workspace services with the following properties: + +* Means for packaging and versioning workspaces and workspace services +* Providing unified structure for deployment definitions (scripts, pipelines) so that the process can be easily automated +* Solid developer experience - easy to use and learn + +Porter meets all these requirements well. Porter packages cloud application into a versioned, self-contained Docker container called a Porter bundle. -Hosting the Porter runner in a container is an expected design idea and appealing due to its cost effectiveness among other things. However, that would create a nested Docker environment, "Docker in Docker". Although this is possible using [Azure CNAB Driver](https://github.com/deislabs/cnab-azure-driver), the solution is less reliable and troubleshooting becomes difficult; due to the environment's ephemeral nature, there is not much in addition to the [Application Insights](https://docs.microsoft.com/azure/azure-monitor/app/app-insights-overview) logs the developer can rely on. In contrast, the developer can always log in to the VM and see what's going on and run tests manually to reproduce bugs. +CNAB spec defines actions that Porter [implements](https://porter.sh/author-bundles/#bundle-actions): **install, upgrade and uninstall**. The developer has practically complete freedom on how to implement logic for these actions. The deployment pipeline definition is created in YAML. The YAML file is called [Porter manifest](https://porter.sh/author-bundles/) and in additon to the actions, it contains the name, version, description of the bundle and defines the input parameters, possible credentials and output. -Porter can keep tap on the installations, but Azure TRE needs a state record that is more tangible. It is instead the responsibility of the API to maintain the state of deployments in configuration store. The state is updated when a user deploys, modifies or deletes workspaces and based on the deployment status messages sent by Resource Processor. All possible states of a workspace or a workspace service are defined by the API in [`resource.py` file](https://github.com/microsoft/AzureTRE/blob/main/api_app/models/domain/resource.py). +Furthermore, Porter provides a set of [mixins](https://porter.sh/mixins/) - analogous to the concrete actions in GitHub workflows and tasks in Azure DevOps pipelines - which simplify and reduce the development cost when implementing deployment logic. For example, Terraform mixin installs the required tools and provides a clean step in the pipeline to execute Terraform deployments. [Exec mixin](https://porter.sh/mixins/exec/) allows running any command or script; especially useful, if no suitable mixin for a specific technology is available. Implementing custom mixins is possible too. + +### Porter Azure plugin + +Resource Processor uses [Porter Azure plugin](https://github.com/getporter/azure-plugins) to store Porter data in TRE management storage account. The storage table, named `porter`, is created during the bootstrapping phase of TRE deployment. The `/resource_processor/run.sh` script generates a `config.toml` file in Porter home folder to enable the Azure plugin when the image is started. + +### Porter bundle inputs + +When Porter runs bundle actions, it passes input parameters. Full set of inputs that Porter passes can be found in [config.py](../../resource_processor/shared/config.py). + +!!! info + Note that Resource Processor does not pass any location-related attributes when running bundle actions. Instead, a `location` attribute is passed from the API. This is so that different TRE resources could be potentially deployed to different regions. diff --git a/docs/tre-templates/workspace-services/guacamole.md b/docs/tre-templates/workspace-services/guacamole.md index 534969a962..c04008d852 100644 --- a/docs/tre-templates/workspace-services/guacamole.md +++ b/docs/tre-templates/workspace-services/guacamole.md @@ -18,13 +18,6 @@ Service Tags: When deploying a Guacamole service into a workspace the following properties need to be configured. -### Required Properties - -| Property | Options | Description | -| -------- | ------- | ----------- | -| `ws_client_id` | Valid client ID of the Workspace App Registration. | The OpenID client ID which should be submitted to the OpenID service when necessary. This value is typically provided to you by the OpenID service when OpenID credentials are generated for your application. | -| `ws_client_secret` | Valid client secret. | - ### Optional Properties | Property | Options | Description | diff --git a/docs/tre-templates/workspaces/base.md b/docs/tre-templates/workspaces/base.md index 7e1872e271..fadea7a60b 100644 --- a/docs/tre-templates/workspaces/base.md +++ b/docs/tre-templates/workspaces/base.md @@ -11,6 +11,17 @@ The base workspace template contains the following resources: - Network Security Group - App Service Plan +## Workspace Configuration + +When deploying a workspace the following properties need to be configured. + +### Required Properties + +| Property | Options | Description | +| -------- | ------- | ----------- | +| `client_id` | Valid client ID of the Workspace App Registration. | The OpenID client ID which should be submitted to the OpenID service when necessary. This value is typically provided to you by the OpenID service when OpenID credentials are generated for your application. | +| `client_secret` | Valid client secret. | + ## Azure Trusted Services *Azure Trusted Services* are allowed to connect to both the key vault and storage account provsioned within the workspace. If this is undesirable additonal resources without this setting configured can be deployed. diff --git a/docs/tre-workspace-authors/authoring-workspace-templates.md b/docs/tre-workspace-authors/authoring-workspace-templates.md index 71f5a5ffa8..e4127012a1 100644 --- a/docs/tre-workspace-authors/authoring-workspace-templates.md +++ b/docs/tre-workspace-authors/authoring-workspace-templates.md @@ -11,21 +11,7 @@ This document describes the requirements, and the process to author a template. To create a bundle from scratch follow the Porter [Quickstart Guide](https://porter.sh/quickstart/) ([`porter create` CLI command](https://porter.sh/cli/porter_create/) will generate a new bundle in the current directory). -## Background - -Azure TRE needed a solution for implementing and deploying workspaces and workspace services with the following properties: - -* Means for packaging and versioning workspaces and workspace services -* Providing unified structure for deployment definitions (scripts, pipelines) so that the process can be easily automated -* Solid developer experience - easy to use and learn - -Porter meets all these requirements well. Porter packages cloud application into a versioned, self-contained Docker container called a Porter bundle. - - -CNAB spec defines actions that Porter [implements](https://porter.sh/author-bundles/#bundle-actions): **install, upgrade and uninstall**. The developer has practically complete freedom on how to implement logic for these actions. The deployment pipeline definition is created in YAML - something that is very familiar to anyone that have experience in creating continuous integration/deployment (CI/CD) pipelines with [GitHub Actions](https://github.com/features/actions) (workflows) or in [Azure DevOps](https://azure.microsoft.com/services/devops/pipelines/). The YAML file is called [Porter manifest](https://porter.sh/author-bundles/) and in additon to the actions, it contains the name, version, description of the bundle and defines the input parameters, possible credentials and output. - -Furthermore, Porter provides a set of [mixins](https://porter.sh/mixins/) - analogous to the concrete actions in GitHub workflows and tasks in Azure DevOps pipelines - which simplify and reduce the development cost when implementing deployment logic. For example, Terraform mixin installs the required tools and provides a clean step in the pipeline to execute Terraform deployments. [Exec mixin](https://porter.sh/mixins/exec/) allows running any command or script; especially useful, if no suitable mixin for a specific technology is available. Implementing custom mixins is possible too. - +Read more about Porter in [Resource Processor doc](../tre-developers/resource-processor.md#porter). ## Prerequisites diff --git a/e2e_tests/helpers.py b/e2e_tests/helpers.py index d042a0b2b5..bb0a6045ad 100644 --- a/e2e_tests/helpers.py +++ b/e2e_tests/helpers.py @@ -1,5 +1,5 @@ import asyncio -from typing import Optional +from typing import Optional, Tuple from contextlib import asynccontextmanager from httpx import AsyncClient, Timeout import logging @@ -220,7 +220,7 @@ async def get_identifier_uri(client, workspace_id: str, auth_headers) -> str: return f"api://{workspace['properties']['scope_id'].lstrip('api://')}" -async def get_workspace_owner_token(admin_token, workspace_id, verify) -> Optional[str]: +async def get_workspace_auth_details(admin_token, workspace_id, verify) -> Tuple[str, str]: async with AsyncClient(verify=verify) as client: auth_headers = get_auth_header(admin_token) scope_uri = await get_identifier_uri(client, workspace_id, auth_headers) @@ -241,9 +241,7 @@ async def get_workspace_owner_token(admin_token, workspace_id, verify) -> Option except JSONDecodeError: raise Exception("Failed to parse response as JSON: {}".format(response.content)) - if "access_token" not in responseJson: + if "access_token" not in responseJson or response.status_code != status.HTTP_200_OK: raise Exception("Failed to get access_token: {}".format(response.content)) - token = responseJson["access_token"] - - return token if (response.status_code == status.HTTP_200_OK) else None + return responseJson["access_token"], scope_uri diff --git a/e2e_tests/requirements.txt b/e2e_tests/requirements.txt index e861d17adc..c1bdacfb8b 100644 --- a/e2e_tests/requirements.txt +++ b/e2e_tests/requirements.txt @@ -1,5 +1,5 @@ # API -httpx~=0.22.0 +httpx~=0.23.0 pytest==7.1.1 pytest-asyncio==0.18.2 starlette==0.17.1 diff --git a/e2e_tests/test_performance.py b/e2e_tests/test_performance.py index 3eca1dfbb1..ed43e66d33 100644 --- a/e2e_tests/test_performance.py +++ b/e2e_tests/test_performance.py @@ -1,8 +1,7 @@ import asyncio import pytest - import config -from helpers import disable_and_delete_resource, post_resource +from helpers import disable_and_delete_resource, get_workspace_auth_details, post_resource from resources import strings pytestmark = pytest.mark.asyncio @@ -10,7 +9,7 @@ @pytest.mark.performance @pytest.mark.timeout(3000) -async def test_parallel_resource_creations(admin_token, workspace_owner_token, verify) -> None: +async def test_parallel_resource_creations(admin_token, verify) -> None: """Creates N workspaces in parallel, and creates a workspace service in each, in parallel""" number_workspaces = 2 @@ -28,23 +27,24 @@ async def test_parallel_resource_creations(admin_token, workspace_owner_token, v } } - task = asyncio.create_task(post_resource(payload, strings.API_WORKSPACES, 'workspace', workspace_owner_token, admin_token, verify)) + task = asyncio.create_task(post_resource(payload=payload, endpoint=strings.API_WORKSPACES, access_token=admin_token, verify=verify)) tasks.append(task) resource_paths = await asyncio.gather(*tasks) # Now disable + delete them all in parallel tasks = [] - for ws, _ in resource_paths: - task = asyncio.create_task(disable_and_delete_resource(f'/api{ws}', 'workspace', workspace_owner_token, admin_token, verify)) + for workspace_path, _ in resource_paths: + task = asyncio.create_task(disable_and_delete_resource(f'/api{workspace_path}', admin_token, verify)) tasks.append(task) await asyncio.gather(*tasks) +@pytest.mark.skip @pytest.mark.performance @pytest.mark.timeout(3000) -async def test_bulk_updates_to_ensure_each_resource_updated_in_series(admin_token, workspace_owner_token, verify) -> None: +async def test_bulk_updates_to_ensure_each_resource_updated_in_series(admin_token, verify) -> None: """Optionally creates a workspace and workspace service, then creates N number of VMs in parallel, patches each, and deletes them""" @@ -53,7 +53,9 @@ async def test_bulk_updates_to_ensure_each_resource_updated_in_series(admin_toke # To avoid creating + deleting a workspace + service in this test, set the vars for existing ones in ./templates/core/.env # PERF_TEST_WORKSPACE_ID | PERF_TEST_WORKSPACE_SERVICE_ID - if config.PERF_TEST_WORKSPACE_ID == "": + workspace_id = config.PERF_TEST_WORKSPACE_ID + + if workspace_id == "": # create the workspace to use payload = { "templateName": "tre-workspace-base", @@ -61,14 +63,17 @@ async def test_bulk_updates_to_ensure_each_resource_updated_in_series(admin_toke "display_name": "E2E test guacamole service", "description": "", "address_space_size": "small", - "client_id": f"{config.TEST_WORKSPACE_APP_ID}" + "client_id": f"{config.TEST_WORKSPACE_APP_ID}", + "client_secret": f"{config.TEST_WORKSPACE_APP_SECRET}" } } - workspace_path, _ = await post_resource(payload, strings.API_WORKSPACES, 'workspace', workspace_owner_token, admin_token, verify) + workspace_path, workspace_id = await post_resource(payload, strings.API_WORKSPACES, admin_token, verify) else: workspace_path = f"/workspaces/{config.PERF_TEST_WORKSPACE_ID}" + workspace_owner_token, scope_uri = await get_workspace_auth_details(admin_token=admin_token, workspace_id=workspace_id, verify=verify) + if config.PERF_TEST_WORKSPACE_SERVICE_ID == "": # create a guac service service_payload = { @@ -76,12 +81,15 @@ async def test_bulk_updates_to_ensure_each_resource_updated_in_series(admin_toke "properties": { "display_name": "Workspace service test", "description": "", - "ws_client_id": f"{config.TEST_WORKSPACE_APP_ID}", - "ws_client_secret": f"{config.TEST_WORKSPACE_APP_SECRET}" + "workspace_identifier_uri": scope_uri } } - workspace_service_path, _ = await post_resource(service_payload, f'/api{workspace_path}/{strings.API_WORKSPACE_SERVICES}', 'workspace_service', workspace_owner_token, None, verify) + workspace_service_path, _ = await post_resource( + payload=service_payload, + endpoint=f'/api{workspace_path}/{strings.API_WORKSPACE_SERVICES}', + access_token=workspace_owner_token, + verify=verify) else: workspace_service_path = f"{workspace_path}/{strings.API_WORKSPACE_SERVICES}/{config.PERF_TEST_WORKSPACE_SERVICE_ID}" @@ -97,7 +105,11 @@ async def test_bulk_updates_to_ensure_each_resource_updated_in_series(admin_toke tasks = [] for i in range(number_vms): - task = asyncio.create_task(post_resource(user_resource_payload, f'/api{workspace_service_path}/{strings.API_USER_RESOURCES}', 'user_resource', workspace_owner_token, None, verify)) + task = asyncio.create_task(post_resource( + payload=user_resource_payload, + endpoint=f'/api{workspace_service_path}/{strings.API_USER_RESOURCES}', + access_token=workspace_owner_token, + verify=verify)) tasks.append(task) resource_paths = await asyncio.gather(*tasks) @@ -113,17 +125,23 @@ async def test_bulk_updates_to_ensure_each_resource_updated_in_series(admin_toke "display_name": f'Perf test VM update {i}', } } - await post_resource(patch_payload, f'/api{resource_path}', 'user_resource', workspace_owner_token, None, verify, method="PATCH", wait=False) + await post_resource( + payload=patch_payload, + endpoint=f'/api{resource_path}', + access_token=workspace_owner_token, + verify=verify, + method="PATCH", + wait=False) # clear up all the VMs in parallel # NOTE: Due to bug https://github.com/microsoft/AzureTRE/issues/1163 - this VM delete step currently fails - task = asyncio.create_task(disable_and_delete_resource(f'/api{resource_path}', 'user_resource', workspace_owner_token, None, verify)) + task = asyncio.create_task(disable_and_delete_resource(f'/api{resource_path}', workspace_owner_token, verify)) tasks.append(task) await asyncio.gather(*tasks) # clear up workspace + service (if we created them) if config.PERF_TEST_WORKSPACE_SERVICE_ID == "": - await disable_and_delete_resource(f'/api{workspace_service_path}', 'workspace_service', workspace_owner_token, None, verify) + await disable_and_delete_resource(f'/api{workspace_service_path}', workspace_owner_token, verify) if config.PERF_TEST_WORKSPACE_ID == "": - await disable_and_delete_resource(f'/api{workspace_path}', 'workspace', workspace_owner_token, admin_token, verify) + await disable_and_delete_resource(f'/api{workspace_path}', admin_token, verify) diff --git a/e2e_tests/test_shared_services.py b/e2e_tests/test_shared_services.py index 71e950f0d4..bbca3e6809 100644 --- a/e2e_tests/test_shared_services.py +++ b/e2e_tests/test_shared_services.py @@ -79,12 +79,10 @@ async def test_patch_firewall(admin_token, verify): shared_service_path = f'/shared-services/{shared_service_firewall["id"]}' await post_resource( - patch_payload, - f"/api{shared_service_path}", - "shared_service", - admin_token, - None, - verify, + payload=patch_payload, + endpoint=f"/api{shared_service_path}", + access_token=admin_token, + verify=verify, method="PATCH", ) @@ -109,7 +107,7 @@ async def test_create_shared_service(template_name, admin_token, verify) -> None f"Shared service {template_name} already exists (id {id}), deleting it first..." ) await disable_and_delete_resource( - f"/api/shared-services/{id}", "shared_service", admin_token, None, verify + f"/api/shared-services/{id}", admin_token, verify ) post_payload = { @@ -121,14 +119,12 @@ async def test_create_shared_service(template_name, admin_token, verify) -> None } shared_service_path, _ = await post_resource( - post_payload, - "/api/shared-services", - "shared_service", - admin_token, - None, - verify, + payload=post_payload, + endpoint="/api/shared-services", + access_token=admin_token, + verify=verify, ) await disable_and_delete_resource( - f"/api{shared_service_path}", "shared_service", admin_token, None, verify + f"/api{shared_service_path}", admin_token, verify ) diff --git a/e2e_tests/test_workspace_services.py b/e2e_tests/test_workspace_services.py index baf917bda2..9302c6d25a 100644 --- a/e2e_tests/test_workspace_services.py +++ b/e2e_tests/test_workspace_services.py @@ -1,7 +1,7 @@ import pytest import config -from helpers import disable_and_delete_resource, get_workspace_owner_token, post_resource +from helpers import disable_and_delete_resource, get_workspace_auth_details, post_resource from resources import strings @@ -18,20 +18,20 @@ async def test_create_guacamole_service_into_base_workspace(admin_token, verify) "display_name": "E2E test guacamole service", "description": "workspace for E2E", "address_space_size": "small", - "client_id": f"{config.TEST_WORKSPACE_APP_ID}" + "client_id": f"{config.TEST_WORKSPACE_APP_ID}", + "client_secret": f"{config.TEST_WORKSPACE_APP_SECRET}" } } workspace_path, workspace_id = await post_resource(payload, strings.API_WORKSPACES, access_token=admin_token, verify=verify) - workspace_owner_token = await get_workspace_owner_token(admin_token=admin_token, workspace_id=workspace_id, verify=verify) + workspace_owner_token, scope_uri = await get_workspace_auth_details(admin_token=admin_token, workspace_id=workspace_id, verify=verify) service_payload = { "templateName": "tre-service-guacamole", "properties": { "display_name": "Workspace service test", "description": "Workspace service for E2E test", - "ws_client_id": f"{config.TEST_WORKSPACE_APP_ID}", - "ws_client_secret": f"{config.TEST_WORKSPACE_APP_SECRET}" + "workspace_identifier_uri": scope_uri } } @@ -57,7 +57,7 @@ async def test_create_guacamole_service_into_base_workspace(admin_token, verify) @pytest.mark.extended_aad @pytest.mark.timeout(3000) -async def test_create_guacamole_service_into_aad_workspace(admin_token, workspace_owner_token, verify) -> None: +async def test_create_guacamole_service_into_aad_workspace(admin_token, verify) -> None: """This test will create a Guacamole service but will create a workspace and automatically register the AAD Application""" payload = { @@ -71,15 +71,14 @@ async def test_create_guacamole_service_into_aad_workspace(admin_token, workspac } workspace_path, workspace_id = await post_resource(payload, strings.API_WORKSPACES, access_token=admin_token, verify=verify) - workspace_owner_token = await get_workspace_owner_token(admin_token=admin_token, workspace_id=workspace_id, verify=verify) + workspace_owner_token, scope_uri = await get_workspace_auth_details(admin_token=admin_token, workspace_id=workspace_id, verify=verify) service_payload = { "templateName": "tre-service-guacamole", "properties": { "display_name": "Workspace service test", "description": "Workspace service for E2E test", - "ws_client_id": f"{config.TEST_WORKSPACE_APP_ID}", - "ws_client_secret": f"{config.TEST_WORKSPACE_APP_SECRET}" + "workspace_identifier_uri": scope_uri } } diff --git a/mkdocs.yml b/mkdocs.yml index f380814876..db05f41501 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,6 +66,7 @@ nav: - API: 'tre-developers/api.md' - Resource Processor: 'tre-developers/resource-processor.md' - End to End Tests: 'tre-developers/end-to-end-tests.md' + - Letsencrypt: 'tre-developers/letsencrypt.md' - TRE Workspace Authors: - Authoring Workspace Templates: 'tre-workspace-authors/authoring-workspace-templates.md' - Firewall Rules: 'tre-workspace-authors/firewall-rules.md' diff --git a/resource_processor/version.txt b/resource_processor/version.txt index f4bd7163b1..493f7415d7 100644 --- a/resource_processor/version.txt +++ b/resource_processor/version.txt @@ -1 +1 @@ -__version__ = "0.1.21" +__version__ = "0.3.0" diff --git a/resource_processor/vmss_porter/Dockerfile b/resource_processor/vmss_porter/Dockerfile index b7361a5e92..a51aa17e8c 100644 --- a/resource_processor/vmss_porter/Dockerfile +++ b/resource_processor/vmss_porter/Dockerfile @@ -1,7 +1,7 @@ FROM python:3.8-slim-buster # Install Azure CLI -ARG AZURE_CLI_VERSION=2.29.2-1~buster +ARG AZURE_CLI_VERSION=2.36.0-1~buster COPY scripts/azure-cli.sh /tmp/ RUN export AZURE_CLI_VERSION=${AZURE_CLI_VERSION} \ && /tmp/azure-cli.sh diff --git a/templates/core/terraform/.terraform.lock.hcl b/templates/core/terraform/.terraform.lock.hcl index 61aa39d650..84749a34d9 100644 --- a/templates/core/terraform/.terraform.lock.hcl +++ b/templates/core/terraform/.terraform.lock.hcl @@ -2,21 +2,22 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/azurerm" { - version = "2.97.0" - constraints = "2.97.0" + version = "3.5.0" + constraints = "3.5.0" hashes = [ - "h1:XxT+XM/leTXa21aTnJjPBfNBQ8cLE4gYDg01WEZsV1U=", - "zh:0aac80e6d2b8ddf33d558ac893d52688e8abf8a0b995cfc3c35eb84afbf432a3", - "zh:11191068cb732208ebc8662651782f63db329a25f7ea1cd50cd91622a2c247b7", - "zh:36c8334194e7d605682053c7c70fbb2a650d9b0a7bcc44d5cdda4f205818438a", - "zh:3a5e01276added995e875b42ecc6b36ff73d267f0c096c87195bd2b1fff4f5b2", - "zh:557e38371657e6ed8aae9192d01480c4cca7c0f7ade6022f1aec247a6384922b", - "zh:67b913c280c5858549477a4b05e77078b1a5234de77c7bddd4ee1e8e237d5665", - "zh:7aeca864ce45b295db734cd968f7596ff12cd7c522ee89d53f432dae7c2b5d18", - "zh:b6127d7a796eaf9756dd212667eb48f79c0e78729589ec8ccf68e0b36ebb4e54", - "zh:bed448238740f897d1b399e5123b3a9eba256b981846f9ee92b71493446ca684", - "zh:c351a1bba34c3bd06fff75e4c15e4db0456268479463c2471598068ea1c5c884", - "zh:d073c24d0a4756e79b39f41f552d526800f9fb0ad0a74f742ac8de61b6416a3a", + "h1:T4XsCHDT839VehWKdxbVsLn0ECjcQaUTzbSGW055pgM=", + "zh:0d8ae6d6e87f44ed4a178be03d6466339b0bb578ab54c2677e365a8281b0bb7d", + "zh:29d250d1a18d49652b28f234ecd17687b36c875dc47877a678e587d5d136b054", + "zh:2e69ba373cf009e8a60b36d04f3dbc4638708d1bf88be9f96b3e52cbf8f47f31", + "zh:53d525dd84ac63b5f749bfbc6b70a202dacf29597664d2ab1165efea6f24f630", + "zh:a25024d574ccd5ae6c2962f3bb71d510f62899f493b1ed096f2f7f0e2b18f975", + "zh:aabc64fe64319b95aaba1d1866f87abc7b10adae37d2eafa2f85f37317fdd49f", + "zh:acc6a977814897cb23d3b3753213281334238f8bce6d2b21e9f04fc4087ee980", + "zh:b24987e9416c39cd59c0fa41c139a97406b9955f0607fcafbf3315014456338a", + "zh:c550eae45fd32acdbe32b4e5c450ae95df6cb18903ac7216b1b07b23a16ce045", + "zh:c8f83b763b643893dcb6933a6bcee824cb514e06e7e5c5f5ac4ba187e66d7e22", + "zh:dcdac07e7ea18464dea729717870c275de9453775243c231e1fb305cad0ee597", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } @@ -39,40 +40,59 @@ provider "registry.terraform.io/hashicorp/http" { } provider "registry.terraform.io/hashicorp/local" { - version = "2.2.2" + version = "2.2.3" hashes = [ - "h1:5UYW2wJ320IggrzLt8tLD6MowePqycWtH1b2RInHZkE=", - "zh:027e4873c69da214e2fed131666d5de92089732a11d096b68257da54d30b6f9d", - "zh:0ba2216e16cfb72538d76a4c4945b4567a76f7edbfef926b1c5a08d7bba2a043", - "zh:1fee8f6aae1833c27caa96e156cf99a681b6f085e476d7e1b77d285e21d182c1", - "zh:2e8a3e72e877003df1c390a231e0d8e827eba9f788606e643f8e061218750360", - "zh:719008f9e262aa1523a6f9132adbe9eee93c648c2981f8359ce41a40e6425433", + "h1:aWp5iSUxBGgPv1UnV5yag9Pb0N+U1I0sZb38AXBFO8A=", + "zh:04f0978bb3e052707b8e82e46780c371ac1c66b689b4a23bbc2f58865ab7d5c0", + "zh:6484f1b3e9e3771eb7cc8e8bab8b35f939a55d550b3f4fb2ab141a24269ee6aa", + "zh:78a56d59a013cb0f7eb1c92815d6eb5cf07f8b5f0ae20b96d049e73db915b238", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:9a70fdbe6ef955c4919a4519caca116f34c19c7ddedd77990fbe4f80fe66dc84", - "zh:abc412423d670cbb6264827fa80e1ffdc4a74aff3f19ba6a239dd87b85b15bec", - "zh:ae953a62c94d2a2a0822e5717fafc54e454af57bd6ed02cd301b9786765c1dd3", - "zh:be0910bdf46698560f9e86f51a4ff795c62c02f8dc82b2b1dab77a0b3a93f61e", - "zh:e58f9083b7971919b95f553227adaa7abe864fce976f0166cf4d65fc17257ff2", - "zh:ff4f77cbdbb22cc98182821c7ef84dce16298ab0e997d5c7fae97247f7a4bcb0", + "zh:8aa9950f4c4db37239bcb62e19910c49e47043f6c8587e5b0396619923657797", + "zh:996beea85f9084a725ff0e6473a4594deb5266727c5f56e9c1c7c62ded6addbb", + "zh:9a7ef7a21f48fabfd145b2e2a4240ca57517ad155017e86a30860d7c0c109de3", + "zh:a63e70ac052aa25120113bcddd50c1f3cfe61f681a93a50cea5595a4b2cc3e1c", + "zh:a6e8d46f94108e049ad85dbed60354236dc0b9b5ec8eabe01c4580280a43d3b8", + "zh:bb112ce7efbfcfa0e65ed97fa245ef348e0fd5bfa5a7e4ab2091a9bd469f0a9e", + "zh:d7bec0da5c094c6955efed100f3fe22fca8866859f87c025be1760feb174d6d9", + "zh:fb9f271b72094d07cef8154cd3d50e9aa818a0ea39130bc193132ad7b23076fd", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.1.1" + hashes = [ + "h1:71sNUDvmiJcijsvfXpiLCz0lXIBSsEJjMxljt7hxMhw=", + "zh:063466f41f1d9fd0dd93722840c1314f046d8760b1812fa67c34de0afcba5597", + "zh:08c058e367de6debdad35fc24d97131c7cf75103baec8279aba3506a08b53faf", + "zh:73ce6dff935150d6ddc6ac4a10071e02647d10175c173cfe5dca81f3d13d8afe", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8fdd792a626413502e68c195f2097352bdc6a0df694f7df350ed784741eb587e", + "zh:976bbaf268cb497400fd5b3c774d218f3933271864345f18deebe4dcbfcd6afa", + "zh:b21b78ca581f98f4cdb7a366b03ae9db23a73dfa7df12c533d7c19b68e9e72e5", + "zh:b7fc0c1615dbdb1d6fd4abb9c7dc7da286631f7ca2299fb9cd4664258ccfbff4", + "zh:d1efc942b2c44345e0c29bc976594cb7278c38cfb8897b344669eafbc3cddf46", + "zh:e356c245b3cd9d4789bab010893566acace682d7db877e52d40fc4ca34a50924", + "zh:ea98802ba92fcfa8cf12cbce2e9e7ebe999afbf8ed47fa45fc847a098d89468b", + "zh:eff8872458806499889f6927b5d954560f3d74bf20b6043409edf94d26cd906f", ] } provider "registry.terraform.io/hashicorp/random" { - version = "3.1.3" + version = "3.2.0" hashes = [ - "h1:nLWniS8xhb32qRQy+n4bDPjQ7YWZPVMR3v1vSrx7QyY=", - "zh:26e07aa32e403303fc212a4367b4d67188ac965c37a9812e07acee1470687a73", - "zh:27386f48e9c9d849fbb5a8828d461fde35e71f6b6c9fc235bc4ae8403eb9c92d", - "zh:5f4edda4c94240297bbd9b83618fd362348cadf6bf24ea65ea0e1844d7ccedc0", - "zh:646313a907126cd5e69f6a9fafe816e9154fccdc04541e06fed02bb3a8fa2d2e", - "zh:7349692932a5d462f8dee1500ab60401594dddb94e9aa6bf6c4c0bd53e91bbb8", + "h1:eeUh6cJ6wKLLuo4q9uQ0CA1Zvfqya4Wn1LecLCN8KKs=", + "zh:2960977ce9a7d6a7d3e934e75ec5814735626f95c186ad95a9102344a1a38ac1", + "zh:2fd012abfabe7076f3f2f402eeef4970e20574d20ffec57c162b02b6e848c32f", + "zh:4cd3234671cf01c913023418b227eb78b0659f2cd2e0b387be1f0bb607d29889", + "zh:52e695b4fa3fae735ffc901edff8183745f980923510a744db7616e8f10dc499", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:9034daba8d9b32b35930d168f363af04cecb153d5849a7e4a5966c97c5dc956e", - "zh:bb81dfca59ef5f949ef39f19ea4f4de25479907abc28cdaa36d12ecd7c0a9699", - "zh:bcf7806b99b4c248439ae02c8e21f77aff9fadbc019ce619b929eef09d1221bb", - "zh:d708e14d169e61f326535dd08eecd3811cd4942555a6f8efabc37dbff9c6fc61", - "zh:dc294e19a46e1cefb9e557a7b789c8dd8f319beca99b8c265181bc633dc434cc", - "zh:f9d758ee53c55dc016dd736427b6b0c3c8eb4d0dbbc785b6a3579b0ffedd9e42", + "zh:848b4a294e5ba15192ee4bfd199c07f60a437d7572efcd2d89db036e1ebc0e6e", + "zh:9d49aa432a05748a9527e95448cebee1238c87c97c7e8dec694bfd709683f9c7", + "zh:b4ad4cf289d3f7408649b74b8639918833613f2a1f3cf51b51f4b2fdaa412dd2", + "zh:c1544c4b416096fb8d8dbf84c4488584a2844a30dd533b957e9e9e60a165f24e", + "zh:dc737d6b4591cad8c9a1d0b347e587e846d8d901789b29b4dd401b6cdf82c017", + "zh:f5645fd39f749dbbf847cbdc87ba0dbd141143f12917a6a8904faf8a9b64111e", + "zh:fdedf610e0d020878a8f1fedda8105e0c33a7e23c4792fca54460685552de308", ] } diff --git a/templates/core/terraform/admin-jumpbox.tf b/templates/core/terraform/admin-jumpbox.tf index edcc6175b0..5e80e54e6b 100644 --- a/templates/core/terraform/admin-jumpbox.tf +++ b/templates/core/terraform/admin-jumpbox.tf @@ -2,6 +2,7 @@ resource "azurerm_network_interface" "jumpbox_nic" { name = "nic-vm-${var.tre_id}" resource_group_name = azurerm_resource_group.core.name location = azurerm_resource_group.core.location + tags = local.tre_core_tags ip_configuration { name = "internalIPConfig" @@ -42,6 +43,8 @@ resource "azurerm_windows_virtual_machine" "jumpbox" { allow_extension_operations = true admin_username = random_string.username.result admin_password = random_password.password.result + tags = local.tre_core_tags + custom_data = base64encode(data.template_file.vm_config.rendered) @@ -61,9 +64,7 @@ resource "azurerm_windows_virtual_machine" "jumpbox" { type = "SystemAssigned" } - tags = { - environment = "staging" - } + } resource "azurerm_key_vault_secret" "jumpbox_credentials" { diff --git a/templates/core/terraform/airlock/eventgrid_topics.tf b/templates/core/terraform/airlock/eventgrid_topics.tf new file mode 100644 index 0000000000..0ef1d82141 --- /dev/null +++ b/templates/core/terraform/airlock/eventgrid_topics.tf @@ -0,0 +1,148 @@ +# Event grid topics +resource "azurerm_eventgrid_topic" "step_result" { + name = local.step_result_topic_name + location = var.location + resource_group_name = var.resource_group_name + + tags = { + Publishers = "Airlock Orchestrator;" + } +} + +resource "azurerm_eventgrid_topic" "status_changed" { + name = local.status_changed_topic_name + location = var.location + resource_group_name = var.resource_group_name + + tags = { + Publishers = "TRE API;" + } +} + +# System topic +resource "azurerm_eventgrid_system_topic" "import_inprogress_blob_created" { + name = local.import_inprogress_sys_topic_name + location = var.location + resource_group_name = var.resource_group_name + source_arm_resource_id = azurerm_storage_account.sa_import_in_progress.id + topic_type = "Microsoft.Storage.StorageAccounts" + + tags = { + Publishers = "airlock;import-in-progress-sa" + } + + depends_on = [ + azurerm_storage_account.sa_import_in_progress + ] + + lifecycle { ignore_changes = [tags] } +} + + +resource "azurerm_eventgrid_system_topic" "import_rejected_blob_created" { + name = local.import_rejected_sys_topic_name + location = var.location + resource_group_name = var.resource_group_name + source_arm_resource_id = azurerm_storage_account.sa_import_rejected.id + topic_type = "Microsoft.Storage.StorageAccounts" + + tags = { + Publishers = "airlock;import-rejected-sa" + } + + depends_on = [ + azurerm_storage_account.sa_import_rejected + ] + + lifecycle { ignore_changes = [tags] } +} + +resource "azurerm_eventgrid_system_topic" "export_approved_blob_created" { + name = local.export_approved_sys_topic_name + location = var.location + resource_group_name = var.resource_group_name + source_arm_resource_id = azurerm_storage_account.sa_export_approved.id + topic_type = "Microsoft.Storage.StorageAccounts" + + tags = { + Publishers = "airlock;export-approved-sa" + } + + depends_on = [ + azurerm_storage_account.sa_export_approved + ] + + lifecycle { ignore_changes = [tags] } +} + + +# Custom topic (for scanning) +resource "azurerm_eventgrid_topic" "scan_result" { + name = local.scan_result_topic_name + location = var.location + resource_group_name = var.resource_group_name + + tags = { + Publishers = "airlock;custom scanning service;" + } + + lifecycle { ignore_changes = [tags] } +} + +## Subscriptions + +resource "azurerm_eventgrid_event_subscription" "step_result" { + name = local.step_result_eventgrid_subscription_name + scope = azurerm_eventgrid_topic.step_result.id + + service_bus_queue_endpoint_id = azurerm_servicebus_queue.step_result.id + + depends_on = [ + azurerm_eventgrid_topic.step_result + ] +} + +resource "azurerm_eventgrid_event_subscription" "status_changed" { + name = local.status_changed_eventgrid_subscription_name + scope = azurerm_eventgrid_topic.status_changed.id + + service_bus_queue_endpoint_id = azurerm_servicebus_queue.status_changed.id + + depends_on = [ + azurerm_eventgrid_topic.status_changed + ] +} + +resource "azurerm_eventgrid_event_subscription" "import_inprogress_blob_created" { + name = local.import_inprogress_eventgrid_subscription_name + scope = azurerm_storage_account.sa_import_in_progress.id + + service_bus_queue_endpoint_id = azurerm_servicebus_queue.import_in_progress_blob_created.id + + depends_on = [ + azurerm_eventgrid_system_topic.import_inprogress_blob_created + ] +} + +resource "azurerm_eventgrid_event_subscription" "import_rejected_blob_created" { + name = local.import_rejected_eventgrid_subscription_name + scope = azurerm_storage_account.sa_import_rejected.id + + service_bus_queue_endpoint_id = azurerm_servicebus_queue.import_rejected_blob_created.id + + depends_on = [ + azurerm_eventgrid_system_topic.import_rejected_blob_created + ] +} + +resource "azurerm_eventgrid_event_subscription" "export_approved_blob_created" { + name = local.export_approved_eventgrid_subscription_name + scope = azurerm_storage_account.sa_export_approved.id + + service_bus_queue_endpoint_id = azurerm_servicebus_queue.export_approved_blob_created.id + + depends_on = [ + azurerm_eventgrid_system_topic.export_approved_blob_created + ] +} + diff --git a/templates/core/terraform/airlock/locals.tf b/templates/core/terraform/airlock/locals.tf new file mode 100644 index 0000000000..080b0e05d5 --- /dev/null +++ b/templates/core/terraform/airlock/locals.tf @@ -0,0 +1,36 @@ +locals { + # STorage AirLock EXternal + import_external_storage_name = lower(replace("stalexim${var.tre_id}", "-", "")) + # STorage AirLock InProgress IMport + import_in_progress_storage_name = lower(replace("stalipim${var.tre_id}", "-", "")) + # STorage AirLock REJected IMport + import_rejected_storage_name = lower(replace("stalrejim${var.tre_id}", "-", "")) + # STorage AirLock APProved EXPort + export_approved_storage_name = lower(replace("stalappexp${var.tre_id}", "-", "")) + + import_inprogress_sys_topic_name = "evgt-airlock-import-in-progress-${var.tre_id}" + import_rejected_sys_topic_name = "evgt-airlock-import-rejected-${var.tre_id}" + export_approved_sys_topic_name = "evgt-airlock-export-approved-${var.tre_id}" + + scan_result_topic_name = "evgt-airlock-scan-result-${var.tre_id}" + step_result_topic_name = "evgt-airlock-step-result-${var.tre_id}" + status_changed_topic_name = "evgt-airlock-status-changed-${var.tre_id}" + + step_result_queue_name = "airlock-step-result" + status_changed_queue_name = "airlock-status-changed" + scan_result_queue_name = "airlock-scan-result" + import_inprogress_queue_name = "airlock-import-in-progress-blob-created" + import_rejected_queue_name = "airlock-import-rejected-blob-created" + + import_approved_queue_name = "airlock-import-approved-blob-created" + export_inprogress_queue_name = "airlock-export-inprogress-blob-created" + export_rejected_queue_name = "airlock-export-rejected-blob-created" + export_approved_queue_name = "airlock-export-approved-blob-created" + + step_result_eventgrid_subscription_name = "evgs-airlock-update-status" + status_changed_eventgrid_subscription_name = "evgs-airlock-status-changed" + import_inprogress_eventgrid_subscription_name = "evgs-airlock-import-in-progress-blob-created" + import_rejected_eventgrid_subscription_name = "evgs-airlock-import-rejected-blob-created" + export_approved_eventgrid_subscription_name = "evgs-airlock-export-approved-blob-created" + +} diff --git a/templates/core/terraform/airlock/service_bus.tf b/templates/core/terraform/airlock/service_bus.tf new file mode 100644 index 0000000000..580ccb4bba --- /dev/null +++ b/templates/core/terraform/airlock/service_bus.tf @@ -0,0 +1,76 @@ +# Utilize the existing service bus - add new queue +data "azurerm_servicebus_namespace" "airlock_sb" { + name = "sb-${var.tre_id}" + resource_group_name = var.resource_group_name + +} + +resource "azurerm_servicebus_queue" "step_result" { + name = local.step_result_queue_name + namespace_id = data.azurerm_servicebus_namespace.airlock_sb.id + + enable_partitioning = false +} + +resource "azurerm_servicebus_queue" "status_changed" { + name = local.status_changed_queue_name + namespace_id = data.azurerm_servicebus_namespace.airlock_sb.id + + enable_partitioning = false +} + + +resource "azurerm_servicebus_queue" "import_in_progress_blob_created" { + name = local.import_inprogress_queue_name + namespace_id = data.azurerm_servicebus_namespace.airlock_sb.id + + enable_partitioning = false +} + + +resource "azurerm_servicebus_queue" "import_rejected_blob_created" { + name = local.import_rejected_queue_name + namespace_id = data.azurerm_servicebus_namespace.airlock_sb.id + + enable_partitioning = false +} + + +resource "azurerm_servicebus_queue" "scan_result" { + name = local.scan_result_queue_name + namespace_id = data.azurerm_servicebus_namespace.airlock_sb.id + + enable_partitioning = false +} + +resource "azurerm_servicebus_queue" "import_approved_blob_created" { + name = local.import_approved_queue_name + namespace_id = data.azurerm_servicebus_namespace.airlock_sb.id + + enable_partitioning = false +} + +resource "azurerm_servicebus_queue" "export_in_progress_blob_created" { + name = local.export_inprogress_queue_name + namespace_id = data.azurerm_servicebus_namespace.airlock_sb.id + + enable_partitioning = false +} + +resource "azurerm_servicebus_queue" "export_rejected_blob_created" { + name = local.export_rejected_queue_name + namespace_id = data.azurerm_servicebus_namespace.airlock_sb.id + + enable_partitioning = false +} + +# Approved export +resource "azurerm_servicebus_queue" "export_approved_blob_created" { + name = local.export_approved_queue_name + namespace_id = data.azurerm_servicebus_namespace.airlock_sb.id + + enable_partitioning = false +} + + + diff --git a/templates/core/terraform/airlock/storage_accounts.tf b/templates/core/terraform/airlock/storage_accounts.tf new file mode 100644 index 0000000000..e038556d6e --- /dev/null +++ b/templates/core/terraform/airlock/storage_accounts.tf @@ -0,0 +1,141 @@ +# 'External' storage account - drop location for import +resource "azurerm_storage_account" "sa_external_import" { + name = local.import_external_storage_name + location = var.location + resource_group_name = var.resource_group_name + account_tier = "Standard" + account_replication_type = "GRS" + + # Don't allow anonymous access (unrelated to the 'public' networking rules) + allow_nested_items_to_be_public = false + + # Important! we rely on the fact that the blob craeted events are issued when the creation of the blobs are done. + # This is true ONLY when Hierarchical Namespace is DISABLED + is_hns_enabled = false + + tags = { + description = "airlock;import;external" + } + + lifecycle { ignore_changes = [tags] } +} + +# 'Approved' export +resource "azurerm_storage_account" "sa_export_approved" { + name = local.export_approved_storage_name + location = var.location + resource_group_name = var.resource_group_name + account_tier = "Standard" + account_replication_type = "GRS" + + # Don't allow anonymous access (unrelated to the 'public' networking rules) + allow_nested_items_to_be_public = false + + # Important! we rely on the fact that the blob craeted events are issued when the creation of the blobs are done. + # This is true ONLY when Hierarchical Namespace is DISABLED + is_hns_enabled = false + + tags = { + description = "airlock;export;approved" + } + + lifecycle { ignore_changes = [tags] } +} + +# 'In-Progress' storage account +resource "azurerm_storage_account" "sa_import_in_progress" { + name = local.import_in_progress_storage_name + location = var.location + resource_group_name = var.resource_group_name + account_tier = "Standard" + account_replication_type = "GRS" + allow_nested_items_to_be_public = false + + # Important! we rely on the fact that the blob craeted events are issued when the creation of the blobs are done. + # This is true ONLY when Hierarchical Namespace is DISABLED + is_hns_enabled = false + + tags = { + description = "airlock;import;in-progress" + } + + network_rules { + default_action = var.enable_local_debugging ? "Allow" : "Deny" + bypass = ["AzureServices"] + } + + lifecycle { ignore_changes = [tags] } +} + +data "azurerm_private_dns_zone" "blobcore" { + name = "privatelink.blob.core.windows.net" + resource_group_name = var.resource_group_name +} + +resource "azurerm_private_endpoint" "stg_ip_import_pe" { + name = "stg-ip-import-blob-${var.tre_id}" + location = var.location + resource_group_name = var.resource_group_name + subnet_id = var.shared_subnet_id + + lifecycle { ignore_changes = [tags] } + + private_dns_zone_group { + name = "private-dns-zone-group-stg-import-ip" + private_dns_zone_ids = [data.azurerm_private_dns_zone.blobcore.id] + } + + private_service_connection { + name = "psc-stgipimport-${var.tre_id}" + private_connection_resource_id = azurerm_storage_account.sa_import_in_progress.id + is_manual_connection = false + subresource_names = ["Blob"] + } +} + + +# 'Rejected' storage account +resource "azurerm_storage_account" "sa_import_rejected" { + name = local.import_rejected_storage_name + location = var.location + resource_group_name = var.resource_group_name + account_tier = "Standard" + account_replication_type = "GRS" + allow_nested_items_to_be_public = false + + # Important! we rely on the fact that the blob craeted events are issued when the creation of the blobs are done. + # This is true ONLY when Hierarchical Namespace is DISABLED + is_hns_enabled = false + + tags = { + description = "airlock;import;rejected" + } + + network_rules { + default_action = var.enable_local_debugging ? "Allow" : "Deny" + bypass = ["AzureServices"] + } + + lifecycle { ignore_changes = [tags] } +} + +resource "azurerm_private_endpoint" "stgipimportpe" { + name = "stg-import-rej-blob-${var.tre_id}" + location = var.location + resource_group_name = var.resource_group_name + subnet_id = var.shared_subnet_id + + lifecycle { ignore_changes = [tags] } + + private_dns_zone_group { + name = "private-dns-zone-group-stg-import-rej" + private_dns_zone_ids = [data.azurerm_private_dns_zone.blobcore.id] + } + + private_service_connection { + name = "psc-stg-import-rej-${var.tre_id}" + private_connection_resource_id = azurerm_storage_account.sa_import_rejected.id + is_manual_connection = false + subresource_names = ["Blob"] + } +} diff --git a/templates/core/terraform/airlock/variables.tf b/templates/core/terraform/airlock/variables.tf new file mode 100644 index 0000000000..850cb96e1f --- /dev/null +++ b/templates/core/terraform/airlock/variables.tf @@ -0,0 +1,5 @@ +variable "tre_id" {} +variable "location" {} +variable "resource_group_name" {} +variable "shared_subnet_id" {} +variable "enable_local_debugging" {} diff --git a/templates/core/terraform/api-webapp.tf b/templates/core/terraform/api-webapp.tf index 40421e5541..34da2e2548 100644 --- a/templates/core/terraform/api-webapp.tf +++ b/templates/core/terraform/api-webapp.tf @@ -10,9 +10,9 @@ resource "azurerm_app_service_plan" "core" { name = "plan-${var.tre_id}" resource_group_name = azurerm_resource_group.core.name location = azurerm_resource_group.core.location - reserved = true kind = "linux" - + reserved = true + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } sku { @@ -29,6 +29,7 @@ resource "azurerm_app_service" "api" { app_service_plan_id = azurerm_app_service_plan.core.id https_only = true key_vault_reference_identity_id = azurerm_user_assigned_identity.id.id + tags = local.tre_core_tags app_settings = { "APPLICATIONINSIGHTS_CONNECTION_STRING" = module.azure_monitor.app_insights_connection_string @@ -37,7 +38,6 @@ resource "azurerm_app_service" "api" { "ApplicationInsightsAgent_EXTENSION_VERSION" = "~3" "XDT_MicrosoftApplicationInsights_Mode" = "default" "WEBSITES_PORT" = "8000" - "WEBSITE_VNET_ROUTE_ALL" = 1 "DOCKER_REGISTRY_SERVER_URL" = "https://${var.docker_registry_server}" "STATE_STORE_ENDPOINT" = azurerm_cosmosdb_account.tre-db-account.endpoint "COSMOSDB_ACCOUNT_NAME" = azurerm_cosmosdb_account.tre-db-account.name @@ -66,28 +66,27 @@ resource "azurerm_app_service" "api" { site_config { linux_fx_version = "DOCKER|${var.docker_registry_server}/${var.api_image_repository}:${local.version}" + vnet_route_all_enabled = true remote_debugging_enabled = false scm_use_main_ip_restriction = true acr_use_managed_identity_credentials = true acr_user_managed_identity_client_id = azurerm_user_assigned_identity.id.client_id + always_on = true + min_tls_version = "1.2" + ftps_state = "Disabled" + websockets_enabled = false cors { allowed_origins = [] support_credentials = false } - always_on = true - min_tls_version = "1.2" - ip_restriction { action = "Deny" ip_address = "0.0.0.0/0" name = "Deny all" priority = 2147483647 } - - ftps_state = "FtpsOnly" - websockets_enabled = false } logs { @@ -109,6 +108,7 @@ resource "azurerm_private_endpoint" "api_private_endpoint" { resource_group_name = azurerm_resource_group.core.name location = azurerm_resource_group.core.location subnet_id = module.network.shared_subnet_id + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } diff --git a/templates/core/terraform/appgateway/appgateway.tf b/templates/core/terraform/appgateway/appgateway.tf index 958169bae0..21abc62889 100644 --- a/templates/core/terraform/appgateway/appgateway.tf +++ b/templates/core/terraform/appgateway/appgateway.tf @@ -5,14 +5,16 @@ resource "azurerm_public_ip" "appgwpip" { allocation_method = "Static" # Static IPs are allocated immediately sku = "Standard" domain_name_label = var.tre_id + tags = local.tre_core_tags - lifecycle { ignore_changes = [tags] } + lifecycle { ignore_changes = [tags, zones] } } resource "azurerm_user_assigned_identity" "agw_id" { resource_group_name = var.resource_group_name location = var.location name = "id-agw-${var.tre_id}" + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -21,6 +23,7 @@ resource "azurerm_application_gateway" "agw" { name = "agw-${var.tre_id}" resource_group_name = var.resource_group_name location = var.location + tags = local.tre_core_tags sku { name = "Standard_v2" @@ -180,12 +183,7 @@ resource "azurerm_application_gateway" "agw" { } # We don't want Terraform to revert certificate cycle changes. We assume the certificate will be renewed in keyvault. - lifecycle { - ignore_changes = [ - ssl_certificate, - tags - ] - } + lifecycle { ignore_changes = [ssl_certificate, tags] } } diff --git a/templates/core/terraform/appgateway/locals.tf b/templates/core/terraform/appgateway/locals.tf index e5740887c8..57aaa1bd03 100644 --- a/templates/core/terraform/appgateway/locals.tf +++ b/templates/core/terraform/appgateway/locals.tf @@ -24,4 +24,8 @@ locals { redirect_configuration_name = "rdrcfg-tosecure" certificate_name = "cert-primary" + tre_core_tags = { + tre_id = var.tre_id + tre_core_service_id = var.tre_id + } } diff --git a/templates/core/terraform/appgateway/staticweb.tf b/templates/core/terraform/appgateway/staticweb.tf index 7cc7d91c31..28b4a96892 100644 --- a/templates/core/terraform/appgateway/staticweb.tf +++ b/templates/core/terraform/appgateway/staticweb.tf @@ -1,18 +1,17 @@ data "azurerm_client_config" "deployer" {} +# See https://microsoft.github.io/AzureTRE/tre-developers/letsencrypt/ resource "azurerm_storage_account" "staticweb" { - name = local.staticweb_storage_name - resource_group_name = var.resource_group_name - location = var.location - account_kind = "StorageV2" - account_tier = "Standard" - account_replication_type = "LRS" - enable_https_traffic_only = true - allow_blob_public_access = false - - tags = { - tre_id = var.tre_id - } + name = local.staticweb_storage_name + resource_group_name = var.resource_group_name + location = var.location + account_kind = "StorageV2" + account_tier = "Standard" + account_replication_type = "LRS" + enable_https_traffic_only = true + allow_nested_items_to_be_public = false + + tags = local.tre_core_tags static_website { index_document = "index.html" @@ -39,6 +38,7 @@ resource "azurerm_private_endpoint" "webpe" { location = var.location resource_group_name = var.resource_group_name subnet_id = var.shared_subnet + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } @@ -48,7 +48,7 @@ resource "azurerm_private_endpoint" "webpe" { } private_service_connection { - name = "psc-web--${local.staticweb_storage_name}" + name = "psc-web-${local.staticweb_storage_name}" private_connection_resource_id = azurerm_storage_account.staticweb.id is_manual_connection = false subresource_names = ["web"] diff --git a/templates/core/terraform/azure-monitor/ampls.json b/templates/core/terraform/azure-monitor/ampls.json index a033c3b3c0..9092192f45 100644 --- a/templates/core/terraform/azure-monitor/ampls.json +++ b/templates/core/terraform/azure-monitor/ampls.json @@ -10,6 +10,9 @@ }, "app_insights_name": { "type": "String" + }, + "tre_core_tags": { + "type": "Object" } }, "variables": {}, @@ -19,6 +22,7 @@ "apiVersion": "2021-07-01-preview", "name": "[parameters('private_link_scope_name')]", "location": "global", + "tags": "[parameters('tre_core_tags')]", "properties": { "accessModeSettings": { "queryAccessMode":"Open", @@ -30,6 +34,7 @@ "type": "microsoft.insights/privatelinkscopes/scopedresources", "apiVersion": "2019-10-17-preview", "name": "[concat(parameters('private_link_scope_name'), '/', concat(parameters('workspace_name'), '-connection'))]", + "tags": "[parameters('tre_core_tags')]", "dependsOn": [ "[resourceId('microsoft.insights/privatelinkscopes', parameters('private_link_scope_name'))]" ], @@ -41,6 +46,7 @@ "type": "microsoft.insights/privatelinkscopes/scopedresources", "apiVersion": "2019-10-17-preview", "name": "[concat(parameters('private_link_scope_name'), '/', concat(parameters('app_insights_name'), '-connection'))]", + "tags": "[parameters('tre_core_tags')]", "dependsOn": [ "[resourceId('microsoft.insights/privatelinkscopes', parameters('private_link_scope_name'))]" ], diff --git a/templates/core/terraform/azure-monitor/app_insights.json b/templates/core/terraform/azure-monitor/app_insights.json index f4e7aeaf9b..400f27f5d9 100644 --- a/templates/core/terraform/azure-monitor/app_insights.json +++ b/templates/core/terraform/azure-monitor/app_insights.json @@ -16,6 +16,9 @@ }, "storage_account_name": { "type": "String" + }, + "tre_core_tags": { + "type": "Object" } }, "variables": {}, @@ -26,6 +29,7 @@ "name": "[parameters('app_insights_name')]", "location": "[parameters('location')]", "kind": "web", + "tags": "[parameters('tre_core_tags')]", "properties": { "Application_Type": "[parameters('application_type')]", "SamplingPercentage": 100, diff --git a/templates/core/terraform/azure-monitor/azure-monitor.tf b/templates/core/terraform/azure-monitor/azure-monitor.tf index 23c923157c..8e1962744b 100644 --- a/templates/core/terraform/azure-monitor/azure-monitor.tf +++ b/templates/core/terraform/azure-monitor/azure-monitor.tf @@ -3,7 +3,8 @@ resource "azurerm_log_analytics_workspace" "core" { resource_group_name = var.resource_group_name location = var.location retention_in_days = 30 - sku = "pergb2018" + sku = "PerGB2018" + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -11,13 +12,14 @@ resource "azurerm_log_analytics_workspace" "core" { # Storage account for Application Insights # Because Private Link is enabled on Application Performance Management (APM), Bring Your Own Storage (BYOS) approach is required resource "azurerm_storage_account" "app_insights" { - name = lower(replace("stappinsights${var.tre_id}", "-", "")) - resource_group_name = var.resource_group_name - location = var.location - account_kind = "StorageV2" - account_tier = "Standard" - account_replication_type = "LRS" - allow_blob_public_access = false + name = lower(replace("stappinsights${var.tre_id}", "-", "")) + resource_group_name = var.resource_group_name + location = var.location + account_kind = "StorageV2" + account_tier = "Standard" + account_replication_type = "LRS" + allow_nested_items_to_be_public = false + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -50,6 +52,9 @@ resource "azurerm_resource_group_template_deployment" "app_insights_core" { "storage_account_name" = { value = azurerm_storage_account.app_insights.name } + "tre_core_tags" = { + value = local.tre_core_tags + } }) } @@ -65,6 +70,7 @@ resource "azurerm_resource_group_template_deployment" "ampls_core" { deployment_mode = "Incremental" template_content = data.local_file.ampls_arm_template.content + parameters_content = jsonencode({ "private_link_scope_name" = { value = "ampls-${var.tre_id}" @@ -75,6 +81,9 @@ resource "azurerm_resource_group_template_deployment" "ampls_core" { "app_insights_name" = { value = local.app_insights_name } + "tre_core_tags" = { + value = local.tre_core_tags + } }) depends_on = [ @@ -88,6 +97,7 @@ resource "azurerm_private_endpoint" "azure_monitor_private_endpoint" { resource_group_name = var.resource_group_name location = var.location subnet_id = var.shared_subnet_id + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } diff --git a/templates/core/terraform/azure-monitor/locals.tf b/templates/core/terraform/azure-monitor/locals.tf index 0db81706f9..11464c94d3 100644 --- a/templates/core/terraform/azure-monitor/locals.tf +++ b/templates/core/terraform/azure-monitor/locals.tf @@ -1,3 +1,7 @@ locals { app_insights_name = "appi-${var.tre_id}" + tre_core_tags = { + tre_id = var.tre_id + tre_core_service_id = var.tre_id + } } diff --git a/templates/core/terraform/bastion.tf b/templates/core/terraform/bastion.tf index 8373c21fe7..1eec8f3d6d 100644 --- a/templates/core/terraform/bastion.tf +++ b/templates/core/terraform/bastion.tf @@ -4,8 +4,9 @@ resource "azurerm_public_ip" "bastion" { location = azurerm_resource_group.core.location allocation_method = "Static" sku = "Standard" + tags = local.tre_core_tags - lifecycle { ignore_changes = [tags] } + lifecycle { ignore_changes = [tags, zones] } } resource "azurerm_bastion_host" "bastion" { @@ -19,6 +20,8 @@ resource "azurerm_bastion_host" "bastion" { public_ip_address_id = azurerm_public_ip.bastion.id } + tags = local.tre_core_tags + lifecycle { ignore_changes = [tags] } } diff --git a/templates/core/terraform/identity.tf b/templates/core/terraform/identity.tf index 7bd18b3e2e..34bb200ee9 100644 --- a/templates/core/terraform/identity.tf +++ b/templates/core/terraform/identity.tf @@ -1,6 +1,7 @@ resource "azurerm_user_assigned_identity" "id" { resource_group_name = azurerm_resource_group.core.name location = azurerm_resource_group.core.location + tags = local.tre_core_tags name = "id-api-${var.tre_id}" diff --git a/templates/core/terraform/keyvault.tf b/templates/core/terraform/keyvault.tf index c7cbdc095b..0cadb78d77 100644 --- a/templates/core/terraform/keyvault.tf +++ b/templates/core/terraform/keyvault.tf @@ -5,6 +5,7 @@ resource "azurerm_key_vault" "kv" { resource_group_name = azurerm_resource_group.core.name sku_name = "standard" purge_protection_enabled = var.keyvault_purge_protection_enabled + tags = local.tre_core_tags lifecycle { ignore_changes = [access_policy, tags] } } @@ -44,6 +45,7 @@ resource "azurerm_private_endpoint" "kvpe" { location = azurerm_resource_group.core.location resource_group_name = azurerm_resource_group.core.name subnet_id = module.network.shared_subnet_id + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } diff --git a/templates/core/terraform/locals.tf b/templates/core/terraform/locals.tf index 1da0b512cf..796817bb64 100644 --- a/templates/core/terraform/locals.tf +++ b/templates/core/terraform/locals.tf @@ -21,3 +21,10 @@ data "http" "myip" { locals { myip = var.public_deployment_ip_address != "" ? var.public_deployment_ip_address : chomp(data.http.myip[0].body) } + +locals { + tre_core_tags = { + tre_id = var.tre_id + tre_core_service_id = var.tre_id + } +} diff --git a/templates/core/terraform/main.tf b/templates/core/terraform/main.tf index 8012cc6e3a..983f179248 100644 --- a/templates/core/terraform/main.tf +++ b/templates/core/terraform/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "=2.97.0" + version = "=3.5.0" } } @@ -72,6 +72,20 @@ module "appgateway" { ] } +module "airlock_resources" { + source = "./airlock" + tre_id = var.tre_id + location = var.location + resource_group_name = azurerm_resource_group.core.name + shared_subnet_id = module.network.shared_subnet_id + enable_local_debugging = var.enable_local_debugging + + depends_on = [ + azurerm_servicebus_namespace.sb, + module.network + ] +} + module "resource_processor_vmss_porter" { count = var.resource_processor_type == "vmss_porter" ? 1 : 0 source = "./resource_processor/vmss_porter" @@ -90,6 +104,7 @@ module "resource_processor_vmss_porter" { mgmt_resource_group_name = var.mgmt_resource_group_name terraform_state_container_name = var.terraform_state_container_name key_vault_name = azurerm_key_vault.kv.name + key_vault_id = azurerm_key_vault.kv.id subscription_id = var.arm_subscription_id resource_processor_number_processes_per_instance = var.resource_processor_number_processes_per_instance diff --git a/templates/core/terraform/network/dns_zones.tf b/templates/core/terraform/network/dns_zones.tf index 5dc8c55cf5..37c18819c8 100644 --- a/templates/core/terraform/network/dns_zones.tf +++ b/templates/core/terraform/network/dns_zones.tf @@ -9,6 +9,7 @@ resource "azurerm_private_dns_zone" "azure_monitor" { name = "privatelink.monitor.azure.com" resource_group_name = var.resource_group_name + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -19,6 +20,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "azure_monitor" { virtual_network_id = azurerm_virtual_network.core.id private_dns_zone_name = azurerm_private_dns_zone.azure_monitor.name registration_enabled = false + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -26,6 +28,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "azure_monitor" { resource "azurerm_private_dns_zone" "azure_monitor_oms_opinsights" { name = "privatelink.oms.opinsights.azure.com" resource_group_name = var.resource_group_name + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -36,6 +39,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "azure_monitor_oms_opin virtual_network_id = azurerm_virtual_network.core.id private_dns_zone_name = azurerm_private_dns_zone.azure_monitor_oms_opinsights.name registration_enabled = false + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -43,7 +47,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "azure_monitor_oms_opin resource "azurerm_private_dns_zone" "azure_monitor_ods_opinsights" { name = "privatelink.ods.opinsights.azure.com" resource_group_name = var.resource_group_name - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -53,14 +57,14 @@ resource "azurerm_private_dns_zone_virtual_network_link" "azure_monitor_ods_opin virtual_network_id = azurerm_virtual_network.core.id private_dns_zone_name = azurerm_private_dns_zone.azure_monitor_ods_opinsights.name registration_enabled = false - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } resource "azurerm_private_dns_zone" "azure_monitor_agentsvc" { name = "privatelink.agentsvc.azure-automation.net" resource_group_name = var.resource_group_name - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -70,7 +74,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "azure_monitor_agentsvc virtual_network_id = azurerm_virtual_network.core.id private_dns_zone_name = azurerm_private_dns_zone.azure_monitor_agentsvc.name registration_enabled = false - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -78,7 +82,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "azure_monitor_agentsvc resource "azurerm_private_dns_zone" "blobcore" { name = "privatelink.blob.core.windows.net" resource_group_name = var.resource_group_name - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -87,7 +91,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "blobcore" { resource_group_name = var.resource_group_name private_dns_zone_name = azurerm_private_dns_zone.blobcore.name virtual_network_id = azurerm_virtual_network.core.id - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -95,7 +99,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "blobcore" { resource "azurerm_private_dns_zone" "azurewebsites" { name = "privatelink.azurewebsites.net" resource_group_name = var.resource_group_name - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -105,7 +109,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "azurewebsites" { private_dns_zone_name = azurerm_private_dns_zone.azurewebsites.name name = "azurewebsites-link" registration_enabled = false - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -113,7 +117,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "azurewebsites" { resource "azurerm_private_dns_zone" "mysql" { name = "privatelink.mysql.database.azure.com" resource_group_name = var.resource_group_name - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -123,7 +127,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "mysql" { private_dns_zone_name = azurerm_private_dns_zone.mysql.name name = "azurewebsites-link" registration_enabled = false - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -131,7 +135,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "mysql" { resource "azurerm_private_dns_zone" "static_web" { name = "privatelink.web.core.windows.net" resource_group_name = var.resource_group_name - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -140,7 +144,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "webcorelink" { resource_group_name = var.resource_group_name private_dns_zone_name = azurerm_private_dns_zone.static_web.name virtual_network_id = azurerm_virtual_network.core.id - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -148,7 +152,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "webcorelink" { resource "azurerm_private_dns_zone" "filecore" { name = "privatelink.file.core.windows.net" resource_group_name = var.resource_group_name - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -157,7 +161,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "filecorelink" { resource_group_name = var.resource_group_name private_dns_zone_name = azurerm_private_dns_zone.filecore.name virtual_network_id = azurerm_virtual_network.core.id - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -165,7 +169,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "filecorelink" { resource "azurerm_private_dns_zone" "vaultcore" { name = "privatelink.vaultcore.azure.net" resource_group_name = var.resource_group_name - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -174,7 +178,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "vaultcore" { resource_group_name = var.resource_group_name private_dns_zone_name = azurerm_private_dns_zone.vaultcore.name virtual_network_id = azurerm_virtual_network.core.id - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -182,7 +186,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "vaultcore" { resource "azurerm_private_dns_zone" "azurecr" { name = "privatelink.azurecr.io" resource_group_name = var.resource_group_name - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -191,7 +195,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "acrlink" { resource_group_name = var.resource_group_name private_dns_zone_name = azurerm_private_dns_zone.azurecr.name virtual_network_id = azurerm_virtual_network.core.id - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -199,7 +203,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "acrlink" { resource "azurerm_private_dns_zone" "azureml" { name = "privatelink.api.azureml.ms" resource_group_name = var.resource_group_name - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -207,7 +211,7 @@ resource "azurerm_private_dns_zone" "azureml" { resource "azurerm_private_dns_zone" "azuremlcert" { name = "privatelink.cert.api.azureml.ms" resource_group_name = var.resource_group_name - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -215,13 +219,20 @@ resource "azurerm_private_dns_zone" "azuremlcert" { resource "azurerm_private_dns_zone" "notebooks" { name = "privatelink.notebooks.azure.net" resource_group_name = var.resource_group_name - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } resource "azurerm_private_dns_zone" "postgres" { name = "privatelink.postgres.database.azure.com" resource_group_name = var.resource_group_name + tags = local.tre_core_tags + lifecycle { ignore_changes = [tags] } +} + +resource "azurerm_private_dns_zone" "nexus" { + name = "nexus-${var.tre_id}.${var.location}.cloudapp.azure.com" + resource_group_name = var.resource_group_name lifecycle { ignore_changes = [tags] } } diff --git a/templates/core/terraform/network/locals.tf b/templates/core/terraform/network/locals.tf index 3af3d5f405..d25759b163 100644 --- a/templates/core/terraform/network/locals.tf +++ b/templates/core/terraform/network/locals.tf @@ -7,4 +7,8 @@ locals { shared_services_subnet_address_prefix = local.core_services_vnet_subnets[4] # .0 - .254 aci_subnet_address_prefix = local.core_services_vnet_subnets[5] # .0 - .254 resource_processor_subnet_address_prefix = local.core_services_vnet_subnets[6] # .0 - .254 + tre_core_tags = { + tre_id = var.tre_id + tre_core_service_id = var.tre_id + } } diff --git a/templates/core/terraform/network/network.tf b/templates/core/terraform/network/network.tf index a4aeb9deee..a45039abbd 100644 --- a/templates/core/terraform/network/network.tf +++ b/templates/core/terraform/network/network.tf @@ -3,7 +3,7 @@ resource "azurerm_virtual_network" "core" { location = var.location resource_group_name = var.resource_group_name address_space = [var.core_address_space] - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } diff --git a/templates/core/terraform/network/network_security_groups.tf b/templates/core/terraform/network/network_security_groups.tf index 2a6f310fd8..3119767ab0 100644 --- a/templates/core/terraform/network/network_security_groups.tf +++ b/templates/core/terraform/network/network_security_groups.tf @@ -4,6 +4,7 @@ resource "azurerm_network_security_group" "bastion" { name = "nsg-bastion-subnet" location = var.location resource_group_name = var.resource_group_name + tags = local.tre_core_tags security_rule { name = "AllowInboundInternet" @@ -113,6 +114,7 @@ resource "azurerm_network_security_group" "app_gw" { name = "nsg-app-gw" location = var.location resource_group_name = var.resource_group_name + tags = local.tre_core_tags security_rule { name = "AllowInboundGatewayManager" @@ -150,6 +152,7 @@ resource "azurerm_network_security_group" "default_rules" { name = "nsg-default-rules" location = var.location resource_group_name = var.resource_group_name + tags = local.tre_core_tags } resource "azurerm_subnet_network_security_group_association" "shared" { diff --git a/templates/core/terraform/resource_processor/vmss_porter/cloud-config.yaml b/templates/core/terraform/resource_processor/vmss_porter/cloud-config.yaml index a8ee2f05f2..141d11766f 100644 --- a/templates/core/terraform/resource_processor/vmss_porter/cloud-config.yaml +++ b/templates/core/terraform/resource_processor/vmss_porter/cloud-config.yaml @@ -23,6 +23,15 @@ packages: - gnupg2 - pass +# create the docker group +groups: + - docker + +# add default auto created user to docker group +system_info: + default_user: + groups: [docker] + write_files: - path: .env content: | diff --git a/templates/core/terraform/resource_processor/vmss_porter/locals.tf b/templates/core/terraform/resource_processor/vmss_porter/locals.tf index 4c697699f3..f292d6eaa1 100644 --- a/templates/core/terraform/resource_processor/vmss_porter/locals.tf +++ b/templates/core/terraform/resource_processor/vmss_porter/locals.tf @@ -1,3 +1,7 @@ locals { version = replace(replace(replace(data.local_file.version.content, "__version__ = \"", ""), "\"", ""), "\n", "") -} \ No newline at end of file + tre_core_tags = { + tre_id = var.tre_id + tre_core_service_id = var.tre_id + } +} diff --git a/templates/core/terraform/resource_processor/vmss_porter/main.tf b/templates/core/terraform/resource_processor/vmss_porter/main.tf index dfb134341c..8b67c8110c 100644 --- a/templates/core/terraform/resource_processor/vmss_porter/main.tf +++ b/templates/core/terraform/resource_processor/vmss_porter/main.tf @@ -49,22 +49,30 @@ resource "random_password" "password" { resource "azurerm_key_vault_secret" "resource_processor_vmss_password" { name = "resource-processor-vmss-password" value = random_password.password.result - key_vault_id = data.azurerm_key_vault.kv.id + key_vault_id = var.key_vault_id } resource "azurerm_user_assigned_identity" "vmss_msi" { name = "id-vmss-${var.tre_id}" location = var.location resource_group_name = var.resource_group_name + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } resource "azurerm_linux_virtual_machine_scale_set" "vm_linux" { - - name = "vmss-rp-porter-${var.tre_id}" - location = var.location - resource_group_name = var.resource_group_name - upgrade_mode = "Automatic" + name = "vmss-rp-porter-${var.tre_id}" + location = var.location + resource_group_name = var.resource_group_name + sku = "Standard_B2s" + instances = 1 + admin_username = "adminuser" + disable_password_authentication = false + admin_password = random_password.password.result + custom_data = data.template_cloudinit_config.config.rendered + encryption_at_host_enabled = false + upgrade_mode = "Automatic" + tags = local.tre_core_tags extension { auto_upgrade_minor_version = false @@ -96,23 +104,11 @@ resource "azurerm_linux_virtual_machine_scale_set" "vm_linux" { } - encryption_at_host_enabled = false - identity { type = "UserAssigned" identity_ids = [azurerm_user_assigned_identity.vmss_msi.id] } - - sku = "Standard_B2s" - instances = 1 - - admin_username = "adminuser" - disable_password_authentication = false - - admin_password = random_password.password.result - custom_data = data.template_cloudinit_config.config.rendered - source_image_reference { publisher = "Canonical" offer = "UbuntuServer" @@ -136,7 +132,26 @@ resource "azurerm_linux_virtual_machine_scale_set" "vm_linux" { } } - lifecycle { ignore_changes = [tags] } + lifecycle { + ignore_changes = [tags] + } +} + +# CustomData (e.g. image tag to run) changes will only take affect after vmss instances are reimaged. +# https://docs.microsoft.com/en-us/azure/virtual-machines/custom-data#can-i-update-custom-data-after-the-vm-has-been-created +resource "null_resource" "vm_linux_reimage" { + provisioner "local-exec" { + command = "az vmss reimage --name ${azurerm_linux_virtual_machine_scale_set.vm_linux.name} --resource-group ${var.resource_group_name}" + } + + triggers = { + # although we mainly want to catch image tag changes, this covers any custom data change. + custom_data_hash = sha256(data.template_cloudinit_config.config.rendered) + } + + depends_on = [ + azurerm_linux_virtual_machine_scale_set.vm_linux + ] } resource "azurerm_role_assignment" "vmss_acr_pull" { @@ -172,9 +187,10 @@ resource "azurerm_role_assignment" "subscription_contributor" { } resource "azurerm_key_vault_access_policy" "resource_processor" { - key_vault_id = data.azurerm_key_vault.kv.id + key_vault_id = var.key_vault_id tenant_id = azurerm_user_assigned_identity.vmss_msi.tenant_id object_id = azurerm_user_assigned_identity.vmss_msi.principal_id - secret_permissions = ["Get", "List", "Set", "Delete", "Purge", "Recover"] + secret_permissions = ["Get", "List", "Set", "Delete", "Purge", "Recover"] + certificate_permissions = ["Get", "Recover", "Import", "Delete", "Purge"] } diff --git a/templates/core/terraform/resource_processor/vmss_porter/variables.tf b/templates/core/terraform/resource_processor/vmss_porter/variables.tf index 50f874affd..7e292f5d8a 100644 --- a/templates/core/terraform/resource_processor/vmss_porter/variables.tf +++ b/templates/core/terraform/resource_processor/vmss_porter/variables.tf @@ -13,6 +13,7 @@ variable "mgmt_resource_group_name" {} variable "terraform_state_container_name" {} variable "app_insights_connection_string" {} variable "key_vault_name" {} +variable "key_vault_id" {} variable "resource_processor_number_processes_per_instance" {} variable "subscription_id" { description = "The subscription id to create the resource processor permission/role. If not supplied will use the TF context." diff --git a/templates/core/terraform/servicebus.tf b/templates/core/terraform/servicebus.tf index 39d12749cb..0ff6f6913d 100644 --- a/templates/core/terraform/servicebus.tf +++ b/templates/core/terraform/servicebus.tf @@ -4,6 +4,7 @@ resource "azurerm_servicebus_namespace" "sb" { resource_group_name = azurerm_resource_group.core.name sku = "Premium" capacity = "1" + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -30,7 +31,7 @@ resource "azurerm_servicebus_queue" "service_bus_deployment_status_update_queue" resource "azurerm_private_dns_zone" "servicebus" { name = "privatelink.servicebus.windows.net" resource_group_name = azurerm_resource_group.core.name - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -39,6 +40,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "servicebuslink" { resource_group_name = azurerm_resource_group.core.name private_dns_zone_name = azurerm_private_dns_zone.servicebus.name virtual_network_id = module.network.core_vnet_id + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -48,6 +50,7 @@ resource "azurerm_private_endpoint" "sbpe" { location = azurerm_resource_group.core.location resource_group_name = azurerm_resource_group.core.name subnet_id = module.network.resource_processor_subnet_id + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } diff --git a/templates/core/terraform/statestore.tf b/templates/core/terraform/statestore.tf index 1f3fb80e76..7c2d1a7c3c 100644 --- a/templates/core/terraform/statestore.tf +++ b/templates/core/terraform/statestore.tf @@ -6,6 +6,7 @@ resource "azurerm_cosmosdb_account" "tre-db-account" { kind = "GlobalDocumentDB" enable_automatic_failover = false ip_range_filter = var.enable_local_debugging ? local.myip : null + tags = local.tre_core_tags consistency_policy { consistency_level = "BoundedStaleness" @@ -39,7 +40,7 @@ resource "azurerm_management_lock" "tre-db" { resource "azurerm_private_dns_zone" "cosmos" { name = "privatelink.documents.azure.com" resource_group_name = azurerm_resource_group.core.name - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -48,7 +49,7 @@ resource "azurerm_private_dns_zone_virtual_network_link" "cosmos_documents_dns_l resource_group_name = azurerm_resource_group.core.name private_dns_zone_name = azurerm_private_dns_zone.cosmos.name virtual_network_id = module.network.core_vnet_id - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -57,7 +58,7 @@ resource "azurerm_private_endpoint" "sspe" { location = azurerm_resource_group.core.location resource_group_name = azurerm_resource_group.core.name subnet_id = module.network.shared_subnet_id - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } private_dns_zone_group { diff --git a/templates/core/terraform/storage.tf b/templates/core/terraform/storage.tf index 2460f534ec..0a28533cf9 100644 --- a/templates/core/terraform/storage.tf +++ b/templates/core/terraform/storage.tf @@ -4,7 +4,7 @@ resource "azurerm_storage_account" "stg" { location = azurerm_resource_group.core.location account_tier = "Standard" account_replication_type = "LRS" - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } } @@ -28,7 +28,7 @@ resource "azurerm_private_endpoint" "blobpe" { location = azurerm_resource_group.core.location resource_group_name = azurerm_resource_group.core.name subnet_id = module.network.shared_subnet_id - + tags = local.tre_core_tags lifecycle { ignore_changes = [tags] } private_dns_zone_group { diff --git a/templates/shared_services/certs/.dockerignore b/templates/shared_services/certs/.dockerignore new file mode 100644 index 0000000000..852f29463e --- /dev/null +++ b/templates/shared_services/certs/.dockerignore @@ -0,0 +1,7 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Put files here that you don't want copied into your bundle's invocation image +.gitignore +**/.terraform/* +**/.terraform.lock.hcl +**/*_backend.tf +Dockerfile.tmpl diff --git a/templates/shared_services/certs/.gitignore b/templates/shared_services/certs/.gitignore new file mode 100644 index 0000000000..73a68e4976 --- /dev/null +++ b/templates/shared_services/certs/.gitignore @@ -0,0 +1,2 @@ +.cnab/ +.terraform* diff --git a/templates/shared_services/certs/Dockerfile.tmpl b/templates/shared_services/certs/Dockerfile.tmpl new file mode 100644 index 0000000000..d7e1a1f563 --- /dev/null +++ b/templates/shared_services/certs/Dockerfile.tmpl @@ -0,0 +1,26 @@ +FROM python:3.8 + +ARG BUNDLE_DIR + +RUN apt-get update \ + && apt-get install -y ca-certificates \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +# Install Azure CLI +RUN apt-get update \ + && apt-get install -y ca-certificates jq curl apt-transport-https lsb-release gnupg \ + && curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null \ + && AZ_REPO=$(lsb_release -cs) \ + && echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" | tee /etc/apt/sources.list.d/azure-cli.list \ + && apt-get update && apt-get -y install azure-cli \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +# Install Certbot +RUN apt-get update && apt-get install -y python3 python3-venv libaugeas0 \ + && python3 -m venv /opt/certbot/ \ + && /opt/certbot/bin/pip install --no-cache-dir --upgrade pip \ + && /opt/certbot/bin/pip install --no-cache-dir certbot \ + && apt-get clean -y && rm -rf /var/lib/apt/lists/* + +# Use the BUNDLE_DIR build argument to copy files into the bundle +COPY . $BUNDLE_DIR diff --git a/templates/shared_services/certs/parameters.json b/templates/shared_services/certs/parameters.json new file mode 100755 index 0000000000..7c41929a0a --- /dev/null +++ b/templates/shared_services/certs/parameters.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": "1.0.0-DRAFT", + "name": "base", + "created": "2021-06-04T13:37:29.5071039+03:00", + "modified": "2021-06-04T13:37:29.5071039+03:00", + "parameters": [ + { + "name": "tre_id", + "source": { + "env": "TRE_ID" + } + }, + { + "name": "azure_location", + "source": { + "env": "LOCATION" + } + }, + { + "name": "tfstate_container_name", + "source": { + "env": "TERRAFORM_STATE_CONTAINER_NAME" + } + }, + { + "name": "tfstate_resource_group_name", + "source": { + "env": "MGMT_RESOURCE_GROUP_NAME" + } + }, + { + "name": "tfstate_storage_account_name", + "source": { + "env": "MGMT_STORAGE_ACCOUNT_NAME" + } + } + ] +} diff --git a/templates/shared_services/certs/porter.yaml b/templates/shared_services/certs/porter.yaml new file mode 100755 index 0000000000..8e5664472f --- /dev/null +++ b/templates/shared_services/certs/porter.yaml @@ -0,0 +1,137 @@ +--- +name: tre-shared-service-certs +version: 0.0.11 +description: "An Azure TRE shared service to generate certificates for a specified internal domain using Letsencrypt" +registry: azuretre +dockerfile: Dockerfile.tmpl + +credentials: + - name: azure_tenant_id + env: ARM_TENANT_ID + - name: azure_subscription_id + env: ARM_SUBSCRIPTION_ID + - name: azure_client_id + env: ARM_CLIENT_ID + - name: azure_client_secret + env: ARM_CLIENT_SECRET + +parameters: + - name: tre_id + type: string + description: "The ID of the parent TRE instance e.g., mytre-dev-3142" + - name: tfstate_resource_group_name + type: string + description: "Resource group containing the Terraform state storage account" + - name: tfstate_storage_account_name + type: string + description: "The name of the Terraform state storage account" + - name: tfstate_container_name + type: string + default: "tfstate" + description: "The name of the Terraform state storage container" + - name: arm_use_msi + env: ARM_USE_MSI + type: boolean + default: false + - name: domain_prefix + type: string + description: "The FQDN prefix (prepended to {TRE_ID}.{LOCATION}.cloudapp.azure.com) to generate certificate for" + - name: cert_name + type: string + description: "What to call the certificate exported to KeyVault (alphanumeric and '-' only)" + +mixins: + - exec + - terraform: + clientVersion: 1.1.5 + - az + +install: + - terraform: + description: "Deploy shared service" + input: false + vars: + tre_id: "{{ bundle.parameters.tre_id }}" + arm_tenant_id: "{{ bundle.credentials.azure_tenant_id }}" + arm_client_id: "{{ bundle.credentials.azure_client_id }}" + arm_client_secret: "{{ bundle.credentials.azure_client_secret }}" + arm_use_msi: "{{ bundle.parameters.arm_use_msi }}" + domain_prefix: "{{ bundle.parameters.domain_prefix }}" + cert_name: "{{ bundle.parameters.cert_name }}" + backendConfig: + resource_group_name: + "{{ bundle.parameters.tfstate_resource_group_name }}" + storage_account_name: + "{{ bundle.parameters.tfstate_storage_account_name }}" + container_name: + "{{ bundle.parameters.tfstate_container_name }}" + key: + "{{ bundle.parameters.tre_id }}-shared-service-certs" + +upgrade: + - exec: + description: "Upgrade shared service" + command: echo + arguments: + - "This shared service does not implement upgrade action" + +uninstall: + - terraform: + description: "Tear down shared service" + input: false + vars: + tre_id: "{{ bundle.parameters.tre_id }}" + arm_tenant_id: "{{ bundle.credentials.azure_tenant_id }}" + arm_client_id: "{{ bundle.credentials.azure_client_id }}" + arm_client_secret: "{{ bundle.credentials.azure_client_secret }}" + arm_use_msi: "{{ bundle.parameters.arm_use_msi }}" + domain_prefix: "{{ bundle.parameters.domain_prefix }}" + cert_name: "{{ bundle.parameters.cert_name }}" + backendConfig: + resource_group_name: + "{{ bundle.parameters.tfstate_resource_group_name }}" + storage_account_name: + "{{ bundle.parameters.tfstate_storage_account_name }}" + container_name: + "{{ bundle.parameters.tfstate_container_name }}" + key: + "{{ bundle.parameters.tre_id }}-shared-service-certs" + +generate: + - terraform: + arguments: + - "output" + description: "Get Terraform output variables" + backendConfig: + resource_group_name: + "{{ bundle.parameters.tfstate_resource_group_name }}" + storage_account_name: + "{{ bundle.parameters.tfstate_storage_account_name }}" + container_name: "{{ bundle.parameters.tfstate_container_name }}" + key: "{{ bundle.parameters.tre_id }}-shared-service-certs" + outputs: + - name: fqdn + - name: application_gateway_name + - name: storage_account_name + - name: resource_group_name + - name: keyvault_name + - az: + description: + "Login to Azure" + arguments: + - login + flags: + identity: + username: "{{ bundle.credentials.azure_client_id }}" + - exec: + description: "Generate/renew certificate" + command: bash + arguments: + - ./scripts/letsencrypt.sh + flags: + fqdn: "{{ bundle.outputs.fqdn }}" + application_gateway_name: "{{ bundle.outputs.application_gateway_name }}" + storage_account_name: "{{ bundle.outputs.storage_account_name }}" + resource_group_name: "{{ bundle.outputs.resource_group_name }}" + keyvault_name: "{{ bundle.outputs.keyvault_name }}" + cert_name: "{{ bundle.parameters.cert_name }}" diff --git a/templates/shared_services/certs/scripts/auth-hook.sh b/templates/shared_services/certs/scripts/auth-hook.sh new file mode 100755 index 0000000000..e25a0eea30 --- /dev/null +++ b/templates/shared_services/certs/scripts/auth-hook.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +cat << EOF > 'validation.txt' +${CERTBOT_VALIDATION} +EOF + +# shellcheck disable=SC2016 +az storage blob upload \ + --account-name "${STORAGE_ACCOUNT_NAME}" \ + --auth-mode login \ + --container-name '$web' \ + --file 'validation.txt' \ + --name ".well-known/acme-challenge/${CERTBOT_TOKEN}" \ + --no-progress \ + --only-show-errors + +sleep 10s diff --git a/templates/shared_services/certs/scripts/cleanup-hook.sh b/templates/shared_services/certs/scripts/cleanup-hook.sh new file mode 100755 index 0000000000..a9bf588e2f --- /dev/null +++ b/templates/shared_services/certs/scripts/cleanup-hook.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/templates/shared_services/certs/scripts/letsencrypt.sh b/templates/shared_services/certs/scripts/letsencrypt.sh new file mode 100755 index 0000000000..3ffd9db524 --- /dev/null +++ b/templates/shared_services/certs/scripts/letsencrypt.sh @@ -0,0 +1,148 @@ +#!/bin/bash +set -e + +script_dir=$(realpath "$(dirname "${BASH_SOURCE[0]}")") + +while [ "$1" != "" ]; do + case $1 in + --storage_account_name) + shift + storage_account_name=$1 + ;; + --fqdn) + shift + fqdn=$1 + ;; + --keyvault_name) + shift + keyvault_name=$1 + ;; + --resource_group_name) + shift + resource_group_name=$1 + ;; + --application_gateway_name) + shift + application_gateway_name=$1 + ;; + --cert_name) + shift + cert_name=$1 + ;; + *) + echo "Unexpected argument: '$1'" + usage + ;; + esac + + if [[ -z "$2" ]]; then + # if no more args then stop processing + break + fi + + shift # remove the current value for `$1` and use the next +done + +# done with processing args and can set this +set -o nounset + +# Start the Application Gateway if stopped +echo "Checking app gateway status" +if [[ $(az network application-gateway list --output json --query "[?resourceGroup=='rg-${TRE_ID}'&&name=='agw-certs-${TRE_ID}'&&operationalState=='Stopped'] | length(@)") != 0 ]]; then + echo "App gateway stopped. Starting..." + az network application-gateway start -g "rg-$TRE_ID" -n "agw-certs-$TRE_ID" +else + echo "App gateway running" +fi + +echo "Checking for index.html file in storage account" + +# Create the default index.html page +cat << EOF > index.html + + +EOF + +# shellcheck disable=SC2016 +indexExists=$(az storage blob list -o json \ + --account-name "${storage_account_name}" \ + --auth-mode login \ + --container-name '$web' \ + --query "[?name=='index.html'].name" \ + | jq 'length') + +if [[ ${indexExists} -lt 1 ]]; then + echo "No existing file found. Uploading index.html file" + + # shellcheck disable=SC2016 + az storage blob upload \ + --account-name "${storage_account_name}" \ + --auth-mode login \ + --container-name '$web' \ + --file index.html \ + --name index.html \ + --no-progress \ + --only-show-errors + + # Wait a bit for the App Gateway health probe to notice + echo "Waiting 30s for app gateway health probe" + sleep 30s +else + echo "index.html already present" +fi + +ledir="${script_dir}/../letsencrypt" +mkdir -p "${ledir}/logs" + +# Initiate the ACME challange +echo "Initiating ACME challenge" +export STORAGE_ACCOUNT_NAME="${storage_account_name}" +/opt/certbot/bin/certbot certonly \ + --config-dir "${ledir}" \ + --work-dir "${ledir}" \ + --logs-dir "${ledir}"/logs \ + --manual \ + --preferred-challenges=http \ + --manual-auth-hook "${script_dir}"/auth-hook.sh \ + --manual-cleanup-hook "${script_dir}"/cleanup-hook.sh \ + --domain "${fqdn}" \ + --non-interactive \ + --agree-tos \ + --register-unsafely-without-email + + +# Convert the generated certificate to a .pfx +echo "Got cert. Converting to PFX" +CERT_DIR="${ledir}/live/${fqdn}" +CERT_PASSWORD=$(openssl rand -base64 30) +openssl pkcs12 -export \ + -inkey "${CERT_DIR}/privkey.pem" \ + -in "${CERT_DIR}/fullchain.pem" \ + -out "${CERT_DIR}/aci.pfx" \ + -passout "pass:${CERT_PASSWORD}" + +# Save cert and password to KeyVault +echo "Importing cert to KeyVault ${keyvault_name}" +sid=$(az keyvault certificate import \ + -o json \ + --vault-name "${keyvault_name}" \ + --name "${cert_name}" \ + --file "${CERT_DIR}/aci.pfx" \ + --password "${CERT_PASSWORD}" \ + | jq -r '.sid') + +echo "Saving certificate password to KV with key ${cert_name}-password" +az keyvault secret set --name "${cert_name}"-password \ + --vault-name "${keyvault_name}" \ + --value "${CERT_PASSWORD}" + +echo "Updating SSL cert in app gateway" +az network application-gateway ssl-cert update \ + --resource-group "${resource_group_name}" \ + --gateway-name "${application_gateway_name}" \ + --name 'cert-primary' \ + --key-vault-secret-id "${sid}" + +# Stop the app gateway once done to save cost +echo "Stopping app gateway" +az network application-gateway stop -g "rg-$TRE_ID" -n "agw-certs-$TRE_ID" diff --git a/templates/shared_services/certs/template_schema.json b/templates/shared_services/certs/template_schema.json new file mode 100644 index 0000000000..6a10beb70d --- /dev/null +++ b/templates/shared_services/certs/template_schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://github.com/microsoft/AzureTRE/templates/shared_services/certs/template_schema.json", + "type": "object", + "title": "Certs Service", + "description": "Provides SSL Certs for a specified internal domain", + "required": [ + "domain_prefix", + "cert_name" + ], + "properties": { + "domain_prefix": { + "$id": "#/properties/domain_prefix", + "type": "string", + "title": "Domain prefix", + "description": "The FQDN prefix (which will be prepended to {TRE_ID}.{LOCATION}.cloudapp.azure.com) to generate a certificate for" + }, + "cert_name": { + "$id": "#/properties/cert_name", + "type": "string", + "title": "Cert name", + "description": "What to call the certificate that's exported to KeyVault (alphanumeric and '-' only)" + } + } +} diff --git a/templates/shared_services/certs/terraform/appgateway.tf b/templates/shared_services/certs/terraform/appgateway.tf new file mode 100644 index 0000000000..d8cb701a5d --- /dev/null +++ b/templates/shared_services/certs/terraform/appgateway.tf @@ -0,0 +1,192 @@ +resource "null_resource" "az_login_sp" { + count = var.arm_use_msi == true ? 0 : 1 + provisioner "local-exec" { + command = "az login --service-principal --username ${var.arm_client_id} --password ${var.arm_client_secret} --tenant ${var.arm_tenant_id}" + } + + triggers = { + timestamp = timestamp() + } +} + +resource "null_resource" "az_login_msi" { + count = var.arm_use_msi == true ? 1 : 0 + provisioner "local-exec" { + command = "az login --identity -u '${data.azurerm_client_config.current.client_id}'" + } + + triggers = { + timestamp = timestamp() + } +} + +resource "azurerm_public_ip" "appgwpip" { + name = "pip-cert-${var.domain_prefix}-${var.tre_id}" + resource_group_name = data.azurerm_resource_group.rg.name + location = data.azurerm_resource_group.rg.location + allocation_method = "Static" + sku = "Standard" + domain_name_label = "${var.domain_prefix}-${var.tre_id}" + + lifecycle { ignore_changes = [tags, zones] } +} + +resource "azurerm_user_assigned_identity" "agw_id" { + resource_group_name = data.azurerm_resource_group.rg.name + location = data.azurerm_resource_group.rg.location + name = "id-agw-certs-${var.tre_id}" + + lifecycle { ignore_changes = [tags] } +} + +resource "azurerm_application_gateway" "agw" { + name = "agw-certs-${var.tre_id}" + resource_group_name = data.azurerm_resource_group.rg.name + location = data.azurerm_resource_group.rg.location + + sku { + name = "Standard_v2" + tier = "Standard_v2" + capacity = 1 + } + + # User-assign managed identify id required to access certificate in KeyVault + identity { + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.agw_id.id] + } + + # Internal subnet for gateway backend. + gateway_ip_configuration { + name = "gateway-ip-configuration" + subnet_id = data.azurerm_subnet.app_gw_subnet.id + } + + # HTTP Port + frontend_port { + name = local.insecure_frontend_port_name + port = 80 + } + + # HTTPS Port + frontend_port { + name = local.secure_frontend_port_name + port = 443 + } + + # Public front-end + frontend_ip_configuration { + name = local.frontend_ip_configuration_name + public_ip_address_id = azurerm_public_ip.appgwpip.id + } + + # Primary SSL cert linked to KeyVault + ssl_certificate { + name = "cert-primary" + key_vault_secret_id = azurerm_key_vault_certificate.tlscert.secret_id + } + + # Backend pool with the static website in storage account. + backend_address_pool { + name = local.staticweb_backend_pool_name + fqdns = [azurerm_storage_account.staticweb.primary_web_host] + } + + # Backend settings for static web. + backend_http_settings { + name = local.staticweb_http_setting_name + cookie_based_affinity = "Disabled" + port = 443 + protocol = "Https" + request_timeout = 60 + pick_host_name_from_backend_address = true + } + + # Public HTTPS listener + http_listener { + name = local.secure_listener_name + frontend_ip_configuration_name = local.frontend_ip_configuration_name + frontend_port_name = local.secure_frontend_port_name + protocol = "Https" + ssl_certificate_name = "cert-primary" + } + + # Public HTTP listener + http_listener { + name = local.insecure_listener_name + frontend_ip_configuration_name = local.frontend_ip_configuration_name + frontend_port_name = local.insecure_frontend_port_name + protocol = "Http" + } + + request_routing_rule { + name = local.request_routing_rule_name + rule_type = "PathBasedRouting" + http_listener_name = local.secure_listener_name + url_path_map_name = local.app_path_map_name + } + + # Routing rule to redirect non-secure traffic to HTTPS + request_routing_rule { + name = local.redirect_request_routing_rule_name + rule_type = "PathBasedRouting" + http_listener_name = local.insecure_listener_name + url_path_map_name = local.redirect_path_map_name + } + + # Default traffic is routed to the static website. + url_path_map { + name = local.app_path_map_name + default_backend_address_pool_name = local.staticweb_backend_pool_name + default_backend_http_settings_name = local.staticweb_http_setting_name + + path_rule { + name = "all" + paths = ["/*"] + backend_address_pool_name = local.staticweb_backend_pool_name + backend_http_settings_name = local.staticweb_http_setting_name + } + } + + # Redirect any HTTP traffic to HTTPS unless its the ACME challenge path used for LetsEncrypt validation. + url_path_map { + name = local.redirect_path_map_name + default_redirect_configuration_name = local.redirect_configuration_name + + path_rule { + name = "acme" + paths = ["/.well-known/acme-challenge/*"] + backend_address_pool_name = local.staticweb_backend_pool_name + backend_http_settings_name = local.staticweb_http_setting_name + } + } + + # Redirect to HTTPS + redirect_configuration { + name = local.redirect_configuration_name + redirect_type = "Permanent" + target_listener_name = local.secure_listener_name + include_path = true + include_query_string = true + } + + # We don't want Terraform to revert certificate cycle changes. We assume the certificate will be renewed in keyvault. + lifecycle { + ignore_changes = [ + ssl_certificate, + tags + ] + } + + depends_on = [ + azurerm_key_vault_access_policy.app_gw_managed_identity, + null_resource.az_login_sp, + null_resource.az_login_msi + ] + + # Stop app gateway once provisioned to save cost until the generate custom action is invoked (which will start/stop as required) + provisioner "local-exec" { + command = "az network application-gateway stop -g ${data.azurerm_resource_group.rg.name} -n agw-certs-${var.tre_id}" + } + +} diff --git a/templates/shared_services/certs/terraform/certificate.tf b/templates/shared_services/certs/terraform/certificate.tf new file mode 100644 index 0000000000..44d39980c2 --- /dev/null +++ b/templates/shared_services/certs/terraform/certificate.tf @@ -0,0 +1,40 @@ +resource "azurerm_key_vault_access_policy" "app_gw_managed_identity" { + key_vault_id = data.azurerm_key_vault.key_vault.id + tenant_id = azurerm_user_assigned_identity.agw_id.tenant_id + object_id = azurerm_user_assigned_identity.agw_id.principal_id + + key_permissions = ["Get"] + secret_permissions = ["Get"] +} + +resource "azurerm_key_vault_certificate" "tlscert" { + name = var.cert_name + key_vault_id = data.azurerm_key_vault.key_vault.id + + # This is a temporary self-signed cert for CN=temp + certificate { + contents = "MIIKOgIBAzCCCfYGCSqGSIb3DQEHAaCCCecEggnjMIIJ3zCCBgAGCSqGSIb3DQEHAaCCBfEEggXtMIIF6TCCBeUGCyqGSIb3DQEMCgECoIIE/jCCBPowHAYKKoZIhvcNAQwBAzAOBAiyQRLJomrNiwICB9AEggTYT6yfTxjl0APqhcP+C2qQp/NbcyJfgpSJf6EEnWEcOuxXdaipNsClUyzaJ8B+v7GCOwjkoMqY/jgO4o8d5SlZbpu+yZ3ItTWQlPbFK83uwn8kTxZzhScEujsV2txPNxe5z/kyrrgNyvRlOcmasBhN24hrv0SkuGJHu31xc+5I7AJjSbblp8ccDQgJHjHAf1oMxz05ZhQazfRZU6WElDvvMP5ZGybxGpyksw4/A9GZz0XiMiJha3bJqpyAeZ/HXYucgnO5/ztOnCNgQC+qaQdOxSKLub0aFwfnnRZtfdRE4JcFRMr4ONGaUOR59OB6MYBZmk+X1sPFar/f+zNA4j6/yhTX6gf0EbjPjmDu1WP02qiJ9jTZv0pD07J98eHU58iir7i/E5eyJ/8VYpinBJC4OCwyGyDWysv1XY7TnZmT9Vwu27I/1ONseY1wbwXarqixXJVhUXLHsMwluNAGUNigMZqMm+3UEsam0Q2ie5FPCWusLBaASvzNL8t9oyLEhJ48Op/7fcX3/OXb8EIceEpNCBZD0nRLsxWA03aO/+vp4iGKhxlzsTx7YqLKFU7ADxsZChYgzLIHAVEfrxPIRWYUIqzbSWutCswMdvw2iZRfWHDpz1jvNJ0aErRyDCQRimjm8kLTzaDTuAscmefmG1cx465I7olRpeVv96i3tBbf0xrKcqjQuz4yepAzvFcPv48GTrM6thngUdjApcPgZScsAv71V2WvW3mQF/TywJlvOSdsG5Hr6bH6jD6PEBkgwBtjZpKokzoWNgw3qpeqiGRdcpYQRv4qrad70Lk99wcaM1wQ5AfZDUTx+PWVlHL/+ikTO+i0UgXoQHp9JFG9vhRdT82tgNKEcFXWWng1+Vlpqk3l5XLi1MZWmBGMPNRb4uqVgH4ekWEvdZEyClbrIGOUe6zPntMsrDXh0M+zi9jqVrB88gLXgTw1UweGwR/xOlRZa/bBg4RwDzcePABia3A4ksREIatZ5rJFipTc6edeaQWFT6V5Q7nqv1Zzoxp3qV7zhc9eCkReOQCTs8Ds8uALP/szLIdT7vPIslme6PTX1mzjBWMnDhoca8xGvEPGBMgxY9kozQXwlgp2JdXw5JuW99rEBkpGuEtDu+yyjiBz80EKIi7kN8VltxAfZsOOt5KpHGleAdkhQifO8c92RC4oivzOREUKtD9cusqjdMrbXHWaJ9mBKbwdvR3SqqGGgxKC3I0nX7jYbHAC7HZTkq7Nuh5QcQmqjaAuOozLUiiv3eVO3FGMmSNFkXtf96NAE+66lx6q8PSXuOJl/EGNS94d5xNyLupqLljFgAwtPRtX/V01BqfuKgwqVAfwP2knBHhJ/CUi0wdG6Ev8YTPnQ+3OG2b0NGM7le6Qi9j07qgpWO2s0xS0jt3FGJeJi/LKxHp7A/7asKp5Evzrw+Nejfbit3EL8fml8DjLDZS7D/6HWYiVEqW85IDf2+iWOGztZIczeHYQqX7t7POv3G22x+PyXHCPSOF4F40sk0Mzw3FaWyow7KDpc0p3u1h/GuLKXgiP0ixNcmCCBdmHfYF/uCiaVoQGeh1rg08tnATdDOozBwaSB1jhiMn3DADy/7kGcej3Da+7elnY4ivWzWZEtv465eDrruxsalcTdbitNOUiPJM/Mos/bjwvlDGB0zATBgkqhkiG9w0BCRUxBgQEAQAAADBdBgkqhkiG9w0BCRQxUB5OAHQAZQAtADAAOQA4ADIANAA5AGUAZQAtADkAZgAxADEALQA0AGUAZQBjAC0AYQAxADYAMwAtAGMAZgBiAGMAZgBhADMANwA5ADQANQBiMF0GCSsGAQQBgjcRATFQHk4ATQBpAGMAcgBvAHMAbwBmAHQAIABTAG8AZgB0AHcAYQByAGUAIABLAGUAeQAgAFMAdABvAHIAYQBnAGUAIABQAHIAbwB2AGkAZABlAHIwggPXBgkqhkiG9w0BBwagggPIMIIDxAIBADCCA70GCSqGSIb3DQEHATAcBgoqhkiG9w0BDAEDMA4ECM9JyfoDC5veAgIH0ICCA5BHYKLRmWNcYkQVs7Hn/BAw9uTJ+kzH9ZkjwgoRl0aSanS0TIOH+gKwjJgy4YIenm2H1KXoNhDBqqxXDvBIF1K5JP7OsQMCiViU8pdL8GmC5NOE4SPokIRe+2fUQBHtzaZx4GtkwpHq1Tlsv+2U9EmeuQTBX2C1Amj9Dup2uwYrGfSx6bVoIP8yEzB29f/3gxSmNLQAvlXjO5hdYZM6dDRX+SaYmIJHPg8SYG6EuzaJ2a++f2qJ1f7XtgB/Yt9/KPP4AnPTaEQMyR08CyJTf2MxAOBHHza+x4SqaWOEzL/SeOTalaQTY5vj7c6LkgYIXiyYi1QK3VHeJ+NYPBFgu4+B0Sk1ef8jZ6XeSnsxXj6lhLb8sUiemwsEfueh3RKpOhzQEZDRVYP6B/k3Nv9JwuOUiSWdgPGkhyTWu+xOD4+bixCMUJ0AI+MLEjI+ixkP429Ylp9nxyWxPUTG+PW2AR/euuCW222yEjzJzz0VPJXWeUWVyRzoYicrBWEvK3/KnnkWafYti2Ugsee5gCMCmeKeFKoCKEwdMzdAfafC3FI6x2LnUGNefv6E3hIzw6v0vYlEUmmgb5aI/zjCCXix4lSuqYtvfwtLZIf4aEu1OoCRE35zkHlIUh2Cf5kERPawNM6V/qnKEV/SsQQyNtOI2iUi11cBHkGwOxDzBiWkidhOZ8UB9r+453G8zWNWwXWdhxoYHF1CYDtjy2lrzw+ou5jW0yI1G82a2UpZkPMtMoXTuoErVgAfW2jZ894vFEsIqeny1FVoQtpKhmdX1hatxR3AyRYR2L9tdD1BcGMll6FkgJjqYNcV8mbZzLzFtYNoSljGGOr46xhW4Bfv1jPw2eKXJULMWwWxaHq4rTSbWq5q5eElM0OkPmAyrqoVbd8uwATmIqtwOgmJqeIh0HgmkuaJfDMSfOTBnamS2B9Fl01TvNV5EWmMC1qROBW8yi/7meEPmDyfWOHz5v6ltG5Ra5xC0UHv/ljYpce1kWKPTlGUEknhZwAbeyDpuy6ljneGtSdQPz9uqrzyMnDWGEguVQYubmxVbXICwzGI06UBB9JVRuXPuUHj3j0rSLny/9/A1sUb2kwXNhVlOQGMDWW/kcY5SsWqTb+z+rXL90aaaWmc6Mu0tJv/HZMraJ3KDR6Y1WXUUzYHBegMtwOWTtyVwb0b8W2Oo0c0g3t1dNcVY/nYbikx/lgC4Xanco7+i4gCVA0wOzAfMAcGBSsOAwIaBBR2TY2pU6UZFCDZUtZIO6qHyC3VSQQUogR884dqPwcCWXjPjzBB832hmIkCAgfQ" + password = "0000000000" + } + + certificate_policy { + issuer_parameters { + name = "Self" + } + key_properties { + key_size = 2048 + exportable = true + key_type = "RSA" + reuse_key = false + } + secret_properties { + content_type = "application/x-pkcs12" + } + } + + # The certificate will get replaced with a real one, so we don't want Terraform to try and revert it. + lifecycle { + ignore_changes = all + } + +} diff --git a/templates/shared_services/certs/terraform/data.tf b/templates/shared_services/certs/terraform/data.tf new file mode 100644 index 0000000000..9de429226f --- /dev/null +++ b/templates/shared_services/certs/terraform/data.tf @@ -0,0 +1,32 @@ +data "azurerm_client_config" "current" {} + +data "azurerm_resource_group" "rg" { + name = "rg-${var.tre_id}" +} + +data "azurerm_key_vault" "key_vault" { + name = "kv-${var.tre_id}" + resource_group_name = data.azurerm_resource_group.rg.name +} + +data "azurerm_subnet" "app_gw_subnet" { + name = "AppGwSubnet" + virtual_network_name = "vnet-${var.tre_id}" + resource_group_name = data.azurerm_resource_group.rg.name +} + +data "azurerm_subnet" "resource_processor" { + name = "ResourceProcessorSubnet" + virtual_network_name = "vnet-${var.tre_id}" + resource_group_name = data.azurerm_resource_group.rg.name +} + +data "azurerm_firewall" "fw" { + name = "fw-${var.tre_id}" + resource_group_name = data.azurerm_resource_group.rg.name +} + +data "azurerm_user_assigned_identity" "resource_processor_vmss_id" { + name = "id-vmss-${var.tre_id}" + resource_group_name = "rg-${var.tre_id}" +} diff --git a/templates/shared_services/certs/terraform/firewall.tf b/templates/shared_services/certs/terraform/firewall.tf new file mode 100644 index 0000000000..b6a30f0023 --- /dev/null +++ b/templates/shared_services/certs/terraform/firewall.tf @@ -0,0 +1,25 @@ +resource "azurerm_firewall_application_rule_collection" "resource_processor_letsencrypt" { + name = "resource_processor_subnet_letsencrypt" + azure_firewall_name = data.azurerm_firewall.fw.name + resource_group_name = data.azurerm_firewall.fw.resource_group_name + priority = 601 + action = "Allow" + + rule { + name = "letsencrypt-acme" + protocol { + port = "443" + type = "Https" + } + protocol { + port = "80" + type = "Http" + } + + target_fqdns = [ + "acme-v02.api.letsencrypt.org" + ] + + source_addresses = data.azurerm_subnet.resource_processor.address_prefixes + } +} diff --git a/templates/shared_services/certs/terraform/locals.tf b/templates/shared_services/certs/terraform/locals.tf new file mode 100644 index 0000000000..3ea8558a13 --- /dev/null +++ b/templates/shared_services/certs/terraform/locals.tf @@ -0,0 +1,21 @@ +locals { + staticweb_storage_name = lower(replace("stwebcerts${var.tre_id}", "-", "")) + + staticweb_backend_pool_name = "beap-certs-staticweb" + app_path_map_name = "upm-certs" + redirect_path_map_name = "upm-certs-redirect" + + insecure_frontend_port_name = "feport-certs-insecure" + secure_frontend_port_name = "feport-certs-secure" + + frontend_ip_configuration_name = "feip-certs-public" + + staticweb_http_setting_name = "be-htst-certs-staticweb" + + insecure_listener_name = "httplstn-certs-insecure" + secure_listener_name = "httplstn-certs-secure" + + redirect_request_routing_rule_name = "rqrt-certs-redirect" + request_routing_rule_name = "rqrt-certs-application" + redirect_configuration_name = "rdrcfg-certs-tosecure" +} diff --git a/templates/shared_services/certs/terraform/main.tf b/templates/shared_services/certs/terraform/main.tf new file mode 100644 index 0000000000..2ead62214d --- /dev/null +++ b/templates/shared_services/certs/terraform/main.tf @@ -0,0 +1,22 @@ +# Azure Provider source and version being used +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "=3.4.0" + } + } + + backend "azurerm" {} +} + +provider "azurerm" { + features { + key_vault { + # Don't purge secrets on destroy (this would fail due to purge protection being enabled on keyvault) + purge_soft_deleted_secrets_on_destroy = false + # When recreating a shared service, recover any previously soft deleted secrets + recover_soft_deleted_secrets = true + } + } +} diff --git a/templates/shared_services/certs/terraform/output.tf b/templates/shared_services/certs/terraform/output.tf new file mode 100644 index 0000000000..882e91b2da --- /dev/null +++ b/templates/shared_services/certs/terraform/output.tf @@ -0,0 +1,19 @@ +output "fqdn" { + value = azurerm_public_ip.appgwpip.fqdn +} + +output "application_gateway_name" { + value = azurerm_application_gateway.agw.name +} + +output "storage_account_name" { + value = azurerm_storage_account.staticweb.name +} + +output "resource_group_name" { + value = azurerm_application_gateway.agw.resource_group_name +} + +output "keyvault_name" { + value = data.azurerm_key_vault.key_vault.name +} diff --git a/templates/shared_services/certs/terraform/staticweb.tf b/templates/shared_services/certs/terraform/staticweb.tf new file mode 100644 index 0000000000..5b8445c46d --- /dev/null +++ b/templates/shared_services/certs/terraform/staticweb.tf @@ -0,0 +1,27 @@ +# See https://microsoft.github.io/AzureTRE/tre-developers/letsencrypt/ +resource "azurerm_storage_account" "staticweb" { + name = local.staticweb_storage_name + resource_group_name = data.azurerm_resource_group.rg.name + location = data.azurerm_resource_group.rg.location + account_kind = "StorageV2" + account_tier = "Standard" + account_replication_type = "LRS" + enable_https_traffic_only = true + + tags = { + tre_id = var.tre_id + } + + static_website { + index_document = "index.html" + error_404_document = "404.html" + } + + lifecycle { ignore_changes = [tags] } +} + +resource "azurerm_role_assignment" "stgwriter" { + scope = azurerm_storage_account.staticweb.id + role_definition_name = "Storage Blob Data Contributor" + principal_id = data.azurerm_user_assigned_identity.resource_processor_vmss_id.principal_id +} diff --git a/templates/shared_services/certs/terraform/variables.tf b/templates/shared_services/certs/terraform/variables.tf new file mode 100644 index 0000000000..9a493e14a3 --- /dev/null +++ b/templates/shared_services/certs/terraform/variables.tf @@ -0,0 +1,19 @@ +variable "tre_id" { + type = string +} + +variable "arm_use_msi" { + type = bool +} + +variable "arm_tenant_id" {} +variable "arm_client_id" {} +variable "arm_client_secret" {} + +variable "domain_prefix" { + type = string +} + +variable "cert_name" { + type = string +} diff --git a/templates/shared_services/firewall/parameters.json b/templates/shared_services/firewall/parameters.json index a6b6bbf2ab..5e2d9d66ef 100755 --- a/templates/shared_services/firewall/parameters.json +++ b/templates/shared_services/firewall/parameters.json @@ -10,6 +10,12 @@ "env": "TRE_ID" } }, + { + "name": "id", + "source": { + "env": "ID" + } + }, { "name": "tfstate_container_name", "source": { diff --git a/templates/shared_services/firewall/porter.yaml b/templates/shared_services/firewall/porter.yaml index 4b64405355..8a45aa14eb 100644 --- a/templates/shared_services/firewall/porter.yaml +++ b/templates/shared_services/firewall/porter.yaml @@ -1,6 +1,6 @@ --- name: tre-shared-service-firewall -version: 0.0.7 +version: 0.3.0 description: "An Azure TRE Firewall shared service" registry: azuretre dockerfile: Dockerfile.tmpl @@ -19,6 +19,9 @@ parameters: - name: tre_id type: string description: "The ID of the parent TRE instance e.g., mytre-dev-3142" + - name: id + type: string + description: "Resource ID" - name: tfstate_resource_group_name type: string description: "Resource group containing the Terraform state storage account" @@ -37,7 +40,6 @@ parameters: type: string default: "W10=" #b64 for [] - mixins: - exec - terraform: @@ -49,16 +51,13 @@ install: input: false vars: tre_id: "{{ bundle.parameters.tre_id }}" + tre_resource_id: "{{ bundle.parameters.id }}" api_driven_rule_collections_b64: "{{ bundle.parameters.rule_collections }}" backendConfig: - resource_group_name: - "{{ bundle.parameters.tfstate_resource_group_name }}" - storage_account_name: - "{{ bundle.parameters.tfstate_storage_account_name }}" - container_name: - "{{ bundle.parameters.tfstate_container_name }}" - key: - "{{ bundle.parameters.tre_id }}-shared-service-firewall" + resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" + storage_account_name: "{{ bundle.parameters.tfstate_storage_account_name }}" + container_name: "{{ bundle.parameters.tfstate_container_name }}" + key: "{{ bundle.parameters.tre_id }}-shared-service-firewall" upgrade: - terraform: @@ -68,14 +67,10 @@ upgrade: tre_id: "{{ bundle.parameters.tre_id }}" api_driven_rule_collections_b64: "{{ bundle.parameters.rule_collections }}" backendConfig: - resource_group_name: - "{{ bundle.parameters.tfstate_resource_group_name }}" - storage_account_name: - "{{ bundle.parameters.tfstate_storage_account_name }}" - container_name: - "{{ bundle.parameters.tfstate_container_name }}" - key: - "{{ bundle.parameters.tre_id }}-shared-service-firewall" + resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" + storage_account_name: "{{ bundle.parameters.tfstate_storage_account_name }}" + container_name: "{{ bundle.parameters.tfstate_container_name }}" + key: "{{ bundle.parameters.tre_id }}-shared-service-firewall" uninstall: - terraform: @@ -83,13 +78,10 @@ uninstall: input: false vars: tre_id: "{{ bundle.parameters.tre_id }}" + tre_resource_id: "{{ bundle.parameters.id }}" api_driven_rule_collections_b64: "{{ bundle.parameters.rule_collections }}" backendConfig: - resource_group_name: - "{{ bundle.parameters.tfstate_resource_group_name }}" - storage_account_name: - "{{ bundle.parameters.tfstate_storage_account_name }}" - container_name: - "{{ bundle.parameters.tfstate_container_name }}" - key: - "{{ bundle.parameters.tre_id }}-shared-service-firewall" + resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" + storage_account_name: "{{ bundle.parameters.tfstate_storage_account_name }}" + container_name: "{{ bundle.parameters.tfstate_container_name }}" + key: "{{ bundle.parameters.tre_id }}-shared-service-firewall" diff --git a/templates/shared_services/firewall/terraform/firewall.tf b/templates/shared_services/firewall/terraform/firewall.tf index 5fb2df2e02..549b1d4701 100644 --- a/templates/shared_services/firewall/terraform/firewall.tf +++ b/templates/shared_services/firewall/terraform/firewall.tf @@ -5,7 +5,7 @@ resource "azurerm_public_ip" "fwpip" { allocation_method = "Static" sku = "Standard" - lifecycle { ignore_changes = [tags] } + lifecycle { ignore_changes = [tags, zones] } } resource "azurerm_firewall" "fw" { @@ -13,6 +13,8 @@ resource "azurerm_firewall" "fw" { name = "fw-${var.tre_id}" resource_group_name = local.core_resource_group_name location = data.azurerm_resource_group.rg.location + sku_tier = "Standard" + sku_name = "AZFW_VNet" ip_configuration { name = "fw-ip-configuration" subnet_id = data.azurerm_subnet.firewall.id @@ -31,10 +33,10 @@ resource "azurerm_management_lock" "fw" { } resource "azurerm_monitor_diagnostic_setting" "firewall" { - name = "diagnostics-firewall-${var.tre_id}" - target_resource_id = azurerm_firewall.fw.id - log_analytics_workspace_id = data.azurerm_log_analytics_workspace.tre.id - log_analytics_destination_type = "Dedicated" + name = "diagnostics-fw-${var.tre_id}" + target_resource_id = azurerm_firewall.fw.id + log_analytics_workspace_id = data.azurerm_log_analytics_workspace.tre.id + #log_analytics_destination_type = "Dedicated" log { category = "AzureFirewallApplicationRule" diff --git a/templates/shared_services/firewall/terraform/main.tf b/templates/shared_services/firewall/terraform/main.tf index 715a8db036..fa8b6fa244 100644 --- a/templates/shared_services/firewall/terraform/main.tf +++ b/templates/shared_services/firewall/terraform/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "=2.97.0" + version = "=3.5.0" } } diff --git a/templates/shared_services/firewall/terraform/variables.tf b/templates/shared_services/firewall/terraform/variables.tf index 53c9d511c4..c4f1dc6b38 100644 --- a/templates/shared_services/firewall/terraform/variables.tf +++ b/templates/shared_services/firewall/terraform/variables.tf @@ -3,6 +3,11 @@ variable "tre_id" { description = "Unique TRE ID" } +variable "tre_resource_id" { + type = string + description = "Resource ID" +} + variable "stateful_resources_locked" { type = bool default = true diff --git a/templates/shared_services/gitea/parameters.json b/templates/shared_services/gitea/parameters.json index 877de77b84..a10a636db4 100755 --- a/templates/shared_services/gitea/parameters.json +++ b/templates/shared_services/gitea/parameters.json @@ -10,6 +10,12 @@ "env": "TRE_ID" } }, + { + "name": "id", + "source": { + "env": "ID" + } + }, { "name": "acr_name", "source": { diff --git a/templates/shared_services/gitea/porter.yaml b/templates/shared_services/gitea/porter.yaml index 39c3b39254..5ba055a495 100644 --- a/templates/shared_services/gitea/porter.yaml +++ b/templates/shared_services/gitea/porter.yaml @@ -1,6 +1,6 @@ --- name: tre-shared-service-gitea -version: 0.0.5 +version: 0.3.1 description: "A Gitea shared service" registry: azuretre dockerfile: Dockerfile.tmpl @@ -18,6 +18,9 @@ parameters: - name: tre_id type: string description: "The ID of the parent TRE instance e.g., mytre-dev-3142" + - name: id + type: string + description: "Resource ID" - name: mgmt_acr_name type: string description: "The name of the Azure Container Registry" @@ -45,6 +48,7 @@ install: input: false vars: tre_id: "{{ bundle.parameters.tre_id }}" + tre_resource_id: "{{ bundle.parameters.id }}" mgmt_resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" acr_name: "{{ bundle.parameters.mgmt_acr_name }}" @@ -67,6 +71,7 @@ uninstall: input: false vars: tre_id: "{{ bundle.parameters.tre_id }}" + tre_resource_id: "{{ bundle.parameters.id }}" mgmt_resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" acr_name: "{{ bundle.parameters.mgmt_acr_name }}" diff --git a/templates/shared_services/gitea/terraform/.terraform.lock.hcl b/templates/shared_services/gitea/terraform/.terraform.lock.hcl index f12bff178e..e7d57a8fe4 100644 --- a/templates/shared_services/gitea/terraform/.terraform.lock.hcl +++ b/templates/shared_services/gitea/terraform/.terraform.lock.hcl @@ -2,59 +2,59 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/azurerm" { - version = "3.4.0" - constraints = "3.4.0" + version = "3.5.0" + constraints = "3.5.0" hashes = [ - "h1:h78yKGgOFrU/N5ntockxN7XF/ufv47j77+oauO2GKqk=", - "zh:4e9913fc3378436d19150c334e5906eafb83a4af3a270423cb7cdda94b27371f", - "zh:5b3d0cec2a600dc1f6633baa8fc36368c5c330fd7654861edcfa76f760a8f6a9", - "zh:5e0e1f899027bc182f31d996c9611e5ba27a034c848d7b0519b39e559fc4f38d", - "zh:66e3a1383ed6a0370989f6fd6abcfa63ccf6918ae535108595af57b9c20a9257", - "zh:688493baf6a116a399b737d74c11080051aca1ab087e5cddd14cc683b7e45c76", - "zh:9e471d85d52343e3ba778f3a94626d820fbec97bb589a3ac7a6a0939b9387770", - "zh:be1e85635daca1768f26962a4cbbadbf7fd13d9da8f9f188e938beca542c2ad5", - "zh:c00e14b6aa566eb9995cb0e1611a18fb8650d9f35c7636a7643a1b6e22660226", - "zh:c40711e5021838fd879da4c9e6b8f7e72104ada2adf0f3ba22e1cc32c3c54086", - "zh:cc62f8541de8d79577e57664e4f03c1fca893d455e5fb238d20668389c0f09ee", - "zh:cd9cbb5c6e5ceb5fcc7c4d0cab516ff209667d1b539b8c7436bd5e452c6aba8f", + "h1:T4XsCHDT839VehWKdxbVsLn0ECjcQaUTzbSGW055pgM=", + "zh:0d8ae6d6e87f44ed4a178be03d6466339b0bb578ab54c2677e365a8281b0bb7d", + "zh:29d250d1a18d49652b28f234ecd17687b36c875dc47877a678e587d5d136b054", + "zh:2e69ba373cf009e8a60b36d04f3dbc4638708d1bf88be9f96b3e52cbf8f47f31", + "zh:53d525dd84ac63b5f749bfbc6b70a202dacf29597664d2ab1165efea6f24f630", + "zh:a25024d574ccd5ae6c2962f3bb71d510f62899f493b1ed096f2f7f0e2b18f975", + "zh:aabc64fe64319b95aaba1d1866f87abc7b10adae37d2eafa2f85f37317fdd49f", + "zh:acc6a977814897cb23d3b3753213281334238f8bce6d2b21e9f04fc4087ee980", + "zh:b24987e9416c39cd59c0fa41c139a97406b9955f0607fcafbf3315014456338a", + "zh:c550eae45fd32acdbe32b4e5c450ae95df6cb18903ac7216b1b07b23a16ce045", + "zh:c8f83b763b643893dcb6933a6bcee824cb514e06e7e5c5f5ac4ba187e66d7e22", + "zh:dcdac07e7ea18464dea729717870c275de9453775243c231e1fb305cad0ee597", "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } provider "registry.terraform.io/hashicorp/local" { - version = "2.2.2" + version = "2.2.3" hashes = [ - "h1:5UYW2wJ320IggrzLt8tLD6MowePqycWtH1b2RInHZkE=", - "zh:027e4873c69da214e2fed131666d5de92089732a11d096b68257da54d30b6f9d", - "zh:0ba2216e16cfb72538d76a4c4945b4567a76f7edbfef926b1c5a08d7bba2a043", - "zh:1fee8f6aae1833c27caa96e156cf99a681b6f085e476d7e1b77d285e21d182c1", - "zh:2e8a3e72e877003df1c390a231e0d8e827eba9f788606e643f8e061218750360", - "zh:719008f9e262aa1523a6f9132adbe9eee93c648c2981f8359ce41a40e6425433", + "h1:aWp5iSUxBGgPv1UnV5yag9Pb0N+U1I0sZb38AXBFO8A=", + "zh:04f0978bb3e052707b8e82e46780c371ac1c66b689b4a23bbc2f58865ab7d5c0", + "zh:6484f1b3e9e3771eb7cc8e8bab8b35f939a55d550b3f4fb2ab141a24269ee6aa", + "zh:78a56d59a013cb0f7eb1c92815d6eb5cf07f8b5f0ae20b96d049e73db915b238", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:9a70fdbe6ef955c4919a4519caca116f34c19c7ddedd77990fbe4f80fe66dc84", - "zh:abc412423d670cbb6264827fa80e1ffdc4a74aff3f19ba6a239dd87b85b15bec", - "zh:ae953a62c94d2a2a0822e5717fafc54e454af57bd6ed02cd301b9786765c1dd3", - "zh:be0910bdf46698560f9e86f51a4ff795c62c02f8dc82b2b1dab77a0b3a93f61e", - "zh:e58f9083b7971919b95f553227adaa7abe864fce976f0166cf4d65fc17257ff2", - "zh:ff4f77cbdbb22cc98182821c7ef84dce16298ab0e997d5c7fae97247f7a4bcb0", + "zh:8aa9950f4c4db37239bcb62e19910c49e47043f6c8587e5b0396619923657797", + "zh:996beea85f9084a725ff0e6473a4594deb5266727c5f56e9c1c7c62ded6addbb", + "zh:9a7ef7a21f48fabfd145b2e2a4240ca57517ad155017e86a30860d7c0c109de3", + "zh:a63e70ac052aa25120113bcddd50c1f3cfe61f681a93a50cea5595a4b2cc3e1c", + "zh:a6e8d46f94108e049ad85dbed60354236dc0b9b5ec8eabe01c4580280a43d3b8", + "zh:bb112ce7efbfcfa0e65ed97fa245ef348e0fd5bfa5a7e4ab2091a9bd469f0a9e", + "zh:d7bec0da5c094c6955efed100f3fe22fca8866859f87c025be1760feb174d6d9", + "zh:fb9f271b72094d07cef8154cd3d50e9aa818a0ea39130bc193132ad7b23076fd", ] } provider "registry.terraform.io/hashicorp/random" { - version = "3.1.3" + version = "3.2.0" hashes = [ - "h1:nLWniS8xhb32qRQy+n4bDPjQ7YWZPVMR3v1vSrx7QyY=", - "zh:26e07aa32e403303fc212a4367b4d67188ac965c37a9812e07acee1470687a73", - "zh:27386f48e9c9d849fbb5a8828d461fde35e71f6b6c9fc235bc4ae8403eb9c92d", - "zh:5f4edda4c94240297bbd9b83618fd362348cadf6bf24ea65ea0e1844d7ccedc0", - "zh:646313a907126cd5e69f6a9fafe816e9154fccdc04541e06fed02bb3a8fa2d2e", - "zh:7349692932a5d462f8dee1500ab60401594dddb94e9aa6bf6c4c0bd53e91bbb8", + "h1:eeUh6cJ6wKLLuo4q9uQ0CA1Zvfqya4Wn1LecLCN8KKs=", + "zh:2960977ce9a7d6a7d3e934e75ec5814735626f95c186ad95a9102344a1a38ac1", + "zh:2fd012abfabe7076f3f2f402eeef4970e20574d20ffec57c162b02b6e848c32f", + "zh:4cd3234671cf01c913023418b227eb78b0659f2cd2e0b387be1f0bb607d29889", + "zh:52e695b4fa3fae735ffc901edff8183745f980923510a744db7616e8f10dc499", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:9034daba8d9b32b35930d168f363af04cecb153d5849a7e4a5966c97c5dc956e", - "zh:bb81dfca59ef5f949ef39f19ea4f4de25479907abc28cdaa36d12ecd7c0a9699", - "zh:bcf7806b99b4c248439ae02c8e21f77aff9fadbc019ce619b929eef09d1221bb", - "zh:d708e14d169e61f326535dd08eecd3811cd4942555a6f8efabc37dbff9c6fc61", - "zh:dc294e19a46e1cefb9e557a7b789c8dd8f319beca99b8c265181bc633dc434cc", - "zh:f9d758ee53c55dc016dd736427b6b0c3c8eb4d0dbbc785b6a3579b0ffedd9e42", + "zh:848b4a294e5ba15192ee4bfd199c07f60a437d7572efcd2d89db036e1ebc0e6e", + "zh:9d49aa432a05748a9527e95448cebee1238c87c97c7e8dec694bfd709683f9c7", + "zh:b4ad4cf289d3f7408649b74b8639918833613f2a1f3cf51b51f4b2fdaa412dd2", + "zh:c1544c4b416096fb8d8dbf84c4488584a2844a30dd533b957e9e9e60a165f24e", + "zh:dc737d6b4591cad8c9a1d0b347e587e846d8d901789b29b4dd401b6cdf82c017", + "zh:f5645fd39f749dbbf847cbdc87ba0dbd141143f12917a6a8904faf8a9b64111e", + "zh:fdedf610e0d020878a8f1fedda8105e0c33a7e23c4792fca54460685552de308", ] } diff --git a/templates/shared_services/gitea/terraform/gitea-webapp.tf b/templates/shared_services/gitea/terraform/gitea-webapp.tf index 4b5a2df2ec..18527c8675 100644 --- a/templates/shared_services/gitea/terraform/gitea-webapp.tf +++ b/templates/shared_services/gitea/terraform/gitea-webapp.tf @@ -63,25 +63,23 @@ resource "azurerm_app_service" "gitea" { scm_use_main_ip_restriction = true acr_use_managed_identity_credentials = true acr_user_managed_identity_client_id = azurerm_user_assigned_identity.gitea_id.client_id - + ftps_state = "Disabled" + websockets_enabled = false + always_on = true + min_tls_version = "1.2" + vnet_route_all_enabled = true cors { allowed_origins = [] support_credentials = false } - always_on = true - min_tls_version = "1.2" - vnet_route_all_enabled = true - ip_restriction { action = "Deny" ip_address = "0.0.0.0/0" name = "Deny all" priority = 2147483647 } - - websockets_enabled = false } storage_account { diff --git a/templates/shared_services/gitea/terraform/main.tf b/templates/shared_services/gitea/terraform/main.tf index 2ead62214d..42aabdf7d7 100644 --- a/templates/shared_services/gitea/terraform/main.tf +++ b/templates/shared_services/gitea/terraform/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "=3.4.0" + version = "=3.5.0" } } diff --git a/templates/shared_services/gitea/terraform/variables.tf b/templates/shared_services/gitea/terraform/variables.tf index 8f68399d5d..e4b4864fd4 100644 --- a/templates/shared_services/gitea/terraform/variables.tf +++ b/templates/shared_services/gitea/terraform/variables.tf @@ -3,6 +3,11 @@ variable "tre_id" { description = "Unique TRE ID" } +variable "tre_resource_id" { + type = string + description = "Resource ID" +} + variable "gitea_allowed_fqdns" { type = string description = "comma seperated string of allowed FQDNs for Gitea" diff --git a/templates/shared_services/gitea/version.txt b/templates/shared_services/gitea/version.txt index 9cb17e7976..e19434e2e3 100644 --- a/templates/shared_services/gitea/version.txt +++ b/templates/shared_services/gitea/version.txt @@ -1 +1 @@ -__version__ = "0.1.8" +__version__ = "0.3.3" diff --git a/templates/shared_services/sonatype-nexus-vm/.dockerignore b/templates/shared_services/sonatype-nexus-vm/.dockerignore new file mode 100644 index 0000000000..36177107fc --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/.dockerignore @@ -0,0 +1,6 @@ +# See https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Put files here that you don't want copied into your bundle's invocation image +.gitignore +**/.terraform/* +**/*_backend.tf +Dockerfile.tmpl diff --git a/templates/shared_services/sonatype-nexus-vm/.gitignore b/templates/shared_services/sonatype-nexus-vm/.gitignore new file mode 100644 index 0000000000..e08a3e22b9 --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/.gitignore @@ -0,0 +1 @@ +.cnab/ diff --git a/templates/shared_services/sonatype-nexus-vm/Dockerfile.tmpl b/templates/shared_services/sonatype-nexus-vm/Dockerfile.tmpl new file mode 100644 index 0000000000..3f8524ce64 --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/Dockerfile.tmpl @@ -0,0 +1,29 @@ +FROM debian:stretch-slim + +ARG BUNDLE_DIR + +RUN apt-get update && apt-get install -y ca-certificates + +# Install Azure CLI +RUN apt-get update \ + && apt-get install -y ca-certificates jq curl apt-transport-https lsb-release gnupg \ + && curl -sL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | tee /etc/apt/trusted.gpg.d/microsoft.gpg > /dev/null \ + && AZ_REPO=$(lsb_release -cs) \ + && echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $AZ_REPO main" | tee /etc/apt/sources.list.d/azure-cli.list \ + && apt-get update && apt-get -y install azure-cli + +# This is a template Dockerfile for the bundle's invocation image +# You can customize it to use different base images, install tools and copy configuration files. +# +# Porter will use it as a template and append lines to it for the mixins +# and to set the CMD appropriately for the CNAB specification. +# +# Add the following line to porter.yaml to instruct Porter to use this template +# dockerfile: Dockerfile.tmpl + +# You can control where the mixin's Dockerfile lines are inserted into this file by moving "# PORTER_MIXINS" line +# another location in this file. If you remove that line, the mixins generated content is appended to this file. +# PORTER_MIXINS + +# Use the BUNDLE_DIR build argument to copy files into the bundle +COPY . $BUNDLE_DIR diff --git a/templates/shared_services/sonatype-nexus-vm/parameters.json b/templates/shared_services/sonatype-nexus-vm/parameters.json new file mode 100755 index 0000000000..c01ad0266b --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/parameters.json @@ -0,0 +1,32 @@ +{ + "schemaVersion": "1.0.0-DRAFT+TODO", + "name": "base", + "created": "2021-06-04T13:37:29.5071039+03:00", + "modified": "2021-06-04T13:37:29.5071039+03:00", + "parameters": [ + { + "name": "tre_id", + "source": { + "env": "TRE_ID" + } + }, + { + "name": "tfstate_container_name", + "source": { + "env": "TERRAFORM_STATE_CONTAINER_NAME" + } + }, + { + "name": "tfstate_resource_group_name", + "source": { + "env": "MGMT_RESOURCE_GROUP_NAME" + } + }, + { + "name": "tfstate_storage_account_name", + "source": { + "env": "MGMT_STORAGE_ACCOUNT_NAME" + } + } + ] +} diff --git a/templates/shared_services/sonatype-nexus-vm/porter.yaml b/templates/shared_services/sonatype-nexus-vm/porter.yaml new file mode 100644 index 0000000000..5cfc16b8eb --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/porter.yaml @@ -0,0 +1,75 @@ +--- +name: tre-shared-service-sonatype-nexus +version: 2.0.0 +description: "A Sonatype Nexus shared service" +registry: azuretre +credentials: + - name: azure_tenant_id + env: ARM_TENANT_ID + - name: azure_subscription_id + env: ARM_SUBSCRIPTION_ID + - name: azure_client_id + env: ARM_CLIENT_ID + - name: azure_client_secret + env: ARM_CLIENT_SECRET +parameters: + - name: tre_id + type: string + description: "The ID of the parent TRE instance e.g., mytre-dev-3142" + - name: tfstate_resource_group_name + type: string + description: "Resource group containing the Terraform state storage account" + - name: tfstate_storage_account_name + type: string + description: "The name of the Terraform state storage account" + - name: tfstate_container_name + type: string + default: "tfstate" + description: "The name of the Terraform state storage container" + - name: arm_use_msi + env: ARM_USE_MSI + default: false + - name: ssl_cert_name + type: string + default: "nexus-ssl" + description: "Name of the certificate for configuring Nexus SSL with (stored in the core KeyVault)" + +mixins: + - exec + - az + - terraform: + clientVersion: 1.1.5 +install: + - terraform: + description: "Deploy shared service" + input: false + vars: + tre_id: "{{ bundle.parameters.tre_id }}" + ssl_cert_name: "{{ bundle.parameters.ssl_cert_name }}" + backendConfig: + resource_group_name: + "{{ bundle.parameters.tfstate_resource_group_name }}" + storage_account_name: + "{{ bundle.parameters.tfstate_storage_account_name }}" + container_name: "{{ bundle.parameters.tfstate_container_name }}" + key: "{{ bundle.parameters.tre_id }}-shared-service-sonatype-nexus-vm" +upgrade: + - exec: + description: "Upgrade shared service" + command: echo + arguments: + - "This shared service does not implement upgrade action" +uninstall: + - terraform: + description: "Tear down shared service" + input: false + vars: + tre_id: "{{ bundle.parameters.tre_id }}" + ssl_cert_name: "{{ bundle.parameters.ssl_cert_name }}" + backendConfig: + resource_group_name: + "{{ bundle.parameters.tfstate_resource_group_name }}" + storage_account_name: + "{{ bundle.parameters.tfstate_storage_account_name }}" + container_name: "{{ bundle.parameters.tfstate_container_name }}" + key: "{{ bundle.parameters.tre_id }}-shared-service-sonatype-nexus-vm" diff --git a/templates/shared_services/sonatype-nexus-vm/scripts/configure_nexus_repos.sh b/templates/shared_services/sonatype-nexus-vm/scripts/configure_nexus_repos.sh new file mode 100644 index 0000000000..2a148bb2d3 --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/scripts/configure_nexus_repos.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -o pipefail +set -o nounset +# set -o xtrace + +if [ -z "$1" ] + then + echo 'Nexus password needs to be passed as argument' +fi + +timeout=300 +echo 'Checking for ./nexus_repos_config directory...' +while [ ! -d "$(dirname "${BASH_SOURCE[0]}")"/nexus_repos_config ]; do + # Wait for /nexus_repos_config with json config files to be copied into vm + if [ $timeout == 0 ]; then + echo 'ERROR - Timeout while waiting for nexus_repos_config directory' + exit 1 + fi + sleep 1 + ((timeout--)) +done + +# Create proxy for each .json file +for filename in "$(dirname "${BASH_SOURCE[0]}")"/nexus_repos_config/*.json; do + echo "Found config file: $filename. Sending to Nexus..." + # Check if apt proxy + base_type=$( jq .baseType "$filename" | sed 's/"//g') + repo_type=$( jq .repoType "$filename" | sed 's/"//g') + repo_name=$(jq .name "$filename" | sed 's/"//g') + base_url=http://localhost/service/rest/v1/repositories/$base_type/$repo_type + + config_timeout=300 + status_code=1 + while [ $status_code != 201 ]; do + status_code=$(curl -iu admin:"$1" -XPOST \ + "$base_url" \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d @"$filename" \ + -k -s -w "%{http_code}" -o /dev/null) + echo "Response received from Nexus: $status_code" + + if [ $config_timeout == 0 ]; then + echo "ERROR - Timeout while trying to configure $repo_name" + exit 1 + elif [ "$status_code" != 201 ]; then + sleep 1 + ((config_timeout--)) + fi + done +done diff --git a/templates/shared_services/sonatype-nexus-vm/scripts/configure_nexus_ssl.sh b/templates/shared_services/sonatype-nexus-vm/scripts/configure_nexus_ssl.sh new file mode 100644 index 0000000000..9c57e86323 --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/scripts/configure_nexus_ssl.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# Configure Nexus to use certificate to serve proxies over https + +set -o errexit +set -o pipefail +set -o nounset +# set -o xtrace + +# Prepare ssl certificate +az login --identity -u "${MSI_ID}" --allow-no-subscriptions +# -- get cert from kv as secret so it contains private key +echo 'Getting cert and cert password from Keyvault...' +az keyvault secret download --vault-name "${VAULT_NAME}" --name "${SSL_CERT_NAME}" --file temp.pfx --encoding base64 +cert_password=$(az keyvault secret show --vault-name "${VAULT_NAME}" \ + --name "${SSL_CERT_PASSWORD_NAME}" -o tsv --query value) +# -- az cli strips out password from cert so we re-add by converting to PEM then PFX with pwd +openssl pkcs12 -in temp.pfx -out temp.pem -nodes -password pass: +openssl pkcs12 -export -out nexus-ssl.pfx -in temp.pem -password "pass:$cert_password" + +# Import ssl cert to keystore within Nexus volume +keystore_timeout=300 +echo 'Checking for nexus-data/keystores directory...' +while [ ! -d /etc/nexus-data/keystores ]; do + # Wait for /keystore dir to be created by container first + if [ $keystore_timeout == 0 ]; then + echo 'ERROR - Timeout while waiting for Nexus to create nexus-data/keystores' + exit 1 + fi + sleep 1 + ((keystore_timeout--)) +done +echo 'Directory found. Importing ssl cert into nexus-data/keystores/keystore.jks...' +keytool -v -importkeystore -noprompt -srckeystore nexus-ssl.pfx -srcstoretype PKCS12 \ + -destkeystore /etc/nexus-data/keystores/keystore.jks \ + -deststoretype JKS -srcstorepass "$cert_password" -deststorepass "$cert_password" + +# Configure Jetty instance within Nexus to consume ssl cert +echo 'Modifying Nexus Jetty configuration to enable ssl...' +mkdir -p /etc/nexus-data/etc/jetty +# -- first need to copy default Jetty config to persistent volume so isn't overwritten on restart +docker exec -u root nexus cp /opt/sonatype/nexus/etc/jetty/jetty-https.xml /nexus-data/etc/jetty/ +# -- then we replace password values with the ssl cert keystore password +xmlstarlet ed -P --inplace \ + -u "/Configure[@id='Server']/New[@id='sslContextFactory']/Set[@name='KeyStorePassword']" \ + -v "$cert_password" /etc/nexus-data/etc/jetty/jetty-https.xml +xmlstarlet ed -P --inplace \ + -u "/Configure[@id='Server']/New[@id='sslContextFactory']/Set[@name='KeyManagerPassword']" \ + -v "$cert_password" /etc/nexus-data/etc/jetty/jetty-https.xml +xmlstarlet ed -P --inplace \ + -u "/Configure[@id='Server']/New[@id='sslContextFactory']/Set[@name='TrustStorePassword']" \ + -v "$cert_password" /etc/nexus-data/etc/jetty/jetty-https.xml +# -- then update the location of our keystore +xmlstarlet ed -P --inplace \ + -u "/Configure[@id='Server']/New[@id='sslContextFactory']/Set[@name='KeyStorePath']" \ + -v /nexus-data/keystores/keystore.jks /etc/nexus-data/etc/jetty/jetty-https.xml +xmlstarlet ed -P --inplace \ + -u "/Configure[@id='Server']/New[@id='sslContextFactory']/Set[@name='TrustStorePath']" \ + -v /nexus-data/keystores/keystore.jks /etc/nexus-data/etc/jetty/jetty-https.xml + +# Add jetty configuration and ssl port to Nexus properties +cat >> /etc/nexus-data/etc/nexus.properties <<'EOF' +application-port-ssl=8443 +nexus-args=$${jetty.etc}/jetty.xml,$${jetty.etc}/jetty-http.xml,$${jetty.etc}/jetty-requestlog.xml,/nexus-data/etc/jetty/jetty-https.xml +EOF + +# Restart the container for changes to take effect +docker restart nexus +echo 'Nexus ssl configuration completed.' diff --git a/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/apt-pypi_proxy_conf.json b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/apt-pypi_proxy_conf.json new file mode 100644 index 0000000000..757aaedd4c --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/apt-pypi_proxy_conf.json @@ -0,0 +1,36 @@ +{ + "name": "apt-pypi", + "online": true, + "storage": { + "blobStoreName": "default", + "strictContentTypeValidation": true, + "write_policy": "ALLOW" + }, + "proxy": { + "remoteUrl": "https://pypi.org", + "contentMaxAge": 1440, + "metadataMaxAge": 1440 + }, + "negativeCache": { + "enabled": true, + "timeToLive": 1440 + }, + "httpClient": { + "blocked": false, + "autoBlock": true, + "connection": { + "retries": 0, + "userAgentSuffix": "string", + "timeout": 60, + "enableCircularRedirects": false, + "enableCookies": false, + "useTrustStore": false + } + }, + "apt": { + "distribution": "bionic", + "flat": false + }, + "baseType": "apt", + "repoType": "proxy" +} \ No newline at end of file diff --git a/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/conda_forge_proxy_conf.json b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/conda_forge_proxy_conf.json new file mode 100644 index 0000000000..710d9866ae --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/conda_forge_proxy_conf.json @@ -0,0 +1,32 @@ +{ + "name": "conda-forge", + "online": true, + "storage": { + "blobStoreName": "default", + "strictContentTypeValidation": true, + "write_policy": "ALLOW" + }, + "proxy": { + "remoteUrl": "https://conda.anaconda.org/conda-forge/", + "contentMaxAge": 1440, + "metadataMaxAge": 1440 + }, + "negativeCache": { + "enabled": true, + "timeToLive": 1440 + }, + "httpClient": { + "blocked": false, + "autoBlock": true, + "connection": { + "retries": 0, + "userAgentSuffix": "string", + "timeout": 60, + "enableCircularRedirects": false, + "enableCookies": false, + "useTrustStore": false + } + }, + "baseType": "conda", + "repoType": "proxy" +} diff --git a/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/conda_proxy_conf.json b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/conda_proxy_conf.json new file mode 100644 index 0000000000..83f5bce642 --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/conda_proxy_conf.json @@ -0,0 +1,32 @@ +{ + "name": "conda", + "online": true, + "storage": { + "blobStoreName": "default", + "strictContentTypeValidation": true, + "write_policy": "ALLOW" + }, + "proxy": { + "remoteUrl": "https://repo.anaconda.com/pkgs/main", + "contentMaxAge": 1440, + "metadataMaxAge": 1440 + }, + "negativeCache": { + "enabled": true, + "timeToLive": 1440 + }, + "httpClient": { + "blocked": false, + "autoBlock": true, + "connection": { + "retries": 0, + "userAgentSuffix": "string", + "timeout": 60, + "enableCircularRedirects": false, + "enableCookies": false, + "useTrustStore": false + } + }, + "baseType": "conda", + "repoType": "proxy" +} diff --git a/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/docker_gpg_proxy_conf.json b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/docker_gpg_proxy_conf.json new file mode 100644 index 0000000000..df20329f16 --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/docker_gpg_proxy_conf.json @@ -0,0 +1,32 @@ +{ + "name": "docker-public-key", + "online": true, + "storage": { + "blobStoreName": "default", + "strictContentTypeValidation": true, + "write_policy": "ALLOW" + }, + "proxy": { + "remoteUrl": "https://download.docker.com/linux/ubuntu/", + "contentMaxAge": 1440, + "metadataMaxAge": 1440 + }, + "negativeCache": { + "enabled": true, + "timeToLive": 1440 + }, + "httpClient": { + "blocked": false, + "autoBlock": true, + "connection": { + "retries": 0, + "userAgentSuffix": "string", + "timeout": 60, + "enableCircularRedirects": false, + "enableCookies": false, + "useTrustStore": false + } + }, + "baseType": "raw", + "repoType": "proxy" +} diff --git a/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/docker_hub_proxy_conf.json b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/docker_hub_proxy_conf.json new file mode 100644 index 0000000000..a4dc84a70f --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/docker_hub_proxy_conf.json @@ -0,0 +1,40 @@ +{ + "name": "docker-hub", + "online": true, + "storage": { + "blobStoreName": "default", + "strictContentTypeValidation": true, + "write_policy": "ALLOW" + }, + "proxy": { + "remoteUrl": "https://registry-1.docker.io", + "contentMaxAge": 1440, + "metadataMaxAge": 1440 + }, + "dockerProxy": { + "indexType": "HUB" + }, + "docker": { + "v1Enabled": true, + "forceBasicAuth": false, + "httpsPort": 8083 + }, + "negativeCache": { + "enabled": true, + "timeToLive": 1440 + }, + "httpClient": { + "blocked": false, + "autoBlock": true, + "connection": { + "retries": 0, + "userAgentSuffix": "string", + "timeout": 60, + "enableCircularRedirects": false, + "enableCookies": false, + "useTrustStore": false + } + }, + "baseType": "docker", + "repoType": "proxy" +} diff --git a/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/docker_proxy_conf.json b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/docker_proxy_conf.json new file mode 100644 index 0000000000..3e4469c46b --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/docker_proxy_conf.json @@ -0,0 +1,36 @@ +{ + "name": "docker", + "online": true, + "storage": { + "blobStoreName": "default", + "strictContentTypeValidation": true, + "write_policy": "ALLOW" + }, + "proxy": { + "remoteUrl": "https://download.docker.com/linux/ubuntu/", + "contentMaxAge": 1440, + "metadataMaxAge": 1440 + }, + "negativeCache": { + "enabled": true, + "timeToLive": 1440 + }, + "httpClient": { + "blocked": false, + "autoBlock": true, + "connection": { + "retries": 0, + "userAgentSuffix": "string", + "timeout": 60, + "enableCircularRedirects": false, + "enableCookies": false, + "useTrustStore": false + } + }, + "apt": { + "distribution": "bionic", + "flat": false + }, + "baseType": "apt", + "repoType": "proxy" +} diff --git a/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/pypi_proxy_conf.json b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/pypi_proxy_conf.json new file mode 100644 index 0000000000..5429f5c0c1 --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/pypi_proxy_conf.json @@ -0,0 +1,32 @@ +{ + "name": "pypi", + "online": true, + "storage": { + "blobStoreName": "default", + "strictContentTypeValidation": true, + "write_policy": "ALLOW" + }, + "proxy": { + "remoteUrl": "https://pypi.org", + "contentMaxAge": 1440, + "metadataMaxAge": 1440 + }, + "negativeCache": { + "enabled": true, + "timeToLive": 1440 + }, + "httpClient": { + "blocked": false, + "autoBlock": true, + "connection": { + "retries": 0, + "userAgentSuffix": "string", + "timeout": 60, + "enableCircularRedirects": false, + "enableCookies": false, + "useTrustStore": false + } + }, + "baseType": "pypi", + "repoType": "proxy" +} diff --git a/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/ubuntu_proxy_conf.json b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/ubuntu_proxy_conf.json new file mode 100644 index 0000000000..10820ba8b2 --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/ubuntu_proxy_conf.json @@ -0,0 +1,36 @@ +{ + "name": "ubuntu", + "online": true, + "storage": { + "blobStoreName": "default", + "strictContentTypeValidation": true, + "write_policy": "ALLOW" + }, + "proxy": { + "remoteUrl": "http://archive.ubuntu.com/ubuntu/", + "contentMaxAge": 1440, + "metadataMaxAge": 1440 + }, + "negativeCache": { + "enabled": true, + "timeToLive": 1440 + }, + "httpClient": { + "blocked": false, + "autoBlock": true, + "connection": { + "retries": 0, + "userAgentSuffix": "string", + "timeout": 60, + "enableCircularRedirects": false, + "enableCookies": false, + "useTrustStore": false + } + }, + "apt": { + "distribution": "bionic", + "flat": false + }, + "baseType": "apt", + "repoType": "proxy" +} diff --git a/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/ubuntu_security_proxy_conf.json b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/ubuntu_security_proxy_conf.json new file mode 100644 index 0000000000..c1f72dece1 --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/scripts/nexus_repos_config/ubuntu_security_proxy_conf.json @@ -0,0 +1,36 @@ +{ + "name": "ubuntu-security", + "online": true, + "storage": { + "blobStoreName": "default", + "strictContentTypeValidation": true, + "write_policy": "ALLOW" + }, + "proxy": { + "remoteUrl": "http://security.ubuntu.com/ubuntu/", + "contentMaxAge": 1440, + "metadataMaxAge": 1440 + }, + "negativeCache": { + "enabled": true, + "timeToLive": 1440 + }, + "httpClient": { + "blocked": false, + "autoBlock": true, + "connection": { + "retries": 0, + "userAgentSuffix": "string", + "timeout": 60, + "enableCircularRedirects": false, + "enableCookies": false, + "useTrustStore": false + } + }, + "apt": { + "distribution": "bionic-security", + "flat": false + }, + "baseType": "apt", + "repoType": "proxy" +} diff --git a/templates/shared_services/sonatype-nexus-vm/scripts/reset_nexus_password.sh b/templates/shared_services/sonatype-nexus-vm/scripts/reset_nexus_password.sh new file mode 100644 index 0000000000..8383d6d6dc --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/scripts/reset_nexus_password.sh @@ -0,0 +1,45 @@ +#!/bin/bash +set -o pipefail +set -o nounset +# set -o xtrace + +if [ -z "$1" ] + then + echo 'New password to set needs to be passed as argument' +fi + +# Get the current password so we can post to the API +# (this is created in /nexus-data mounted volume as part of Nexus container start-up) +password_timeout=300 +echo 'Checking for Nexus admin password file...' +while [ ! -f /etc/nexus-data/admin.password ]; do + # We must first wait for the file to be created + if [ $password_timeout == 0 ]; then + echo 'ERROR - Timeout while waiting for nexus-data/admin.password to be created' + exit 1 + fi + sleep 1 + ((password_timeout--)) +done +current_password=$(cat /etc/nexus-data/admin.password) + +# Set own admin password so we can connect to repository manager later on using TF KV secret +reset_timeout=300 +echo "Nexus default admin password found ($current_password). Resetting..." +res=1 +while test "$res" != "0"; do + curl -ifu admin:"$current_password" -XPUT -H 'Content-Type:text/plain' --data "$1" \ + http://localhost/service/rest/v1/security/users/admin/change-password + res=$? + echo "Attempt to reset password finished with code $res" + if test "$res" == "0"; then + echo 'Password reset successfully.' + else + if [ $reset_timeout == 0 ]; then + echo 'ERROR - Timeout while trying to reset Nexus admin password' + exit 1 + fi + sleep 5 + ((reset_timeout+=5)) + fi +done diff --git a/templates/shared_services/sonatype-nexus-vm/template_schema.json b/templates/shared_services/sonatype-nexus-vm/template_schema.json new file mode 100644 index 0000000000..baf7e777a7 --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/template_schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://github.com/microsoft/AzureTRE/templates/shared_services/sonatype-nexus-vm/template_schema.json", + "type": "object", + "title": "Nexus Shared Service", + "description": "Provides Nexus shared service", + "required": [ + "ssl_cert_name" + ], + "properties": { + "ssl_cert_name": { + "type": "string", + "title": "SSL certificate name", + "description": "The name of the certificate to use (located in the core KeyVault) for configuring Nexus SSL" + } + } +} diff --git a/templates/shared_services/sonatype-nexus-vm/terraform/cloud-config.yaml b/templates/shared_services/sonatype-nexus-vm/terraform/cloud-config.yaml new file mode 100644 index 0000000000..336cc95ae4 --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/terraform/cloud-config.yaml @@ -0,0 +1,53 @@ +--- +#cloud-config +package_upgrade: true +apt: + sources: + docker.list: + source: deb [arch=amd64] + https://download.docker.com/linux/ubuntu $RELEASE stable + keyid: 9DC858229FC7DD38854AE2D88D81803C0EBFCD88 + keyserver: hkp://keyserver.ubuntu.com:80 + azure-cli.list: + source: deb [arch=amd64] + https://packages.microsoft.com/repos/azure-cli/ $RELEASE main + keyid: BC528686B50D79E339D3721CEB3E94ADBE1229CF + keyserver: hkp://keyserver.ubuntu.com:80 + +packages: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-compose + - gnupg2 + - pass + - azure-cli + - default-jre + - xmlstarlet + - jq + +# create the docker group +groups: + - docker + +# Add default auto created user to docker group +system_info: + default_user: + groups: [docker] + +runcmd: + - export DEBIAN_FRONTEND=noninteractive + # Give the Nexus process write permissions on the folder mounted as persistent volume + - chown -R 200 /etc/nexus-data + # Run the nexus container with mapped volume for nexus config + - docker run -d -p 80:8081 -p 443:8443 -v /etc/nexus-data:/nexus-data + --restart always + --name nexus + --log-driver local + sonatype/nexus3 + # Reset the admin password of Nexus to the one created by TF and stored in KeyVault + - bash /tmp/reset_nexus_password.sh "${NEXUS_ADMIN_PASSWORD}" + # Invoke Nexus SSL configuration (which will also be ran as CRON daily to renew cert) + - bash /etc/cron.daily/configure_nexus_ssl.sh + # Configure Nexus repositories + - bash /tmp/configure_nexus_repos.sh "${NEXUS_ADMIN_PASSWORD}" diff --git a/templates/shared_services/sonatype-nexus-vm/terraform/data.tf b/templates/shared_services/sonatype-nexus-vm/terraform/data.tf new file mode 100644 index 0000000000..275a017f8e --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/terraform/data.tf @@ -0,0 +1,49 @@ +data "azurerm_log_analytics_workspace" "tre" { + name = "log-${var.tre_id}" + resource_group_name = local.core_resource_group_name +} + +data "azurerm_virtual_network" "core" { + name = local.core_vnet + resource_group_name = local.core_resource_group_name +} + +data "azurerm_subnet" "shared" { + resource_group_name = local.core_resource_group_name + virtual_network_name = local.core_vnet + name = "SharedSubnet" +} + +data "azurerm_firewall" "fw" { + name = "fw-${var.tre_id}" + resource_group_name = local.core_resource_group_name +} + +data "azurerm_key_vault" "kv" { + name = "kv-${var.tre_id}" + resource_group_name = local.core_resource_group_name +} + +data "azurerm_key_vault_certificate" "nexus_cert" { + name = var.ssl_cert_name + key_vault_id = data.azurerm_key_vault.kv.id +} + +data "azurerm_key_vault_secret" "nexus_cert_password" { + name = "${data.azurerm_key_vault_certificate.nexus_cert.name}-password" + key_vault_id = data.azurerm_key_vault.kv.id +} + +data "azurerm_storage_account" "nexus" { + name = local.storage_account_name + resource_group_name = local.core_resource_group_name +} + +data "azurerm_resource_group" "rg" { + name = local.core_resource_group_name +} + +data "azurerm_private_dns_zone" "nexus" { + name = "nexus-${var.tre_id}.${data.azurerm_resource_group.rg.location}.cloudapp.azure.com" + resource_group_name = local.core_resource_group_name +} diff --git a/templates/shared_services/sonatype-nexus-vm/terraform/deploy.sh b/templates/shared_services/sonatype-nexus-vm/terraform/deploy.sh new file mode 100755 index 0000000000..2a371d27e3 --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/terraform/deploy.sh @@ -0,0 +1,10 @@ +#!/bin/bash +export TF_LOG="" + +terraform init -input=false -backend=true -reconfigure -upgrade \ + -backend-config="resource_group_name=${TF_VAR_mgmt_resource_group_name:?}" \ + -backend-config="storage_account_name=${TF_VAR_mgmt_storage_account_name:?}" \ + -backend-config="container_name=${TF_VAR_terraform_state_container_name:?}" \ + -backend-config="key=${TRE_ID:?}-shared-service-sonatype-nexus" +terraform plan +terraform apply -auto-approve diff --git a/templates/shared_services/sonatype-nexus-vm/terraform/destroy.sh b/templates/shared_services/sonatype-nexus-vm/terraform/destroy.sh new file mode 100755 index 0000000000..853a7142f3 --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/terraform/destroy.sh @@ -0,0 +1,10 @@ +#!/bin/bash +export TF_LOG="" + +terraform init -input=false -backend=true -reconfigure -upgrade \ + -backend-config="resource_group_name=${TF_VAR_mgmt_resource_group_name:?}" \ + -backend-config="storage_account_name=${TF_VAR_mgmt_storage_account_name:?}" \ + -backend-config="container_name=${TF_VAR_terraform_state_container_name:?}" \ + -backend-config="key=${TRE_ID:?}-shared-service-sonatype-nexus" + +terraform destroy -auto-approve diff --git a/templates/shared_services/sonatype-nexus-vm/terraform/firewall.tf b/templates/shared_services/sonatype-nexus-vm/terraform/firewall.tf new file mode 100644 index 0000000000..5b5d26f2d2 --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/terraform/firewall.tf @@ -0,0 +1,22 @@ +resource "azurerm_firewall_application_rule_collection" "shared_subnet_sonatype_nexus" { + name = "shared_subnet_sonatype_nexus" + azure_firewall_name = data.azurerm_firewall.fw.name + resource_group_name = data.azurerm_firewall.fw.resource_group_name + priority = 105 + action = "Allow" + + rule { + name = "nexus-package-sources" + protocol { + port = "443" + type = "Https" + } + protocol { + port = "80" + type = "Http" + } + + target_fqdns = local.nexus_allowed_fqdns_list + source_addresses = data.azurerm_subnet.shared.address_prefixes + } +} diff --git a/templates/shared_services/sonatype-nexus-vm/terraform/locals.tf b/templates/shared_services/sonatype-nexus-vm/terraform/locals.tf new file mode 100644 index 0000000000..d9736d661d --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/terraform/locals.tf @@ -0,0 +1,8 @@ +locals { + core_vnet = "vnet-${var.tre_id}" + core_resource_group_name = "rg-${var.tre_id}" + firewall_name = "fw-${var.tre_id}" + nexus_allowed_fqdns = "*pypi.org,files.pythonhosted.org,security.ubuntu.com,archive.ubuntu.com,keyserver.ubuntu.com,repo.anaconda.com,*.docker.com,*.docker.io,conda.anaconda.org,azure.archive.ubuntu.com, packages.microsoft.com" + nexus_allowed_fqdns_list = distinct(compact(split(",", replace(local.nexus_allowed_fqdns, " ", "")))) + storage_account_name = lower(replace("stg-${var.tre_id}", "-", "")) +} diff --git a/templates/shared_services/sonatype-nexus-vm/terraform/main.tf b/templates/shared_services/sonatype-nexus-vm/terraform/main.tf new file mode 100644 index 0000000000..2ead62214d --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/terraform/main.tf @@ -0,0 +1,22 @@ +# Azure Provider source and version being used +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "=3.4.0" + } + } + + backend "azurerm" {} +} + +provider "azurerm" { + features { + key_vault { + # Don't purge secrets on destroy (this would fail due to purge protection being enabled on keyvault) + purge_soft_deleted_secrets_on_destroy = false + # When recreating a shared service, recover any previously soft deleted secrets + recover_soft_deleted_secrets = true + } + } +} diff --git a/templates/shared_services/sonatype-nexus-vm/terraform/output.tf b/templates/shared_services/sonatype-nexus-vm/terraform/output.tf new file mode 100644 index 0000000000..b92393b369 --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/terraform/output.tf @@ -0,0 +1,3 @@ +output "nexus_fqdn" { + value = azurerm_private_dns_a_record.nexus_vm.fqdn +} diff --git a/templates/shared_services/sonatype-nexus-vm/terraform/variables.tf b/templates/shared_services/sonatype-nexus-vm/terraform/variables.tf new file mode 100644 index 0000000000..604381e20d --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/terraform/variables.tf @@ -0,0 +1,7 @@ +variable "tre_id" { + type = string +} + +variable "ssl_cert_name" { + type = string +} diff --git a/templates/shared_services/sonatype-nexus-vm/terraform/vm.tf b/templates/shared_services/sonatype-nexus-vm/terraform/vm.tf new file mode 100644 index 0000000000..301b70ac40 --- /dev/null +++ b/templates/shared_services/sonatype-nexus-vm/terraform/vm.tf @@ -0,0 +1,190 @@ +resource "azurerm_network_interface" "nexus" { + name = "nic-nexus-${var.tre_id}" + location = data.azurerm_resource_group.rg.location + resource_group_name = local.core_resource_group_name + + ip_configuration { + name = "primary" + subnet_id = data.azurerm_subnet.shared.id + private_ip_address_allocation = "Dynamic" + } +} + +resource "azurerm_private_dns_zone_virtual_network_link" "nexus_core_vnet" { + name = "nexuslink-core" + resource_group_name = local.core_resource_group_name + private_dns_zone_name = data.azurerm_private_dns_zone.nexus.name + virtual_network_id = data.azurerm_virtual_network.core.id +} + +resource "azurerm_private_dns_a_record" "nexus_vm" { + name = "@" + zone_name = data.azurerm_private_dns_zone.nexus.name + resource_group_name = local.core_resource_group_name + ttl = 300 + records = [azurerm_linux_virtual_machine.nexus.private_ip_address] +} + +resource "random_password" "nexus_vm_password" { + length = 16 + lower = true + min_lower = 1 + upper = true + min_upper = 1 + number = true + min_numeric = 1 + special = true + min_special = 1 + override_special = "_%@" +} + +resource "random_password" "nexus_admin_password" { + length = 16 + lower = true + min_lower = 1 + upper = true + min_upper = 1 + number = true + min_numeric = 1 + special = true + min_special = 1 + override_special = "_%" +} + +resource "azurerm_key_vault_secret" "nexus_vm_password" { + name = "nexus-vm-password" + value = random_password.nexus_vm_password.result + key_vault_id = data.azurerm_key_vault.kv.id +} + +resource "azurerm_key_vault_secret" "nexus_admin_password" { + name = "nexus-admin-password" + value = random_password.nexus_admin_password.result + key_vault_id = data.azurerm_key_vault.kv.id +} + +resource "azurerm_user_assigned_identity" "nexus_msi" { + name = "id-nexus-${var.tre_id}" + location = data.azurerm_resource_group.rg.location + resource_group_name = local.core_resource_group_name + lifecycle { ignore_changes = [tags] } +} + +resource "azurerm_key_vault_access_policy" "nexus_msi" { + key_vault_id = data.azurerm_key_vault.kv.id + tenant_id = azurerm_user_assigned_identity.nexus_msi.tenant_id + object_id = azurerm_user_assigned_identity.nexus_msi.principal_id + + secret_permissions = ["Get", "Recover"] +} + +resource "azurerm_linux_virtual_machine" "nexus" { + name = "nexus-${var.tre_id}" + resource_group_name = local.core_resource_group_name + location = data.azurerm_resource_group.rg.location + network_interface_ids = [azurerm_network_interface.nexus.id] + size = "Standard_B2s" + disable_password_authentication = false + admin_username = "adminuser" + admin_password = random_password.nexus_vm_password.result + + custom_data = data.template_cloudinit_config.nexus_config.rendered + + lifecycle { ignore_changes = [tags] } + + source_image_reference { + publisher = "Canonical" + offer = "UbuntuServer" + sku = "18.04-LTS" + version = "latest" + } + + os_disk { + name = "osdisk-nexus-${var.tre_id}" + caching = "ReadWrite" + storage_account_type = "Standard_LRS" + } + + identity { + type = "UserAssigned" + identity_ids = [azurerm_user_assigned_identity.nexus_msi.id] + } + + boot_diagnostics { + storage_account_uri = data.azurerm_storage_account.nexus.primary_blob_endpoint + } + + depends_on = [ + azurerm_key_vault_access_policy.nexus_msi, + azurerm_firewall_application_rule_collection.shared_subnet_sonatype_nexus + ] + + connection { + type = "ssh" + host = azurerm_network_interface.nexus.private_ip_address + user = "adminuser" + password = random_password.nexus_vm_password.result + agent = false + timeout = "10m" + } + + provisioner "file" { + source = "${path.module}/../scripts/nexus_repos_config" + destination = "/tmp/nexus_repos_config" + } +} + +data "template_cloudinit_config" "nexus_config" { + gzip = true + base64_encode = true + + part { + content_type = "text/cloud-config" + content = data.template_file.nexus_bootstrapping.rendered + } + + part { + content_type = "text/cloud-config" + content = jsonencode({ + write_files = [ + { + content = file("${path.module}/../scripts/configure_nexus_repos.sh") + path = "/tmp/configure_nexus_repos.sh" + permissions = "0744" + }, + { + content = data.template_file.configure_nexus_ssl.rendered + path = "/etc/cron.daily/configure_nexus_ssl.sh" + permissions = "0755" + }, + { + content = "nexus.skipDefaultRepositories=true\n" + path = "/etc/nexus-data/etc/nexus.properties" + permissions = "0755" + }, + { + content = file("${path.module}/../scripts/reset_nexus_password.sh") + path = "/tmp/reset_nexus_password.sh" + permissions = "0744" + } + ] + }) + } +} + +data "template_file" "nexus_bootstrapping" { + template = file("${path.module}/cloud-config.yaml") + vars = { + NEXUS_ADMIN_PASSWORD = random_password.nexus_admin_password.result + } +} + +data "template_file" "configure_nexus_ssl" { + template = file("${path.module}/../scripts/configure_nexus_ssl.sh") + vars = { + MSI_ID = azurerm_user_assigned_identity.nexus_msi.id + VAULT_NAME = data.azurerm_key_vault.kv.name + SSL_CERT_NAME = data.azurerm_key_vault_certificate.nexus_cert.name + SSL_CERT_PASSWORD_NAME = data.azurerm_key_vault_secret.nexus_cert_password.name + } +} diff --git a/templates/shared_services/sonatype-nexus/parameters.json b/templates/shared_services/sonatype-nexus/parameters.json index c01ad0266b..38d90219d7 100755 --- a/templates/shared_services/sonatype-nexus/parameters.json +++ b/templates/shared_services/sonatype-nexus/parameters.json @@ -10,6 +10,12 @@ "env": "TRE_ID" } }, + { + "name": "id", + "source": { + "env": "ID" + } + }, { "name": "tfstate_container_name", "source": { diff --git a/templates/shared_services/sonatype-nexus/porter.yaml b/templates/shared_services/sonatype-nexus/porter.yaml index 150565da27..0f5d9b8531 100644 --- a/templates/shared_services/sonatype-nexus/porter.yaml +++ b/templates/shared_services/sonatype-nexus/porter.yaml @@ -1,6 +1,6 @@ --- name: tre-shared-service-nexus -version: 0.0.2 +version: 0.3.1 description: "A Sonatype Nexus shared service" registry: azuretre credentials: @@ -16,6 +16,9 @@ parameters: - name: tre_id type: string description: "The ID of the parent TRE instance e.g., mytre-dev-3142" + - name: id + type: string + description: "Resource ID" - name: tfstate_resource_group_name type: string description: "Resource group containing the Terraform state storage account" @@ -40,6 +43,7 @@ install: input: false vars: tre_id: "{{ bundle.parameters.tre_id }}" + tre_resource_id: "{{ bundle.parameters.id }}" backendConfig: resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" @@ -59,6 +63,7 @@ uninstall: input: false vars: tre_id: "{{ bundle.parameters.tre_id }}" + tre_resource_id: "{{ bundle.parameters.id }}" backendConfig: resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" diff --git a/templates/shared_services/sonatype-nexus/terraform/.terraform.lock.hcl b/templates/shared_services/sonatype-nexus/terraform/.terraform.lock.hcl index 6ce0e29592..f980a633f3 100644 --- a/templates/shared_services/sonatype-nexus/terraform/.terraform.lock.hcl +++ b/templates/shared_services/sonatype-nexus/terraform/.terraform.lock.hcl @@ -2,38 +2,40 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/azurerm" { - version = "2.97.0" - constraints = "2.97.0" + version = "3.5.0" + constraints = "3.5.0" hashes = [ - "h1:XxT+XM/leTXa21aTnJjPBfNBQ8cLE4gYDg01WEZsV1U=", - "zh:0aac80e6d2b8ddf33d558ac893d52688e8abf8a0b995cfc3c35eb84afbf432a3", - "zh:11191068cb732208ebc8662651782f63db329a25f7ea1cd50cd91622a2c247b7", - "zh:36c8334194e7d605682053c7c70fbb2a650d9b0a7bcc44d5cdda4f205818438a", - "zh:3a5e01276added995e875b42ecc6b36ff73d267f0c096c87195bd2b1fff4f5b2", - "zh:557e38371657e6ed8aae9192d01480c4cca7c0f7ade6022f1aec247a6384922b", - "zh:67b913c280c5858549477a4b05e77078b1a5234de77c7bddd4ee1e8e237d5665", - "zh:7aeca864ce45b295db734cd968f7596ff12cd7c522ee89d53f432dae7c2b5d18", - "zh:b6127d7a796eaf9756dd212667eb48f79c0e78729589ec8ccf68e0b36ebb4e54", - "zh:bed448238740f897d1b399e5123b3a9eba256b981846f9ee92b71493446ca684", - "zh:c351a1bba34c3bd06fff75e4c15e4db0456268479463c2471598068ea1c5c884", - "zh:d073c24d0a4756e79b39f41f552d526800f9fb0ad0a74f742ac8de61b6416a3a", + "h1:T4XsCHDT839VehWKdxbVsLn0ECjcQaUTzbSGW055pgM=", + "zh:0d8ae6d6e87f44ed4a178be03d6466339b0bb578ab54c2677e365a8281b0bb7d", + "zh:29d250d1a18d49652b28f234ecd17687b36c875dc47877a678e587d5d136b054", + "zh:2e69ba373cf009e8a60b36d04f3dbc4638708d1bf88be9f96b3e52cbf8f47f31", + "zh:53d525dd84ac63b5f749bfbc6b70a202dacf29597664d2ab1165efea6f24f630", + "zh:a25024d574ccd5ae6c2962f3bb71d510f62899f493b1ed096f2f7f0e2b18f975", + "zh:aabc64fe64319b95aaba1d1866f87abc7b10adae37d2eafa2f85f37317fdd49f", + "zh:acc6a977814897cb23d3b3753213281334238f8bce6d2b21e9f04fc4087ee980", + "zh:b24987e9416c39cd59c0fa41c139a97406b9955f0607fcafbf3315014456338a", + "zh:c550eae45fd32acdbe32b4e5c450ae95df6cb18903ac7216b1b07b23a16ce045", + "zh:c8f83b763b643893dcb6933a6bcee824cb514e06e7e5c5f5ac4ba187e66d7e22", + "zh:dcdac07e7ea18464dea729717870c275de9453775243c231e1fb305cad0ee597", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } provider "registry.terraform.io/hashicorp/null" { - version = "3.1.0" + version = "3.1.1" hashes = [ - "h1:vpC6bgUQoJ0znqIKVFevOdq+YQw42bRq0u+H3nto8nA=", - "zh:02a1675fd8de126a00460942aaae242e65ca3380b5bb192e8773ef3da9073fd2", - "zh:53e30545ff8926a8e30ad30648991ca8b93b6fa496272cd23b26763c8ee84515", - "zh:5f9200bf708913621d0f6514179d89700e9aa3097c77dac730e8ba6e5901d521", - "zh:9ebf4d9704faba06b3ec7242c773c0fbfe12d62db7d00356d4f55385fc69bfb2", - "zh:a6576c81adc70326e4e1c999c04ad9ca37113a6e925aefab4765e5a5198efa7e", - "zh:a8a42d13346347aff6c63a37cda9b2c6aa5cc384a55b2fe6d6adfa390e609c53", - "zh:c797744d08a5307d50210e0454f91ca4d1c7621c68740441cf4579390452321d", - "zh:cecb6a304046df34c11229f20a80b24b1603960b794d68361a67c5efe58e62b8", - "zh:e1371aa1e502000d9974cfaff5be4cfa02f47b17400005a16f14d2ef30dc2a70", - "zh:fc39cc1fe71234a0b0369d5c5c7f876c71b956d23d7d6f518289737a001ba69b", - "zh:fea4227271ebf7d9e2b61b89ce2328c7262acd9fd190e1fd6d15a591abfa848e", + "h1:71sNUDvmiJcijsvfXpiLCz0lXIBSsEJjMxljt7hxMhw=", + "zh:063466f41f1d9fd0dd93722840c1314f046d8760b1812fa67c34de0afcba5597", + "zh:08c058e367de6debdad35fc24d97131c7cf75103baec8279aba3506a08b53faf", + "zh:73ce6dff935150d6ddc6ac4a10071e02647d10175c173cfe5dca81f3d13d8afe", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8fdd792a626413502e68c195f2097352bdc6a0df694f7df350ed784741eb587e", + "zh:976bbaf268cb497400fd5b3c774d218f3933271864345f18deebe4dcbfcd6afa", + "zh:b21b78ca581f98f4cdb7a366b03ae9db23a73dfa7df12c533d7c19b68e9e72e5", + "zh:b7fc0c1615dbdb1d6fd4abb9c7dc7da286631f7ca2299fb9cd4664258ccfbff4", + "zh:d1efc942b2c44345e0c29bc976594cb7278c38cfb8897b344669eafbc3cddf46", + "zh:e356c245b3cd9d4789bab010893566acace682d7db877e52d40fc4ca34a50924", + "zh:ea98802ba92fcfa8cf12cbce2e9e7ebe999afbf8ed47fa45fc847a098d89468b", + "zh:eff8872458806499889f6927b5d954560f3d74bf20b6043409edf94d26cd906f", ] } diff --git a/templates/shared_services/sonatype-nexus/terraform/main.tf b/templates/shared_services/sonatype-nexus/terraform/main.tf index 715a8db036..fa8b6fa244 100644 --- a/templates/shared_services/sonatype-nexus/terraform/main.tf +++ b/templates/shared_services/sonatype-nexus/terraform/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "=2.97.0" + version = "=3.5.0" } } diff --git a/templates/shared_services/sonatype-nexus/terraform/variables.tf b/templates/shared_services/sonatype-nexus/terraform/variables.tf index 737ac10733..6ce7ab3b33 100644 --- a/templates/shared_services/sonatype-nexus/terraform/variables.tf +++ b/templates/shared_services/sonatype-nexus/terraform/variables.tf @@ -3,6 +3,11 @@ variable "tre_id" { description = "Unique TRE ID" } +variable "tre_resource_id" { + type = string + description = "Resource ID" +} + variable "nexus_storage_limit" { type = number description = "Space allocated in GB for the Nexus data in Azure Files Share" diff --git a/templates/shared_services/sonatype-nexus/terraform/webapp.tf b/templates/shared_services/sonatype-nexus/terraform/webapp.tf index cb210e6c40..b7044e0709 100644 --- a/templates/shared_services/sonatype-nexus/terraform/webapp.tf +++ b/templates/shared_services/sonatype-nexus/terraform/webapp.tf @@ -7,9 +7,8 @@ resource "azurerm_app_service" "nexus" { app_settings = { APPINSIGHTS_INSTRUMENTATIONKEY = data.azurerm_application_insights.core.instrumentation_key - WEBSITES_PORT = "8081" # nexus web-ui listens here - WEBSITES_CONTAINER_START_TIME_LIMIT = "900" # nexus takes a while to start-up - WEBSITE_VNET_ROUTE_ALL = 1 + WEBSITES_PORT = "8081" # nexus web-ui listens here + WEBSITES_CONTAINER_START_TIME_LIMIT = "900" # nexus takes a while to start-up WEBSITE_DNS_SERVER = "168.63.129.16" # required to access storage over private endpoints WEBSITES_ENABLE_APP_SERVICE_STORAGE = false DOCKER_REGISTRY_SERVER_URL = "https://index.docker.io/v1" @@ -21,9 +20,11 @@ resource "azurerm_app_service" "nexus" { linux_fx_version = "DOCKER|sonatype/nexus3" remote_debugging_enabled = false scm_use_main_ip_restriction = true - - always_on = true - min_tls_version = "1.2" + ftps_state = "Disabled" + always_on = true + min_tls_version = "1.2" + websockets_enabled = false + vnet_route_all_enabled = true ip_restriction { action = "Deny" @@ -31,8 +32,6 @@ resource "azurerm_app_service" "nexus" { name = "Deny all" priority = 2147483647 } - - websockets_enabled = false } storage_account { diff --git a/templates/workspace_services/azureml/porter.yaml b/templates/workspace_services/azureml/porter.yaml index cdffe945ba..e5b4340c26 100644 --- a/templates/workspace_services/azureml/porter.yaml +++ b/templates/workspace_services/azureml/porter.yaml @@ -1,6 +1,6 @@ --- name: tre-service-azureml -version: 0.1.9 +version: 0.3.0 description: "An Azure TRE service for Azure Machine Learning" registry: azuretre dockerfile: Dockerfile.tmpl diff --git a/templates/workspace_services/azureml/user_resources/aml_compute/porter.yaml b/templates/workspace_services/azureml/user_resources/aml_compute/porter.yaml index ac905db7fe..45d1c56b79 100644 --- a/templates/workspace_services/azureml/user_resources/aml_compute/porter.yaml +++ b/templates/workspace_services/azureml/user_resources/aml_compute/porter.yaml @@ -1,6 +1,6 @@ --- name: tre-user-resource-aml-compute-instance -version: 0.1.1 +version: 0.3.0 description: "Azure Machine Learning Compute Instance" registry: azuretre dockerfile: Dockerfile.tmpl diff --git a/templates/workspace_services/devtestlabs/porter.yaml b/templates/workspace_services/devtestlabs/porter.yaml index 8fa596d432..9e9767a23e 100644 --- a/templates/workspace_services/devtestlabs/porter.yaml +++ b/templates/workspace_services/devtestlabs/porter.yaml @@ -1,6 +1,6 @@ --- name: tre-service-devtestlabs -version: 0.1.0 +version: 0.3.0 description: "An Azure TRE service for Dev Test Labs" registry: azuretre diff --git a/templates/workspace_services/gitea/.dockerignore b/templates/workspace_services/gitea/.dockerignore index 23d80b34ba..f3d6416e3f 100644 --- a/templates/workspace_services/gitea/.dockerignore +++ b/templates/workspace_services/gitea/.dockerignore @@ -4,3 +4,6 @@ Dockerfile.tmpl terraform/deploy.sh terraform/destroy.sh + +# Local .terraform directories +**/.terraform/* diff --git a/templates/workspace_services/gitea/porter.yaml b/templates/workspace_services/gitea/porter.yaml index a66314a715..2cc1456076 100644 --- a/templates/workspace_services/gitea/porter.yaml +++ b/templates/workspace_services/gitea/porter.yaml @@ -1,6 +1,6 @@ --- name: tre-workspace-service-gitea -version: 0.2.17 +version: 0.3.1 description: "A Gitea workspace service" registry: azuretre dockerfile: Dockerfile.tmpl diff --git a/templates/workspace_services/gitea/terraform/.terraform.lock.hcl b/templates/workspace_services/gitea/terraform/.terraform.lock.hcl index 00b91198f9..e7d57a8fe4 100644 --- a/templates/workspace_services/gitea/terraform/.terraform.lock.hcl +++ b/templates/workspace_services/gitea/terraform/.terraform.lock.hcl @@ -2,56 +2,59 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/azurerm" { - version = "2.97.0" - constraints = "2.97.0" + version = "3.5.0" + constraints = "3.5.0" hashes = [ - "h1:XxT+XM/leTXa21aTnJjPBfNBQ8cLE4gYDg01WEZsV1U=", - "zh:0aac80e6d2b8ddf33d558ac893d52688e8abf8a0b995cfc3c35eb84afbf432a3", - "zh:11191068cb732208ebc8662651782f63db329a25f7ea1cd50cd91622a2c247b7", - "zh:36c8334194e7d605682053c7c70fbb2a650d9b0a7bcc44d5cdda4f205818438a", - "zh:3a5e01276added995e875b42ecc6b36ff73d267f0c096c87195bd2b1fff4f5b2", - "zh:557e38371657e6ed8aae9192d01480c4cca7c0f7ade6022f1aec247a6384922b", - "zh:67b913c280c5858549477a4b05e77078b1a5234de77c7bddd4ee1e8e237d5665", - "zh:7aeca864ce45b295db734cd968f7596ff12cd7c522ee89d53f432dae7c2b5d18", - "zh:b6127d7a796eaf9756dd212667eb48f79c0e78729589ec8ccf68e0b36ebb4e54", - "zh:bed448238740f897d1b399e5123b3a9eba256b981846f9ee92b71493446ca684", - "zh:c351a1bba34c3bd06fff75e4c15e4db0456268479463c2471598068ea1c5c884", - "zh:d073c24d0a4756e79b39f41f552d526800f9fb0ad0a74f742ac8de61b6416a3a", + "h1:T4XsCHDT839VehWKdxbVsLn0ECjcQaUTzbSGW055pgM=", + "zh:0d8ae6d6e87f44ed4a178be03d6466339b0bb578ab54c2677e365a8281b0bb7d", + "zh:29d250d1a18d49652b28f234ecd17687b36c875dc47877a678e587d5d136b054", + "zh:2e69ba373cf009e8a60b36d04f3dbc4638708d1bf88be9f96b3e52cbf8f47f31", + "zh:53d525dd84ac63b5f749bfbc6b70a202dacf29597664d2ab1165efea6f24f630", + "zh:a25024d574ccd5ae6c2962f3bb71d510f62899f493b1ed096f2f7f0e2b18f975", + "zh:aabc64fe64319b95aaba1d1866f87abc7b10adae37d2eafa2f85f37317fdd49f", + "zh:acc6a977814897cb23d3b3753213281334238f8bce6d2b21e9f04fc4087ee980", + "zh:b24987e9416c39cd59c0fa41c139a97406b9955f0607fcafbf3315014456338a", + "zh:c550eae45fd32acdbe32b4e5c450ae95df6cb18903ac7216b1b07b23a16ce045", + "zh:c8f83b763b643893dcb6933a6bcee824cb514e06e7e5c5f5ac4ba187e66d7e22", + "zh:dcdac07e7ea18464dea729717870c275de9453775243c231e1fb305cad0ee597", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } provider "registry.terraform.io/hashicorp/local" { - version = "2.1.0" + version = "2.2.3" hashes = [ - "h1:EYZdckuGU3n6APs97nS2LxZm3dDtGqyM4qaIvsmac8o=", - "zh:0f1ec65101fa35050978d483d6e8916664b7556800348456ff3d09454ac1eae2", - "zh:36e42ac19f5d68467aacf07e6adcf83c7486f2e5b5f4339e9671f68525fc87ab", - "zh:6db9db2a1819e77b1642ec3b5e95042b202aee8151a0256d289f2e141bf3ceb3", - "zh:719dfd97bb9ddce99f7d741260b8ece2682b363735c764cac83303f02386075a", - "zh:7598bb86e0378fd97eaa04638c1a4c75f960f62f69d3662e6d80ffa5a89847fe", - "zh:ad0a188b52517fec9eca393f1e2c9daea362b33ae2eb38a857b6b09949a727c1", - "zh:c46846c8df66a13fee6eff7dc5d528a7f868ae0dcf92d79deaac73cc297ed20c", - "zh:dc1a20a2eec12095d04bf6da5321f535351a594a636912361db20eb2a707ccc4", - "zh:e57ab4771a9d999401f6badd8b018558357d3cbdf3d33cc0c4f83e818ca8e94b", - "zh:ebdcde208072b4b0f8d305ebf2bfdc62c926e0717599dcf8ec2fd8c5845031c3", - "zh:ef34c52b68933bedd0868a13ccfd59ff1c820f299760b3c02e008dc95e2ece91", + "h1:aWp5iSUxBGgPv1UnV5yag9Pb0N+U1I0sZb38AXBFO8A=", + "zh:04f0978bb3e052707b8e82e46780c371ac1c66b689b4a23bbc2f58865ab7d5c0", + "zh:6484f1b3e9e3771eb7cc8e8bab8b35f939a55d550b3f4fb2ab141a24269ee6aa", + "zh:78a56d59a013cb0f7eb1c92815d6eb5cf07f8b5f0ae20b96d049e73db915b238", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8aa9950f4c4db37239bcb62e19910c49e47043f6c8587e5b0396619923657797", + "zh:996beea85f9084a725ff0e6473a4594deb5266727c5f56e9c1c7c62ded6addbb", + "zh:9a7ef7a21f48fabfd145b2e2a4240ca57517ad155017e86a30860d7c0c109de3", + "zh:a63e70ac052aa25120113bcddd50c1f3cfe61f681a93a50cea5595a4b2cc3e1c", + "zh:a6e8d46f94108e049ad85dbed60354236dc0b9b5ec8eabe01c4580280a43d3b8", + "zh:bb112ce7efbfcfa0e65ed97fa245ef348e0fd5bfa5a7e4ab2091a9bd469f0a9e", + "zh:d7bec0da5c094c6955efed100f3fe22fca8866859f87c025be1760feb174d6d9", + "zh:fb9f271b72094d07cef8154cd3d50e9aa818a0ea39130bc193132ad7b23076fd", ] } provider "registry.terraform.io/hashicorp/random" { - version = "3.1.0" + version = "3.2.0" hashes = [ - "h1:BZMEPucF+pbu9gsPk0G0BHx7YP04+tKdq2MrRDF1EDM=", - "zh:2bbb3339f0643b5daa07480ef4397bd23a79963cc364cdfbb4e86354cb7725bc", - "zh:3cd456047805bf639fbf2c761b1848880ea703a054f76db51852008b11008626", - "zh:4f251b0eda5bb5e3dc26ea4400dba200018213654b69b4a5f96abee815b4f5ff", - "zh:7011332745ea061e517fe1319bd6c75054a314155cb2c1199a5b01fe1889a7e2", - "zh:738ed82858317ccc246691c8b85995bc125ac3b4143043219bd0437adc56c992", - "zh:7dbe52fac7bb21227acd7529b487511c91f4107db9cc4414f50d04ffc3cab427", - "zh:a3a9251fb15f93e4cfc1789800fc2d7414bbc18944ad4c5c98f466e6477c42bc", - "zh:a543ec1a3a8c20635cf374110bd2f87c07374cf2c50617eee2c669b3ceeeaa9f", - "zh:d9ab41d556a48bd7059f0810cf020500635bfc696c9fc3adab5ea8915c1d886b", - "zh:d9e13427a7d011dbd654e591b0337e6074eef8c3b9bb11b2e39eaaf257044fd7", - "zh:f7605bd1437752114baf601bdf6931debe6dc6bfe3006eb7e9bb9080931dca8a", + "h1:eeUh6cJ6wKLLuo4q9uQ0CA1Zvfqya4Wn1LecLCN8KKs=", + "zh:2960977ce9a7d6a7d3e934e75ec5814735626f95c186ad95a9102344a1a38ac1", + "zh:2fd012abfabe7076f3f2f402eeef4970e20574d20ffec57c162b02b6e848c32f", + "zh:4cd3234671cf01c913023418b227eb78b0659f2cd2e0b387be1f0bb607d29889", + "zh:52e695b4fa3fae735ffc901edff8183745f980923510a744db7616e8f10dc499", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:848b4a294e5ba15192ee4bfd199c07f60a437d7572efcd2d89db036e1ebc0e6e", + "zh:9d49aa432a05748a9527e95448cebee1238c87c97c7e8dec694bfd709683f9c7", + "zh:b4ad4cf289d3f7408649b74b8639918833613f2a1f3cf51b51f4b2fdaa412dd2", + "zh:c1544c4b416096fb8d8dbf84c4488584a2844a30dd533b957e9e9e60a165f24e", + "zh:dc737d6b4591cad8c9a1d0b347e587e846d8d901789b29b4dd401b6cdf82c017", + "zh:f5645fd39f749dbbf847cbdc87ba0dbd141143f12917a6a8904faf8a9b64111e", + "zh:fdedf610e0d020878a8f1fedda8105e0c33a7e23c4792fca54460685552de308", ] } diff --git a/templates/workspace_services/gitea/terraform/gitea-webapp.tf b/templates/workspace_services/gitea/terraform/gitea-webapp.tf index 6e595dff26..be7e0bac21 100644 --- a/templates/workspace_services/gitea/terraform/gitea-webapp.tf +++ b/templates/workspace_services/gitea/terraform/gitea-webapp.tf @@ -75,6 +75,7 @@ resource "azurerm_app_service" "gitea" { min_tls_version = "1.2" vnet_route_all_enabled = true websockets_enabled = false + ftps_state = "Disabled" } storage_account { diff --git a/templates/workspace_services/gitea/terraform/main.tf b/templates/workspace_services/gitea/terraform/main.tf index accc59ca4d..7287dba7d5 100644 --- a/templates/workspace_services/gitea/terraform/main.tf +++ b/templates/workspace_services/gitea/terraform/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "=2.97.0" + version = "=3.5.0" } } diff --git a/templates/workspace_services/gitea/version.txt b/templates/workspace_services/gitea/version.txt index aa0be21390..260c070a89 100644 --- a/templates/workspace_services/gitea/version.txt +++ b/templates/workspace_services/gitea/version.txt @@ -1 +1 @@ -__version__ = "0.2.17" +__version__ = "0.3.1" diff --git a/templates/workspace_services/guacamole/.env.sample b/templates/workspace_services/guacamole/.env.sample index 9a1eb81075..0f1ba1666b 100644 --- a/templates/workspace_services/guacamole/.env.sample +++ b/templates/workspace_services/guacamole/.env.sample @@ -1,10 +1,25 @@ # GUID to identify the workspace service ID=__CHANGE_ME__ +TRE_RESOURCE_ID=__SAME_AS_ID__ # GUID to identify the workspace bundle WORKSPACE_ID="__CHANGE_ME__" # Guacamole image tag to use (version in templates\workspace\services\guacamole\version.txt) GUACAMOLE_IMAGE_TAG="__CHANGE_ME__" -WS_CLIENT_ID="__CHANGE_ME__" -WS_CLIENT_SECRET="__CHANGE_ME__" +MGMT_ACR_NAME="__CHANGE_ME__" + +# This is scope_id of the workspace (api://) and you can find this as a workspace +# property in cosmos +WORKSPACE_IDENTIFIER_URI="__CHANGE_ME__" + +ARM_USE_MSI=false +GUAC_DISABLE_COPY=true +GUAC_DISABLE_PASTE=false +GUAC_ENABLE_DRIVE=true +GUAC_DRIVE_NAME="transfer" +GUAC_DRIVE_PATH="/guac-transfer" +GUAC_DISABLE_DOWNLOAD=true +IS_EXPOSED_EXTERNALLY=false +image_name="guac-server" +image_tag="" diff --git a/templates/workspace_services/guacamole/guacamole-server/docker/Dockerfile b/templates/workspace_services/guacamole/guacamole-server/docker/Dockerfile index c9ab6fa88b..d2d9165231 100644 --- a/templates/workspace_services/guacamole/guacamole-server/docker/Dockerfile +++ b/templates/workspace_services/guacamole/guacamole-server/docker/Dockerfile @@ -1,12 +1,12 @@ -FROM maven:3-jdk-11-openj9 AS client_build +FROM maven:3-jdk-11 AS client_build -COPY ./guacamole-auth-azure/pom.xml pom.xml -COPY ./guacamole-auth-azure/src src +COPY ./guacamole-auth-azure/pom.xml /pom.xml +COPY ./guacamole-auth-azure/src /src COPY ./docker/maven_package_and_exit_succesfully.sh /tmp/ RUN bash /tmp/maven_package_and_exit_succesfully.sh FROM scratch as test-results -COPY --from=client_build /target/surefire-reports/* ./ +COPY --from=client_build /target/surefire-reports/* / FROM guacamole/guacd:1.4.0 @@ -14,27 +14,31 @@ ARG GUACAMOLE_AZURE_VERSION=0.1.1 ENV DEBIAN_FRONTEND=noninteractive +# https://github.com/microsoft/AzureTRE/issues/1937 +# hadolint ignore=DL3002 USER root # dependencies - -RUN apt-get update && apt-get install wget curl openssh-server apt-transport-https gnupg -y && \ +# hadolint ignore=DL3008 +RUN apt-get update && apt-get install wget curl openssh-server apt-transport-https gnupg -y --no-install-recommends && \ apt-get autoclean && apt-get autoremove && rm -rf /var/lib/apt/lists/* -RUN wget https://github.com/AdoptOpenJDK/openjdk11-binaries/releases/download/jdk-11.0.11%2B9/OpenJDK11U-jre_x64_linux_hotspot_11.0.11_9.tar.gz && \ - mkdir -p /usr/lib/jvm/java-11-openjdk-amd64/ && \ - tar xzf OpenJDK11U-jre_x64_linux_hotspot_11.0.11_9.tar.gz -C /usr/lib/jvm/java-11-openjdk-amd64/ --strip-components=1 && \ - export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH && java -version - -RUN \ - TOMCAT_VER=`curl --silent http://mirror.vorboss.net/apache/tomcat/tomcat-9/ | grep v9 -m 1 | awk '{split($5,c,">v") ; split(c[2],d,"/") ; print d[1]}'` && \ - wget -N http://mirror.vorboss.net/apache/tomcat/tomcat-9/v${TOMCAT_VER}/bin/apache-tomcat-${TOMCAT_VER}.tar.gz &&\ - tar xzf apache-tomcat-${TOMCAT_VER}.tar.gz && \ - rm -f apache-tomcat-${TOMCAT_VER}.tar.gz && \ - mv apache-tomcat-${TOMCAT_VER}/ /usr/share/tomcat9/ - ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 +RUN JAVA_ARCHIVE=java.tar.gz && \ + wget -O "$JAVA_ARCHIVE" "https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.15%2B10/OpenJDK11U-jre_x64_linux_hotspot_11.0.15_10.tar.gz" --progress=dot:giga && \ + mkdir -p "$JAVA_HOME" && \ + tar xzf "$JAVA_ARCHIVE" -C "$JAVA_HOME" --strip-components=1 && \ + rm -f "$JAVA_ARCHIVE" && \ + export PATH="$JAVA_HOME"/bin:"$PATH" && java -version + ENV CATALINA_BASE=/usr/share/tomcat9/ +RUN TOMCAT_ARCHIVE=tomcat.tar.gz && \ + TOMCAT_VER=9.0.62 && \ + wget -O "$TOMCAT_ARCHIVE" -N "http://mirror.vorboss.net/apache/tomcat/tomcat-9/v${TOMCAT_VER}/bin/apache-tomcat-${TOMCAT_VER}.tar.gz" --progress=dot:giga && \ + tar xzf "$TOMCAT_ARCHIVE" && \ + rm -f "$TOMCAT_ARCHIVE" && \ + mv apache-tomcat-${TOMCAT_VER}/ "$CATALINA_BASE" + ENV GUACAMOLE_HOME=/guacamole/ ENV GUACAMOLE_LIB="${GUACAMOLE_HOME}/lib/" ENV CLASSPATH=${GUACAMOLE_LIB}:${CLASSPATH} @@ -43,23 +47,29 @@ RUN mkdir /guac-transfer COPY ./docker/guacamole/ ${GUACAMOLE_HOME} -RUN wget -O s6-overlay.tar.gz https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-amd64.tar.gz && tar xzvf s6-overlay.tar.gz -C / +RUN S6_ARCHIVE=s6-overlay.tar.gz && \ + wget -O "$S6_ARCHIVE" "https://github.com/just-containers/s6-overlay/releases/download/v2.2.0.3/s6-overlay-amd64.tar.gz" --progress=dot:giga && \ + tar xzvf "$S6_ARCHIVE" -C / && \ + rm -f "$S6_ARCHIVE" COPY ./docker/sshd_config /etc/ssh/ COPY ./docker/services /etc/services.d/ # retrieve auth integration from build image -COPY --from=client_build /target/lib/* ${GUACAMOLE_LIB} -COPY --from=client_build /target/guacamole-auth-tre-${GUACAMOLE_AZURE_VERSION}.jar "${GUACAMOLE_HOME}/extensions/" - -RUN wget -O ${GUACAMOLE_HOME}/guacamole.war 'http://apache.org/dyn/closer.cgi?action=download&filename=guacamole/1.4.0/binary/guacamole-1.4.0.war' +COPY --from=client_build /target/lib/* "${GUACAMOLE_LIB}" +COPY --from=client_build "/target/guacamole-auth-tre-${GUACAMOLE_AZURE_VERSION}.jar" "${GUACAMOLE_HOME}/extensions/" -RUN mkdir /etc/oauth2 +RUN wget -O "${GUACAMOLE_HOME}/guacamole.war" "http://apache.org/dyn/closer.cgi?action=download&filename=guacamole/1.4.0/binary/guacamole-1.4.0.war" --progress=dot:giga -RUN wget -O /etc/oauth2/oauth2-proxy-v7.2.1.linux-amd64.tar.gz 'https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v7.2.1/oauth2-proxy-v7.2.1.linux-amd64.tar.gz' +ENV OAUTH2_PROXY_HOME=/etc/oauth2-proxy +RUN OAUTH2_PROXY_ARCHIVE=oauth2-proxy.tar.gz && \ + wget -O "$OAUTH2_PROXY_ARCHIVE" "https://github.com/oauth2-proxy/oauth2-proxy/releases/download/v7.2.1/oauth2-proxy-v7.2.1.linux-amd64.tar.gz" --progress=dot:giga && \ + mkdir -p "$OAUTH2_PROXY_HOME" && \ + tar zxpf "$OAUTH2_PROXY_ARCHIVE" -C "$OAUTH2_PROXY_HOME" --strip-components=1 && \ + rm -f "$OAUTH2_PROXY_ARCHIVE" -COPY ./docker/index.jsp /usr/share/tomcat9/webapps/ROOT/index.jsp +COPY ./docker/index.jsp "$CATALINA_BASE"/webapps/ROOT/index.jsp ENTRYPOINT [ "/init" ] diff --git a/templates/workspace_services/guacamole/guacamole-server/docker/services/oauth/run b/templates/workspace_services/guacamole/guacamole-server/docker/services/oauth/run index 422935a1d8..7b66bc0d88 100644 --- a/templates/workspace_services/guacamole/guacamole-server/docker/services/oauth/run +++ b/templates/workspace_services/guacamole/guacamole-server/docker/services/oauth/run @@ -2,28 +2,23 @@ set -x echo >&2 "starting oauth2-proxy" -tar zxvpf /etc/oauth2/oauth2-proxy-v7.2.1.linux-amd64.tar.gz - -chmod +x ./oauth2-proxy-v7.2.1.linux-amd64/oauth2-proxy -./oauth2-proxy-v7.2.1.linux-amd64/oauth2-proxy --version - cookiesecret=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tr -d -- '\n' | tr -- '+/' '-_'; echo) -./oauth2-proxy-v7.2.1.linux-amd64/oauth2-proxy \ +"${OAUTH2_PROXY_HOME}"/oauth2-proxy \ --provider oidc \ --skip-provider-button \ --cookie-secret "${cookiesecret}" \ --oidc-issuer-url "${OAUTH2_PROXY_OIDC_ISSUER_URL}" \ ---upstream http://0.0.0.0:8080 \ ---email-domain "${OAUTH2_PROXY_EMAIL_DOMAIN}" \ +--upstream http://0.0.0.0:8080 \ +--email-domain "${OAUTH2_PROXY_EMAIL_DOMAIN}" \ --redirect-url "${OAUTH2_PROXY_REDIRECT_URI}" --pass-host-header true \ --show-debug-on-error true --pass-authorization-header true --pass-user-headers true \ --http-address http://0.0.0.0:8085 \ --https-address https://0.0.0.0:8086 \ ---cookie-secure false \ +--cookie-secure true \ --reverse-proxy true \ --pass-access-token true \ --set-xauthrequest true \ --pass-basic-auth true \ --cookie-refresh 50m \ ---scope "openid offline_access api://${AUDIENCE}/user_impersonation" +--scope "openid offline_access ${AUDIENCE}/user_impersonation" diff --git a/templates/workspace_services/guacamole/parameters.json b/templates/workspace_services/guacamole/parameters.json index b4e8a56251..58ea44d531 100755 --- a/templates/workspace_services/guacamole/parameters.json +++ b/templates/workspace_services/guacamole/parameters.json @@ -33,18 +33,6 @@ "env": "MGMT_RESOURCE_GROUP_NAME" } }, - { - "name": "ws_client_id", - "source": { - "env": "WS_CLIENT_ID" - } - }, - { - "name": "ws_client_secret", - "source": { - "env": "WS_CLIENT_SECRET" - } - }, { "name": "tfstate_container_name", "source": { diff --git a/templates/workspace_services/guacamole/porter.yaml b/templates/workspace_services/guacamole/porter.yaml index 39a0be920c..bee12c0a6b 100644 --- a/templates/workspace_services/guacamole/porter.yaml +++ b/templates/workspace_services/guacamole/porter.yaml @@ -1,6 +1,6 @@ --- name: tre-service-guacamole -version: 0.1.15 +version: 0.3.5 description: "An Azure TRE service for Guacamole" registry: azuretre dockerfile: Dockerfile.tmpl @@ -73,18 +73,11 @@ parameters: description: "Determines if the web app will be available over public/internet or private networks" - - name: ws_client_id + - name: workspace_identifier_uri type: string - env: WS_CLIENT_ID + env: WORKSPACE_IDENTIFIER_URI description: - "The WS client ID which should be submitted to the OpenID - service when necessary. This value is typically provided to you - when you create the ws application" - - name: ws_client_secret - type: string - env: WS_CLIENT_SECRET - description: - "The WS client secret" + "This is used to define the scope of the oauth2 redirection (e.g. api://treid_ws_wsid)" # the following are added automatically by the resource processor - name: id type: string @@ -139,9 +132,8 @@ install: guac_drive_path: "{{ bundle.parameters.guac_drive_path }}" guac_disable_download: "{{ bundle.parameters.guac_disable_download }}" is_exposed_externally: "{{ bundle.parameters.is_exposed_externally }}" - ws_client_id: "{{ bundle.parameters.ws_client_id }}" - ws_client_secret: "{{ bundle.parameters.ws_client_secret }}" tre_resource_id: "{{ bundle.parameters.id }}" + workspace_identifier_uri: "{{ bundle.parameters.workspace_identifier_uri }}" backendConfig: resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" @@ -181,9 +173,8 @@ uninstall: guac_drive_path: "{{ bundle.parameters.guac_drive_path }}" guac_disable_download: "{{ bundle.parameters.guac_disable_download }}" is_exposed_externally: "{{ bundle.parameters.is_exposed_externally }}" - ws_client_id: "{{ bundle.parameters.ws_client_id }}" - ws_client_secret: "{{ bundle.parameters.ws_client_secret }}" tre_resource_id: "{{ bundle.parameters.id }}" + workspace_identifier_uri: "{{ bundle.parameters.workspace_identifier_uri }}" backendConfig: resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" diff --git a/templates/workspace_services/guacamole/template_schema.json b/templates/workspace_services/guacamole/template_schema.json index c823e979ac..707c2ceff5 100644 --- a/templates/workspace_services/guacamole/template_schema.json +++ b/templates/workspace_services/guacamole/template_schema.json @@ -40,17 +40,11 @@ "title": "Expose externally", "description": "Is the Guacamole service exposed outside of the vnet" }, - "ws_client_id": { - "$id": "#/properties/ws_client_id", + "workspace_identifier_uri": { + "$id": "#/properties/workspace_identifier_uri", "type": "string", - "title": "workspace app client id", - "description": "The workspace app client ID which should be submitted to the OpenID service when necessary. This value is typically provided to you by the OpenID service when OpenID credentials are generated for your application." - }, - "ws_client_secret": { - "$id": "#/properties/ws_client_secret", - "type": "string", - "title": "workspace app client secret", - "description": "The workspace app client secret which should be submitted to the OpenID service when necessary. This value is typically provided to you by the OpenID service when OpenID credentials are generated for your application." + "title": "Workspace Identifier URI (Scope)", + "description": "The audience/scope/identifier uri of the workspace in AAD" } } } diff --git a/templates/workspace_services/guacamole/terraform/.terraform.lock.hcl b/templates/workspace_services/guacamole/terraform/.terraform.lock.hcl index aaf49c54bd..5021a13caa 100644 --- a/templates/workspace_services/guacamole/terraform/.terraform.lock.hcl +++ b/templates/workspace_services/guacamole/terraform/.terraform.lock.hcl @@ -2,57 +2,78 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/azurerm" { - version = "2.97.0" - constraints = "2.97.0" + version = "3.5.0" + constraints = "3.5.0" hashes = [ - "h1:XxT+XM/leTXa21aTnJjPBfNBQ8cLE4gYDg01WEZsV1U=", - "zh:0aac80e6d2b8ddf33d558ac893d52688e8abf8a0b995cfc3c35eb84afbf432a3", - "zh:11191068cb732208ebc8662651782f63db329a25f7ea1cd50cd91622a2c247b7", - "zh:36c8334194e7d605682053c7c70fbb2a650d9b0a7bcc44d5cdda4f205818438a", - "zh:3a5e01276added995e875b42ecc6b36ff73d267f0c096c87195bd2b1fff4f5b2", - "zh:557e38371657e6ed8aae9192d01480c4cca7c0f7ade6022f1aec247a6384922b", - "zh:67b913c280c5858549477a4b05e77078b1a5234de77c7bddd4ee1e8e237d5665", - "zh:7aeca864ce45b295db734cd968f7596ff12cd7c522ee89d53f432dae7c2b5d18", - "zh:b6127d7a796eaf9756dd212667eb48f79c0e78729589ec8ccf68e0b36ebb4e54", - "zh:bed448238740f897d1b399e5123b3a9eba256b981846f9ee92b71493446ca684", - "zh:c351a1bba34c3bd06fff75e4c15e4db0456268479463c2471598068ea1c5c884", - "zh:d073c24d0a4756e79b39f41f552d526800f9fb0ad0a74f742ac8de61b6416a3a", + "h1:T4XsCHDT839VehWKdxbVsLn0ECjcQaUTzbSGW055pgM=", + "zh:0d8ae6d6e87f44ed4a178be03d6466339b0bb578ab54c2677e365a8281b0bb7d", + "zh:29d250d1a18d49652b28f234ecd17687b36c875dc47877a678e587d5d136b054", + "zh:2e69ba373cf009e8a60b36d04f3dbc4638708d1bf88be9f96b3e52cbf8f47f31", + "zh:53d525dd84ac63b5f749bfbc6b70a202dacf29597664d2ab1165efea6f24f630", + "zh:a25024d574ccd5ae6c2962f3bb71d510f62899f493b1ed096f2f7f0e2b18f975", + "zh:aabc64fe64319b95aaba1d1866f87abc7b10adae37d2eafa2f85f37317fdd49f", + "zh:acc6a977814897cb23d3b3753213281334238f8bce6d2b21e9f04fc4087ee980", + "zh:b24987e9416c39cd59c0fa41c139a97406b9955f0607fcafbf3315014456338a", + "zh:c550eae45fd32acdbe32b4e5c450ae95df6cb18903ac7216b1b07b23a16ce045", + "zh:c8f83b763b643893dcb6933a6bcee824cb514e06e7e5c5f5ac4ba187e66d7e22", + "zh:dcdac07e7ea18464dea729717870c275de9453775243c231e1fb305cad0ee597", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } provider "registry.terraform.io/hashicorp/external" { - version = "2.2.0" + version = "2.2.2" hashes = [ - "h1:iU5OVMibHvIxbj2Dye1q3aYpjYXS3bKL9iZWZyh+xTg=", - "zh:094c3cfae140fbb70fb0e272b1df833b4d7467c6c819fbf59a3e8ac0922f95b6", - "zh:15c3906abbc1cd03a72afd02bda9caeeb5f6ca421292c32ddeb2acd7a3488669", - "zh:388c14bceeb1593bb16cadedc8f5ad7d41d398197db049dc0871bc847aa61083", - "zh:5696772136b6763faade0cc065fafc2bf06493021b943826be0144790fae514a", - "zh:6427c693b1b750644d5b633395e54617dc36ae717a531a5cde8cb0246b6593ca", - "zh:7196d9845eeffa3158f5e3067bf8b7ad489490aa26d29e2da1ad4c8924463469", + "h1:e7RpnZ2PbJEEPnfsg7V0FNwbfSk0/Z3FdrLsXINBmDY=", + "zh:0b84ab0af2e28606e9c0c1289343949339221c3ab126616b831ddb5aaef5f5ca", + "zh:10cf5c9b9524ca2e4302bf02368dc6aac29fb50aeaa6f7758cce9aa36ae87a28", + "zh:56a016ee871c8501acb3f2ee3b51592ad7c3871a1757b098838349b17762ba6b", + "zh:719d6ef39c50e4cffc67aa67d74d195adaf42afcf62beab132dafdb500347d39", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", - "zh:8850d3ce9e5f5776b9349890ce4e2c4056defe16ed741dc845045942a6d9e025", - "zh:a2c6fc6cf087b35ebd6b6f20272ed32d4217ea9936c1dd630baa46d86718a455", - "zh:ac709be4ea5c9a6e1ab80e864d24cd9f8e6aaea29fb5dbe1de0897e2e86c3c17", - "zh:dcf806f044801fae5b21ae2754dc3c19c68e458d4584965752ce49be75305ff5", - "zh:f875b34be86c3439899828978638ef7e2d41a9e5e32397858a0c31daeaa1abc2", + "zh:7fbfc4d37435ac2f717b0316f872f558f608596b389b895fcb549f118462d327", + "zh:8ac71408204db606ce63fe8f9aeaf1ddc7751d57d586ec421e62d440c402e955", + "zh:a4cacdb06f114454b6ed0033add28006afa3f65a0ea7a43befe45fc82e6809fb", + "zh:bb5ce3132b52ae32b6cc005bc9f7627b95259b9ffe556de4dad60d47d47f21f0", + "zh:bb60d2976f125ffd232a7ccb4b3f81e7109578b23c9c6179f13a11d125dca82a", + "zh:f9540ecd2e056d6e71b9ea5f5a5cf8f63dd5c25394b9db831083a9d4ea99b372", + "zh:ffd998b55b8a64d4335a090b6956b4bf8855b290f7554dd38db3302de9c41809", + ] +} + +provider "registry.terraform.io/hashicorp/local" { + version = "2.2.3" + hashes = [ + "h1:aWp5iSUxBGgPv1UnV5yag9Pb0N+U1I0sZb38AXBFO8A=", + "zh:04f0978bb3e052707b8e82e46780c371ac1c66b689b4a23bbc2f58865ab7d5c0", + "zh:6484f1b3e9e3771eb7cc8e8bab8b35f939a55d550b3f4fb2ab141a24269ee6aa", + "zh:78a56d59a013cb0f7eb1c92815d6eb5cf07f8b5f0ae20b96d049e73db915b238", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8aa9950f4c4db37239bcb62e19910c49e47043f6c8587e5b0396619923657797", + "zh:996beea85f9084a725ff0e6473a4594deb5266727c5f56e9c1c7c62ded6addbb", + "zh:9a7ef7a21f48fabfd145b2e2a4240ca57517ad155017e86a30860d7c0c109de3", + "zh:a63e70ac052aa25120113bcddd50c1f3cfe61f681a93a50cea5595a4b2cc3e1c", + "zh:a6e8d46f94108e049ad85dbed60354236dc0b9b5ec8eabe01c4580280a43d3b8", + "zh:bb112ce7efbfcfa0e65ed97fa245ef348e0fd5bfa5a7e4ab2091a9bd469f0a9e", + "zh:d7bec0da5c094c6955efed100f3fe22fca8866859f87c025be1760feb174d6d9", + "zh:fb9f271b72094d07cef8154cd3d50e9aa818a0ea39130bc193132ad7b23076fd", ] } provider "registry.terraform.io/hashicorp/null" { - version = "3.1.0" + version = "3.1.1" hashes = [ - "h1:vpC6bgUQoJ0znqIKVFevOdq+YQw42bRq0u+H3nto8nA=", - "zh:02a1675fd8de126a00460942aaae242e65ca3380b5bb192e8773ef3da9073fd2", - "zh:53e30545ff8926a8e30ad30648991ca8b93b6fa496272cd23b26763c8ee84515", - "zh:5f9200bf708913621d0f6514179d89700e9aa3097c77dac730e8ba6e5901d521", - "zh:9ebf4d9704faba06b3ec7242c773c0fbfe12d62db7d00356d4f55385fc69bfb2", - "zh:a6576c81adc70326e4e1c999c04ad9ca37113a6e925aefab4765e5a5198efa7e", - "zh:a8a42d13346347aff6c63a37cda9b2c6aa5cc384a55b2fe6d6adfa390e609c53", - "zh:c797744d08a5307d50210e0454f91ca4d1c7621c68740441cf4579390452321d", - "zh:cecb6a304046df34c11229f20a80b24b1603960b794d68361a67c5efe58e62b8", - "zh:e1371aa1e502000d9974cfaff5be4cfa02f47b17400005a16f14d2ef30dc2a70", - "zh:fc39cc1fe71234a0b0369d5c5c7f876c71b956d23d7d6f518289737a001ba69b", - "zh:fea4227271ebf7d9e2b61b89ce2328c7262acd9fd190e1fd6d15a591abfa848e", + "h1:71sNUDvmiJcijsvfXpiLCz0lXIBSsEJjMxljt7hxMhw=", + "zh:063466f41f1d9fd0dd93722840c1314f046d8760b1812fa67c34de0afcba5597", + "zh:08c058e367de6debdad35fc24d97131c7cf75103baec8279aba3506a08b53faf", + "zh:73ce6dff935150d6ddc6ac4a10071e02647d10175c173cfe5dca81f3d13d8afe", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8fdd792a626413502e68c195f2097352bdc6a0df694f7df350ed784741eb587e", + "zh:976bbaf268cb497400fd5b3c774d218f3933271864345f18deebe4dcbfcd6afa", + "zh:b21b78ca581f98f4cdb7a366b03ae9db23a73dfa7df12c533d7c19b68e9e72e5", + "zh:b7fc0c1615dbdb1d6fd4abb9c7dc7da286631f7ca2299fb9cd4664258ccfbff4", + "zh:d1efc942b2c44345e0c29bc976594cb7278c38cfb8897b344669eafbc3cddf46", + "zh:e356c245b3cd9d4789bab010893566acace682d7db877e52d40fc4ca34a50924", + "zh:ea98802ba92fcfa8cf12cbce2e9e7ebe999afbf8ed47fa45fc847a098d89468b", + "zh:eff8872458806499889f6927b5d954560f3d74bf20b6043409edf94d26cd906f", ] } diff --git a/templates/workspace_services/guacamole/terraform/firewall.tf b/templates/workspace_services/guacamole/terraform/firewall.tf index 41375ec09f..044aa42cf5 100644 --- a/templates/workspace_services/guacamole/terraform/firewall.tf +++ b/templates/workspace_services/guacamole/terraform/firewall.tf @@ -6,7 +6,9 @@ data "azurerm_firewall" "fw" { resource_group_name = "rg-${var.tre_id}" } -resource "null_resource" "az_login" { +resource "null_resource" "az_login_msi" { + count = var.arm_use_msi ? 1 : 0 + provisioner "local-exec" { command = "az login --identity -u '${data.azurerm_client_config.current.client_id}'" } @@ -15,6 +17,17 @@ resource "null_resource" "az_login" { } } +resource "null_resource" "az_login_spn" { + count = var.arm_use_msi ? 0 : 1 + + provisioner "local-exec" { + command = "az login --service-principal -u '${var.arm_client_id}' -p '${var.arm_client_secret}' -t '${var.arm_tenant_id}'" + } + triggers = { + timestamp = timestamp() + } +} + data "external" "rule_priorities" { program = ["bash", "-c", "./get_firewall_priorities.sh"] @@ -24,7 +37,8 @@ data "external" "rule_priorities" { service_resource_name_suffix = local.service_resource_name_suffix } depends_on = [ - null_resource.az_login + null_resource.az_login_msi, + null_resource.az_login_spn ] } diff --git a/templates/workspace_services/guacamole/terraform/main.tf b/templates/workspace_services/guacamole/terraform/main.tf index 6b2571cb86..f4f544bb7f 100644 --- a/templates/workspace_services/guacamole/terraform/main.tf +++ b/templates/workspace_services/guacamole/terraform/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "=2.97.0" + version = "=3.5.0" } } backend "azurerm" { @@ -39,6 +39,16 @@ data "azurerm_key_vault_secret" "aad_tenant_id" { key_vault_id = data.azurerm_key_vault.ws.id } +data "azurerm_key_vault_secret" "workspace_client_id" { + name = "workspace-client-id" + key_vault_id = data.azurerm_key_vault.ws.id +} + +data "azurerm_key_vault_secret" "workspace_client_secret" { + name = "workspace-client-secret" + key_vault_id = data.azurerm_key_vault.ws.id +} + data "azurerm_subnet" "web_apps" { name = "WebAppsSubnet" virtual_network_name = data.azurerm_virtual_network.ws.name diff --git a/templates/workspace_services/guacamole/terraform/variables.tf b/templates/workspace_services/guacamole/terraform/variables.tf index 4fd1e8bebb..805735c6ad 100644 --- a/templates/workspace_services/guacamole/terraform/variables.tf +++ b/templates/workspace_services/guacamole/terraform/variables.tf @@ -16,5 +16,4 @@ variable "guac_drive_path" {} variable "guac_disable_download" {} variable "is_exposed_externally" {} variable "tre_resource_id" {} -variable "ws_client_id" {} -variable "ws_client_secret" {} +variable "workspace_identifier_uri" {} diff --git a/templates/workspace_services/guacamole/terraform/web_app.tf b/templates/workspace_services/guacamole/terraform/web_app.tf index dd03e5ae0c..ea3ea51afa 100644 --- a/templates/workspace_services/guacamole/terraform/web_app.tf +++ b/templates/workspace_services/guacamole/terraform/web_app.tf @@ -25,33 +25,34 @@ resource "azurerm_app_service" "guacamole" { http2_enabled = true acr_use_managed_identity_credentials = true acr_user_managed_identity_client_id = azurerm_user_assigned_identity.guacamole_id.client_id + ftps_state = "Disabled" + vnet_route_all_enabled = true } app_settings = { WEBSITES_PORT = "8085" - WEBSITE_VNET_ROUTE_ALL = "1" WEBSITE_DNS_SERVER = "168.63.129.16" SCM_DO_BUILD_DURING_DEPLOYMENT = "True" TENANT_ID = data.azurerm_client_config.current.tenant_id KEYVAULT_URL = data.azurerm_key_vault.ws.vault_uri API_URL = local.api_url - SERVICE_ID = "${var.tre_resource_id}" - WORKSPACE_ID = "${var.workspace_id}" + SERVICE_ID = var.tre_resource_id + WORKSPACE_ID = var.workspace_id MANAGED_IDENTITY_CLIENT_ID = azurerm_user_assigned_identity.guacamole_id.client_id # Guacmole configuration - GUAC_DISABLE_COPY = "${var.guac_disable_copy}" - GUAC_DISABLE_PASTE = "${var.guac_disable_paste}" - GUAC_ENABLE_DRIVE = "${var.guac_enable_drive}" - GUAC_DRIVE_NAME = "${var.guac_drive_name}" - GUAC_DRIVE_PATH = "${var.guac_drive_path}" - GUAC_DISABLE_DOWNLOAD = "${var.guac_disable_download}" - AUDIENCE = "${var.ws_client_id}" + GUAC_DISABLE_COPY = var.guac_disable_copy + GUAC_DISABLE_PASTE = var.guac_disable_paste + GUAC_ENABLE_DRIVE = var.guac_enable_drive + GUAC_DRIVE_NAME = var.guac_drive_name + GUAC_DRIVE_PATH = var.guac_drive_path + GUAC_DISABLE_DOWNLOAD = var.guac_disable_download + AUDIENCE = var.workspace_identifier_uri ISSUER = local.issuer - OAUTH2_PROXY_CLIENT_ID = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.workspace_client_id.id})" - OAUTH2_PROXY_CLIENT_SECRET = "@Microsoft.KeyVault(SecretUri=${azurerm_key_vault_secret.workspace_client_secret.id})" + OAUTH2_PROXY_CLIENT_ID = "@Microsoft.KeyVault(SecretUri=${data.azurerm_key_vault_secret.workspace_client_id.id})" + OAUTH2_PROXY_CLIENT_SECRET = "@Microsoft.KeyVault(SecretUri=${data.azurerm_key_vault_secret.workspace_client_secret.id})" OAUTH2_PROXY_REDIRECT_URI = "https://${local.webapp_name}.azurewebsites.net/oauth2/callback" OAUTH2_PROXY_EMAIL_DOMAIN = "\"*\"" # oauth proxy will allow all email domains only when the value is "*" OAUTH2_PROXY_OIDC_ISSUER_URL = "https://login.microsoftonline.com/${local.aad_tenant_id}/v2.0" @@ -80,11 +81,6 @@ resource "azurerm_app_service" "guacamole" { type = "UserAssigned" identity_ids = [azurerm_user_assigned_identity.guacamole_id.id] } - - depends_on = [ - azurerm_key_vault_secret.workspace_client_id, - azurerm_key_vault_secret.workspace_client_secret - ] } resource "azurerm_monitor_diagnostic_setting" "guacamole" { @@ -222,22 +218,3 @@ resource "azurerm_key_vault_access_policy" "guacamole_policy" { secret_permissions = ["Get", "List", ] } - -resource "azurerm_key_vault_secret" "workspace_client_id" { - name = "workspace-client-id" - value = var.ws_client_id - key_vault_id = data.azurerm_key_vault.ws.id - depends_on = [ - azurerm_key_vault_access_policy.guacamole_policy - ] -} - -resource "azurerm_key_vault_secret" "workspace_client_secret" { - name = "workspace-client-secret" - value = var.ws_client_secret - key_vault_id = data.azurerm_key_vault.ws.id - depends_on = [ - azurerm_key_vault_access_policy.guacamole_policy - ] -} - diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/.dockerignore b/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/.dockerignore new file mode 100644 index 0000000000..97f0012a92 --- /dev/null +++ b/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/.dockerignore @@ -0,0 +1,2 @@ +# Local .terraform directories +**/.terraform/* diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/porter.yaml b/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/porter.yaml index 8c5874b840..5520924be6 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/porter.yaml +++ b/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/porter.yaml @@ -1,6 +1,6 @@ --- name: tre-service-guacamole-linuxvm -version: 0.1.11 +version: 0.3.2 description: "An Azure TRE User Resource Template for Guacamole (Linux)" registry: azuretre dockerfile: Dockerfile.tmpl @@ -56,6 +56,10 @@ parameters: - name: shared_storage_name type: string default: "vm-shared-storage" + - name: nexus_version + type: string + default: "V1" + description: "Which Nexus proxy service to use, i.e. V1 for the App Service-based Nexus or V2 for the VM-based service" outputs: - name: ip @@ -98,6 +102,7 @@ install: vm_size: "{{ bundle.parameters.vm_size }}" shared_storage_access: "{{ bundle.parameters.shared_storage_access }}" shared_storage_name: "{{ bundle.parameters.shared_storage_name }}" + nexus_version: "{{ bundle.parameters.nexus_version }}" backendConfig: resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" @@ -126,6 +131,7 @@ upgrade: vm_size: "{{ bundle.parameters.vm_size }}" shared_storage_access: "{{ bundle.parameters.shared_storage_access }}" shared_storage_name: "{{ bundle.parameters.shared_storage_name }}" + nexus_version: "{{ bundle.parameters.nexus_version }}" backendConfig: resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" @@ -154,6 +160,7 @@ uninstall: vm_size: "{{ bundle.parameters.vm_size }}" shared_storage_access: "{{ bundle.parameters.shared_storage_access }}" shared_storage_name: "{{ bundle.parameters.shared_storage_name }}" + nexus_version: "{{ bundle.parameters.nexus_version }}" backendConfig: resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/template_schema.json b/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/template_schema.json index 7e5027675f..27db5c3d0f 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/template_schema.json +++ b/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/template_schema.json @@ -29,6 +29,17 @@ "16 CPU | 64GB RAM" ], "updateable": true + }, + "nexus_version": { + "$id": "#/properties/nexus_version", + "type": "string", + "title": "Nexus", + "description": "Which Nexus proxy service to use, i.e. V1 for the App Service-based Nexus or V2 for the VM-based service", + "enum": [ + "V1", + "V2" + ], + "default": "V1" } } } diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/.terraform.lock.hcl b/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/.terraform.lock.hcl index 61c54c45c8..3748610cfc 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/.terraform.lock.hcl +++ b/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/.terraform.lock.hcl @@ -2,39 +2,41 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/azurerm" { - version = "2.97.0" - constraints = "2.97.0" + version = "3.5.0" + constraints = "3.5.0" hashes = [ - "h1:XxT+XM/leTXa21aTnJjPBfNBQ8cLE4gYDg01WEZsV1U=", - "zh:0aac80e6d2b8ddf33d558ac893d52688e8abf8a0b995cfc3c35eb84afbf432a3", - "zh:11191068cb732208ebc8662651782f63db329a25f7ea1cd50cd91622a2c247b7", - "zh:36c8334194e7d605682053c7c70fbb2a650d9b0a7bcc44d5cdda4f205818438a", - "zh:3a5e01276added995e875b42ecc6b36ff73d267f0c096c87195bd2b1fff4f5b2", - "zh:557e38371657e6ed8aae9192d01480c4cca7c0f7ade6022f1aec247a6384922b", - "zh:67b913c280c5858549477a4b05e77078b1a5234de77c7bddd4ee1e8e237d5665", - "zh:7aeca864ce45b295db734cd968f7596ff12cd7c522ee89d53f432dae7c2b5d18", - "zh:b6127d7a796eaf9756dd212667eb48f79c0e78729589ec8ccf68e0b36ebb4e54", - "zh:bed448238740f897d1b399e5123b3a9eba256b981846f9ee92b71493446ca684", - "zh:c351a1bba34c3bd06fff75e4c15e4db0456268479463c2471598068ea1c5c884", - "zh:d073c24d0a4756e79b39f41f552d526800f9fb0ad0a74f742ac8de61b6416a3a", + "h1:T4XsCHDT839VehWKdxbVsLn0ECjcQaUTzbSGW055pgM=", + "zh:0d8ae6d6e87f44ed4a178be03d6466339b0bb578ab54c2677e365a8281b0bb7d", + "zh:29d250d1a18d49652b28f234ecd17687b36c875dc47877a678e587d5d136b054", + "zh:2e69ba373cf009e8a60b36d04f3dbc4638708d1bf88be9f96b3e52cbf8f47f31", + "zh:53d525dd84ac63b5f749bfbc6b70a202dacf29597664d2ab1165efea6f24f630", + "zh:a25024d574ccd5ae6c2962f3bb71d510f62899f493b1ed096f2f7f0e2b18f975", + "zh:aabc64fe64319b95aaba1d1866f87abc7b10adae37d2eafa2f85f37317fdd49f", + "zh:acc6a977814897cb23d3b3753213281334238f8bce6d2b21e9f04fc4087ee980", + "zh:b24987e9416c39cd59c0fa41c139a97406b9955f0607fcafbf3315014456338a", + "zh:c550eae45fd32acdbe32b4e5c450ae95df6cb18903ac7216b1b07b23a16ce045", + "zh:c8f83b763b643893dcb6933a6bcee824cb514e06e7e5c5f5ac4ba187e66d7e22", + "zh:dcdac07e7ea18464dea729717870c275de9453775243c231e1fb305cad0ee597", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } provider "registry.terraform.io/hashicorp/random" { - version = "3.1.0" + version = "3.2.0" hashes = [ - "h1:BZMEPucF+pbu9gsPk0G0BHx7YP04+tKdq2MrRDF1EDM=", - "zh:2bbb3339f0643b5daa07480ef4397bd23a79963cc364cdfbb4e86354cb7725bc", - "zh:3cd456047805bf639fbf2c761b1848880ea703a054f76db51852008b11008626", - "zh:4f251b0eda5bb5e3dc26ea4400dba200018213654b69b4a5f96abee815b4f5ff", - "zh:7011332745ea061e517fe1319bd6c75054a314155cb2c1199a5b01fe1889a7e2", - "zh:738ed82858317ccc246691c8b85995bc125ac3b4143043219bd0437adc56c992", - "zh:7dbe52fac7bb21227acd7529b487511c91f4107db9cc4414f50d04ffc3cab427", - "zh:a3a9251fb15f93e4cfc1789800fc2d7414bbc18944ad4c5c98f466e6477c42bc", - "zh:a543ec1a3a8c20635cf374110bd2f87c07374cf2c50617eee2c669b3ceeeaa9f", - "zh:d9ab41d556a48bd7059f0810cf020500635bfc696c9fc3adab5ea8915c1d886b", - "zh:d9e13427a7d011dbd654e591b0337e6074eef8c3b9bb11b2e39eaaf257044fd7", - "zh:f7605bd1437752114baf601bdf6931debe6dc6bfe3006eb7e9bb9080931dca8a", + "h1:eeUh6cJ6wKLLuo4q9uQ0CA1Zvfqya4Wn1LecLCN8KKs=", + "zh:2960977ce9a7d6a7d3e934e75ec5814735626f95c186ad95a9102344a1a38ac1", + "zh:2fd012abfabe7076f3f2f402eeef4970e20574d20ffec57c162b02b6e848c32f", + "zh:4cd3234671cf01c913023418b227eb78b0659f2cd2e0b387be1f0bb607d29889", + "zh:52e695b4fa3fae735ffc901edff8183745f980923510a744db7616e8f10dc499", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:848b4a294e5ba15192ee4bfd199c07f60a437d7572efcd2d89db036e1ebc0e6e", + "zh:9d49aa432a05748a9527e95448cebee1238c87c97c7e8dec694bfd709683f9c7", + "zh:b4ad4cf289d3f7408649b74b8639918833613f2a1f3cf51b51f4b2fdaa412dd2", + "zh:c1544c4b416096fb8d8dbf84c4488584a2844a30dd533b957e9e9e60a165f24e", + "zh:dc737d6b4591cad8c9a1d0b347e587e846d8d901789b29b4dd401b6cdf82c017", + "zh:f5645fd39f749dbbf847cbdc87ba0dbd141143f12917a6a8904faf8a9b64111e", + "zh:fdedf610e0d020878a8f1fedda8105e0c33a7e23c4792fca54460685552de308", ] } diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/linuxvm.tf b/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/linuxvm.tf index 7ae293288b..dc929a2ddc 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/linuxvm.tf +++ b/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/linuxvm.tf @@ -97,7 +97,7 @@ data "template_file" "vm_config" { storage_account_key = data.azurerm_storage_account.stg.primary_access_key http_endpoint = data.azurerm_storage_account.stg.primary_file_endpoint fileshare_name = data.azurerm_storage_share.shared_storage.name - nexus_proxy_url = local.nexus_proxy_url + nexus_proxy_url = local.nexus_proxy_url[var.nexus_version] conda_config = local.image_ref[var.image].conda_config ? 1 : 0 } } @@ -105,14 +105,14 @@ data "template_file" "vm_config" { data "template_file" "pypi_sources_config" { template = file("${path.module}/pypi_sources_config.sh") vars = { - nexus_proxy_url = local.nexus_proxy_url + nexus_proxy_url = local.nexus_proxy_url[var.nexus_version] } } data "template_file" "apt_sources_config" { template = file("${path.module}/apt_sources_config.yml") vars = { - nexus_proxy_url = local.nexus_proxy_url + nexus_proxy_url = local.nexus_proxy_url[var.nexus_version] } } diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/locals.tf b/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/locals.tf index e684eb37d5..1f75144c03 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/locals.tf +++ b/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/locals.tf @@ -5,11 +5,15 @@ locals { workspace_resource_name_suffix = "${var.tre_id}-ws-${local.short_workspace_id}" service_resource_name_suffix = "${var.tre_id}-ws-${local.short_workspace_id}-svc-${local.short_service_id}" core_vnet = "vnet-${var.tre_id}" - core_resource_group_name = "rg-${var.tre_id}" + core_resource_group_name = data.azurerm_resource_group.core.name vm_name = "linuxvm${local.short_service_id}" keyvault_name = lower("kv-${substr(local.workspace_resource_name_suffix, -20, -1)}") storage_name = lower(replace("stg${substr(local.workspace_resource_name_suffix, -8, -1)}", "-", "")) - nexus_proxy_url = "https://nexus-${var.tre_id}.azurewebsites.net" + + nexus_proxy_url = { + "V1" = "https://nexus-${var.tre_id}.azurewebsites.net", + "V2" = "https://nexus-${var.tre_id}.${data.azurerm_resource_group.core.location}.cloudapp.azure.com" + } vm_size = { "2 CPU | 8GB RAM" = { value = "Standard_D2s_v5" }, "4 CPU | 16GB RAM" = { value = "Standard_D4s_v5" }, diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/main.tf b/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/main.tf index da10b88845..c0d76a1f20 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/main.tf +++ b/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "=2.97.0" + version = "=3.5.0" } } backend "azurerm" { @@ -21,6 +21,10 @@ data "azurerm_resource_group" "ws" { name = "rg-${var.tre_id}-ws-${local.short_workspace_id}" } +data "azurerm_resource_group" "core" { + name = "rg-${var.tre_id}" +} + data "azurerm_virtual_network" "ws" { name = "vnet-${var.tre_id}-ws-${local.short_workspace_id}" resource_group_name = data.azurerm_resource_group.ws.name diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/variables.tf b/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/variables.tf index 11b9a24565..3f3fd36c4d 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/variables.tf +++ b/templates/workspace_services/guacamole/user_resources/guacamole-azure-linuxvm/terraform/variables.tf @@ -12,3 +12,4 @@ variable "image" {} variable "vm_size" {} variable "shared_storage_access" {} variable "shared_storage_name" {} +variable "nexus_version" {} diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/.dockerignore b/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/.dockerignore new file mode 100644 index 0000000000..97f0012a92 --- /dev/null +++ b/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/.dockerignore @@ -0,0 +1,2 @@ +# Local .terraform directories +**/.terraform/* diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/porter.yaml b/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/porter.yaml index 1577773cef..c904a2fefd 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/porter.yaml +++ b/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/porter.yaml @@ -1,6 +1,6 @@ --- name: tre-service-guacamole-windowsvm -version: 0.1.11 +version: 0.3.2 description: "An Azure TRE User Resource Template for Guacamole (Windows 10)" registry: azuretre dockerfile: Dockerfile.tmpl @@ -56,6 +56,11 @@ parameters: - name: shared_storage_name type: string default: "vm-shared-storage" + - name: nexus_version + type: string + default: "V1" + description: "Which Nexus proxy service to use, i.e. V1 for the App Service-based Nexus or V2 for the VM-based service" + outputs: - name: ip @@ -98,6 +103,7 @@ install: vm_size: "{{ bundle.parameters.vm_size }}" shared_storage_access: "{{ bundle.parameters.shared_storage_access }}" shared_storage_name: "{{ bundle.parameters.shared_storage_name }}" + nexus_version: "{{ bundle.parameters.nexus_version }}" backendConfig: resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" @@ -126,6 +132,7 @@ upgrade: vm_size: "{{ bundle.parameters.vm_size }}" shared_storage_access: "{{ bundle.parameters.shared_storage_access }}" shared_storage_name: "{{ bundle.parameters.shared_storage_name }}" + nexus_version: "{{ bundle.parameters.nexus_version }}" backendConfig: resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" @@ -154,6 +161,7 @@ uninstall: vm_size: "{{ bundle.parameters.vm_size }}" shared_storage_access: "{{ bundle.parameters.shared_storage_access }}" shared_storage_name: "{{ bundle.parameters.shared_storage_name }}" + nexus_version: "{{ bundle.parameters.nexus_version }}" backendConfig: resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/template_schema.json b/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/template_schema.json index f4eb91c241..2e7f2a2433 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/template_schema.json +++ b/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/template_schema.json @@ -29,6 +29,17 @@ "16 CPU | 64GB RAM" ], "updateable": true + }, + "nexus_version": { + "$id": "#/properties/nexus_version", + "type": "string", + "title": "Nexus", + "description": "Which Nexus proxy service to use, i.e. V1 for the App Service-based Nexus or V2 for the VM-based service", + "enum": [ + "V1", + "V2" + ], + "default": "V1" } } } diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/.terraform.lock.hcl b/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/.terraform.lock.hcl index 61c54c45c8..3748610cfc 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/.terraform.lock.hcl +++ b/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/.terraform.lock.hcl @@ -2,39 +2,41 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/azurerm" { - version = "2.97.0" - constraints = "2.97.0" + version = "3.5.0" + constraints = "3.5.0" hashes = [ - "h1:XxT+XM/leTXa21aTnJjPBfNBQ8cLE4gYDg01WEZsV1U=", - "zh:0aac80e6d2b8ddf33d558ac893d52688e8abf8a0b995cfc3c35eb84afbf432a3", - "zh:11191068cb732208ebc8662651782f63db329a25f7ea1cd50cd91622a2c247b7", - "zh:36c8334194e7d605682053c7c70fbb2a650d9b0a7bcc44d5cdda4f205818438a", - "zh:3a5e01276added995e875b42ecc6b36ff73d267f0c096c87195bd2b1fff4f5b2", - "zh:557e38371657e6ed8aae9192d01480c4cca7c0f7ade6022f1aec247a6384922b", - "zh:67b913c280c5858549477a4b05e77078b1a5234de77c7bddd4ee1e8e237d5665", - "zh:7aeca864ce45b295db734cd968f7596ff12cd7c522ee89d53f432dae7c2b5d18", - "zh:b6127d7a796eaf9756dd212667eb48f79c0e78729589ec8ccf68e0b36ebb4e54", - "zh:bed448238740f897d1b399e5123b3a9eba256b981846f9ee92b71493446ca684", - "zh:c351a1bba34c3bd06fff75e4c15e4db0456268479463c2471598068ea1c5c884", - "zh:d073c24d0a4756e79b39f41f552d526800f9fb0ad0a74f742ac8de61b6416a3a", + "h1:T4XsCHDT839VehWKdxbVsLn0ECjcQaUTzbSGW055pgM=", + "zh:0d8ae6d6e87f44ed4a178be03d6466339b0bb578ab54c2677e365a8281b0bb7d", + "zh:29d250d1a18d49652b28f234ecd17687b36c875dc47877a678e587d5d136b054", + "zh:2e69ba373cf009e8a60b36d04f3dbc4638708d1bf88be9f96b3e52cbf8f47f31", + "zh:53d525dd84ac63b5f749bfbc6b70a202dacf29597664d2ab1165efea6f24f630", + "zh:a25024d574ccd5ae6c2962f3bb71d510f62899f493b1ed096f2f7f0e2b18f975", + "zh:aabc64fe64319b95aaba1d1866f87abc7b10adae37d2eafa2f85f37317fdd49f", + "zh:acc6a977814897cb23d3b3753213281334238f8bce6d2b21e9f04fc4087ee980", + "zh:b24987e9416c39cd59c0fa41c139a97406b9955f0607fcafbf3315014456338a", + "zh:c550eae45fd32acdbe32b4e5c450ae95df6cb18903ac7216b1b07b23a16ce045", + "zh:c8f83b763b643893dcb6933a6bcee824cb514e06e7e5c5f5ac4ba187e66d7e22", + "zh:dcdac07e7ea18464dea729717870c275de9453775243c231e1fb305cad0ee597", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } provider "registry.terraform.io/hashicorp/random" { - version = "3.1.0" + version = "3.2.0" hashes = [ - "h1:BZMEPucF+pbu9gsPk0G0BHx7YP04+tKdq2MrRDF1EDM=", - "zh:2bbb3339f0643b5daa07480ef4397bd23a79963cc364cdfbb4e86354cb7725bc", - "zh:3cd456047805bf639fbf2c761b1848880ea703a054f76db51852008b11008626", - "zh:4f251b0eda5bb5e3dc26ea4400dba200018213654b69b4a5f96abee815b4f5ff", - "zh:7011332745ea061e517fe1319bd6c75054a314155cb2c1199a5b01fe1889a7e2", - "zh:738ed82858317ccc246691c8b85995bc125ac3b4143043219bd0437adc56c992", - "zh:7dbe52fac7bb21227acd7529b487511c91f4107db9cc4414f50d04ffc3cab427", - "zh:a3a9251fb15f93e4cfc1789800fc2d7414bbc18944ad4c5c98f466e6477c42bc", - "zh:a543ec1a3a8c20635cf374110bd2f87c07374cf2c50617eee2c669b3ceeeaa9f", - "zh:d9ab41d556a48bd7059f0810cf020500635bfc696c9fc3adab5ea8915c1d886b", - "zh:d9e13427a7d011dbd654e591b0337e6074eef8c3b9bb11b2e39eaaf257044fd7", - "zh:f7605bd1437752114baf601bdf6931debe6dc6bfe3006eb7e9bb9080931dca8a", + "h1:eeUh6cJ6wKLLuo4q9uQ0CA1Zvfqya4Wn1LecLCN8KKs=", + "zh:2960977ce9a7d6a7d3e934e75ec5814735626f95c186ad95a9102344a1a38ac1", + "zh:2fd012abfabe7076f3f2f402eeef4970e20574d20ffec57c162b02b6e848c32f", + "zh:4cd3234671cf01c913023418b227eb78b0659f2cd2e0b387be1f0bb607d29889", + "zh:52e695b4fa3fae735ffc901edff8183745f980923510a744db7616e8f10dc499", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:848b4a294e5ba15192ee4bfd199c07f60a437d7572efcd2d89db036e1ebc0e6e", + "zh:9d49aa432a05748a9527e95448cebee1238c87c97c7e8dec694bfd709683f9c7", + "zh:b4ad4cf289d3f7408649b74b8639918833613f2a1f3cf51b51f4b2fdaa412dd2", + "zh:c1544c4b416096fb8d8dbf84c4488584a2844a30dd533b957e9e9e60a165f24e", + "zh:dc737d6b4591cad8c9a1d0b347e587e846d8d901789b29b4dd401b6cdf82c017", + "zh:f5645fd39f749dbbf847cbdc87ba0dbd141143f12917a6a8904faf8a9b64111e", + "zh:fdedf610e0d020878a8f1fedda8105e0c33a7e23c4792fca54460685552de308", ] } diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/locals.tf b/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/locals.tf index 58004abd4f..43e0ffec35 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/locals.tf +++ b/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/locals.tf @@ -5,11 +5,15 @@ locals { workspace_resource_name_suffix = "${var.tre_id}-ws-${local.short_workspace_id}" service_resource_name_suffix = "${var.tre_id}-ws-${local.short_workspace_id}-svc-${local.short_service_id}" core_vnet = "vnet-${var.tre_id}" - core_resource_group_name = "rg-${var.tre_id}" + core_resource_group_name = data.azurerm_resource_group.core.name vm_name = "windowsvm${local.short_service_id}" keyvault_name = lower("kv-${substr(local.workspace_resource_name_suffix, -20, -1)}") storage_name = lower(replace("stg${substr(local.workspace_resource_name_suffix, -8, -1)}", "-", "")) - nexus_proxy_url = "https://nexus-${var.tre_id}.azurewebsites.net" + + nexus_proxy_url = { + "V1" = "https://nexus-${var.tre_id}.azurewebsites.net", + "V2" = "https://nexus-${var.tre_id}.${data.azurerm_resource_group.core.location}.cloudapp.azure.com" + } vm_size = { "2 CPU | 8GB RAM" = { value = "Standard_D2s_v5" }, "4 CPU | 16GB RAM" = { value = "Standard_D4s_v5" }, diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/main.tf b/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/main.tf index 01c5f0b88d..93318cb6e2 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/main.tf +++ b/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "=2.97.0" + version = "=3.5.0" } } backend "azurerm" { @@ -21,6 +21,10 @@ data "azurerm_resource_group" "ws" { name = "rg-${var.tre_id}-ws-${local.short_workspace_id}" } +data "azurerm_resource_group" "core" { + name = "rg-${var.tre_id}" +} + data "azurerm_virtual_network" "ws" { name = "vnet-${var.tre_id}-ws-${local.short_workspace_id}" resource_group_name = data.azurerm_resource_group.ws.name diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/variables.tf b/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/variables.tf index 11b9a24565..3f3fd36c4d 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/variables.tf +++ b/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/variables.tf @@ -12,3 +12,4 @@ variable "image" {} variable "vm_size" {} variable "shared_storage_access" {} variable "shared_storage_name" {} +variable "nexus_version" {} diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/windowsvm.tf b/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/windowsvm.tf index 17c74117dc..10a036d5a1 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/windowsvm.tf +++ b/templates/workspace_services/guacamole/user_resources/guacamole-azure-windowsvm/terraform/windowsvm.tf @@ -68,7 +68,7 @@ resource "azurerm_windows_virtual_machine" "windowsvm" { } resource "azurerm_virtual_machine_extension" "config_script" { - name = "${azurerm_windows_virtual_machine.windowsvm.name}-vmextention" + name = "${azurerm_windows_virtual_machine.windowsvm.name}-vmextension" virtual_machine_id = azurerm_windows_virtual_machine.windowsvm.id publisher = "Microsoft.Compute" type = "CustomScriptExtension" @@ -90,7 +90,7 @@ resource "azurerm_key_vault_secret" "windowsvm_password" { data "template_file" "vm_config" { template = file("${path.module}/vm_config.ps1") vars = { - nexus_proxy_url = local.nexus_proxy_url + nexus_proxy_url = local.nexus_proxy_url[var.nexus_version] SharedStorageAccess = tobool(var.shared_storage_access) ? 1 : 0 StorageAccountName = data.azurerm_storage_account.stg.name StorageAccountKey = data.azurerm_storage_account.stg.primary_access_key diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/.dockerignore b/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/.dockerignore new file mode 100644 index 0000000000..97f0012a92 --- /dev/null +++ b/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/.dockerignore @@ -0,0 +1,2 @@ +# Local .terraform directories +**/.terraform/* diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/porter.yaml b/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/porter.yaml index 75ee52e3ad..116073f4df 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/porter.yaml +++ b/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/porter.yaml @@ -1,6 +1,6 @@ --- name: tre-service-dev-vm -version: 0.1.11 +version: 0.3.2 description: "An Azure TRE User Resource Template for a Dev VM" registry: azuretre dockerfile: Dockerfile.tmpl @@ -56,6 +56,10 @@ parameters: - name: shared_storage_name type: string default: "vm-shared-storage" + - name: nexus_version + type: string + default: "V1" + description: "Which Nexus proxy service to use, i.e. V1 for the App Service-based Nexus or V2 for the VM-based service" outputs: - name: ip @@ -98,6 +102,7 @@ install: vm_size: "{{ bundle.parameters.vm_size }}" shared_storage_access: "{{ bundle.parameters.shared_storage_access }}" shared_storage_name: "{{ bundle.parameters.shared_storage_name }}" + nexus_version: "{{ bundle.parameters.nexus_version }}" backendConfig: resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" @@ -126,6 +131,7 @@ upgrade: vm_size: "{{ bundle.parameters.vm_size }}" shared_storage_access: "{{ bundle.parameters.shared_storage_access }}" shared_storage_name: "{{ bundle.parameters.shared_storage_name }}" + nexus_version: "{{ bundle.parameters.nexus_version }}" backendConfig: resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" @@ -154,6 +160,7 @@ uninstall: vm_size: "{{ bundle.parameters.vm_size }}" shared_storage_access: "{{ bundle.parameters.shared_storage_access }}" shared_storage_name: "{{ bundle.parameters.shared_storage_name }}" + nexus_version: "{{ bundle.parameters.nexus_version }}" backendConfig: resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/template_schema.json b/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/template_schema.json index 535bcb14b3..badd6cb24d 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/template_schema.json +++ b/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/template_schema.json @@ -29,6 +29,17 @@ "16 CPU | 64GB RAM" ], "updateable": true + }, + "nexus_version": { + "$id": "#/properties/nexus_version", + "type": "string", + "title": "Nexus", + "description": "Which Nexus proxy service to use, i.e. V1 for the App Service-based Nexus or V2 for the VM-based service", + "enum": [ + "V1", + "V2" + ], + "default": "V1" } }, "pipeline": { @@ -37,7 +48,7 @@ "stepId": "6d2d7eb7-984e-4330-bd3c-c7ec98658402", "stepTitle": "Update the firewall the first time", "resourceTemplateName": "tre-shared-service-firewall", - "resourceType": "shared_service", + "resourceType": "shared-service", "resourceAction": "upgrade", "properties": [ { @@ -53,7 +64,7 @@ "stepId": "2fe8a6a7-2c27-4c49-8773-127df8a48b4e", "stepTitle": "Update the firewall the second time", "resourceTemplateName": "tre-shared-service-firewall", - "resourceType": "shared_service", + "resourceType": "shared-service", "resourceAction": "upgrade", "properties": [ { diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/.terraform.lock.hcl b/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/.terraform.lock.hcl index 61c54c45c8..3748610cfc 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/.terraform.lock.hcl +++ b/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/.terraform.lock.hcl @@ -2,39 +2,41 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/azurerm" { - version = "2.97.0" - constraints = "2.97.0" + version = "3.5.0" + constraints = "3.5.0" hashes = [ - "h1:XxT+XM/leTXa21aTnJjPBfNBQ8cLE4gYDg01WEZsV1U=", - "zh:0aac80e6d2b8ddf33d558ac893d52688e8abf8a0b995cfc3c35eb84afbf432a3", - "zh:11191068cb732208ebc8662651782f63db329a25f7ea1cd50cd91622a2c247b7", - "zh:36c8334194e7d605682053c7c70fbb2a650d9b0a7bcc44d5cdda4f205818438a", - "zh:3a5e01276added995e875b42ecc6b36ff73d267f0c096c87195bd2b1fff4f5b2", - "zh:557e38371657e6ed8aae9192d01480c4cca7c0f7ade6022f1aec247a6384922b", - "zh:67b913c280c5858549477a4b05e77078b1a5234de77c7bddd4ee1e8e237d5665", - "zh:7aeca864ce45b295db734cd968f7596ff12cd7c522ee89d53f432dae7c2b5d18", - "zh:b6127d7a796eaf9756dd212667eb48f79c0e78729589ec8ccf68e0b36ebb4e54", - "zh:bed448238740f897d1b399e5123b3a9eba256b981846f9ee92b71493446ca684", - "zh:c351a1bba34c3bd06fff75e4c15e4db0456268479463c2471598068ea1c5c884", - "zh:d073c24d0a4756e79b39f41f552d526800f9fb0ad0a74f742ac8de61b6416a3a", + "h1:T4XsCHDT839VehWKdxbVsLn0ECjcQaUTzbSGW055pgM=", + "zh:0d8ae6d6e87f44ed4a178be03d6466339b0bb578ab54c2677e365a8281b0bb7d", + "zh:29d250d1a18d49652b28f234ecd17687b36c875dc47877a678e587d5d136b054", + "zh:2e69ba373cf009e8a60b36d04f3dbc4638708d1bf88be9f96b3e52cbf8f47f31", + "zh:53d525dd84ac63b5f749bfbc6b70a202dacf29597664d2ab1165efea6f24f630", + "zh:a25024d574ccd5ae6c2962f3bb71d510f62899f493b1ed096f2f7f0e2b18f975", + "zh:aabc64fe64319b95aaba1d1866f87abc7b10adae37d2eafa2f85f37317fdd49f", + "zh:acc6a977814897cb23d3b3753213281334238f8bce6d2b21e9f04fc4087ee980", + "zh:b24987e9416c39cd59c0fa41c139a97406b9955f0607fcafbf3315014456338a", + "zh:c550eae45fd32acdbe32b4e5c450ae95df6cb18903ac7216b1b07b23a16ce045", + "zh:c8f83b763b643893dcb6933a6bcee824cb514e06e7e5c5f5ac4ba187e66d7e22", + "zh:dcdac07e7ea18464dea729717870c275de9453775243c231e1fb305cad0ee597", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } provider "registry.terraform.io/hashicorp/random" { - version = "3.1.0" + version = "3.2.0" hashes = [ - "h1:BZMEPucF+pbu9gsPk0G0BHx7YP04+tKdq2MrRDF1EDM=", - "zh:2bbb3339f0643b5daa07480ef4397bd23a79963cc364cdfbb4e86354cb7725bc", - "zh:3cd456047805bf639fbf2c761b1848880ea703a054f76db51852008b11008626", - "zh:4f251b0eda5bb5e3dc26ea4400dba200018213654b69b4a5f96abee815b4f5ff", - "zh:7011332745ea061e517fe1319bd6c75054a314155cb2c1199a5b01fe1889a7e2", - "zh:738ed82858317ccc246691c8b85995bc125ac3b4143043219bd0437adc56c992", - "zh:7dbe52fac7bb21227acd7529b487511c91f4107db9cc4414f50d04ffc3cab427", - "zh:a3a9251fb15f93e4cfc1789800fc2d7414bbc18944ad4c5c98f466e6477c42bc", - "zh:a543ec1a3a8c20635cf374110bd2f87c07374cf2c50617eee2c669b3ceeeaa9f", - "zh:d9ab41d556a48bd7059f0810cf020500635bfc696c9fc3adab5ea8915c1d886b", - "zh:d9e13427a7d011dbd654e591b0337e6074eef8c3b9bb11b2e39eaaf257044fd7", - "zh:f7605bd1437752114baf601bdf6931debe6dc6bfe3006eb7e9bb9080931dca8a", + "h1:eeUh6cJ6wKLLuo4q9uQ0CA1Zvfqya4Wn1LecLCN8KKs=", + "zh:2960977ce9a7d6a7d3e934e75ec5814735626f95c186ad95a9102344a1a38ac1", + "zh:2fd012abfabe7076f3f2f402eeef4970e20574d20ffec57c162b02b6e848c32f", + "zh:4cd3234671cf01c913023418b227eb78b0659f2cd2e0b387be1f0bb607d29889", + "zh:52e695b4fa3fae735ffc901edff8183745f980923510a744db7616e8f10dc499", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:848b4a294e5ba15192ee4bfd199c07f60a437d7572efcd2d89db036e1ebc0e6e", + "zh:9d49aa432a05748a9527e95448cebee1238c87c97c7e8dec694bfd709683f9c7", + "zh:b4ad4cf289d3f7408649b74b8639918833613f2a1f3cf51b51f4b2fdaa412dd2", + "zh:c1544c4b416096fb8d8dbf84c4488584a2844a30dd533b957e9e9e60a165f24e", + "zh:dc737d6b4591cad8c9a1d0b347e587e846d8d901789b29b4dd401b6cdf82c017", + "zh:f5645fd39f749dbbf847cbdc87ba0dbd141143f12917a6a8904faf8a9b64111e", + "zh:fdedf610e0d020878a8f1fedda8105e0c33a7e23c4792fca54460685552de308", ] } diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/linuxvm.tf b/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/linuxvm.tf index 7ae293288b..dc929a2ddc 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/linuxvm.tf +++ b/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/linuxvm.tf @@ -97,7 +97,7 @@ data "template_file" "vm_config" { storage_account_key = data.azurerm_storage_account.stg.primary_access_key http_endpoint = data.azurerm_storage_account.stg.primary_file_endpoint fileshare_name = data.azurerm_storage_share.shared_storage.name - nexus_proxy_url = local.nexus_proxy_url + nexus_proxy_url = local.nexus_proxy_url[var.nexus_version] conda_config = local.image_ref[var.image].conda_config ? 1 : 0 } } @@ -105,14 +105,14 @@ data "template_file" "vm_config" { data "template_file" "pypi_sources_config" { template = file("${path.module}/pypi_sources_config.sh") vars = { - nexus_proxy_url = local.nexus_proxy_url + nexus_proxy_url = local.nexus_proxy_url[var.nexus_version] } } data "template_file" "apt_sources_config" { template = file("${path.module}/apt_sources_config.yml") vars = { - nexus_proxy_url = local.nexus_proxy_url + nexus_proxy_url = local.nexus_proxy_url[var.nexus_version] } } diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/locals.tf b/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/locals.tf index e684eb37d5..07d6ca0c62 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/locals.tf +++ b/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/locals.tf @@ -9,7 +9,11 @@ locals { vm_name = "linuxvm${local.short_service_id}" keyvault_name = lower("kv-${substr(local.workspace_resource_name_suffix, -20, -1)}") storage_name = lower(replace("stg${substr(local.workspace_resource_name_suffix, -8, -1)}", "-", "")) - nexus_proxy_url = "https://nexus-${var.tre_id}.azurewebsites.net" + + nexus_proxy_url = { + "V1" = "https://nexus-${var.tre_id}.azurewebsites.net", + "V2" = "https://nexus-${var.tre_id}.${data.azurerm_resource_group.core.location}.cloudapp.azure.com" + } vm_size = { "2 CPU | 8GB RAM" = { value = "Standard_D2s_v5" }, "4 CPU | 16GB RAM" = { value = "Standard_D4s_v5" }, diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/main.tf b/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/main.tf index da10b88845..c0d76a1f20 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/main.tf +++ b/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/main.tf @@ -3,7 +3,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "=2.97.0" + version = "=3.5.0" } } backend "azurerm" { @@ -21,6 +21,10 @@ data "azurerm_resource_group" "ws" { name = "rg-${var.tre_id}-ws-${local.short_workspace_id}" } +data "azurerm_resource_group" "core" { + name = "rg-${var.tre_id}" +} + data "azurerm_virtual_network" "ws" { name = "vnet-${var.tre_id}-ws-${local.short_workspace_id}" resource_group_name = data.azurerm_resource_group.ws.name diff --git a/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/variables.tf b/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/variables.tf index 11b9a24565..3f3fd36c4d 100644 --- a/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/variables.tf +++ b/templates/workspace_services/guacamole/user_resources/guacamole-dev-vm/terraform/variables.tf @@ -12,3 +12,4 @@ variable "image" {} variable "vm_size" {} variable "shared_storage_access" {} variable "shared_storage_name" {} +variable "nexus_version" {} diff --git a/templates/workspace_services/guacamole/version.txt b/templates/workspace_services/guacamole/version.txt index 970659c2b1..f9aa3e1109 100644 --- a/templates/workspace_services/guacamole/version.txt +++ b/templates/workspace_services/guacamole/version.txt @@ -1 +1 @@ -__version__ = "0.1.16" +__version__ = "0.3.2" diff --git a/templates/workspace_services/innereye/porter.yaml b/templates/workspace_services/innereye/porter.yaml index c82c7b7265..e03ede698d 100644 --- a/templates/workspace_services/innereye/porter.yaml +++ b/templates/workspace_services/innereye/porter.yaml @@ -1,6 +1,6 @@ --- name: tre-service-innereye -version: 0.1.6 +version: 0.3.0 description: "An Azure TRE service for InnerEye Deep Learning" registry: azuretre dockerfile: Dockerfile.tmpl diff --git a/templates/workspace_services/mlflow/mlflow-server/version.txt b/templates/workspace_services/mlflow/mlflow-server/version.txt index 3dc1f76bc6..493f7415d7 100644 --- a/templates/workspace_services/mlflow/mlflow-server/version.txt +++ b/templates/workspace_services/mlflow/mlflow-server/version.txt @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.3.0" diff --git a/templates/workspace_services/mlflow/porter.yaml b/templates/workspace_services/mlflow/porter.yaml index ca72784ece..ae71ec1666 100644 --- a/templates/workspace_services/mlflow/porter.yaml +++ b/templates/workspace_services/mlflow/porter.yaml @@ -1,6 +1,6 @@ --- name: tre-service-mlflow -version: 0.1.0 +version: 0.3.1 description: "An Azure TRE service for MLflow machine learning lifecycle" registry: azuretre dockerfile: Dockerfile.tmpl diff --git a/templates/workspace_services/mlflow/terraform/web_app.tf b/templates/workspace_services/mlflow/terraform/web_app.tf index cd1077b4a3..8719ed7183 100644 --- a/templates/workspace_services/mlflow/terraform/web_app.tf +++ b/templates/workspace_services/mlflow/terraform/web_app.tf @@ -61,6 +61,7 @@ resource "azurerm_app_service" "mlflow" { http2_enabled = true acr_use_managed_identity_credentials = true vnet_route_all_enabled = true + ftps_state = "Disabled" } app_settings = { diff --git a/templates/workspaces/base/.env.sample b/templates/workspaces/base/.env.sample index 4fe71f963b..190959e4e5 100644 --- a/templates/workspaces/base/.env.sample +++ b/templates/workspaces/base/.env.sample @@ -1,11 +1,18 @@ TRE_RESOURCE_ID="__CHANGE_ME__" -AUTH_TENANT_ID="__CHANGE_ME__" -AUTH_CLIENT_ID="__CHANGE_ME__" -AUTH_CLIENT_SECRET="__CHANGE_ME__" ARM_CLIENT_ID="__CHANGE_ME__" ARM_CLIENT_SECRET="__CHANGE_ME__" ARM_TENANT_ID="__CHANGE_ME__" ARM_SUBSCRIPTION_ID="__CHANGE_ME__" +AUTH_TENANT_ID="__CHANGE_ME__" + +# These are passed in if Terraform will create the Workspace AAD Application +AUTH_CLIENT_ID="__CHANGE_ME__" +AUTH_CLIENT_SECRET="__CHANGE_ME__" +WORKSPACE_OWNER_OBJECT_ID="__CHANGE_ME__" + +# These are passed in if you register the Workspace AAD Application before hand +CLIENT_ID="__CHANGE_ME__ +CLIENT_SECRET="__CHANGE_ME__ ADDRESS_SPACE="10.2.8.0/24" SHARED_STORAGE_QUOTA=50 diff --git a/templates/workspaces/base/porter.yaml b/templates/workspaces/base/porter.yaml index 32b0e652f2..30b7f4abcd 100644 --- a/templates/workspaces/base/porter.yaml +++ b/templates/workspaces/base/porter.yaml @@ -1,6 +1,6 @@ --- name: tre-workspace-base -version: 0.2.13 +version: 0.3.5 description: "A base Azure TRE workspace" registry: azuretre @@ -58,9 +58,18 @@ parameters: type: boolean default: false description: "Whether this bundle should register the workspace in AAD" + - name: workspace_owner_object_id + type: string + description: "The object id of the user that will be granted WorkspaceOwner after it is created." - name: client_id type: string - description: "The client id of the workspace in the identity provider." + description: "The client id of the workspace in the identity provider. This value is typically provided to you + when you create the ws application" + - name: client_secret + type: string + description: "The client secret of the workspace in the identity provider. This value is typically provided to you + when you create the ws application" + default: "" - name: scope_id type: string default: "" @@ -126,7 +135,9 @@ install: auth_client_id: "{{ bundle.credentials.auth_client_id }}" auth_client_secret: "{{ bundle.credentials.auth_client_secret }}" auth_tenant_id: "{{ bundle.credentials.auth_tenant_id }}" + workspace_owner_object_id: "{{ bundle.parameters.workspace_owner_object_id }}" client_id: "{{ bundle.parameters.client_id }}" + client_secret: "{{ bundle.parameters.client_secret }}" scope_id: "{{ bundle.parameters.scope_id }}" sp_id: "{{ bundle.parameters.sp_id }}" app_role_id_workspace_owner: "{{ bundle.parameters.app_role_id_workspace_owner }}" @@ -168,6 +179,12 @@ uninstall: auth_client_id: "{{ bundle.credentials.auth_client_id }}" auth_client_secret: "{{ bundle.credentials.auth_client_secret }}" auth_tenant_id: "{{ bundle.credentials.auth_tenant_id }}" + workspace_owner_object_id: "{{ bundle.parameters.workspace_owner_object_id }}" + client_id: "{{ bundle.parameters.client_id }}" + scope_id: "{{ bundle.parameters.scope_id }}" + sp_id: "{{ bundle.parameters.sp_id }}" + app_role_id_workspace_owner: "{{ bundle.parameters.app_role_id_workspace_owner }}" + app_role_id_workspace_researcher: "{{ bundle.parameters.app_role_id_workspace_researcher }}" backendConfig: resource_group_name: "{{ bundle.parameters.tfstate_resource_group_name }}" @@ -175,3 +192,9 @@ uninstall: "{{ bundle.parameters.tfstate_storage_account_name }}" container_name: "{{ bundle.parameters.tfstate_container_name }}" key: "{{ bundle.parameters.tre_id }}-ws-{{ bundle.parameters.id }}" + outputs: + - name: app_role_id_workspace_owner + - name: app_role_id_workspace_researcher + - name: client_id + - name: scope_id + - name: sp_id diff --git a/templates/workspaces/base/terraform/.terraform.lock.hcl b/templates/workspaces/base/terraform/.terraform.lock.hcl index 5c12a0f59f..ac63de398b 100644 --- a/templates/workspaces/base/terraform/.terraform.lock.hcl +++ b/templates/workspaces/base/terraform/.terraform.lock.hcl @@ -22,21 +22,21 @@ provider "registry.terraform.io/hashicorp/azuread" { } provider "registry.terraform.io/hashicorp/azurerm" { - version = "3.4.0" - constraints = "3.4.0" + version = "3.5.0" + constraints = "3.5.0" hashes = [ - "h1:h78yKGgOFrU/N5ntockxN7XF/ufv47j77+oauO2GKqk=", - "zh:4e9913fc3378436d19150c334e5906eafb83a4af3a270423cb7cdda94b27371f", - "zh:5b3d0cec2a600dc1f6633baa8fc36368c5c330fd7654861edcfa76f760a8f6a9", - "zh:5e0e1f899027bc182f31d996c9611e5ba27a034c848d7b0519b39e559fc4f38d", - "zh:66e3a1383ed6a0370989f6fd6abcfa63ccf6918ae535108595af57b9c20a9257", - "zh:688493baf6a116a399b737d74c11080051aca1ab087e5cddd14cc683b7e45c76", - "zh:9e471d85d52343e3ba778f3a94626d820fbec97bb589a3ac7a6a0939b9387770", - "zh:be1e85635daca1768f26962a4cbbadbf7fd13d9da8f9f188e938beca542c2ad5", - "zh:c00e14b6aa566eb9995cb0e1611a18fb8650d9f35c7636a7643a1b6e22660226", - "zh:c40711e5021838fd879da4c9e6b8f7e72104ada2adf0f3ba22e1cc32c3c54086", - "zh:cc62f8541de8d79577e57664e4f03c1fca893d455e5fb238d20668389c0f09ee", - "zh:cd9cbb5c6e5ceb5fcc7c4d0cab516ff209667d1b539b8c7436bd5e452c6aba8f", + "h1:T4XsCHDT839VehWKdxbVsLn0ECjcQaUTzbSGW055pgM=", + "zh:0d8ae6d6e87f44ed4a178be03d6466339b0bb578ab54c2677e365a8281b0bb7d", + "zh:29d250d1a18d49652b28f234ecd17687b36c875dc47877a678e587d5d136b054", + "zh:2e69ba373cf009e8a60b36d04f3dbc4638708d1bf88be9f96b3e52cbf8f47f31", + "zh:53d525dd84ac63b5f749bfbc6b70a202dacf29597664d2ab1165efea6f24f630", + "zh:a25024d574ccd5ae6c2962f3bb71d510f62899f493b1ed096f2f7f0e2b18f975", + "zh:aabc64fe64319b95aaba1d1866f87abc7b10adae37d2eafa2f85f37317fdd49f", + "zh:acc6a977814897cb23d3b3753213281334238f8bce6d2b21e9f04fc4087ee980", + "zh:b24987e9416c39cd59c0fa41c139a97406b9955f0607fcafbf3315014456338a", + "zh:c550eae45fd32acdbe32b4e5c450ae95df6cb18903ac7216b1b07b23a16ce045", + "zh:c8f83b763b643893dcb6933a6bcee824cb514e06e7e5c5f5ac4ba187e66d7e22", + "zh:dcdac07e7ea18464dea729717870c275de9453775243c231e1fb305cad0ee597", "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } diff --git a/templates/workspaces/base/terraform/aad/aad.tf b/templates/workspaces/base/terraform/aad/aad.tf index 8f985748fa..3ac54e10fb 100644 --- a/templates/workspaces/base/terraform/aad/aad.tf +++ b/templates/workspaces/base/terraform/aad/aad.tf @@ -102,3 +102,9 @@ resource "azurerm_key_vault_secret" "client_secret" { value = azuread_service_principal_password.workspace.value key_vault_id = var.key_vault_id } + +resource "azuread_app_role_assignment" "workspace_owner" { + app_role_id = azuread_service_principal.workspace.app_role_ids["WorkspaceOwner"] + principal_object_id = var.workspace_owner_object_id + resource_object_id = azuread_service_principal.workspace.object_id +} diff --git a/templates/workspaces/base/terraform/aad/variables.tf b/templates/workspaces/base/terraform/aad/variables.tf index 220dc4c7e6..4f4b8ce6ad 100644 --- a/templates/workspaces/base/terraform/aad/variables.tf +++ b/templates/workspaces/base/terraform/aad/variables.tf @@ -1,2 +1,3 @@ variable "key_vault_id" {} -variable "workspace_resource_name_suffix" {} \ No newline at end of file +variable "workspace_resource_name_suffix" {} +variable "workspace_owner_object_id" {} diff --git a/templates/workspaces/base/terraform/airlock/eventgrid_topics.tf b/templates/workspaces/base/terraform/airlock/eventgrid_topics.tf new file mode 100644 index 0000000000..44ccf97b36 --- /dev/null +++ b/templates/workspaces/base/terraform/airlock/eventgrid_topics.tf @@ -0,0 +1,112 @@ +# System topics +resource "azurerm_eventgrid_system_topic" "import_approved_blob_created" { + name = local.import_approved_sys_topic_name + location = var.location + resource_group_name = var.ws_resource_group_name + source_arm_resource_id = azurerm_storage_account.sa_import_approved.id + topic_type = "Microsoft.Storage.StorageAccounts" + + tags = { + Publishers = "airlock;accepted-import-sa" + } + + depends_on = [ + azurerm_storage_account.sa_import_approved + ] + + lifecycle { ignore_changes = [tags] } +} + +resource "azurerm_eventgrid_system_topic" "export_inprogress_blob_created" { + name = local.export_inprogress_sys_topic_name + location = var.location + resource_group_name = var.ws_resource_group_name + source_arm_resource_id = azurerm_storage_account.sa_export_inprogress.id + topic_type = "Microsoft.Storage.StorageAccounts" + + tags = { + Publishers = "airlock;inprogress-export-sa" + } + + depends_on = [ + azurerm_storage_account.sa_export_inprogress + ] + + lifecycle { ignore_changes = [tags] } +} + + +resource "azurerm_eventgrid_system_topic" "export_rejected_blob_created" { + name = local.export_rejected_sys_topic_name + location = var.location + resource_group_name = var.ws_resource_group_name + source_arm_resource_id = azurerm_storage_account.sa_export_rejected.id + topic_type = "Microsoft.Storage.StorageAccounts" + + tags = { + Publishers = "airlock;rejected-export-sa" + } + + depends_on = [ + azurerm_storage_account.sa_export_rejected + ] + + lifecycle { ignore_changes = [tags] } +} + +data "azurerm_servicebus_namespace" "airlock_sb" { + name = "sb-${var.tre_id}" + resource_group_name = local.core_resource_group_name +} + +data "azurerm_servicebus_queue" "import_approved_blob_created" { + name = local.import_approved_queue_name + resource_group_name = local.core_resource_group_name + namespace_name = "sb-${var.tre_id}" +} + +data "azurerm_servicebus_queue" "export_in_progress_blob_created" { + name = local.export_inprogress_queue_name + resource_group_name = local.core_resource_group_name + namespace_name = "sb-${var.tre_id}" +} + +data "azurerm_servicebus_queue" "export_rejected_blob_created" { + name = local.export_rejected_queue_name + resource_group_name = local.core_resource_group_name + namespace_name = "sb-${var.tre_id}" +} + +## Subscriptions +resource "azurerm_eventgrid_event_subscription" "import_approved_blob_created" { + name = "import-approved-blob-created-${var.short_workspace_id}" + scope = azurerm_storage_account.sa_import_approved.id + + service_bus_queue_endpoint_id = data.azurerm_servicebus_queue.import_approved_blob_created.id + + depends_on = [ + azurerm_eventgrid_system_topic.import_approved_blob_created + ] +} + +resource "azurerm_eventgrid_event_subscription" "export_inprogress_blob_created" { + name = "export-inprogress-blob-created-${var.short_workspace_id}" + scope = azurerm_storage_account.sa_export_inprogress.id + + service_bus_queue_endpoint_id = data.azurerm_servicebus_queue.export_in_progress_blob_created.id + + depends_on = [ + azurerm_eventgrid_system_topic.export_inprogress_blob_created + ] +} + +resource "azurerm_eventgrid_event_subscription" "export_rejected_blob_created" { + name = "export-rejected-blob-created-${var.short_workspace_id}" + scope = azurerm_storage_account.sa_export_rejected.id + + service_bus_queue_endpoint_id = data.azurerm_servicebus_queue.export_rejected_blob_created.id + + depends_on = [ + azurerm_eventgrid_system_topic.export_rejected_blob_created + ] +} diff --git a/templates/workspaces/base/terraform/airlock/locals.tf b/templates/workspaces/base/terraform/airlock/locals.tf new file mode 100644 index 0000000000..698effb384 --- /dev/null +++ b/templates/workspaces/base/terraform/airlock/locals.tf @@ -0,0 +1,21 @@ +locals { + core_resource_group_name = "rg-${var.tre_id}" + workspace_resource_name_suffix = "${var.tre_id}-ws-${var.short_workspace_id}" + + import_approved_sys_topic_name = "evgt-airlock-import-approved-${local.workspace_resource_name_suffix}" + export_inprogress_sys_topic_name = "evgt-airlock-export-inprog-${local.workspace_resource_name_suffix}" + export_rejected_sys_topic_name = "evgt-airlock-export-rejected-${local.workspace_resource_name_suffix}" + + export_rejected_queue_name = "airlock-export-rejected-blob-created" + import_approved_queue_name = "airlock-import-approved-blob-created" + export_inprogress_queue_name = "airlock-export-inprogress-blob-created" + + # STorage AirLock APProved IMport + import_approved_storage_name = lower(replace("stalimapp${substr(local.workspace_resource_name_suffix, -8, -1)}", "-", "")) + # STorage AirLock INTernal EXport + export_internal_storage_name = lower(replace("stalexint${substr(local.workspace_resource_name_suffix, -8, -1)}", "-", "")) + # STorage AirLock InProgress EXport + export_inprogress_storage_name = lower(replace("stalexip${substr(local.workspace_resource_name_suffix, -8, -1)}", "-", "")) + # STorage AirLock REJected EXport + export_rejected_storage_name = lower(replace("stalexrej${substr(local.workspace_resource_name_suffix, -8, -1)}", "-", "")) +} diff --git a/templates/workspaces/base/terraform/airlock/storage_accounts.tf b/templates/workspaces/base/terraform/airlock/storage_accounts.tf new file mode 100644 index 0000000000..bd50a9056c --- /dev/null +++ b/templates/workspaces/base/terraform/airlock/storage_accounts.tf @@ -0,0 +1,172 @@ +# 'Approved' storage account +resource "azurerm_storage_account" "sa_import_approved" { + name = local.import_approved_storage_name + location = var.location + resource_group_name = var.ws_resource_group_name + account_tier = "Standard" + account_replication_type = "GRS" + allow_nested_items_to_be_public = false + + # Important! we rely on the fact that the blob craeted events are issued when the creation of the blobs are done. + # This is true ONLY when Hierarchical Namespace is DISABLED + is_hns_enabled = false + + tags = { + description = "airlock;import;approved" + } + + lifecycle { ignore_changes = [tags] } +} + +data "azurerm_private_dns_zone" "blobcore" { + name = "privatelink.blob.core.windows.net" + resource_group_name = local.core_resource_group_name +} + +resource "azurerm_private_endpoint" "import_approved_pe" { + name = "pe-sa-import-approved-blob-${var.short_workspace_id}" + location = var.location + resource_group_name = var.ws_resource_group_name + subnet_id = var.services_subnet_id + + lifecycle { ignore_changes = [tags] } + + private_dns_zone_group { + name = "private-dns-zone-group-sa-import-approved" + private_dns_zone_ids = [data.azurerm_private_dns_zone.blobcore.id] + } + + private_service_connection { + name = "psc-sa-import-approved-${var.short_workspace_id}" + private_connection_resource_id = azurerm_storage_account.sa_import_approved.id + is_manual_connection = false + subresource_names = ["Blob"] + } +} + + +# 'Drop' location for export +resource "azurerm_storage_account" "sa_export_internal" { + name = local.export_internal_storage_name + location = var.location + resource_group_name = var.ws_resource_group_name + account_tier = "Standard" + account_replication_type = "GRS" + allow_nested_items_to_be_public = false + + # Important! we rely on the fact that the blob craeted events are issued when the creation of the blobs are done. + # This is true ONLY when Hierarchical Namespace is DISABLED + is_hns_enabled = false + + tags = { + description = "airlock;export;internal" + } + + lifecycle { ignore_changes = [tags] } +} + + +resource "azurerm_private_endpoint" "export_internal_pe" { + name = "pe-sa-export-int-blob-${var.short_workspace_id}" + location = var.location + resource_group_name = var.ws_resource_group_name + subnet_id = var.services_subnet_id + + lifecycle { ignore_changes = [tags] } + + private_dns_zone_group { + name = "private-dns-zone-group-sa-export-int" + private_dns_zone_ids = [data.azurerm_private_dns_zone.blobcore.id] + } + + private_service_connection { + name = "psc-sa-export-int-${var.short_workspace_id}" + private_connection_resource_id = azurerm_storage_account.sa_export_internal.id + is_manual_connection = false + subresource_names = ["Blob"] + } +} + +# 'In-progress' location for export +resource "azurerm_storage_account" "sa_export_inprogress" { + name = local.export_inprogress_storage_name + location = var.location + resource_group_name = var.ws_resource_group_name + account_tier = "Standard" + account_replication_type = "GRS" + allow_nested_items_to_be_public = false + + # Important! we rely on the fact that the blob craeted events are issued when the creation of the blobs are done. + # This is true ONLY when Hierarchical Namespace is DISABLED + is_hns_enabled = false + + tags = { + description = "airlock;export;inprogress" + } + + lifecycle { ignore_changes = [tags] } +} + + +resource "azurerm_private_endpoint" "export_inprogress_pe" { + name = "pe-sa-ip-export-blob-${var.short_workspace_id}" + location = var.location + resource_group_name = var.ws_resource_group_name + subnet_id = var.services_subnet_id + + lifecycle { ignore_changes = [tags] } + + private_dns_zone_group { + name = "private-dns-zone-group-sa-export-ip" + private_dns_zone_ids = [data.azurerm_private_dns_zone.blobcore.id] + } + + private_service_connection { + name = "psc-sa-export-ip-${var.short_workspace_id}" + private_connection_resource_id = azurerm_storage_account.sa_export_inprogress.id + is_manual_connection = false + subresource_names = ["Blob"] + } +} + +# 'Rejected' location for export +resource "azurerm_storage_account" "sa_export_rejected" { + name = local.export_rejected_storage_name + location = var.location + resource_group_name = var.ws_resource_group_name + account_tier = "Standard" + account_replication_type = "GRS" + allow_nested_items_to_be_public = false + + # Important! we rely on the fact that the blob craeted events are issued when the creation of the blobs are done. + # This is true ONLY when Hierarchical Namespace is DISABLED + is_hns_enabled = false + + tags = { + description = "airlock;export;rejected" + } + + lifecycle { ignore_changes = [tags] } +} + + +resource "azurerm_private_endpoint" "export_rejected_pe" { + name = "pe-sa-export-rej-blob-${var.short_workspace_id}" + location = var.location + resource_group_name = var.ws_resource_group_name + subnet_id = var.services_subnet_id + + lifecycle { ignore_changes = [tags] } + + private_dns_zone_group { + name = "private-dns-zone-group-sa-export-rej" + private_dns_zone_ids = [data.azurerm_private_dns_zone.blobcore.id] + } + + private_service_connection { + name = "psc-sa-export-rej-${var.short_workspace_id}" + private_connection_resource_id = azurerm_storage_account.sa_export_rejected.id + is_manual_connection = false + subresource_names = ["Blob"] + } +} diff --git a/templates/workspaces/base/terraform/airlock/variables.tf b/templates/workspaces/base/terraform/airlock/variables.tf new file mode 100644 index 0000000000..7c9affab48 --- /dev/null +++ b/templates/workspaces/base/terraform/airlock/variables.tf @@ -0,0 +1,6 @@ +variable "location" {} +variable "tre_id" {} +variable "ws_resource_group_name" {} +variable "enable_local_debugging" {} +variable "services_subnet_id" {} +variable "short_workspace_id" {} diff --git a/templates/workspaces/base/terraform/keyvault.tf b/templates/workspaces/base/terraform/keyvault.tf index e1b1cb64a2..a8ac5931cf 100644 --- a/templates/workspaces/base/terraform/keyvault.tf +++ b/templates/workspaces/base/terraform/keyvault.tf @@ -49,7 +49,7 @@ resource "azurerm_key_vault_access_policy" "resource_processor" { tenant_id = data.azurerm_user_assigned_identity.resource_processor_vmss_id.tenant_id object_id = data.azurerm_user_assigned_identity.resource_processor_vmss_id.principal_id - secret_permissions = ["Get", "List", "Set", "Delete", "Purge"] + secret_permissions = ["Get", "List", "Set", "Delete", "Purge", "Recover"] } # If running the terraform locally @@ -59,7 +59,7 @@ resource "azurerm_key_vault_access_policy" "deployer" { tenant_id = data.azurerm_client_config.current.tenant_id object_id = data.azurerm_client_config.current.object_id - secret_permissions = ["Get", "List", "Set", "Delete", "Purge"] + secret_permissions = ["Get", "List", "Set", "Delete", "Purge", "Recover"] } resource "null_resource" "wait_for_dns_vault" { @@ -85,3 +85,31 @@ resource "azurerm_key_vault_secret" "aad_tenant_id" { null_resource.wait_for_dns_vault ] } + +# This secret only gets written if Terraform is not responsible for +# registering the AAD Application +resource "azurerm_key_vault_secret" "client_id" { + name = "workspace-client-id" + value = var.client_id + key_vault_id = azurerm_key_vault.kv.id + count = var.register_aad_application ? 0 : 1 + depends_on = [ + azurerm_key_vault_access_policy.deployer, + azurerm_key_vault_access_policy.resource_processor, + null_resource.wait_for_dns_vault + ] +} + +# This secret only gets written if Terraform is not responsible for +# registering the AAD Application +resource "azurerm_key_vault_secret" "client_secret" { + name = "workspace-client-secret" + value = var.client_secret + key_vault_id = azurerm_key_vault.kv.id + count = var.register_aad_application ? 0 : 1 + depends_on = [ + azurerm_key_vault_access_policy.deployer, + azurerm_key_vault_access_policy.resource_processor, + null_resource.wait_for_dns_vault + ] +} diff --git a/templates/workspaces/base/terraform/network/data.tf b/templates/workspaces/base/terraform/network/data.tf index 929ca9d36f..8f293553a2 100644 --- a/templates/workspaces/base/terraform/network/data.tf +++ b/templates/workspaces/base/terraform/network/data.tf @@ -81,3 +81,8 @@ data "azurerm_private_dns_zone" "postgres" { name = "privatelink.postgres.database.azure.com" resource_group_name = local.core_resource_group_name } + +data "azurerm_private_dns_zone" "nexus" { + name = "nexus-${var.tre_id}.${var.location}.cloudapp.azure.com" + resource_group_name = local.core_resource_group_name +} diff --git a/templates/workspaces/base/terraform/network/zone_links.tf b/templates/workspaces/base/terraform/network/zone_links.tf index 84947ad82e..7d3da49b31 100644 --- a/templates/workspaces/base/terraform/network/zone_links.tf +++ b/templates/workspaces/base/terraform/network/zone_links.tf @@ -88,3 +88,12 @@ resource "azurerm_private_dns_zone_virtual_network_link" "postgreslink" { lifecycle { ignore_changes = [tags] } } + +resource "azurerm_private_dns_zone_virtual_network_link" "nexuslink" { + name = "nexuslink-${local.workspace_resource_name_suffix}" + resource_group_name = local.core_resource_group_name + private_dns_zone_name = data.azurerm_private_dns_zone.nexus.name + virtual_network_id = azurerm_virtual_network.ws.id + + lifecycle { ignore_changes = [tags] } +} diff --git a/templates/workspaces/base/terraform/providers.tf b/templates/workspaces/base/terraform/providers.tf index c5ddaeb448..6fbcc43ebb 100644 --- a/templates/workspaces/base/terraform/providers.tf +++ b/templates/workspaces/base/terraform/providers.tf @@ -2,7 +2,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "=3.4.0" + version = "=3.5.0" } azuread = { source = "hashicorp/azuread" diff --git a/templates/workspaces/base/terraform/variables.tf b/templates/workspaces/base/terraform/variables.tf index 09ee986e51..359ed5d445 100644 --- a/templates/workspaces/base/terraform/variables.tf +++ b/templates/workspaces/base/terraform/variables.tf @@ -78,6 +78,11 @@ variable "client_id" { default = "" description = "The client id of the workspace in the identity provider, this is passed in so that we may return it as an output." } +variable "client_secret" { + type = string + default = "" + description = "The client secret of the workspace in the identity provider, this is passed in so that we may return it as an output." +} variable "sp_id" { type = string default = "" diff --git a/templates/workspaces/base/terraform/workspace.tf b/templates/workspaces/base/terraform/workspace.tf index 45f8a84f30..c52501434a 100644 --- a/templates/workspaces/base/terraform/workspace.tf +++ b/templates/workspaces/base/terraform/workspace.tf @@ -28,9 +28,24 @@ module "aad" { count = var.register_aad_application ? 1 : 0 key_vault_id = azurerm_key_vault.kv.id workspace_resource_name_suffix = local.workspace_resource_name_suffix + workspace_owner_object_id = var.workspace_owner_object_id depends_on = [ azurerm_key_vault_access_policy.deployer, azurerm_key_vault_access_policy.resource_processor, - azurerm_private_endpoint.kvpe + null_resource.wait_for_dns_vault + ] +} + +module "airlock" { + source = "./airlock" + location = var.location + tre_id = var.tre_id + ws_resource_group_name = azurerm_resource_group.ws.name + enable_local_debugging = true + services_subnet_id = module.network.services_subnet_id + short_workspace_id = local.short_workspace_id + + depends_on = [ + module.network, ] } diff --git a/templates/workspaces/innereye/porter.yaml b/templates/workspaces/innereye/porter.yaml index d2fcba8b2e..c3bb249f64 100644 --- a/templates/workspaces/innereye/porter.yaml +++ b/templates/workspaces/innereye/porter.yaml @@ -1,7 +1,7 @@ --- name: tre-workspace-innereye -version: 0.1.10 +version: 0.3.0 description: "An Azure TRE workspace with Azure Machine Learning, Dev Test Labs and InnerEye deep learning" registry: azuretre