Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add mirror creation script #2008

Draft
wants to merge 46 commits into
base: develop-v5.1.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
bc9c5ee
WIP: Add mirror creation script
JimMadge Jul 10, 2024
c41196a
Add description
JimMadge Jul 10, 2024
d949996
Add delete script
JimMadge Jul 10, 2024
9405f9a
Correct pull mirror creation
JimMadge Jul 10, 2024
9a26635
Configure pull mirror repository
JimMadge Jul 10, 2024
e8b2c17
Fix linting
JimMadge Jul 10, 2024
9e1a5f2
Remove unused variables
JimMadge Jul 10, 2024
7f8fe87
Rewrite mirror creation as function app
JimMadge Jul 10, 2024
fabd2b2
Restructure directory
JimMadge Jul 10, 2024
68e85f2
Rename function and api root
JimMadge Jul 10, 2024
3b15ff2
Remove unused method to get args
JimMadge Jul 10, 2024
783999e
Correct status code check
JimMadge Jul 10, 2024
5ad78a4
Add exception for magic number
JimMadge Jul 10, 2024
bef8bcc
Add typings for azure functions
JimMadge Jul 10, 2024
00f3036
Remove empty comment
JimMadge Jul 10, 2024
f453d39
Remove old script
JimMadge Jul 11, 2024
428c649
Put both routes in one function app
JimMadge Jul 11, 2024
2e2dae3
Generalise argument parsing
JimMadge Jul 11, 2024
eb7bdab
Generalise response handling
JimMadge Jul 11, 2024
99dc8cb
Add return statement
JimMadge Jul 11, 2024
0a41555
Add basic test
JimMadge Jul 11, 2024
fb41278
Add delete mirror test
JimMadge Jul 11, 2024
003d24f
Add test for missing arguments
JimMadge Jul 11, 2024
8b63b8e
Add basic test for create mirror
JimMadge Jul 11, 2024
b88a380
Add test for failure to delete mirror
JimMadge Jul 11, 2024
76e5f44
Add failure tests for mirror creation
JimMadge Jul 11, 2024
edd65dc
Remove some magic strings
JimMadge Jul 11, 2024
e9c8f29
Run lint:fmt
JimMadge Jul 11, 2024
9cf8350
WIP: Add app service infrastructure
JimMadge Jul 12, 2024
6863e17
Merge remote-tracking branch 'origin/develop' into gitea_mirror_creation
JimMadge Jul 30, 2024
a91f17f
WIP remove resource group
JimMadge Jul 30, 2024
e8be73a
WIP: Add webapp
JimMadge Jul 30, 2024
b5cc5e1
Fix linting
JimMadge Jul 30, 2024
fd82175
Add apps component to sre
JimMadge Jul 30, 2024
0b6ba29
Fix import
JimMadge Jul 30, 2024
9031e8b
Merge remote-tracking branch 'origin/develop' into gitea_mirror_creation
JimMadge Jul 30, 2024
491a27a
Correct tags and HTTPS enum
JimMadge Jul 31, 2024
0753fa1
Fix arguments
JimMadge Jul 31, 2024
7fdc5fb
Change Function App name
JimMadge Jul 31, 2024
8dc0356
Update WebApp type
JimMadge Jul 31, 2024
e495c66
WIP: Add connection string
JimMadge Aug 1, 2024
6dffae9
Set webapp to always on
JimMadge Aug 1, 2024
5aec4e5
Set Python version
JimMadge Aug 1, 2024
97e4ca8
Restructure using v1 programming model
JimMadge Aug 1, 2024
f3c8838
Update requirements
JimMadge Aug 2, 2024
afdf574
Sort imports
JimMadge Aug 2, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions data_safe_haven/infrastructure/programs/declarative_sre.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
SREApplicationGatewayComponent,
SREApplicationGatewayProps,
)
from .sre.apps import (
SREAppsComponent,
SREAppsProps,
)
from .sre.apt_proxy_server import SREAptProxyServerComponent, SREAptProxyServerProps
from .sre.backup import (
SREBackupComponent,
Expand Down Expand Up @@ -327,6 +331,17 @@ def __call__(self) -> None:
tags=self.tags,
)

