Skip to content

HYPERFLEET-864 - feat: support Go template lists and conditionals in manifest resources#96

Open
xueli181114 wants to merge 2 commits intoopenshift-hyperfleet:mainfrom
xueli181114:HYPERFLEET-864
Open

HYPERFLEET-864 - feat: support Go template lists and conditionals in manifest resources#96
xueli181114 wants to merge 2 commits intoopenshift-hyperfleet:mainfrom
xueli181114:HYPERFLEET-864

Conversation

@xueli181114
Copy link
Copy Markdown
Contributor

@xueli181114 xueli181114 commented Apr 2, 2026

Summary

  • Manifest ref: files are now loaded as raw strings instead of being parsed as YAML, allowing structural Go templates ({{ if }}, {{ range }}, {{ else }}) to be used for conditional properties and list generation
  • Inline manifests support the same templates via YAML multiline string syntax (manifest: |)
  • Templates are rendered at execution time (when params are available), then the rendered output is parsed as YAML
  • K8s manifest validation is skipped for string manifests since validation cannot occur before template rendering

Changes

  • loader.go: New loadRawFile() function; loadTaskConfigFileReferences stores manifest refs as raw strings
  • resource_executor.go: New renderStringManifest() method handles string manifests (render template → parse YAML → marshal JSON)
  • validator.go: Skip K8s manifest validation for string manifests (deferred to execution time)

Test plan

  • Existing unit tests updated and passing
  • New tests for structural Go templates ({{ if }}, {{ range }}, {{ if/else }})
  • New test for customer's subnet list use case
  • New tests for ref file and inline string manifest loading
  • CI pipeline passes

Relates to: HYPERFLEET-864

Summary by CodeRabbit

  • New Features

    • Referenced or inlined manifests are now stored as raw template strings and rendered at runtime; GVK info can be extracted from template-containing manifests to enable execution without prior YAML parsing.
    • Build script now detects container platform for image builds.
  • Bug Fixes

    • Validation defers schema checks for template-based manifests to avoid false errors.
  • Tests

    • Broader test coverage for templated manifests, GVK resolution, rendering edge cases, and end-to-end flows.
  • Chores

    • Integration image base updated for CI.

@openshift-ci openshift-ci bot requested review from aredenba-rh and rafabene April 2, 2026 03:20
@openshift-ci
Copy link
Copy Markdown

openshift-ci bot commented Apr 2, 2026

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign mischulee for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found 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

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Referenced manifests (manifest.ref and manifestWork.ref) are now read as raw file strings via a new secure loadRawFile helper and stored in Resource.Manifest without YAML parsing, preserving Go-template syntax. Validator logic now runs template-variable checks for string manifests and skips Kubernetes structural/semantic validation for them; map-based manifests retain prior validations. At execution, string manifests are rendered with the Go-template engine, the rendered YAML is parsed into maps, and those maps are marshaled to JSON bytes for application. BuildRef/post.payloads remain YAML-parsed.

Sequence Diagram

