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
56 changes: 56 additions & 0 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Pack and Deploy

on:
push:
branches:
- main
- staging
workflow_dispatch:
inputs:
environment:
description: 'Environment (Production or Staging)'
required: true
type: choice
options:
- Production
- Staging
jobs:
setup:
runs-on: ubuntu-latest
permissions:
contents: read
environment: ${{ github.event.inputs.environment != '' && github.event.inputs.environment || (github.ref == 'refs/heads/main' && 'Production' || 'Staging') }}
outputs:
charm_name: ${{ steps.setup-vars.outputs.charm_name }}
channel: ${{ steps.setup-vars.outputs.channel }}
juju_controller_name: ${{ steps.setup-vars.outputs.juju_controller_name }}
juju_model_name: ${{ steps.setup-vars.outputs.juju_model_name }}
environment: ${{ steps.setup-vars.outputs.environment }}
steps:
- name: setup vars
id: setup-vars
run: |
echo "charm_name=${{ vars.CHARM_NAME }}" >> $GITHUB_OUTPUT
echo "channel=${{ vars.CHANNEL }}" >> $GITHUB_OUTPUT
echo "juju_controller_name=${{ vars.JUJU_CONTROLLER_NAME }}" >> $GITHUB_OUTPUT
echo "juju_model_name=${{ vars.JUJU_MODEL_NAME }}" >> $GITHUB_OUTPUT
echo "environment=${{ github.event.inputs.environment != '' && github.event.inputs.environment || (github.ref == 'refs/heads/main' && 'Production' || 'Staging') }}" >> $GITHUB_OUTPUT

deploy:
needs: setup
name: Deploy
uses: canonical/webteam-devops/.github/workflows/deploy.yaml@main
permissions:
contents: read
deployments: write
packages: write
with:
environment: ${{ needs.setup.outputs.environment }}
charm_name: ${{ needs.setup.outputs.charm_name }}
channel: ${{ needs.setup.outputs.channel }}
juju_controller_name: ${{ needs.setup.outputs.juju_controller_name }}
juju_model_name: ${{ needs.setup.outputs.juju_model_name }}
secrets:
VAULT_APPROLE_ROLE_ID: ${{ secrets.VAULT_APPROLE_ROLE_ID }}
VAULT_APPROLE_SECRET_ID: ${{ secrets.VAULT_APPROLE_SECRET_ID }}
CHARMHUB_TOKEN: ${{ secrets.CHARMHUB_TOKEN }}
16 changes: 16 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@ jobs:
docker run --network="host" -v .:/app cypress/base:22.18.0 \
bash "-c" "cd /app && npx cypress install && yarn run test-e2e"

pack-rock:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4

- name: Setup LXD
uses: canonical/setup-lxd@main

- name: Setup rockcraft
run: sudo snap install rockcraft --classic

- name: Pack rock
run: rockcraft pack

lint-python:
runs-on: ubuntu-latest
permissions:
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,7 @@ coverage/
.webcache_blog/
.coverage
cypress/screenshots/

# Charming artifacts
*.charm
*.rock
15 changes: 15 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# This file serves as an entry point for the rock image. It is required by the PaaS app charmer.
# The flask application must be defined in this file under the variable name `app`.
# See - https://documentation.ubuntu.com/rockcraft/en/latest/reference/extensions/flask-framework/
import os
import logging

# canonicalwebteam.flask-base requires SECRET_KEY to be set, this must be done before importing the app
os.environ["SECRET_KEY"] = os.environ["FLASK_SECRET_KEY"]

# disable talisker logger, as it is not used in this application and clutters logs
logging.getLogger("talisker.context").disabled = True

from webapp.app import create_app

app = create_app()
10 changes: 10 additions & 0 deletions charm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
venv/
build/
*.charm
.tox/
.coverage
__pycache__/
*.py[cod]
.idea
.vscode/
lib/
55 changes: 55 additions & 0 deletions charm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# The Charm for the snapcraft.io website

