diff --git a/.gitignore b/.gitignore index 2c8d6a1d78..93057f9194 100644 --- a/.gitignore +++ b/.gitignore @@ -189,3 +189,11 @@ bin/ projects/ installers/olm/operator_*.yaml installers/olm/bundles +installers/olm/tools +installers/olm/catalogs + +# Generated namespace-scoped manifests +config/manager/namespace/operator.yaml +config/rbac/namespace/role.yaml +config/rbac/namespace/role_binding.yaml +config/rbac/namespace/service_account.yaml diff --git a/config/marketplace/kustomization.yaml b/config/marketplace/kustomization.yaml deleted file mode 100644 index 42bd0a3da5..0000000000 --- a/config/marketplace/kustomization.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: -- ../operator - diff --git a/installers/olm/Makefile b/installers/olm/Makefile index 8ec3d8f80f..2d9c9feca2 100644 --- a/installers/olm/Makefile +++ b/installers/olm/Makefile @@ -2,97 +2,69 @@ # Percona Server MongoDB Operator - OLM Bundle Generation # ============================================================================== -# Default target .DEFAULT_GOAL := help .SUFFIXES: SHELL := /bin/bash -# ============================================================================== -# Configuration Variables -# ============================================================================== - -# Project configuration +REGISTRY ?= docker.io +PROD_REPOSITORY ?= percona +DEV_REPOSITORY ?= perconalab NAME ?= percona-server-mongodb-operator -IMAGE_TAG_OWNER ?= perconalab -IMAGE_TAG_BASE ?= $(IMAGE_TAG_OWNER)/$(NAME) -MODE ?= namespace +BUNDLE_TYPES := community certified -# Version detection SED := $(shell which gsed || which sed) VERSION ?= $(shell git rev-parse --abbrev-ref HEAD | $(SED) -e 's^/^-^g; s^[.]^-^g;' | tr '[:upper:]' '[:lower:]') -IMAGE := $(IMAGE_TAG_BASE):$(VERSION) - -# Bundle configuration -OPENSHIFT_VERSIONS ?= v4.16-v4.19 -PACKAGE_CHANNEL ?= stable -MIN_KUBE_VERSION ?= "" -DOCKER_DEFAULT_PLATFORM ?= linux/amd64 +CSV_VERSION ?= $(shell printf '%s\n' '$(VERSION)' | $(SED) -nE 's/.*([0-9]+)[.-]([0-9]+)[.-]([0-9]+).*/\1.\2.\3/p') +RELEASE_VERSIONS ?= ../../e2e-tests/release_versions -# Paths -REPO_ROOT := $(shell git rev-parse --show-toplevel) -KUSTOMIZE := $(REPO_ROOT)/bin/kustomize - -# Tool versions -OPERATOR_SDK_VERSION := v1.41.1 +CONFIRM_PUSH ?= 1 -# Bundle image configuration -BUNDLE_IMG ?= $(IMAGE_TAG_BASE):community-bundle-$(VERSION) +BUNDLE_TYPE ?= community +BUNDLE_IMAGE_VERSION ?= $(CSV_VERSION) +BUNDLE_PLATFORM ?= linux/amd64 +BUNDLE_PACKAGE_CHANNEL ?= stable +BUNDLE_SKIP_DIGEST_FAILURE ?= 0 +BUNDLE_DEV_REPO ?= $(REGISTRY)/$(DEV_REPOSITORY)/$(NAME) +BUNDLE_PROD_REPO ?= $(REGISTRY)/$(PROD_REPOSITORY)/$(NAME) -# System detection for tool downloads -UNAME_S := $(shell uname -s) -UNAME_M := $(shell uname -m) -OS_KERNEL := $(shell echo "$(UNAME_S)" | tr '[:upper:]' '[:lower:]') -OS_MACHINE := $(UNAME_M) +CATALOG_BUNDLE_LIMIT ?= 2 +CATALOG_PLATFORM ?= linux/amd64 +CATALOG_NAMESPACE ?= openshift-marketplace +CATALOG_BUILD_PUSH ?= 1 -# Display colors -GREEN := $(shell tput setaf 2) -RESET := $(shell tput sgr0) - -# Export variables for generate.sh -export VERSION OPENSHIFT_VERSIONS PACKAGE_CHANNEL MIN_KUBE_VERSION DOCKER_DEFAULT_PLATFORM MODE - -# ============================================================================== -# Bundle Targets -# ============================================================================== +REPO_ROOT := $(shell git rev-parse --show-toplevel) +KUSTOMIZE := $(REPO_ROOT)/bin/kustomize -DISTROS := community redhat marketplace +OS_KERNEL ?= $(shell uname -s | tr '[:upper:]' '[:lower:]') +OS_MACHINE ?= $(shell uname -m | sed -e 's/^x86_64$$/amd64/' -e 's/^x86_/amd/' -e 's/^aarch64$$/arm64/') +SYSTEM := $(OS_KERNEL)-$(OS_MACHINE) +TOOLS_DIR := tools/$(SYSTEM) -.PHONY: bundles -bundles: ## Build all OLM bundles (community, redhat, marketplace) -bundles: check-prereqs $(DISTROS:%=bundles/%) +CONTAINER ?= docker +JQ_VERSION := 1.7.1 +JQ_PLATFORM := $(if $(filter darwin,$(OS_KERNEL)),macos,$(OS_KERNEL)) +OPERATOR_SDK_VERSION := v1.41.1 +OPM_VERSION := v1.66.0 -.PHONY: $(DISTROS:%=bundles/%) -$(DISTROS:%=bundles/%): bundles/%: tools/operator-sdk - @echo "$(GREEN)Building $* bundle...$(RESET)" - cd ../../config/manager/$(MODE)/ && $(KUSTOMIZE) edit set image psmdb-operator=$(IMAGE) - ./generate.sh $* - ./tools/operator-sdk bundle validate $@ --select-optional='suite=operatorframework' - $(if $(filter community,$*),./tools/operator-sdk bundle validate $@ --select-optional='name=community' --optional-values='index-path=$@/Dockerfile') - @echo "$(GREEN)✓ Bundle stored in installers/olm/bundles/$*$(RESET)" +export PATH := $(CURDIR)/$(TOOLS_DIR):$(PATH) # ============================================================================== -# Docker Build & Push Targets +# Helpers # ============================================================================== +GREEN := $(shell tput setaf 2) +RESET := $(shell tput sgr0) -.PHONY: build -build: ## Build community bundle Docker image -build: - @echo "$(GREEN)Building bundle Docker image...$(RESET)" - docker build -f bundles/community/Dockerfile -t $(BUNDLE_IMG) --platform=linux/amd64 bundles/community - @echo "$(GREEN)✓ Bundle image built: $(BUNDLE_IMG)$(RESET)" +bundle_distribution = $(if $(filter certified,$(1)),redhat,$(1)) -.PHONY: push -push: ## Push bundle Docker image to registry - @echo "$(GREEN)Pushing bundle image to registry...$(RESET)" - docker push $(BUNDLE_IMG) - @echo "$(GREEN)✓ Bundle image pushed: $(BUNDLE_IMG)$(RESET)" +export VERSION CSV_VERSION BUNDLE_IMAGE_VERSION BUNDLE_PACKAGE_CHANNEL +export PACKAGE_NAME_OVERRIDE CSV_NAME_OVERRIDE +export BUNDLE_PLATFORM CONFIRM_PUSH BUNDLE_SKIP_DIGEST_FAILURE # ============================================================================== -# Utility Targets +# Validation and Tool Helpers # ============================================================================== - .PHONY: check-prereqs -check-prereqs: check-version check-git check-tools +check-prereqs: check-version check-csv-version check-tools tools $(KUSTOMIZE) .PHONY: check-version check-version: @@ -100,85 +72,263 @@ ifndef VERSION $(error VERSION is not set) endif -.PHONY: check-git -check-git: - @if ! git rev-parse --git-dir > /dev/null 2>&1; then \ - echo "Error: Not in a git repository"; \ +.PHONY: check-csv-version +check-csv-version: +ifndef CSV_VERSION + $(error CSV_VERSION is not set and could not be parsed from VERSION=$(VERSION)) +endif + @if ! [[ "$(CSV_VERSION)" =~ ^[0-9]+\.[0-9]+\.[0-9]+$$ ]]; then \ + echo "CSV_VERSION must be a semver without prerelease/build metadata, got: $(CSV_VERSION)"; \ exit 1; \ fi .PHONY: check-tools check-tools: - @for cmd in gawk gcsplit yq; do \ + @for cmd in curl gawk gcsplit yq yamllint envsubst '$(CONTAINER)'; do \ if ! command -v $$cmd >/dev/null 2>&1; then \ echo "Error: $$cmd is required but not installed"; \ exit 1; \ fi; \ done -.PHONY: install-olm -install-olm: ## Install OLM in Kubernetes cluster -install-olm: tools/operator-sdk - ./tools/operator-sdk olm install +$(KUSTOMIZE): + $(MAKE) -C ../.. bin/kustomize -.PHONY: clean -clean: ## Remove generated files and downloaded tools - rm -rf ./bundles ./projects ./tools +.PHONY: tools +tools: $(TOOLS_DIR) $(TOOLS_DIR)/jq $(TOOLS_DIR)/operator-sdk $(TOOLS_DIR)/opm -.PHONY: help -help: ## Show this help message - @awk 'BEGIN {FS = ": ## "; printf "\n$(GREEN)Usage:$(RESET)\n make [target]\n\n$(GREEN)Targets:$(RESET)\n"} /^[a-zA-Z_-]+: ## / {printf " %-20s %s\n", $$1, $$2}' $(MAKEFILE_LIST) +$(TOOLS_DIR): + mkdir -p $(TOOLS_DIR) + +$(TOOLS_DIR)/jq: + curl -fsSL "https://github.com/jqlang/jq/releases/download/jq-$(JQ_VERSION)/jq-$(JQ_PLATFORM)-$(OS_MACHINE)" -o $@.tmp + mv $@.tmp $@ + chmod +x $@ + +$(TOOLS_DIR)/operator-sdk: | $(TOOLS_DIR) + curl -fsSL "https://github.com/operator-framework/operator-sdk/releases/download/$(OPERATOR_SDK_VERSION)/operator-sdk_$(OS_KERNEL)_$(OS_MACHINE)" -o $@.tmp + mv $@.tmp $@ + chmod +x $@ + +$(TOOLS_DIR)/opm: | $(TOOLS_DIR) + curl -fsSL "https://github.com/operator-framework/operator-registry/releases/download/$(OPM_VERSION)/$(OS_KERNEL)-$(OS_MACHINE)-opm" -o $@.tmp + mv $@.tmp $@ + chmod +x $@ # ============================================================================== -# Tool Management +# Generic Bundle Rules # ============================================================================== +.PHONY: generate-bundle check-bundle-type -.PHONY: tools -tools: ## Download required tools -tools: tools/operator-sdk +check-bundle-type: + @if [[ "$(BUNDLE_TYPE)" != "community" && "$(BUNDLE_TYPE)" != "certified" ]]; then \ + echo "BUNDLE_TYPE must be community or certified"; \ + exit 1; \ + fi -# Download operator-sdk -tools/operator-sdk: - @echo "Downloading operator-sdk $(OPERATOR_SDK_VERSION)..." - @install -d tools - @curl -fSL --fail -o '$@' \ - 'https://github.com/operator-framework/operator-sdk/releases/download/$(OPERATOR_SDK_VERSION)/operator-sdk_$(OS_KERNEL)_$(OS_MACHINE)' \ - || { rm -f '$@'; echo "Failed to download operator-sdk"; exit 1; } - @chmod +x '$@' - @echo "✓ operator-sdk installed" +generate-bundle: check-bundle-type tools $(KUSTOMIZE) check-version check-csv-version check-tools + @echo "$(GREEN)Building $(BUNDLE_TYPE) bundle...$(RESET)" + @set -euo pipefail; \ + distribution='$(BUNDLE_TYPE)'; \ + if [[ '$(BUNDLE_TYPE)' == 'certified' ]]; then \ + distribution='redhat'; \ + fi; \ + manager_kustomization='../../config/manager/namespace/kustomization.yaml'; \ + original_manager_kustomization="$$(mktemp)"; \ + cp "$${manager_kustomization}" "$${original_manager_kustomization}"; \ + trap 'cp "$${original_manager_kustomization}" "$${manager_kustomization}"; rm -f "$${original_manager_kustomization}"' EXIT; \ + ./generate.sh "$${distribution}" && echo "$(GREEN)Bundle stored in installers/olm/bundles/$${distribution}$(RESET)" # ============================================================================== -# Development Targets +# Development Rules +# ============================================================================== + +## Generate all development OLM bundles +.PHONY: bundles bundle $(BUNDLE_TYPES:%=bundle/%) +bundles: ## Generate all development OLM bundles +bundles: bundle +bundle: $(BUNDLE_TYPES:%=bundle/%) + +bundle/community: +bundle/certified: + +$(BUNDLE_TYPES:%=bundle/%): bundle/%: check-version check-csv-version + @package='$(NAME)'; \ + if [[ "$*" == "certified" ]]; then \ + package='$(NAME)-certified'; \ + fi; \ + csv_name="$${package}.v$(CSV_VERSION)"; \ + $(MAKE) generate-bundle \ + BUNDLE_TYPE='$*' \ + PACKAGE_NAME_OVERRIDE="$${package}" \ + CSV_NAME_OVERRIDE="$${csv_name}" + +## Build and push all development catalog images to DEV_REPOSITORY +.PHONY: catalog-build-push $(BUNDLE_TYPES:%=catalog-build-push/%) +catalog-build-push: ## Build and push all development catalog images +catalog-build-push: $(BUNDLE_TYPES:%=catalog-build-push/%) + +catalog-build-push/community: +catalog-build-push/certified: + +$(BUNDLE_TYPES:%=catalog-build-push/%): catalog-build-push/%: tools check-version check-csv-version + @if [[ "$(CATALOG_BUILD_PUSH)" == "true" || "$(CATALOG_BUILD_PUSH)" == "1" ]]; then \ + BUNDLE_IMAGE_VERSION='$(BUNDLE_IMAGE_VERSION)' \ + CATALOG_BUNDLE_LIMIT='$(CATALOG_BUNDLE_LIMIT)' \ + CATALOG_PLATFORM='$(CATALOG_PLATFORM)' \ + CONTAINER='$(CONTAINER)' \ + CONFIRM_PUSH='$(CONFIRM_PUSH)' \ + ./build-catalog.sh build \ + '$*' \ + '$(BUNDLE_DEV_REPO)' \ + '$(BUNDLE_DEV_REPO)'; \ + else \ + echo "[olm] Skipping catalog build and push"; \ + fi + +## Build, push, and deploy all development catalog sources +.PHONY: deploy $(BUNDLE_TYPES:%=deploy/%) +deploy: ## Build, push, and deploy all development catalog sources +deploy: $(BUNDLE_TYPES:%=deploy/%) + +deploy/community: +deploy/certified: + +$(BUNDLE_TYPES:%=deploy/%): deploy/%: tools check-version check-csv-version + $(MAKE) bundle/$* + $(MAKE) build/$* + $(MAKE) push/$* + $(MAKE) catalog-build-push/$* + CATALOG_NAMESPACE='$(CATALOG_NAMESPACE)' \ + NAME='$(NAME)' \ + bash ./build-catalog.sh apply '$*' '$(BUNDLE_DEV_REPO)' + +# ============================================================================== +# Production Rules +# ============================================================================== + +## Generate all production OLM bundles. +.PHONY: bundles-prod $(BUNDLE_TYPES:%=bundle-prod/%) +bundles-prod: ## Generate all production OLM bundles +bundles-prod: $(BUNDLE_TYPES:%=bundle-prod/%) + +bundle-prod/community: +bundle-prod/certified: + +$(BUNDLE_TYPES:%=bundle-prod/%): bundle-prod/%: + $(MAKE) generate-bundle BUNDLE_TYPE='$*' + +## Build production bundles, optionally build/push the catalog, and deploy it +.PHONY: deploy-prod $(BUNDLE_TYPES:%=deploy-prod/%) +deploy-prod: ## Build production bundles, catalog images, and deploy catalog sources +deploy-prod: $(BUNDLE_TYPES:%=deploy-prod/%) + +deploy-prod/community: +deploy-prod/certified: + +$(BUNDLE_TYPES:%=deploy-prod/%): deploy-prod/%: tools check-version check-csv-version + $(MAKE) bundle-prod/$* + $(MAKE) build-prod BUNDLE_TYPE='$*' + $(MAKE) push-prod BUNDLE_TYPE='$*' + @if [[ "$(CATALOG_BUILD_PUSH)" == "true" || "$(CATALOG_BUILD_PUSH)" == "1" ]]; then \ + BUNDLE_IMAGE_VERSION='$(BUNDLE_IMAGE_VERSION)' \ + CATALOG_BUNDLE_LIMIT='$(CATALOG_BUNDLE_LIMIT)' \ + CATALOG_PLATFORM='$(CATALOG_PLATFORM)' \ + CONTAINER='$(CONTAINER)' \ + CONFIRM_PUSH='$(CONFIRM_PUSH)' \ + bash ./build-catalog.sh build \ + '$*' \ + '$(BUNDLE_DEV_REPO)' \ + '$(BUNDLE_PROD_REPO)'; \ + else \ + echo "[olm] Skipping catalog build and push"; \ + fi + CATALOG_NAMESPACE='$(CATALOG_NAMESPACE)' \ + NAME='$(NAME)' \ + bash ./build-catalog.sh apply '$*' '$(BUNDLE_DEV_REPO)' + +# ============================================================================== +# Image Rules +# ============================================================================== + +.PHONY: build build-prod $(BUNDLE_TYPES:%=build/%) $(BUNDLE_TYPES:%=build-prod/%) +build: ## Build bundle image locally (set BUNDLE_TYPE=community|certified) +build-prod: ## Build production bundle image locally (set BUNDLE_TYPE=community|certified) + +$(BUNDLE_TYPES:%=build/%): build/%: + $(MAKE) build BUNDLE_TYPE='$*' + +$(BUNDLE_TYPES:%=build-prod/%): build-prod/%: + $(MAKE) build-prod BUNDLE_TYPE='$*' + +build: BUNDLE_REPO = $(BUNDLE_DEV_REPO) +build-prod: BUNDLE_REPO = $(BUNDLE_PROD_REPO) +build build-prod: check-bundle-type check-version check-csv-version + BUNDLE_PLATFORM='$(BUNDLE_PLATFORM)' \ + ./build-bundle.sh \ + build \ + '$(CONTAINER)' \ + 'bundles/$(call bundle_distribution,$(BUNDLE_TYPE))' \ + '$(call bundle_distribution,$(BUNDLE_TYPE))' \ + '$(BUNDLE_IMAGE_VERSION)' \ + '$(BUNDLE_REPO)' + + +.PHONY: push push-prod $(BUNDLE_TYPES:%=push/%) $(BUNDLE_TYPES:%=push-prod/%) +push: ## Push existing bundle image (set BUNDLE_TYPE=community|certified) +push-prod: ## Push existing production bundle image (set BUNDLE_TYPE=community|certified) + +$(BUNDLE_TYPES:%=push/%): push/%: + $(MAKE) push BUNDLE_TYPE='$*' + +$(BUNDLE_TYPES:%=push-prod/%): push-prod/%: + $(MAKE) push-prod BUNDLE_TYPE='$*' + +push: BUNDLE_REPO = $(BUNDLE_DEV_REPO) +push-prod: BUNDLE_REPO = $(BUNDLE_PROD_REPO) +push push-prod: check-bundle-type check-version check-csv-version + ./build-bundle.sh \ + push \ + '$(CONTAINER)' \ + 'bundles/$(call bundle_distribution,$(BUNDLE_TYPE))' \ + '$(call bundle_distribution,$(BUNDLE_TYPE))' \ + '$(BUNDLE_IMAGE_VERSION)' \ + '$(BUNDLE_REPO)' + +.PHONY: build-bundle-images +build-bundle-images: check-version check-csv-version + $(MAKE) build BUNDLE_TYPE=community + $(MAKE) build BUNDLE_TYPE=certified + +# ============================================================================== +# Validation Rules # ============================================================================== .PHONY: validate -validate: ## Validate existing bundles without rebuilding - @for distro in $(DISTROS); do \ - if [ -d "bundles/$$distro" ]; then \ - echo "Validating $$distro bundle..."; \ - ./tools/operator-sdk bundle validate "bundles/$$distro" --select-optional='suite=operatorframework' || exit 1; \ - fi; \ - done - @echo "$(GREEN)✓ All bundles validated$(RESET)" +validate: ## Run bundle image and directory validation +validate: tools +validate: $(BUNDLE_TYPES:%=validate-%-image) +validate: $(BUNDLE_TYPES:%=validate-%-directory) -.PHONY: list-versions -list-versions: ## Show current version information - @echo "Current configuration:" - @echo " VERSION: $(VERSION)" - @echo " IMAGE: $(IMAGE)" - @echo " MODE: $(MODE)" - @echo " OPENSHIFT_VERSIONS: $(OPENSHIFT_VERSIONS)" - @echo " MIN_KUBE_VERSION: $(MIN_KUBE_VERSION)" +.PHONY: validate/community validate/certified +validate/community validate/certified: validate/%: tools + ./validate-image.sh '$(CONTAINER)' 'bundles/$(call bundle_distribution,$*)' + ./validate-directory.sh 'bundles/$(call bundle_distribution,$*)' + +validate-%-directory: + ./validate-directory.sh 'bundles/$(call bundle_distribution,$*)' + +validate-%-image: + ./validate-image.sh '$(CONTAINER)' 'bundles/$(call bundle_distribution,$*)' # ============================================================================== -# Kustomize Integration (from root Makefile) +# Utility Rules # ============================================================================== -# Include go-get-tool function from root Makefile if kustomize target is needed -ifneq (,$(findstring kustomize,$(MAKECMDGOALS))) -include ../../Makefile -endif +.PHONY: clean +clean: ## Remove generated files and downloaded tools + rm -rf ./bundles ./catalogs ./projects ./tools -.PHONY: kustomize -kustomize: ## Download kustomize locally if necessary - $(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v4@latest) \ No newline at end of file +.PHONY: help +help: ## Show this help message + @awk 'BEGIN {FS = ": ## "; printf "\n$(GREEN)Usage:$(RESET)\n make [target]\n\n$(GREEN)Targets:$(RESET)\n"} /^[a-zA-Z0-9_\/%-]+: ## / {printf " %-28s %s\n", $$1, $$2}' $(MAKEFILE_LIST) diff --git a/installers/olm/README.md b/installers/olm/README.md index 8ffbc3ea60..358b507e0c 100644 --- a/installers/olm/README.md +++ b/installers/olm/README.md @@ -1,23 +1,358 @@ -1. To generate bundle correctly please set env variables (default values for these variables you can check in makefile): +# Percona Server for MongoDB Operator OLM bundles + +This directory contains the automation used to generate, validate, build, publish, +and deploy OLM bundle content for the Percona Server for MongoDB Operator. + +Two bundle types are supported: + +- `community` +- `certified` + +Bundles are generated for namespace-scoped installation. Certified bundles keep +`MultiNamespace` and `AllNamespaces` unsupported, matching the current certified +OperatorHub bundle style. + +## Requirements + +Install the host tools checked by `make`: + +```bash +gawk +gcsplit +yq +yamllint +envsubst +kubectl +docker +``` + +The Makefile downloads the OLM helper tools into: + +```text +installers/olm/tools/- +``` + +```bash +make tools +``` + +Downloaded tools: + +- `jq` +- `operator-sdk` +- `opm` + +## Variables + +Most workflows only require `VERSION`. + +```bash +export VERSION=1.23.0 +``` + +Useful optional variables: + +| Variable | Description | Example | +| --- | --- | --- | +| `CSV_VERSION` | CSV version. Defaults to the first `x.y.z` parsed from `VERSION`. | `1.23.0` | +| `REGISTRY` | Registry used for operator, bundle, and catalog images. | `docker.io` | +| `PROD_REPOSITORY` | Repository namespace used for production bundle images. | `percona` | +| `DEV_REPOSITORY` | Repository namespace used for development bundle images and all catalog images. | `perconalab` | +| `BUNDLE_DEV_REPO` | Repository used by development bundle images. | `docker.io/perconalab/percona-server-mongodb-operator` | +| `BUNDLE_PROD_REPO` | Repository used by production bundle images. | `docker.io/percona/percona-server-mongodb-operator` | +| `BUNDLE_PLATFORM` | Platform used when building current bundle images locally. | `linux/amd64` | +| `BUNDLE_PACKAGE_CHANNEL` | Package channel used for the generated current bundle. | `stable` | +| `CONFIRM_PUSH` | Ask for confirmation before pushing bundle and catalog images. Set to `0` for non-interactive runs. | `1` | +| `CATALOG_BUNDLE_LIMIT` | Number of previous OperatorHub bundle versions to include in rendered catalogs, in addition to the current release bundle. | `2` | +| `CATALOG_PLATFORM` | Platform used when building previous OperatorHub bundle images and catalog images. | `linux/amd64` | +| `CATALOG_BUILD_PUSH` | Build and push catalog images during deploy targets when enabled. | `1` | +| `BUNDLE_SKIP_DIGEST_FAILURE` | Continue certified bundle generation when a non-required digest cannot be resolved. Missing digests are rendered as ``. | `1` | + +OpenShift versions are resolved from `../../e2e-tests/release_versions` and used +to render `com.redhat.openshift.versions`. + +Override only when necessary: + +```bash +export OPENSHIFT_VERSIONS="v4.18-v4.22" +``` + +--- + +# Development + +Development bundle targets generate bundle directories only. Build bundle +images explicitly with `make build/` and push existing local images with +`make push/`. + +Development catalog sources use `CATALOG_ENV=dev` by default, while package +names keep the production names. Release targets override `CATALOG_ENV=prod`. + +Packages: + +- `percona-server-mongodb-operator` +- `percona-server-mongodb-operator-certified` + +Catalog sources: + +- `community-dev` +- `certified-dev` + +## Generate bundles + +```bash +make bundles VERSION=1.23.0 +make bundle/community VERSION=1.23.0 +make bundle/certified VERSION=1.23.0 +``` + +## Build bundle images + +Build a generated bundle image locally: + +```bash +make build/community VERSION=1.23.0 +make build/certified VERSION=1.23.0 +``` + +## Push bundle images + +Push an existing generated bundle image: + +```bash +make push/community VERSION=1.23.0 +make push/certified VERSION=1.23.0 +``` + +The manual development bundle workflow is: + +```text +make bundle/community + ↓ +make build/community + ↓ +make push/community +``` + +## Build catalogs + +Catalog build targets render and push the catalog image to `BUNDLE_DEV_REPO` +with the `-dev-catalog` suffix. The current release bundle image must already +exist in `BUNDLE_DEV_REPO` with the normal bundle tag. + +For the latest `CATALOG_BUNDLE_LIMIT` previous versions already published in +OperatorHub, `build-catalog.sh` downloads the bundle manifests from the GitHub +community or certified OperatorHub repositories, builds bundle images with the +`-dev-catalog` suffix, and pushes them before rendering the catalog. + +With the default `CATALOG_BUNDLE_LIMIT=2`, each catalog contains: + +```text +current release bundle +latest previous GitHub OperatorHub bundle +second latest previous GitHub OperatorHub bundle +``` + +For example, with `VERSION=1.23.0` and previous GitHub versions `1.22.0` and +`1.21.2`, the community catalog uses: + +```text +docker.io/perconalab/percona-server-mongodb-operator:1.23.0-community-bundle +docker.io/perconalab/percona-server-mongodb-operator:1.22.0-community-bundle-dev-catalog +docker.io/perconalab/percona-server-mongodb-operator:1.21.2-community-bundle-dev-catalog +``` + +The certified catalog follows the same pattern: + +```text +docker.io/perconalab/percona-server-mongodb-operator:1.23.0-certified-bundle +docker.io/perconalab/percona-server-mongodb-operator:1.22.0-certified-bundle-dev-catalog +docker.io/perconalab/percona-server-mongodb-operator:1.21.2-certified-bundle-dev-catalog +``` + +All bundles are rendered into their own versioned channel. `BUNDLE_PACKAGE_CHANNEL` +sets the channel prefix (`stable` by default), so versions are rendered into +channels such as `stable-v1.23`, `stable-v1.22`, and `stable-v1.21`. + +```bash +make catalog-build-push/community VERSION=1.23.0 +make catalog-build-push/certified VERSION=1.23.0 +``` + +## Personal catalog testing + +Override `DEV_REPOSITORY` to build and push development bundles and catalog +images to a personal repository namespace. Use `CATALOG_NAMESPACE=olm` on +clusters where the OpenShift console reads the software catalog from `olm`. + +```bash +make deploy/community \ + VERSION=1.23.0 \ + CSV_VERSION=1.23.0 \ + DEV_REPOSITORY=my-repository \ + CATALOG_NAMESPACE=olm +``` + +This publishes images such as: + +```text +docker.io/my-repository/percona-server-mongodb-operator:1.23.0-community-bundle +docker.io/my-repository/percona-server-mongodb-operator:1.22.0-community-bundle-dev-catalog +docker.io/my-repository/percona-server-mongodb-operator:community-dev-catalog +``` + +## Deploy catalogs + +Deploy generates the bundle, builds and pushes the current bundle image, builds +and pushes the catalog image, and applies the CatalogSource. + +```bash +make deploy/community VERSION=1.23.0 +make deploy/certified VERSION=1.23.0 +make deploy VERSION=1.23.0 +``` + +After deployment, verify the packages: + +```bash +kubectl get packagemanifest percona-server-mongodb-operator -n openshift-marketplace +kubectl get packagemanifest percona-server-mongodb-operator-certified -n openshift-marketplace +``` + +or search in the OpenShift console for: + +```text +Percona Distribution for MongoDB Operator +``` + +--- + +# Release (`*-prod`) + +Release targets use the `-prod` suffix. + +## Generate release bundles + +```bash +make bundles-prod VERSION=1.23.0 +make bundle-prod/community VERSION=1.23.0 +make bundle-prod/certified VERSION=1.23.0 +``` + +## Deploy release catalogs + +Release deploy targets generate production bundles, build and push production +bundle images, and build and push catalog images through +`build-catalog.sh build` when `CATALOG_BUILD_PUSH=1`. Catalog images use +`BUNDLE_DEV_REPO` with the `-prod-catalog` suffix; production deploys switch the +bundle image repository to `BUNDLE_PROD_REPO`. + +```bash +make deploy-prod/community VERSION=1.23.0 +make deploy-prod/certified VERSION=1.23.0 +make deploy-prod VERSION=1.23.0 +``` + +Build a production bundle image manually: + +```bash +make build-prod BUNDLE_TYPE=community VERSION=1.23.0 +make build-prod BUNDLE_TYPE=certified VERSION=1.23.0 +``` + +Push a production bundle image manually: + ```bash -# operator version -export VERSION=1.18.0 -# By default we use perconalab for tag owner. Please update this variable to use another repo -export IMAGE_TAG_OWNER=percona -# Min k8s version -export MIN_KUBE_VERSION=1.27.0 -# Openshift versions: -export OPENSHIFT_VERSIONS="v4.13-v4.16" -# Set namespace or cluster (to generate bundles for cluster-wide) -export MODE=namespace +make push-prod BUNDLE_TYPE=community VERSION=1.23.0 +make push-prod BUNDLE_TYPE=certified VERSION=1.23.0 ``` -2. Also it could be useful to check variable in makefile and update if you need something extra. For the most cases to update these variables is enough -3. Update spec.description in bundle.csv.yaml with features added in this release. -4. Run bundle generation: + +Each deploy performs the complete release workflow: + +```text +generate production bundle + ↓ +build production bundle image + ↓ +push production bundle image + ↓ +build and push catalog image pointing to the production bundle + ↓ +apply CatalogSource +``` + +Unlike the development targets, release bundles use the production package names: + +- `percona-server-mongodb-operator` +- `percona-server-mongodb-operator-certified` + +--- + +# Validation + +Validate every generated bundle: + +```bash +make validate VERSION=1.23.0 +``` + +Or validate a single bundle: + +```bash +make validate/community VERSION=1.23.0 +make validate/certified VERSION=1.23.0 +``` + +Validation uses: + +- `validate-image.sh` +- `validate-directory.sh` + +--- + +# Certified Metadata + +Certified bundles resolve image metadata and related image digests through +`distributions/redhat.sh`. + +The public certified bundle type maps to the internal `redhat` distribution. +Generated certified bundle files are written under: + +```text +installers/olm/bundles/redhat +``` + +The bundle image tag still uses the public `certified` name, for example: + +```text +:1.23.0-certified-bundle +``` + +Bundle generation fails when: + +- a required image is missing; +- a certified image tag does not match the expected pattern; +- the required `clustersync` tag is not found in the Red Hat repository. + +When `BUNDLE_SKIP_DIGEST_FAILURE=1`, missing non-`clustersync` digests are rendered as +`` and reported in the build output. The `clustersync` tag is always +checked strictly, so generation stops immediately when +`registry.connect.redhat.com/...:-clustersync` is not found in the +Red Hat repository. + +--- + +# Cleanup + +Remove generated bundles, catalogs, temporary SDK projects, and downloaded +tools: + ```bash -# Generate all bundles community redhat and marketplace: -make bundles -# Generate only specific bundle: -make bundles/community +make clean ``` +Display all available targets: + +```bash +make help +``` diff --git a/installers/olm/build-bundle.sh b/installers/olm/build-bundle.sh new file mode 100755 index 0000000000..56f59d40ac --- /dev/null +++ b/installers/olm/build-bundle.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash + +set -euo pipefail + +is_true() { + case "${1:-}" in + 1|true|TRUE|yes|YES|y|Y) + return 0 + ;; + *) + return 1 + ;; + esac +} + +confirm_push() { + local image="$1" + local answer + + if ! is_true "${CONFIRM_PUSH:-1}"; then + return 0 + fi + + if [[ -r /dev/tty ]]; then + read -r -p "Push bundle image ${image}? [y/N] " answer /dev/null + + "${container}" buildx build \ + --platform "${platforms}" \ + -t "${image}" \ + --load \ + . + + popd >/dev/null +} + +push_image() { + local container="$1" + local image="$2" + + confirm_push "${image}" || { + echo "Bundle image push skipped: ${image}" + exit 1 + } + "${container}" push "${image}" +} + +main() { + local action="${1:-}" + + if [[ "${action}" != "build" && "${action}" != "push" ]]; then + echo "Usage: $0 build|push CONTAINER BUNDLE_DIR DISTRO VERSION BUNDLE_REPO" >&2 + exit 1 + fi + + shift + if [[ "$#" -ne 5 ]]; then + echo "Usage: $0 build|push CONTAINER BUNDLE_DIR DISTRO VERSION BUNDLE_REPO" >&2 + exit 1 + fi + + local container="$1" + local directory="$2" + local distro="$3" + local version="$4" + local bundle_repo="$5" + local image + + image=$(bundle_image "${distro}" "${version}" "${bundle_repo}") + + case "${action}" in + build) + build_image "${container}" "${directory}" "${image}" + ;; + push) + push_image "${container}" "${image}" + ;; + esac +} + +main "$@" diff --git a/installers/olm/build-catalog.sh b/installers/olm/build-catalog.sh new file mode 100755 index 0000000000..2e00123d33 --- /dev/null +++ b/installers/olm/build-catalog.sh @@ -0,0 +1,345 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ACTION="${1:-}" +BUNDLE_TYPE="${2:-}" +CATALOG_REPO="${3:-}" +BUNDLE_REPO="${4:-}" + +CONTAINER="${CONTAINER:-docker}" +CATALOG_PLATFORM="${CATALOG_PLATFORM:-linux/amd64,linux/arm64}" +CATALOG_BUNDLE_LIMIT="${CATALOG_BUNDLE_LIMIT:-2}" +CATALOG_NAMESPACE="${CATALOG_NAMESPACE:-openshift-marketplace}" +CONFIRM_PUSH="${CONFIRM_PUSH:-1}" +NAME="${NAME:-percona-server-mongodb-operator}" +CATALOG_ENV="${CATALOG_ENV:-dev}" + +usage() { + cat < $0 build \ + + + $0 apply \ + + +Examples: + BUNDLE_IMAGE_VERSION=1.23.0 $0 build \ + community \ + docker.io/perconalab/percona-server-mongodb-operator \ + docker.io/perconalab/percona-server-mongodb-operator + + $0 apply \ + community \ + docker.io/perconalab/percona-server-mongodb-operator + + $0 apply \ + certified \ + docker.io/percona/percona-server-mongodb-operator + # -> CatalogSource name: certified-prod +EOF +} + +die() { + echo "[olm] ERROR: $*" >&2 + exit 1 +} + +require() { + command -v "$1" >/dev/null 2>&1 || die "$1 is required" +} + +is_true() { + case "${1:-}" in + 1|true|TRUE|yes|YES|y|Y) return 0 ;; + *) return 1 ;; + esac +} + +configure_bundle_type() { + case "$BUNDLE_TYPE" in + community) + GITHUB_REPO="k8s-operatorhub/community-operators" + OPERATOR_PATH="operators/percona-server-mongodb-operator" + ;; + certified) + GITHUB_REPO="redhat-openshift-ecosystem/certified-operators" + OPERATOR_PATH="operators/percona-server-mongodb-operator-certified" + ;; + *) + usage + die "unsupported bundle type: ${BUNDLE_TYPE:-empty}" + ;; + esac +} + +catalog_image() { + if [[ -n "$CATALOG_ENV" ]]; then + echo -n "${CATALOG_REPO}:${BUNDLE_TYPE}-${CATALOG_ENV}-catalog" + return + fi + echo -n "${CATALOG_REPO}:${BUNDLE_TYPE}-catalog" +} + +current_bundle_image() { + echo -n "${BUNDLE_REPO}:${BUNDLE_IMAGE_VERSION}-${BUNDLE_TYPE}-bundle" +} + +previous_bundle_image() { + if [[ -n "$CATALOG_ENV" ]]; then + echo -n "${CATALOG_REPO}:$1-${BUNDLE_TYPE}-bundle-${CATALOG_ENV}-catalog" + return + fi + echo -n "${CATALOG_REPO}:$1-${BUNDLE_TYPE}-bundle-catalog" +} + +catalog_source_name() { + if [[ -n "$CATALOG_ENV" ]]; then + echo -n "${BUNDLE_TYPE}-${CATALOG_ENV}" + return + fi + echo -n "${BUNDLE_TYPE}" +} + +list_bundle_images() { + local prefix="$1" + local version + + for version in "${OLD_VERSIONS[@]}"; do + echo "${prefix}$(previous_bundle_image "$version")" + done + + echo "${prefix}$(current_bundle_image)" +} + +download_operatorhub() { + local archive="${TMP_DIR}/operatorhub.tar.gz" + + echo "[olm] Downloading ${GITHUB_REPO}" + + mkdir -p "$OPERATORHUB_DIR" + + curl -fsSL \ + "https://github.com/${GITHUB_REPO}/archive/refs/heads/main.tar.gz" \ + -o "$archive" + + tar -xzf "$archive" \ + -C "$OPERATORHUB_DIR" \ + --strip-components=1 +} + +find_previous_versions() { + local operator_dir="${OPERATORHUB_DIR}/${OPERATOR_PATH}" + + [[ -d "$operator_dir" ]] || + die "operator directory not found: ${operator_dir}" + + find "$operator_dir" \ + -mindepth 1 \ + -maxdepth 1 \ + -type d \ + -exec basename {} \; | + grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | + awk -v current="$BUNDLE_IMAGE_VERSION" ' + $0 != current && !seen[$0]++ { print } + ' | + sort -t. -k1,1n -k2,2n -k3,3n | + tail -n "$CATALOG_BUNDLE_LIMIT" +} + +confirm_build() { + local answer + local tty_source=/dev/stdin + + is_true "$CONFIRM_PUSH" || return 0 + + [[ -r /dev/tty ]] && tty_source=/dev/tty + + echo + echo "[olm] Images to build and push:" + list_bundle_images " - " + echo + + read -r -p "Build and push these images? [y/N] " answer <"$tty_source" + + case "$answer" in + y|Y|yes|YES) ;; + *) + echo "[olm] Build and push skipped" + exit 0 + ;; + esac +} + +write_bundle_dockerfile() { + local bundle_dir="$1" + local annotations="${bundle_dir}/metadata/annotations.yaml" + + [[ -f "$annotations" ]] || + die "bundle annotations not found: ${annotations}" + + [[ -d "${bundle_dir}/manifests" ]] || + die "bundle manifests not found: ${bundle_dir}/manifests" + + { + echo "FROM scratch" + + yq -o=json '.annotations' "$annotations" | + jq -r ' + to_entries[] | + "LABEL \(.key)=\(.value | tostring | @json)" + ' + + echo + echo "COPY manifests/ /manifests/" + echo "COPY metadata/ /metadata/" + } >"${bundle_dir}/Dockerfile" +} + +build_previous_bundle() { + local version="$1" + local bundle_dir="${OPERATORHUB_DIR}/${OPERATOR_PATH}/${version}" + local image + + image="$(previous_bundle_image "$version")" + + echo "[olm] Building and pushing bundle ${image}" + + write_bundle_dockerfile "$bundle_dir" + + "$CONTAINER" buildx build \ + --platform "$CATALOG_PLATFORM" \ + --tag "$image" \ + --push \ + "$bundle_dir" +} + +write_catalog_template() { + { + echo "Schema: olm.semver" + echo "GenerateMajorChannels: false" + echo "GenerateMinorChannels: false" + echo "Stable:" + echo " Bundles:" + list_bundle_images " - Image: " + } >"$CATALOG_TEMPLATE" +} + +build_catalog() { + local image + + image="$(catalog_image)" + + mkdir -p "$CATALOG_DIR" + write_catalog_template + + echo "[olm] Catalog bundles:" + list_bundle_images " - " + echo "[olm] Rendering ${BUNDLE_TYPE} catalog" + + opm alpha render-template semver \ + -o yaml \ + <"$CATALOG_TEMPLATE" \ + >"${CATALOG_DIR}/catalog.yaml" + + echo "[olm] Validating ${BUNDLE_TYPE} catalog" + opm validate "$CATALOG_DIR" + + echo "[olm] Generating catalog Dockerfile" + opm generate dockerfile "$CATALOG_DIR" + + echo "[olm] Building and pushing catalog ${image}" + + "$CONTAINER" buildx build \ + --platform "$CATALOG_PLATFORM" \ + --file "${CATALOG_DIR}.Dockerfile" \ + --tag "$image" \ + --push \ + "$CATALOG_ROOT" + + echo "[olm] Catalog image pushed: ${image}" +} + +run_build() { + local version + + BUNDLE_IMAGE_VERSION="${BUNDLE_IMAGE_VERSION:-}" + [[ -n "$CATALOG_REPO" ]] || die "catalog repository is required" + [[ -n "$BUNDLE_REPO" ]] || die "bundle repository is required" + [[ -n "$BUNDLE_IMAGE_VERSION" ]] || die "BUNDLE_IMAGE_VERSION is required" + [[ "$CATALOG_BUNDLE_LIMIT" =~ ^[0-9]+$ ]] || + die "CATALOG_BUNDLE_LIMIT must be a non-negative integer" + + for command in curl tar jq yq opm "$CONTAINER"; do + require "$command" + done + + TMP_DIR="$(mktemp -d)" + OPERATORHUB_DIR="${TMP_DIR}/operatorhub" + CATALOG_ROOT="${TMP_DIR}/catalog-build" + CATALOG_DIR="${CATALOG_ROOT}/catalog" + CATALOG_TEMPLATE="${TMP_DIR}/catalog-template.yaml" + + trap 'rm -rf "$TMP_DIR"' EXIT + + download_operatorhub + + OLD_VERSIONS=() + + while IFS= read -r version; do + [[ -n "$version" ]] && OLD_VERSIONS+=("$version") + done < <(find_previous_versions) + + [[ "${#OLD_VERSIONS[@]}" -eq "$CATALOG_BUNDLE_LIMIT" ]] || + die "expected ${CATALOG_BUNDLE_LIMIT} previous versions, found ${#OLD_VERSIONS[@]}" + + confirm_build + + for version in "${OLD_VERSIONS[@]}"; do + build_previous_bundle "$version" + done + + build_catalog +} + +run_apply() { + local source_name + local image + + require kubectl + [[ -n "$CATALOG_REPO" ]] || die "catalog repository is required" + + source_name="$(catalog_source_name)" + image="$(catalog_image)" + + echo "[olm] Applying CatalogSource ${source_name}" + echo "[olm] Namespace: ${CATALOG_NAMESPACE}" + echo "[olm] Image: ${image}" + + kubectl apply -f - <- Percona Distribution for MongoDB Operator automates the creation, modification, or deletion of items in your Percona Server for MongoDB environment createdAt: "" @@ -38,52 +40,60 @@ spec: description: |- ## Percona is Cloud Native - The Percona Distribution for MongoDB Kubernetes Operator automates the creation, modification, or deletion of items in your Percona Server for MongoDB environment. - The Operator contains the necessary Kubernetes settings to maintain a consistent Percona Server for MongoDB - instance modification, or deletion of items in your Percona Server for MongoDB environment. - The Operator contains the necessary Kubernetes settings to maintain a consistent Percona Server for MongoDB instance. + The Percona Distribution for MongoDB Kubernetes Operator automates the + creation, modification, or deletion of items in your Percona Server for + MongoDB environment. + The Operator contains the necessary Kubernetes settings to maintain a + consistent Percona Server for MongoDB instance. Consult the - [documentation](https://www.percona.com/doc/kubernetes-operator-for-psmongodb/index.html/) + [documentation](https://docs.percona.com/percona-operator-for-mongodb/index.html) on the Percona Kubernetes Operator for Percona Server for MongoDB for complete details on capabilities and options. ### Supported Features - + * **Scale Your Cluster** - change the `size` parameter to [add or remove members](https://www.percona.com/doc/kubernetes-operator-for-psmongodb/scaling.html) of the replica set. Three is the minimum recommended size for a functioning replica set. - - + * **Add Monitoring** - [Percona Monitoring and Management (PMM) can be easily deployed](https://www.percona.com/doc/kubernetes-operator-for-psmongodb/monitoring.html) to monitor your Percona Server for MongoDB replica set(s). The recommended installation process uses Helm, the package manager for Kubernetes. - - * **Automate Your Backups** - [configure automatic - backups](https://www.percona.com/doc/kubernetes-operator-for-psmongodb/backups.html) - to run on a scheduled basis or run an on-demand backup at any time. Backups - are performed using Percona Backup for MongoDB (PBM) and can be stored on - local PVs or in any S3-compatible cloud storage provider. - - * **Physical Backups** - [configure physical backups](https://docs.percona.com/percona-operator-for-mongodb/backups.html#physical) - - * **Automated volume expansion** - Kubernetes supports the Persistent Volume expansion as a stable feature since v1.24. Using it with the Operator - previously involved manual operations. Now this is automated, and users can resize their PVCs by just changing - the value of the resources.requests.storage option in the PerconaServerMongoDB custom resource. - This feature is in a technical preview stage and is not recommended for production environments. - - + * **Configure Backups** - [configure scheduled and on-demand + backups](https://docs.percona.com/percona-operator-for-mongodb/backups.html) + in the Custom Resource. Backups are performed using Percona Backup for + MongoDB (PBM), with support for logical, physical, and external PVC + snapshot backup types. Store backups in S3-compatible storage, Google + Cloud Storage with Workload Identity, Oracle Cloud Infrastructure Object + Storage, Alibaba Cloud OSS, Azure Blob Storage, or a remote file server. + + * **Automated Storage Scaling** - configure the Operator to monitor + Persistent Volume Claim usage and automatically expand storage when usage + reaches configured thresholds. Manual volume expansion is also supported + by changing resources.requests.storage in the PerconaServerMongoDB custom + resource. + + * **Real-time replication and migration** - use + [Percona ClusterSync for MongoDB](https://docs.percona.com/percona-operator-for-mongodb/clustersync.html) + for near-zero downtime migrations and live replication between MongoDB + deployments. + + * **TLS Certificate Management** - [integrate with + cert-manager](https://docs.percona.com/percona-operator-for-mongodb/tls-cert-manager.html) + and tune TLS certificate management behavior. + + ### Common Configurations - - + + * **Set Member as Arbiter** - [Set up a replica set which contains an arbiter](https://www.percona.com/doc/kubernetes-operator-for-psmongodb/arbiter.html), which participates in elections but does not store any data. This reduces @@ -94,27 +104,25 @@ spec: ServiceType](https://www.percona.com/doc/kubernetes-operator-for-psmongodb/expose.html) you can expose replica set members outside of Kubernetes or provide statically assigned IP addresses. - - + + * **Utilize Local Storage Options** - [with support for Local Storage you can mount existing data directories](https://www.percona.com/doc/kubernetes-operator-for-psmongodb/storage.html) into your replica set managed by Kubernetes or utilize high performance hardware for local storage rather than network storage for your database. - - + + ### Before You Start - - + + Add the PSMDB user `Secret` to Kubernetes. User information must be placed in the data section of the `secrets.yaml` - file with Base64-encoded logins and passwords for the user accounts. - - + + Below is a sample `secrets.yaml` file for the correct formatting. - - + ``` apiVersion: v1 kind: Secret @@ -139,11 +147,11 @@ spec: links: - name: Percona url: 'https://www.percona.com/' - - name: Percona Kubernetes Operators Landing Page - url: 'https://www.percona.com/software/percona-kubernetes-operators' - name: Documentation - url: 'https://docs.percona.com/percona-operator-for-mongodb/' - - name: Github + url: 'https://docs.percona.com/percona-operator-for-mongodb/index.html' + - name: Cloud Native Landing Page + url: 'https://www.percona.com/cloud-native/' + - name: GitHub url: 'https://github.com/percona/percona-server-mongodb-operator' maintainers: - name: Percona @@ -154,85 +162,7 @@ spec: mediatype: image/svg+xml customresourcedefinitions: - owned: - - description: Instance of a Percona Server for MongoDB replica set - displayName: PerconaServerMongoDB - kind: PerconaServerMongoDB - name: perconaservermongodbs.psmdb.percona.com - version: v1 - specDescriptors: [ ] - statusDescriptors: [ ] - resources: - - version: v1 - kind: Deployment - name: '' - - version: v1 - kind: Service - name: '' - - version: v1 - kind: ReplicaSet - name: '' - - version: v1 - kind: Pod - name: '' - - version: v1 - kind: Secret - name: '' - - version: v1 - kind: ConfigMap - name: '' - - description: Instance of a Percona Server for MongoDB Backup - displayName: PerconaServerMongoDBBackup - kind: PerconaServerMongoDBBackup - name: perconaservermongodbbackups.psmdb.percona.com - version: v1 - specDescriptors: [ ] - statusDescriptors: [ ] - resources: - - version: v1 - kind: Deployment - name: '' - - version: v1 - kind: Service - name: '' - - version: v1 - kind: ReplicaSet - name: '' - - version: v1 - kind: Pod - name: '' - - version: v1 - kind: Secret - name: '' - - version: v1 - kind: ConfigMap - name: '' - - description: Instance of a Percona Server for MongoDB Restore - displayName: PerconaServerMongoDBRestore - kind: PerconaServerMongoDBRestore - name: perconaservermongodbrestores.psmdb.percona.com - version: v1 - specDescriptors: [ ] - statusDescriptors: [ ] - resources: - - version: v1 - kind: Deployment - name: '' - - version: v1 - kind: Service - name: '' - - version: v1 - kind: ReplicaSet - name: '' - - version: v1 - kind: Pod - name: '' - - version: v1 - kind: Secret - name: '' - - version: v1 - kind: ConfigMap - name: '' + owned: [ ] required: [ ] install: strategy: deployment @@ -250,4 +180,4 @@ spec: strategy: deployment spec: permissions: - deployments: \ No newline at end of file + deployments: diff --git a/installers/olm/bundle.relatedImages.yaml b/installers/olm/bundle.relatedImages.yaml deleted file mode 100644 index 6036e5a2d7..0000000000 --- a/installers/olm/bundle.relatedImages.yaml +++ /dev/null @@ -1,16 +0,0 @@ -- name: mongod8.0 - image: registry.connect.redhat.com/percona/percona-server-mongodb-operator-containers@sha256: -- name: mongod7.0 - image: registry.connect.redhat.com/percona/percona-server-mongodb-operator-containers@sha256: -- name: mongod6.0 - image: registry.connect.redhat.com/percona/percona-server-mongodb-operator-containers@sha256: -- name: backup - image: registry.connect.redhat.com/percona/percona-server-mongodb-operator-containers@sha256: -- name: pmm - image: registry.connect.redhat.com/percona/percona-server-mongodb-operator-containers@sha256: -- name: pmm3 - image: registry.connect.redhat.com/percona/percona-server-mongodb-operator-containers@sha256: -- name: logcollector - image: registry.connect.redhat.com/percona/percona-server-mongodb-operator-containers@sha256: -- name: operator - image: registry.connect.redhat.com/percona/percona-server-mongodb-operator@sha256: diff --git a/installers/olm/distributions/community.sh b/installers/olm/distributions/community.sh new file mode 100644 index 0000000000..7a5c89dd70 --- /dev/null +++ b/installers/olm/distributions/community.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +set_release_image_ref() { + local variable="$1" + local key="$2" + local image + + image="$(release_version_value "${key}")" || return + + case "${image}" in + *.*/*|*:*/*|localhost/*) printf -v "${variable}" '%s' "${image}" ;; + *) printf -v "${variable}" 'docker.io/%s' "${image}" ;; + esac +} + +build_distribution_data() { + local operator backup logcollector mongod pmm clustersync + local image_ref + + for image_ref in \ + "operator IMAGE_OPERATOR" \ + "backup IMAGE_BACKUP" \ + "logcollector IMAGE_LOGCOLLECTOR" \ + "mongod IMAGE_MONGOD80" \ + "pmm IMAGE_PMM3_CLIENT" \ + "clustersync IMAGE_CLUSTERSYNC"; do + set_release_image_ref ${image_ref} || return + done + + jq -nc \ + --arg operator "${operator}" \ + --arg backup "${backup}" \ + --arg logcollector "${logcollector}" \ + --arg mongod "${mongod}" \ + --arg pmm "${pmm}" \ + --arg clustersync "${clustersync}" \ + '{ + images: + ({ + operator: $operator, + backup: $backup, + logcollector: $logcollector, + "mongod8.0": $mongod, + pmm3: $pmm + } + + (if $clustersync == "" then {} else {clustersync: $clustersync} end)) + }' +} + +distribution_package_name() { + echo -n "${component_name}" +} + +customize_csv() { + yq -P eval --inplace \ + '.metadata.annotations["olm.skipRange"] = env(skip_range)' \ + "$1" +} diff --git a/installers/olm/distributions/redhat.sh b/installers/olm/distributions/redhat.sh new file mode 100755 index 0000000000..1417f52aa2 --- /dev/null +++ b/installers/olm/distributions/redhat.sh @@ -0,0 +1,323 @@ +#!/usr/bin/env bash +set -euo pipefail + +# shellcheck disable=SC2016 +redhat_release="${VERSION}" +redhat_skips_min_version="${REDHAT_SKIPS_MIN_VERSION:-1.17.0}" +redhat_registry="${REDHAT_REGISTRY:-registry.connect.redhat.com}" +redhat_catalog_api="${REDHAT_CATALOG_API:-https://catalog.redhat.com/api/containers/v1}" +redhat_catalog_curl_timeout="${REDHAT_CATALOG_CURL_TIMEOUT:-20}" +redhat_operator_repository="${REDHAT_OPERATOR_REPOSITORY:-percona/percona-server-mongodb-operator}" +redhat_containers_repository="${REDHAT_CONTAINERS_REPOSITORY:-percona/percona-server-mongodb-operator-containers}" +redhat_operator_tag="${REDHAT_OPERATOR_TAG:-${redhat_release}}" +redhat_related_images="[]" +redhat_missing_digests=() +release_versions_file="${repo_root}/e2e-tests/release_versions" + +image_tag() { + local image="$1" + + echo -n "${image##*:}" +} + +digest_key() { + echo -n "$1" \ + | "$sed" -E 's/[^[:alnum:]]+/_/g' \ + | tr '[:lower:]' '[:upper:]' +} + +catalog_digest() { + local repository="$1" + local tag="$2" + local digest + + debug "Resolving Red Hat digest for ${redhat_registry}/${repository}:${tag}" + + digest="$( + curl -fsSL \ + --connect-timeout 5 \ + --max-time "${redhat_catalog_curl_timeout}" \ + "${redhat_catalog_api}/repositories/registry/${redhat_registry}/repository/${repository}/tag/${tag}" \ + 2>/dev/null \ + | jq -er '.docker_image_digest // .data.docker_image_digest // .data[0].docker_image_digest' 2>/dev/null + )" || digest="$( + curl -fsSL \ + --connect-timeout 5 \ + --max-time "${redhat_catalog_curl_timeout}" \ + "${redhat_catalog_api}/repositories/registry/${redhat_registry}/repository/${repository}/images?page_size=500" \ + 2>/dev/null \ + | jq -er \ + --arg tag "${tag}" \ + 'first(.data[] | select(any(.repositories[]?.tags[]?; .name == $tag)) | .docker_image_digest)' \ + 2>/dev/null + )" || digest="" + + if [[ -n "${digest}" && "${digest}" != "null" ]]; then + echo -n "sha256:${digest#sha256:}" + return + fi + + return 1 +} + +set_image_ref() { + local key="$1" + local name="$2" + local repository="$3" + local tag="$4" + local digest_var + local digest + + digest_var="REDHAT_IMAGE_DIGEST_$(digest_key "${key}")" + digest="${!digest_var:-}" + + if [[ -z "${digest}" ]]; then + digest="$(catalog_digest "${repository}" "${tag}")" || digest="" + fi + + if [[ -z "${digest}" ]]; then + if [[ "${BUNDLE_SKIP_DIGEST_FAILURE:-0}" != "1" ]]; then + abort "unable to resolve digest for ${redhat_registry}/${repository}:${tag}; set BUNDLE_SKIP_DIGEST_FAILURE=1 to continue with " + fi + + digest="" + redhat_missing_digests+=("${name}:${redhat_registry}/${repository}:${tag}") + fi + + if [[ "${digest}" != "" ]]; then + digest="sha256:${digest#sha256:}" + fi + + echo -n "${redhat_registry}/${repository}@${digest}" +} + +validate_certified_tag() { + local key="$1" + local tag="$2" + local source_tag="${3:-}" + local expected="" + + case "${key}" in + IMAGE_OPERATOR) + expected="${redhat_release}" + ;; + IMAGE_MONGOD60|IMAGE_MONGOD70|IMAGE_MONGOD80) + expected="${redhat_release}-psmdb-${source_tag}" + ;; + IMAGE_BACKUP) + expected="${redhat_release}-backup" + ;; + IMAGE_PMM_CLIENT) + expected="${redhat_release}-pmm" + ;; + IMAGE_PMM3_CLIENT) + expected="${redhat_release}-pmm3" + ;; + IMAGE_LOGCOLLECTOR) + expected="${redhat_release}-logcollector-${source_tag}" + ;; + IMAGE_CLUSTERSYNC) + expected="${redhat_release}-clustersync" + ;; + *) + abort "unsupported certified image key: ${key}" + ;; + esac + + [[ "${tag}" == "${expected}" ]] \ + || abort "invalid Red Hat tag for ${key}: got '${tag}', expected '${expected}'" +} + +add_related_image() { + local key="$1" + local name="$2" + local repository="$3" + local tag="$4" + local source_tag="${5:-}" + local image + + validate_certified_tag "${key}" "${tag}" "${source_tag}" + + image="$(set_image_ref "${key}" "${name}" "${repository}" "${tag}")" + + log "Related image ${name}: ${image}" + + redhat_related_images="$( + jq -c \ + --arg name "${name}" \ + --arg image "${image}" \ + '. + [{ name: $name, image: $image }]' \ + <<<"${redhat_related_images}" + )" +} + +related_image_by_name() { + local name="$1" + + jq --raw-output \ + --arg name "${name}" \ + 'map(select(.name == $name)) | last.image // ""' \ + <<<"${redhat_related_images}" +} + +require_release_image() { + local key="$1" + + if [[ -z "${!key:-}" ]]; then + abort "${key} is required in ${release_versions_file}" + fi +} + +report_missing_digests() { + local item + + [[ "${#redhat_missing_digests[@]}" -eq 0 ]] && return + + log "Digest resolution failed for the following image(s); was used because BUNDLE_SKIP_DIGEST_FAILURE is enabled:" + for item in "${redhat_missing_digests[@]}"; do + log " - ${item}" + done +} + +build_redhat_related_images() { + local mongod60_tag + local mongod70_tag + local mongod80_tag + local logcollector_tag + + log "Building Red Hat related images from ${release_versions_file}" + + [[ -f "${release_versions_file}" ]] \ + || abort "release versions file not found: ${release_versions_file}" + + # shellcheck source=/dev/null + source "${release_versions_file}" + + for key in \ + IMAGE_OPERATOR \ + IMAGE_MONGOD60 \ + IMAGE_MONGOD70 \ + IMAGE_MONGOD80 \ + IMAGE_BACKUP \ + IMAGE_PMM_CLIENT \ + IMAGE_PMM3_CLIENT \ + IMAGE_LOGCOLLECTOR \ + IMAGE_CLUSTERSYNC; do + require_release_image "${key}" + done + + mongod60_tag="$(image_tag "${IMAGE_MONGOD60}")" + mongod70_tag="$(image_tag "${IMAGE_MONGOD70}")" + mongod80_tag="$(image_tag "${IMAGE_MONGOD80}")" + logcollector_tag="$(image_tag "${IMAGE_LOGCOLLECTOR}")" + + add_related_image "IMAGE_MONGOD80" "mongod8.0" "${redhat_containers_repository}" "${redhat_release}-psmdb-${mongod80_tag}" "${mongod80_tag}" + add_related_image "IMAGE_MONGOD70" "mongod7.0" "${redhat_containers_repository}" "${redhat_release}-psmdb-${mongod70_tag}" "${mongod70_tag}" + add_related_image "IMAGE_MONGOD60" "mongod6.0" "${redhat_containers_repository}" "${redhat_release}-psmdb-${mongod60_tag}" "${mongod60_tag}" + add_related_image "IMAGE_BACKUP" "backup" "${redhat_containers_repository}" "${redhat_release}-backup" + add_related_image "IMAGE_PMM_CLIENT" "pmm" "${redhat_containers_repository}" "${redhat_release}-pmm" + add_related_image "IMAGE_PMM3_CLIENT" "pmm3" "${redhat_containers_repository}" "${redhat_release}-pmm3" + add_related_image "IMAGE_CLUSTERSYNC" "clustersync" "${redhat_containers_repository}" "${redhat_release}-clustersync" + add_related_image "IMAGE_LOGCOLLECTOR" "logcollector" "${redhat_containers_repository}" "${redhat_release}-logcollector-${logcollector_tag}" "${logcollector_tag}" + add_related_image "IMAGE_OPERATOR" "operator" "${redhat_operator_repository}" "${redhat_operator_tag}" + + report_missing_digests + + jq -nc \ + --arg operator_image "$(related_image_by_name operator)" \ + --argjson related_images "${redhat_related_images}" \ + '{ + operatorImage: $operator_image, + relatedImages: $related_images + }' +} + +build_redhat_skips() { + local min_version="${redhat_skips_min_version}" + local current_version="v${redhat_release#v}" + + min_version="v${min_version#v}" + + log "Building Red Hat skips from ${min_version} up to ${current_version}" + + git -C "${repo_root}" tag --list 'v*' \ + | jq -Rsc \ + --arg min_version "${min_version}" \ + --arg current_version "${current_version}" \ + --arg package_name "${component_name}-certified" \ + ' + def version_parts: + ltrimstr("v") + | split(".") + | map(tonumber); + + ($min_version | version_parts) as $min + | ($current_version | version_parts) as $current + | split("\n") + | map(select(length > 0)) + | map(select(test("^v[0-9]+\\.[0-9]+\\.[0-9]+$"))) + | map({ + tag: ., + version: version_parts + }) + | map( + select( + .version >= $min + and .version < $current + ) + ) + | sort_by(.version) + | map($package_name + "." + .tag) + ' +} + +distribution_package_name() { + echo -n "${component_name}-certified" +} + +customize_csv() { + yq -P eval --inplace ' + .spec.relatedImages = (strenv(relatedImages) | from_json) | + .spec.skips = (strenv(skips) | from_json) | + .metadata.annotations.certified = "true" | + .metadata.annotations["features.operators.openshift.io/disconnected"] = "true" | + .metadata.name = strenv(name_certified) + ' "$1" +} + +build_distribution_data() { + local redhat_data + local images + local related_images + local skips + + redhat_data="$(build_redhat_related_images)" || return + + jq -e \ + '.operatorImage and (.relatedImages | type == "array")' \ + >/dev/null \ + <<<"${redhat_data}" \ + || abort "Invalid Red Hat image data" + + images="$( + jq -c ' + .operatorImage as $operator + | reduce .relatedImages[] as $item + ({}; .[$item.name] = $item.image) + | .operator = $operator + ' <<<"${redhat_data}" + )" + + related_images="$(jq -c '.relatedImages' <<<"${redhat_data}")" + skips="$(build_redhat_skips)" + + jq -nc \ + --argjson images "${images}" \ + --argjson related_images "${related_images}" \ + --argjson skips "${skips}" \ + '{ + images: $images, + relatedImages: $related_images, + skips: $skips + }' +} diff --git a/installers/olm/generate.sh b/installers/olm/generate.sh index e5964dd35a..da5bcc85f1 100755 --- a/installers/olm/generate.sh +++ b/installers/olm/generate.sh @@ -1,182 +1,262 @@ #!/usr/bin/env bash +set -euo pipefail +shopt -s inherit_errexit 2>/dev/null || true -# Install -# brew install gawk coreutils -for command in gawk gcsplit; do - if ! command -v $command &>/dev/null; then - echo "Error: $command is not installed. Please install it: brew install $command" >&2 - exit 1 - fi -done - -set -eu - -DISTRIBUTION="$1" +DISTRIBUTION="${1:?Distribution argument required (community|redhat)}" cd "${BASH_SOURCE[0]%/*}" -bundle_directory="bundles/${DISTRIBUTION}" -project_directory="projects/${DISTRIBUTION}" -go_api_directory=$(cd ../../pkg/apis && pwd) +sed=$(command -v gsed || command -v sed) +date=$(command -v gdate || command -v date) +repo_root="$(cd ../.. && pwd)" +release_versions_file="${repo_root}/e2e-tests/release_versions" +bundle_name="${BUNDLE_NAME:-${DISTRIBUTION}}" +bundle_directory="bundles/${bundle_name}" +project_directory="projects/${bundle_name}" +go_api_directory="$(cd ../../pkg/apis && pwd)" -# The 'operators.operatorframework.io.bundle.package.v1' package name for each -# bundle (updated for the 'certified' and 'marketplace' bundles). -package_name='percona-server-mongodb-operator' - -# The project name used by operator-sdk for initial bundle generation. -project_name='percona-server-mongodb-operator' - -# The prefix for the 'clusterserviceversion.yaml' file. -# Per OLM guidance, the filename for the clusterserviceversion.yaml must be prefixed -# with the Operator's package name for the 'redhat' and 'marketplace' bundles. -# https://github.com/redhat-openshift-ecosystem/certification-releases/blob/main/4.9/ga/troubleshooting.md#get-supported-versions -file_name='percona-server-mongodb-operator' +# Used both as the operator-sdk project name and as the CSV file stem. +component_name="percona-server-mongodb-operator" NS_RESOURCE_RBAC="../rbac/namespace" -CLUSTER_RESOURCE_RBAC="../rbac/cluster" NS_RESOURCE_OPERATOR="../manager/namespace" -CLUSTER_RESOURCE_OPERATOR="../manager/cluster" KUSTOMIZATION_FILE="../../config/bundle/kustomization.yaml" -if [ "${MODE}" == "cluster" ]; then - suffix="-cw" - mode="Cluster" - rulesLevel="ClusterPermissions" - sed -i '' "s|$NS_RESOURCE_RBAC|$CLUSTER_RESOURCE_RBAC|g" "$KUSTOMIZATION_FILE" - sed -i '' "s|$NS_RESOURCE_OPERATOR|$CLUSTER_RESOURCE_OPERATOR|g" "$KUSTOMIZATION_FILE" -elif [ "${MODE}" == "namespace" ]; then - suffix="" - mode="" - rulesLevel="permissions" - sed -i '' "s|$CLUSTER_RESOURCE_RBAC|$NS_RESOURCE_RBAC|g" "$KUSTOMIZATION_FILE" - sed -i '' "s|$CLUSTER_RESOURCE_OPERATOR|$NS_RESOURCE_OPERATOR|g" "$KUSTOMIZATION_FILE" -else - echo "Please add MODE variable. It could be either namespace or cluster" +relatedImages="[]" +skips="[]" +containerImage="" +csv_stem="" +distribution_data="{}" + +log() { + echo >&2 "[olm] $*" +} + +abort() { + echo >&2 "[olm] ERROR: $*" exit 1 -fi -# Copy operator file to config: -cp ../../deploy/operator.yaml ../../config/manager/namespace -cp ../../deploy/cw-operator.yaml ../../config/manager/cluster - -# Copy RBAC: -gcsplit --elide-empty-files -f output- ../../deploy/rbac.yaml "/^---$/" "{*}" -target_dir="../../config/rbac/namespace" -mv output-00 "$target_dir/role.yaml" -mv output-01 "$target_dir/service_account.yaml" -mv output-02 "$target_dir/role_binding.yaml" - -# Copy RBAC for CW: -gcsplit --elide-empty-files -f output- ../../deploy/cw-rbac.yaml "/^---$/" "{*}" -target_dir="../../config/rbac/cluster" -mv output-00 "$target_dir/role.yaml" -mv output-01 "$target_dir/service_account.yaml" -mv output-02 "$target_dir/role_binding.yaml" - -kubectl kustomize "../../config/${DISTRIBUTION}" >operator_yamls.yaml - -export role="${mode}Role" - -update_yaml_images() { - local yaml_file="$1" - - if [ ! -f "$yaml_file" ]; then - echo "Error: File '$yaml_file' does not exist." - return 1 +} + +debug() { + if [[ "${OLM_VERBOSE:-0}" == "1" || "${OLM_VERBOSE:-false}" == "true" ]]; then + log "$@" + fi +} + +run_quiet() { + local description="$1" + shift + + local output_path="" + if [[ "$1" == "-o" ]]; then + output_path="$2" + shift 2 + fi + + if [[ "${OLM_VERBOSE:-0}" == "1" || "${OLM_VERBOSE:-false}" == "true" ]]; then + if [[ -n "${output_path}" ]]; then + "$@" >"${output_path}" + else + "$@" + fi + return + fi + + local capture_file + capture_file="$(mktemp)" + + if [[ -n "${output_path}" ]]; then + "$@" >"${output_path}" 2>"${capture_file}" && { rm -f "${capture_file}"; return; } + else + "$@" >"${capture_file}" 2>&1 && { rm -f "${capture_file}"; return; } + fi + + cat "${capture_file}" >&2 + rm -f "${capture_file}" + abort "${description} failed" +} + +require() { + if [ $# -eq 1 ]; then + command -v "$1" >/dev/null 2>&1 \ + || abort "$1 not found in PATH" + else + "$@" >/dev/null 2>&1 \ + || abort "Failed running: $*" fi +} + +sed_in_place() { + local expression="$1" + local file="$2" + local tmp_file + + tmp_file="$(mktemp)" + "$sed" "$expression" "$file" >"$tmp_file" + mv "$tmp_file" "$file" +} + +check_tools() { + local command + + for command in gawk gcsplit yq jq kubectl operator-sdk yamllint envsubst; do + require "$command" + done +} + +release_version_value() { + local key="$1" + + local version + version="$(awk -F= -v key="${key}" '$1 == key { print $2 }' "${release_versions_file}" \ + | tr -d '"' \ + | tail -1)" + + if [[ -z "${version}" ]]; then + abort "Missing ${key} in ${release_versions_file}" + fi + + echo -n "${version}" +} + +resolve_openshift_versions() { + local openshift_min + local openshift_max + + if [[ -n "${OPENSHIFT_VERSIONS:-}" ]]; then + echo -n "${OPENSHIFT_VERSIONS}" + return + fi + + [[ -f "${release_versions_file}" ]] \ + || abort "OPENSHIFT_VERSIONS is not set and ${release_versions_file} does not exist" + + openshift_min="$(release_version_value "OPENSHIFT_MIN" | awk -F. '{ print "v" $1 "." $2 }')" + openshift_max="$(release_version_value "OPENSHIFT_MAX" | awk -F. '{ print "v" $1 "." $2 }')" + + [[ -n "${openshift_min}" && -n "${openshift_max}" ]] \ + || abort "OPENSHIFT_MIN and OPENSHIFT_MAX must be set in ${release_versions_file}" + + echo -n "${openshift_min}-${openshift_max}" +} + +load_distribution_hooks() { + local hook_file="distributions/${DISTRIBUTION}.sh" + + [[ "${DISTRIBUTION}" == "community" || "${DISTRIBUTION}" == "redhat" ]] \ + || abort "Unknown distribution: ${DISTRIBUTION}" + [[ -f "${hook_file}" ]] || abort "Distribution hooks not found: ${hook_file}" + + log "Loading distribution hooks from ${hook_file}" + # shellcheck source=/dev/null + source "${hook_file}" - local temp_file - temp_file=$(mktemp) + for hook in build_distribution_data distribution_package_name customize_csv; do + declare -F "${hook}" >/dev/null || abort "Missing distribution hook: ${hook}" + done +} + +configure_namespace_manifests() { + sed_in_place "s|../rbac/cluster|$NS_RESOURCE_RBAC|g" "$KUSTOMIZATION_FILE" + sed_in_place "s|../manager/cluster|$NS_RESOURCE_OPERATOR|g" "$KUSTOMIZATION_FILE" +} + +prepare_operator_sources() { + log "Preparing namespace-scoped operator manifests" + + cp ../../deploy/operator.yaml ../../config/manager/namespace + + gcsplit --elide-empty-files -f output- ../../deploy/rbac.yaml "/^---$/" "{*}" >/dev/null + mv output-00 ../../config/rbac/namespace/role.yaml + mv output-01 ../../config/rbac/namespace/service_account.yaml + mv output-02 ../../config/rbac/namespace/role_binding.yaml +} + +render_operator_manifests() { + log "Rendering operator manifests for ${DISTRIBUTION}" + + run_quiet "Rendering operator manifests" -o operator_yamls.yaml \ + kubectl kustomize "../../config/${DISTRIBUTION}" + + yq eval '. | select(.kind == "CustomResourceDefinition")' operator_yamls.yaml >operator_crds.yaml + yq eval '. | select(.kind == "Deployment")' operator_yamls.yaml >operator_deployments.yaml + yq eval '. | select(.kind == "ServiceAccount")' operator_yamls.yaml >operator_accounts.yaml + yq eval '. | select(.kind == "Role")' operator_yamls.yaml >operator_roles.yaml +} + +create_sdk_workspace() { + log "Creating Operator SDK workspace" - sed -E 's/(("image":|"initImage":|containerImage:|image:|initImage:)[ ]*"?)([^"]+)("?)/\1docker.io\/\3\4/g' "$yaml_file" >"$temp_file" - mv "$temp_file" "$yaml_file" + rm -rf "${project_directory}" + install -d "${project_directory}" - echo "File '$yaml_file' updated successfully." + ( + cd "${project_directory}" + run_quiet "Creating Operator SDK workspace" \ + operator-sdk init --fetch-deps="false" --project-name="${component_name}" + + yq eval '[. | {"group": .spec.group, "kind": .spec.names.kind, "version": .spec.versions[].name}]' \ + ../../../../deploy/crd.yaml >crd_gvks.yaml + + yq eval --inplace '.multigroup = true | .resources = load("crd_gvks.yaml" | fromyaml) | .' ./PROJECT + + ln -s "${go_api_directory}" . + run_quiet "Generating Operator SDK kustomize manifests" \ + operator-sdk generate kustomize manifests --interactive="false" + ) +} + +create_bundle_directory() { + log "Creating bundle directory ${bundle_directory}" + + rm -rf "${bundle_directory}" + install -d \ + "${bundle_directory}/manifests" \ + "${bundle_directory}/metadata" } -yq eval '. | select(.kind == "CustomResourceDefinition")' operator_yamls.yaml >operator_crds.yaml -yq eval '. | select(.kind == "Deployment")' operator_yamls.yaml >operator_deployments.yaml -yq eval '. | select(.kind == "ServiceAccount")' operator_yamls.yaml >operator_accounts.yaml -yq eval '. | select(.kind == env(role))' operator_yamls.yaml >operator_roles${suffix}.yaml - -## Recreate the Operator SDK project. - -[ ! -d "${project_directory}" ] || rm -r "${project_directory}" -install -d "${project_directory}" -( - cd "${project_directory}" - operator-sdk init --fetch-deps='false' --project-name=${project_name} - - # Generate CRD descriptions from Go markers. - # https://sdk.operatorframework.io/docs/building-operators/golang/references/markers/ - yq eval '[. | {"group": .spec.group, "kind": .spec.names.kind, "version": .spec.versions[].name}]' ../../../../deploy/crd.yaml >crd_gvks.yaml - - yq eval --inplace '.multigroup = true | .resources = load("crd_gvks.yaml" | fromyaml) | .' ./PROJECT - - ln -s "${go_api_directory}" . - operator-sdk generate kustomize manifests --interactive='false' --verbose -) - -# Recreate the OLM bundle. -[ ! -d "${bundle_directory}" ] || rm -r "${bundle_directory}" -install -d \ - "${bundle_directory}/manifests" \ - "${bundle_directory}/metadata" - -# Render bundle annotations and strip comments. -# Per Red Hat we should not include the org.opencontainers annotations in the -# 'redhat' & 'marketplace' annotations.yaml file, so only add them for 'community'. -# - https://coreos.slack.com/team/UP1LZCC1Y - -export package="${package_name}" -export package_channel="${PACKAGE_CHANNEL}${suffix}" -export openshift_supported_versions="${OPENSHIFT_VERSIONS}" - -yq eval '.annotations["operators.operatorframework.io.bundle.channels.v1"] = env(package_channel) | - .annotations["operators.operatorframework.io.bundle.channel.default.v1"] = env(package_channel) | - .annotations["com.redhat.openshift.versions"] = env(openshift_supported_versions)' \ - bundle.annotations.yaml >"${bundle_directory}/metadata/annotations.yaml" - -if [ "${DISTRIBUTION}" == 'community' ]; then - # community-operators - yq eval --inplace ' - .annotations["operators.operatorframework.io.bundle.package.v1"] = "percona-server-mongodb-operator" | - .annotations["org.opencontainers.image.authors"] = "info@percona.com" | - .annotations["org.opencontainers.image.url"] = "https://percona.com" | - .annotations["org.opencontainers.image.vendor"] = "Percona"' \ - "${bundle_directory}/metadata/annotations.yaml" - -# certified-operators -elif [ "${DISTRIBUTION}" == 'redhat' ]; then - yq eval --inplace ' - .annotations["operators.operatorframework.io.bundle.package.v1"] = "percona-server-mongodb-operator-certified" ' \ - "${bundle_directory}/metadata/annotations.yaml" - -# redhat-marketplace -elif [ "${DISTRIBUTION}" == 'marketplace' ]; then - yq eval --inplace ' - .annotations["operators.operatorframework.io.bundle.package.v1"] = "percona-server-mongodb-operator-certified-rhmp" ' \ - "${bundle_directory}/metadata/annotations.yaml" -fi - -# Copy annotations into Dockerfile LABELs. - -labels=$(yq eval -r '.annotations | to_entries | map("LABEL " + .key + "=" + (.value | tojson)) | join("\n")' \ - "${bundle_directory}/metadata/annotations.yaml") - -labels="${labels} +render_bundle_metadata() { + log "Rendering bundle metadata" + + export package="${PACKAGE_NAME_OVERRIDE:-$(distribution_package_name)}" + export package_channel="${BUNDLE_PACKAGE_CHANNEL:-stable}" + export openshift_supported_versions + openshift_supported_versions="$(resolve_openshift_versions)" + + yq eval ' + .annotations["operators.operatorframework.io.bundle.channels.v1"] = env(package_channel) | + .annotations["operators.operatorframework.io.bundle.channel.default.v1"] = env(package_channel) | + .annotations["operators.operatorframework.io.bundle.package.v1"] = env(package) | + .annotations["com.redhat.openshift.versions"] = env(openshift_supported_versions) | + .annotations["org.opencontainers.image.authors"] = "info@percona.com" | + .annotations["org.opencontainers.image.url"] = "https://percona.com" | + .annotations["org.opencontainers.image.vendor"] = "Percona" + ' bundle.annotations.yaml >"${bundle_directory}/metadata/annotations.yaml" +} + +render_bundle_dockerfile() { + local labels + + labels="$(yq eval -r '.annotations | to_entries | map("LABEL " + .key + "=" + (.value | tojson)) | join("\n")' \ + "${bundle_directory}/metadata/annotations.yaml")" + + labels="${labels} LABEL com.redhat.delivery.backport=true LABEL com.redhat.delivery.operator.bundle=true" -echo "$labels" + LABELS="${labels}" envsubst "${bundle_directory}/Dockerfile" + awk '{gsub(/^[ \t]+/, " "); print}' "${bundle_directory}/Dockerfile" >"${bundle_directory}/Dockerfile.new" + mv "${bundle_directory}/Dockerfile.new" "${bundle_directory}/Dockerfile" +} -LABELS="${labels}" envsubst "${bundle_directory}/Dockerfile" +write_crd_manifests() { + local crd_names -awk '{gsub(/^[ \t]+/, " "); print}' "${bundle_directory}/Dockerfile" >"${bundle_directory}/Dockerfile.new" && mv "${bundle_directory}/Dockerfile.new" "${bundle_directory}/Dockerfile" + log "Writing CRD manifests" -# Include CRDs as manifests. -crd_names=$(yq eval -o=tsv '.metadata.name' ../../deploy/crd.yaml) + crd_names="$(yq eval -o=tsv '.metadata.name' ../../deploy/crd.yaml)" -gawk -v names="${crd_names}" -v bundle_directory="${bundle_directory}" ' + gawk -v names="${crd_names}" -v bundle_directory="${bundle_directory}" ' BEGIN { split(names, name_array, " "); idx=1; @@ -197,111 +277,266 @@ BEGIN { } ' ../../deploy/crd.yaml -find "${bundle_directory}/manifests" -type f -name "*.crd.yaml" -exec sed -i '' '1s/^/---\n/; ${/^---$/d;}' {} + + find "${bundle_directory}/manifests" -type f -name "*.crd.yaml" -print0 | while IFS= read -r -d '' file; do + # shellcheck disable=SC2016 + sed_in_place '1s/^/---\ +/; ${/^---$/d;}' "$file" + done +} + +validate_manifest_inputs() { + yq eval -i '[.]' operator_deployments.yaml + yq eval 'length == 1' operator_deployments.yaml --exit-status >/dev/null \ + || abort "expected exactly one deployment: $(yq eval . operator_deployments.yaml)" -abort() { - echo >&2 "$@" - exit 1 + yq eval -i '[.]' operator_accounts.yaml + yq eval 'length == 1' operator_accounts.yaml --exit-status >/dev/null \ + || abort "too many service accounts: $(yq eval . operator_accounts.yaml)" + + yq eval -i '[.]' operator_roles.yaml + yq eval 'length == 1' operator_roles.yaml --exit-status >/dev/null \ + || abort "too many roles: $(yq eval . operator_roles.yaml)" } -dump() { yq --color-output; } -# The first command render yaml correctly and the second extract data. +build_examples() { + local cr_example + local backup_example + local clustersync_example + local images + local restore_example + + images="$(jq -c '.images' <<<"${distribution_data}")" + + cr_example="$( + yq eval -o=json ../../deploy/cr.yaml | + jq \ + --argjson images "${images}" \ + ' + def insert_after($k; $new): + to_entries as $e + | reduce $e[] as $item ({}; + . + {($item.key): $item.value} + | if $item.key == $k then . + $new else . end + ); + + .spec |= ( + if has("initImage") then del(.initImage) else . end + | .image = $images["mongod8.0"] + | insert_after("image"; {"initImage": $images.operator}) + | if has("initImage") then . else . + {"initImage": $images.operator} end + | .pmm.image = $images.pmm3 + | .backup.image = $images.backup + | .logcollector.image = $images.logcollector + ) + ' + )" + + clustersync_example="null" + if jq -e '.clustersync? | strings | length > 0' >/dev/null <<<"${images}"; then + clustersync_example="$( + yq eval -o=json ../../deploy/clustersync.yaml | + jq -s \ + --argjson images "${images}" \ + ' + map( + select(.kind == "PerconaServerMongoDBClusterSync") + | .spec.image = $images.clustersync + ) + | first + ' + )" + fi + + backup_example="$(yq eval -o=json ../../deploy/backup/backup.yaml)" + restore_example="$(yq eval -o=json ../../deploy/backup/restore.yaml)" -yq eval -i '[.]' operator_deployments.yaml && yq eval 'length == 1' operator_deployments.yaml --exit-status >/dev/null || abort "too many deployments accounts!" $'\n'"$(yq eval . operator_deployments.yaml)" + jq -n "[${cr_example}, ${backup_example}, ${restore_example}, ${clustersync_example}] | map(select(. != null))" +} -yq eval -i '[.]' operator_accounts.yaml && yq eval 'length == 1' operator_accounts.yaml --exit-status >/dev/null || abort "too many service accounts!" $'\n'"$(yq eval . operator_accounts.yaml)" +build_managed_resources() { + yq eval -o=json '.' operator_roles.yaml | + jq ' + def kind: + { + "certificaterequests": "CertificateRequest", + "certificates": "Certificate", + "configmaps": "ConfigMap", + "cronjobs": "CronJob", + "deployments": "Deployment", + "issuers": "Issuer", + "persistentvolumeclaims": "PersistentVolumeClaim", + "poddisruptionbudgets": "PodDisruptionBudget", + "pods": "Pod", + "replicasets": "ReplicaSet", + "secrets": "Secret", + "serviceexports": "ServiceExport", + "serviceimports": "ServiceImport", + "services": "Service", + "statefulsets": "StatefulSet", + "volumesnapshots": "VolumeSnapshot" + }[.] // .; + + def version($apiGroup): + if $apiGroup == "" then "v1" + else $apiGroup + "/v1" + end; + + [ + (if type == "array" then . else [.] end)[].rules[] + | select((.verbs // []) | any(. == "create" or . == "update" or . == "patch" or . == "delete" or . == "deletecollection")) + | .apiGroups[] as $apiGroup + | select($apiGroup != "psmdb.percona.com") + | .resources[] + | select((contains("/") | not) and . != "events" and . != "leases") + | { + "version": version($apiGroup), + "kind": kind, + "name": "" + } + ] | unique_by(.version + "/" + .kind) | sort_by(.version, .kind) + ' +} -yq eval -i '[.]' operator_roles${suffix}.yaml && yq eval 'length == 1' operator_roles${suffix}.yaml --exit-status >/dev/null || abort "too many roles!" $'\n'"$(yq eval . operator_roles${suffix}.yaml)" +build_owned_crds() { + local managed_resources + + managed_resources="$(build_managed_resources)" + + yq eval-all -o=json '[.]' ../../deploy/crd.yaml | + jq --argjson managed_resources "${managed_resources}" ' + def crd_description: + { + "PerconaServerMongoDB": "Instance of a Percona Server for MongoDB replica set", + "PerconaServerMongoDBBackup": "Instance of a Percona Server for MongoDB Backup", + "PerconaServerMongoDBRestore": "Instance of a Percona Server for MongoDB Restore", + "PerconaServerMongoDBClusterSync": "Instance of a Percona Server for MongoDB Cluster Sync" + }[.spec.names.kind] // ("Instance of a " + .spec.names.kind); + + [ + .[] + | select(.kind == "CustomResourceDefinition") + | { + "description": crd_description, + "displayName": .spec.names.kind, + "kind": .spec.names.kind, + "name": .metadata.name, + "version": (.spec.versions[] | select(.storage == true) | .name), + "specDescriptors": [], + "statusDescriptors": [], + "resources": (if .spec.names.kind == "PerconaServerMongoDB" then $managed_resources else [] end) + } + ] + ' +} -# Render bundle CSV and strip comments. -csv_stem=$(yq -r '.projectName' "${project_directory}/PROJECT") +prepare_distribution() { + distribution_data="$(build_distribution_data)" || exit $? -deployment=$(yq eval operator_deployments.yaml) -containerImage="$(yq eval '.[0].spec.template.spec.containers[0].image' operator_deployments.yaml)" + jq -e ' + (.images | type == "object") and + ((["operator", "backup", "logcollector", "mongod8.0", "pmm3"] - (.images | keys)) | length == 0) + ' >/dev/null <<<"${distribution_data}" \ + || abort "Distribution data is missing required images" -# Include initImage in the example CR. -# Keep it adjacent to spec.image for readability. -cr_example=$( - yq eval -o=json ../../deploy/cr.yaml | - jq --arg initImage "$containerImage" ' - def insert_after($k; $new): - to_entries as $e - | reduce $e[] as $item ({}; - . + {($item.key): $item.value} - | if $item.key == $k then . + $new else . end - ); + containerImage="$(jq -er '.images.operator' <<<"${distribution_data}")" + relatedImages="$(jq -c '.relatedImages // []' <<<"${distribution_data}")" + skips="$(jq -c '.skips // []' <<<"${distribution_data}")" +} - .spec |= ( - if has("initImage") then del(.initImage) else . end - | insert_after("image"; {"initImage": $initImage}) - | if has("initImage") then . else . + {"initImage": $initImage} end - ) - ' -) -backup_example=$(yq eval -o=json ../../deploy/backup/backup.yaml) -restore_example=$(yq eval -o=json ../../deploy/backup/restore.yaml) -full_example=$(jq -n "[${cr_example}, ${backup_example}, ${restore_example}]") -account=$(yq eval '.[] | .metadata.name' operator_accounts.yaml) -rules=$(yq eval '.[] | .rules' operator_roles${suffix}.yaml) -version="${VERSION}${suffix}" - -timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - -relatedImages=$(yq eval bundle.relatedImages.yaml) - -export examples="${full_example}" -export deployment=$deployment -export account=$account -export rules=$rules -export version="${version}" -export stem="${csv_stem}" -export timestamp=$timestamp -export name="${csv_stem}.v${VERSION}${suffix}" -export name_certified="${csv_stem}-certified.v${VERSION}${suffix}" -export name_certified_rhmp="${csv_stem}-certified-rhmp.v${VERSION}${suffix}" -export skip_range="<${VERSION}" -export containerImage="$containerImage" -export relatedImages=$relatedImages -export rulesLevel=${rulesLevel} - -yq eval ' - .metadata.annotations["alm-examples"] = strenv(examples) | - .metadata.annotations["containerImage"] = env(containerImage) | - .metadata.annotations["olm.skipRange"] = env(skip_range) | - .metadata.annotations["createdAt"] = strenv(timestamp) | - .metadata.name = env(name) | - .spec.version = env(version) | - .spec.install.spec[strenv(rulesLevel)] = [{ "serviceAccountName": env(account), "rules": env(rules) }] | - .spec.install.spec.deployments = [( env(deployment) | .[] |{ "name": .metadata.name, "spec": .spec} )]' bundle.csv.yaml >"${bundle_directory}/manifests/${file_name}.clusterserviceversion.yaml" - -if [ "${DISTRIBUTION}" == "community" ]; then - update_yaml_images "bundles/$DISTRIBUTION/manifests/${file_name}.clusterserviceversion.yaml" -elif [ "${DISTRIBUTION}" == "redhat" ]; then - - yq eval --inplace ' - .spec.relatedImages = env(relatedImages) | - .metadata.annotations.certified = "true" | - .metadata.annotations["containerImage"] = "registry.connect.redhat.com/percona/percona-server-mongodb-operator@sha256:" | - .metadata.name = strenv(name_certified)' \ - "${bundle_directory}/manifests/${file_name}.clusterserviceversion.yaml" - -elif [ "${DISTRIBUTION}" == "marketplace" ]; then - # Annotations needed when targeting Red Hat Marketplace - export package_url="https://marketplace.redhat.com/en-us/operators/${file_name}" - yq --inplace ' - .metadata.name = env(name_certified_rhmp) | - .metadata.annotations["containerImage"] = "registry.connect.redhat.com/percona/percona-server-mongodb-operator@sha256:" | - .metadata.annotations["marketplace.openshift.io/remote-workflow"] = - "https://marketplace.redhat.com/en-us/operators/percona-server-mongodb-operator-certified-rhmp/pricing?utm_source=openshift_console" | - .metadata.annotations["marketplace.openshift.io/support-workflow"] = - "https://marketplace.redhat.com/en-us/operators/percona-server-mongodb-operator-certified-rhmp/support?utm_source=openshift_console" | - .spec.relatedImages = env(relatedImages)' \ - "${bundle_directory}/manifests/${file_name}.clusterserviceversion.yaml" -fi - -# delete blank lines. -sed -i '' '/^$/d' "${bundle_directory}/manifests/${file_name}.clusterserviceversion.yaml" - -if >/dev/null command -v tree; then tree -C "${bundle_directory}"; fi - -yamllint -d '{extends: default, rules: {line-length: disable, indentation: disable}}' bundles/"$DISTRIBUTION" \ No newline at end of file +render_csv() { + local account + local deployment + local examples + local owned_crds + local rules + local timestamp + local version + local csv_file + local icon_base64 + + log "Rendering CSV" + + csv_stem="$(yq -r '.projectName' "${project_directory}/PROJECT")" + deployment="$(yq eval operator_deployments.yaml)" + + if [[ -z "${containerImage}" ]]; then + containerImage="$(yq eval '.[0].spec.template.spec.containers[0].image' operator_deployments.yaml)" + else + deployment="$( + IMAGE="${containerImage}" yq eval '.[0].spec.template.spec.containers[0].image = env(IMAGE)' \ + <<<"${deployment}" + )" + fi + + examples="$(build_examples)" + owned_crds="$(build_owned_crds)" + account="$(yq eval '.[] | .metadata.name' operator_accounts.yaml)" + rules="$(yq eval '.[] | .rules' operator_roles.yaml)" + version="${CSV_VERSION:-${VERSION}}" + timestamp="$("$date" -u +"%Y-%m-%dT%H:%M:%SZ")" + csv_file="${bundle_directory}/manifests/${component_name}.clusterserviceversion.yaml" + icon_base64="$(base64 <"${repo_root}/kubernetes.svg" | tr -d '\n')" + + export examples + export owned_crds + export deployment + export account + export rules + export version + export stem="${csv_stem}" + export timestamp + export name="${CSV_NAME_OVERRIDE:-${csv_stem}.v${version}}" + export name_certified="${CSV_NAME_OVERRIDE:-${csv_stem}-certified.v${version}}" + export skip_range="<${version}" + export containerImage + export relatedImages + export skips + export icon_base64 + + yq -P eval ' + .metadata.annotations["alm-examples"] = strenv(examples) | + .metadata.annotations["containerImage"] = env(containerImage) | + .metadata.annotations["createdAt"] = strenv(timestamp) | + .metadata.name = env(name) | + .spec.version = env(version) | + .spec.icon = [{ "base64data": strenv(icon_base64), "mediatype": "image/svg+xml" }] | + .spec.customresourcedefinitions.owned = (strenv(owned_crds) | from_json) | + .spec.install.spec.permissions = [{ "serviceAccountName": env(account), "rules": env(rules) }] | + .spec.install.spec.deployments = (env(deployment) | [.[] | { "name": .metadata.name, "spec": .spec }])' \ + bundle.csv.yaml >"${csv_file}" + + customize_csv "${csv_file}" +} + +validate_bundle() { + if [[ "${OLM_VERBOSE:-0}" == "1" || "${OLM_VERBOSE:-false}" == "true" ]] && command -v tree >/dev/null 2>&1; then + tree -C "${bundle_directory}" + fi + + run_quiet "YAML validation" \ + yamllint -d '{extends: default, rules: {line-length: disable, indentation: disable}}' "${bundle_directory}" +} + +normalize_bundle_permissions() { + chmod -R a+rX "${bundle_directory}" +} + +main() { + check_tools + load_distribution_hooks + configure_namespace_manifests + prepare_operator_sources + render_operator_manifests + create_sdk_workspace + create_bundle_directory + render_bundle_metadata + render_bundle_dockerfile + write_crd_manifests + validate_manifest_inputs + prepare_distribution + render_csv + normalize_bundle_permissions + validate_bundle +} + +main "$@" diff --git a/installers/olm/validate-directory.sh b/installers/olm/validate-directory.sh new file mode 100755 index 0000000000..4a9e7f93a5 --- /dev/null +++ b/installers/olm/validate-directory.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail + +validate_bundle_directory() { + local directory="$1" + + operator-sdk bundle validate "${directory}" --select-optional='suite=operatorframework' +} + +validate_bundle_directory "$@" diff --git a/installers/olm/validate-image.sh b/installers/olm/validate-image.sh new file mode 100755 index 0000000000..107337e15a --- /dev/null +++ b/installers/olm/validate-image.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail + +push_trap_exit() { + local -a array + + eval "array=($(trap -p EXIT))" + + # shellcheck disable=SC2064 + trap "$1;${array[2]-}" EXIT +} + +wait_registry() { + local port="$1" + local attempt + + for attempt in $(seq 1 30); do + if curl -fsSL "http://127.0.0.1:${port}/v2/" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + + echo "registry container did not become ready" >&2 + return 1 +} + +TMPDIR="$(mktemp -d)" +push_trap_exit "rm -rf '${TMPDIR}'" +export TMPDIR + +validate_bundle_image() { + local container="$1" + local directory="$2" + local image + local port + local registry + + directory="$(cd "${directory}" && pwd)" + command -v curl >/dev/null 2>&1 || { + echo "curl is required" >&2 + exit 1 + } + + export DOCKER_DEFAULT_PLATFORM="${DOCKER_DEFAULT_PLATFORM:-linux/amd64}" + + registry="$( + "${container}" run \ + --detach \ + --publish-all \ + docker.io/library/registry:2 + )" + + push_trap_exit "echo -n 'Removing '; '${container}' rm '${registry}'" + push_trap_exit "echo -n 'Stopping '; '${container}' stop '${registry}'" + + port="$( + "${container}" inspect "${registry}" \ + --format='{{ (index .NetworkSettings.Ports "5000/tcp" 0).HostPort }}' + )" + wait_registry "${port}" + + image="localhost:${port}/psmdb-operator-bundle:latest" + + "${container}" build \ + --platform="${DOCKER_DEFAULT_PLATFORM}" \ + --tag "${image}" \ + "${directory}" + + "${container}" push "${image}" + + opm alpha bundle validate \ + --use-http \ + --image-builder="${container}" \ + --optional-validators="operatorhub,bundle-objects" \ + --tag="${image}" +} + +validate_bundle_image "$@" diff --git a/kubernetes.svg b/kubernetes.svg index e826807da3..301e5bf866 100644 --- a/kubernetes.svg +++ b/kubernetes.svg @@ -1,14 +1,21 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + + + \ No newline at end of file