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
172 changes: 130 additions & 42 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@
# PIP_NO_BINARY If set, passed as --build-arg to buildah so Containerfiles
# can set ENV PIP_NO_BINARY. Use ":all:" to force pip to
# build all packages from source instead of using wheels.
# REGISTRY_AUTH_FILE Authentication file passed explicitly to buildah push.
# REGISTRY_CERT_DIR TLS certificate directory passed explicitly to buildah push.

set -euo pipefail

Expand All @@ -102,6 +104,8 @@ UPSTREAM_CONSTRAINTS="upper-constraints.txt"
DEFAULT_STREAM="${DEFAULT_STREAM:-master}"
SKIP_HASH_UPDATE="${SKIP_HASH_UPDATE:-}"
PIP_NO_BINARY="${PIP_NO_BINARY:-}"
REGISTRY_AUTH_FILE="${REGISTRY_AUTH_FILE:-}"
Comment thread
rebtoor marked this conversation as resolved.
REGISTRY_CERT_DIR="${REGISTRY_CERT_DIR:-}"
PARALLEL="${PARALLEL:-$(nproc)}"

# Discover all buildable images from the directory structure.
Expand Down Expand Up @@ -380,10 +384,32 @@ push_image() {
name="$(image_name "${dir_name}")"

IFS=',' read -ra tags <<< "${TAG}"
local registry_args=()
[[ -n "${REGISTRY_AUTH_FILE}" ]] && \
registry_args+=(--authfile "${REGISTRY_AUTH_FILE}")
[[ -n "${REGISTRY_CERT_DIR}" ]] && \
registry_args+=(--cert-dir "${REGISTRY_CERT_DIR}")

for t in "${tags[@]}"; do
local full_tag="${REGISTRY}/${NAMESPACE}/${name}:${t}"
echo "=== Pushing ${full_tag} ==="
buildah push "${full_tag}"
buildah push "${registry_args[@]}" "${full_tag}"
done
}

# Print the exact image references produced for a target.
target_refs() {
local dir_name
local name
local tag
local -a tags
resolve_targets_array "$@" || return 1
for dir_name in "${_RESOLVED_TARGETS[@]}"; do
name="$(image_name "${dir_name}")"
IFS=',' read -ra tags <<< "${TAG}"
for tag in "${tags[@]}"; do
echo "${REGISTRY}/${NAMESPACE}/${name}:${tag}"
done
done
}

Expand All @@ -407,50 +433,93 @@ list_images() {
fi
}

