-
Notifications
You must be signed in to change notification settings - Fork 46
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
extensions: new flask extension #317
Changes from 36 commits
3f3a222
caa7dbf
128c58d
016b2b1
10ec7d4
8f622fa
d5a048d
dde4b33
f40b7dd
2364f45
ce8c2b5
2f4e560
d6926e3
21d7dfb
2bc660d
310408f
6f1a627
8c25807
1e8f860
2830f97
d5890cd
efbfa3b
781eab6
97aa7e2
b31ba69
75049de
10ec23d
3212eae
0fe5723
ce2ce3e
6e3f682
463e298
cab47e8
b89ceb7
bf684a3
ce9e067
3841e78
2340cb6
5487cb5
1df6a35
5d3a099
c51286e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,7 @@ fs | |
GID | ||
github | ||
GPG | ||
Gunicorn | ||
https | ||
init | ||
interoperable | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
Using the flask extension | ||
------------------------- | ||
|
||
The Flask extension is compatible with the ``bare``, ``ubuntu:20.04``, and | ||
``ubuntu:22.04`` bases. To employ it, include ``extensions: [flask]`` in your | ||
``rockcraft.yaml`` file. | ||
|
||
Example: | ||
|
||
.. code-block:: yaml | ||
|
||
name: example-flask | ||
summary: A Flask application | ||
description: A ROCK packing a Flask application via the flask extension | ||
version: "0.1" | ||
base: bare | ||
license: Apache-2.0 | ||
|
||
extensions: | ||
- flask | ||
|
||
flask/install-app: | ||
prime: | ||
- -srv/flask/app/.git | ||
- -srv/flask/app/.venv | ||
- -srv/flask/app/.yarn | ||
- -srv/flask/app/node_modules | ||
|
||
Managing project files with the flask extension | ||
----------------------------------------------- | ||
|
||
The prime declaration must be included in the specially-named | ||
``flask/install-app`` section to instruct the flask extension on which files | ||
to include or exclude from the project directory in the ROCK image. | ||
tigarmo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
For example, to include only select files: | ||
|
||
.. code-block:: yaml | ||
|
||
flask/install-app: | ||
prime: | ||
- srv/flask/app/static | ||
- srv/flask/app/.env | ||
- srv/flask/app/webapp | ||
- srv/flask/app/templates | ||
|
||
To exclude certain files from the project directory in the rock image, | ||
add the following part to ``rockcraft.yaml``: | ||
|
||
.. code-block:: yaml | ||
|
||
flask/install-app: | ||
prime: | ||
- -srv/flask/app/.git | ||
- -srv/flask/app/.venv | ||
- -srv/flask/app/.yarn | ||
- -srv/flask/app/node_modules |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
Extensions | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 ty for starting this page |
||
********** | ||
|
||
Just as the Snapcraft extensions are designed to simplify Snap creation, | ||
Rockcraft extensions are crafted to expand and modify the user-provided | ||
rockcraft project file, aiming to minimise the boilerplate code when | ||
initiating a new rock. | ||
|
||
The ``flask`` extension | ||
----------------------- | ||
|
||
The Flask extension streamlines the process of building Flask application rocks. | ||
|
||
It facilitates the installation of Flask application dependencies, including | ||
Gunicorn, in the rock image. Additionally, it transfers your project files to | ||
``/srv/flask/app`` within the rock image. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,3 +14,4 @@ Rockcraft's components, commands and keywords. | |
parts | ||
commands | ||
part_properties | ||
extensions |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- | ||
# | ||
# Copyright 2023 Canonical Ltd. | ||
# | ||
# This program is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License version 3 as | ||
# published by the Free Software Foundation. | ||
# | ||
# This program is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
"""An experimental extension for the Flask framework.""" | ||
|
||
import copy | ||
import posixpath | ||
import re | ||
from typing import Any, Dict, Optional, Tuple | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Which python version is used on rockcraft? Some of these are deprecated and should be replaced with the built in types There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rockcraft is targeting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jdkandersson is right with regards to the current best practices but the rest of rockcraft's codebase is still using the types from |
||
|
||
from overrides import override | ||
|
||
from ..errors import ExtensionError | ||
from ._utils import _apply_extension_property | ||
from .extension import Extension | ||
|
||
|
||
class Flask(Extension): | ||
"""An extension for constructing Python applications based on the Flask framework.""" | ||
|
||
@staticmethod | ||
@override | ||
def get_supported_bases() -> Tuple[str, ...]: | ||
"""Return supported bases.""" | ||
return "bare", "ubuntu:20.04", "ubuntu:22.04" | ||
|
||
@staticmethod | ||
@override | ||
def is_experimental(base: Optional[str]) -> bool: | ||
"""Check if the extension is in an experimental state.""" | ||
return True | ||
|
||
@override | ||
def get_root_snippet(self) -> Dict[str, Any]: | ||
"""Fill in some default root components for Flask. | ||
|
||
Default values: | ||
- run_user: _daemon_ | ||
- build-base: ubuntu:22.04 (only if user specify bare without a build-base) | ||
- platform: amd64 | ||
""" | ||
snippet: Dict[str, Any] = {} | ||
if "run_user" not in self.yaml_data: | ||
snippet["run_user"] = "_daemon_" | ||
if ( | ||
"build-base" not in self.yaml_data | ||
and self.yaml_data.get("base", "bare") == "bare" | ||
): | ||
snippet["build-base"] = "ubuntu:22.04" | ||
if "platforms" not in self.yaml_data: | ||
snippet["platforms"] = {"amd64": {}} | ||
current_parts = copy.deepcopy(self.yaml_data.get("parts", {})) | ||
current_parts.update(self._gen_new_parts()) | ||
snippet["parts"] = current_parts | ||
snippet["services"] = self._gen_services() | ||
return snippet | ||
|
||
def _gen_services(self): | ||
"""Return the services snipped to be applied to the rockcraft file.""" | ||
services = { | ||
"flask": { | ||
"override": "replace", | ||
"startup": "enabled", | ||
"command": "/bin/python3 -m gunicorn --bind 0.0.0.0:8000 app:app", | ||
"user": "_daemon_", | ||
"working-dir": "/srv/flask/app", | ||
} | ||
} | ||
existing_services = copy.deepcopy(self.yaml_data.get("services", {})) | ||
for existing_service_name, existing_service in existing_services.items(): | ||
if existing_service_name in services: | ||
services[existing_service_name].update(existing_service) | ||
else: | ||
services[existing_service_name] = existing_service | ||
return services | ||
|
||
@override | ||
def get_part_snippet(self) -> Dict[str, Any]: | ||
"""Return the part snippet to apply to existing parts.""" | ||
return {} | ||
|
||
def _merge_part(self, base_part: dict, new_part: dict) -> dict: | ||
"""Merge two part definitions by the extension part merging rule.""" | ||
result = {} | ||
properties = set(base_part.keys()).union(set(new_part.keys())) | ||
for property_name in properties: | ||
if property_name in base_part and property_name not in new_part: | ||
result[property_name] = base_part[property_name] | ||
elif property_name not in base_part and property_name in new_part: | ||
result[property_name] = new_part[property_name] | ||
else: | ||
result[property_name] = _apply_extension_property( | ||
base_part[property_name], new_part[property_name] | ||
) | ||
return result | ||
|
||
def _merge_existing_part(self, part_name: str, part_def: dict) -> dict: | ||
"""Merge the new part with the existing part in the current rockcraft.yaml.""" | ||
existing_part = self.yaml_data.get("parts", {}).get(part_name, {}) | ||
return self._merge_part(existing_part, part_def) | ||
|
||
def _gen_new_parts(self) -> Dict[str, Any]: | ||
"""Generate new parts for the flask extension. | ||
|
||
Parts added: | ||
- flask/dependencies: install Python dependencies | ||
- flask/install-app: copy the flask project into the OCI image | ||
""" | ||
if not (self.project_root / "requirements.txt").exists(): | ||
raise ExtensionError( | ||
"missing requirements.txt file, " | ||
"flask extension requires this file with flask specified as a dependency" | ||
) | ||
source_files = [f.name for f in sorted(self.project_root.iterdir())] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can go with this for now, but this extension as is, only serves 12f, as one cannot have https://github.com/flaskfactory/my-flask.git as source entry for building your application |
||
renaming_map = {f: posixpath.join("srv/flask/app", f) for f in source_files} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was trying to find the handling of requiring prime and checking that everything in there has the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That has been added in the latest commit, thanks! |
||
install_app_part_name = "flask/install-app" | ||
dependencies_part_name = "flask/dependencies" | ||
|
||
if install_app_part_name not in self.yaml_data.get("parts", {}): | ||
raise ExtensionError( | ||
"flask extension required flask/install-app not found " | ||
"in parts of the rockcraft file" | ||
) | ||
install_prime = self.yaml_data["parts"][install_app_part_name].get("prime") | ||
if not install_prime: | ||
raise ExtensionError( | ||
"flask extension required prime list not found or empty" | ||
"in the flask/install-app part of the rockcraft file" | ||
) | ||
if not all(re.match("-? *srv/flask/app", p) for p in install_prime): | ||
raise ExtensionError( | ||
"flask extension required prime entry in the flask/install-app part" | ||
"to start with srv/flask/app" | ||
) | ||
|
||
# Users are required to compile any static assets prior to executing the | ||
# rockcraft pack command, so assets can be included in the final OCI image. | ||
install_app_part = { | ||
"plugin": "dump", | ||
"source": ".", | ||
"organize": renaming_map, | ||
"stage": list(renaming_map.values()), | ||
} | ||
dependencies_part = { | ||
"plugin": "python", | ||
"stage-packages": ["python3-venv"], | ||
"source": ".", | ||
"python-packages": ["gunicorn"], | ||
"python-requirements": ["requirements.txt"], | ||
} | ||
snippet = { | ||
dependencies_part_name: self._merge_existing_part( | ||
dependencies_part_name, dependencies_part | ||
), | ||
install_app_part_name: self._merge_existing_part( | ||
install_app_part_name, install_app_part | ||
), | ||
} | ||
if self.yaml_data["base"] == "bare": | ||
snippet["flask/container-processing"] = { | ||
"plugin": "nil", | ||
"source": ".", | ||
"override-build": "mkdir -m 777 ${CRAFT_PART_INSTALL}/tmp", | ||
} | ||
return snippet | ||
|
||
@override | ||
def get_parts_snippet(self) -> Dict[str, Any]: | ||
"""Return the parts to add to parts.""" | ||
return {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
test |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from flask import Flask # pyright: ignore[reportMissingImports] | ||
|
||
app = Flask(__name__) | ||
|
||
|
||
@app.route("/") | ||
def ok(): | ||
return "ok" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
flask |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
name: flask-extension | ||
summary: OCI image for a flask project. | ||
description: OCI image for a flask project. | ||
version: "0.1" | ||
base: bare | ||
license: Apache-2.0 | ||
|
||
extensions: | ||
weiiwang01 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
- flask | ||
|
||
parts: | ||
flask/install-app: | ||
prime: | ||
- -srv/flask/app/README | ||
- -srv/flask/app/node_modules |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
console.log("hello") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
summary: flask extension test | ||
|
||
execute: | | ||
export ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true | ||
|
||
run_rockcraft pack | ||
|
||
test -f flask-extension_0.1_amd64.rock | ||
test ! -d work | ||
|
||
# Ensure docker does not have this container image | ||
docker rmi --force flask-extension | ||
# Install container | ||
sudo /snap/rockcraft/current/bin/skopeo --insecure-policy copy oci-archive:flask-extension_0.1_amd64.rock docker-daemon:flask-extension:latest | ||
# Ensure container exists | ||
docker images flask-extension | MATCH "flask-extension" | ||
|
||
# ensure container doesn't exist | ||
docker rm -f flask-extension-container | ||
|
||
# test the flask project is ready to run inside the container | ||
tigarmo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
docker run --rm --entrypoint /bin/python3 flask-extension -m gunicorn --chdir /srv/flask/app --check-config app:app | ||
docker run --rm --entrypoint /bin/python3 flask-extension -c "import pathlib;assert pathlib.Path('/srv/flask/app/static/js/test.js').is_file()" | ||
docker run --rm --entrypoint /bin/python3 flask-extension -c "import pathlib;assert not pathlib.Path('/srv/flask/app/node_modules').exists()" | ||
|
||
# test the part merging | ||
docker run --rm --entrypoint /bin/python3 flask-extension -c "import pathlib;assert not pathlib.Path('/srv/flask/app/README').exists()" | ||
|
||
# test the default flask service | ||
docker run --rm --name flask-extension-container -d -p 8137:8000 flask-extension | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should remove this container in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added, thanks! |
||
retry -n 5 --wait 2 curl localhost:8137 | ||
[ "$(curl -sSf localhost:8137)" == "ok" ] | ||
|
||
restore: | | ||
rm -f flask-extension_0.1_amd64.rock | ||
docker rmi -f flask-extension |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check that this meets all the requirements, e.g., we have made the
prime
required nowThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Documents has been updated, thanks!