sequenceDiagram
    participant ConfigLoader as Config Loader
    participant FS as File System
    participant Validator as Validator
    participant Executor as Executor
    participant TemplateEngine as Template Engine
    participant YAML as YAML Parser

    ConfigLoader->>FS: safe-join path, os.ReadFile (loadRawFile)
    FS-->>ConfigLoader: raw file content (string with templates)
    ConfigLoader->>ConfigLoader: set Resource.Manifest = raw string

    Validator->>ConfigLoader: inspect Resource.Manifest
    alt Manifest is string
        Validator->>Validator: validateTemplateString (skip K8s structural checks)
    else Manifest is map
        Validator->>Validator: validateTemplateMap + K8s structure/semantic checks
    end

    Executor->>ConfigLoader: get Resource.Manifest
    alt Manifest is string
        Executor->>TemplateEngine: render template -> rendered YAML string
        TemplateEngine-->>Executor: rendered YAML
        Executor->>YAML: parse rendered YAML -> map[string]interface{}
        YAML-->>Executor: parsed map
        Executor->>Executor: json.Marshal(parsed map) -> bytes
    else Manifest is map
        Executor->>Executor: render templates recursively in map (manifest.RenderManifestData)
        Executor->>Executor: json.Marshal(map) -> bytes
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main feature addition: support for Go template lists and conditionals in manifest resources, which is the core objective of this changeset.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/configloader/validator.go`:
- Around line 562-567: The current check skips all validation for string
manifests (resource.Manifest as string), which bypasses template-variable
validation; update the branch so that before continuing you run
validateTemplateVariables(...) on the raw string manifest (using the same
context/params used for map manifests), surface/return any validation errors
from validateTemplateVariables, but still skip K8s shape/schema validation for
strings; reference resource.Manifest and validateTemplateVariables() when making
the change and preserve the existing control-flow that continues after template
validation.

In `@internal/executor/resource_executor.go`:
- Around line 225-227: String-backed manifests are rendered in
renderStringManifest but discoverResource still calls resolveGVK(resource) which
returns empty GVK for non-map manifests; change the flow so renderStringManifest
(or the rendering step) computes the actual GVK from the rendered object (call
it renderedGVK) and pass that renderedGVK into discoverResource instead of
letting discoverResource recompute from resource.Manifest; update
discoverResource's signature/usage to accept renderedGVK and use it in place of
resolveGVK(resource) (also apply the same adjustment for the other occurrence
around lines 258-283) so discovery uses the real GVK derived from the rendered
object.
🪄 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: Pro

Run ID: 83424ad6-6c90-4d54-8041-c21c4c368c81

📥 Commits

Reviewing files that changed from the base of the PR and between c35357b and 493595a.

📒 Files selected for processing (5)
  • internal/configloader/loader.go
  • internal/configloader/loader_test.go
  • internal/configloader/validator.go
  • internal/executor/resource_executor.go
  • internal/executor/resource_executor_test.go

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/configloader/validator_test.go`:
- Around line 374-386: The test currently assumes any string Resource.Manifest
skips K8s validation; either update the validator to only bypass validation when
the manifest contains template delimiters (e.g., detect "{{" / "}}" in
Resource.Manifest in newTaskValidator/ValidateStructure) or change the test to
expect a validation error for a non-templated YAML string; specifically, narrow
the bypass logic so Resource.Manifest without template delimiters is
parsed/validated (or explicitly detect templated strings before bypassing) and
then update this test that calls newTaskValidator(cfg) and asserts
ValidateStructure()/ValidateSemantic() accordingly.

In `@internal/executor/resource_executor_test.go`:
- Around line 615-628: The test currently accepts an empty manifest producing
JSON "null", but renderToBytes should fail fast for empty string manifests;
update the implementation of re.renderToBytes (the renderer function used in the
test) to detect resource.Manifest == "" (or YAML that parses to a nil/empty
document) and return a clear render/parse error instead of returning "null",
then update the test expectation to require an error (use
require.Error/require.Contains on the returned error) rather than asserting
"null". Ensure the check is applied early in re.renderToBytes so callers like
apply/discovery never receive a null manifest.
🪄 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: Pro

Run ID: a1f623e9-2179-4f48-8dce-d79e60950487

📥 Commits

Reviewing files that changed from the base of the PR and between 493595a and 75e8fbd.

📒 Files selected for processing (3)
  • internal/configloader/loader_test.go
  • internal/configloader/validator_test.go
  • internal/executor/resource_executor_test.go

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
internal/executor/resource_executor.go (2)

263-280: ⚠️ Potential issue | 🟠 Major

Reject manifests that render to an empty document.

The guard at Line 263 only catches an empty source string. A template like {{ if .enabled }}...{{ end }} with enabled=false renders to empty output, yaml.Unmarshal leaves manifestData nil, and json.Marshal(nil) produces "null" again.

Suggested fix
 	rendered, err := utils.RenderTemplate(manifestStr, params)
 	if err != nil {
 		return nil, fmt.Errorf("failed to render manifest template: %w", err)
 	}
