Skip to content
Open
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
63 changes: 63 additions & 0 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Packs the rock and the charm
#
# Only the packing jobs are included for now. The deploy jobs (publish the rock
# to a registry and `juju deploy`/`refresh` the charm) will be added in a
# follow-up PR once PS7 resources are provisioned by IS.
#
# NOTE: when deploy jobs are added here, gate them to the default branch
# (e.g. `if: github.ref == 'refs/heads/main'`) so they never run from forks/PRs.

name: Pack

on:
pull_request:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note: remove before merging

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I will remove it after the PR is approved before merging

push:
branches:
- main
workflow_dispatch:

jobs:
pack-rock:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up LXD
uses: canonical/setup-lxd@main

- name: Set up Rockcraft
run: sudo snap install rockcraft --classic --channel=latest/stable

- name: Pack rock
run: rockcraft pack -v

- name: Upload rock artifact
uses: actions/upload-artifact@v4
with:
name: products-api-rock
path: "*.rock"
if-no-files-found: error

pack-charm:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up LXD
uses: canonical/setup-lxd@main

- name: Set up Charmcraft
run: sudo snap install charmcraft --classic --channel=latest/stable

- name: Pack charm
working-directory: charm
run: charmcraft pack -v

- name: Upload charm artifact
uses: actions/upload-artifact@v4
with:
name: products-api-charm
path: "charm/*.charm"
if-no-files-found: error
12 changes: 12 additions & 0 deletions charm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
venv/
build/
*.charm
.tox/
.coverage
__pycache__/
*.py[cod]
.idea
.vscode/

# Charm libraries are fetched by `charmcraft pack` (flask-framework extension)
lib/
26 changes: 26 additions & 0 deletions charm/charmcraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: products-api

type: charm

base: ubuntu@24.04

platforms:
amd64:

# (Required)
summary: REST API for Canonical product, deployment and version metadata

# (Required)
description: |
products-api is a Flask REST API that stores and serves Canonical product,
deployment, and version metadata, backed by PostgreSQL.

extensions:
- flask-framework

# products-api requires a PostgreSQL database; block until it is integrated.
requires:
postgresql:
interface: postgresql_client
optional: false
limit: 1
41 changes: 41 additions & 0 deletions charm/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Testing tools configuration
[tool.coverage.run]
branch = true

[tool.coverage.report]
show_missing = true

[tool.pytest.ini_options]
minversion = "6.0"
log_cli_level = "INFO"

# Linting tools configuration
[tool.ruff]
line-length = 99
lint.select = ["E", "W", "F", "C", "N", "D", "I001"]
lint.ignore = [
"D105",
"D107",
"D203",
"D204",
"D213",
"D215",
"D400",
"D404",
"D406",
"D407",
"D408",
"D409",
"D413",
]
extend-exclude = ["__pycache__", "*.egg_info"]
lint.per-file-ignores = {"tests/*" = ["D100","D101","D102","D103","D104"]}

[tool.ruff.lint.mccabe]
max-complexity = 10

[tool.codespell]
skip = "build,lib,venv,icon.svg,.tox,.git,.mypy_cache,.ruff_cache,.coverage"

[tool.pyright]
include = ["src/**.py"]
Comment on lines +40 to +41
2 changes: 2 additions & 0 deletions charm/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ops ~= 2.17
paas-charm>=1.0,<2
30 changes: 30 additions & 0 deletions charm/src/charm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
# Copyright 2026 atakan.saglam@canonical.com
# See LICENSE file for licensing details.

"""Flask Charm entrypoint."""

import logging
import typing

import ops

import paas_charm.flask

logger = logging.getLogger(__name__)


