Skip to content

Commit

Permalink
go-framework init and extension
Browse files Browse the repository at this point in the history
  • Loading branch information
javierdelapuente committed Aug 8, 2024
1 parent fd21df2 commit 4acc1a3
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 31 deletions.
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 @@ -18,7 +18,7 @@

from charmcraft.extensions._utils import apply_extensions
from charmcraft.extensions.extension import Extension
from charmcraft.extensions.gunicorn import DjangoFramework, FlaskFramework
from charmcraft.extensions.twelvefactor import DjangoFramework, FlaskFramework, GoFramework
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)
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 _TwelveFactorBase(Extension):
"""A base class for 12-factor applications."""

_CHARM_LIBS = [
{"lib": "traefik_k8s.ingress", "version": "2"},
Expand Down Expand Up @@ -138,10 +119,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 +138,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,8 +165,36 @@ 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(_TwelveFactorBase):
"""Extension for 12-factor Flask applications."""

framework = "flask"
Expand All @@ -195,6 +204,7 @@ class FlaskFramework(_GunicornBase):
}
}
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,7 +243,7 @@ def is_experimental(base: tuple[str, ...] | None) -> bool: # noqa: ARG004
return False


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

framework = "django"
Expand All @@ -248,6 +258,7 @@ class DjangoFramework(_GunicornBase):
},
}
options = {
**GUNICORN_WEBSERVER_OPTIONS,
"django-debug": {
"type": "boolean",
"default": False,
Expand All @@ -262,3 +273,45 @@ 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(_TwelveFactorBase):
"""Extension for 12-factor Go applications."""

framework = "go"
actions = {
"rotate-secret-key": {
"description": "Rotate the go secret key. Users will be forced to log in again. This might be useful if a security breach occurs."
}
}
options = {
"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.",
},
"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",
},
}

@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-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/
53 changes: 53 additions & 0 deletions charmcraft/templates/init-go-framework/charmcraft.yaml.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# This file configures Charmcraft.
# See https://juju.is/docs/sdk/charmcraft-config for guidance.

name: {{ name }}

type: charm

bases:
- build-on:
- name: ubuntu
channel: "22.04"
run-on:
- name: ubuntu
channel: "22.04"

# (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
1 change: 1 addition & 0 deletions charmcraft/templates/init-go-framework/requirements.txt.j2
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 GoCharm(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(GoCharm)
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from charmcraft.errors import ExtensionError
from charmcraft.extensions import apply_extensions
from charmcraft.extensions.gunicorn import DjangoFramework, FlaskFramework
from charmcraft.extensions.twelvefactor import DjangoFramework, FlaskFramework, GoFramework


def make_flask_input_yaml():
Expand Down Expand Up @@ -63,7 +63,7 @@ def flask_input_yaml_fixture():
{"lib": "saml_integrator.saml", "version": "0"},
],
"config": {
"options": {**FlaskFramework.options, **FlaskFramework._WEBSERVER_OPTIONS}
"options": {**FlaskFramework.options},
},
"parts": {
"charm": {
Expand Down Expand Up @@ -123,7 +123,7 @@ def flask_input_yaml_fixture():
{"lib": "saml_integrator.saml", "version": "0"},
],
"config": {
"options": {**DjangoFramework.options, **DjangoFramework._WEBSERVER_OPTIONS}
"options": {**DjangoFramework.options},
},
"parts": {
"charm": {
Expand Down Expand Up @@ -152,6 +152,66 @@ def flask_input_yaml_fixture():
"type": "charm",
},
),
(
{
"type": "charm",
"name": "test-go",
"summary": "test summary",
"description": "test description",
"bases": [{"name": "ubuntu", "channel": "22.04"}],
"extensions": ["go-framework"],
},
True,
{
"actions": GoFramework.actions,
"assumes": ["k8s-api"],
"bases": [{"channel": "22.04", "name": "ubuntu"}],
"containers": {
"app": {"resource": "app-image"},
},
"description": "test description",
"name": "test-go",
"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": {**GoFramework.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": "go application image.",
"type": "oci-image",
},
},
"summary": "test summary",
"type": "charm",
},
),
],
)
def test_apply_extensions_correct(monkeypatch, experimental, tmp_path, input_yaml, expected):
Expand Down Expand Up @@ -184,7 +244,6 @@ def test_flask_merge_options(flask_input_yaml, tmp_path):
assert applied["config"] == {
"options": {
**FlaskFramework.options,
**FlaskFramework._WEBSERVER_OPTIONS,
**added_options,
}
}
Expand Down

0 comments on commit 4acc1a3

Please sign in to comment.