Skip to content

Conversation

asmit27rai
Copy link
Contributor

@asmit27rai asmit27rai commented May 20, 2025

Summary

In api/v1alpha1/controlplane_types.go

  • Add PostCreateHooks field for multiple hooks
  • Deprecate legacy single hook fields
  • Add PostCreateHookUse struct

In pkg/reconcilers/shared/postcreate_hook.go

  • Process both legacy and new hooks
  • Handle multiple hooks in sequence
  • Update status per hook

Backward Compatibility Strategy

Old Fields: Continue to work as before but are marked deprecated.

New Fields: Allow multiple hooks via PostCreateHooks.

Reconciler: Processes both old and new fields, merging them into a single list.

This ensures existing users using postCreateHook and postCreateHookVars are unaffected, while new users can adopt postCreateHooks. The CLI can be updated separately to support multiple hooks via new flags while maintaining backward compatibility with the old ones.

Fixes #225

Summary by CodeRabbit

  • New Features
    • Added support for multiple post-creation hooks with individualized variables and shared global variables for control planes.
  • Bug Fixes
    • Improved error handling and status updates when applying post-creation hooks.
  • Documentation
    • Updated CRD documentation to reflect new fields and deprecate old ones.
  • Chores
    • Enhanced test cleanup scripts for better error resilience and cleaner output.

Signed-off-by: asmit27rai <raiasmit10@gmail.com>
@asmit27rai
Copy link
Contributor Author

@pdettori I closed #371 due to many unnecessary commits..
This is revised form of it...

Signed-off-by: asmit27rai <raiasmit10@gmail.com>
@pdettori
Copy link
Contributor

@asmit27rai - thanks, I will review - in the meantime, could you pls. set the correct PR type? The CI check is failing. It should be ✨ feature:

