From 886bd768dc255f847389d1d1c192c4fffe2ad871 Mon Sep 17 00:00:00 2001 From: Tom Gundersen Date: Fri, 21 May 2021 14:10:32 +0100 Subject: [PATCH] devel: add full-stack development environment This integrates all the Image Builder components needed by the cloud.redhat.com frontend and allows them to be developed and run locally using `docker compose`. This should make it simple to make patches across the different components and develop them in tandem. Thanks to Achilleas Koutsou for the initial idea and implementation in osbuild-composer. Signed-off-by: Tom Gundersen --- README.md | 58 +------- devel/.env | 5 + devel/.gitignore | 1 + devel/README.md | 45 +++++++ devel/config/composer/osbuild-composer.toml | 7 + devel/config/spandx/local-frontend-and-api.js | 127 ++++++++++++++++++ devel/config/x509/openssl.cnf | 85 ++++++++++++ devel/docker-compose.yml | 115 ++++++++++++++++ devel/gen-certs.sh | 97 +++++++++++++ devel/setup.sh | 6 + 10 files changed, 490 insertions(+), 56 deletions(-) create mode 100644 devel/.env create mode 100644 devel/.gitignore create mode 100644 devel/README.md create mode 100644 devel/config/composer/osbuild-composer.toml create mode 100644 devel/config/spandx/local-frontend-and-api.js create mode 100644 devel/config/x509/openssl.cnf create mode 100644 devel/docker-compose.yml create mode 100755 devel/gen-certs.sh create mode 100755 devel/setup.sh diff --git a/README.md b/README.md index 7729d0ec..9d67ac07 100644 --- a/README.md +++ b/README.md @@ -31,62 +31,8 @@ against the chrome and backend at cloud.redhat.com. The UI should be running on https://prod.foo.redhat.com:1337/apps/image-builder/landing. - - ## Backend Development To develop both the frontend and the backend you can again use the proxy to run both the -frontend and backend locally against the chrome at cloud.redhat.com. In addition to the -above: - -1. Clone the image-builder (backend) repository: https://github.com/osbuild/image-builder - -2. Setting up the proxy - - As before, choose a runner (podman or docker), and point the SPANDX_CONFIG variable to - `profile/local-frontend-and-api-with-identity.js` included in - image-builder-frontend. - - ``` - sudo insights-proxy/scripts/patch-etc-hosts.sh - export RUNNER="podman" - export SPANDX_CONFIG=$PATH_TO/image-builder-frontend/profiles/local-frontend-and-api-with-identity.js - sudo -E insights-proxy/scripts/run.sh - ``` - -3. Setting up osbuild-composer(-api) - - The easiest way to do this is to call `schutzbots/provision-composer.sh` from - the `osbuild/image-builder` project. This will install composer, generate - the needed certs, and put the configuration in place. - -4. Starting up image-builder - - Point the URL to wherever composer is hosted, the client certificates and CA - should be reused or copied over from the composer host, they're located in - `/etc/osbuild-composer`. - - In the image-builder checkout directory - - ``` - make build - OSBUILD_URL="https://$composer-url:$composer-port/api/composer/v1" \ - OSBUILD_CERT_PATH=/path/to/client-crt.pem \ - OSBUILD_KEY_PATH=/path/to/client-key.pem \ - OSBUILD_CA_PATH=/path/to/ca-crt.pem \ - ./image-builder - ``` - -5. Starting up image-builder-frontend - - In the image-builder-frontend checkout directory - - ``` - npm install - npm start - ``` - -The UI should be running on -https://prod.foo.redhat.com:1337/apps/image-builder/landing, the api -(image-builder) on -https://prod.foo.redhat.com:1337/api/image-builder/v1/openapi.json +frontend and backend locally against the chrome at cloud.redhat.com. For instructions +see [devel/README.md](devel/README.md). diff --git a/devel/.env b/devel/.env new file mode 100644 index 00000000..cf6bf219 --- /dev/null +++ b/devel/.env @@ -0,0 +1,5 @@ +COMPOSE_PROJECT_NAME=image-builder +STATE_DIR=./state +COMPOSER_CONFIG_DIR=./config/composer +WORKER_CONFIG_DIR=./config/worker +SPANDX_CONFIG=./config/spandx/local-frontend-and-api.js diff --git a/devel/.gitignore b/devel/.gitignore new file mode 100644 index 00000000..ff72b5c7 --- /dev/null +++ b/devel/.gitignore @@ -0,0 +1 @@ +state diff --git a/devel/README.md b/devel/README.md new file mode 100644 index 00000000..205aaf21 --- /dev/null +++ b/devel/README.md @@ -0,0 +1,45 @@ +# devtools + +Development Tools for Image Builder + +## Setup + +To start local development, first clone the image bulider stack: + +```bash +git clone git@github.com:osbuild/osbuild.git +git clone git@github.com:osbuild/osbuild-composer.git +git clone git@github.com:osbuild/image-builder.git +git clone git@github.com:osbuild/image-builder-frontend.git +``` + +Secondly redirect a few domains to localhost. One for each environment +of cloud.redhat.com that exists. You only need the ones you will be +developing against. If you are outside the Red Hat VPN, only `prod` is +available: + +```bash +echo "127.0.0.1 prod.foo.redhat.com" >> /etc/hosts +echo "127.0.0.1 qa.foo.redhat.com" >> /etc/hosts +echo "127.0.0.1 ci.foo.redhat.com" >> /etc/hosts +echo "127.0.0.1 stage.foo.redhat.com" >> /etc/hosts +``` + +Lastly run the setup tool from image-builder-frontend to generate TLS certs +and potentially other runtime configuration. + +```bash +cd image-builder-frontend/devel +./setup.sh +``` + +## Run + +```bash +docker compose up +``` + +Access the service through the GUI: +[http://prod.foo.redhat.com:1337/beta/](http://prod.foo.redhat.com:1337/beta/), or +directly through the API: +[https://prod.foo.redhat.com:1337/docs/api/image-builder](https://prod.foo.redhat.com:1337/docs/api/image-builder). diff --git a/devel/config/composer/osbuild-composer.toml b/devel/config/composer/osbuild-composer.toml new file mode 100644 index 00000000..b14b20c2 --- /dev/null +++ b/devel/config/composer/osbuild-composer.toml @@ -0,0 +1,7 @@ +[worker] +allowed_domains = [ "localhost", "worker.osbuild.org" ] +ca = "/etc/osbuild-composer/ca-crt.pem" + +[koji] +allowed_domains = [ "client.osbuild.org" ] +ca = "/etc/osbuild-composer/ca-crt.pem" diff --git a/devel/config/spandx/local-frontend-and-api.js b/devel/config/spandx/local-frontend-and-api.js new file mode 100644 index 00000000..81331a0d --- /dev/null +++ b/devel/config/spandx/local-frontend-and-api.js @@ -0,0 +1,127 @@ +/*global module*/ +const jwt = require('jsonwebtoken'); +const cookie = require('cookie'); +const fs = require('fs'); +const base64 = require('base-64'); + +const SECTION = 'insights'; +const APP_ID = 'image-builder'; +const FRONTEND_PORT = 8002; +const API_PORT = 8086; +const routes = {}; + +const PORTAL_BACKEND_MARKER = 'PORTAL_BACKEND_MARKER'; + +const keycloakPubkeys = { + prod: fs.readFileSync('/certs/keycloak.prod.cert', 'utf8'), + stage: fs.readFileSync('/certs/keycloak.stage.cert', 'utf8'), + qa: fs.readFileSync('/certs/keycloak.qa.cert', 'utf8') +}; + +const buildUser = input => { + + const user = { + entitlements: { + insights: { is_entitled: true }, + smart_management: { is_entitled: true }, + openshift: { is_entitled: true }, + hybrid: { is_entitled: true }, + migrations: { is_entitled: true }, + ansible: { is_entitled: true } + }, + identity: { + account_number: input.account_number, + type: 'User', + user: { + username: input.username, + email: input.email, + first_name: input.first_name, + last_name: input.last_name, + is_active: true, + is_org_admin: input.is_org_admin, + is_internal: input.is_internal, + locale: input.locale + }, + + internal: { + org_id: input.account_id + } + } + }; + + return user; +}; + +const envMap = { + ci: { + keycloakPubkey: keycloakPubkeys.qa, + target: 'https://ci.cloud.redhat.com', + str: 'ci' + }, + qa: { + keycloakPubkey: keycloakPubkeys.qa, + target: 'https://qa.cloud.redhat.com', + str: 'qa' + }, + stage: { + keycloakPubkey: keycloakPubkeys.stage, + target: 'https://stage.cloud.redhat.com', + str: 'stage' + }, + prod: { + keycloakPubkey: keycloakPubkeys.prod, + target: 'https://cloud.redhat.com', + str: 'prod' + } +}; + +const authPlugin = (req, res, target) => { + let env = envMap.prod; + + switch (req.headers['x-spandx-origin']) { + case 'ci.foo.redhat.com': env = envMap.ci; break; + case 'qa.foo.redhat.com': env = envMap.qa; break; + case 'stage.foo.redhat.com': env = envMap.stage; break; + case 'prod.foo.redhat.com': env = envMap.prod; break; + default: env = false; + } + + if (target === PORTAL_BACKEND_MARKER) { + target = env.target; + console.log(` --> mangled ${PORTAL_BACKEND_MARKER} to ${target}`); + } + + const noop = { then: (cb) => { cb(target); } }; + if (!req || !req.headers || !req.headers.cookie) { return noop; } // no cookies short circut + + const cookies = cookie.parse(req.headers.cookie); + if (!cookies.cs_jwt) { return noop; } // no rh_jwt short circut + + var decoded = jwt.decode(cookies.cs_jwt); + const user = buildUser(decoded); + const unicodeUser = new Buffer(JSON.stringify(user), "utf8"); + req.headers["x-rh-identity"] = unicodeUser.toString("base64"); + return new Promise((resolve, reject) => resolve(target)); +}; + + + +routes[`/beta/${SECTION}/${APP_ID}`] = { host: `http://frontend:${FRONTEND_PORT}` }; +routes[`/${SECTION}/${APP_ID}`] = { host: `http://frontend:${FRONTEND_PORT}` }; +routes[`/beta/apps/${APP_ID}`] = { host: `http://frontend:${FRONTEND_PORT}` }; +routes[`/apps/${APP_ID}`] = { host: `http://frontend:${FRONTEND_PORT}` }; +routes[`/api/${APP_ID}`] = { host: `http://backend:${API_PORT}` }; +routes['/apps/chrome'] = { host: PORTAL_BACKEND_MARKER }; +routes['/apps/beta/chrome'] = { host: PORTAL_BACKEND_MARKER }; + +module.exports = { + bs: { + notify: false, + https: { + key: '/ssl/key.pem', + cert: '/ssl/cert.pem' + } + }, + routerPlugin: authPlugin, + routes: routes, +}; diff --git a/devel/config/x509/openssl.cnf b/devel/config/x509/openssl.cnf new file mode 100644 index 00000000..7ff0d5cc --- /dev/null +++ b/devel/config/x509/openssl.cnf @@ -0,0 +1,85 @@ +# +# ca options +# + +[ca] +default_ca = osbuild_ca + +[osbuild_ca] +database = ./index.txt +new_certs_dir = ./certs +rand_serial = yes + +certificate = ca.cert.pem +private_key = private/ca.key.pem + +default_days = 3650 +default_md = sha256 + +x509_extensions = osbuild_ca_ext + +# See WARNINGS in `man openssl ca`. This is ok, becasue it only copies +# extensions that are not already specified in `osbuild_ca_ext`. +copy_extensions = copy + +preserve = no +policy = osbuild_ca_policy + +# We want to issue multiple certificates with the same subject in the +# testing environment. +unique_subject = no + + +[osbuild_ca_ext] +basicConstraints = critical, CA:TRUE +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always, issuer:always +keyUsage = critical, digitalSignature, cRLSign, keyCertSign + + +[osbuild_ca_policy] +commonName = supplied +emailAddress = supplied + + +# +# Extensions for server certificates +# + +[osbuild_server_ext] +basicConstraints = critical, CA:FALSE +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid, issuer:always +keyUsage = critical, digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth + + +# +# Extensions for client certificates +# + +[osbuild_client_ext] +basicConstraints = CA:FALSE +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid,issuer +keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment +extendedKeyUsage = clientAuth + + +# +# req options +# + +[req] +default_md = sha256 +default_bits = 2048 +distinguished_name = osbuild_distinguished_name + + +# +# Only prompt for CN +# + +[osbuild_distinguished_name] +CN = Common Name +emailAddress = E-Mail Address diff --git a/devel/docker-compose.yml b/devel/docker-compose.yml new file mode 100644 index 00000000..271987b5 --- /dev/null +++ b/devel/docker-compose.yml @@ -0,0 +1,115 @@ +version: '2.4' +services: + composer: + image: local/osbuild-composer + build: + context: ../../osbuild-composer + dockerfile: ./distribution/Dockerfile-ubi + volumes: + - ${COMPOSER_CONFIG_DIR}/osbuild-composer.toml:/etc/osbuild-composer/osbuild-composer.toml + - ${STATE_DIR}/x509/ca-crt.pem:/etc/osbuild-composer/ca-crt.pem + - ${STATE_DIR}/x509/composer-crt.pem:/etc/osbuild-composer/composer-crt.pem + - ${STATE_DIR}/x509/composer-key.pem:/etc/osbuild-composer/composer-key.pem + networks: + net: + ipv4_address: 172.31.0.10 + worker: + image: local/osbuild-worker + build: + context: ../../osbuild-composer + dockerfile: ./distribution/Dockerfile-worker + # override the entrypoint to specify composer hostname and port + entrypoint: /usr/libexec/osbuild-composer/osbuild-worker composer:8700 + volumes: + - ${STATE_DIR}/x509/ca-crt.pem:/etc/osbuild-composer/ca-crt.pem + - ${STATE_DIR}/x509/worker-crt.pem:/etc/osbuild-composer/worker-crt.pem + - ${STATE_DIR}/x509/worker-key.pem:/etc/osbuild-composer/worker-key.pem + environment: + - CACHE_DIRECTORY=/var/cache/osbuild-composer + privileged: true + cap_add: + - MKNOD + - SYS_ADMIN + - NET_ADMIN + networks: + net: + ipv4_address: 172.31.0.20 + depends_on: + - "composer" + postgres: + image: postgres:10.5 + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + volumes: + - ../../image-builder/internal/db/migrations/1_create_table_images.up.sql:/docker-entrypoint-initdb.d/create_tables.sql + networks: + net: + ipv4_address: 172.31.0.30 + backend: + image: local/image-builder + build: + context: ../../image-builder + dockerfile: ./distribution/Dockerfile-ubi + volumes: + - ${STATE_DIR}/x509/ca-crt.pem:/etc/image-builder/ca-crt.pem + - ${STATE_DIR}/x509/client-crt.pem:/etc/image-builder/client-crt.pem + - ${STATE_DIR}/x509/client-key.pem:/etc/image-builder/client-key.pem + environment: + - LISTEN_ADDRESS=backend:8086 + - LOG_LEVEL=DEBUG + - ALLOWED_ORG_IDS=* + - PGHOST=postgres + - PGPORT=5432 + - PGDATABASE=postgres + - PGUSER=postgres + - PGPASSWORD=postgres + - OSBUILD_URL=https://composer:9196 + - DISTRIBUTIONS_DIR=/app/distributions + - OSBUILD_CERT_PATH=/etc/image-builder/client-crt.pem + - OSBUILD_KEY_PATH=/etc/image-builder/client-key.pem + - OSBUILD_CA_PATH=/etc/image-builder/ca-crt.pem + networks: + net: + ipv4_address: 172.31.0.40 + depends_on: + - "composer" + - "postgres" + frontend: + image: local/image-builder-frontend + build: + context: ../../image-builder-frontend + dockerfile: ./distribution/Dockerfile + environment: + - HOST=frontend + networks: + net: + ipv4_address: 172.31.0.50 + insightsproxy: + image: redhatinsights/insights-proxy:latest + security_opt: + - label=disable + environment: + - CUSTOM_CONF=true + volumes: + - ${SPANDX_CONFIG}:/config/spandx.config.js + extra_hosts: + - "prod.foo.redhat.com:127.0.0.1" + - "qa.foo.redhat.com:127.0.0.1" + - "ci.foo.redhat.com:127.0.0.1" + - "stage.foo.redhat.com:127.0.0.1" + networks: + net: + ipv4_address: 172.31.0.60 + ports: + - 1337:1337 + depends_on: + - "backend" + - "frontend" + +networks: + net: + ipam: + driver: default + config: + - subnet: 172.31.0.0/16 diff --git a/devel/gen-certs.sh b/devel/gen-certs.sh new file mode 100755 index 00000000..c6032799 --- /dev/null +++ b/devel/gen-certs.sh @@ -0,0 +1,97 @@ +#!/bin/bash +if (( $# != 3 )); then + echo "Usage: $0 " + echo + echo "Positional arguments" + echo " OpenSSL configuration file" + echo " Destination directory for the generated files" + echo " Working directory for the generation process" + exit 1 +fi + +set -euxo pipefail +# Generate all X.509 certificates for the tests +# The whole generation is done in a $CADIR to better represent how osbuild-ca +# it. +OPENSSL_CONFIG="$1" +CERTDIR="$2" +CADIR="$3" + +# The $CADIR might exist from a previous test (current Schutzbot's imperfection) +rm -rf "$CADIR" || true +mkdir -p "$CADIR" "$CERTDIR" + +# Convert the arguments to real paths so we can safely change working directory +OPENSSL_CONFIG="$(realpath "${OPENSSL_CONFIG}")" +CERTDIR="$(realpath "${CERTDIR}")" +CADIR="$(realpath "${CADIR}")" + +pushd "$CADIR" + mkdir certs private + touch index.txt + + # Generate a CA. + openssl req -config "$OPENSSL_CONFIG" \ + -keyout private/ca.key.pem \ + -new -nodes -x509 -extensions osbuild_ca_ext \ + -out ca.cert.pem -subj "/CN=osbuild.org" + + # Copy the private key to the location expected by the tests + cp ca.cert.pem "$CERTDIR"/ca-crt.pem + + # Generate a composer certificate. + openssl req -config "$OPENSSL_CONFIG" \ + -keyout "$CERTDIR"/composer-key.pem \ + -new -nodes \ + -out /tmp/composer-csr.pem \ + -subj "/CN=localhost/emailAddress=osbuild@example.com" \ + -addext "subjectAltName=DNS:localhost, DNS:composer" + + openssl ca -batch -config "$OPENSSL_CONFIG" \ + -extensions osbuild_server_ext \ + -in /tmp/composer-csr.pem \ + -out "$CERTDIR"/composer-crt.pem + + # Generate a worker certificate. + openssl req -config "$OPENSSL_CONFIG" \ + -keyout "$CERTDIR"/worker-key.pem \ + -new -nodes \ + -out /tmp/worker-csr.pem \ + -subj "/CN=localhost/emailAddress=osbuild@example.com" \ + -addext "subjectAltName=DNS:localhost, DNS:worker" + + openssl ca -batch -config "$OPENSSL_CONFIG" \ + -extensions osbuild_client_ext \ + -in /tmp/worker-csr.pem \ + -out "$CERTDIR"/worker-crt.pem + + # Generate a client certificate. + openssl req -config "$OPENSSL_CONFIG" \ + -keyout "$CERTDIR"/client-key.pem \ + -new -nodes \ + -out /tmp/client-csr.pem \ + -subj "/CN=client.osbuild.org/emailAddress=osbuild@example.com" \ + -addext "subjectAltName=DNS:client.osbuild.org" + + openssl ca -batch -config "$OPENSSL_CONFIG" \ + -extensions osbuild_client_ext \ + -in /tmp/client-csr.pem \ + -out "$CERTDIR"/client-crt.pem + + # Client keys are used by tests to access the composer APIs. Allow all users access. + chmod 644 "$CERTDIR"/client-key.pem + + # Generate a kojihub certificate. + openssl req -config "$OPENSSL_CONFIG" \ + -keyout "$CERTDIR"/kojihub-key.pem \ + -new -nodes \ + -out /tmp/kojihub-csr.pem \ + -subj "/CN=localhost/emailAddress=osbuild@example.com" \ + -addext "subjectAltName=DNS:localhost" + + openssl ca -batch -config "$OPENSSL_CONFIG" \ + -extensions osbuild_server_ext \ + -in /tmp/kojihub-csr.pem \ + -out "$CERTDIR"/kojihub-crt.pem + +popd diff --git a/devel/setup.sh b/devel/setup.sh new file mode 100755 index 00000000..fa9ad6c4 --- /dev/null +++ b/devel/setup.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +./gen-certs.sh \ + config/x509/openssl.cnf \ + state/x509 \ + state/x509/ca