# Resolve which images to process (accepts one or more targets)
# Resolve one or more image expressions in their requested order. A
# comma-separated expression is an explicit ordered union. Separate
# positional targets preserve the upstream multi-target behavior.
# The caller is responsible for including "base" if needed.
resolve_targets() {
local all_images
all_images=($(discover_images))
local resolved=()
local -a resolved=()
local expression item image item_images
declare -A seen=()

for target in "$@"; do
if [[ "${target}" == "all" ]]; then
for expression in "$@"; do
if [[ "${expression}" == "all" ]]; then
echo "${all_images[@]}"
return
fi

local found=0
if [[ "${expression}" == *,* ]]; then
Comment thread
rebtoor marked this conversation as resolved.
local -a requested
IFS=',' read -ra requested <<< "${expression}"
for item in "${requested[@]}"; do
if [[ -z "${item}" || "${item}" != "${item//[[:space:]]/}" ]]; then
echo "ERROR: Invalid empty or whitespace-containing target '${item}'" >&2
return 1
fi
if ! item_images=$(resolve_targets "${item}"); then
return 1
fi
for image in ${item_images}; do
if [[ -z "${seen[${image}]:-}" ]]; then
resolved+=("${image}")
seen["${image}"]=1
fi
done
done
continue
fi

# Exact match
for dir_name in "${all_images[@]}"; do
if [[ "${dir_name}" == "${target}" ]]; then
resolved+=("${target}")
local found=0
for image in "${all_images[@]}"; do
if [[ "${image}" == "${expression}" ]]; then
if [[ -z "${seen[${image}]:-}" ]]; then
resolved+=("${image}")
seen["${image}"]=1
fi
found=1
break
fi
done
[[ ${found} -eq 1 ]] && continue

# Project prefix match
for dir_name in "${all_images[@]}"; do
if [[ "${dir_name}" == "${target}/"* ]]; then
resolved+=("${dir_name}")
for image in "${all_images[@]}"; do
if [[ "${image}" == "${expression}/"* ]]; then
if [[ -z "${seen[${image}]:-}" ]]; then
resolved+=("${image}")
seen["${image}"]=1
fi
found=1
fi
done
[[ ${found} -eq 1 ]] && continue

echo "ERROR: Unknown image or project '${target}'" >&2
echo "ERROR: Unknown image or project '${expression}'" >&2
echo "Available images:" >&2
for dir_name in "${all_images[@]}"; do
echo " ${dir_name}" >&2
for image in "${all_images[@]}"; do
echo " ${image}" >&2
done
return 1
done

if [[ ${#resolved[@]} -eq 0 ]]; then
echo "ERROR: Explicit target selection is empty" >&2
return 1
fi
echo "${resolved[@]}"
}

# Resolve an expression without losing failures in command substitutions.
declare -a _RESOLVED_TARGETS=()
resolve_targets_array() {
local output
if ! output=$(resolve_targets "$@"); then
return 1
fi
_RESOLVED_TARGETS=(${output})
}

# Clone a repo at a branch tip (or tag) and store the resolved commit hash
# in _CLONE_RESULT. Must NOT be called via command substitution ($(...))
# because _AUTO_CLONED assignments would be lost in the subshell.
Expand Down Expand Up @@ -1128,11 +1197,24 @@ case "${ACTION}" in
;;
build-parallel)
_bp_targets=($(resolve_targets "${TARGETS[@]}"))
if [[ ! "${PARALLEL}" =~ ^[1-9][0-9]*$ ]]; then
Comment thread
rebtoor marked this conversation as resolved.
echo "ERROR: PARALLEL must be a positive integer" >&2
exit 1
fi
if [[ -n "${BUILD_LOGS_DIR:-}" ]]; then
_bp_logdir="${BUILD_LOGS_DIR}"
mkdir -p "${_bp_logdir}"
else
_bp_logdir=$(mktemp -d)
fi

# Build base first (all service images depend on it)
for _bp_img in "${_bp_targets[@]}"; do
[[ -n "$(project_name "${_bp_img}")" ]] && continue
build_image "${_bp_img}"
set -o pipefail
build_image "${_bp_img}" 2>&1 |
sed -u "s|^|[${_bp_img}] |" |
tee "${_bp_logdir}/${_bp_img//\//_}.log"
done

# Pre-clone sources so parallel builds don't race on the same directories
Expand All @@ -1142,12 +1224,6 @@ case "${ACTION}" in
done

# Build service images in parallel (max PARALLEL at a time)
if [[ -n "${BUILD_LOGS_DIR:-}" ]]; then
_bp_logdir="${BUILD_LOGS_DIR}"
mkdir -p "${_bp_logdir}"
else
_bp_logdir=$(mktemp -d)
fi
_bp_service_imgs=()
for _bp_img in "${_bp_targets[@]}"; do
[[ -z "$(project_name "${_bp_img}")" ]] && continue
Expand All @@ -1161,47 +1237,50 @@ case "${ACTION}" in
_bp_running=0

for _bp_img in "${_bp_service_imgs[@]}"; do
# Wait for a slot if at the limit
while [[ ${_bp_running} -ge ${PARALLEL} ]]; do
if ! wait -n; then
_bp_finished=""
if wait -n -p _bp_finished "${!_bp_pids[@]}"; then
unset '_bp_pids['"${_bp_finished}"']'
((_bp_running--)) || true
else
_bp_fail=1
[[ -n "${_bp_finished}" ]] && unset '_bp_pids['"${_bp_finished}"']'
break 2
fi
((_bp_running--)) || true
done

_bp_log="${_bp_logdir}/${_bp_img//\//_}.log"
build_image "${_bp_img}" > "${_bp_log}" 2>&1 &
(
set -o pipefail
build_image "${_bp_img}" 2>&1 |
sed -u "s|^|[${_bp_img}] |" |
tee "${_bp_log}"
) &
_bp_pids[$!]="${_bp_img}"
((_bp_running++)) || true
done

# Wait for remaining builds
if [[ ${_bp_fail} -eq 0 ]]; then
while [[ ${_bp_running} -gt 0 ]]; do
if ! wait -n; then
_bp_finished=""
if wait -n -p _bp_finished "${!_bp_pids[@]}"; then
unset '_bp_pids['"${_bp_finished}"']'
((_bp_running--)) || true
else
_bp_fail=1
[[ -n "${_bp_finished}" ]] && unset '_bp_pids['"${_bp_finished}"']'
break
fi
((_bp_running--)) || true
done
fi

# Show logs for all builds
for _bp_log in "${_bp_logdir}"/*.log; do
_bp_name=$(basename "${_bp_log}" .log)
echo "=== ${_bp_name} ==="
cat "${_bp_log}"
echo ""
done

if [[ ${_bp_fail} -eq 1 ]]; then
echo "ERROR: A build failed, killing remaining builds" >&2
echo "ERROR: A build failed; stopping remaining builds" >&2
for _bp_pid in "${!_bp_pids[@]}"; do
kill "${_bp_pid}" 2>/dev/null || true
done
wait 2>/dev/null || true
[[ -z "${BUILD_LOGS_DIR:-}" ]] && rm -rf "${_bp_logdir}"
echo "Build logs are available in ${_bp_logdir}" >&2
exit 1
fi

Expand All @@ -1223,6 +1302,13 @@ case "${ACTION}" in
push_image "${img}"
done
;;
refs)
target_refs "${TARGETS[@]}"
;;
resolve)
resolve_targets_array "${TARGETS[@]}"
printf '%s\n' "${_RESOLVED_TARGETS[@]}"
;;
update-sources)
if [[ -n "${SKIP_HASH_UPDATE}" ]]; then
echo "=== Skipping hash update (SKIP_HASH_UPDATE is set) ==="
Expand Down Expand Up @@ -1330,7 +1416,7 @@ case "${ACTION}" in
list_images
;;
*)
echo "Usage: STREAM=<name> $0 {build|build-parallel|push|update-sources|update-lockfiles|install-deps|list} [target ...]"
echo "Usage: STREAM=<name> $0 {build|build-parallel|push|refs|resolve|update-sources|update-lockfiles|install-deps|list} [target ...]"
echo ""
echo "Images (discovered from containers/):"
for dir_name in $(discover_images); do
Expand All @@ -1354,6 +1440,8 @@ case "${ACTION}" in
echo " BUILD_LOGS_DIR Persist build-parallel logs to this directory"
echo " SKIP_HASH_UPDATE Skip updating pinned hashes; regenerate locks only"
echo " PIP_NO_BINARY Pass PIP_NO_BINARY to container build (e.g., ':all:')"
echo " REGISTRY_AUTH_FILE Registry authentication file for pushes"
echo " REGISTRY_CERT_DIR Registry TLS certificate directory for pushes"
echo ""
echo "Source directories: containers/<project>/src/<name>/"
echo "Overrides: containers/<project>/src/overrides/<pkg>/"
Expand Down
88 changes: 88 additions & 0 deletions docs/developer-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,94 @@ All image tags are verified to exist locally before any push begins.
./build.sh list
```

## Zuul content provider

The `s2i-openstack-container-content-provider` job runs on the
CentOS Stream 10 nodeset host named `builder`. All host preparation, registry
validation, builds, publication, result generation, and cleanup target that
host explicitly. The Zuul executor controls Ansible but does not perform those
mutations.

In this repository the provider defaults to `all`, so every maintained image
is built from its exact `sources.txt` pins. A child job can set `s2i_ci_images`
to an explicit list of image targets; the provider resolves the selection
through `build.sh` and publishes only that set. The caller is responsible
for including `base` in the list if service images depend on it.

To compose the provider in an operator repository, inherit the job, add the
container repository and any related repositories to `required-projects`, set
`s2i_ci_container_project`, and override `s2i_ci_images` with the required
subset. Zuul places those projects in the shared buildset workspace, but this
provider does not yet consume speculative service checkouts or build operators.
Those integrations remain follow-up scope; the current provider always uses
maintained source pins.

### Image deployment metadata

The repository-level `containers/image-mappings.yaml` associates exact build
targets with OpenStackVersion custom-image fields. Unlisted targets have no
deployment mapping but are still built and returned. The consolidated
`watcher/watcher-base` image declares:

```yaml
openstack_version:
custom_container_images:
watcher/watcher-base:
- watcherAPIImage
- watcherApplierImage
- watcherDecisionEngineImage
```

All three keys resolve to the same exact `openstack-watcher-base` reference.
The image contains the API, applier, and decision-engine entry points and the
union of their runtime dependencies. Watcher is intentionally not split into
process-specific images.

Both Cyborg images build and publish but are absent from the central mapping.
Their exact references therefore appear in provider diagnostics without adding
fields to the default deployment map.

A child job may provide `s2i_ci_image_mappings` as a mapping from a selected
image target to a replacement list of keys. Replacement is per image rather
than additive. The provider records whether each effective list came from
tracked or inventory metadata and rejects malformed values, unknown or unbuilt
image targets, empty key strings, duplicate keys, and a key assigned to more
than one image.

### Registry and returned data

The provider starts or inherits a Zuul buildset registry, validates push and
pull with a dedicated UBI tag, builds and pushes the selected image set, and
pulls every exact result back. Credentials and certificate data remain in
Zuul secret data. Returned public diagnostics use the buildset registry's
reachable host or IP and port, never the builder-local registry alias.

`s2i_ci_content.images` contains every exact successful reference, including
base and both unmapped Cyborg images. The partial
`s2i_content_provider_os_custom_container_images` map contains only effective
keys joined to exact successful references. The legacy global OS registry URL
remains the neutral `null` sentinel, while its namespace/tag and gating-repo
fields remain empty or false because this selective provider does not publish a
complete OpenStack image namespace. `s2i_cifmw_build_images_output`
remains an empty mapping and is not repurposed for service images.

All returned variables use the `s2i_content_provider_` or `s2i_cifmw_` prefix
to avoid colliding with the standard `openstack-k8s-operators-content-provider`
when both run as parents of the same consumer job.

Intended references are written before build mutation. Post-run cleanup
removes only those exact Podman pullback and Buildah build tags, verifies exact
absence, and removes a buildset registry only when its ownership marker is
valid.
Per-image parallel logs and registry/result manifests are retained under
`zuul-output/logs/container-build/`.

The provider pauses while dependent jobs run. Private onboarding may attach a
trivial child that prints the returned registry paths and maps. That debug job
does not pull images, patch an OpenStackVersion resource, deploy OpenStack, or
invoke a downstream repository's playbooks. Downstream consumption is separate
work.

## Build architecture

### Base image (`openstack-base`)
Expand Down
Loading