Skip to content
Merged
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
30 changes: 30 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# The image build uses the repository root as context. Keep the context small:
# the Dockerfile copies only service_catalog/, docs/static/img/ and objects/dc/.
.git
.github
.venv
.env
.mypy_cache
.pytest_cache
.ruff_cache
__pycache__
*.py[cod]
.agents
.specify
dev
checks
generators
transforms
templates
schemas
queries
menus
scripts
tests
docs/**
!docs/static/img/**
objects/**
!objects/dc/**
service_catalog/tests
service_catalog/AGENTS.md
service_catalog/pyrightconfig.json
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ export INFRAHUB_GIT_LOCAL=false
# The service catalog provides a web UI for managing data center deployments
export INFRAHUB_SERVICE_CATALOG=false

# Build the service catalog image from this checkout instead of pulling the
# published one (registry.opsmill.io/opsmill/infrahub-demo-dc-streamlit), and
# mount its sources so local edits are picked up
# Set to "true" to build locally; `uv run invoke start --rebuild` does the same for one run
export INFRAHUB_SERVICE_CATALOG_BUILD=false

# Default branch to use when the service catalog application starts
# This is the branch that will be selected by default in the UI
export DEFAULT_BRANCH=main
Expand Down
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,13 @@ updates:
semver-major-days: 30
ignore:
- dependency-name: "github/gh-aw-actions/**" # managed by gh aw compile

# Base image and uv binary of the published service catalog image.
- package-ecosystem: "docker"
directory: "/service_catalog"
target-branch: main
schedule:
interval: weekly
cooldown:
default-days: 7
semver-major-days: 30
326 changes: 326 additions & 0 deletions .github/workflows/publish-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
---
# yamllint disable rule:truthy rule:line-length
name: Publish images

# Builds the Streamlit Service Catalog image and publishes it to the OpsMill
# Harbor registry (public alias registry.opsmill.io):
#
# registry.opsmill.io/opsmill/infrahub-demo-dc-streamlit
#
# Pull requests build and smoke-test the image without publishing and without
# reading any secret. Pushes to main, the weekly rebuild and manual runs with
# `publish` enabled push linux/amd64 and linux/arm64 images, then sign them and
# attach SBOMs, as the other OpsMill images are.
#
# Needs, in the repository settings: variable HARBOR_HOST and secrets
# HARBOR_USERNAME and HARBOR_PASSWORD (a Harbor robot account allowed to push to
# the `opsmill` project).

on:
pull_request:
paths:
- "service_catalog/**"
- "objects/dc/**"
- "docs/static/img/**"
- "pyproject.toml"
- "uv.lock"
- ".dockerignore"
- ".github/workflows/publish-images.yml"
push:
branches:
- main
paths:
- "service_catalog/**"
- "objects/dc/**"
- "docs/static/img/**"
- "pyproject.toml"
- "uv.lock"
- ".dockerignore"
- ".github/workflows/publish-images.yml"
schedule:
# Weekly, so the base image's security fixes reach the published `main` tag.
- cron: "41 4 * * 1"
workflow_dispatch:
inputs:
publish:
description: "Push the image to Harbor"
type: boolean
required: false
default: false

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# A publish run must finish, so only pull request runs are cancelled.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read

env:
IMAGE_NAME: opsmill/infrahub-demo-dc-streamlit

jobs:
test-build:
name: Build and smoke-test the image
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@v7

- uses: docker/setup-buildx-action@v4

- name: Build the image for linux/amd64
uses: docker/build-push-action@v7
with:
context: .
file: service_catalog/Dockerfile
platforms: linux/amd64
load: true
tags: smoke:streamlit
cache-from: type=gha,scope=streamlit-amd64
cache-to: type=gha,mode=max,scope=streamlit-amd64

- name: Check the application files are in the image
run: |
docker run --rm --entrypoint python smoke:streamlit -c "
import pathlib
import utils.config
assert pathlib.Path('/objects/dc/dc-arista-s.yml').is_file(), 'DC templates missing'
assert pathlib.Path('/app/assets/infrahub-hori.svg').is_file(), 'logos missing'
print('application files ok')
"

- name: Check the container runs as a non-root user
run: |
uid=$(docker run --rm --entrypoint id smoke:streamlit -u)
if [ "${uid}" = "0" ]; then
echo "::error::The image runs as root."
exit 1
fi

- name: Check the container reports healthy
run: |
docker run -d --name smoke \
-e INFRAHUB_ADDRESS=http://localhost:1 \
-e INFRAHUB_API_TOKEN=smoke-test \
-p 8501:8501 \
smoke:streamlit
for _ in $(seq 1 30); do
if curl --fail --silent "http://localhost:8501/_stcore/health"; then
echo
echo "The container is healthy."
exit 0
fi
sleep 2
done
echo "::error::The container did not become healthy within 60 seconds."
docker logs smoke
exit 1

metadata:
name: Compute image tags and labels
needs: test-build
if: ${{ github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || inputs.publish) }}
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
steps:
# `main` follows the default branch. `sha-<commit>` never moves, so a
# deployment pins it. A manual run from another branch gets only the
# `sha-` tag, so it cannot replace `main`.
- name: Resolve tags and labels
id: meta
env:
SHA: ${{ github.sha }}
REF_NAME: ${{ github.ref_name }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
REPOSITORY: ${{ github.repository }}
SERVER_URL: ${{ github.server_url }}
run: |
set -euo pipefail
{
echo "tags<<EOF"
echo "sha-${SHA::7}"
if [ "${REF_NAME}" = "${DEFAULT_BRANCH}" ]; then
echo "main"
fi
echo "EOF"
echo "labels<<EOF"
echo "org.opencontainers.image.source=${SERVER_URL}/${REPOSITORY}"
echo "org.opencontainers.image.revision=${SHA}"
echo "org.opencontainers.image.licenses=MIT"
echo "org.opencontainers.image.vendor=OpsMill"
echo "EOF"
} >> "$GITHUB_OUTPUT"

build:
name: "Build (${{ matrix.platform }})"
needs: metadata
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
strategy:
fail-fast: true
matrix:
include:
- platform: linux/amd64
runner: ubuntu-24.04
arch: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
arch: arm64
steps:
- uses: actions/checkout@v7

- uses: docker/setup-buildx-action@v4

- name: Login to Harbor
uses: docker/login-action@v4
with:
registry: ${{ vars.HARBOR_HOST }}
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_PASSWORD }}

- name: Build and push by digest
id: push
uses: docker/build-push-action@v7
with:
context: .
file: service_catalog/Dockerfile
platforms: ${{ matrix.platform }}
provenance: false # avoids cross-platform "unknown" entries
labels: ${{ needs.metadata.outputs.labels }}
outputs: type=image,name=${{ vars.HARBOR_HOST }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=streamlit-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=streamlit-${{ matrix.arch }}

- name: Export digest
env:
DIGEST: ${{ steps.push.outputs.digest }}
run: |
mkdir -p /tmp/digests
touch "/tmp/digests/${DIGEST#sha256:}"

- name: Upload digest
uses: actions/upload-artifact@v7
with:
name: digests-streamlit-${{ matrix.arch }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1

merge:
name: Publish the multi-architecture manifest
needs: [metadata, build]
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Download digests
uses: actions/download-artifact@v8
with:
path: /tmp/digests
pattern: digests-streamlit-*
merge-multiple: true

- uses: docker/setup-buildx-action@v4

- name: Login to Harbor
uses: docker/login-action@v4
with:
registry: ${{ vars.HARBOR_HOST }}
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_PASSWORD }}

- name: Create the multi-architecture manifest
working-directory: /tmp/digests
env:
IMAGE: ${{ vars.HARBOR_HOST }}/${{ env.IMAGE_NAME }}
TAGS: ${{ needs.metadata.outputs.tags }}
run: |
set -euo pipefail
SOURCES=()
for digest in *; do
SOURCES+=("${IMAGE}@sha256:${digest}")
done
TAG_ARGS=()
while IFS= read -r tag; do
[ -z "${tag}" ] && continue
TAG_ARGS+=(--tag "${IMAGE}:${tag}")
done <<< "${TAGS}"
docker buildx imagetools create "${TAG_ARGS[@]}" "${SOURCES[@]}"

- name: Summarise the published tags
env:
IMAGE: ${{ vars.HARBOR_HOST }}/${{ env.IMAGE_NAME }}
TAGS: ${{ needs.metadata.outputs.tags }}
run: |
set -euo pipefail
FIRST_TAG=$(printf '%s\n' "${TAGS}" | sed -n '1p')
digest=$(docker buildx imagetools inspect "${IMAGE}:${FIRST_TAG}" \
--format '{{json .Manifest}}' | jq -r '.digest')
{
echo "Digest: \`${digest}\`"
echo ""
while IFS= read -r tag; do
[ -z "${tag}" ] && continue
echo "- \`registry.opsmill.io/${IMAGE_NAME}:${tag}\`"
done <<< "${TAGS}"
} >> "$GITHUB_STEP_SUMMARY"

sign:
name: Sign and attach SBOM
needs: [metadata, merge]
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
id-token: write
steps:
- name: Install cosign
uses: sigstore/cosign-installer@v4.1.2

- name: Install Syft
uses: anchore/sbom-action/download-syft@v0.24.2

- name: Login to Harbor
uses: docker/login-action@v4
with:
registry: ${{ vars.HARBOR_HOST }}
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_PASSWORD }}

# The `sha-<commit>` tag never moves, so it resolves to the manifest
# that the previous job pushed.
- name: Resolve the manifest digest
env:
IMAGE: ${{ vars.HARBOR_HOST }}/${{ env.IMAGE_NAME }}
run: |
set -euo pipefail
digest=$(docker buildx imagetools inspect "${IMAGE}:sha-${GITHUB_SHA::7}" \
--format '{{json .Manifest}}' | jq -r '.digest')
echo "REFERENCE=${IMAGE}@${digest}" >> "$GITHUB_ENV"

- name: Sign the manifest
run: |
cosign sign --yes --recursive --new-bundle-format=false --use-signing-config=false \
"${REFERENCE}"

- name: Generate the SBOMs
run: |
syft "${REFERENCE}" --output spdx-json=sbom.spdx.json
syft "${REFERENCE}" --output cyclonedx-json=sbom.cdx.json

- name: Attest the SBOMs
run: |
cosign attest --yes --type spdxjson --predicate sbom.spdx.json "${REFERENCE}"
cosign attest --yes --type cyclonedx --predicate sbom.cdx.json "${REFERENCE}"

- name: Upload the SBOMs
uses: actions/upload-artifact@v7
with:
name: sbom-streamlit-${{ github.sha }}
path: |
sbom.spdx.json
sbom.cdx.json
retention-days: 90
Loading
Loading