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

feat: go-framework init profile and extension #1800

Merged
merged 13 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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"
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/
javierdelapuente marked this conversation as resolved.
Show resolved Hide resolved
.coverage
__pycache__/
*.py[cod]
.idea
.vscode/
55 changes: 55 additions & 0 deletions charmcraft/templates/init-go-framework/charmcraft.yaml.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps include a note explaining what should be done including which command to run to check the architecture, e.g., uname -m and how to interpret the command output?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Added comment with dpkg --print-architecture command, as that is the one giving the correct name (debian architecture names are used)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Added comment with dpkg --print-architecture command, as that is the one giving the correct name (debian architecture names are used)

# ppc64el:
# s390x:

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

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

extensions:
- go-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
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-go-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.

"""Go Charm entrypoint."""

import logging
import typing

import ops

import paas_app_charmer.go

logger = logging.getLogger(__name__)


class {{ class_name }}(paas_app_charmer.go.Charm):
"""Go 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 }})
Loading
Loading