diff --git a/.github/workflows/openapi-schema-check.yml b/.github/workflows/openapi-schema-check.yml new file mode 100644 index 000000000..1c0d06838 --- /dev/null +++ b/.github/workflows/openapi-schema-check.yml @@ -0,0 +1,64 @@ +name: OpenAPI Schema Check + +on: + schedule: + # Run daily at 7:00 AM UTC + - cron: '0 7 * * *' + workflow_dispatch: # Allow manual trigger + +permissions: + contents: write + pull-requests: write + +jobs: + check-schemas: + name: Check OpenAPI Schemas + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Install dependencies + run: | + YQ_VERSION="v4.53.2" + YQ_SHA256="d56bf5c6819e8e696340c312bd70f849dc1678a7cda9c2ad63eebd906371d56b" + curl -sL "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" -o /usr/local/bin/yq + echo "${YQ_SHA256} /usr/local/bin/yq" | sha256sum -c - + chmod +x /usr/local/bin/yq + pip install PyYAML==6.0.2 + + - name: Regenerate OpenAPI schemas + run: make generate-openapi-schemas + + - name: Create Pull Request + uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'chore: Update OpenAPI schemas for PolicyGenerator' + committer: GitHub Actions Bot + author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> + branch: update-openapi-schemas + delete-branch: true + title: 'Update OpenAPI schemas for PolicyGenerator list merging' + body: | + ## Automated OpenAPI Schema Update + + The `schema.openapi` files used by ACM PolicyGenerator for strategic + list merging are out of date with respect to the current Subscription + channels and upstream CRDs. + + ### Changed files + - `hack/crd-schema-config.json` — updated channels / refs + - `telco-ran/.../acmpolicygenerator/schema.openapi` + - `telco-core/configuration/schema.openapi` + + ### How to Review + - Verify the updated refs match the expected operator release branches + - Check that merge keys are still correct for the new CRD versions + + --- + *This PR was automatically generated by `make generate-openapi-schemas`.* + labels: | + automated diff --git a/Makefile b/Makefile index 1118ea2c4..1fc5515b7 100644 --- a/Makefile +++ b/Makefile @@ -119,6 +119,31 @@ test-kustomize: ## Validate all kustomization.yaml files can build test-shellcheck: ## Validate shell scripts with shellcheck ./hack/test-shellcheck.sh +.PHONY: generate-schema-config +generate-schema-config: ## Regenerate hack/crd-schema-config.json from Subscription CRs + python3 hack/generate-schema-config.py + +.PHONY: generate-openapi-schemas +generate-openapi-schemas: generate-schema-config ## Regenerate schema.openapi files from CRDs for ACM PolicyGenerator + python3 hack/extract-schema.py --config hack/crd-schema-config.json --component ran \ + -o telco-ran/configuration/argocd/example/acmpolicygenerator/schema.openapi + cp telco-ran/configuration/argocd/example/acmpolicygenerator/schema.openapi \ + telco-ran/configuration/argocd/example/acmpolicygenerator/hub-side-templating/schema.openapi + python3 hack/extract-schema.py --config hack/crd-schema-config.json --component core \ + -o telco-core/configuration/schema.openapi + +.PHONY: check-openapi-schemas +check-openapi-schemas: generate-openapi-schemas ## Verify schema.openapi files are up-to-date + @if ! git diff --exit-code hack/crd-schema-config.json \ + telco-ran/configuration/argocd/example/acmpolicygenerator/schema.openapi \ + telco-ran/configuration/argocd/example/acmpolicygenerator/hub-side-templating/schema.openapi \ + telco-core/configuration/schema.openapi; then \ + echo ""; \ + echo "ERROR: OpenAPI schema files are out of date."; \ + echo "Run 'make generate-openapi-schemas' and commit the result."; \ + exit 1; \ + fi + ci-validate: lintCheck check-reference-core check-reference-ran check-reference-hub .PHONY: check-reference-core diff --git a/VERSION_UPDATE_GUIDE.md b/VERSION_UPDATE_GUIDE.md index ec3c81e63..4903695a1 100644 --- a/VERSION_UPDATE_GUIDE.md +++ b/VERSION_UPDATE_GUIDE.md @@ -77,14 +77,29 @@ patches: - Update PolicyGenerator placement specifications which use version - Policy generator policy names MUST use version numbers which match the OpenShift version number (e.g., `core-cluster-config-4.23`) -### 3. Image references +### 3. OpenAPI Schema for PolicyGenerator List Merging + +Regenerate the OpenAPI schemas for PolicyGenerator list merging: + +```bash +make generate-openapi-schemas +``` + +This first regenerates `hack/crd-schema-config.json` from the Subscription CRs +(picking up updated channels and refs), then regenerates the `schema.openapi` +files in `telco-ran` and `telco-core` from upstream CRDs. If an operator's +GitHub branch naming convention does not match its Subscription channel (e.g., +the branch is `v6.5` instead of `release-6.5`), manually update the `ref` field +for that CRD entry in `hack/crd-schema-config.json`. + +### 4. Image references The reference configuration includes image pullspecs in several places which use version specific tags for the image. These must be updated. These pullspecs may be in the reference-crs or in PolicyGenerator patches. Common places where image tags are used include: - CatalogSource images -- Update both the reference-cr and the PolicyGenerator overlay patch to align with the OpenShift version - Numa Resources Operator (NROP) -- Update image spec in the Scheduler (sched.yaml) and PolicyGenerator overlay patch to align with the OpenShift version - Upgrade policy -- Update the version and image pull spec in the PolicyGenerator overlay patch to align with the OpenShift version - The telco-hub reference includes a plugins Policy CR with image pullspec references which must be updated to the correct versions. The ztp-site-generate image aligns with OpenShift. The multicluster-operator image aligns with MCE. -### 4. ACM AgentServiceConfig +### 5. ACM AgentServiceConfig Add new OpenShift version osImages entries and remove the oldest version to maintain only 3 versions: @@ -115,7 +130,7 @@ osImages: **Note**: The kube-compare variant uses templating and doesn't require manual entry removal - it renders from the actual config. -### 5. Mirror Registry Configuration +### 6. Mirror Registry Configuration **File**: `telco-hub/install/mirror-registry/imageset-config.yaml` @@ -123,7 +138,7 @@ The versions of each component in the mirror configuration must be updated. Thes - Mirroring versions MUST match the Subscription versions for each operator - The additional images MUST include the images specified in the Hub reference plugins Policy -### 6. Installation Examples (ClusterInstance) +### 7. Installation Examples (ClusterInstance) Find all ClusterInstance CRs (`kind: ClusterInstance`) and update clusterImageSetNameRef to align with the OpenShift version: @@ -138,7 +153,7 @@ Find all ClusterInstance CRs (`kind: ClusterInstance`) and update clusterImageSe Change: `clusterImageSetNameRef: "openshift-4.22"` → `"openshift-4.23"` -### 7. Other locations +### 8. Other locations Other versioning information to be reviewed and updated - Annotations added in PolicyGenerator overlay patches to trigger non-compliance, eg `noop-for-triggering-noncompliance: "22"` - Descriptive text in metadata.yaml, eg `This reference was designed for OpenShift 4.22` diff --git a/hack/crd-schema-config.json b/hack/crd-schema-config.json new file mode 100644 index 000000000..6b2d20801 --- /dev/null +++ b/hack/crd-schema-config.json @@ -0,0 +1,138 @@ +{ + "_comment": [ + "Single source of truth for OpenAPI schema generation.", + "Static fields (source, merge_keys, version, package_name, ref_rule)", + "are manually maintained.", + "Dynamic fields (subscription_channel, components, source.github.ref)", + "are updated by: python3 hack/generate-schema-config.py", + "To regenerate schemas: make generate-openapi-schemas" + ], + "crds": [ + { + "name": "PtpConfig", + "package_name": "ptp-operator", + "ref_rule": "release-ocp", + "components": [ + "ran" + ], + "subscription_channel": "stable", + "source": { + "github": { + "owner": "openshift", + "repo": "ptp-operator", + "ref": "release-4.22", + "path": "config/crd/bases/ptp.openshift.io_ptpconfigs.yaml" + } + }, + "merge_keys": { + "spec.profile": "name", + "spec.recommend": "profile", + "status.matchList": "nodeName" + } + }, + { + "name": "ClusterLogForwarder", + "package_name": "cluster-logging", + "ref_rule": "release-channel", + "components": [ + "core", + "ran" + ], + "subscription_channel": "stable-6.5", + "source": { + "github": { + "owner": "openshift", + "repo": "cluster-logging-operator", + "ref": "release-6.5", + "path": "config/crd/bases/observability.openshift.io_clusterlogforwarders.yaml" + } + }, + "merge_keys": { + "spec.filters": "name", + "spec.inputs": "name", + "spec.outputs": "name", + "spec.pipelines": "name" + } + }, + { + "name": "Tuned", + "ref_rule": "release-ocp", + "components": [ + "core", + "ran" + ], + "subscription_channel": "built-in (NTO)", + "source": { + "github": { + "owner": "openshift", + "repo": "cluster-node-tuning-operator", + "ref": "release-4.22", + "path": "manifests/20-tuned.crd.yaml" + } + }, + "merge_keys": { + "spec.profile": "name", + "spec.recommend": "profile" + } + }, + { + "name": "PerformanceProfile", + "version": "v2", + "ref_rule": "release-ocp", + "components": [ + "core", + "ran" + ], + "subscription_channel": "built-in (NTO)", + "source": { + "github": { + "owner": "openshift", + "repo": "cluster-node-tuning-operator", + "ref": "release-4.22", + "path": "manifests/20-performance-profile.crd.yaml" + } + }, + "merge_keys": {} + }, + { + "name": "NUMAResourcesOperator", + "package_name": "numaresources-operator", + "ref_rule": "release-ocp", + "components": [ + "core" + ], + "subscription_channel": "4.22", + "source": { + "github": { + "owner": "openshift-kni", + "repo": "numaresources-operator", + "ref": "release-4.22", + "path": "config/crd/bases/nodetopology.openshift.io_numaresourcesoperators.yaml" + } + }, + "merge_keys": { + "spec.nodeGroups": "poolName" + } + }, + { + "name": "ImageDigestMirrorSet", + "ref_rule": "release-ocp", + "components": [ + "core", + "ran" + ], + "subscription_channel": "built-in (OCP)", + "source": { + "github": { + "owner": "openshift", + "repo": "api", + "ref": "release-4.22", + "path": "config/v1/zz_generated.crd-manifests/0000_10_config-operator_01_imagedigestmirrorsets.crd.yaml" + } + }, + "merge_keys": { + "spec.imageDigestMirrors": "source" + } + } + ] +} diff --git a/hack/extract-schema.py b/hack/extract-schema.py new file mode 100755 index 000000000..ad913b95d --- /dev/null +++ b/hack/extract-schema.py @@ -0,0 +1,493 @@ +#!/usr/bin/env python3 +""" +Extract OpenAPI merge schemas from Kubernetes CRDs for ACM PolicyGenerator. + +Downloads CRDs from GitHub or a cluster, extracts property schemas, and injects +strategic merge patch directives to produce schema.openapi files compatible with +ACM PolicyGenerator's openapi directive. + +Most CRDs do not include x-kubernetes-list-type/x-kubernetes-list-map-keys +annotations, so merge keys are specified in the config file based on domain +knowledge of each CR's API. When a CRD does include these annotations, the +script auto-detects them. + +Usage: + # Generate schema for a specific component + ./extract-schema.py --config hack/crd-schema-config.json --component ran -o schema.openapi + + # Generate schema for all CRDs in the config + ./extract-schema.py --config hack/crd-schema-config.json -o schema.openapi + + # Quick test with a single CRD from a cluster + oc get crd ptpconfigs.ptp.openshift.io -o json > /tmp/crd.json + ./extract-schema.py --from-file /tmp/crd.json + + # Pipe from GitHub via yq + curl -sL https://raw.githubusercontent.com/.../crd.yaml | yq -o json \\ + | ./extract-schema.py --from-file - +""" + +import argparse +import json +import os +import subprocess +import sys +import urllib.request +import urllib.error + + +# Annotations to strip from CRD schemas (converted or irrelevant for PolicyGenerator) +_STRIP_KEYS = frozenset({ + "x-kubernetes-list-type", + "x-kubernetes-list-map-keys", + "x-kubernetes-validations", + "x-kubernetes-map-type", +}) + + +def load_crd_from_file(path): + """Load CRD(s) from a JSON file. Use '-' for stdin.""" + if path == "-": + data = json.load(sys.stdin) + else: + with open(path) as f: + data = json.load(f) + + if data.get("kind") == "CustomResourceDefinitionList": + return data.get("items", []) + if data.get("kind") == "CustomResourceDefinition": + return [data] + raise ValueError(f"Expected CustomResourceDefinition, got {data.get('kind')}") + + +def load_crd_from_cluster(crd_name): + """Fetch a CRD from a running cluster via oc/kubectl.""" + for tool in ("oc", "kubectl"): + try: + result = subprocess.run( + [tool, "get", "crd", crd_name, "-o", "json"], + capture_output=True, text=True, check=True + ) + return [json.loads(result.stdout)] + except (subprocess.CalledProcessError, FileNotFoundError): + continue + raise RuntimeError(f"Could not fetch CRD {crd_name} (tried oc and kubectl)") + + +def fetch_github_crd(owner, repo, ref, path): + """Fetch a CRD from GitHub, converting YAML to JSON via yq if needed.""" + raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/{ref}/{path}" + req = urllib.request.Request(raw_url) + token = os.environ.get("GITHUB_TOKEN") + if token: + req.add_header("Authorization", f"token {token}") + + try: + resp = urllib.request.urlopen(req, timeout=30) + except urllib.error.HTTPError as e: + url = f"https://github.com/{owner}/{repo}/tree/{ref}" + raise RuntimeError( + f"Failed to download CRD from {owner}/{repo}@{ref} (HTTP {e.code}).\n" + f" Path: {path}\n" + f" Verify the ref exists: {url}" + ) from None + + with resp: + content = resp.read() + + # Try JSON first + try: + data = json.loads(content) + if data.get("kind") == "CustomResourceDefinition": + return [data] + except (json.JSONDecodeError, AttributeError): + pass + + # YAML — convert via yq + try: + result = subprocess.run( + ["yq", "-o", "json"], + input=content, capture_output=True, check=True + ) + data = json.loads(result.stdout) + if data.get("kind") == "CustomResourceDefinition": + return [data] + except (subprocess.CalledProcessError, FileNotFoundError): + pass + raise RuntimeError( + f"Could not parse {raw_url}. Install yq (https://github.com/mikefarah/yq) " + f"for YAML conversion, or provide JSON input." + ) + + +def make_type_name(group, version, kind): + """Convert API group/version/kind to schema.openapi type name. + + Reverses the DNS-style group, appends version and kind: + ptp.openshift.io / v1 / PtpConfig -> io.openshift.ptp.v1.PtpConfig + """ + reversed_group = ".".join(reversed(group.split("."))) + return f"{reversed_group}.{version}.{kind}" + + +def resolve_path(schema, path_parts): + """Walk into a schema following a dotted path like 'spec.profile.items.match'. + + Automatically descends into 'properties' at each object level and 'items' + for arrays, so the path can use just the field names: + 'spec.profile' -> properties.spec.properties.profile + 'spec.recommend.match' -> properties.spec.properties.recommend.items.properties.match + """ + node = schema + for part in path_parts: + # Try direct key first (for explicit 'items' in path) + if part in node: + node = node[part] + continue + # Descend into properties + if "properties" in node and part in node["properties"]: + node = node["properties"][part] + continue + # Descend through items.properties for arrays + if "items" in node: + items = node["items"] + if "properties" in items and part in items["properties"]: + node = items["properties"][part] + continue + raise KeyError(f"Cannot resolve path segment '{part}' in schema") + return node + + +def convert_schema(schema): + """Convert CRD schema to schema.openapi format. + + Strips CRD-specific annotations. If the CRD happens to include + x-kubernetes-list-type: map annotations, converts them to the + x-kubernetes-patch-* equivalents automatically. + """ + if not isinstance(schema, dict): + return schema + + is_map_list = schema.get("x-kubernetes-list-type") == "map" + map_keys = schema.get("x-kubernetes-list-map-keys", []) + + result = {} + for key, value in schema.items(): + if key in _STRIP_KEYS: + continue + if isinstance(value, dict): + result[key] = convert_schema(value) + elif isinstance(value, list): + result[key] = [ + convert_schema(item) if isinstance(item, dict) else item + for item in value + ] + else: + result[key] = value + + # Auto-detected merge keys from CRD annotations + if is_map_list and map_keys: + if len(map_keys) == 1: + result["x-kubernetes-patch-merge-key"] = map_keys[0] + result["x-kubernetes-patch-strategy"] = "merge" + else: + print( + f" WARNING: multi-key list-map {map_keys} cannot be represented " + f"with x-kubernetes-patch-merge-key (single key only), skipping. " + f"Add an explicit merge_keys override in crd-schema-config.json.", + file=sys.stderr, + ) + + return result + + +def inject_merge_keys(schema, merge_keys): + """Inject x-kubernetes-patch-* directives at specified paths. + + merge_keys is a dict mapping dotted paths to merge key field names: + {"spec.profile": "name", "spec.recommend": "profile"} + """ + for path, merge_key in merge_keys.items(): + parts = path.split(".") + try: + node = resolve_path(schema, parts) + except KeyError as e: + print(f" WARNING: merge key path '{path}' not found: {e}", file=sys.stderr) + continue + + if node.get("type") != "array": + print(f" WARNING: '{path}' is not an array, skipping merge key", file=sys.stderr) + continue + + node["x-kubernetes-patch-merge-key"] = merge_key + node["x-kubernetes-patch-strategy"] = "merge" + + +def prune_schema(schema): + """Prune a schema to only the minimal structure needed for merge directives. + + For merge-annotated arrays, keeps only the type and merge directives — no + item properties. If a merge-annotated array contains nested arrays that + also have merge directives, those are kept as well. + + Example output for a merge-annotated array: + {"type": "array", "x-kubernetes-patch-merge-key": "name", + "x-kubernetes-patch-strategy": "merge"} + """ + if not isinstance(schema, dict): + return None + + # If this node has merge directives, keep it minimally + if "x-kubernetes-patch-strategy" in schema: + result = { + "type": schema.get("type", "array"), + "x-kubernetes-patch-strategy": schema["x-kubernetes-patch-strategy"], + } + if "x-kubernetes-patch-merge-key" in schema: + result["x-kubernetes-patch-merge-key"] = schema["x-kubernetes-patch-merge-key"] + # Check for nested merge-annotated arrays inside items + if "items" in schema: + nested = prune_schema(schema["items"]) + if nested is not None: + result["items"] = nested + return result + + pruned = {} + + # Recurse into properties + if "properties" in schema: + pruned_props = {} + for prop_name, prop_schema in schema["properties"].items(): + result = prune_schema(prop_schema) + if result is not None: + pruned_props[prop_name] = result + if pruned_props: + pruned["properties"] = pruned_props + if "type" in schema: + pruned["type"] = schema["type"] + + # Recurse into items (for arrays without merge directives but with + # nested merge-annotated children) + if "items" in schema: + result = prune_schema(schema["items"]) + if result is not None: + pruned["items"] = result + if "type" in schema: + pruned["type"] = schema["type"] + + if not pruned: + return None + return pruned + + +def extract_crd_schema(crd, merge_keys=None, preferred_version=None): + """Extract a schema.openapi definition from a CRD. + + Args: + crd: Parsed CRD dict + merge_keys: Optional dict of path -> merge_key to inject + preferred_version: Optional API version to use (e.g. "v2"). If not + specified, uses the storage version or first served version. + + Returns (type_name, schema) or (None, None). + """ + group = crd["spec"]["group"] + kind = crd["spec"]["names"]["kind"] + versions = crd["spec"].get("versions", []) + + # If a preferred version is specified, try it first + if preferred_version: + matching = [v for v in versions if v["name"] == preferred_version] + if matching: + versions = matching + [v for v in versions if v["name"] != preferred_version] + else: + storage = [v for v in versions if v.get("storage") and v.get("served", True)] + served = [v for v in versions if v.get("served", True) and v not in storage] + versions = storage + served + + # Use the first version with a schema + for v in versions: + openapi_schema = v.get("schema", {}).get("openAPIV3Schema") + if not openapi_schema: + continue + + converted = convert_schema(openapi_schema) + + # Inject manually-specified merge keys + if merge_keys: + inject_merge_keys(converted, merge_keys) + + # Prune to only keep subtrees with merge directives + pruned = prune_schema(converted) + if pruned is None: + continue + + pruned["type"] = "object" + pruned["x-kubernetes-group-version-kind"] = [{ + "group": group, + "kind": kind, + "version": v["name"], + }] + + type_name = make_type_name(group, v["name"], kind) + return type_name, pruned + + return None, None + + +def process_config(config_path, component=None): + """Process a JSON config file. + + Config format: + { + "crds": [ + { + "name": "PtpConfig", + "source": { + "github": { + "owner": "openshift", + "repo": "ptp-operator", + "ref": "release-4.22", + "path": "config/crd/bases/ptp.openshift.io_ptpconfigs.yaml" + } + }, + "merge_keys": { + "spec.profile": "name", + "spec.recommend": "profile", + "status.matchList": "nodeName" + } + }, + { + "name": "PerformanceProfile", + "version": "v2", + "source": { "github": { "...": "..." } }, + "merge_keys": { "spec.hugepages.pages": "size" } + } + ] + } + + Fields: + name: Human-readable label for log output. + version: Preferred API version to extract (e.g. "v2"). If omitted, + uses the first version with a schema. + source: One of "github", "cluster", or "file". + merge_keys: Maps dotted field paths to merge key field names. + Most CRDs lack x-kubernetes-list-type annotations, so + merge keys must be specified explicitly based on domain + knowledge of each CR's API. + + Source types: "github", "cluster", "file". + merge_keys: maps dotted field paths to the merge key field name. + components: list of component names this CRD belongs to (e.g. ["ran", "core"]). + Use --component to filter. + """ + try: + with open(config_path) as f: + config = json.load(f) + except FileNotFoundError: + print(f"ERROR: Config file not found: {config_path}", file=sys.stderr) + sys.exit(1) + + definitions = {} + + for entry in config.get("crds", []): + # Filter by component if specified + if component: + entry_components = entry.get("components", []) + if component not in entry_components: + continue + source = entry.get("source", {}) + name = entry.get("name", "unknown") + merge_keys = entry.get("merge_keys", {}) + preferred_version = entry.get("version") + + try: + crds = _load_crds(source, name) + + for crd in crds: + type_name, schema = extract_crd_schema(crd, merge_keys, preferred_version) + if type_name and schema: + definitions[type_name] = schema + print(f" + {type_name}", file=sys.stderr) + else: + crd_kind = crd.get("spec", {}).get("names", {}).get("kind", "?") + print(f" - {crd_kind}: no schema found, skipping", file=sys.stderr) + + except RuntimeError as e: + print(f" ERROR: {e}", file=sys.stderr) + sys.exit(1) + + return definitions + + +def _load_crds(source, name): + """Load CRDs from the configured source.""" + if "github" in source: + gh = source["github"] + print(f"Downloading {name} from {gh['owner']}/{gh['repo']}@{gh['ref']}...", + file=sys.stderr) + return fetch_github_crd(gh["owner"], gh["repo"], gh["ref"], gh["path"]) + elif "file" in source: + print(f"Loading {name} from {source['file']}...", file=sys.stderr) + return load_crd_from_file(source["file"]) + elif "cluster" in source: + print(f"Fetching {name} from cluster ({source['cluster']})...", file=sys.stderr) + return load_crd_from_cluster(source["cluster"]) + else: + raise ValueError(f"No source configured for {name}") + + +def main(): + parser = argparse.ArgumentParser( + description="Extract OpenAPI merge schemas from CRDs for ACM PolicyGenerator" + ) + + source = parser.add_mutually_exclusive_group(required=True) + source.add_argument("--config", + help="JSON config file listing CRDs, sources, and merge keys") + source.add_argument("--from-file", + help="Local CRD JSON file (use '-' for stdin)") + source.add_argument("--from-cluster", + help="CRD resource name to fetch from cluster") + + parser.add_argument("--output", "-o", help="Output file (default: stdout)") + parser.add_argument("--component", + help="Only include CRDs tagged for this component (e.g. ran, core, hub)") + + args = parser.parse_args() + + definitions = {} + + if args.config: + definitions.update(process_config(args.config, component=args.component)) + elif args.from_file: + for crd in load_crd_from_file(args.from_file): + type_name, schema = extract_crd_schema(crd) + if type_name: + definitions[type_name] = schema + print(f" + {type_name}", file=sys.stderr) + elif args.from_cluster: + for crd in load_crd_from_cluster(args.from_cluster): + type_name, schema = extract_crd_schema(crd) + if type_name: + definitions[type_name] = schema + print(f" + {type_name}", file=sys.stderr) + + if not definitions: + print("ERROR: No schemas extracted from any CRD", file=sys.stderr) + sys.exit(1) + + # Sort for stable, diffable output + sorted_defs = dict(sorted(definitions.items())) + output = json.dumps({"definitions": sorted_defs}, indent=2) + "\n" + + if args.output: + with open(args.output, "w") as f: + f.write(output) + print(f"\nWrote {args.output} ({len(definitions)} definitions)", file=sys.stderr) + else: + print(output) + + +if __name__ == "__main__": + main() diff --git a/hack/generate-schema-config.py b/hack/generate-schema-config.py new file mode 100755 index 000000000..62c493b39 --- /dev/null +++ b/hack/generate-schema-config.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +Update hack/crd-schema-config.json with current Subscription channels, +components, and git refs by scanning the repository. + +The JSON config is the single source of truth for the static CRD mapping +(owner, repo, path, merge_keys). This script only updates the dynamic fields: + - subscription_channel: from Subscription CRs in the repo + - components: which components (ran/core) have a Subscription for the operator + - source.github.ref: derived from channel + OCP version conventions + +Usage: + python3 hack/generate-schema-config.py # update in-place + python3 hack/generate-schema-config.py --dry-run # preview changes +""" + +import argparse +import json +import re +import sys +from pathlib import Path + +import yaml + +REPO_ROOT = Path(__file__).resolve().parent.parent +CONFIG_PATH = REPO_ROOT / "hack" / "crd-schema-config.json" + +# Only components that use ACM PolicyGenerator are scanned. +# telco-hub is excluded because it has no PolicyGenerator CR manifests. +SUBSCRIPTION_DIRS = { + "ran": REPO_ROOT / "telco-ran" / "configuration" / "source-crs", + "core": REPO_ROOT / "telco-core" / "configuration" / "reference-crs", +} + + +def detect_ocp_version(): + """Detect the target OCP version from the NROP Subscription channel.""" + nrop_sub = SUBSCRIPTION_DIRS["core"] / "required" / "scheduling" / "NROPSubscription.yaml" + if not nrop_sub.exists(): + return None + with open(nrop_sub) as f: + doc = yaml.safe_load(f) + channel = doc.get("spec", {}).get("channel", "") + m = re.search(r"(\d+\.\d+)", str(channel)) + return m.group(1) if m else None + + +def scan_subscriptions(): + """Scan Subscription CRs. Returns {package_name: {channel, components}}.""" + results = {} + for component, scan_dir in SUBSCRIPTION_DIRS.items(): + if not scan_dir.exists(): + continue + for yaml_file in scan_dir.rglob("*Subscription*.yaml"): + if "deprecated" in str(yaml_file): + continue + with open(yaml_file) as f: + doc = yaml.safe_load(f) + if not isinstance(doc, dict) or doc.get("kind") != "Subscription": + continue + + spec = doc.get("spec", {}) + pkg_name = spec.get("name") + channel = spec.get("channel") + + if pkg_name and channel: + if pkg_name not in results: + results[pkg_name] = {"channel": channel, "components": set()} + elif results[pkg_name]["channel"] != channel: + print( + f" WARNING: package '{pkg_name}' has different channels " + f"across components: '{results[pkg_name]['channel']}' vs " + f"'{channel}'. Split the entry in hack/crd-schema-config.json " + f"into separate per-component entries.", + file=sys.stderr, + ) + results[pkg_name]["components"].add(component) + + return results + + +def derive_ref(entry, channel, ocp_version): + """Derive git ref from channel and ref_rule. + + ref_rule in the config controls how the ref is derived: + - "release-ocp": use release-{OCP_VERSION} + - "release-channel": use release-{version_from_channel} + - absent/null: leave ref unchanged + """ + ref_rule = entry.get("ref_rule") + if not ref_rule: + return entry.get("source", {}).get("github", {}).get("ref") + + if ref_rule == "release-ocp" and ocp_version: + return f"release-{ocp_version}" + elif ref_rule == "release-channel" and channel: + m = re.search(r"(\d+[\.\d]*)", channel) + if m: + return f"release-{m.group(1)}" + + return entry["source"]["github"].get("ref") + + +def main(): + parser = argparse.ArgumentParser( + description="Update hack/crd-schema-config.json from Subscription CRs" + ) + parser.add_argument("--dry-run", action="store_true", + help="Print updated config without writing") + args = parser.parse_args() + + ocp_version = detect_ocp_version() + if not ocp_version: + print("ERROR: Could not detect OCP version", file=sys.stderr) + sys.exit(1) + print(f"Detected OCP version: {ocp_version}", file=sys.stderr) + + subscriptions = scan_subscriptions() + + with open(CONFIG_PATH) as f: + config = json.load(f) + + for entry in config.get("crds", []): + pkg = entry.get("package_name") + sub = subscriptions.get(pkg) if pkg else None + + if sub: + entry["subscription_channel"] = sub["channel"] + entry["components"] = sorted(sub["components"]) + elif pkg: + entry.pop("subscription_channel", None) + entry["components"] = [] + + new_ref = derive_ref(entry, sub["channel"] if sub else None, ocp_version) + if new_ref: + entry["source"]["github"]["ref"] = new_ref + + output = json.dumps(config, indent=2) + "\n" + + if args.dry_run: + print(output) + else: + with open(CONFIG_PATH, "w") as f: + f.write(output) + print(f"Updated {CONFIG_PATH}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/telco-core/configuration/schema.openapi b/telco-core/configuration/schema.openapi new file mode 100644 index 000000000..f11b39be3 --- /dev/null +++ b/telco-core/configuration/schema.openapi @@ -0,0 +1,127 @@ +{ + "definitions": { + "io.openshift.config.v1.ImageDigestMirrorSet": { + "properties": { + "spec": { + "properties": { + "imageDigestMirrors": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "source" + } + }, + "type": "object" + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "config.openshift.io", + "kind": "ImageDigestMirrorSet", + "version": "v1" + } + ] + }, + "io.openshift.nodetopology.v1.NUMAResourcesOperator": { + "properties": { + "spec": { + "properties": { + "nodeGroups": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "poolName" + } + }, + "type": "object" + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "nodetopology.openshift.io", + "kind": "NUMAResourcesOperator", + "version": "v1" + } + ] + }, + "io.openshift.observability.v1.ClusterLogForwarder": { + "properties": { + "spec": { + "properties": { + "collector": { + "properties": { + "resources": { + "properties": { + "claims": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "filters": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" + }, + "inputs": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" + }, + "outputs": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" + }, + "pipelines": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" + } + }, + "type": "object" + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "observability.openshift.io", + "kind": "ClusterLogForwarder", + "version": "v1" + } + ] + }, + "io.openshift.tuned.v1.Tuned": { + "properties": { + "spec": { + "properties": { + "profile": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" + }, + "recommend": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "profile" + } + }, + "type": "object" + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "tuned.openshift.io", + "kind": "Tuned", + "version": "v1" + } + ] + } + } +} diff --git a/telco-ran/configuration/argocd/example/acmpolicygenerator/README.md b/telco-ran/configuration/argocd/example/acmpolicygenerator/README.md index ee8bd4f9f..64c1f97be 100644 --- a/telco-ran/configuration/argocd/example/acmpolicygenerator/README.md +++ b/telco-ran/configuration/argocd/example/acmpolicygenerator/README.md @@ -67,8 +67,153 @@ spec: ``` # Patching CR objects containing lists -Creating patches for CR objects containing lists is not currently supported by ACM the Generator Plugin. As a workwaround, the full content of final object must be contained in the patch. -The pgt2acmpg tool supports creating such a patch that contains the full content of the CR, see below. + +By default, ACM PolicyGenerator replaces entire lists when patching. To enable +strategic merge (matching list elements by key and merging fields), a +`schema.openapi` file must be referenced from the PolicyGenerator manifest: + +```yaml +manifests: + - path: source-crs/ptp-operator/configuration/PtpConfigSlave.yaml + patches: + - spec: + profile: + - name: slave # matched by merge key "name" + interface: ens5f0 # only this field is patched + openapi: + path: schema.openapi # enables list merging +``` + +Without the `openapi` directive, the patch above would replace the entire +`spec.profile` array. With it, PolicyGenerator matches the element where +`name: slave` and merges only the `interface` field, preserving other fields +and other array elements. + +## How the schema.openapi file works + +The `schema.openapi` file is a JSON document containing OpenAPI schema +definitions with strategic merge patch directives: + +- `x-kubernetes-patch-strategy: merge` — marks an array as merge-capable +- `x-kubernetes-patch-merge-key: ` — specifies which field in array + elements is used as the unique key for matching + +These are the same directives Kubernetes uses internally for strategic merge +patch on built-in resources. PolicyGenerator reads them to determine how to +merge list patches. + +## Atomic vs map-type lists + +Not all lists support merge-by-key. There are two types: + +- **Map-type lists** have elements with a natural unique key (e.g., PtpConfig + `spec.profile[]` where each element has a unique `name`). These support + strategic merge and have entries in `schema.openapi`. +- **Atomic lists** are simple arrays (e.g., `spec.additionalKernelArgs[]` in + PerformanceProfile, or `spec.nicSelector.pfNames[]` in SriovNetworkNodePolicy). + These have no merge key — the entire list is always replaced. No schema + entry is needed for atomic lists. + +## Generating and updating the schema + +The `schema.openapi` files are **generated artifacts** produced by two scripts: + +1. `hack/generate-schema-config.py` — scans Subscription CRs in the repo to + auto-populate `hack/crd-schema-config.json` with CRD sources, channels, + components, and git refs. The `merge_keys` and `version` fields in the + config are **manually maintained** and preserved across regenerations. +2. `hack/extract-schema.py` — downloads CRDs from GitHub, extracts minimal + merge directive schemas, and outputs `schema.openapi` files. + +To regenerate everything: + +```bash +make generate-openapi-schemas +``` + +This first runs `generate-schema-config` (updating `hack/crd-schema-config.json` +from Subscription CRs), then regenerates the `schema.openapi` files. + +The CRD sources use best-effort version mapping based on the Subscription +channels in the reference configuration. Most CRDs do not include +`x-kubernetes-list-type` annotations, so the merge keys are specified in the +config based on domain knowledge of each CR's API. + +### Generating a schema for a single CRD + +You can use `hack/extract-schema.py` directly to generate a schema from a +single CRD, without modifying the config file. This is useful for testing or +for adding a one-off CRD to your deployment: + +```bash +# From a running cluster +oc get crd ptpconfigs.ptp.openshift.io -o json \ + | ./hack/extract-schema.py --from-file - + +# From a local file (YAML requires yq to be installed) +./hack/extract-schema.py --from-file /tmp/mycrd.json +``` + +This outputs the schema to stdout. Note that merge keys from CRD annotations +are auto-detected, but most CRDs lack these annotations. To manually specify +merge keys for a single CRD, add it to `hack/crd-schema-config.json` instead +(see below). + +### Updating for your environment + +If your deployment uses CRs with lists not covered by the provided schema, you +can extend it: + +1. Obtain the CRD for your CR (from a cluster or operator GitHub repo): + ```bash + oc get crd -o json > /tmp/mycrd.json + ``` +2. Extract the schema and identify list fields that need merge keys +3. Add the CRD to `hack/crd-schema-config.json` with appropriate merge keys +4. Run `make generate-openapi-schemas` to regenerate + +The merge key for a list field is the field within each element that uniquely +identifies it (typically `name`, but varies by API). Check the operator +documentation or CRD source to determine the correct key. + +### Version updates + +When updating to a new OpenShift release, update the `ref` fields in +`hack/crd-schema-config.json` to match the new operator release branches, +then run `make generate-openapi-schemas`. The schema structure may change between +operator versions. + +### Different operator versions across components + +If an operator uses different Subscription channels in `telco-ran` vs +`telco-core` (e.g., `stable-6.4` in ran and `stable-6.5` in core), the +generation script will warn about the conflict. To handle this, split the +CRD entry in `hack/crd-schema-config.json` into two separate entries — one +per component: + +```json +{ + "name": "ClusterLogForwarder", + "package_name": "cluster-logging", + "ref_rule": "release-channel", + "components": ["core"], + "subscription_channel": "stable-6.5", + "source": { "github": { "ref": "release-6.5", "...": "..." } }, + "merge_keys": { "...": "..." } +}, +{ + "name": "ClusterLogForwarder", + "package_name": "cluster-logging", + "ref_rule": "release-channel", + "components": ["ran"], + "subscription_channel": "stable-6.4", + "source": { "github": { "ref": "release-6.4", "...": "..." } }, + "merge_keys": { "...": "..." } +} +``` + +Each entry is filtered by `--component` during schema generation, so they +produce independent schemas for their respective components. # The ACM PolicyGenerator version of the DU reference configuration The ACM PolicyGenerator version of this reference configuration is functionally diff --git a/telco-ran/configuration/argocd/example/acmpolicygenerator/hub-side-templating/schema.openapi b/telco-ran/configuration/argocd/example/acmpolicygenerator/hub-side-templating/schema.openapi index b8ebb6640..981a4271e 100644 --- a/telco-ran/configuration/argocd/example/acmpolicygenerator/hub-side-templating/schema.openapi +++ b/telco-ran/configuration/argocd/example/acmpolicygenerator/hub-side-templating/schema.openapi @@ -1,172 +1,102 @@ { "definitions": { - "io.openshift.ptp.v1.PtpConfig": { - "description": "PtpConfig is the Schema for the ptpconfigs API", + "io.openshift.config.v1.ImageDigestMirrorSet": { "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "metadata": { - "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta_v2", - "description": "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata" - }, "spec": { - "description": "PtpConfigSpec defines the desired state of PtpConfig", "properties": { - "profile": { - "x-kubernetes-patch-merge-key": "name", + "imageDigestMirrors": { + "type": "array", "x-kubernetes-patch-strategy": "merge", - "items": { - "properties": { - "interface": { - "type": "string" - }, - "name": { - "type": "string" - }, - "phc2sysConf": { - "type": "string" - }, - "phc2sysOpts": { - "type": "string" - }, - "plugins": { - "additionalProperties": { - "x-kubernetes-preserve-unknown-fields": true - }, - "type": "object" - }, - "ptp4lConf": { - "type": "string" - }, - "ptp4lOpts": { - "type": "string" - }, - "ptpClockThreshold": { - "properties": { - "holdOverTimeout": { - "description": "clock state to stay in holdover state in secs", - "format": "int64", - "type": "integer" - }, - "maxOffsetThreshold": { - "description": "max offset in nano secs", - "format": "int64", - "type": "integer" - }, - "minOffsetThreshold": { - "description": "min offset in nano secs", - "format": "int64", - "type": "integer" - } - }, - "type": "object" - }, - "ptpSchedulingPolicy": { - "enum": [ - "SCHED_OTHER", - "SCHED_FIFO" - ], - "type": "string" - }, - "ptpSchedulingPriority": { - "format": "int64", - "maximum": 65, - "minimum": 1, - "type": "integer" - }, - "ptpSettings": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - "ts2phcConf": { - "type": "string" + "x-kubernetes-patch-merge-key": "source" + } + }, + "type": "object" + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "config.openshift.io", + "kind": "ImageDigestMirrorSet", + "version": "v1" + } + ] + }, + "io.openshift.observability.v1.ClusterLogForwarder": { + "properties": { + "spec": { + "properties": { + "collector": { + "properties": { + "resources": { + "properties": { + "claims": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" + } }, - "ts2phcOpts": { - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" + "type": "object" + } }, - "type": "array" + "type": "object" + }, + "filters": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" + }, + "inputs": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" + }, + "outputs": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" + }, + "pipelines": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" + } + }, + "type": "object" + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "observability.openshift.io", + "kind": "ClusterLogForwarder", + "version": "v1" + } + ] + }, + "io.openshift.ptp.v1.PtpConfig": { + "properties": { + "spec": { + "properties": { + "profile": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" }, "recommend": { - "x-kubernetes-patch-merge-key": "profile", + "type": "array", "x-kubernetes-patch-strategy": "merge", - "items": { - "properties": { - "match": { - "items": { - "x-kubernetes-patch-merge-key": "nodeName", - "x-kubernetes-patch-strategy": "merge", - "properties": { - "nodeLabel": { - "type": "string" - }, - "nodeName": { - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - "priority": { - "format": "int64", - "type": "integer" - }, - "profile": { - "type": "string" - } - }, - "required": [ - "priority", - "profile" - ], - "type": "object" - }, - "type": "array" + "x-kubernetes-patch-merge-key": "profile" } }, - "required": [ - "profile", - "recommend" - ], "type": "object" }, "status": { - "description": "PtpConfigStatus defines the observed state of PtpConfig", "properties": { "matchList": { - "description": "INSERT ADDITIONAL STATUS FIELD - define observed state of cluster Important: Run \"make\" to regenerate code after modifying this file", - "x-kubernetes-patch-merge-key": "nodeName", + "type": "array", "x-kubernetes-patch-strategy": "merge", - "items": { - "properties": { - "nodeName": { - "type": "string" - }, - "profile": { - "type": "string" - } - }, - "required": [ - "nodeName", - "profile" - ], - "type": "object" - }, - "type": "array" + "x-kubernetes-patch-merge-key": "nodeName" } }, "type": "object" @@ -180,6 +110,33 @@ "version": "v1" } ] + }, + "io.openshift.tuned.v1.Tuned": { + "properties": { + "spec": { + "properties": { + "profile": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" + }, + "recommend": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "profile" + } + }, + "type": "object" + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "tuned.openshift.io", + "kind": "Tuned", + "version": "v1" + } + ] } } -} \ No newline at end of file +} diff --git a/telco-ran/configuration/argocd/example/acmpolicygenerator/schema.openapi b/telco-ran/configuration/argocd/example/acmpolicygenerator/schema.openapi index 9520d66b0..981a4271e 100644 --- a/telco-ran/configuration/argocd/example/acmpolicygenerator/schema.openapi +++ b/telco-ran/configuration/argocd/example/acmpolicygenerator/schema.openapi @@ -1,172 +1,102 @@ { "definitions": { - "io.openshift.ptp.v1.PtpConfig": { - "description": "PtpConfig is the Schema for the ptpconfigs API", + "io.openshift.config.v1.ImageDigestMirrorSet": { "properties": { - "apiVersion": { - "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - "type": "string" - }, - "kind": { - "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - "type": "string" - }, - "metadata": { - "$ref": "#/definitions/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta_v2", - "description": "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata" - }, "spec": { - "description": "PtpConfigSpec defines the desired state of PtpConfig", "properties": { - "profile": { - "x-kubernetes-patch-merge-key": "name", + "imageDigestMirrors": { + "type": "array", "x-kubernetes-patch-strategy": "merge", - "items": { - "properties": { - "interface": { - "type": "string" - }, - "name": { - "type": "string" - }, - "phc2sysConf": { - "type": "string" - }, - "phc2sysOpts": { - "type": "string" - }, - "plugins": { - "additionalProperties": { - "x-kubernetes-preserve-unknown-fields": true - }, - "type": "object" - }, - "ptp4lConf": { - "type": "string" - }, - "ptp4lOpts": { - "type": "string" - }, - "ptpClockThreshold": { - "properties": { - "holdOverTimeout": { - "description": "clock state to stay in holdover state in secs", - "format": "int64", - "type": "integer" - }, - "maxOffsetThreshold": { - "description": "max offset in nano secs", - "format": "int64", - "type": "integer" - }, - "minOffsetThreshold": { - "description": "min offset in nano secs", - "format": "int64", - "type": "integer" - } - }, - "type": "object" - }, - "ptpSchedulingPolicy": { - "enum": [ - "SCHED_OTHER", - "SCHED_FIFO" - ], - "type": "string" - }, - "ptpSchedulingPriority": { - "format": "int64", - "maximum": 65, - "minimum": 1, - "type": "integer" - }, - "ptpSettings": { - "additionalProperties": { - "type": "string" - }, - "type": "object" - }, - "ts2phcConf": { - "type": "string" + "x-kubernetes-patch-merge-key": "source" + } + }, + "type": "object" + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "config.openshift.io", + "kind": "ImageDigestMirrorSet", + "version": "v1" + } + ] + }, + "io.openshift.observability.v1.ClusterLogForwarder": { + "properties": { + "spec": { + "properties": { + "collector": { + "properties": { + "resources": { + "properties": { + "claims": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" + } }, - "ts2phcOpts": { - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" + "type": "object" + } }, - "type": "array" + "type": "object" + }, + "filters": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" + }, + "inputs": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" + }, + "outputs": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" + }, + "pipelines": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" + } + }, + "type": "object" + } + }, + "type": "object", + "x-kubernetes-group-version-kind": [ + { + "group": "observability.openshift.io", + "kind": "ClusterLogForwarder", + "version": "v1" + } + ] + }, + "io.openshift.ptp.v1.PtpConfig": { + "properties": { + "spec": { + "properties": { + "profile": { + "type": "array", + "x-kubernetes-patch-strategy": "merge", + "x-kubernetes-patch-merge-key": "name" }, "recommend": { - "x-kubernetes-patch-merge-key": "profile", + "type": "array", "x-kubernetes-patch-strategy": "merge", - "items": { - "properties": { - "match": { - "items": { - "x-kubernetes-patch-merge-key": "nodeName", - "x-kubernetes-patch-strategy": "merge", - "properties": { - "nodeLabel": { - "type": "string" - }, - "nodeName": { - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - }, - "priority": { - "format": "int64", - "type": "integer" - }, - "profile": { - "type": "string" - } - }, - "required": [ - "priority", - "profile" - ], - "type": "object" - }, - "type": "array" + "x-kubernetes-patch-merge-key": "profile" } }, - "required": [ - "profile", - "recommend" - ], "type": "object" }, "status": { - "description": "PtpConfigStatus defines the observed state of PtpConfig", "properties": { "matchList": { - "description": "INSERT ADDITIONAL STATUS FIELD - define observed state of cluster Important: Run \"make\" to regenerate code after modifying this file", - "x-kubernetes-patch-merge-key": "nodeName", + "type": "array", "x-kubernetes-patch-strategy": "merge", - "items": { - "properties": { - "nodeName": { - "type": "string" - }, - "profile": { - "type": "string" - } - }, - "required": [ - "nodeName", - "profile" - ], - "type": "object" - }, - "type": "array" + "x-kubernetes-patch-merge-key": "nodeName" } }, "type": "object" @@ -181,19 +111,19 @@ } ] }, - "io.openshift.observability.v1.ClusterLogForwarder": { + "io.openshift.tuned.v1.Tuned": { "properties": { "spec": { "properties": { - "filters": { - "x-kubernetes-patch-merge-key": "name", + "profile": { + "type": "array", "x-kubernetes-patch-strategy": "merge", - "type": "array" + "x-kubernetes-patch-merge-key": "name" }, - "outputs": { - "x-kubernetes-patch-merge-key": "name", + "recommend": { + "type": "array", "x-kubernetes-patch-strategy": "merge", - "type": "array" + "x-kubernetes-patch-merge-key": "profile" } }, "type": "object" @@ -202,8 +132,8 @@ "type": "object", "x-kubernetes-group-version-kind": [ { - "group": "observability.openshift.io", - "kind": "ClusterLogForwarder", + "group": "tuned.openshift.io", + "kind": "Tuned", "version": "v1" } ]