# Deploy apps
SREAppsComponent(
"sre_apps",
self.stack_name,
SREAppsProps(
location=self.config.azure.location,
resource_group_name=resource_group.name,
),
tags=self.tags,
)

# Deploy monitoring
monitoring = SREMonitoringComponent(
"sre_monitoring",
Expand Down
196 changes: 196 additions & 0 deletions data_safe_haven/infrastructure/programs/sre/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
"""Pulumi component for SRE function/web apps"""

from collections.abc import Mapping

from pulumi import ComponentResource, FileArchive, Input, Output, ResourceOptions
from pulumi_azure_native import (
storage,
web,
)

from data_safe_haven.functions import (
alphanumeric,
truncate_tokens,
)
from data_safe_haven.resources import resources_path


class SREAppsProps:
"""Properties for SREAppsComponent"""

def __init__(
self,
location: Input[str],
resource_group_name: Input[str],
):
self.location = location
self.resource_group_name = resource_group_name


class SREAppsComponent(ComponentResource):
"""Deploy SRE function/web apps with Pulumi"""

def __init__(
self,
name: str,
stack_name: str,
props: SREAppsProps,
opts: ResourceOptions | None = None,
tags: Input[Mapping[str, Input[str]]] | None = None,
) -> None:
super().__init__("dsh:sre:AppsComponent", name, {}, opts)
child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self))
child_tags = tags if tags else {}

# Deploy storage account
# The storage account holds app data/configuration
storage_account = storage.StorageAccount(
f"{self._name}_storage_account",
account_name=alphanumeric(
f"{''.join(truncate_tokens(stack_name.split('-'), 14))}apps"
)[:24],
kind=storage.Kind.STORAGE_V2,
location=props.location,
resource_group_name=props.resource_group_name,
sku=storage.SkuArgs(name=storage.SkuName.STANDARD_GRS),
opts=child_opts,
tags=child_tags,
)

# Create function apps container
container = storage.BlobContainer(
f"{self._name}_container_functions",
account_name=storage_account.name,
container_name="functions",
public_access=storage.PublicAccess.NONE,
resource_group_name=props.resource_group_name,
opts=ResourceOptions.merge(
child_opts,
ResourceOptions(parent=storage_account),
),
)

# Upload Gitea mirror function app
blob_gitea_mirror = storage.Blob(
f"{self._name}_blob_gitea_mirror",
account_name=storage_account.name,
container_name=container.name,
resource_group_name=props.resource_group_name,
source=FileArchive(
str((resources_path / "gitea_mirror" / "functions").absolute()),
),
opts=ResourceOptions.merge(
child_opts,
ResourceOptions(parent=container),
),
)

# Get URL of app blob
blob_url = get_blob_url(
blob=blob_gitea_mirror,
container=container,
storage_account=storage_account,
resource_group_name=props.resource_group_name,
)

# Get connection string
connection_string = get_connection_string(
resource_group_name=props.resource_group_name,
storage_account=storage_account,
)

# Deploy service plan
app_service_plan = web.AppServicePlan(
f"{self._name}_app_service_plan",
kind="linux",
location=props.location,
name=f"{stack_name}-app-service-plan",
reserved=True,
resource_group_name=props.resource_group_name,
sku={
"name": "B1",
"tier": "Basic",
"size": "B1",
"family": "B",
"capacity": 1,
},
tags=child_tags,
)

# Deploy app
web.WebApp(
f"{self._name}_web_app",
enabled=True,
https_only=True,
kind="functionapp,linux",
location=props.location,
name=f"{stack_name}-gitea-mirror-api",
resource_group_name=props.resource_group_name,
server_farm_id=app_service_plan.id,
site_config=web.SiteConfigArgs(
always_on=True,
app_settings=[
{"name": "AzureWebJobsStorage", "value": connection_string},
{"name": "runtime", "value": "python"},
{"name": "pythonVersion", "value": "3.11"},
{"name": "FUNCTIONS_WORKER_RUNTIME", "value": "python"},
{"name": "WEBSITE_RUN_FROM_PACKAGE", "value": blob_url},
{"name": "FUNCTIONS_EXTENSION_VERSION", "value": "~4"},
],
linux_fx_version="Python|3.11",
),
tags=child_tags,
)


