Skip to content
This repository was archived by the owner on Sep 9, 2026. It is now read-only.

OSAC-1558: add bm_host_metal3_provisioning template role - #358

Merged
openshift-merge-bot[bot] merged 3 commits into
osac-project:mainfrom
carbonin:feat/OSAC-1558
Jun 19, 2026
Merged

openshift-merge-bot[bot] merged 3 commits into
osac-project:mainfrom
carbonin:feat/OSAC-1558

Conversation

@carbonin

@carbonin carbonin commented Jun 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds the bm_host_metal3_provisioning template role for provisioning and deprovisioning bare metal hosts via the Metal3/BMO backend.

create.yaml:

  • Validates hostClass == "metal3", parses templateParameters for imageURL, imageChecksum, checksumType, networkData, sshKey, and userDataSecret
  • Creates networkData and userData Secrets in the BMH namespace as needed
  • Patches BMH spec.image, spec.networkData, and spec.userData, then waits for provisioned state
  • Idempotent — skips provisioning if BMH already has the correct image
  • SSH key injection: generates cloud-init with ssh_authorized_keys, or merges into existing cloud-config user data
  • Fails if SSH key injection is requested but user data is not in cloud-config format

delete.yaml:

  • Patches BMH to remove spec.image, spec.networkData, and spec.userData
  • Waits for BMH to return to available/ready state
  • Deletes networkData and userData Secrets

Test plan

Validated end-to-end on ostest cluster:

  • Provision BMH with image only — BMI reaches Ready, BMH reaches provisioned
  • Deprovision — BMH returns to available, image cleared, Secrets cleaned up
  • Provision with sshKey — userData Secret created with cloud-init ssh_authorized_keys, BMH spec.userData set
  • SSH connectivity verified (ssh fedora@<host> using injected key)
  • Deprovision with userData — Secret deleted, spec.userData cleared
  • ansible-lint passes

Assisted-by: Claude Code noreply@anthropic.com

Summary by CodeRabbit

  • New Features
    • Added Metal3 BareMetalHost provisioning workflow with image configuration, user data, and network data management.
    • Added Metal3 BareMetalHost deprovisioning workflow with automatic resource cleanup and state management.

carbonin added 3 commits June 17, 2026 16:16
Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Nick Carboni <ncarboni@redhat.com>
Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Nick Carboni <ncarboni@redhat.com>
…role

The role now reads sshKey and userDataSecret from templateParameters,
builds cloud-init userData, creates a Secret in the BMH namespace, and
patches the BareMetalHost with spec.userData. Supports SSH key only,
user data only, or merged SSH key into existing cloud-config. Cleanup
on deprovision clears spec.userData and deletes the userData Secret.

Assisted-by: Claude Code <noreply@anthropic.com>
Signed-off-by: Nick Carboni <ncarboni@redhat.com>
@openshift-ci-robot

openshift-ci-robot commented Jun 18, 2026 •

Copy link
Copy Markdown

@carbonin: This pull request references OSAC-1558 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the task to target the "5.0.0" version, but no target version was set.

Details

In response to this:

Summary

Adds the bm_host_metal3_provisioning template role for provisioning and deprovisioning bare metal hosts via the Metal3/BMO backend.

create.yaml:

  • Validates hostClass == "metal3", parses templateParameters for imageURL, imageChecksum, checksumType, networkData, sshKey, and userDataSecret
  • Creates networkData and userData Secrets in the BMH namespace as needed
  • Patches BMH spec.image, spec.networkData, and spec.userData, then waits for provisioned state
  • Idempotent — skips provisioning if BMH already has the correct image
  • SSH key injection: generates cloud-init with ssh_authorized_keys, or merges into existing cloud-config user data
  • Fails if SSH key injection is requested but user data is not in cloud-config format

delete.yaml:

  • Patches BMH to remove spec.image, spec.networkData, and spec.userData
  • Waits for BMH to return to available/ready state
  • Deletes networkData and userData Secrets

Test plan

Validated end-to-end on ostest cluster:

  • Provision BMH with image only — BMI reaches Ready, BMH reaches provisioned
  • Deprovision — BMH returns to available, image cleared, Secrets cleaned up
  • Provision with sshKey — userData Secret created with cloud-init ssh_authorized_keys, BMH spec.userData set
  • SSH connectivity verified (ssh fedora@<host> using injected key)
  • Deprovision with userData — Secret deleted, spec.userData cleared
  • ansible-lint passes