@asmit27rai asmit27rai changed the title feature: implemented multiple PostCreateHooks with backward compatibility ✨ feature: implemented multiple PostCreateHooks with backward compatibility May 21, 2025
hooks = append(hooks, v1alpha1.PostCreateHookUse{
HookName: hcp.Spec.PostCreateHook,
Vars: hcp.Spec.PostCreateHookVars,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

If the same hook is specified both as the legacy field and in the new array, it may be applied twice unless handled elsewhere. Add a check to avoid duplicate execution of hooks (e.g., by hook name).


// Get hook definition
pch := &v1alpha1.PostCreateHook{}
if err := r.Client.Get(ctx, client.ObjectKey{Name: hookName}, pch); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

On transient errors, the function immediately returns, preventing other hooks from being processed. Would be better accumulating errors and reporting them collectively, or ensure that a failed hook (transient or not) doesn’t block other hooks from being attempted

--create-namespace \
--version 4.10.0 \
--set controller.service.type=NodePort \
--set controller.hostPort.enabled=true
Copy link
Contributor

Choose a reason for hiding this comment

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

the script installs ingress-nginx twice: once via Helm and once via kubectl apply. This may lead to race conditions or resource conflicts. Use only one method to install ingress-nginx.

@rxinui rxinui added the priority/high #e11d21 label Jun 12, 2025
@francostellari
Copy link
Contributor

Is there a documentation update that goes along with the Go code change?
I would like to understand how to use the new hook mechanism from KS core-chart.
@Rupam-It can you please monitor this and propose an update of the core-chart when the time comes?

@francostellari
Copy link
Contributor

@asmit27rai when multiple hooks are listed... is there a consistent order by which they are executed? perhaps the order by which they are listed?

Signed-off-by: asmit27rai <raiasmit10@gmail.com>
@asmit27rai
Copy link
Contributor Author

@asmit27rai when multiple hooks are listed... is there a consistent order by which they are executed? perhaps the order by which they are listed?

Yes, hooks are executed in the exact order they are listed:

  • The legacy hook (if specified) is always executed first

  • New hooks are executed in the order they appear in the PostCreateHooks array

  • Duplicates are skipped (first occurrence wins)

Signed-off-by: asmit27rai <raiasmit10@gmail.com>
Signed-off-by: asmit27rai <raiasmit10@gmail.com>
@francostellari
Copy link
Contributor

To be fully clear... this older YAML:

  postCreateHook: wds
  postCreateHookVars:
    ITSName: "{{ $cp.ITSName | default "" }}"
    APIGroups: "{{ $cp.APIGroups | default "" }}"

could be changed to this YAML after this PR merges:

  postCreateHooks:
  -  hookName: wds
     vars:
       ITSName: "{{ $cp.ITSName | default "" }}"
      APIGroups: "{{ $cp.APIGroups | default "" }}"

vars is a dictionary like before or does it need to be a list?

@francostellari
Copy link
Contributor

Since the new vars seems to be local and limited to the specific hook, can the existing postCreateHookVars be used to set "global" variables for all the hooks?

@asmit27rai
Copy link
Contributor Author

To be fully clear... this older YAML:

  postCreateHook: wds
  postCreateHookVars:
    ITSName: "{{ $cp.ITSName | default "" }}"
    APIGroups: "{{ $cp.APIGroups | default "" }}"

could be changed to this YAML after this PR merges:

  postCreateHooks:
  -  hookName: wds
     vars:
       ITSName: "{{ $cp.ITSName | default "" }}"
      APIGroups: "{{ $cp.APIGroups | default "" }}"

vars is a dictionary like before or does it need to be a list?

The new vars field is still a dictionary (map), not a list.
You can confirm it here.

type Var struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

@asmit27rai
Copy link
Contributor Author

Since the new vars seems to be local and limited to the specific hook, can the existing postCreateHookVars be used to set "global" variables for all the hooks?

  • The legacy postCreateHookVars is only applied to the legacy single hook (when using postCreateHook)

  • It is not shared with hooks defined in postCreateHooks

  • Each hook in postCreateHooks has its own isolated vars dictionary

@francostellari
Copy link
Contributor

Since the new vars seems to be local and limited to the specific hook, can the existing postCreateHookVars be used to set "global" variables for all the hooks?

  • The legacy postCreateHookVars is only applied to the legacy single hook (when using postCreateHook)
  • It is not shared with hooks defined in postCreateHooks
  • Each hook in postCreateHooks has its own isolated vars dictionary

@asmit27rai thank you for the clarification.
I'm suggesting that, for KS application, it would be useful to have a global/shared variable without the need of redefining identical variables for all PCHs.
I'm wondering if it would still be possible to enable such a feature without a major redesign,

@Rupam-It
Copy link
Contributor

Rupam-It commented Jun 13, 2025

Also one think is that!
I'm studying about pchs can be used as a shared among helm release so that we can decouple the pch and kubeflex-operator command form helm release!

For context: kubestellar/kubestellar#2935
I'm not sure about the whole PR. Asking just for conformation!
Else to overcome helm ownership model we have to chane in values.yml file in KS core chart

Signed-off-by: asmit27rai <raiasmit10@gmail.com>
@asmit27rai
Copy link
Contributor Author

Since the new vars seems to be local and limited to the specific hook, can the existing postCreateHookVars be used to set "global" variables for all the hooks?

  • The legacy postCreateHookVars is only applied to the legacy single hook (when using postCreateHook)
  • It is not shared with hooks defined in postCreateHooks
  • Each hook in postCreateHooks has its own isolated vars dictionary

@asmit27rai thank you for the clarification. I'm suggesting that, for KS application, it would be useful to have a global/shared variable without the need of redefining identical variables for all PCHs. I'm wondering if it would still be possible to enable such a feature without a major redesign,

@francostellari I added global/shared variable.
Looking for your response

@pdettori
Copy link
Contributor

/lgtm
/approve

Copy link
Contributor

LGTM label has been added.

Git tree hash: 8b8a5e7a8c802a67e798da68eb7dc7014f20b91e

Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: pdettori

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

The pull request process is described here

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

Copy link

coderabbitai bot commented Jun 18, 2025

Walkthrough

The changes introduce support for multiple post-creation hooks in the control plane specification and its CRD schema. The legacy single-hook fields are deprecated, and a new structure allows specifying multiple hooks with individualized variables and optional global variables. The post-create hook reconciliation logic is refactored to process multiple hooks in order.

Changes

File(s) Change Summary
api/v1alpha1/controlplane_types.go Deprecated legacy post-create hook fields; added PostCreateHooks slice and GlobalVars map to ControlPlaneSpec; introduced PostCreateHookUse struct.
config/crd/bases/tenancy.kflex.kubestellar.org_controlplanes.yaml Updated CRD: deprecated legacy fields, added globalVars, and defined postCreateHooks array schema.
config/crd/bases/tenancy.kflex.kubestellar.org_postcreatehooks.yaml Clarified and reformatted description and schema for defaultVars in the PostCreateHook CRD.
pkg/reconcilers/shared/postcreate_hook.go Refactored reconciliation logic to support multiple post-create hooks, variable precedence, and error aggregation.
test/e2e/cleanup.sh Made cleanup commands more robust to errors and suppressed their output.
test/e2e/setup-kubeflex.sh Removed a trailing blank line after a wait command; no functional changes.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Controller
    participant API Server
    participant HookTemplate

    User->>API Server: Create/Update ControlPlane (with multiple PostCreateHooks)
    API Server->>Controller: Notify of ControlPlane change
    loop For each PostCreateHook in order
        Controller->>API Server: Fetch PostCreateHook definition
        Controller->>HookTemplate: Apply hook (merge default, global, user, system vars)
        HookTemplate-->>Controller: Result (success/failure)
        Controller->>API Server: Update ControlPlane status for applied hook
    end
Loading

Assessment against linked issues

Objective Addressed Explanation
Support multiple PostCreateHooks in ControlPlaneSpec (#225)
Deprecate legacy single-hook fields and introduce PostCreateHookUse struct (#225)
Update CRD to reflect new fields and deprecations for post-create hooks (#225)
Refactor reconciliation logic to process multiple hooks in order (#225)

Poem

In the warren of code, we hop with delight,
Now many hooks run, not just one in the night!
With variables shared, and order preserved,
Our control planes flourish—just as they deserved.
🐇✨ Multiple hooks, one hoppy new flight!

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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: 3

🧹 Nitpick comments (5)
test/e2e/cleanup.sh (1)

22-26: Redirection sequence is redundant & shell options relaxed.

&> /dev/null already redirects both stdout and stderr; chaining another 2>&1 afterwards is superfluous:

-kubectl config delete-context cp1 &> /dev/null 2>&1 || true
+kubectl config delete-context cp1 &> /dev/null || true

Same for the other lines.

Side note: this script removed set -e, so silent failures earlier in the file now go unnoticed. Keeping set -e and selectively using || true (as you already do) provides clearer intent.

api/v1alpha1/controlplane_types.go (2)

35-39: Mark deprecated fields with // +optional and comment tags.

PostCreateHook and PostCreateHookVars are flagged as deprecated but not explicitly marked optional via kubebuilder comments. To avoid validation noise in generated CRDs, append // +optional:

-// Deprecated: Use PostCreateHooks instead
-PostCreateHook         *string           `json:"postCreateHook,omitempty"`
+// Deprecated: Use PostCreateHooks instead
+// +optional
+PostCreateHook         *string           `json:"postCreateHook,omitempty"`

Same for PostCreateHookVars.


40-45: Add // +optional to new slice fields for consistency.

PostCreateHooks is omitempty-tagged but misses the kubebuilder +optional marker, whereas GlobalVars has it. This keeps CRD generation symmetric and avoids unexpected “required” behaviour from some tooling.

-// PostCreateHooks specifies multiple post-creation hooks to execute
-PostCreateHooks        []PostCreateHookUse `json:"postCreateHooks,omitempty"`
+// PostCreateHooks specifies multiple post-creation hooks to execute
+// +optional
+PostCreateHooks        []PostCreateHookUse `json:"postCreateHooks,omitempty"`
config/crd/bases/tenancy.kflex.kubestellar.org_controlplanes.yaml (1)

86-115: Schema tightening for globalVars / postCreateHooks.

  1. globalVars allows arbitrary key/value pairs – good – but consider adding
    x-kubernetes-preserve-unknown-fields: false to prevent nested objects (JSON schema default is permissive).

  2. For postCreateHooks items, set x-kubernetes-list-type: map and x-kubernetes-list-map-keys: ["hookName"] to enforce uniqueness on the API side, mirroring your “skip duplicates” controller logic.

These minor schema hints move validation closer to runtime expectations.

pkg/reconcilers/shared/postcreate_hook.go (1)

48-59: Address static-analysis warnings for deprecated fields

The legacy fields are intentionally referenced for backward-compatibility, but staticcheck (SA1019) will keep flagging these lines.
Add a scoped nolint comment to silence the warning and avoid cluttering CI noise:

//nolint:staticcheck // needed for backward-compat until <remove-date>
if hcp.Spec.PostCreateHook != nil {
    ...
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da3855d and a8df3a1.

📒 Files selected for processing (6)
  • api/v1alpha1/controlplane_types.go (1 hunks)
  • config/crd/bases/tenancy.kflex.kubestellar.org_controlplanes.yaml (1 hunks)
  • config/crd/bases/tenancy.kflex.kubestellar.org_postcreatehooks.yaml (1 hunks)
  • pkg/reconcilers/shared/postcreate_hook.go (1 hunks)
  • test/e2e/cleanup.sh (1 hunks)
  • test/e2e/setup-kubeflex.sh (1 hunks)
🧰 Additional context used
🪛 golangci-lint (1.64.8)
pkg/reconcilers/shared/postcreate_hook.go

53-53: SA1019: hcp.Spec.PostCreateHook is deprecated: Use PostCreateHooks instead

(staticcheck)


54-54: SA1019: hcp.Spec.PostCreateHook is deprecated: Use PostCreateHooks instead

(staticcheck)


56-56: SA1019: hcp.Spec.PostCreateHook is deprecated: Use PostCreateHooks instead

(staticcheck)


57-57: SA1019: hcp.Spec.PostCreateHookVars is deprecated: Use PostCreateHooks instead

(staticcheck)

🔇 Additional comments (2)
test/e2e/setup-kubeflex.sh (1)

26-29: ```shell
#!/usr/bin/env bash

Locate Helm chart definitions

echo "Chart.yaml files found:"
fd -t f Chart.yaml || true

Inspect Chart.yaml contents for dependencies

for chart in $(fd -t f Chart.yaml); do
echo -e "\n=== $chart ==="
sed -n '1,200p' "$chart" || true
done

Search for any ingress-nginx mentions in chart templates or values

echo -e "\nSearching for ingress-nginx references in the repo:"
grep -R "ingress-nginx" -n . || true


</details>
<details>
<summary>config/crd/bases/tenancy.kflex.kubestellar.org_postcreatehooks.yaml (1)</summary>

`60-70`: **👍  Description relocation improves readability.**

Moving the `Var` description inside the item block matches common CRD style and helps doc generators. No further issues spotted.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines +47 to 52
type PostCreateHookUse struct {
// Name of the PostCreateHook resource to execute
HookName *string `json:"hookName"`
// Variables to pass to the hook template
Vars map[string]string `json:"vars,omitempty"`
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid pointer for required hookName.

hookName is declared required in the CRD, yet the Go struct uses *string.
A plain string is simpler – no nil checks, no chance of zero-value being marshalled incorrectly:

-// Name of the PostCreateHook resource to execute
-HookName *string           `json:"hookName"`
+// Name of the PostCreateHook resource to execute
+HookName string            `json:"hookName"`

Unless you have a compelling reason to distinguish “unset” vs “set to empty string”, prefer the value type.

📝 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
type PostCreateHookUse struct {
// Name of the PostCreateHook resource to execute
HookName *string `json:"hookName"`
// Variables to pass to the hook template
Vars map[string]string `json:"vars,omitempty"`
}
type PostCreateHookUse struct {
// Name of the PostCreateHook resource to execute
HookName string `json:"hookName"`
// Variables to pass to the hook template
Vars map[string]string `json:"vars,omitempty"`
}
🤖 Prompt for AI Agents
In api/v1alpha1/controlplane_types.go between lines 47 and 52, change the
HookName field from a pointer to a plain string type because it is required in
the CRD. This avoids unnecessary nil checks and ensures the field is always set,
simplifying marshaling and usage. Replace *string with string for HookName and
update any related code accordingly.

Comment on lines +128 to +137
// Update status
if hcp.Status.PostCreateHooks == nil {
hcp.Status.PostCreateHooks = make(map[string]bool)
}
hcp.Status.PostCreateHooks[hookName] = true
if err := r.Client.Status().Update(ctx, hcp); err != nil {
errs = append(errs, fmt.Errorf("failed to update status for hook %s: %w", hookName, err))
// Break on status update error to prevent inconsistent state
break
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Retry status update on conflicts

Status().Update can fail with Conflict when another controller touches the object.
Instead of aborting the entire reconciliation, wrap the update in retry.RetryOnConflict; only give up on non-conflict errors:

err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
    return r.Client.Status().Update(ctx, hcp)
})
if err != nil {
    errs = append(errs, fmt.Errorf("failed to update status for hook %s: %w", hookName, err))
    break
}

This prevents unnecessary requeues caused by transient resource-version races.

🤖 Prompt for AI Agents
In pkg/reconcilers/shared/postcreate_hook.go around lines 128 to 137, the status
update using r.Client.Status().Update can fail with a Conflict error if another
controller modifies the object concurrently. To handle this, wrap the update
call inside retry.RetryOnConflict with retry.DefaultBackoff to retry on conflict
errors. Only append the error and break the loop if the error is not a conflict
after retries. Replace the direct update call with this retry logic to avoid
unnecessary reconciliation aborts due to transient conflicts.

Comment on lines +75 to 147
var errs []error

for _, hook := range hooks {
hookName := *hook.HookName

// Skip already applied hooks
if hcp.Status.PostCreateHooks != nil && hcp.Status.PostCreateHooks[hookName] {
continue
}

logger.Info("Processing post-create hook", "hook", hookName)

// Get hook definition
pch := &v1alpha1.PostCreateHook{}
if err := r.Client.Get(ctx, client.ObjectKey{Name: hookName}, pch); err != nil {
errs = append(errs, fmt.Errorf("failed to get PostCreateHook %s: %w", hookName, err))
continue
}

// Build variables with precedence: defaults -> global -> user vars -> system
vars := make(map[string]interface{})

// 1. Default vars from hook spec
for _, dv := range pch.Spec.DefaultVars {
vars[dv.Name] = dv.Value
}

// 2. Global variables from control plane
for key, val := range hcp.Spec.GlobalVars {
vars[key] = val
}

// 3. User-provided vars from hook use
for key, val := range hook.Vars {
vars[key] = val
}

// 4. System variables
vars["Namespace"] = namespace
vars["ControlPlaneName"] = hcp.Name
vars["HookName"] = hookName

// Apply hook templates
if err := applyPostCreateHook(ctx, r.ClientSet, r.DynamicClient, pch, vars, hcp); err != nil {
if util.IsTransientError(err) {
errs = append(errs, fmt.Errorf("transient error applying hook %s: %w", hookName, err))
} else {
logger.Error(err, "Permanent error applying post-create hook", "hook", hookName)
errs = append(errs, fmt.Errorf("permanent error applying hook %s: %w", hookName, err))
}
continue
}

// Update status
if hcp.Status.PostCreateHooks == nil {
hcp.Status.PostCreateHooks = make(map[string]bool)
}
hcp.Status.PostCreateHooks[hookName] = true
if err := r.Client.Status().Update(ctx, hcp); err != nil {
errs = append(errs, fmt.Errorf("failed to update status for hook %s: %w", hookName, err))
// Break on status update error to prevent inconsistent state
break
}

// Propagate labels
if err := propagateLabels(pch, hcp, r.Client); err != nil {
logger.Error(err, "Failed to propagate labels from hook", "hook", hookName)
}
logger.Error(err, "Failed to apply post-create hook", "hook", *hcp.Spec.PostCreateHook)
}

// Update status if hook was successfully applied
if hcp.Status.PostCreateHooks == nil {
hcp.Status.PostCreateHooks = make(map[string]bool)
}
hcp.Status.PostCreateHooks[*hcp.Spec.PostCreateHook] = true

if err := r.Client.Status().Update(context.TODO(), hcp, &client.SubResourceUpdateOptions{}); err != nil {
return fmt.Errorf("failed to update control plane status: %w", err)
if len(errs) > 0 {
return fmt.Errorf("errors processing hooks: %v", errs)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use utilerrors.NewAggregate for cleaner multi-error handling

Collecting errors into a []error slice and then wrapping it with fmt.Errorf("%v", errs) collapses context and produces unreadable output.
K8s provides an aggregator tailor-made for this:

-import "fmt"
+import (
+   "fmt"
+   utilerrors "k8s.io/apimachinery/pkg/util/errors"
+)

 ...
-var errs []error
+var errs []error
 ...
-if len(errs) > 0 {
-    return fmt.Errorf("errors processing hooks: %v", errs)
+if len(errs) > 0 {
+    return utilerrors.NewAggregate(errs)
 }

This keeps each underlying error intact and plays nicely with callers that want to inspect individual causes.

📝 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
var errs []error
for _, hook := range hooks {
hookName := *hook.HookName
// Skip already applied hooks
if hcp.Status.PostCreateHooks != nil && hcp.Status.PostCreateHooks[hookName] {
continue
}
logger.Info("Processing post-create hook", "hook", hookName)
// Get hook definition
pch := &v1alpha1.PostCreateHook{}
if err := r.Client.Get(ctx, client.ObjectKey{Name: hookName}, pch); err != nil {
errs = append(errs, fmt.Errorf("failed to get PostCreateHook %s: %w", hookName, err))
continue
}
// Build variables with precedence: defaults -> global -> user vars -> system
vars := make(map[string]interface{})
// 1. Default vars from hook spec
for _, dv := range pch.Spec.DefaultVars {
vars[dv.Name] = dv.Value
}
// 2. Global variables from control plane
for key, val := range hcp.Spec.GlobalVars {
vars[key] = val
}
// 3. User-provided vars from hook use
for key, val := range hook.Vars {
vars[key] = val
}
// 4. System variables
vars["Namespace"] = namespace
vars["ControlPlaneName"] = hcp.Name
vars["HookName"] = hookName
// Apply hook templates
if err := applyPostCreateHook(ctx, r.ClientSet, r.DynamicClient, pch, vars, hcp); err != nil {
if util.IsTransientError(err) {
errs = append(errs, fmt.Errorf("transient error applying hook %s: %w", hookName, err))
} else {
logger.Error(err, "Permanent error applying post-create hook", "hook", hookName)
errs = append(errs, fmt.Errorf("permanent error applying hook %s: %w", hookName, err))
}
continue
}
// Update status
if hcp.Status.PostCreateHooks == nil {
hcp.Status.PostCreateHooks = make(map[string]bool)
}
hcp.Status.PostCreateHooks[hookName] = true
if err := r.Client.Status().Update(ctx, hcp); err != nil {
errs = append(errs, fmt.Errorf("failed to update status for hook %s: %w", hookName, err))
// Break on status update error to prevent inconsistent state
break
}
// Propagate labels
if err := propagateLabels(pch, hcp, r.Client); err != nil {
logger.Error(err, "Failed to propagate labels from hook", "hook", hookName)
}
logger.Error(err, "Failed to apply post-create hook", "hook", *hcp.Spec.PostCreateHook)
}
// Update status if hook was successfully applied
if hcp.Status.PostCreateHooks == nil {
hcp.Status.PostCreateHooks = make(map[string]bool)
}
hcp.Status.PostCreateHooks[*hcp.Spec.PostCreateHook] = true
if err := r.Client.Status().Update(context.TODO(), hcp, &client.SubResourceUpdateOptions{}); err != nil {
return fmt.Errorf("failed to update control plane status: %w", err)
if len(errs) > 0 {
return fmt.Errorf("errors processing hooks: %v", errs)
}
// At the top of pkg/reconcilers/shared/postcreate_hook.go
-import "fmt"
+import (
+ "fmt"
+ utilerrors "k8s.io/apimachinery/pkg/util/errors"
+)
...
// Error accumulator
var errs []error
for _, hook := range hooks {
// (all existing per-hook logic unchanged)
}
- if len(errs) > 0 {
- return fmt.Errorf("errors processing hooks: %v", errs)
- }
+ if len(errs) > 0 {
+ return utilerrors.NewAggregate(errs)
+ }
🤖 Prompt for AI Agents
In pkg/reconcilers/shared/postcreate_hook.go around lines 75 to 147, replace the
current error aggregation approach that collects errors in a []error slice and
returns them wrapped with fmt.Errorf. Instead, use utilerrors.NewAggregate to
aggregate the errors. This change preserves each individual error's context and
improves readability and error inspection. Import the utilerrors package if not
already imported, and at the end of the hook processing loop, return
utilerrors.NewAggregate(errs) when errs is not empty.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

feature: Allow more than one PostCreateHook when creating a control plane
5 participants