+	if strings.TrimSpace(rendered) == "" {
+		return nil, fmt.Errorf("empty manifest: template rendered to an empty document")
+	}
 
 	// Parse the rendered string as YAML
 	var manifestData map[string]interface{}
 	if unmarshalErr := yaml.Unmarshal([]byte(rendered), &manifestData); unmarshalErr != nil {
 		return nil, fmt.Errorf("failed to parse rendered manifest as YAML: %w", unmarshalErr)
 	}
+	if len(manifestData) == 0 {
+		return nil, fmt.Errorf("empty manifest: rendered YAML did not contain an object")
+	}
 
 	// Marshal to JSON bytes
 	data, err := json.Marshal(manifestData)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/executor/resource_executor.go` around lines 263 - 280, The rendered
template output (from utils.RenderTemplate called on manifestStr) can be empty
even when the source manifestStr is non-empty, which leads to yaml.Unmarshal
leaving manifestData nil and json.Marshal producing "null"; update the code
after rendering to check that the rendered string is not empty or only
whitespace (e.g., trim and error if empty) and also verify manifestData is
non-nil after yaml.Unmarshal (return an error like "rendered manifest is empty"
if manifestData == nil) before calling json.Marshal; modify the logic around
rendered, manifestData, and json.Marshal to return a clear error when the
rendered document is empty.

82-95: ⚠️ Potential issue | 🟠 Major

Use the rendered object's GVK for discovery.

discoverResource() still resolves GVK from resource.Manifest, so apiVersion/kind templates in either map or string manifests can apply successfully and then fail the post-apply lookup. Lines 89-95 already decode renderedBytes into obj, so pass that GVK forward instead of rescanning the source manifest.

Suggested fix
 func (re *ResourceExecutor) executeResource(
 	ctx context.Context,
 	resource configloader.Resource,
 	execCtx *ExecutionContext,
 ) (ResourceResult, error) {
@@
 	var obj unstructured.Unstructured
+	var renderedGVK schema.GroupVersionKind
 	if unmarshalErr := json.Unmarshal(renderedBytes, &obj.Object); unmarshalErr == nil {
 		result.Kind = obj.GetKind()
 		result.Namespace = obj.GetNamespace()
 		result.ResourceName = obj.GetName()
+		if gv, gvErr := schema.ParseGroupVersion(obj.GetAPIVersion()); gvErr == nil && obj.GetKind() != "" {
+			renderedGVK = gv.WithKind(obj.GetKind())
+		}
 	}
@@
-		discovered, discoverErr := re.discoverResource(ctx, resource, execCtx, transportTarget)
+		discovered, discoverErr := re.discoverResource(ctx, resource, execCtx, transportTarget, renderedGVK)
 func (re *ResourceExecutor) discoverResource(
 	ctx context.Context,
 	resource configloader.Resource,
 	execCtx *ExecutionContext,
 	transportTarget transportclient.TransportContext,
+	renderedGVK schema.GroupVersionKind,
 ) (*unstructured.Unstructured, error) {
@@
-		gvk := re.resolveGVK(resource)
+		gvk := renderedGVK
+		if gvk.Empty() {
+			gvk = re.resolveGVK(resource)
+		}
+		if gvk.Empty() {
+			return nil, fmt.Errorf("failed to resolve GVK for resource %s", resource.Name)
+		}
 
 		return re.client.GetResource(ctx, gvk, namespace, name, transportTarget)
 	}
@@
-		gvk := re.resolveGVK(resource)
+		gvk := renderedGVK
+		if gvk.Empty() {
+			gvk = re.resolveGVK(resource)
+		}
+		if gvk.Empty() {
+			return nil, fmt.Errorf("failed to resolve GVK for resource %s", resource.Name)
+		}
 
 		list, err := re.client.DiscoverResources(ctx, gvk, discoveryConfig, transportTarget)

Also applies to: 141-143

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/executor/resource_executor.go` around lines 82 - 95, The discovery
step currently calls discoverResource() using the original resource.Manifest
which can mismatch after templating; use the decoded rendered object GVK
instead: after json.Unmarshal(renderedBytes, &obj.Object) (the block that sets
result.Kind/Namespace/ResourceName) pass obj.GetAPIVersion()/obj.GetKind() (or
the unstructured.Unstructured obj itself) into discoverResource()/the discovery
call so discovery uses the rendered manifest's GroupVersionKind; repeat the same
change for the other occurrence where discoverResource is invoked later (the
block around the second render/lookup).
🧹 Nitpick comments (2)
internal/configloader/loader_test.go (2)

