Skip to content

Commit

Permalink
feat: fastapi-framework init profile and extension (#1868)
Browse files Browse the repository at this point in the history
This PR implements the spec ISD160 - 12-Factor FastAPI Support" for
charmcraft.

For that, a new init profile called "fastapi-framework" and a new
fastapi extension, called "fastapi-framework" are needed. These
additions are similar to the flask and django init profiles and
extensions.

For the fastapi profile, the base 24.04 is used.

---------

Co-authored-by: Alex Lowe <alex.lowe@canonical.com>
  • Loading branch information
javierdelapuente and lengau committed Sep 17, 2024
1 parent 34b9b3d commit ec881a4
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 5 deletions.
1 change: 1 addition & 0 deletions charmcraft/application/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"flask-framework": "init-flask-framework",
"django-framework": "init-django-framework",
"go-framework": "init-go-framework",
"fastapi-framework": "init-fastapi-framework",
}
DEFAULT_PROFILE = "simple"

Expand Down
8 changes: 7 additions & 1 deletion charmcraft/extensions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
"""Extension processor and related utilities."""

from charmcraft.extensions._utils import apply_extensions
from charmcraft.extensions.app import DjangoFramework, FlaskFramework, GoFramework
from charmcraft.extensions.app import (
DjangoFramework,
FastAPIFramework,
FlaskFramework,
GoFramework,
)
from charmcraft.extensions.extension import Extension
from charmcraft.extensions.registry import (
get_extension_class,
Expand All @@ -42,3 +47,4 @@
register("flask-framework", FlaskFramework)
register("django-framework", DjangoFramework)
register("go-framework", GoFramework)
register("fastapi-framework", FastAPIFramework)
53 changes: 53 additions & 0 deletions charmcraft/extensions/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,3 +314,56 @@ def get_image_name(self) -> str:
def get_container_name(self) -> str:
"""Return name of the container for the app image."""
return "app"


class FastAPIFramework(_AppBase):
"""Extension for 12-factor FastAPI applications."""

framework = "fastapi"
options = {
"webserver-workers": {
"type": "int",
"default": 1,
"description": "Number of workers for uvicorn. Sets env variable WEB_CONCURRENCY. See https://www.uvicorn.org/#command-line-options.",
},
"webserver-port": {
"type": "int",
"default": 8080,
"description": "Bind to a socket with this port. Default: 8000. Sets env variable UVICORN_PORT.",
},
"webserver-log-level": {
"type": "string",
"default": "info",
"description": "Set the log level. Options: 'critical', 'error', 'warning', 'info', 'debug', 'trace'. Sets the env variable UVICORN_LOG_LEVEL.",
},
"metrics-port": {
"type": "int",
"default": 8080,
"description": "Port where the prometheus metrics will be scraped.",
},
"metrics-path": {
"type": "string",
"default": "/metrics",
"description": "Path where the prometheus metrics will be scraped.",
},
"app-secret-key": {
"type": "string",
"description": "Long secret you can use for sessions, csrf or any other thing where you need a random secret shared by all units",
},
}

@staticmethod
@override
def get_supported_bases() -> list[tuple[str, str]]:
"""Return supported bases."""
return [("ubuntu", "24.04")]

@override
def get_image_name(self) -> str:
"""Return name of the app image."""
return "app-image"

@override
def get_container_name(self) -> str:
"""Return name of the container for the app image."""
return "app"
9 changes: 9 additions & 0 deletions charmcraft/templates/init-fastapi-framework/.gitignore.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
venv/
build/
*.charm
.tox/
.coverage
__pycache__/
*.py[cod]
.idea
.vscode/
59 changes: 59 additions & 0 deletions charmcraft/templates/init-fastapi-framework/charmcraft.yaml.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# This file configures Charmcraft.
# See https://juju.is/docs/sdk/charmcraft-config for guidance.

name: {{ name }}

type: charm

base: ubuntu@24.04

# the platforms this charm should be built on and run on.
# you can check your architecture with `dpkg --print-architecture`
platforms:
amd64:
# arm64:
# ppc64el:
# s390x:

# (Required)
summary: A very short one-line summary of the FastAPI application.

# (Required)
description: |
A comprehensive overview of your FastAPI application.

extensions:
- fastapi-framework

# Uncomment the integrations used by your application
# Integrations set to "optional: false" will block the charm
# until the applications are integrated.
# requires:
# mysql:
# interface: mysql_client
# optional: false
# limit: 1
# postgresql:
# interface: postgresql_client
# optional: false
# limit: 1
# mongodb:
# interface: mongodb_client
# optional: false
# limit: 1
# redis:
# interface: redis
# optional: false
# limit: 1
# s3:
# interface: s3
# optional: false
# limit: 1
# saml:
# interface: saml
# optional: false
# limit: 1
# rabbitmq:
# interface: rabbitmq
# optional: false
# limit: 1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
paas-app-charmer==1.*
30 changes: 30 additions & 0 deletions charmcraft/templates/init-fastapi-framework/src/charm.py.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
# Copyright {{ year }} {{ author }}
# See LICENSE file for licensing details.

"""FastAPI Charm entrypoint."""

import logging
import typing

import ops

import paas_app_charmer.fastapi

logger = logging.getLogger(__name__)


class {{ class_name }}(paas_app_charmer.fastapi.Charm):
"""FastAPI Charm service."""

def __init__(self, *args: typing.Any) -> None:
"""Initialize the instance.
Args:
args: passthrough to CharmBase.
"""
super().__init__(*args)


if __name__ == "__main__":
ops.main.main({{ class_name }})
77 changes: 75 additions & 2 deletions tests/extensions/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from charmcraft.extensions import apply_extensions
from charmcraft.extensions.app import (
DjangoFramework,
FastAPIFramework,
FlaskFramework,
GoFramework,
)
Expand Down Expand Up @@ -178,14 +179,20 @@ def flask_input_yaml_fixture():
"name": "test-go",
"summary": "test summary",
"description": "test description",
"bases": [{"name": "ubuntu", "channel": "24.04"}],
"base": "ubuntu@24.04",
"platforms": {
"amd64": None,
},
"extensions": ["go-framework"],
},
True,
{
"actions": GoFramework.actions,
"assumes": ["k8s-api"],
"bases": [{"channel": "24.04", "name": "ubuntu"}],
"base": "ubuntu@24.04",
"platforms": {
"amd64": None,
},
"containers": {
"app": {"resource": "app-image"},
},
Expand Down Expand Up @@ -232,6 +239,72 @@ def flask_input_yaml_fixture():
"type": "charm",
},
),
(
{
"type": "charm",
"name": "test-fastapi",
"summary": "test summary",
"description": "test description",
"base": "ubuntu@24.04",
"platforms": {
"amd64": None,
},
"extensions": ["fastapi-framework"],
},
True,
{
"actions": FastAPIFramework.actions,
"assumes": ["k8s-api"],
"base": "ubuntu@24.04",
"platforms": {
"amd64": None,
},
"containers": {
"app": {"resource": "app-image"},
},
"description": "test description",
"name": "test-fastapi",
"charm-libs": [
{"lib": "traefik_k8s.ingress", "version": "2"},
{"lib": "observability_libs.juju_topology", "version": "0"},
{"lib": "grafana_k8s.grafana_dashboard", "version": "0"},
{"lib": "loki_k8s.loki_push_api", "version": "0"},
{"lib": "data_platform_libs.data_interfaces", "version": "0"},
{"lib": "prometheus_k8s.prometheus_scrape", "version": "0"},
{"lib": "redis_k8s.redis", "version": "0"},
{"lib": "data_platform_libs.s3", "version": "0"},
{"lib": "saml_integrator.saml", "version": "0"},
],
"config": {
"options": {**FastAPIFramework.options},
},
"parts": {
"charm": {
"plugin": "charm",
"source": ".",
"build-snaps": ["rustup"],
"override-build": "rustup default stable\ncraftctl default",
}
},
"peers": {"secret-storage": {"interface": "secret-storage"}},
"provides": {
"metrics-endpoint": {"interface": "prometheus_scrape"},
"grafana-dashboard": {"interface": "grafana_dashboard"},
},
"requires": {
"logging": {"interface": "loki_push_api"},
"ingress": {"interface": "ingress", "limit": 1},
},
"resources": {
"app-image": {
"description": "fastapi application image.",
"type": "oci-image",
},
},
"summary": "test summary",
"type": "charm",
},
),
],
)
def test_apply_extensions_correct(monkeypatch, experimental, tmp_path, input_yaml, expected):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
summary: test charmcraft init with flask-framework profile
summary: test charmcraft init with framework profiles
priority: 500 # This builds pydantic, so do it early
kill-timeout: 75m # Because it builds pydantic, it takes a long time.
systems:
# We only need to run this test once, and it takes a long time.
- ubuntu-22.04-64
environment:
PROFILE/flask: flask-framework
PROFILE/django: django-framework
PROFILE/go: go-framework
PROFILE/fastapi: fastapi-framework
CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: "true"

execute: |
# Required for fetch-libs to succeed since the libraries are not available on
Expand All @@ -14,7 +20,7 @@ execute: |
mkdir -p test-init
cd test-init
charmcraft init --profile flask-framework
charmcraft init --profile "${PROFILE}"
charmcraft fetch-libs
charmcraft pack --verbose
test -f *.charm
Expand Down

0 comments on commit ec881a4

Please sign in to comment.