This charm was created using the [PaaS App Charmer](https://canonical-12-factor-app-support.readthedocs-hosted.com/latest/)

## Local development

To work on this charm locally, you first need to set up an environment, follow [this section](https://juju.is/docs/sdk/write-your-first-kubernetes-charm-for-a-flask-app#heading--set-things-up) of the tutorial.

Then, you can run the following command to pack and upload the rock:

```bash
rockcraft pack
rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false oci-archive:snapcraft-io*.rock docker://localhost:32000/snapcraft-io:1
```

This will pack the application into a [rock](https://documentation.ubuntu.com/rockcraft/en/latest/explanation/rocks/) (OCI image) and upload it to the local registry.

You can deploy the charm locally with:

```bash
cd charm
charmcraft fetch-libs
charmcraft pack
juju deploy ./*.charm --resource flask-app-image=localhost:32000/snapcraft-io:1
```

This will deploy the charm with the rock image you just uploaded attached as a resource.

once `juju status` reports the charm as `active`, you can test the webserver:

```bash
curl {IP_OF_SNAPCRAFT_IO_UNIT}:8000
```

to connect using a browser, the easiest way is to integrate with `nginx-ingress-integrator`:

```bash
juju deploy nginx-ingress-integrator --trust
juju config nginx-ingress-integrator service-hostname=snapcraft.local path-routes=/
juju integrate nginx-ingress-integrator snapcraft-io
```

You can then add `snapcraft.local` to your `/etc/hosts` file with the IP of the multipass vm:

```bash
multipass ls # Get the IP of the VM
echo "{IP_OF_VM} snapcraft.local" | sudo tee -a /etc/hosts
```

> Note: login will not work using this setup, if you'd like to access publisher pages, change the domain to `staging.snapcraft.io`, but make sure to remove the line from `/etc/hosts/` after you're done.


## Design Decisions:
- To keep the codebase clean and charm libraries updated, they are only fetched before packing the charm in the [Github Actions workflow](https://github.com/canonical/snapcraft.io/blob/main/.github/workflows/publish_charm.yaml#L25).
- As all our work is open source, the charm is publicly available on [snapcraft](https://charmhub.io/snapcraft-io), the rock image is also included as a resource. This significantly simplifies deployment.
110 changes: 110 additions & 0 deletions charm/charmcraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: snapcraft-io

type: charm

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

summary: The charm for the snapcraft.io website

description: The charm for the snapcraft.io website, built with the PaaS app charmer

extensions:
- flask-framework

config:
options:
sentry-dsn:
description: "Sentry Data Source Name for the project"
type: string

environment:
description: ""
default: "production"
type: string

marketo-client-id:
description: "Marketo API client ID"
type: string

marketo-client-secret:
description: "Marketo API client secret"
type: string

github-client-id:
description: "GitHub OAuth application ID for prompting users for access to their repositories"
type: string

github-client-secret:
description: "GitHub OAuth application client secret for prompting users for access to their repositories"
type: string

github-snapcraft-user-token:
description: "GitHub application token for automated builds"
type: string

github-snapcraft-bot-user-token:
description: "GitHub application token for CVE data"
type: string

github-webhook-secret:
description: "Secret salt used for signing automated build webhooks"
type: string

github-webhook-host-url:
description: "URL of the automated build webhooks' host"
type: string

lp-api-username:
description: "Launchpad API username"
type: string

lp-api-token:
description: "Launchpad API token"
type: string

lp-api-token-secret:
description: "Launchpad API secret"
type: string

youtube-api-key:
description: ""
type: string

discourse-api-key:
description: ""
type: string

discourse-api-username:
description: ""
type: string

dns-verification-salt:
description: ""
type: string

login-url:
description: "Base URL for SSO login redirects"
default: "https://login.ubuntu.com"
type: string

bsi-url:
description: ""
default: "https://build.snapcraft.io"
type: string

snapstore-dashboard-api-url:
description: "Base URL for SCA backend"
default: "https://dashboard.snapcraft.io/"
type: string

# requires:
# tracing:
# interface: tracing
# optional: true
# limit: 1
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
29 changes: 29 additions & 0 deletions charm/src/charm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env python3

"""Flask Charm entrypoint."""

import logging
import typing

import ops

import paas_charm.flask

logger = logging.getLogger(__name__)


class SnapcraftCharm(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(SnapcraftCharm)

5 changes: 0 additions & 5 deletions konf/site.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,6 @@ env: &env
key: marketo_client_secret
name: snapcraft-io

- name: SEARCH_API_KEY
secretKeyRef:
key: google-custom-search-key
name: google-api

- name: GITHUB_CLIENT_ID
secretKeyRef:
key: github-client-id
Expand Down
5 changes: 0 additions & 5 deletions konf/staging-api.snapcraft.io.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,6 @@ env:
key: marketo_client_secret
name: snapcraft-io

- name: SEARCH_API_KEY
secretKeyRef:
key: google-custom-search-key
name: google-api

- name: LP_API_USERNAME
secretKeyRef:
key: lp-api-username
Expand Down
43 changes: 43 additions & 0 deletions rockcraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: snapcraft-io
base: bare
build-base: ubuntu@22.04
version: "0.1"
summary: Rocked snapcraft.io
description: |
This is the rockcraft for the snapcraft.io website.
platforms:
amd64:
arm64:

extensions:
- flask-framework

parts:
build-ui:
plugin: nil
source: .
source-type: local
build-snaps:
- node/22/stable
override-build: |
set -eux
# install dependencies
npm install -g yarn
yarn install
# build the UI
yarn run build
mkdir -p "$CRAFT_PART_INSTALL/flask/app"
cp -r static "$CRAFT_PART_INSTALL/flask/app/"
flask-framework/install-app:
after:
- build-ui
prime:
- flask/app/.env
- flask/app/app.py
- flask/app/webapp
- flask/app/templates
# - flask/app/static # it already gets copied in the build-ui step
- flask/app/deleted.yaml
- flask/app/redirects.yaml
- flask/app/security.txt
- flask/app/robots.txt
3 changes: 3 additions & 0 deletions security.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Contact: mailto:security@ubuntu.com
Expires: 2025-08-01T00:00:00.000Z
Preferred-Languages: en
5 changes: 0 additions & 5 deletions webapp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,4 @@ class ConfigurationError(Exception):

CONTENT_DIRECTORY = {"PUBLISHER_PAGES": "store/content/publishers/"}

# Docs search
SEARCH_API_KEY = os.getenv("SEARCH_API_KEY")
SEARCH_API_URL = "https://www.googleapis.com/customsearch/v1"
SEARCH_CUSTOM_ID = "009048213575199080868:i3zoqdwqk8o"

APP_NAME = "snapcraft"