1332-1339: Use the nested maestro manifestWork.ref shape here.

TestTransportConfigYAMLParsing models maestro manifests under transport.maestro.manifestWork, but this loader test still feeds the file through resources[].manifest.ref. That means these new raw-string assertions would stay green even if the actual maestro ref-loading path regressed. Please switch this fixture or add a companion case with transport.maestro.manifestWork.ref.

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 the current code and only fix it if needed.

In `@internal/configloader/loader_test.go` around lines 1332 - 1339, The test is
asserting raw-string loading via resources[].Manifest but the real maestro path
is transport.maestro.manifestWork.ref; update the fixture used by
TestTransportConfigYAMLParsing (or add a companion test case) so it places the
manifest under transport.maestro.manifestWork.ref instead of
resources[].manifest.ref, then adjust the assertions to load that nested shape
(or keep the same raw-string assertions but verify they come from
transport.maestro.manifestWork.ref) so regressions in the maestro ref-loading
path are caught.

1460-1464: Cover the default validator path for string manifests.

Every new happy-path string-manifest test calls WithSkipSemanticValidation(), so none of them exercises the new string bypass in internal/configloader/validator.go. Keeping at least one LoadConfig case on the default path—TestLoadConfigManifestRefPlainYAML is the easiest candidate—would prevent regressions from being masked by the test setup.

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

Also applies to: 1507-1511, 1625-1629

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/configloader/loader_test.go` around lines 1460 - 1464, Test suite
bypasses the new string-manifest validation because most happy-path tests call
WithSkipSemanticValidation(); add at least one LoadConfig invocation that does
NOT pass WithSkipSemanticValidation() (e.g., modify
TestLoadConfigManifestRefPlainYAML or add a new case) so the default validation
path in internal/configloader/validator.go for "string" manifests is exercised;
keep the callsite to LoadConfig and existing helper options
(WithAdapterConfigPath, WithTaskConfigPath) but remove
WithSkipSemanticValidation() for that case to ensure the string bypass logic is
covered and prevent regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/build-integration-image.sh`:
- Around line 37-40: The current logic adds both HTTP_PROXY and HTTPS_PROXY
build-args together which can pass empty values and override inherited proxy
settings; update the BUILD_ARGS handling in scripts/build-integration-image.sh
so you only append --build-arg "HTTP_PROXY=..." when PROXY_HTTP is non-empty and
only append --build-arg "HTTPS_PROXY=..." when PROXY_HTTPS is non-empty (i.e.,
check PROXY_HTTP and PROXY_HTTPS separately before pushing to BUILD_ARGS),
preserving existing behavior of printing "Using proxy configuration" when at
least one proxy is set.

In `@test/Dockerfile.integration`:
- Around line 7-9: The Dockerfile currently leaves the image running as root
after the privileged RUN steps; add a USER 1001 instruction immediately after
the privileged operations (after the RUN dnf install -y tar && dnf clean all
block and before the CMD) to switch back to the non-root runtime user, and
ensure any directories referenced later (e.g., /tmp/envtest created on line 25)
are chowned or made writable for UID 1001 (adjust the RUN that creates those
dirs to set correct ownership/permissions) so the container can run as non-root.

---