def get_blob_url(
blob: Input[storage.Blob],
container: Input[storage.BlobContainer],
resource_group_name: Input[str],
storage_account: Input[storage.StorageAccount],
) -> Output[str]:
sas = storage.list_storage_account_service_sas_output(
account_name=storage_account.name,
protocols=storage.HttpProtocol.HTTPS,
shared_access_expiry_time="2030-01-01",
shared_access_start_time="2021-01-01",
resource_group_name=resource_group_name,
# Access to container
resource=storage.SignedResource.C,
# Read access
permissions=storage.Permissions.R,
canonicalized_resource=Output.format(
"/blob/{account_name}/{container_name}",
account_name=storage_account.name,
container_name=container.name,
),
# content_type="application/json",
# cache_control="max-age=5",
# content_disposition="inline",
# content_encoding="deflate",
)
token = sas.service_sas_token
return Output.format(
"https://{storage_account_name}.blob.core.windows.net/{container_name}/{blob_name}?{token}",
storage_account_name=storage_account.name,
container_name=container.name,
blob_name=blob.name,
token=token,
)


def get_connection_string(
resource_group_name: Input[str],
storage_account: Input[storage.StorageAccount],
) -> Output[str]:
storage_account_keys = storage.list_storage_account_keys_output(
resource_group_name=resource_group_name,
account_name=storage_account.name,
)
primary_storage_key = storage_account_keys.keys[0].value

return Output.format(
"DefaultEndpointsProtocol=https;AccountName={storage_account_name};AccountKey={primary_storage_key}",
storage_account_name=storage_account.name,
primary_storage_key=primary_storage_key,
)
48 changes: 48 additions & 0 deletions data_safe_haven/resources/gitea_mirror/functions/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
bin
obj
csx
.vs
edge
Publish

*.user
*.suo
*.cscfg
*.Cache
project.lock.json

/packages
/TestResults

/tools/NuGet.exe
/App_Data
/secrets
/data
.secrets
appsettings.json
local.settings.json

node_modules
dist

# Local python packages
.python_packages/

# Python Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# Azurite artifacts
__blobstorage__
__queuestorage__
__azurite_db*__.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import logging

import azure.functions as func
import requests
from requests.auth import HTTPBasicAuth
from shared_code import (
api_root,
check_args,
get_args,
gitea_host,
handle_response,
migrate_path,
missing_parameters_repsonse,
repos_path,
timeout,
)


def main(req: func.HttpRequest) -> func.HttpResponse:
logging.info("Request received.")

raw_args = get_args(
[
"address",
"name",
"password",
"username",
],
req,
)
args = check_args(raw_args)
if not args:
return missing_parameters_repsonse()

extra_data = {
"description": f"Read-only mirror of {args['address']}",
"mirror": True,
"mirror_interval": "10m",
}

auth = HTTPBasicAuth(
username=args["username"],
password=args["password"],
)

logging.info("Sending request to create mirror.")

response = requests.post(
auth=auth,
data={
"clone_addr": args["address"],
"repo_name": args["name"],
}
| extra_data,
timeout=timeout,
url=gitea_host + api_root + migrate_path,
)

if r := handle_response(response, [201], "Error creating repository."):
return r

# Some arguments of the migrate endpoint seem to be ignored or overwritten.
# We set repository settings here.
logging.info("Sending request to configure mirror repo.")

response = requests.patch(
auth=auth,
data={
"has_actions": False,
"has_issues": False,
"has_packages": False,
"has_projects": False,
"has_pull_requests": False,
"has_releases": False,
"has_wiki": False,
},
timeout=timeout,
url=gitea_host + api_root + repos_path + f"/{args['username']}/{args['name']}",
)

if r := handle_response(response, [200], "Error configuring repository."):
return r

return func.HttpResponse(
"Mirror successfully created.",
status_code=200,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"scriptFile": "__init__.py",
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}
Loading
Loading