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

extensions: new flask extension #317

Merged
merged 42 commits into from
Sep 28, 2023
Merged

Conversation

weiiwang01
Copy link
Contributor

@weiiwang01 weiiwang01 commented Jul 5, 2023

This pull request introduces an experimental Flask extension for the Juju Platform as a Service ecosystem in rockcraft.
The extension aims to facilitate the deployment and management of Flask-based web applications on Juju PaaS.

Here's an example project that uses the flask-extension https://github.com/weiiwang01/dqlite.io/blob/flask-extension/rockcraft.yaml.

  • [] Have you signed the CLA?

Comment on lines 98 to 102
"paas-flask/install-app": {
"plugin": "dump",
"source": ".",
"organize": renaming_map,
"stage": list(renaming_map.values()),
Copy link
Collaborator

Choose a reason for hiding this comment

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

how is this supposed to work? Were we not supposed to use the npm target for this?

We should add some minimal documentation on how to work with this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This approach aligns with the web team's requirements, as they prefer to construct the static assets themselves before executing the rockcraft pack command. And let rockcraft should incorporate the built artifact into the OCI image without making any modifications. I will add some comments about this.

Copy link
Contributor

Choose a reason for hiding this comment

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

For flask, developers don't use npm for static assets since flask is a server side rendered framework primarily. Hence we decided not to add support for npm based static assets in the flask extension. For others it could make sense or those assets should be served, e.g., using a content cache instead or dedicated 12-factor extension

@sergiusens
Copy link
Collaborator

sergiusens commented Jul 14, 2023 via email

@weiiwang01
Copy link
Contributor Author

That'a perfecly reasonable explanation. I would still like to see a spread test for this and perhaps some documentation. Documentation can be a separate PR, but a spread test show casing this seems like a must.

Spread test added, thanks!

@sergiusens sergiusens changed the title Add experimental flask extension support for juju paas extensions: new flask extension Aug 10, 2023
@@ -0,0 +1,58 @@
The Flask Extension
Copy link
Collaborator

Choose a reason for hiding this comment

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

this looks like a good start, it does however mixup reference, explanation, tutorial and how-to

ref: https://diataxis.fr/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gotcha, I have splited the document into a "how-to" and a "reference".
I also added the services field to the extension as well following last week's discussion.

@weiiwang01
Copy link
Contributor Author

@tigarmo I have updated the pull request with some unit tests for the flask extension.

Copy link
Collaborator

@tigarmo tigarmo left a comment

Choose a reason for hiding this comment

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

Thanks! I haven't finished reviewing yet, will pick this up again in a little bit

docs/how-to/index.rst Outdated Show resolved Hide resolved
docs/how-to/use-flask-extension.rst Outdated Show resolved Hide resolved
docs/how-to/use-flask-extension.rst Outdated Show resolved Hide resolved
docs/how-to/use-flask-extension.rst Outdated Show resolved Hide resolved

.. code-block:: yaml

name: example
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we can improve the fields a bit, even if they are just an example:

name: example-flask
summary: A Flask application
description: A ROCK packing a Flask application via the flask extension

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, indeed, updated, thanks!

@@ -0,0 +1,16 @@
Extensions
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍 ty for starting this page

docs/reference/rockcraft.yaml.rst Outdated Show resolved Hide resolved
@@ -26,3 +26,7 @@
"register",
"unregister",
]

from .flask import Flask as _Flask
Copy link
Collaborator

Choose a reason for hiding this comment

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

why the rename?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am not exactly sure why, but indeed it's unnecessary, updated, thanks!

snippet["flask/container-processing"] = {
"plugin": "nil",
"source": ".",
"override-prime": "craftctl default\nmkdir -p tmp\nchmod 777 tmp",
Copy link
Collaborator

Choose a reason for hiding this comment

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

a bit surprised that this works, I guess craftctl default is changing the current dir to CRAFT_PRIME?
regardless, I think it's cleaner/more idiomatic to do something like "override-build": "mkdir -m 777 ${CRAFT_PART_INSTALL}/tmp"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's more cleaner, thanks, updated!


@override
def get_parts_snippet(self) -> Dict[str, Any]:
"""Create necessary parts to facilitate the flask application."""
Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess this docstring is redundant/misleading

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I forgot to updated the docstring, updated, thanks!

Copy link
Collaborator

@tigarmo tigarmo left a comment

Choose a reason for hiding this comment

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

Sorry for the delay, now I'm done!

"working-dir": "/srv/flask/app",
}
}
existing_services = self.yaml_data.get("services", {})
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think for consistency with the part handling this should also be a deepcopy of the existing data

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added, perhaps we should define the yaml_data as a property method. This way, we can prevent such issues at the framework level.

@property
def yaml_data(self) -> Dict[str, Any]:
    return self._yaml_data

"srv/flask/app/requirements.txt",
"srv/flask/app/static",
]
del parts["flask/install-app"]["stage"]
Copy link
Collaborator

Choose a reason for hiding this comment

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

an alternative to sorting + deleting it here, which I assume you did because of the unpredictable ordering, is to have the extension always sort the list of stage files. Then it's more deterministic which is a plus

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea, updated, thanks!


input_yaml["extensions"] = ["flask"]
input_yaml["parts"] = {"flask/install-app": {"prime": ["-srv/flask/app/foobar"]}}
input_yaml["services"] = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

possibly add another service here, to test that this new service is preserved

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added, thanks!