Duplicate comments:
In `@internal/executor/resource_executor.go`:
- Around line 263-280: The rendered template output (from utils.RenderTemplate
called on manifestStr) can be empty even when the source manifestStr is
non-empty, which leads to yaml.Unmarshal leaving manifestData nil and
json.Marshal producing "null"; update the code after rendering to check that the
rendered string is not empty or only whitespace (e.g., trim and error if empty)
and also verify manifestData is non-nil after yaml.Unmarshal (return an error
like "rendered manifest is empty" if manifestData == nil) before calling
json.Marshal; modify the logic around rendered, manifestData, and json.Marshal
to return a clear error when the rendered document is empty.
- Around line 82-95: The discovery step currently calls discoverResource() using
the original resource.Manifest which can mismatch after templating; use the
decoded rendered object GVK instead: after json.Unmarshal(renderedBytes,
&obj.Object) (the block that sets result.Kind/Namespace/ResourceName) pass
obj.GetAPIVersion()/obj.GetKind() (or the unstructured.Unstructured obj itself)
into discoverResource()/the discovery call so discovery uses the rendered
manifest's GroupVersionKind; repeat the same change for the other occurrence
where discoverResource is invoked later (the block around the second
render/lookup).

---

Nitpick comments:
In `@internal/configloader/loader_test.go`:
- Around line 1332-1339: The test is asserting raw-string loading via
resources[].Manifest but the real maestro path is
transport.maestro.manifestWork.ref; update the fixture used by
TestTransportConfigYAMLParsing (or add a companion test case) so it places the
manifest under transport.maestro.manifestWork.ref instead of
resources[].manifest.ref, then adjust the assertions to load that nested shape
(or keep the same raw-string assertions but verify they come from
transport.maestro.manifestWork.ref) so regressions in the maestro ref-loading
path are caught.
- Around line 1460-1464: Test suite bypasses the new string-manifest validation
because most happy-path tests call WithSkipSemanticValidation(); add at least
one LoadConfig invocation that does NOT pass WithSkipSemanticValidation() (e.g.,
modify TestLoadConfigManifestRefPlainYAML or add a new case) so the default
validation path in internal/configloader/validator.go for "string" manifests is
exercised; keep the callsite to LoadConfig and existing helper options
(WithAdapterConfigPath, WithTaskConfigPath) but remove
WithSkipSemanticValidation() for that case to ensure the string bypass logic is
covered and prevent regressions.
🪄 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: Pro

Run ID: 42beea71-0b11-42f2-a7de-bc2dfd15862e

📥 Commits

Reviewing files that changed from the base of the PR and between a326fc9 and 568ed42.

📒 Files selected for processing (18)
  • internal/configloader/loader.go
  • internal/configloader/loader_test.go
  • internal/configloader/validator.go
  • internal/configloader/validator_test.go
  • internal/executor/executor_test.go
  • internal/executor/post_action_executor.go
  • internal/executor/resource_executor.go
  • internal/executor/resource_executor_test.go
  • internal/executor/utils.go
  • internal/executor/utils_test.go
  • internal/manifest/gvk.go
  • internal/manifest/gvk_test.go
  • internal/manifest/render_test.go
  • pkg/utils/map_test.go
  • pkg/utils/template_test.go
  • scripts/build-integration-image.sh
  • scripts/container-runtime.sh
  • test/Dockerfile.integration
💤 Files with no reviewable changes (2)
  • internal/executor/executor_test.go
  • internal/executor/utils_test.go
✅ Files skipped from review due to trivial changes (4)
  • scripts/container-runtime.sh
  • internal/executor/post_action_executor.go
  • internal/configloader/validator_test.go
  • internal/configloader/loader.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/configloader/validator.go

…manifest resources

Manifest ref files are now loaded as raw strings instead of being parsed as
YAML, allowing structural Go templates ({{ if }}, {{ range }}, {{ else }}) to
be used for conditional properties and list generation. Inline manifests support
the same templates via YAML multiline string syntax (manifest: |).

Templates are rendered at execution time when params are available, then the
rendered output is parsed as YAML. K8s manifest validation is skipped for string
manifests since validation cannot occur before template rendering. Template
variable validation is still performed at config load time.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

1 participant