From 564c4417f38f7f156ba428ea806f43972539fe79 Mon Sep 17 00:00:00 2001 From: bxvtr <148579658+bxvtr@users.noreply.github.com> Date: Sat, 15 Aug 2026 17:41:54 +0200 Subject: [PATCH] Fix OCI instance PV encryption create contract Empty-state instance create failed because launch_options expressed PV encryption without NetworkType. Use the top-level create argument. --- CHANGELOG.md | 1 + terraform/compute.tf | 7 +- .../test_terraform_pv_encryption_contract.sh | 77 +++++++++++++++---- 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cc30d1..b9f14a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - Removed current-tree concrete operator home-path metadata and generalized regression tests so they assert classes of forbidden data instead of encoding real identities. - First live `private-runtime-config.yml` stopped fail-closed (`changed=0`) at Vault OCID shape validation after Argo CD, scratch PV/PVC binding, and the OCI Secrets Store CSI Driver/provider were already healthy. The operator Vault ID had prefix `ocid1.vault.oc1`, length 105, and no whitespace; Git required `ocid1.vault.oc1..` and omitted the empty OCI future-use component. Git now validates regional Vault OCIDs as `ocid1.vault.oc1...` (optional future-use) and requires the OCID region component to equal `private_runtime_config_oci_region`. Private-runtime materialization, Instance Principal secret retrieval, generated Kubernetes Secrets, Postgres/MLflow/Monitoring health, and second-converge idempotency remain **not yet live proven**. - First live Monitoring Application remained Healthy but OutOfSync: Argo client-side apply failed for multiple Prometheus Operator CRDs with `metadata.annotations: Too long: must have at most 262144 bytes`, after which dependent `Prometheus` and `Alertmanager` resources could not fully reconcile. Git now sets `ServerSideApply=true` on the Monitoring Application only, while keeping `CreateNamespace=true`, automated prune, selfHeal, and Helm `includeCRDs: true`. Monitoring Synced after this change is **not yet live proven**. +- A fresh OCI V2 clean-room apply failed while creating `oci_core_instance.node`: `launch_options` that only enabled PV encryption in transit produced `400-InvalidParameter` because LaunchOptions requires NetworkType. Git now sets `is_pv_encryption_in_transit_enabled = true` as the supported top-level instance create argument and keeps the scratch attachment paravirtualized with PV encryption in transit. Regression coverage requires that top-level assignment so nested `launch_options` alone cannot satisfy the contract. Renewed clean-room instance creation is **not yet live proven**. ## [0.1.0] - 2026-07-30 diff --git a/terraform/compute.tf b/terraform/compute.tf index 5f201af..3697011 100644 --- a/terraform/compute.tf +++ b/terraform/compute.tf @@ -55,9 +55,10 @@ resource "oci_core_instance" "node" { # The scratch volume attachment requires PV encryption in transit. Leaving # this unset lets OCI default the instance to false, which then rejects the # encrypted paravirtualized attachment. - launch_options { - is_pv_encryption_in_transit_enabled = true - } + # Use the instance create argument. A launch_options block that only sets + # this field is rejected with 400-InvalidParameter unless NetworkType is also + # specified; this pin does not need any other launch option. + is_pv_encryption_in_transit_enabled = true metadata = { ssh_authorized_keys = var.ssh_public_key diff --git a/tests/unit/test_terraform_pv_encryption_contract.sh b/tests/unit/test_terraform_pv_encryption_contract.sh index 24c3d92..23ebfc6 100755 --- a/tests/unit/test_terraform_pv_encryption_contract.sh +++ b/tests/unit/test_terraform_pv_encryption_contract.sh @@ -2,7 +2,6 @@ # Static regression for the OCI PV in-transit encryption contract. # Inspects production Terraform blocks only. # No OCI provider, no remote backend, no credentials, no root plan/apply. - set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" @@ -19,8 +18,21 @@ ROOT = Path(sys.argv[1]).resolve() TERRAFORM_DIR = ROOT / "terraform" INSTANCE_RESOURCE = 'resource "oci_core_instance" "node"' ATTACHMENT_RESOURCE = 'resource "oci_core_volume_attachment" "scratch"' +PV_ATTR = "is_pv_encryption_in_transit_enabled" PV_TRUE = "is_pv_encryption_in_transit_enabled = true" PV_FALSE = "is_pv_encryption_in_transit_enabled = false" +NESTED_INSTANCE_BLOCKS = ( + "shape_config", + "create_vnic_details", + "source_details", + "launch_options", + "instance_options", + "availability_config", + "agent_config", + "platform_config", + "preemptible_instance_config", + "metadata", +) def extract_hcl_block(source: str, marker: str, label: str) -> str: @@ -41,6 +53,12 @@ def extract_hcl_block(source: str, marker: str, label: str) -> str: raise SystemExit(f"{label} is unclosed") +def try_extract_hcl_block(source: str, marker: str) -> str | None: + if source.find(marker) < 0: + return None + return extract_hcl_block(source, marker, marker) + + def strip_hcl_comments(text: str) -> str: stripped_lines = [] for line in text.splitlines(): @@ -69,6 +87,16 @@ def quoted_assignment(block: str, name: str, value: str) -> bool: ) +def resource_top_level(resource_code: str) -> str: + remaining = resource_code + for name in NESTED_INSTANCE_BLOCKS: + nested = try_extract_hcl_block(remaining, name) + while nested is not None: + remaining = remaining.replace(nested, "", 1) + nested = try_extract_hcl_block(remaining, name) + return remaining + + def main() -> None: tf_files = sorted(TERRAFORM_DIR.glob("*.tf")) if not tf_files: @@ -84,20 +112,41 @@ def main() -> None: instance_code = strip_hcl_comments(instance) attachment_code = strip_hcl_comments(attachment) production_code = strip_hcl_comments(joined) - - launch_options = extract_hcl_block( - instance_code, "launch_options", "oci_core_instance.node launch_options" - ) - if not assignment_true(launch_options, "is_pv_encryption_in_transit_enabled"): + top_level = resource_top_level(instance_code) + launch_options = try_extract_hcl_block(instance_code, "launch_options") + + if launch_options is not None: + nested_true = assignment_true(launch_options, PV_ATTR) + nested_false = assignment_false(launch_options, PV_ATTR) + if nested_true and not assignment_true(top_level, PV_ATTR): + raise SystemExit( + "instance PV encryption must not be satisfied solely by " + "nested launch_options" + ) + if nested_true or nested_false: + raise SystemExit( + "oci_core_instance.node must not assign " + "is_pv_encryption_in_transit_enabled inside launch_options" + ) + if not re.search(r'(?m)^\s*network_type\s*=', launch_options): + raise SystemExit( + "oci_core_instance.node launch_options must set network_type; " + "OCI rejects LaunchOptions without NetworkType" + ) + print("PASS: optional launch_options is not the PV encryption contract") + else: + print("PASS: instance does not use launch_options for PV encryption") + + if not assignment_true(top_level, PV_ATTR): raise SystemExit( - "oci_core_instance.node launch_options must set " - "is_pv_encryption_in_transit_enabled = true" + "oci_core_instance.node must set " + f"{PV_TRUE} at resource top level" ) - if assignment_false(launch_options, "is_pv_encryption_in_transit_enabled"): + if assignment_false(instance_code, PV_ATTR): raise SystemExit( "oci_core_instance.node must not disable PV encryption in transit" ) - print("PASS: instance launch_options enables PV encryption in transit") + print("PASS: instance top-level PV encryption in transit is enabled") if not quoted_assignment(attachment_code, "attachment_type", "paravirtualized"): raise SystemExit( @@ -105,12 +154,12 @@ def main() -> None: ) print("PASS: scratch attachment type remains paravirtualized") - if not assignment_true(attachment_code, "is_pv_encryption_in_transit_enabled"): + if not assignment_true(attachment_code, PV_ATTR): raise SystemExit( "oci_core_volume_attachment.scratch must set " - "is_pv_encryption_in_transit_enabled = true" + f"{PV_TRUE}" ) - if assignment_false(attachment_code, "is_pv_encryption_in_transit_enabled"): + if assignment_false(attachment_code, PV_ATTR): raise SystemExit( "oci_core_volume_attachment.scratch must not disable PV encryption" ) @@ -126,7 +175,7 @@ def main() -> None: "production Terraform must not assign " f"{PV_FALSE} (found in {', '.join(false_matches)})" ) - if PV_TRUE not in instance_code or PV_TRUE not in attachment_code: + if PV_TRUE not in top_level or PV_TRUE not in attachment_code: raise SystemExit("instance and attachment PV encryption assignments drifted") if "is_pv_encryption_in_transit_enabled" not in production_code: raise SystemExit("PV encryption contract missing from production Terraform")