Assisted-by: Claude Code noreply@anthropic.com

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci
openshift-ci Bot requested review from adriengentil and danmanor June 18, 2026 20:38
@coderabbitai

coderabbitai Bot commented Jun 18, 2026 •

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Two new Ansible task files are added to the bm_host_metal3_provisioning role. create.yaml implements a 212-line provisioning flow covering input validation, cloud-init user-data construction with SSH key merging, Kubernetes Secret creation, BareMetalHost patching, and state polling. delete.yaml implements an 81-line deprovisioning flow that clears BMH image fields and deletes associated Secrets.

Changes

Metal3 BareMetalHost Provisioning Role

Layer / File(s) Summary
Input validation, BMH lookup, and idempotency guards
collections/.../tasks/create.yaml, collections/.../tasks/delete.yaml
Both files parse externalHostID into namespace/name, perform k8s_info BMH lookups, and emit skip conditions when the BMH is absent or already in the desired provisioning state.
User-data and SSH key preparation
collections/.../tasks/create.yaml
Reads an optional existing userData Secret, generates SSH-key-only cloud-config when no Secret is referenced, and merges an SSH key into existing cloud-init YAML (with #cloud-config header validation) when both are present.
Secret creation and bmh_patch construction
collections/.../tasks/create.yaml
Creates user-data and optionally network-data Kubernetes Secrets, then assembles bmh_patch with image URL/checksum, online: true, and conditional Secret references.
BMH patch, state polling, and Secret cleanup
collections/.../tasks/create.yaml, collections/.../tasks/delete.yaml
Applies the BMH patch and polls until provisioned (create) or nulls image fields and polls until available/ready then deletes Secrets (delete), with a final debug completion message in both paths.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • osac-project/osac-aap#342: Both PRs implement BareMetalInstance-driven Metal3 provisioning flows sourcing hostClass, templateParameters, and externalHostID from bare_metal_instance.spec.*, as part of the HostLease → BareMetalInstance migration.

Suggested reviewers

  • tzumainn
  • ajamias

Poem

A BareMetalHost waits, bare and still,
externalHostID parsed with skill.
SSH keys merge into cloud-init's song,
Secrets patched, state polled all day long.
provisioned at last — the metal sings! 🖥️✨


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (2 errors)

Check name Status Explanation Resolution
No-Injection-Vectors ❌ Error Line 96 in create.yaml contains YAML injection vulnerability: unquoted {{ ssh_key }} allows newline injection into cloud-init, injecting arbitrary YAML keys/values. Quote SSH key on line 96: change - {{ ssh_key }} to - "{{ ssh_key }}" to prevent YAML structure injection via newlines in templateParameters.sshKey.
No-Sensitive-Data-In-Logs ❌ Error Tasks handling SSH keys, cloud-init content, and Secret payloads lack no_log: true, risking credential exposure in logs. Affected: lines 21-24, 84-86, 88-96, 119-127, 129-139, 141-152 of create.y... Add no_log: true to all tasks that handle SSH keys, cloud-init content, or Secret payloads (lines 21-24, 63-127, 129-139, 141-152) to prevent sensitive data exposure in Ansible logs and AAP output.
✅ Passed checks (9 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately describes the main change: adding a new template role for Metal3 BareMetalHost provisioning, which aligns with the changeset's primary purpose.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
No-Hardcoded-Secrets ✅ Passed No hardcoded secrets, API keys, tokens, passwords, base64 strings >32 chars, embedded credentials, or sensitive variable literal assignments detected in the new Ansible task files.
No-Weak-Crypto ✅ Passed No weak cryptography (MD5, SHA1, DES, RC4, 3DES, Blowfish, ECB), custom crypto implementations, or insecure secret comparisons detected. Files are Ansible YAML task definitions for Metal3 provision...
Container-Privileges ✅ Passed PR adds Ansible task files for Metal3 provisioning, not container/K8s manifests. No privileged, hostPID, hostNetwork, hostIPC, SYS_ADMIN, or allowPrivilegeEscalation settings found.
Ai-Attribution ✅ Passed AI tool use is properly disclosed with correct Red Hat "Assisted-by" trailer in commit message; no improper "Co-Authored-By" for AI.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@collections/ansible_collections/osac/templates/roles/bm_host_metal3_provisioning/tasks/create.yaml`:
- Around line 63-139: The tasks that handle sensitive user data and credentials
need to suppress logging to prevent exposure in failures or verbose output. Add
`no_log: true` to the following tasks: "Extract user data content from Secret",
"Generate cloud-init with SSH key only", "Use existing user data as-is (no SSH
key to merge)", "Merge SSH key into existing cloud-init user data" block, and
"Create userData Secret in BMH namespace". This will prevent the
`existing_user_data`, `final_user_data`, and Secret `stringData` containing
credentials and bootstrap material from being logged or displayed in Ansible
output.
- Around line 26-32: Add the kubeconfig parameter to all Kubernetes module calls
in this file to ensure they target the remote cluster instead of the default
Kubernetes context. For every kubernetes.core.k8s_info and kubernetes.core.k8s
task (including the "Get BareMetalHost" task, and those at the other mentioned
line ranges), add the argument kubeconfig: "{{ kubeconfig | default(omit) }}" to
the module parameters. This follows the coding guideline to always include
osac.service.common to get remote_cluster_kubeconfig before creating Kubernetes
resources on remote clusters.
- Around line 84-86: The "Extract user data content from Secret" task at line 86
reads from `.data.userdata` (lowercase u), but the role creates the Secret with
the key `userData` (capital U) at line 139 in the stringData section. These key
names must match for the role to work correctly with user-provided Secrets.
Change the reference in the set_fact task from `.data.userdata` to
`.data.userData` to be consistent with the key that is actually written when the
Secret is created.
- Around line 16-19: The task "Parse externalHostID into namespace and name"
splits the externalHostID without validation, which causes cryptic Jinja index
errors on malformed input instead of clear validation failures. Add a validation
task before this task to ensure bare_metal_instance.spec.externalHostID exists
and contains exactly one forward slash to split into two parts. If validation
fails, use the fail module to provide a clear error message explaining the
expected format. Only proceed to the set_fact task with the split operation
after validation succeeds.
- Around line 49-53: The already_provisioned fact in the "Check if BMH is
already provisioned with the correct image" task only compares the image URL but
ignores checksum validation. Extend the comparison logic to also verify that
both the imageChecksum and checksumType from template_params match the
corresponding values in bmh_info.status.provisioning.image. Add these additional
conditions to the already_provisioned fact so that a provisioning task is
triggered when either the URL or checksum fields differ, preventing incomplete
provisioning when only the checksum changes.
- Around line 88-96: The merge path (around lines 119-127) uses the folded
scalar indicator `>-` for the final_user_data block when it should use the
literal scalar indicator `|` to preserve line breaks and the cloud-config header
format, matching the approach used in the SSH-only path. Change the `>-` to `|`
in the merge path's final_user_data block to ensure the cloud-config YAML
content maintains proper formatting with the header and YAML structure on
separate lines instead of being folded into a single line.

In
`@collections/ansible_collections/osac/templates/roles/bm_host_metal3_provisioning/tasks/delete.yaml`:
- Around line 7-13: The kubernetes.core.k8s_info module call is missing the
kubeconfig parameter, which can cause the task to target the wrong cluster
context when deprovisioning resources. Add the kubeconfig parameter `kubeconfig:
"{{ kubeconfig | default(omit) }}"` to the kubernetes.core.k8s_info task shown
in the diff, and apply the same kubeconfig parameter addition to all other
Kubernetes module calls in this file including the k8s module invocations at the
line ranges mentioned (39-50, 52-61, 63-69, 71-77) to ensure consistent cluster
targeting across all resource operations.
- Around line 27-37: The task "Remove image and networkData from BMH" is
currently gated only on the bmh_has_image condition being true, but this leaves
stale references to Secrets when spec.userData or spec.networkData are present
without an image URL being set. Modify the logic to ensure that spec.userData
and spec.networkData are cleared from the BMH regardless of whether
bmh_has_image is true, so that dangling Secret references are not left behind
after Secret cleanup operations.
- Around line 2-5: The task that sets bmh_namespace and bmh_name by splitting
bare_metal_instance.spec.externalHostID assumes the value contains exactly one
slash without any validation. If the externalHostID is malformed or empty, the
split operation will fail with an unhelpful Jinja index error. Add a validation
step before the split that checks whether externalHostID is in the correct
format (contains exactly one slash resulting in two parts when split), and if
validation fails, use a task to fail with a clear error message that explains
the expected format of externalHostID.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: osac-project/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: a4221858-87cf-4daa-9560-20652a7e8115

📥 Commits

Reviewing files that changed from the base of the PR and between d92092c and f630622.

📒 Files selected for processing (2)
  • collections/ansible_collections/osac/templates/roles/bm_host_metal3_provisioning/tasks/create.yaml
  • collections/ansible_collections/osac/templates/roles/bm_host_metal3_provisioning/tasks/delete.yaml

Comment on lines +16 to +19
- name: Parse externalHostID into namespace and name
ansible.builtin.set_fact:
bmh_namespace: "{{ bare_metal_instance.spec.externalHostID.split('/')[0] }}"
bmh_name: "{{ bare_metal_instance.spec.externalHostID.split('/')[1] }}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate externalHostID before splitting it.

Line 19 assumes spec.externalHostID is exactly namespace/name; malformed input fails with a Jinja index error before the role can produce an actionable validation failure.

Proposed fix
+- name: Validate externalHostID format
+  ansible.builtin.assert:
+    that:
+      - (bare_metal_instance.spec.externalHostID | default('')) is match('^[^/]+/[^/]+$')
+    fail_msg: "spec.externalHostID must be '<namespace>/<name>'"
+
 - name: Parse externalHostID into namespace and name
   ansible.builtin.set_fact:
     bmh_namespace: "{{ bare_metal_instance.spec.externalHostID.split('/')[0] }}"
     bmh_name: "{{ bare_metal_instance.spec.externalHostID.split('/')[1] }}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@collections/ansible_collections/osac/templates/roles/bm_host_metal3_provisioning/tasks/create.yaml`
around lines 16 - 19, The task "Parse externalHostID into namespace and name"
splits the externalHostID without validation, which causes cryptic Jinja index
errors on malformed input instead of clear validation failures. Add a validation
task before this task to ensure bare_metal_instance.spec.externalHostID exists
and contains exactly one forward slash to split into two parts. If validation
fails, use the fail module to provide a clear error message explaining the
expected format. Only proceed to the set_fact task with the split operation
after validation succeeds.

Comment on lines +26 to +32
- name: Get BareMetalHost
kubernetes.core.k8s_info:
api_version: metal3.io/v1alpha1
kind: BareMetalHost
namespace: "{{ bmh_namespace }}"
name: "{{ bmh_name }}"
register: bmh_result

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pass the remote kubeconfig to every Kubernetes module call.

These kubernetes.core.k8s_info / kubernetes.core.k8s tasks omit kubeconfig, so the role can act against the controller/default Kubernetes context instead of the remote cluster prepared for OSAC provisioning.

Proposed fix pattern
   kubernetes.core.k8s_info:
+    kubeconfig: "{{ kubeconfig | default(omit) }}"
     api_version: metal3.io/v1alpha1
     kind: BareMetalHost
   kubernetes.core.k8s:
+    kubeconfig: "{{ kubeconfig | default(omit) }}"
     state: present
     definition:

Apply the same module argument to every Kubernetes task in this file.

As per coding guidelines, “Always include osac.service.common to get remote_cluster_kubeconfig before creating K8s resources on remote clusters.” Based on learnings, use kubeconfig: "{{ kubeconfig | default(omit) }}" for this collection’s role task YAMLs.

Also applies to: 69-75, 129-139, 141-152, 190-197, 199-208

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@collections/ansible_collections/osac/templates/roles/bm_host_metal3_provisioning/tasks/create.yaml`
around lines 26 - 32, Add the kubeconfig parameter to all Kubernetes module
calls in this file to ensure they target the remote cluster instead of the
default Kubernetes context. For every kubernetes.core.k8s_info and
kubernetes.core.k8s task (including the "Get BareMetalHost" task, and those at
the other mentioned line ranges), add the argument kubeconfig: "{{ kubeconfig |
default(omit) }}" to the module parameters. This follows the coding guideline to
always include osac.service.common to get remote_cluster_kubeconfig before
creating Kubernetes resources on remote clusters.

Sources: Coding guidelines, Learnings

Comment on lines +49 to +53
- name: Check if BMH is already provisioned with the correct image
ansible.builtin.set_fact:
already_provisioned: >-
{{ (bmh_info.status.provisioning.state | default('') == 'provisioned') and
(bmh_info.status.provisioning.image.url | default('') == template_params.imageURL) }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include checksum fields in the idempotency guard.

The skip condition only compares the image URL, so a reused URL with a changed imageChecksum or checksumType reports success while leaving the host on the previous image content.

Proposed fix
     already_provisioned: >-
       {{ (bmh_info.status.provisioning.state | default('') == 'provisioned') and
-         (bmh_info.status.provisioning.image.url | default('') == template_params.imageURL) }}
+         (bmh_info.spec.image.url | default('') == template_params.imageURL) and
+         (bmh_info.spec.image.checksum | default('') == (template_params.imageChecksum | default('')) ) and
+         (bmh_info.spec.image.checksumType | default('auto') == (template_params.checksumType | default('auto')) ) }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@collections/ansible_collections/osac/templates/roles/bm_host_metal3_provisioning/tasks/create.yaml`
around lines 49 - 53, The already_provisioned fact in the "Check if BMH is
already provisioned with the correct image" task only compares the image URL but
ignores checksum validation. Extend the comparison logic to also verify that
both the imageChecksum and checksumType from template_params match the
corresponding values in bmh_info.status.provisioning.image. Add these additional
conditions to the already_provisioned fact so that a provisioning task is
triggered when either the URL or checksum fields differ, preventing incomplete
provisioning when only the checksum changes.

Comment on lines +63 to +139
- name: Build userData content
when: ssh_key | length > 0 or user_data_secret_ref | length > 0
block:
- name: Read userDataSecret from BareMetalInstance namespace
when: user_data_secret_ref | length > 0
block:
- name: Fetch userDataSecret
kubernetes.core.k8s_info:
api_version: v1
kind: Secret
namespace: "{{ bare_metal_instance.metadata.namespace }}"
name: "{{ user_data_secret_ref }}"
register: user_data_secret_result

- name: Fail if userDataSecret not found
ansible.builtin.fail:
msg: >-
Secret '{{ user_data_secret_ref }}' not found in namespace
'{{ bare_metal_instance.metadata.namespace }}'
when: user_data_secret_result.resources | length == 0

- name: Extract user data content from Secret
ansible.builtin.set_fact:
existing_user_data: "{{ user_data_secret_result.resources[0].data.userdata | b64decode }}"

- name: Generate cloud-init with SSH key only
when:
- ssh_key | length > 0
- user_data_secret_ref | length == 0
ansible.builtin.set_fact:
final_user_data: |
#cloud-config
ssh_authorized_keys:
- {{ ssh_key }}

- name: Use existing user data as-is (no SSH key to merge)
when:
- ssh_key | length == 0
- user_data_secret_ref | length > 0
ansible.builtin.set_fact:
final_user_data: "{{ existing_user_data }}"

- name: Merge SSH key into existing cloud-init user data
when:
- ssh_key | length > 0
- user_data_secret_ref | length > 0
block:
- name: Fail if user data is not cloud-config format
ansible.builtin.fail:
msg: >-
Cannot inject SSH key into user data that is not cloud-config format.
User data must start with '#cloud-config' to support SSH key injection.
Either remove the ssh_key parameter or provide user data in cloud-config
format with ssh_authorized_keys included.
when: not (existing_user_data | trim is match('^#cloud-config'))

- name: Parse existing cloud-init and merge SSH key
ansible.builtin.set_fact:
final_user_data: >-
#cloud-config
{{ (existing_user_data | regex_replace('^#cloud-config\s*\n?', '') | from_yaml | default({}))
| combine({'ssh_authorized_keys':
((existing_user_data | regex_replace('^#cloud-config\s*\n?', '') | from_yaml | default({})).ssh_authorized_keys | default([]))
+ [ssh_key]})
| to_nice_yaml }}

- name: Create userData Secret in BMH namespace
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: Secret
metadata:
name: "{{ bmh_name }}-user-data"
namespace: "{{ bmh_namespace }}"
stringData:
userData: "{{ final_user_data }}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Suppress Ansible logging for cloud-init and Secret payloads.

existing_user_data, final_user_data, and Secret stringData can contain credentials or bootstrap material; without no_log: true, failures or verbose AAP output can expose them.

Proposed fix pattern
     - name: Build userData content
+      no_log: true
       when: ssh_key | length > 0 or user_data_secret_ref | length > 0
       block:
     - name: Create networkData Secret
+      no_log: true
       when: template_params.networkData | default('') | length > 0
       kubernetes.core.k8s:

Also applies to: 141-152

🧰 Tools
🪛 Checkov (3.3.1)

[low] 63-142: Ensure block is handling task errors properly

(CKV2_ANSIBLE_3)


[low] 66-89: Ensure block is handling task errors properly

(CKV2_ANSIBLE_3)


[low] 105-130: Ensure block is handling task errors properly

(CKV2_ANSIBLE_3)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@collections/ansible_collections/osac/templates/roles/bm_host_metal3_provisioning/tasks/create.yaml`
around lines 63 - 139, The tasks that handle sensitive user data and credentials
need to suppress logging to prevent exposure in failures or verbose output. Add
`no_log: true` to the following tasks: "Extract user data content from Secret",
"Generate cloud-init with SSH key only", "Use existing user data as-is (no SSH
key to merge)", "Merge SSH key into existing cloud-init user data" block, and
"Create userData Secret in BMH namespace". This will prevent the
`existing_user_data`, `final_user_data`, and Secret `stringData` containing
credentials and bootstrap material from being logged or displayed in Ansible
output.

Comment on lines +84 to +86
- name: Extract user data content from Secret
ansible.builtin.set_fact:
existing_user_data: "{{ user_data_secret_result.resources[0].data.userdata | b64decode }}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Read the same userData Secret key that this role writes.

Line 86 reads .data.userdata, but Line 139 creates stringData.userData; a user-provided Secret following this role’s own key contract will fail or decode the wrong field.

Proposed fix
+            - name: Fail if userDataSecret is missing userData key
+              ansible.builtin.fail:
+                msg: "Secret '{{ user_data_secret_ref }}' must contain data.userData"
+              when: user_data_secret_result.resources[0].data.userData is not defined
+
             - name: Extract user data content from Secret
               ansible.builtin.set_fact:
-                existing_user_data: "{{ user_data_secret_result.resources[0].data.userdata | b64decode }}"
+                existing_user_data: "{{ user_data_secret_result.resources[0].data.userData | b64decode }}"

Also applies to: 129-139

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@collections/ansible_collections/osac/templates/roles/bm_host_metal3_provisioning/tasks/create.yaml`
around lines 84 - 86, The "Extract user data content from Secret" task at line
86 reads from `.data.userdata` (lowercase u), but the role creates the Secret
with the key `userData` (capital U) at line 139 in the stringData section. These
key names must match for the role to work correctly with user-provided Secrets.
Change the reference in the set_fact task from `.data.userdata` to
`.data.userData` to be consistent with the key that is actually written when the
Secret is created.

Comment on lines +88 to +96
- name: Generate cloud-init with SSH key only
when:
- ssh_key | length > 0
- user_data_secret_ref | length == 0
ansible.builtin.set_fact:
final_user_data: |
#cloud-config
ssh_authorized_keys:
- {{ ssh_key }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and examine the file
fd -t f "create.yaml" | grep "bm_host_metal3_provisioning"

Repository: osac-project/osac-aap

Length of output: 164


🏁 Script executed:

#!/bin/bash
# Get file size and examine the specific lines
if [ -f "collections/ansible_collections/osac/templates/roles/bm_host_metal3_provisioning/tasks/create.yaml" ]; then
  wc -l "collections/ansible_collections/osac/templates/roles/bm_host_metal3_provisioning/tasks/create.yaml"
  echo "=== Lines 80-100 ==="
  sed -n '80,100p' "collections/ansible_collections/osac/templates/roles/bm_host_metal3_provisioning/tasks/create.yaml"
  echo "=== Lines 110-135 ==="
  sed -n '110,135p' "collections/ansible_collections/osac/templates/roles/bm_host_metal3_provisioning/tasks/create.yaml"
fi

Repository: osac-project/osac-aap

Length of output: 2386


Change the merge path from >- (folded) to | (literal) to preserve the #cloud-config header line.

The SSH-only path (lines 88–96) correctly uses | to preserve literal YAML formatting. However, the merge path (lines 119–127) uses >-, which folds newlines into spaces, causing #cloud-config and the YAML content to render on a single line. This breaks cloud-init parsing.

Proposed fix
- name: Parse existing cloud-init and merge SSH key
  ansible.builtin.set_fact:
    final_user_data: |
      `#cloud-config`
      {{ (existing_user_data | regex_replace('^`#cloud-config`\s*\n?', '') | from_yaml | default({}))
         | combine({'ssh_authorized_keys':
             ((existing_user_data | regex_replace('^`#cloud-config`\s*\n?', '') | from_yaml | default({})).ssh_authorized_keys | default([]))
             + [ssh_key]})
         | to_nice_yaml }}

Consider extracting the regex/from_yaml operation into a separate task to avoid duplication:

+ - name: Parse existing cloud-init content
+   ansible.builtin.set_fact:
+     parsed_user_data: "{{ existing_user_data | regex_replace('^`#cloud-config`\\s*\\n?', '') | from_yaml | default({}, true) }}"
+
  - name: Parse existing cloud-init and merge SSH key
    ansible.builtin.set_fact:
      final_user_data: |
        `#cloud-config`
-       {{ (existing_user_data | regex_replace('^`#cloud-config`\s*\n?', '') | from_yaml | default({}))
-          | combine({'ssh_authorized_keys':
-              ((existing_user_data | regex_replace('^`#cloud-config`\s*\n?', '') | from_yaml | default({})).ssh_authorized_keys | default([]))
+       {{ parsed_user_data
+          | combine({'ssh_authorized_keys':
+              (parsed_user_data.ssh_authorized_keys | default([]))
               + [ssh_key]})
           | to_nice_yaml }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@collections/ansible_collections/osac/templates/roles/bm_host_metal3_provisioning/tasks/create.yaml`
around lines 88 - 96, The merge path (around lines 119-127) uses the folded
scalar indicator `>-` for the final_user_data block when it should use the
literal scalar indicator `|` to preserve line breaks and the cloud-config header
format, matching the approach used in the SSH-only path. Change the `>-` to `|`
in the merge path's final_user_data block to ensure the cloud-config YAML
content maintains proper formatting with the header and YAML structure on
separate lines instead of being folded into a single line.

Comment on lines +2 to +5
- name: Parse externalHostID into namespace and name
ansible.builtin.set_fact:
bmh_namespace: "{{ bare_metal_instance.spec.externalHostID.split('/')[0] }}"
bmh_name: "{{ bare_metal_instance.spec.externalHostID.split('/')[1] }}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate externalHostID before splitting it.

Line 5 assumes spec.externalHostID contains exactly one slash; malformed input fails with an unhelpful Jinja index error before the delete flow can decide whether to skip or deprovision.

Proposed fix
+- name: Validate externalHostID format
+  ansible.builtin.assert:
+    that:
+      - (bare_metal_instance.spec.externalHostID | default('')) is match('^[^/]+/[^/]+$')
+    fail_msg: "spec.externalHostID must be '<namespace>/<name>'"
+
 - name: Parse externalHostID into namespace and name
   ansible.builtin.set_fact:
     bmh_namespace: "{{ bare_metal_instance.spec.externalHostID.split('/')[0] }}"
     bmh_name: "{{ bare_metal_instance.spec.externalHostID.split('/')[1] }}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@collections/ansible_collections/osac/templates/roles/bm_host_metal3_provisioning/tasks/delete.yaml`
around lines 2 - 5, The task that sets bmh_namespace and bmh_name by splitting
bare_metal_instance.spec.externalHostID assumes the value contains exactly one
slash without any validation. If the externalHostID is malformed or empty, the
split operation will fail with an unhelpful Jinja index error. Add a validation
step before the split that checks whether externalHostID is in the correct
format (contains exactly one slash resulting in two parts when split), and if
validation fails, use a task to fail with a clear error message that explains
the expected format of externalHostID.

Comment on lines +7 to +13
- name: Get BareMetalHost
kubernetes.core.k8s_info:
api_version: metal3.io/v1alpha1
kind: BareMetalHost
namespace: "{{ bmh_namespace }}"
name: "{{ bmh_name }}"
register: bmh_result

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pass the remote kubeconfig to every Kubernetes module call.

These Kubernetes tasks also omit kubeconfig, so deprovisioning and Secret deletion can target the wrong cluster context.

Proposed fix pattern
   kubernetes.core.k8s_info:
+    kubeconfig: "{{ kubeconfig | default(omit) }}"
     api_version: metal3.io/v1alpha1
     kind: BareMetalHost
   kubernetes.core.k8s:
+    kubeconfig: "{{ kubeconfig | default(omit) }}"
     api_version: v1
     kind: Secret

Apply the same module argument to every Kubernetes task in this file.

As per coding guidelines, “Always include osac.service.common to get remote_cluster_kubeconfig before creating K8s resources on remote clusters.” Based on learnings, use kubeconfig: "{{ kubeconfig | default(omit) }}" for this collection’s role task YAMLs.

Also applies to: 39-50, 52-61, 63-69, 71-77

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@collections/ansible_collections/osac/templates/roles/bm_host_metal3_provisioning/tasks/delete.yaml`
around lines 7 - 13, The kubernetes.core.k8s_info module call is missing the
kubeconfig parameter, which can cause the task to target the wrong cluster
context when deprovisioning resources. Add the kubeconfig parameter `kubeconfig:
"{{ kubeconfig | default(omit) }}"` to the kubernetes.core.k8s_info task shown
in the diff, and apply the same kubeconfig parameter addition to all other
Kubernetes module calls in this file including the k8s module invocations at the
line ranges mentioned (39-50, 52-61, 63-69, 71-77) to ensure consistent cluster
targeting across all resource operations.

Sources: Coding guidelines, Learnings

Comment on lines +27 to +37
- name: Check if BMH has an image set
ansible.builtin.set_fact:
bmh_has_image: "{{ bmh_info.spec.image is defined and bmh_info.spec.image.url is defined and bmh_info.spec.image.url | length > 0 }}"

- name: Skip deprovisioning when no image is set
when: not (bmh_has_image | bool)
ansible.builtin.debug:
msg: "BMH {{ bmh_namespace }}/{{ bmh_name }} has no image set, skipping deprovisioning"

- name: Remove image and networkData from BMH
when: bmh_has_image | bool

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clear stale BMH data refs even when no image URL is set.

The patch is gated only on bmh_has_image, but the generated Secrets are deleted afterward. If spec.userData or spec.networkData remains while spec.image.url is absent, this leaves the BMH pointing at deleted Secrets.

Proposed fix
-    - name: Check if BMH has an image set
+    - name: Check if BMH has provisioning fields set
       ansible.builtin.set_fact:
-        bmh_has_image: "{{ bmh_info.spec.image is defined and bmh_info.spec.image.url is defined and bmh_info.spec.image.url | length > 0 }}"
+        bmh_has_provisioning_fields: >-
+          {{ (bmh_info.spec.image is defined and bmh_info.spec.image.url is defined and bmh_info.spec.image.url | length > 0) or
+             (bmh_info.spec.networkData is defined) or
+             (bmh_info.spec.userData is defined) }}
 
     - name: Skip deprovisioning when no image is set
-      when: not (bmh_has_image | bool)
+      when: not (bmh_has_provisioning_fields | bool)
       ansible.builtin.debug:
-        msg: "BMH {{ bmh_namespace }}/{{ bmh_name }} has no image set, skipping deprovisioning"
+        msg: "BMH {{ bmh_namespace }}/{{ bmh_name }} has no provisioning fields set, skipping deprovisioning"
 
     - name: Remove image and networkData from BMH
-      when: bmh_has_image | bool
+      when: bmh_has_provisioning_fields | bool
       block:

Also applies to: 63-77

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@collections/ansible_collections/osac/templates/roles/bm_host_metal3_provisioning/tasks/delete.yaml`
around lines 27 - 37, The task "Remove image and networkData from BMH" is
currently gated only on the bmh_has_image condition being true, but this leaves
stale references to Secrets when spec.userData or spec.networkData are present
without an image URL being set. Modify the logic to ensure that spec.userData
and spec.networkData are cleared from the BMH regardless of whether
bmh_has_image is true, so that dangling Secret references are not left behind
after Secret cleanup operations.

@adriengentil

Copy link
Copy Markdown
Contributor

/retest


- name: Parse existing cloud-init and merge SSH key
ansible.builtin.set_fact:
final_user_data: >-

@adriengentil adriengentil Jun 19, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if spec.metaData maps to cloud init metadata, then the ssh keys can be injected there: https://docs.cloud-init.io/en/24.1/explanation/instancedata.html#v1-public-ssh-keys.
It's then it's the image that will configure the default account with these keys (fedora user for Fedora, core user for RHCOS, ...). From what I've seen, it's a common pattern in cloud providers, and even in kubevirt.

@openshift-ci

openshift-ci Bot commented Jun 19, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: adriengentil, carbonin

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants