Skip to content

Add AAP provisioning and deprovisioning workflow - #4

Merged
tzumainn merged 1 commit into
osac-project:mainfrom
DanNiESh:provision-update
May 19, 2026
Merged

Add AAP provisioning and deprovisioning workflow#4
tzumainn merged 1 commit into
osac-project:mainfrom
DanNiESh:provision-update

Conversation

@DanNiESh

Copy link
Copy Markdown
Contributor

Summary

  • Integrate AAP (Ansible Automation Platform) as a provisioning provider to trigger image provisioning and
    deprovisioning jobs for HostLease resources
  • Refactor status management to centralize writes in Reconcile() instead of scattering Status().Update()
    calls across helpers

Changes

Provisioning lifecycle (reconcileProvisioning)

  • When spec.templateID is set (and not "noop"), triggers an AAP provision job before power reconciliation
  • Polls job status on a 30s interval, sets ProvisionTemplateComplete condition to track progress/success/failure
  • Computes a desiredConfigVersion hash to detect spec drift and re-provision
  • Flushes status before polling to prevent duplicate job launches on crash recovery

Deprovisioning lifecycle (reconcileDeprovisioning)

  • On deletion, triggers an AAP deprovision job before removing the finalizer
  • Polls for completion, sets DeprovisionTemplateComplete condition
  • Flushes status after trigger to guard against duplicate jobs on restart

Status management refactor

  • syncHostLeaseStatus now mutates the in-memory object only (no longer calls Status().Update())
  • Reconcile() does a single Status().Update() at the end by comparing old vs new status with
    equality.Semantic.DeepEqual
  • Reconcile split into handleUpdate / reconcileDelete for clarity

Other

  • AAP client configured via env vars (OSAC_AAP_URL, OSAC_AAP_TOKEN, OSAC_AAP_INSECURE_SKIP_VERIFY,
    OSAC_AAP_TEMPLATE_PREFIX)
  • Upgraded Go to 1.26, updated bare-metal-operator -> bare-metal-fulfillment-operator, bumped
    osac-operator to v0.1.1

@DanNiESh
DanNiESh requested review from ajamias, larsks and tzumainn May 19, 2026 13:03
@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Rate limit exceeded

@DanNiESh has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 23 minutes and 22 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: 59062c3d-654b-42bb-a7bc-8bfec14f7ef0

📥 Commits

Reviewing files that changed from the base of the PR and between fd2d163 and 2af5276.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (8)
  • Containerfile
  • cmd/main.go
  • cmd/main_test.go
  • config/samples/v1alpha1_hostlease.yaml
  • go.mod
  • internal/controller/hostlease_controller.go
  • internal/controller/hostlease_controller_test.go
  • internal/controller/hostlease_names.go

Walkthrough

This PR integrates AAP-backed provisioning and deprovisioning workflows into the HostLeaseReconciler. It updates the toolchain (Go 1.25→1.26), replaces bare-metal-operator with bare-metal-fulfillment-operator, introduces AAP environment variable configuration, initializes the provisioning provider in main(), extends the reconciler to accept and use the provider, implements provisioning/deprovisioning job lifecycle management, refactors status persistence logic, and adds comprehensive test coverage. A sample HostLease configuration is updated to use the new provisioning template.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: integrating AAP for provisioning and deprovisioning workflows for HostLease resources, which aligns with the substantial changes throughout the codebase.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, providing clear details about the provisioning/deprovisioning lifecycle, status management refactor, and dependency updates that match the actual changes in the PR.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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: 1

🧹 Nitpick comments (1)
internal/controller/hostlease_controller_test.go (1)

690-837: ⚡ Quick win

Assert “no provisioning call” explicitly in skip-path tests.

These tests only assert Reconcile returns success; they don’t verify that provisioning APIs were not invoked. That can let regressions pass if a provider call happens but still returns nil. Use a test double for ProvisioningProvider with call counters and assert zero invocations in the nil/noop/empty/already-succeeded scenarios (and optionally assert jobs/conditions remain unchanged where applicable).

As per coding guidelines, "-Focus on major issues impacting performance, readability, maintainability and security. Avoid nitpicks and avoid verbosity."

🤖 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 `@internal/controller/hostlease_controller_test.go` around lines 690 - 837, The
tests in Describe("reconcileProvisioning") only assert Reconcile returns
successfully but do not verify that the ProvisioningProvider was not invoked;
add a test-double implementation of provisioning.AAPProvider (or an interface
wrapper used by reconciler.ProvisioningProvider) with a call counter for the
provision method(s) and inject it into each skip-path test (the
nil/noop/empty-template/already-succeeded cases) and assert the counter is zero
after reconciler.Reconcile returns; additionally for the already-succeeded case
assert HostLease.Status.Jobs (and any relevant conditions) remain unchanged to
ensure no new job was created.
🤖 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 `@cmd/main.go`:
- Around line 238-247: When OSAC_AAP_URL (controller.EnvAAPURL) is set but
OSAC_AAP_TOKEN (controller.EnvAAPToken) is missing, modify the boot logic to
fail fast instead of constructing a broken aap client; check the value of
aapToken after reading it from the environment and return/log a fatal error
(stop startup) if it's empty. Update the block that constructs
aap.NewClient(...) and provisioning.NewProvider(...) so you validate aapToken
first and only call aap.NewClient and provisioning.NewProvider with a valid
token, ensuring the application exits with a clear error rather than continuing
with an invalid provisioning.ProviderConfig.

---

Nitpick comments:
In `@internal/controller/hostlease_controller_test.go`:
- Around line 690-837: The tests in Describe("reconcileProvisioning") only
assert Reconcile returns successfully but do not verify that the
ProvisioningProvider was not invoked; add a test-double implementation of
provisioning.AAPProvider (or an interface wrapper used by
reconciler.ProvisioningProvider) with a call counter for the provision method(s)
and inject it into each skip-path test (the
nil/noop/empty-template/already-succeeded cases) and assert the counter is zero
after reconciler.Reconcile returns; additionally for the already-succeeded case
assert HostLease.Status.Jobs (and any relevant conditions) remain unchanged to
ensure no new job was created.
🪄 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: Organization UI

Review profile: CHILL

Plan: Enterprise

Run ID: f1799af5-f1c3-4624-8c77-15a8c12855da

📥 Commits

Reviewing files that changed from the base of the PR and between d206b73 and fd2d163.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (8)
  • Containerfile
  • cmd/main.go
  • cmd/main_test.go
  • config/samples/v1alpha1_hostlease.yaml
  • go.mod
  • internal/controller/hostlease_controller.go
  • internal/controller/hostlease_controller_test.go
  • internal/controller/hostlease_names.go

Comment thread cmd/main.go Outdated
Comment on lines +238 to +247
if aapURL := os.Getenv(controller.EnvAAPURL); aapURL != "" {
aapToken := os.Getenv(controller.EnvAAPToken)
insecureSkipVerify, _ := strconv.ParseBool(os.Getenv(controller.EnvAAPInsecureSkipVerify))
templatePrefix := os.Getenv(controller.EnvAAPTemplatePrefix)
if templatePrefix == "" {
templatePrefix = "osac"
}
aapClient := aap.NewClient(aapURL, aapToken, insecureSkipVerify)
var err error
provisioningProvider, err = provisioning.NewProvider(provisioning.ProviderConfig{

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

Fail fast when AAP URL is configured but token is missing.

When OSAC_AAP_URL is set and OSAC_AAP_TOKEN is empty, the controller still boots with a broken provisioning provider and only fails later at runtime.

Suggested fix
 	if aapURL := os.Getenv(controller.EnvAAPURL); aapURL != "" {
 		aapToken := os.Getenv(controller.EnvAAPToken)
+		if aapToken == "" {
+			setupLog.Error(nil, fmt.Sprintf("%s must be set when %s is configured", controller.EnvAAPToken, controller.EnvAAPURL))
+			os.Exit(1)
+		}
 		insecureSkipVerify, _ := strconv.ParseBool(os.Getenv(controller.EnvAAPInsecureSkipVerify))
 		templatePrefix := os.Getenv(controller.EnvAAPTemplatePrefix)
 		if templatePrefix == "" {
 			templatePrefix = "osac"
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if aapURL := os.Getenv(controller.EnvAAPURL); aapURL != "" {
aapToken := os.Getenv(controller.EnvAAPToken)
insecureSkipVerify, _ := strconv.ParseBool(os.Getenv(controller.EnvAAPInsecureSkipVerify))
templatePrefix := os.Getenv(controller.EnvAAPTemplatePrefix)
if templatePrefix == "" {
templatePrefix = "osac"
}
aapClient := aap.NewClient(aapURL, aapToken, insecureSkipVerify)
var err error
provisioningProvider, err = provisioning.NewProvider(provisioning.ProviderConfig{
if aapURL := os.Getenv(controller.EnvAAPURL); aapURL != "" {
aapToken := os.Getenv(controller.EnvAAPToken)
if aapToken == "" {
setupLog.Error(nil, fmt.Sprintf("%s must be set when %s is configured", controller.EnvAAPToken, controller.EnvAAPURL))
os.Exit(1)
}
insecureSkipVerify, _ := strconv.ParseBool(os.Getenv(controller.EnvAAPInsecureSkipVerify))
templatePrefix := os.Getenv(controller.EnvAAPTemplatePrefix)
if templatePrefix == "" {
templatePrefix = "osac"
}
aapClient := aap.NewClient(aapURL, aapToken, insecureSkipVerify)
var err error
provisioningProvider, err = provisioning.NewProvider(provisioning.ProviderConfig{
🤖 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 `@cmd/main.go` around lines 238 - 247, When OSAC_AAP_URL (controller.EnvAAPURL)
is set but OSAC_AAP_TOKEN (controller.EnvAAPToken) is missing, modify the boot
logic to fail fast instead of constructing a broken aap client; check the value
of aapToken after reading it from the environment and return/log a fatal error
(stop startup) if it's empty. Update the block that constructs
aap.NewClient(...) and provisioning.NewProvider(...) so you validate aapToken
first and only call aap.NewClient and provisioning.NewProvider with a valid
token, ensuring the application exits with a clear error rather than continuing
with an invalid provisioning.ProviderConfig.

@tzumainn tzumainn 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.

Generally, this looks pretty good! Two quick questions:

  • how does the AAP provider know to call playbook_osac_create_host_lease.yml and playbook_osac_delete_host_lease.yml?
  • where are the templates (like bm_host_image_provision) actually passed in?

Comment thread internal/controller/hostlease_names.go Outdated
EnvAAPURL = "OSAC_AAP_URL"
EnvAAPToken = "OSAC_AAP_TOKEN"
EnvAAPInsecureSkipVerify = "OSAC_AAP_INSECURE_SKIP_VERIFY"
EnvAAPTemplatePrefix = "OSAC_AAP_TEMPLATE_PREFIX"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'd move these into cmd/main.go to be consistent with the other OSAC operators. The reason is that these are not necessarily host lease specific values; in an operator with multiple controller, you'd still want to use these values..

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed!

}

// Provisioning runs first — power reconciliation is suspended during provisioning
if r.ProvisioningProvider != nil && hostLease.Spec.TemplateID != "" && hostLease.Spec.TemplateID != "noop" {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is absolutely my fault, but I don't know that noop is actually a good string to use here. Not something that needs to be fixed now, especially since it looks like noop is explicitly set by the bare-metal-fulfillment-operator

@DanNiESh

Copy link
Copy Markdown
Contributor Author

Generally, this looks pretty good! Two quick questions:

  • how does the AAP provider know to call playbook_osac_create_host_lease.yml and playbook_osac_delete_host_lease.yml?
  • where are the templates (like bm_host_image_provision) actually passed in?

resolveTemplateName() takes the action create or delete and converts the resource Kind HostLease to kebab-case (host-lease), then joins them with the prefix (which is set via OSAC_AAP_TEMPLATE_PREFIX.

bm_host_image_provision is passed in as spec.templateID. The osac-create-host-lease.yml reads ansible_eda.event.payload.spec.templateID to decide what role action to take.

@tzumainn

Copy link
Copy Markdown

Generally, this looks pretty good! Two quick questions:

  • how does the AAP provider know to call playbook_osac_create_host_lease.yml and playbook_osac_delete_host_lease.yml?
  • where are the templates (like bm_host_image_provision) actually passed in?

resolveTemplateName() takes the action create or delete and converts the resource Kind HostLease to kebab-case (host-lease), then joins them with the prefix (which is set via OSAC_AAP_TEMPLATE_PREFIX.

bm_host_image_provision is passed in as spec.templateID. The osac-create-host-lease.yml reads ansible_eda.event.payload.spec.templateID to decide what role action to take.

Got it - makes sense!

@DanNiESh
DanNiESh force-pushed the provision-update branch from fd2d163 to 6c65743 Compare May 19, 2026 19:40
- Add provisioning lifecycle: trigger AAP provision jobs when templateID is set,
  poll for completion, and set ProvisionTemplateComplete condition
- Add deprovisioning lifecycle: trigger AAP deprovision jobs on deletion before
  finalizer removal, with status flush to prevent duplicate jobs on crash
- Refactor Reconcile into handleUpdate/reconcileDelete with centralized status updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@DanNiESh
DanNiESh force-pushed the provision-update branch from 6c65743 to 2af5276 Compare May 19, 2026 19:46

@tzumainn tzumainn 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.

Looks good - thanks!

@tzumainn
tzumainn merged commit d7283da into osac-project:main May 19, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants