-
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
Merged
Merged
Changes from all commits
Commits
Show all changes
42 commits
Select commit
Hold shift + click to select a range
3f3a222
Add flask framework extension
weiiwang01 caa7dbf
Update documents in the flask extension
weiiwang01 128c58d
Fix some linting issues
weiiwang01 016b2b1
Fix some linting issues
weiiwang01 10ec7d4
Update the name of the extension
weiiwang01 8f622fa
Some document and naming changes
weiiwang01 d5a048d
Add a spread test for the flask extension
weiiwang01 dde4b33
raise an error when requirements.txt doesn't exist
weiiwang01 f40b7dd
Fix the linting issue
weiiwang01 2364f45
Merge branch 'main' into main
weiiwang01 ce8c2b5
Merge branch 'main' into main
weiiwang01 2f4e560
Add more items to the flask extension spread test
weiiwang01 d6926e3
Merge branch 'main' into main
sergiusens 21d7dfb
Add support for 20.04 in flask extension
weiiwang01 2bc660d
Merge branch 'main' into main
sergiusens 310408f
Add extensions section in the rockcraft.yaml doc
weiiwang01 6f1a627
Update document and part merging
weiiwang01 8c25807
Only allow to overwrite prime in flask/install-app
weiiwang01 1e8f860
Update part merging and documents
weiiwang01 2830f97
Use root snippet to modify parts
weiiwang01 d5890cd
Fix the spread test for the flask extension
weiiwang01 efbfa3b
Add services in flask extension and fix documents
weiiwang01 781eab6
Add tmp in bare containers and fix docs
weiiwang01 97aa7e2
Fix some linting problems
weiiwang01 b31ba69
Merge branch 'main' into main
weiiwang01 75049de
Change to British English and update wordlist
weiiwang01 10ec23d
Add some unit tests for flask extension
weiiwang01 3212eae
Fix the flask extension on windows
weiiwang01 0fe5723
Merge branch 'main' into main
weiiwang01 ce2ce3e
Apply suggestions from code review
weiiwang01 6e3f682
Apply suggestions from code review
weiiwang01 463e298
Update documents
weiiwang01 cab47e8
Mandate the prime in flask/install-app
weiiwang01 b89ceb7
Add flask extension service overwrite test
weiiwang01 bf684a3
Fix linting and spread
weiiwang01 ce9e067
Fix the shellcheck linting problem
weiiwang01 3841e78
Apply suggestions from reviews
weiiwang01 2340cb6
Update the flask extension design
weiiwang01 5487cb5
Fix some linting issues
weiiwang01 1df6a35
Update test_flask_framework.py
weiiwang01 5d3a099
Update flask_framework.py
weiiwang01 c51286e
Remove working-dir from flask service
weiiwang01 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,7 @@ fs | |
GID | ||
github | ||
GPG | ||
Gunicorn | ||
https | ||
init | ||
interoperable | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
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: | ||
- -flask/app/.git | ||
- -flask/app/.venv | ||
- -flask/app/.yarn | ||
- -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
|
||
The extension places the files from the project folder in the ``/flask/app`` | ||
directory in the final image - therefore, all inclusions and exclusions must | ||
be prefixed with ``flask/app``. | ||
|
||
For example, to include only select files: | ||
|
||
.. code-block:: yaml | ||
|
||
flask/install-app: | ||
prime: | ||
- flask/app/static | ||
- flask/app/.env | ||
- flask/app/webapp | ||
- 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: | ||
- -flask/app/.git | ||
- -flask/app/.venv | ||
- -flask/app/.yarn | ||
- -flask/app/node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``/flask/app`` within the rock image. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,216 @@ | ||
# -*- 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 ast | ||
import copy | ||
import fnmatch | ||
import posixpath | ||
import re | ||
from typing import Any, Dict, Optional, Tuple | ||
|
||
from overrides import override | ||
|
||
from ..errors import ExtensionError | ||
from ._utils import _apply_extension_property | ||
from .extension import Extension | ||
|
||
|
||
class FlaskFramework(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 _check_wsgi_path(self): | ||
"""Ensure the flask application can be run with the WSGI path app:app.""" | ||
app_file = self.project_root / "app.py" | ||
if not app_file.exists(): | ||
raise ExtensionError( | ||
"flask application can not be imported from app:app, " | ||
"no app.py file found in the project root" | ||
) | ||
tree = ast.parse(app_file.read_text(encoding="utf-8")) | ||
for node in ast.iter_child_nodes(tree): | ||
if isinstance(node, ast.Assign): | ||
for target in node.targets: | ||
if isinstance(target, ast.Name) and target.id == "app": | ||
return | ||
if isinstance(node, ast.ImportFrom): | ||
for name in node.names: | ||
if (name.asname is not None and name.asname == "app") or ( | ||
name.asname is None and name.name == "app" | ||
): | ||
return | ||
raise ExtensionError( | ||
"flask application can not be imported from app:app, " | ||
"no variable named app in app.py" | ||
) | ||
|
||
def _gen_services(self): | ||
"""Return the services snipped to be applied to the rockcraft file.""" | ||
self._check_wsgi_path() | ||
services = { | ||
"flask": { | ||
"override": "replace", | ||
"startup": "enabled", | ||
"command": "/bin/python3 -m gunicorn --bind 0.0.0.0:8000 --chdir /flask/app app:app", | ||
"user": "_daemon_", | ||
} | ||
} | ||
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())] | ||
renaming_map = { | ||
f: posixpath.join("flask/app", f) | ||
for f in source_files | ||
if not any( | ||
fnmatch.fnmatch(f, p) | ||
for p in ("node_modules", ".git", ".yarn", "*.rock") | ||
) | ||
} | ||
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("-? *flask/app", p) for p in install_prime): | ||
raise ExtensionError( | ||
"flask extension required prime entry in the flask/install-app part" | ||
"to start with 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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
flask |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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-framework | ||
|
||
parts: | ||
flask/install-app: | ||
prime: | ||
- -flask/app/README | ||
- -flask/app/node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
console.log("hello") |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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!