Skip to content

Commit

Permalink
Merge branch 'main' into work/1760/images
Browse files Browse the repository at this point in the history
  • Loading branch information
lengau authored Aug 29, 2024
2 parents 1103e94 + c0b4896 commit f0403dc
Show file tree
Hide file tree
Showing 22 changed files with 360 additions and 90 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ body:
If possible, please paste your charmcraft.yaml contents. This
will be automatically formatted into code, so no need for
backticks.
render: shell
render: yaml
validations:
required: true
- type: textarea
Expand Down
8 changes: 7 additions & 1 deletion .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
// Each ignore is probably connected with an ignore in pyproject.toml.
// Ensure you change this and those simultaneously.
"urllib3",
// Temporary until we remove Windows. https://github.com/canonical/charmcraft/issues/1810
"windows", // We'll update Windows versions manually.
"tox-gh", // As of 1.3.2 tox-gh doesn't support Windows 2019's python 3.7.
],
labels: ["dependencies"], // For convenient searching in GitHub
baseBranches: ["$default", "/^hotfix\\/.*/"],
Expand Down Expand Up @@ -38,7 +40,11 @@
// Automerge patches, pin changes and digest changes.
// Also groups these changes together.
groupName: "bugfixes",
excludeDepPatterns: ["lint/.*", "types/.*"],
excludeDepPatterns: [
"lint/.*",
"types/.*",
"pyright", // Pyright needs to be done separately.
],
matchUpdateTypes: ["patch", "pin", "digest"],
prPriority: 3, // Patches should go first!
automerge: true
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/publish-pypi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- uses: actions/setup-python@v5
with:
python-version: '3.12'
Expand Down
1 change: 1 addition & 0 deletions charmcraft/application/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"machine": "init-machine",
"flask-framework": "init-flask-framework",
"django-framework": "init-django-framework",
"go-framework": "init-go-framework",
}
DEFAULT_PROFILE = "simple"

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