- flask/install-app: copy the flask project into the OCI image
"""
if not (self.project_root / "requirements.txt").exists():
raise ValueError(
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think errors.ExtensionError is more specific here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed, updated, thanks!

@@ -134,3 +134,102 @@ def test_project_load_extensions(fake_extensions, tmp_path):

# New part
assert parts[f"{FullExtension.NAME}/new-part"] == {"plugin": "nil", "source": None}


def test_flask_extensions(fake_extensions, tmp_path, input_yaml, monkeypatch):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for the tests! I think a couple are missing:

  • A test that checks that apply_extensions() fails if requirements.txt is missing
  • A test that checks that build_base: ubuntu:22.04 gets added if the key is missing and base is bare
  • Since the extension has a custom merge for the parts, a test that merges existing keys. For example: a yaml that has:
parts:
    flask/dependencies:
        python-requirements: ["requirements-jammy.txt"]
    

Copy link
Collaborator

Choose a reason for hiding this comment

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

In light of this, I think all of these tests (and these new ones) should be moved to a test_flask module in unit/extensions

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gotcha, I have included more tests and move they to the test_flask.py, thanks!


Example:

.. code-block:: yaml
Copy link
Contributor

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 now

Copy link
Contributor Author

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!

import copy
import os
import posixpath
from typing import Any, Dict, Optional, Tuple
Copy link
Contributor

Choose a reason for hiding this comment

The 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 dict, tuple etc

Copy link
Contributor Author

Choose a reason for hiding this comment

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

rockcraft is targeting python>=3.8

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 typing, so I suggest we keep this for consistency and then in a separate issue we can convert all code

ignores = [".git", "node_modules", ".yarn"]
source_files = [
f
for f in os.listdir(self.project_root)
Copy link
Contributor

Choose a reason for hiding this comment

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

what about using pathlib?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated, thanks!

for f in os.listdir(self.project_root)
if f not in ignores and not f.endswith(".rock")
]
renaming_map = {f: posixpath.join("srv/flask/app", f) for f in source_files}
Copy link
Contributor

Choose a reason for hiding this comment

The 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 srv/flask/app prefix?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That has been added in the latest commit, thanks!

Copy link
Collaborator

@tigarmo tigarmo left a comment

Choose a reason for hiding this comment

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

thanks for all this work @weiiwang01

import copy
import os
import posixpath
from typing import Any, Dict, Optional, Tuple
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 typing, so I suggest we keep this for consistency and then in a separate issue we can convert all code

@@ -25,6 +25,11 @@ execute: |

# 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
Copy link
Collaborator

Choose a reason for hiding this comment

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

we should remove this container in the restore step
docker rm -f flask-extension-container

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added, thanks!



@pytest.mark.usefixtures("flask_extension")
def test_flask_extension_error(tmp_path):
Copy link
Collaborator

Choose a reason for hiding this comment

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

thanks for testing the error cases! I think we should split this into separate tests. It's a bit of work but it makes each individual test easier to reason about, and lowers the chance that they might interfere with each other in unexpected ways

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gotcha, separate them into different test cases with appropriate names, thanks!

docs/how-to/use-flask-extension.rst Show resolved Hide resolved
@sergiusens sergiusens changed the base branch from main to feature/12f September 20, 2023 11:04
@sergiusens
Copy link
Collaborator

when this is merged, a snap of rockcraft with this implementation will show up as edge/12f (the feature branch leaf name)

"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())]
Copy link
Collaborator

Choose a reason for hiding this comment

The 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

@tigarmo
Copy link
Collaborator

tigarmo commented Sep 28, 2023

well done @weiiwang01 !

@tigarmo tigarmo merged commit e2d7756 into canonical:feature/12f Sep 28, 2023
tigarmo added a commit that referenced this pull request Mar 18, 2024
This commit is the squashed version of the long-lived `feature/12f` branch containing the most recent version of the Flask extension in the context of the 12-factor App methodology. The original commit log is kept here for posterity, and follows:

------

* feat: new flask extension (#317)

Co-authored-by: Sergio Schvezov <[email protected]>
Co-authored-by: Tiago Nobrega <[email protected]>

* feat: provide a default prime in the flask extension (#374)

* feat(extensions): add a profile option to the rockcraft init command (#379)

* chore(deps): update craft-providers to 1.19.2

The current version has two bugs (the "looping timer" one and the "not
collecting error logs" one) that are blocking for many users. Ideally we
should merge main into this branch, this is a stopgap before we can
allocate time for it.

* chore: manual updates to align with origin/main

These are related to the change in base notation (ubuntu@ instead of
ubuntu:) and the changes due to general craft-application-flow.

* fix(docs): remove duplicate how-to entries

* Fix schema validation

* Fix typo

* Template requires input variables

* Fix lint

* Fix lint

* Update flask-framework extension

* Remove support for ubuntu:20.04

* ROCK to rock and update copyright info

* Update flask doc with real examples

* Update use-flask-extension.rst

* Update pyright exclude

* Fix some linting issues

* enable extension in spread tests

* Fix use-flask-extension spread test

* Rename ROCK to rock

* Apply suggestions from code review

Co-authored-by: Alex Lowe <[email protected]>

---------

Co-authored-by: Sergio Schvezov <[email protected]>
Co-authored-by: Tiago Nobrega <[email protected]>
Co-authored-by: Gregory Schiano <[email protected]>
Co-authored-by: Alex Lowe <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants