From aa96e36b2ad58930a383f2cd1bc349e275e43790 Mon Sep 17 00:00:00 2001 From: Nicholas Kuechler Date: Wed, 27 May 2026 12:48:12 -0500 Subject: [PATCH] feat(ironic): Adds ironic runbook for performing BMC maintenances. --- components/ironic/runbook-crd/README.md | 46 +++++ .../baremetal.ironicproject.org_runbooks.yaml | 25 ++- .../ironic/runbook-crd/kustomization.yaml | 1 + .../runbooks/runbook_bmc_maintenance.yaml | 20 +++ components/ironic/runbook-operator/role.yaml | 6 +- .../shell-operator-ironic.yaml | 2 +- .../hooks/create_runbook.sh | 160 +++++++++++++++--- .../hooks/delete_runbook.sh | 22 ++- .../hooks/update_runbook.sh | 151 ++++++++++++++--- 9 files changed, 378 insertions(+), 55 deletions(-) create mode 100644 components/ironic/runbook-crd/runbooks/runbook_bmc_maintenance.yaml diff --git a/components/ironic/runbook-crd/README.md b/components/ironic/runbook-crd/README.md index 9eba03f28..151d574fc 100644 --- a/components/ironic/runbook-crd/README.md +++ b/components/ironic/runbook-crd/README.md @@ -90,6 +90,52 @@ spec: | `runbook_disk_cleaning.yaml` | Node Reuse | Secure disk erasure | | `runbook_gpu_node_setup.yaml` | ML/AI | GPU node configuration | +## Running a Runbook + +Once the operator syncs the CRD into Ironic, you execute a runbook by +transitioning a node into the `clean` provisioning state with the runbook +specified. The node must be in `manageable` state before you can trigger +cleaning. + +### OpenStack CLI + +```bash +# Run a runbook by name +openstack baremetal node clean --runbook CUSTOM_BMC_MAINTENANCE + +# Check node state while cleaning runs +openstack baremetal node show -f value -c provision_state +``` + +### Python SDK + +```python +from understack_workflows.ironic_node import transition + +# node must already be in manageable state +transition( + node, + "clean", + expected_state="manageable", + runbook=runbook_uuid, +) +``` + +The `transition` helper calls `set_node_provision_state` and waits for the +node to return to `manageable` once all steps complete. + +### Trait-Based Automatic Execution + +Runbooks can also be triggered automatically by matching node traits. Add the +runbook name as a trait on the node: + +```bash +openstack baremetal node add trait CUSTOM_BMC_MAINTENANCE +``` + +Workflow code (e.g. `apply_firmware_updates` in `ironic_node.py`) can then +discover matching traits and execute the corresponding runbooks in order. + ## Support - **Ironic Documentation**: https://docs.openstack.org/ironic/latest/ diff --git a/components/ironic/runbook-crd/bases/baremetal.ironicproject.org_runbooks.yaml b/components/ironic/runbook-crd/bases/baremetal.ironicproject.org_runbooks.yaml index e165f75c7..7b97e7263 100644 --- a/components/ironic/runbook-crd/bases/baremetal.ironicproject.org_runbooks.yaml +++ b/components/ironic/runbook-crd/bases/baremetal.ironicproject.org_runbooks.yaml @@ -41,11 +41,25 @@ spec: - steps properties: runbookName: - description: 'RunbookName is the unique name of the runbook (REQUIRED). Must match trait naming convention, typically CUSTOM_*. This name is used to match runbooks to nodes via traits.' + description: 'RunbookName is the unique name of the runbook (REQUIRED). From API microversion 1.112+, this is a logical identifier and can be any string of 1-255 characters. Node eligibility is determined by the traits field instead.' type: string - pattern: '^CUSTOM_[A-Z0-9_]+$' + pattern: '^[a-zA-Z0-9._-]+$' minLength: 1 maxLength: 255 + description: + description: 'Description is a human-readable description of the runbook (OPTIONAL). Consistent with other Ironic objects. Available from API microversion 1.112 onwards.' + type: string + nullable: true + maxLength: 1000 + traits: + description: 'Traits is a list of traits that determine which nodes are permitted to use this runbook (OPTIONAL). Decouples runbook eligibility from the runbook name. Each trait must follow the CUSTOM_* naming convention. Available from API microversion 1.112 onwards. Default: []' + type: array + default: [] + items: + type: string + pattern: '^CUSTOM_[A-Z0-9_]+$' + minLength: 1 + maxLength: 255 steps: description: 'Steps is an ordered list of operations to execute (REQUIRED). Minimum 1 step required.' type: array @@ -164,8 +178,13 @@ spec: additionalPrinterColumns: - name: Runbook Name type: string - description: The runbook name used for trait matching + description: The runbook name jsonPath: .spec.runbookName + - name: Description + type: string + description: Human-readable description of the runbook + jsonPath: .spec.description + priority: 1 - name: Steps type: integer description: Number of steps in the runbook diff --git a/components/ironic/runbook-crd/kustomization.yaml b/components/ironic/runbook-crd/kustomization.yaml index 51e36b944..416ca9728 100644 --- a/components/ironic/runbook-crd/kustomization.yaml +++ b/components/ironic/runbook-crd/kustomization.yaml @@ -7,3 +7,4 @@ namespace: openstack # Create namespace if it doesn't exist resources: - bases/baremetal.ironicproject.org_runbooks.yaml + - runbooks/runbook_bmc_maintenance.yaml diff --git a/components/ironic/runbook-crd/runbooks/runbook_bmc_maintenance.yaml b/components/ironic/runbook-crd/runbooks/runbook_bmc_maintenance.yaml new file mode 100644 index 000000000..ea831afe7 --- /dev/null +++ b/components/ironic/runbook-crd/runbooks/runbook_bmc_maintenance.yaml @@ -0,0 +1,20 @@ +apiVersion: baremetal.ironicproject.org/v1alpha1 +kind: IronicRunbook +metadata: + name: bmc-maintenance + namespace: openstack +spec: + runbookName: bmc-maintenance + description: "Performs BMC maintenance operations including clearing the job queue and synchronizing the BMC clock." + traits: + - CUSTOM_DELL_ABC + - CUSTOM_DELL_XYZ + + steps: + - interface: management + step: clear_job_queue + order: 1 + + - interface: management + step: set_bmc_clock + order: 2 diff --git a/components/ironic/runbook-operator/role.yaml b/components/ironic/runbook-operator/role.yaml index 3a91993c4..284d911cd 100644 --- a/components/ironic/runbook-operator/role.yaml +++ b/components/ironic/runbook-operator/role.yaml @@ -6,7 +6,7 @@ metadata: app.kubernetes.io/name: ironicrunbook app.kubernetes.io/component: rbac rules: - # Read-only runbook permissions + # Runbook permissions - apiGroups: - baremetal.ironicproject.org resources: @@ -16,10 +16,12 @@ rules: - list - watch - # Read-only status permissions + # Status update permissions - apiGroups: - baremetal.ironicproject.org resources: - ironicrunbooks/status verbs: - get + - patch + - update diff --git a/components/ironic/runbook-operator/shell-operator-ironic.yaml b/components/ironic/runbook-operator/shell-operator-ironic.yaml index 763df655d..87ca06522 100644 --- a/components/ironic/runbook-operator/shell-operator-ironic.yaml +++ b/components/ironic/runbook-operator/shell-operator-ironic.yaml @@ -17,7 +17,7 @@ spec: restartPolicy: Always containers: - name: shell-operator - image: ghcr.io/rackerlabs/understack/shell-operator-ironic:latest + image: ghcr.io/rackerlabs/understack/shell-operator-ironic:pr-2051 imagePullPolicy: Always env: - name: OS_CLOUD diff --git a/containers/shell-operator-ironic/hooks/create_runbook.sh b/containers/shell-operator-ironic/hooks/create_runbook.sh index 2fceed2c6..0b8371372 100755 --- a/containers/shell-operator-ironic/hooks/create_runbook.sh +++ b/containers/shell-operator-ironic/hooks/create_runbook.sh @@ -10,42 +10,158 @@ if [[ $1 == "--config" ]] ; then "executeHookOnEvent":[ "Added" ] }], "settings": { - "executionMinInterval": 30s, + "executionMinInterval": "30s", "executionBurst": 1 } } EOF else + patch_status() { + local namespace="$1" + local name="$2" + local sync_status="$3" + local message="${4:-}" + local now + now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + local condition_status="True" + local reason="SyncSucceeded" + if [[ "${sync_status}" == "Failed" ]]; then + condition_status="False" + reason="SyncFailed" + fi + + local patch + patch=$(cat </dev/null || \ + echo "[create_runbook] WARNING: failed to patch status for ${name}" + } + + sync_runbook() { + local i="$1" + local resource_name namespace kind runbook_name description public owner + + resource_name=$(jq -r ".${i}.metadata.name" "${BINDING_CONTEXT_PATH}") + namespace=$(jq -r ".${i}.metadata.namespace" "${BINDING_CONTEXT_PATH}") + kind=$(jq -r ".${i}.kind" "${BINDING_CONTEXT_PATH}") + runbook_name=$(jq -r ".${i}.spec.runbookName" "${BINDING_CONTEXT_PATH}") + description=$(jq -r ".${i}.spec.description // empty" "${BINDING_CONTEXT_PATH}") + public=$(jq -r ".${i}.spec.public // empty" "${BINDING_CONTEXT_PATH}") + owner=$(jq -r ".${i}.spec.owner // empty" "${BINDING_CONTEXT_PATH}") + + echo "[create_runbook] Creating runbook kind=${kind} name=${resource_name} namespace=${namespace} runbookName=${runbook_name} description=${description} public=${public} owner=${owner}" + + jq -r ".${i}.spec.steps" "${BINDING_CONTEXT_PATH}" > /tmp/steps.json + + command_args=(baremetal runbook create --name "${runbook_name}" --steps /tmp/steps.json) + + if [[ -n "${description}" ]]; then + command_args+=(--description "${description}") + fi + if [[ -n "${public}" ]]; then + command_args+=(--public "${public}") + fi + if [[ -n "${owner}" ]]; then + command_args+=(--owner "${owner}") + fi + + echo "[create_runbook] Running: openstack ${command_args[*]}" + + if output=$(openstack "${command_args[@]}" 2>&1); then + echo "[create_runbook] SUCCESS: Runbook created in Ironic name=${resource_name} output=${output}" + + traits_json=$(jq -c ".${i}.spec.traits // []" "${BINDING_CONTEXT_PATH}") + if [[ "${traits_json}" != "[]" ]]; then + echo "[create_runbook] Setting traits name=${resource_name} traits=${traits_json}" + ironic_endpoint=$(openstack endpoint list --service baremetal --interface internal -f value -c URL 2>/dev/null | head -1) + if [[ -n "${ironic_endpoint}" ]]; then + token=$(openstack token issue -f value -c id) + echo "[create_runbook] PUT ${ironic_endpoint}/v1/runbooks/${runbook_name}/traits" + trait_response=$(curl -s -X PUT \ + -H "Content-Type: application/json" \ + -H "X-Auth-Token: ${token}" \ + -H "X-OpenStack-Ironic-API-Version: 1.112" \ + -d "{\"traits\": ${traits_json}}" \ + "${ironic_endpoint}/v1/runbooks/${runbook_name}/traits") + echo "[create_runbook] Traits response name=${resource_name} response=${trait_response}" + else + echo "[create_runbook] WARNING: Could not determine Ironic endpoint for traits" + fi + else + echo "[create_runbook] No traits to set name=${resource_name}" + fi + + patch_status "${namespace}" "${resource_name}" "Synced" "Successfully created runbook in Ironic" + echo "[create_runbook] Completed name=${resource_name} status=Synced" + else + # If it already exists, that's OK during sync - not an error + if echo "${output}" | grep -qi "already exists\|Conflict\|409"; then + echo "[create_runbook] Runbook already exists in Ironic name=${resource_name}, skipping create" + patch_status "${namespace}" "${resource_name}" "Synced" "Runbook already exists in Ironic" + else + echo "[create_runbook] FAILED: name=${resource_name} error=${output}" >&2 + patch_status "${namespace}" "${resource_name}" "Failed" "${output}" + return 1 + fi + fi + } + + echo "[create_runbook] Hook invoked, processing binding contexts" binding_count=$(jq -r 'length' "${BINDING_CONTEXT_PATH}") + echo "[create_runbook] Found ${binding_count} binding context(s)" + for ((i = 0; i < binding_count; i++)); do type=$(jq -r ".[$i].type" "${BINDING_CONTEXT_PATH}") + echo "[create_runbook] Processing context=${i} type=${type}" + if [[ $type == "Synchronization" ]] ; then - echo "Implement any reconciliation logic needed here." + echo "[create_runbook] Synchronization event, reconciling existing resources" + objects_count=$(jq -r ".[$i].objects | length" "${BINDING_CONTEXT_PATH}") + echo "[create_runbook] Found ${objects_count} existing IronicRunbook(s) to reconcile" + for ((j = 0; j < objects_count; j++)); do + obj_name=$(jq -r ".[$i].objects[$j].object.metadata.name" "${BINDING_CONTEXT_PATH}") + obj_sync=$(jq -r ".[$i].objects[$j].object.status.syncStatus // empty" "${BINDING_CONTEXT_PATH}") + echo "[create_runbook] Checking name=${obj_name} syncStatus=${obj_sync}" + if [[ -z "${obj_sync}" || "${obj_sync}" == "null" ]]; then + echo "[create_runbook] Resource name=${obj_name} has no syncStatus, needs reconciliation" + # Re-map the jq path to point at the object within the sync event + ORIG_BINDING_CONTEXT_PATH="${BINDING_CONTEXT_PATH}" + jq -r ".[$i].objects[$j].object" "${BINDING_CONTEXT_PATH}" > /tmp/sync_object.json + BINDING_CONTEXT_PATH=/tmp/sync_object.json + sync_runbook "" + BINDING_CONTEXT_PATH="${ORIG_BINDING_CONTEXT_PATH}" + else + echo "[create_runbook] Resource name=${obj_name} already synced, skipping" + fi + done continue fi if [[ $type == "Event" ]] ; then - resource_name=$(jq -r ".[$i].object.metadata.name" "${BINDING_CONTEXT_PATH}") - kind=$(jq -r ".[$i].object.kind" "${BINDING_CONTEXT_PATH}") - - runbook_name=$(jq -r ".[$i].object.spec.runbookName" "${BINDING_CONTEXT_PATH}") - public=$(jq -r ".[$i].object.spec.public" "${BINDING_CONTEXT_PATH}") - owner=$(jq -r ".[$i].object.spec.owner" "${BINDING_CONTEXT_PATH}") - jq -r ".[$i].object.spec.steps" "${BINDING_CONTEXT_PATH}" > /tmp/steps.yaml - - # Ironic's runbook extra field is essentially a dict of dicts, representing a key values. baremetal cli allows you - # to pass in multiple --extra options, adding any you do pass. We would need to make an initial query to determine - # existing extras, and then sync the differences. This work is probably better suited to a full controller implementation. - # extra=$(jq -r '.spec.extra | [to_entries[] | "--extra \(.key)=\(.value | @json | @sh)"] | join(" ")' ${BINDING_CONTEXT_PATH}) - - command_args=(baremetal runbook create --name "${runbook_name}" --public "${public}" --steps /tmp/steps.yaml) - if [[ -n "${owner}" && "${owner}" != "null" ]]; then - command_args+=(--owner "${owner}") + sync_runbook "[$i].object" + if [[ $? -ne 0 ]]; then + exit 1 fi - - echo "${kind}/${resource_name} created, running: openstack ${command_args[*]}" - - openstack "${command_args[@]}" fi done + echo "[create_runbook] Hook finished" fi diff --git a/containers/shell-operator-ironic/hooks/delete_runbook.sh b/containers/shell-operator-ironic/hooks/delete_runbook.sh index 53439d30a..ec4c36d88 100755 --- a/containers/shell-operator-ironic/hooks/delete_runbook.sh +++ b/containers/shell-operator-ironic/hooks/delete_runbook.sh @@ -10,30 +10,42 @@ if [[ $1 == "--config" ]] ; then "executeHookOnEvent":[ "Deleted" ] }], "settings": { - "executionMinInterval": 30s, + "executionMinInterval": "30s", "executionBurst": 1 } } EOF else + echo "[delete_runbook] Hook invoked, processing binding contexts" binding_count=$(jq -r 'length' "${BINDING_CONTEXT_PATH}") + echo "[delete_runbook] Found ${binding_count} binding context(s)" + for ((i = 0; i < binding_count; i++)); do type=$(jq -r ".[$i].type" "${BINDING_CONTEXT_PATH}") + echo "[delete_runbook] Processing context=${i} type=${type}" + if [[ $type == "Synchronization" ]] ; then - echo "Implement any reconciliation logic needed here." + echo "[delete_runbook] Synchronization event, nothing to do for deletes" continue fi if [[ $type == "Event" ]] ; then resource_name=$(jq -r ".[$i].object.metadata.name" "${BINDING_CONTEXT_PATH}") kind=$(jq -r ".[$i].object.kind" "${BINDING_CONTEXT_PATH}") - runbook_name=$(jq -r ".[$i].object.spec.runbookName" "${BINDING_CONTEXT_PATH}") + echo "[delete_runbook] Deleting runbook kind=${kind} name=${resource_name} runbookName=${runbook_name}" + command_args=(baremetal runbook delete "${runbook_name}") - echo "${kind}/${resource_name} deleted, running: openstack ${command_args[*]}" + echo "[delete_runbook] Running: openstack ${command_args[*]}" - openstack "${command_args[@]}" + if output=$(openstack "${command_args[@]}" 2>&1); then + echo "[delete_runbook] SUCCESS: Runbook deleted from Ironic name=${resource_name} output=${output}" + else + echo "[delete_runbook] FAILED: name=${resource_name} error=${output}" >&2 + exit 1 + fi fi done + echo "[delete_runbook] Hook finished" fi diff --git a/containers/shell-operator-ironic/hooks/update_runbook.sh b/containers/shell-operator-ironic/hooks/update_runbook.sh index 216ec5c7e..cbd74dd31 100755 --- a/containers/shell-operator-ironic/hooks/update_runbook.sh +++ b/containers/shell-operator-ironic/hooks/update_runbook.sh @@ -10,42 +10,149 @@ if [[ $1 == "--config" ]] ; then "executeHookOnEvent":[ "Modified" ] }], "settings": { - "executionMinInterval": 30s, + "executionMinInterval": "30s", "executionBurst": 1 } } EOF else + patch_status() { + local namespace="$1" + local name="$2" + local sync_status="$3" + local message="${4:-}" + local now + now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + + local condition_status="True" + local reason="SyncSucceeded" + if [[ "${sync_status}" == "Failed" ]]; then + condition_status="False" + reason="SyncFailed" + fi + + local patch + patch=$(cat </dev/null || \ + echo "[update_runbook] WARNING: failed to patch status for ${name}" + } + + sync_runbook() { + local i="$1" + local resource_name namespace kind runbook_name description public owner + + resource_name=$(jq -r ".${i}.metadata.name" "${BINDING_CONTEXT_PATH}") + namespace=$(jq -r ".${i}.metadata.namespace" "${BINDING_CONTEXT_PATH}") + kind=$(jq -r ".${i}.kind" "${BINDING_CONTEXT_PATH}") + runbook_name=$(jq -r ".${i}.spec.runbookName" "${BINDING_CONTEXT_PATH}") + description=$(jq -r ".${i}.spec.description // empty" "${BINDING_CONTEXT_PATH}") + public=$(jq -r ".${i}.spec.public // empty" "${BINDING_CONTEXT_PATH}") + owner=$(jq -r ".${i}.spec.owner // empty" "${BINDING_CONTEXT_PATH}") + + echo "[update_runbook] Updating runbook kind=${kind} name=${resource_name} namespace=${namespace} runbookName=${runbook_name} description=${description} public=${public} owner=${owner}" + + jq -r ".${i}.spec.steps" "${BINDING_CONTEXT_PATH}" > /tmp/steps.json + + command_args=(baremetal runbook set "${runbook_name}") + command_args+=(--name "${runbook_name}" --steps /tmp/steps.json) + + if [[ -n "${description}" ]]; then + command_args+=(--description "${description}") + fi + if [[ -n "${owner}" ]]; then + command_args+=(--owner "${owner}") + fi + + echo "[update_runbook] Running: openstack ${command_args[*]}" + + if output=$(openstack "${command_args[@]}" 2>&1); then + echo "[update_runbook] SUCCESS: Runbook updated in Ironic name=${resource_name} output=${output}" + + traits_json=$(jq -c ".${i}.spec.traits // []" "${BINDING_CONTEXT_PATH}") + if [[ "${traits_json}" != "[]" ]]; then + echo "[update_runbook] Setting traits name=${resource_name} traits=${traits_json}" + ironic_endpoint=$(openstack endpoint list --service baremetal --interface internal -f value -c URL 2>/dev/null | head -1) + if [[ -n "${ironic_endpoint}" ]]; then + token=$(openstack token issue -f value -c id) + echo "[update_runbook] PUT ${ironic_endpoint}/v1/runbooks/${runbook_name}/traits" + trait_response=$(curl -s -X PUT \ + -H "Content-Type: application/json" \ + -H "X-Auth-Token: ${token}" \ + -H "X-OpenStack-Ironic-API-Version: 1.112" \ + -d "{\"traits\": ${traits_json}}" \ + "${ironic_endpoint}/v1/runbooks/${runbook_name}/traits") + echo "[update_runbook] Traits response name=${resource_name} response=${trait_response}" + else + echo "[update_runbook] WARNING: Could not determine Ironic endpoint for traits" + fi + else + echo "[update_runbook] No traits to set name=${resource_name}" + fi + + patch_status "${namespace}" "${resource_name}" "Synced" "Successfully updated runbook in Ironic" + echo "[update_runbook] Completed name=${resource_name} status=Synced" + else + echo "[update_runbook] FAILED: name=${resource_name} error=${output}" >&2 + patch_status "${namespace}" "${resource_name}" "Failed" "${output}" + return 1 + fi + } + + echo "[update_runbook] Hook invoked, processing binding contexts" binding_count=$(jq -r 'length' "${BINDING_CONTEXT_PATH}") + echo "[update_runbook] Found ${binding_count} binding context(s)" + for ((i = 0; i < binding_count; i++)); do type=$(jq -r ".[$i].type" "${BINDING_CONTEXT_PATH}") + echo "[update_runbook] Processing context=${i} type=${type}" + if [[ $type == "Synchronization" ]] ; then - echo "Implement any reconciliation logic needed here." + echo "[update_runbook] Synchronization event, reconciling existing resources" + objects_count=$(jq -r ".[$i].objects | length" "${BINDING_CONTEXT_PATH}") + echo "[update_runbook] Found ${objects_count} existing IronicRunbook(s) to reconcile" + for ((j = 0; j < objects_count; j++)); do + obj_name=$(jq -r ".[$i].objects[$j].object.metadata.name" "${BINDING_CONTEXT_PATH}") + obj_sync=$(jq -r ".[$i].objects[$j].object.status.syncStatus // empty" "${BINDING_CONTEXT_PATH}") + echo "[update_runbook] Checking name=${obj_name} syncStatus=${obj_sync}" + if [[ -z "${obj_sync}" || "${obj_sync}" == "null" || "${obj_sync}" == "Failed" ]]; then + echo "[update_runbook] Resource name=${obj_name} needs sync (syncStatus=${obj_sync}), reconciling" + ORIG_BINDING_CONTEXT_PATH="${BINDING_CONTEXT_PATH}" + jq -r ".[$i].objects[$j].object" "${BINDING_CONTEXT_PATH}" > /tmp/sync_object.json + BINDING_CONTEXT_PATH=/tmp/sync_object.json + sync_runbook "" + BINDING_CONTEXT_PATH="${ORIG_BINDING_CONTEXT_PATH}" + else + echo "[update_runbook] Resource name=${obj_name} already synced, skipping" + fi + done continue fi if [[ $type == "Event" ]] ; then - resource_name=$(jq -r ".[$i].object.metadata.name" "${BINDING_CONTEXT_PATH}") - kind=$(jq -r ".[$i].object.kind" "${BINDING_CONTEXT_PATH}") - - runbook_name=$(jq -r ".[$i].object.spec.runbookName" "${BINDING_CONTEXT_PATH}") - public=$(jq -r ".[$i].object.spec.public" "${BINDING_CONTEXT_PATH}") - owner=$(jq -r ".[$i].object.spec.owner" "${BINDING_CONTEXT_PATH}") - jq -r ".[$i].object.spec.steps" "${BINDING_CONTEXT_PATH}" > /tmp/steps.yaml - - # Ironic's runbook extra field is essentially a dict of dicts, representing a key values. baremetal cli allows you - # to pass in multiple --extra options, adding any you do pass. We would need to make an initial query to determine - # existing extras, and then sync the differences. This work is probably better suited to a full controller implementation. - # extra=$(jq -r '.spec.extra | [to_entries[] | "--extra \(.key)=\(.value | @json | @sh)"] | join(" ")' ${BINDING_CONTEXT_PATH}) - - command_args=(baremetal runbook set "${runbook_name}" --public "${public}" --steps /tmp/steps.yaml) - if [[ -n "${owner}" && "${owner}" != "null" ]]; then - command_args+=(--owner "${owner}") + sync_runbook "[$i].object" + if [[ $? -ne 0 ]]; then + exit 1 fi - - echo "${kind}/${resource_name} updated, running: openstack ${command_args[*]}" - - openstack "${command_args[@]}" fi done + echo "[update_runbook] Hook finished" fi