from charmcraft.extensions._utils import apply_extensions
from charmcraft.extensions.app import DjangoFramework, FlaskFramework, GoFramework
from charmcraft.extensions.extension import Extension
from charmcraft.extensions.gunicorn import DjangoFramework, FlaskFramework
from charmcraft.extensions.registry import (
get_extension_class,
get_extension_names,
Expand All @@ -41,3 +41,4 @@

register("flask-framework", FlaskFramework)
register("django-framework", DjangoFramework)
register("go-framework", GoFramework)
122 changes: 87 additions & 35 deletions charmcraft/extensions/gunicorn.py → charmcraft/extensions/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,8 @@
from .extension import Extension


class _GunicornBase(Extension):
"""A base class for 12-factor WSGI applications."""

_WEBSERVER_OPTIONS = {
"webserver-keepalive": {
"type": "int",
"description": "Time in seconds for webserver to wait for requests on a Keep-Alive connection.",
},
"webserver-threads": {
"type": "int",
"description": "Run each webserver worker with the specified number of threads.",
},
"webserver-timeout": {
"type": "int",
"description": "Time in seconds to kill and restart silent webserver workers.",
},
"webserver-workers": {
"type": "int",
"description": "The number of webserver worker processes for handling requests.",
},
}
class _AppBase(Extension):
"""A base class for 12-factor applications."""

_CHARM_LIBS = [
{"lib": "traefik_k8s.ingress", "version": "2"},
Expand All @@ -71,7 +52,12 @@ def is_experimental(base: tuple[str, ...] | None) -> bool: # noqa: ARG004
return True

framework: str
actions: dict
actions: dict = {
"rotate-secret-key": {
"description": "Rotate the secret key. Users will be forced to log in again. This might be useful if a security breach occurs."
}
}

options: dict

def _get_nested(self, obj: dict, path: str) -> dict:
Expand Down Expand Up @@ -138,10 +124,10 @@ def _get_root_snippet(self) -> dict[str, Any]:
return {
"assumes": ["k8s-api"],
"containers": {
f"{self.framework}-app": {"resource": f"{self.framework}-app-image"},
self.get_container_name(): {"resource": self.get_image_name()},
},
"resources": {
f"{self.framework}-app-image": {
self.get_image_name(): {
"type": "oci-image",
"description": f"{self.framework} application image.",
},
Expand All @@ -157,7 +143,7 @@ def _get_root_snippet(self) -> dict[str, Any]:
"metrics-endpoint": {"interface": "prometheus_scrape"},
"grafana-dashboard": {"interface": "grafana_dashboard"},
},
"config": {"options": {**self._WEBSERVER_OPTIONS, **self.options}},
"config": {"options": self.options},
"parts": {
"charm": {
"plugin": "charm",
Expand All @@ -184,17 +170,41 @@ def get_parts_snippet(self) -> dict[str, Any]:
"""Return the parts to add to parts."""
return {}

def get_container_name(self) -> str:
"""Return name of the container for the app image."""
return f"{self.framework}-app"

def get_image_name(self) -> str:
"""Return name of the app image."""
return f"{self.framework}-app-image"


GUNICORN_WEBSERVER_OPTIONS = {
"webserver-keepalive": {
"type": "int",
"description": "Time in seconds for webserver to wait for requests on a Keep-Alive connection.",
},
"webserver-threads": {
"type": "int",
"description": "Run each webserver worker with the specified number of threads.",
},
"webserver-timeout": {
"type": "int",
"description": "Time in seconds to kill and restart silent webserver workers.",
},
"webserver-workers": {
"type": "int",
"description": "The number of webserver worker processes for handling requests.",
},
}


class FlaskFramework(_GunicornBase):
class FlaskFramework(_AppBase):
"""Extension for 12-factor Flask applications."""

framework = "flask"
actions = {
"rotate-secret-key": {
"description": "Rotate the flask secret key. Users will be forced to log in again. This might be useful if a security breach occurs."
}
}
options = {
**GUNICORN_WEBSERVER_OPTIONS,
"flask-application-root": {
"type": "string",
"description": "Path in which the application / web server is mounted. This configuration will set the FLASK_APPLICATION_ROOT environment variable. Run `app.config.from_prefixed_env()` in your Flask application in order to receive this configuration.",
Expand Down Expand Up @@ -233,21 +243,20 @@ def is_experimental(base: tuple[str, ...] | None) -> bool: # noqa: ARG004
return False


class DjangoFramework(_GunicornBase):
class DjangoFramework(_AppBase):
"""Extension for 12-factor Django applications."""

framework = "django"
actions = {
"rotate-secret-key": {
"description": "Rotate the django secret key. Users will be forced to log in again. This might be useful if a security breach occurs."
},
**_AppBase.actions,
"create-superuser": {
"description": "Create a new Django superuser account.",
"params": {"username": {"type": "string"}, "email": {"type": "string"}},
"required": ["username", "email"],
},
}
options = {
**GUNICORN_WEBSERVER_OPTIONS,
"django-debug": {
"type": "boolean",
"default": False,
Expand All @@ -262,3 +271,46 @@ class DjangoFramework(_GunicornBase):
"description": "A comma-separated list of host/domain names that this Django site can serve. This configuration will set the DJANGO_ALLOWED_HOSTS environment variable with its content being a JSON encoded list.",
},
}


class GoFramework(_AppBase):
"""Extension for 12-factor Go applications."""

framework = "go"
options = {
"app-port": {
"type": "int",
"default": 8080,
"description": "Default port where the application will listen on.",
},
"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"
13 changes: 11 additions & 2 deletions charmcraft/models/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import pydantic
from craft_application import models
from craft_application.models import base
from craft_cli import CraftError
from typing_extensions import Self, override

Expand All @@ -37,12 +38,20 @@ class CharmMetadata(models.BaseMetadata):
of a charm.
"""

model_config = pydantic.ConfigDict(
validate_assignment=True,
extra="ignore",
populate_by_name=True,
alias_generator=base.alias_generator,
)

name: models.ProjectName
display_name: models.ProjectTitle | None = None
summary: pydantic.StrictStr
description: pydantic.StrictStr
maintainers: list[pydantic.StrictStr] | None = None
assumes: list[str | dict[str, list | dict]] | None = None
charm_user: str | None = None
containers: dict[str, Any] | None = None
devices: dict[str, Any] | None = None
docs: pydantic.AnyHttpUrl | None = None
Expand All @@ -65,12 +74,12 @@ def from_charm(cls, charm: Charm) -> Self:
Performs the necessary renaming and reorganisation.
"""
charm_dict = charm.model_dump(
include={"title"} | const.METADATA_YAML_KEYS,
exclude_none=True,
by_alias=True,
exclude_defaults=False,
)

# Flatten links and match to the appropriate metadata.yaml name
# Flatten links and match to the appropriate metadata.yaml schema
if charm.links is not None:
links = charm.links.marshal()
if "documentation" in links:
Expand Down
18 changes: 18 additions & 0 deletions charmcraft/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,24 @@ class CharmProject(CharmcraftProject):
"""
),
)
charm_user: Literal["root", "sudoer", "non-root"] | None = pydantic.Field(
default=None,
description=textwrap.dedent(
"""\
Specifies that the charm code does not need to be run as root. Possible
values are:
- ``root``: the charm will run as root
- ``sudoer``: the charm will run as a non-root user with access to root
privileges using ``sudo``.
- ``non-root``: the charm will run as a non-root user without ``sudo``.
Only affects Kubernetes charms on Juju 3.6.0 or later. If not specified,
Juju will use
`its default behaviour <https://juju.is/docs/sdk/metadata-yaml#heading--charm-user>`_.
"""
),
)
containers: dict[str, Any] | None = pydantic.Field(
default=None,
description=textwrap.dedent(
Expand Down
4 changes: 2 additions & 2 deletions charmcraft/templates/init-django-framework/src/charm.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import paas_app_charmer.django
logger = logging.getLogger(__name__)


class DjangoCharm(paas_app_charmer.django.Charm):
class {{ class_name }}(paas_app_charmer.django.Charm):
"""Django Charm service."""

def __init__(self, *args: typing.Any) -> None:
Expand All @@ -27,4 +27,4 @@ class DjangoCharm(paas_app_charmer.django.Charm):


if __name__ == "__main__":
ops.main.main(DjangoCharm)
ops.main.main({{ class_name }})
4 changes: 2 additions & 2 deletions charmcraft/templates/init-flask-framework/src/charm.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import paas_app_charmer.flask
logger = logging.getLogger(__name__)


class FlaskCharm(paas_app_charmer.flask.Charm):
class {{ class_name }}(paas_app_charmer.flask.Charm):
"""Flask Charm service."""

def __init__(self, *args: typing.Any) -> None:
Expand All @@ -27,4 +27,4 @@ class FlaskCharm(paas_app_charmer.flask.Charm):


if __name__ == "__main__":
ops.main.main(FlaskCharm)
ops.main.main({{ class_name }})
9 changes: 9 additions & 0 deletions charmcraft/templates/init-go-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/
Loading

0 comments on commit f0403dc

Please sign in to comment.