class ProductsApiCharm(paas_charm.flask.Charm):
"""Flask 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(ProductsApiCharm)
80 changes: 80 additions & 0 deletions charm/tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Copyright 2026 atakan.saglam@canonical.com
# See LICENSE file for licensing details.

[tox]
no_package = True
skip_missing_interpreters = True
env_list = format, lint, static
min_version = 4.0.0

[vars]
src_path = {tox_root}/src
;tests_path = {tox_root}/tests
all_path = {[vars]src_path}
Comment on lines +10 to +13

[testenv]
set_env =
PYTHONPATH = {tox_root}/lib:{[vars]src_path}
PYTHONBREAKPOINT=pdb.set_trace
PY_COLORS=1
pass_env =
PYTHONPATH
CHARM_BUILD_DIR
MODEL_SETTINGS

[testenv:format]
description = Apply coding style standards to code
deps =
ruff
commands =
ruff format {[vars]all_path}
ruff check --fix {[vars]all_path}

[testenv:lint]
description = Check code against coding style standards
deps =
ruff
codespell
commands =
codespell {tox_root}
ruff check {[vars]all_path}
ruff format --check --diff {[vars]all_path}

[testenv:unit]
description = Run unit tests
deps =
pytest
coverage[toml]
-r {tox_root}/requirements.txt
commands =
coverage run --source={[vars]src_path} \
-m pytest \
--tb native \
-v \
-s \
{posargs} \
{[vars]tests_path}/unit
coverage report

[testenv:static]
description = Run static type checks
deps =
pyright
-r {tox_root}/requirements.txt
commands =
pyright {posargs}

[testenv:integration]
description = Run integration tests
deps =
pytest
juju
pytest-operator
-r {tox_root}/requirements.txt
commands =
pytest -v \
-s \
--tb native \
--log-cli-level=INFO \
{posargs} \
{[vars]tests_path}/integration
14 changes: 14 additions & 0 deletions migrate.sh
Comment thread
britneywwc marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash

set -e

# Database migration hook for the charm (paas-charm) deployment.
#
# Under the charm, ./entrypoint is not used: paas-charm launches gunicorn
# directly and runs this script once, on a single unit, before the web service
# starts. It is the only place schema migrations happen in that deployment.
#
# `flask db upgrade` is idempotent, so it is safe across restarts and multiple
# units. The DB URL is resolved by webapp.app (DATABASE_URL or the charm's
# POSTGRESQL_DB_CONNECT_STRING).
python3 -m flask --app webapp.app db upgrade
10 changes: 6 additions & 4 deletions migrations/env.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from logging.config import fileConfig
import os
import sys
from sqlalchemy import engine_from_config
from sqlalchemy import pool
Expand All @@ -23,10 +22,13 @@
# Logging setup is non-critical so we skip it rather than block migrations.
pass

# Set SQLAlchemy URL from Flask config or environment
db_url = os.environ.get("DATABASE_URL", app.config.get("SQLALCHEMY_DATABASE_URI"))
# Resolve the database URL from the Flask app config. webapp/app.py already
# bridges DATABASE_URL (local/dotrun) and the charm-injected
# POSTGRESQL_DB_CONNECT_STRING, so we reuse that single source here instead of
# reading os.environ directly.
db_url = app.config.get("SQLALCHEMY_DATABASE_URI")
if not db_url:
raise RuntimeError("DATABASE_URL must be set for migrations.")
raise RuntimeError("No database URL configured for migrations.")
config.set_main_option("sqlalchemy.url", db_url)

# add your model's MetaData object here
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ psycopg2-binary>=2.9.9
SQLAlchemy==2.0.23
SQLAlchemy-Utils>=0.41.2
Talisker>=0.16.0
gunicorn>=20.0.4
gunicorn[gevent]>=20.0.4
setuptools
25 changes: 25 additions & 0 deletions rockcraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: products-api
base: bare
build-base: ubuntu@24.04
version: "0.1" # just for humans. Semantic versioning is recommended
summary: REST API for Canonical product, deployment and version metadata
description: |
products-api is a Flask REST API that stores and serves Canonical product,
deployment, and version metadata, backed by PostgreSQL.
platforms:
amd64:

extensions:
- flask-framework

parts:
flask-framework/install-app:
prime:
# A Flask prime override fully replaces the default file list. Application
# code is under webapp/ (not app/), so it must be listed explicitly, along
# with the migration entrypoint and Alembic assets.
- flask/app/app.py
- flask/app/webapp
- flask/app/migrations
- flask/app/migrate.sh
- flask/app/alembic.ini
7 changes: 6 additions & 1 deletion webapp/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
app.json.sort_keys = False

# Database Configuration
app.config["SQLALCHEMY_DATABASE_URI"] = get_flask_env("DATABASE_URL")
# When deployed via the 12-factor charm, paas-charm's PostgreSQL integration
# injects POSTGRESQL_DB_CONNECT_STRING. Fall back to it so the same code runs
# unchanged under local/dotrun (DATABASE_URL) and the charm runtime.
app.config["SQLALCHEMY_DATABASE_URI"] = get_flask_env(
"DATABASE_URL"
) or get_flask_env("POSTGRESQL_DB_CONNECT_STRING")
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

# API Documentation
Expand Down
Loading