Skip to content

feat: Implemented interface for k8s client and maestro client integrtaion in executor#38

Draft
xueli181114 wants to merge 1 commit intoopenshift-hyperfleet:mainfrom
xueli181114:HYPERFLEET-566
Draft

feat: Implemented interface for k8s client and maestro client integrtaion in executor#38
xueli181114 wants to merge 1 commit intoopenshift-hyperfleet:mainfrom
xueli181114:HYPERFLEET-566

Conversation

@xueli181114
Copy link
Contributor

@xueli181114 xueli181114 commented Feb 5, 2026

Summary by CodeRabbit

  • Refactor

    • Restructured executor to use a pipeline-based execution framework with distinct execution phases.
    • Introduced multi-backend resource application abstraction supporting both direct Kubernetes and ManifestWork approaches.
    • Centralized utility functions for configuration, path resolution, type conversion, and template rendering.
    • Reorganized project structure with modular package layout and expanded test coverage.
  • New Features

    • Added resource discovery capabilities using label selectors and namespace-based filtering.
    • Introduced manifest validation and rendering utilities for resource management.

@openshift-ci openshift-ci bot requested review from tirthct and vkareh February 5, 2026 11:55
@openshift-ci
Copy link

openshift-ci bot commented Feb 5, 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 86254860 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

@xueli181114 xueli181114 marked this pull request as draft February 5, 2026 11:55
@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

Walkthrough

This PR refactors the executor from individual phase executors to a unified pipeline-based architecture. It centralizes utility functions into pkg/utils (type conversions, path resolution, template rendering, map operations), migrates generation logic to internal/manifest, introduces a resource_applier abstraction with k8s_client and maestro_client backends, and restructures parameter extraction, preconditions, resources, and post-actions as composable pipeline phases. Old executor files are replaced with phase implementations, k8s_client gains batch apply capabilities, and maestro_client becomes a ResourceApplier implementation. README reflects the modularization with expanded test structure and explicit component boundaries.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Executor Client
    participant Pipeline as Pipeline
    participant ParamPhase as ParamExtractionPhase
    participant PrecondPhase as PreconditionsPhase
    participant ResourcePhase as ResourcesPhase
    participant PostPhase as PostActionsPhase
    participant Applier as ResourceApplier

    Client->>Pipeline: Execute(ctx, execCtx)
    Pipeline->>ParamPhase: Execute(ctx, execCtx)
    Note over ParamPhase: Extract params from env/event/secrets
    ParamPhase->>ParamPhase: extractConfigParams()
    ParamPhase-->>Pipeline: result

    Pipeline->>PrecondPhase: Execute(ctx, execCtx)
    Note over PrecondPhase: Check preconditions via API
    PrecondPhase->>PrecondPhase: executeAll()
    PrecondPhase-->>Pipeline: result

    alt Preconditions Met
        Pipeline->>ResourcePhase: Execute(ctx, execCtx)
        Note over ResourcePhase: Apply resources via applier
        ResourcePhase->>ResourcePhase: buildManifest()
        ResourcePhase->>Applier: ApplyResources(ctx, resources)
        Applier-->>ResourcePhase: ApplyResourcesResult
        ResourcePhase-->>Pipeline: result
    else Preconditions NotMet
        Pipeline->>Pipeline: Skip Resource & Post phases
    end

    alt Resources Succeeded
        Pipeline->>PostPhase: Execute(ctx, execCtx)
        Note over PostPhase: Always execute post-actions
        PostPhase->>PostPhase: executeAll()
        PostPhase-->>Pipeline: result
    else Resources Failed
        Pipeline->>PostPhase: Execute(ctx, execCtx)
        Note over PostPhase: Post-actions run regardless
        PostPhase-->>Pipeline: result
    end

    Pipeline-->>Client: []PhaseResult
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

This is a substantial architectural refactoring spanning 40+ files with heterogeneous changes across multiple subsystems. The changes introduce a new pipeline-phase execution model, establish new abstraction boundaries (resource_applier), centralize utilities into pkg/utils, migrate core logic to internal/manifest, and refactor three major executor components. The high density of logic changes, diverse file types affected, and multiple interconnected architectural patterns require detailed examination of each subsystem's interactions.

Possibly related PRs

  • PR #37 — Introduces and modifies maestro_client, manifest package, and resource-apply abstractions to enable Maestro/OCM as a ResourceApplier backend alongside k8s_client.
  • PR #13 — Modifies executor component surface (parameter extraction, preconditions, resources, post-actions) and introduces phase-based execution patterns that form the foundation for this refactor.
  • PR #20 — Refactors criteria/evaluator with JSONPath and CEL support, which the executor phases use for condition evaluation and manifest templating.

Suggested labels

refactor, architecture, executor

Suggested reviewers

  • yasun1
  • jsell-rh
  • AlexVulaj
🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title mentions 'integrtaion' (misspelled) and uses vague phrasing like 'Implemented interface' that doesn't convey the main architectural changes. The PR primarily refactors the executor to use a pipeline-based phase architecture with modular resource application backends, not just 'k8s client and maestro client integration'. Consider revising the title to something like 'feat: Refactor executor to pipeline-based phase architecture with pluggable resource appliers' to more accurately reflect the substantial refactoring of the executor structure and the introduction of the new phase-based execution model.
Docstring Coverage ⚠️ Warning Docstring coverage is 69.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

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

🤖 Fix all issues with AI agents
In `@internal/executor/executor.go`:
- Around line 26-49: The stored phase instances on Executor
(paramExtractionPhase, preconditionsPhase, resourcesPhase, postActionsPhase) are
shareable mutable state and can race across concurrent Execute runs; change the
code to create fresh phase instances inside the Executor.Execute method (call
NewParamExtractionPhase, NewPreconditionsPhase, NewResourcesPhase,
NewPostActionsPhase at the start of Execute) and use those local variables for
pipeline execution and for buildExecutionResult reads instead of the Executor
fields; alternatively, if you must keep them on the Executor, protect all phase
Execute and buildExecutionResult accesses with a mutex on the Executor to
serialize runs—update references in buildExecutionResult and Execute to use the
new local instances or to acquire the mutex accordingly.

In `@internal/executor/phase_param_extraction.go`:
- Around line 56-70: The extractConfigParams method currently calls extractParam
with execCtx.Ctx which can differ from the context passed into Execute; change
extractConfigParams (ParamExtractionPhase.extractConfigParams) to accept the
Execute context (ctx context.Context) or otherwise use the ctx from Execute and
pass that ctx into extractParam instead of execCtx.Ctx, and update Execute's
call site to forward its ctx into extractConfigParams; ensure all references to
execCtx.Ctx inside extractConfigParams are replaced with the supplied ctx so
ExecutionContext and extractParam always use the same request-scoped context.
- Around line 171-179: The addMetadataParams function accesses
p.config.Metadata.Name/Namespace/Labels without checking p.config.Metadata; add
a defensive nil check in ParamExtractionPhase.addMetadataParams to handle
missing Metadata (either return/skip adding metadata or populate defaults) or
alternatively mark the AdapterConfig.Metadata field as required
(validate:"required") in the types definition; update addMetadataParams to
safely read p.config.Metadata only after confirming it's non-nil and use empty
values/maps when absent to avoid panics.

In `@internal/k8s_client/apply.go`:
- Around line 228-261: The waitForDeletion loop currently relies solely on the
caller's context and can poll indefinitely; modify Client.waitForDeletion to
impose a built-in timeout by creating a derived context with
context.WithTimeout(ctx, <reasonableDuration>) (e.g., deletionTimeout constant)
and use that derived context in the select loop (remember to defer cancel()), so
the function will stop polling when the deadline is exceeded; update the
error/log path to report the deadline (wrap/return ctx.Err() when deadline
exceeded) and keep using GetResource and apierrors.IsNotFound as the existence
check; alternatively, if you prefer no hard-coded timeout, add clear
documentation to the waitForDeletion comment that callers MUST pass a context
with a deadline.

In `@internal/k8s_client/mock.go`:
- Around line 105-121: The ApplyResource method on MockK8sClient stores the
incoming newManifest directly, risking external mutation; change it to store a
deep copy and return that copy in the ApplyResult: use newManifest.DeepCopy()
when assigning to m.Resources[key] (and return the copied object as the Resource
in the ApplyResult) so behavior matches CreateResource/UpdateResource and
prevents outside changes from affecting the mock's internal state.

In `@internal/maestro_client/resource_applier.go`:
- Around line 248-263: determineOperation currently relies on ResourceVersion
(which is always set) and thus always returns OperationUpdate; instead change
the contract so the caller (ApplyManifestWork) passes the actual operation it
determined: modify determineOperation signature to accept an explicit
manifest.Operation (or a boolean isCreate) and return that operation rather than
inferring from ResourceVersion; update all call sites (notably
ApplyManifestWork) to pass the operation computed there (using its generation
comparison logic) into determineOperation and remove the ResourceVersion-based
logic inside determineOperation.

In `@pkg/utils/convert.go`:
- Around line 92-95: The float32/float64 cases in the type switch in
pkg/utils/convert.go currently cast directly to int64 (cases for float32 and
float64) which can produce undefined/incorrect results for values outside the
int64 range; update those cases to first check bounds using math.MaxInt64 and
math.MinInt64 (and detect NaN/±Inf) and return a clear overflow/error instead of
casting when v > MaxInt64 or v < MinInt64 (for float32 convert via float64 first
or compare using float32 limits), otherwise safely cast to int64; ensure the
error returned is descriptive so callers of the conversion function can handle
the overflow.
🧹 Nitpick comments (10)
internal/manifest/render.go (1)

48-57: Consider adding index to slice rendering errors for better debugging.

When rendering fails for a slice element, the error doesn't indicate which index failed. This can make debugging difficult for large arrays.

💡 Proposed improvement
 	case []interface{}:
 		result := make([]interface{}, len(val))
 		for i, item := range val {
 			rendered, err := renderValue(item, params)
 			if err != nil {
-				return nil, err
+				return nil, fmt.Errorf("index [%d]: %w", i, err)
 			}
 			result[i] = rendered
 		}
 		return result, nil
pkg/utils/template.go (1)

21-26: date and dateFormat are identical functions.

Both functions have the same signature and implementation. Consider consolidating to a single function, or if both are kept for compatibility reasons, add a comment explaining why.

💡 Suggested consolidation
 	"date": func(layout string, t time.Time) string {
 		return t.Format(layout)
 	},
-	"dateFormat": func(layout string, t time.Time) string {
-		return t.Format(layout)
-	},
+	// dateFormat is an alias for date, provided for template compatibility
+	"dateFormat": func(layout string, t time.Time) string {
+		return t.Format(layout)
+	},

Or simply:

"dateFormat": nil, // set after declaration
// Then after the map: TemplateFuncs["dateFormat"] = TemplateFuncs["date"]
internal/maestro_client/operations_test.go (1)

3-4: Stale file path reference in comment.

The comment references internal/generation/generation_test.go, but the generation functionality has been migrated to the manifest package. Consider updating this path to reflect the new location.

📝 Suggested fix
 // Package maestro_client tests
 //
-// Note: Tests for manifest.ValidateGeneration, manifest.ValidateGenerationFromUnstructured,
-// and manifest.ValidateManifestWorkGeneration are in internal/generation/generation_test.go.
+// Note: Tests for manifest.ValidateGeneration, manifest.ValidateGenerationFromUnstructured,
+// and manifest.ValidateManifestWorkGeneration are in internal/manifest/generation_test.go.
 // This file contains tests specific to maestro_client functionality.
pkg/utils/map.go (1)

130-162: Consider using standard library functions for path operations.

The splitPath and joinPath functions can be simplified using strings.Split and strings.Join from the standard library, which are well-tested and more idiomatic.

♻️ Suggested simplification
+import "strings"
+
 // splitPath splits a dot-separated path into parts.
 func splitPath(path string) []string {
 	if path == "" {
 		return nil
 	}
-	// Simple split - doesn't handle escaped dots
-	result := make([]string, 0)
-	start := 0
-	for i := 0; i < len(path); i++ {
-		if path[i] == '.' {
-			if i > start {
-				result = append(result, path[start:i])
-			}
-			start = i + 1
-		}
-	}
-	if start < len(path) {
-		result = append(result, path[start:])
-	}
-	return result
+	return strings.Split(path, ".")
 }
 
 // joinPath joins path parts with dots.
 func joinPath(parts []string) string {
-	if len(parts) == 0 {
-		return ""
-	}
-	result := parts[0]
-	for i := 1; i < len(parts); i++ {
-		result += "." + parts[i]
-	}
-	return result
+	return strings.Join(parts, ".")
 }

Note: The current implementation filters empty segments (e.g., "a..b"["a", "b"]), while strings.Split preserves them (["a", "", "b"]). If filtering is intentional, keep the current implementation or add explicit filtering.

internal/executor/phase.go (1)

74-85: Skipped phases due to prior failure are marked as StatusSuccess.

When a phase is skipped because a previous phase failed (lines 77-84), the Status is set to StatusSuccess, which seems semantically incorrect. A skipped phase didn't actually succeed — it was never executed. This could be misleading when analyzing results.

Consider using a distinct status (e.g., StatusSkipped) if available in ExecutionStatus, or clarify in documentation that StatusSuccess combined with Skipped=true indicates an intentional skip rather than actual execution success.

♻️ Potential clarification

If a StatusSkipped constant exists or can be added:

 results = append(results, PhaseResult{
 	Phase:      phaseName,
-	Status:     StatusSuccess,
+	Status:     StatusSkipped,
 	Skipped:    true,
 	SkipReason: "previous phase failed",
 	Duration:   time.Since(start),
 })

Alternatively, document that Skipped=true should be checked first, and Status only applies when Skipped=false.

internal/manifest/generation.go (2)

241-250: Orphaned documentation comment without corresponding function.

Lines 241-250 contain a documentation comment for ValidateManifest but no function implementation follows. This comment appears to be detached from its function, which based on the relevant snippets exists in internal/manifest/manifest.go. Consider removing this orphaned comment to avoid confusion.

🔧 Suggested fix
-// ValidateManifest validates a Kubernetes manifest has all required fields and annotations.
-// Returns error if:
-//   - Object is nil
-//   - apiVersion is missing
-//   - kind is missing
-//   - metadata.name is missing
-//   - generation annotation is missing or invalid
-//
-// This is used by both k8s_client (for direct K8s resources) and maestro_client (for ManifestWork payloads).
-
 // GetLatestGenerationFromList returns the resource with the highest generation annotation from a list.

374-378: Silently skipping malformed label selector pairs may hide configuration errors.

When MatchesLabels encounters a malformed selector pair (missing =), it silently continues. This could mask invalid selector strings in configuration. Consider logging a warning or returning an error for malformed selectors.

🔧 Suggested improvement
 	for _, pair := range pairs {
 		kv := strings.SplitN(pair, "=", 2)
 		if len(kv) != 2 {
-			continue
+			continue // Skip malformed pair (e.g., empty string between commas)
 		}
internal/executor/phase_param_extraction.go (1)

111-127: Default case silently treats unknown source prefixes as event data paths.

When an unrecognized source prefix is provided (e.g., "unknown.foo"), it falls through to extractFromEvent, which may return confusing errors or unexpected behavior. Consider logging a warning or validating known source prefixes explicitly.

internal/maestro_client/resource_applier.go (1)

164-191: Incorrect GroupResource.Resource field: should be the resource name (plural), not Kind.

In Kubernetes API conventions, GroupResource.Resource should be the plural resource name (e.g., "deployments"), not the Kind (e.g., "Deployment"). Using gvk.Kind will produce incorrect error messages and may not match expected API conventions.

However, since this is constructing a NotFound error for a resource within a ManifestWork (not a real K8s resource lookup), this is an internal detail. The error message will still be informative, but it's technically incorrect.

🔧 Consider documenting or fixing the GroupResource usage

If you want technically correct API errors, you'd need a way to determine the plural resource name. For now, adding a comment explaining the limitation would suffice:

 	// Resource not found in ManifestWork
+	// Note: Using Kind as Resource since we don't have the plural resource name available
 	gr := schema.GroupResource{Group: gvk.Group, Resource: gvk.Kind}
 	return nil, apierrors.NewNotFound(gr, name)
internal/executor/phase_preconditions.go (1)

61-66: Setting both SetError and SetSkipped on precondition failure may cause confusion.

When a precondition evaluation fails with an error (e.g., API call failure), the code calls both SetError and SetSkipped. This could lead to ambiguous state where ResourcesSkipped is true but an error also occurred. Consider whether SetSkipped should only be called for non-error "not met" cases.

🔧 Suggested clarification
 	if outcome.Error != nil {
 		// Process execution error: precondition evaluation failed
 		execCtx.SetError("PreconditionFailed", outcome.Error.Error())
-		execCtx.SetSkipped("PreconditionFailed", outcome.Error.Error())
 		return fmt.Errorf("precondition evaluation failed: %w", outcome.Error)
 	}

If downstream phases need to know resources should be skipped on error, consider a separate flag or letting the returned error handle flow control.

Comment on lines +26 to +49
log := config.Logger

// Create phases (each phase contains its own business logic)
paramExtractionPhase := NewParamExtractionPhase(config.AdapterConfig, config.K8sClient, log)
preconditionsPhase := NewPreconditionsPhase(config.APIClient, config.AdapterConfig, log)
resourcesPhase := NewResourcesPhase(config.K8sClient, config.AdapterConfig, log)
postActionsPhase := NewPostActionsPhase(config.APIClient, config.AdapterConfig, log)

// Create pipeline with all phases
pipeline := NewPipeline(log,
paramExtractionPhase,
preconditionsPhase,
resourcesPhase,
postActionsPhase,
)

return &Executor{
config: config,
precondExecutor: newPreconditionExecutor(config),
resourceExecutor: newResourceExecutor(config),
postActionExecutor: newPostActionExecutor(config),
log: config.Logger,
config: config,
log: log,
pipeline: pipeline,
paramExtractionPhase: paramExtractionPhase,
preconditionsPhase: preconditionsPhase,
resourcesPhase: resourcesPhase,
postActionsPhase: postActionsPhase,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's look at the phase Execute implementations
echo "=== Phase Execute implementations ==="
rg -n --type=go -C5 'func \(.*\*.*Phase\) Execute' internal/executor/

echo ""
echo "=== State assignments in executor package ==="
rg -n --type=go '\.(results|outcome|state)\s*=' internal/executor/

echo ""
echo "=== Phase struct definitions ==="
rg -n --type=go '^type.*Phase struct' internal/executor/

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 4105


🏁 Script executed:

#!/bin/bash
# Get phase struct definitions with their fields
echo "=== Phase struct definitions with fields ==="
rg -n --type=go -A10 '^type.*Phase struct' internal/executor/

echo ""
echo "=== Executor struct definition ==="
rg -n --type=go -A15 '^type Executor struct' internal/executor/

echo ""
echo "=== Executor.Execute method ==="
rg -n --type=go -A20 'func \(\s*e\s*\*Executor\) Execute\(' internal/executor/

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 6361


🏁 Script executed:

#!/bin/bash
# Get full Executor.Execute method
echo "=== Full Executor.Execute method ==="
rg -n --type=go -A100 'func \(\s*e\s*\*Executor\) Execute\(' internal/executor/executor.go | head -120

echo ""
echo "=== How results are retrieved (GetResults methods) ==="
rg -n --type=go 'func.*GetResults\|GetOutcome\|GetPreconditions' internal/executor/

echo ""
echo "=== Pipeline Execute method ==="
rg -n --type=go -A30 'func \(\s*p\s*\*Pipeline\) Execute\(' internal/executor/

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 6039


Guard shared phase state across concurrent Executor execution.

Lines 29-38 store phase instances on the Executor. These phases maintain mutable state fields (results, outcome) that are written during each phase Execute and read in buildExecutionResult (lines 157, 159, 166, 170). If the same Executor instance is used concurrently, results can race: one execution could be writing to a phase's state while another reads it. Consider instantiating phases per Execute call, adding synchronization (mutex), or documenting and enforcing single-threaded use.

🤖 Prompt for AI Agents
In `@internal/executor/executor.go` around lines 26 - 49, The stored phase
instances on Executor (paramExtractionPhase, preconditionsPhase, resourcesPhase,
postActionsPhase) are shareable mutable state and can race across concurrent
Execute runs; change the code to create fresh phase instances inside the
Executor.Execute method (call NewParamExtractionPhase, NewPreconditionsPhase,
NewResourcesPhase, NewPostActionsPhase at the start of Execute) and use those
local variables for pipeline execution and for buildExecutionResult reads
instead of the Executor fields; alternatively, if you must keep them on the
Executor, protect all phase Execute and buildExecutionResult accesses with a
mutex on the Executor to serialize runs—update references in
buildExecutionResult and Execute to use the new local instances or to acquire
the mutex accordingly.

Comment on lines +56 to +70
// extractConfigParams extracts all configured parameters and populates execCtx.Params
func (p *ParamExtractionPhase) extractConfigParams(execCtx *ExecutionContext) error {
for _, param := range p.config.Spec.Params {
value, err := p.extractParam(execCtx.Ctx, param, execCtx.EventData)
if err != nil {
if param.Required {
return NewExecutorError(PhaseParamExtraction, param.Name,
fmt.Sprintf("failed to extract required parameter '%s' from source '%s'", param.Name, param.Source), err)
}
// Use default for non-required params if extraction fails
if param.Default != nil {
execCtx.Params[param.Name] = param.Default
}
continue
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent context usage: extractParam uses execCtx.Ctx instead of the ctx passed to Execute.

The Execute method receives a context.Context parameter, but extractConfigParams passes execCtx.Ctx to extractParam. This could lead to issues if the contexts differ (e.g., different cancellation or deadline). Use the context from Execute consistently.

🔧 Suggested fix
-func (p *ParamExtractionPhase) extractConfigParams(execCtx *ExecutionContext) error {
+func (p *ParamExtractionPhase) extractConfigParams(ctx context.Context, execCtx *ExecutionContext) error {
 	for _, param := range p.config.Spec.Params {
-		value, err := p.extractParam(execCtx.Ctx, param, execCtx.EventData)
+		value, err := p.extractParam(ctx, param, execCtx.EventData)

And update the call site in Execute:

 func (p *ParamExtractionPhase) Execute(ctx context.Context, execCtx *ExecutionContext) error {
 	// Extract configured parameters
-	if err := p.extractConfigParams(execCtx); err != nil {
+	if err := p.extractConfigParams(ctx, execCtx); err != nil {
🤖 Prompt for AI Agents
In `@internal/executor/phase_param_extraction.go` around lines 56 - 70, The
extractConfigParams method currently calls extractParam with execCtx.Ctx which
can differ from the context passed into Execute; change extractConfigParams
(ParamExtractionPhase.extractConfigParams) to accept the Execute context (ctx
context.Context) or otherwise use the ctx from Execute and pass that ctx into
extractParam instead of execCtx.Ctx, and update Execute's call site to forward
its ctx into extractConfigParams; ensure all references to execCtx.Ctx inside
extractConfigParams are replaced with the supplied ctx so ExecutionContext and
extractParam always use the same request-scoped context.

Comment on lines +171 to +179
// addMetadataParams adds adapter and event metadata to execCtx.Params
func (p *ParamExtractionPhase) addMetadataParams(execCtx *ExecutionContext) {
// Add metadata from adapter config
execCtx.Params["metadata"] = map[string]interface{}{
"name": p.config.Metadata.Name,
"namespace": p.config.Metadata.Namespace,
"labels": p.config.Metadata.Labels,
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find and examine the Metadata struct definition
rg -nA10 'type Metadata struct' internal/config_loader/

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 912


🏁 Script executed:

# Check validator_schema.go for Metadata validation rules
cat -n internal/config_loader/validator_schema.go | sed -n '1,200p'

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 157


🏁 Script executed:

# Search for Metadata in validator_schema.go to see validation setup
rg -n 'Metadata' internal/config_loader/validator_schema.go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 165


🏁 Script executed:

# Find validation-related files
fd -t f 'validat' internal/config_loader/

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 202


🏁 Script executed:

# Check AdapterConfig struct definition for Metadata field
rg -nA30 'type AdapterConfig struct' internal/config_loader/types.go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 1465


🏁 Script executed:

# Check how ParamExtractionPhase is initialized and config is guaranteed
rg -nB5 'ParamExtractionPhase' internal/executor/ | head -50

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 3841


🏁 Script executed:

# Check validator.go for Metadata validation logic
cat -n internal/config_loader/validator.go | head -100

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 4013


🏁 Script executed:

# Check if there's additional validation for Metadata as required
rg -nA10 'Metadata' internal/config_loader/validator.go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 850


🏁 Script executed:

# Get the full context of the addMetadataParams function
cat -n internal/executor/phase_param_extraction.go | sed -n '165,185p'

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 936


🏁 Script executed:

# Check struct_validator.go
cat -n internal/config_loader/struct_validator.go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 11377


🏁 Script executed:

# Search for any special handling of Metadata as required
rg -nC5 'Metadata' internal/config_loader/struct_validator.go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 65


🏁 Script executed:

# Search for how configs are loaded
rg -n 'LoadConfig\|UnmarshalYAML\|yaml.Unmarshal' internal/config_loader/ -A5 | head -60

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 65


🏁 Script executed:

# Check tests to see if Metadata is always present
fd -t f '*test.go' internal/config_loader/ -exec rg -l 'Metadata' {} \;

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 571


🏁 Script executed:

# Look at example configs or test data
find . -name '*.yaml' -o -name '*.yml' | head -20

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 754


🏁 Script executed:

# Check example adapter config
cat ./charts/examples/adapterconfig.yaml

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 8540


🏁 Script executed:

# Check the adapter config template
cat ./configs/adapter-config-template.yaml

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 13053


🏁 Script executed:

# Search for config loading function
rg -n 'func.*Load' internal/config_loader/ -A10 | head -80

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 5511


🏁 Script executed:

# Get the full Load function and Parse function from loader.go
cat -n internal/config_loader/loader.go | sed -n '80,150p'

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 2880


🏁 Script executed:

# Check if Metadata is ever nil or uninitialized in the config loading process
rg -n 'Metadata' internal/config_loader/loader.go -B2 -A2

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 65


🏁 Script executed:

# Check actual test to see if Metadata is always present in loaded configs
cat -n internal/config_loader/loader_test.go | sed -n '14,75p'

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 2072


Add defensive nil check for p.config.Metadata fields or validate Metadata as required.

The Metadata field in AdapterConfig lacks a validate:"required" tag, so it could theoretically be nil when parsing a config that omits the metadata section. Accessing p.config.Metadata.Name, Namespace, and Labels directly in addMetadataParams without defensive checks risks a panic. Either:

  1. Add a nil check: if p.config.Metadata == nil { /* handle error */ }
  2. Mark Metadata as required in the struct tag: validate:"required" on line 72 of types.go

This is low-risk in practice since all example configs include metadata, but defensive coding is safer.

🤖 Prompt for AI Agents
In `@internal/executor/phase_param_extraction.go` around lines 171 - 179, The
addMetadataParams function accesses p.config.Metadata.Name/Namespace/Labels
without checking p.config.Metadata; add a defensive nil check in
ParamExtractionPhase.addMetadataParams to handle missing Metadata (either
return/skip adding metadata or populate defaults) or alternatively mark the
AdapterConfig.Metadata field as required (validate:"required") in the types
definition; update addMetadataParams to safely read p.config.Metadata only after
confirming it's non-nil and use empty values/maps when absent to avoid panics.

Comment on lines +228 to +261
// waitForDeletion polls until the resource is confirmed deleted or context times out.
// Returns nil when the resource is confirmed gone (NotFound), or an error otherwise.
func (c *Client) waitForDeletion(
ctx context.Context,
gvk schema.GroupVersionKind,
namespace, name string,
) error {
const pollInterval = 100 * time.Millisecond

ticker := time.NewTicker(pollInterval)
defer ticker.Stop()

for {
select {
case <-ctx.Done():
c.log.Warnf(ctx, "Context cancelled/timed out while waiting for deletion of %s/%s", gvk.Kind, name)
return fmt.Errorf("context cancelled while waiting for resource deletion: %w", ctx.Err())
case <-ticker.C:
_, err := c.GetResource(ctx, gvk, namespace, name)
if err != nil {
// NotFound means the resource is deleted - this is success
if apierrors.IsNotFound(err) {
c.log.Debugf(ctx, "Resource deletion confirmed: %s/%s", gvk.Kind, name)
return nil
}
// Any other error is unexpected
c.log.Errorf(ctx, "Error checking deletion status for %s/%s: %v", gvk.Kind, name, err)
return fmt.Errorf("error checking deletion status: %w", err)
}
// Resource still exists, continue polling
c.log.Debugf(ctx, "Resource %s/%s still exists, waiting for deletion...", gvk.Kind, name)
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -nC5 'waitForDeletion' --type=go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 1676


🏁 Script executed:

sed -n '200,230p' internal/k8s_client/apply.go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 1421


🏁 Script executed:

rg -nC3 'recreateResource' --type=go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 1241


🏁 Script executed:

sed -n '80,120p' internal/k8s_client/apply.go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 1341


🏁 Script executed:

sed -n '1,80p' internal/k8s_client/apply.go | grep -A 20 "func.*ctx"

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 65


🏁 Script executed:

sed -n '40,100p' internal/k8s_client/apply.go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 1868


🏁 Script executed:

rg -nC3 '\.ApplyResource\(' --type=go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 1512


🏁 Script executed:

sed -n '150,180p' internal/k8s_client/apply.go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 1038


🏁 Script executed:

rg -nC3 'ApplyResources' --type=go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 11325


🏁 Script executed:

sed -n '80,110p' internal/executor/phase_resources.go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 1345


🏁 Script executed:

rg -nC3 'executeAll' --type=go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 4782


🏁 Script executed:

rg -nC3 'ResourcesPhase.*Execute\|\.Execute\(ctx' internal/executor --type=go | head -50

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 65


🏁 Script executed:

rg 'context\.WithTimeout|context\.WithCancel' --type=go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 2938


🏁 Script executed:

sed -n '1,100p' cmd/adapter/main.go | grep -A 20 "ctx.*WithCancel"

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 65


🏁 Script executed:

rg -n 'ctx.*WithCancel\|executor.*Execute' cmd/adapter/main.go -A 5 -B 2

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 65


🏁 Script executed:

cat cmd/adapter/main.go | head -200

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 6870


Add a timeout safeguard or document the context requirement for waitForDeletion.

The function has no built-in maximum polling duration and relies entirely on context cancellation for termination. Verification shows that the production context chain (cmd/adapter/main.goResourcesPhaseApplyResourceswaitForDeletion) uses context.WithCancel() without a timeout, meaning if a resource deletion hangs indefinitely in Kubernetes, the polling will continue forever with repeated GET requests at 100ms intervals.

Either add a built-in timeout to waitForDeletion itself, or clearly document that callers must provide a context with an explicit timeout deadline.

🤖 Prompt for AI Agents
In `@internal/k8s_client/apply.go` around lines 228 - 261, The waitForDeletion
loop currently relies solely on the caller's context and can poll indefinitely;
modify Client.waitForDeletion to impose a built-in timeout by creating a derived
context with context.WithTimeout(ctx, <reasonableDuration>) (e.g.,
deletionTimeout constant) and use that derived context in the select loop
(remember to defer cancel()), so the function will stop polling when the
deadline is exceeded; update the error/log path to report the deadline
(wrap/return ctx.Err() when deadline exceeded) and keep using GetResource and
apierrors.IsNotFound as the existence check; alternatively, if you prefer no
hard-coded timeout, add clear documentation to the waitForDeletion comment that
callers MUST pass a context with a deadline.

Comment on lines +105 to +121
// ApplyResource implements K8sClient.ApplyResource
func (m *MockK8sClient) ApplyResource(ctx context.Context, newManifest *unstructured.Unstructured, existing *unstructured.Unstructured, opts *ApplyOptions) (*ApplyResult, error) {
if m.ApplyResourceError != nil {
return nil, m.ApplyResourceError
}
if m.ApplyResourceResult != nil {
return m.ApplyResourceResult, nil
}
// Default behavior: store the resource and return create result
key := newManifest.GetNamespace() + "/" + newManifest.GetName()
m.Resources[key] = newManifest
return &ApplyResult{
Resource: newManifest,
Operation: manifest.OperationCreate,
Reason: "mock apply",
}, nil
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing DeepCopy when storing resource in mock.

Other methods (CreateResource, UpdateResource) use DeepCopy() when storing resources to prevent external mutations from affecting the mock's internal state. ApplyResource should follow the same pattern for consistency.

🔧 Proposed fix
 // Default behavior: store the resource and return create result
 key := newManifest.GetNamespace() + "/" + newManifest.GetName()
-m.Resources[key] = newManifest
+m.Resources[key] = newManifest.DeepCopy()
 return &ApplyResult{
-	Resource:  newManifest,
+	Resource:  newManifest.DeepCopy(),
 	Operation: manifest.OperationCreate,
 	Reason:    "mock apply",
 }, nil
📝 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
// ApplyResource implements K8sClient.ApplyResource
func (m *MockK8sClient) ApplyResource(ctx context.Context, newManifest *unstructured.Unstructured, existing *unstructured.Unstructured, opts *ApplyOptions) (*ApplyResult, error) {
if m.ApplyResourceError != nil {
return nil, m.ApplyResourceError
}
if m.ApplyResourceResult != nil {
return m.ApplyResourceResult, nil
}
// Default behavior: store the resource and return create result
key := newManifest.GetNamespace() + "/" + newManifest.GetName()
m.Resources[key] = newManifest
return &ApplyResult{
Resource: newManifest,
Operation: manifest.OperationCreate,
Reason: "mock apply",
}, nil
}
// ApplyResource implements K8sClient.ApplyResource
func (m *MockK8sClient) ApplyResource(ctx context.Context, newManifest *unstructured.Unstructured, existing *unstructured.Unstructured, opts *ApplyOptions) (*ApplyResult, error) {
if m.ApplyResourceError != nil {
return nil, m.ApplyResourceError
}
if m.ApplyResourceResult != nil {
return m.ApplyResourceResult, nil
}
// Default behavior: store the resource and return create result
key := newManifest.GetNamespace() + "/" + newManifest.GetName()
m.Resources[key] = newManifest.DeepCopy()
return &ApplyResult{
Resource: newManifest.DeepCopy(),
Operation: manifest.OperationCreate,
Reason: "mock apply",
}, nil
}
🤖 Prompt for AI Agents
In `@internal/k8s_client/mock.go` around lines 105 - 121, The ApplyResource method
on MockK8sClient stores the incoming newManifest directly, risking external
mutation; change it to store a deep copy and return that copy in the
ApplyResult: use newManifest.DeepCopy() when assigning to m.Resources[key] (and
return the copied object as the Resource in the ApplyResult) so behavior matches
CreateResource/UpdateResource and prevents outside changes from affecting the
mock's internal state.

Comment on lines +248 to +263
// determineOperation determines the operation that was performed based on the ManifestWork.
func (a *ResourceApplier) determineOperation(work *workv1.ManifestWork) manifest.Operation {
if work == nil {
return manifest.OperationCreate
}

// Check if this is a new ManifestWork (no resourceVersion set by server)
// or an updated one
if work.ResourceVersion == "" {
return manifest.OperationCreate
}

// If the generation matches what we set, it could be create or update
// The actual determination was done in ApplyManifestWork
return manifest.OperationUpdate
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the ApplyManifestWork function implementation
cd /repo && ast-grep --pattern 'func ($receiver) ApplyManifestWork($$$) {
  $$$
}'

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 138


🏁 Script executed:

#!/bin/bash
# Find ApplyManifestWork function in the codebase
ast-grep --pattern 'func ($receiver) ApplyManifestWork($$$) {
  $$$
}'

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 65


🏁 Script executed:

#!/bin/bash
# Search for ApplyManifestWork function definition
rg "func.*ApplyManifestWork" -A 20

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 3510


🏁 Script executed:

#!/bin/bash
# Read the full ApplyManifestWork function
rg "func \(c \*Client\) ApplyManifestWork" -A 100 internal/maestro_client/operations.go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 3574


🏁 Script executed:

#!/bin/bash
# Find where determineOperation is called
rg "determineOperation" internal/maestro_client/resource_applier.go -B 5 -A 5

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 872


🏁 Script executed:

#!/bin/bash
# Check what CreateManifestWork and PatchManifestWork return
rg "func \(c \*Client\) CreateManifestWork" -A 30 internal/maestro_client/operations.go | head -40

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 1283


🏁 Script executed:

#!/bin/bash
# Check what PatchManifestWork returns
rg "func \(c \*Client\) PatchManifestWork" -A 30 internal/maestro_client/operations.go | head -40

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 887


🏁 Script executed:

#!/bin/bash
# Verify that Kubernetes client operations set ResourceVersion
# Check the full CreateManifestWork to see what's returned
rg "func \(c \*Client\) CreateManifestWork" -A 50 internal/maestro_client/operations.go | head -55

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 1889


🏁 Script executed:

#!/bin/bash
# Get more context on where determineOperation is used in resource_applier.go
rg "determineOperation" internal/maestro_client/resource_applier.go -B 10 -A 10

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 1467


🏁 Script executed:

#!/bin/bash
# Check the full context of determineOperation function
cat -n internal/maestro_client/resource_applier.go | sed -n '248,270p'

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 1061


🏁 Script executed:

#!/bin/bash
# Verify: Check if there's any scenario where ApplyManifestWork might return work with empty ResourceVersion
# Look at what ApplyManifestWork actually returns
rg "switch decision.Operation" -A 20 internal/maestro_client/operations.go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 950


🏁 Script executed:

#!/bin/bash
# Double check: understand standard Kubernetes behavior
# Check if there's any documentation or comments about ResourceVersion behavior
rg "ResourceVersion" internal/maestro_client/ -B 2 -A 2 | head -50

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 469


🏁 Script executed:

#!/bin/bash
# Let's check what the actual flow is when ApplyManifestWork is called
# and verify the context of how determineOperation is used
rg "ApplyManifestWork" internal/maestro_client/resource_applier.go -B 5 -A 15

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 2268


🏁 Script executed:

#!/bin/bash
# Check how the result of determineOperation is used
rg "a.determineOperation\|determineOperation\(appliedWork\)" internal/maestro_client/resource_applier.go -A 15

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 65


🏁 Script executed:

#!/bin/bash
# Find how the result of determineOperation is used
rg "op := a.determineOperation" internal/maestro_client/resource_applier.go -A 20

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 817


🏁 Script executed:

#!/bin/bash
# Verify the Kubernetes behavior: when Create API is called, does it set ResourceVersion?
# Check test files to see what the actual behavior is
rg "ResourceVersion" test/ -l | head -5

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 196


determineOperation will always return OperationUpdate because ResourceVersion is set by the Kubernetes API for both create and patch operations.

The function checks if work.ResourceVersion == "" to determine if a new resource was created. However, the Kubernetes API server sets ResourceVersion on all successful responses—whether from Create, Patch, or Update operations. Since ApplyManifestWork returns the result directly from these API calls, the ResourceVersion will never be empty.

This means:

  • Create operations: ResourceVersion is set by the server → function returns OperationUpdate (incorrect)
  • Patch operations: ResourceVersion is set by the server → function returns OperationUpdate (correct)

The actual operation type was already correctly determined in ApplyManifestWork using generation comparison, but that information is lost by the time the function checks ResourceVersion. The caller receives an incorrect operation type for creates.

🤖 Prompt for AI Agents
In `@internal/maestro_client/resource_applier.go` around lines 248 - 263,
determineOperation currently relies on ResourceVersion (which is always set) and
thus always returns OperationUpdate; instead change the contract so the caller
(ApplyManifestWork) passes the actual operation it determined: modify
determineOperation signature to accept an explicit manifest.Operation (or a
boolean isCreate) and return that operation rather than inferring from
ResourceVersion; update all call sites (notably ApplyManifestWork) to pass the
operation computed there (using its generation comparison logic) into
determineOperation and remove the ResourceVersion-based logic inside
determineOperation.

Comment on lines +92 to +95
case float32:
return int64(v), nil
case float64:
return int64(v), nil
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Float-to-int64 conversion may produce unexpected results for extreme values.

When converting float64 values outside the int64 range (e.g., 1e20 or -1e20), the conversion produces undefined behavior per the Go spec. Consider adding bounds checking similar to the uint64 overflow check.

🛡️ Proposed fix
 	case float32:
-		return int64(v), nil
+		if v > float32(math.MaxInt64) || v < float32(math.MinInt64) {
+			return 0, fmt.Errorf("float32 value %v overflows int64", v)
+		}
+		return int64(v), nil
 	case float64:
-		return int64(v), nil
+		if v > float64(math.MaxInt64) || v < float64(math.MinInt64) {
+			return 0, fmt.Errorf("float64 value %v overflows int64", v)
+		}
+		return int64(v), nil
📝 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
case float32:
return int64(v), nil
case float64:
return int64(v), nil
case float32:
if v > float32(math.MaxInt64) || v < float32(math.MinInt64) {
return 0, fmt.Errorf("float32 value %v overflows int64", v)
}
return int64(v), nil
case float64:
if v > float64(math.MaxInt64) || v < float64(math.MinInt64) {
return 0, fmt.Errorf("float64 value %v overflows int64", v)
}
return int64(v), nil
🤖 Prompt for AI Agents
In `@pkg/utils/convert.go` around lines 92 - 95, The float32/float64 cases in the
type switch in pkg/utils/convert.go currently cast directly to int64 (cases for
float32 and float64) which can produce undefined/incorrect results for values
outside the int64 range; update those cases to first check bounds using
math.MaxInt64 and math.MinInt64 (and detect NaN/±Inf) and return a clear
overflow/error instead of casting when v > MaxInt64 or v < MinInt64 (for float32
convert via float64 first or compare using float32 limits), otherwise safely
cast to int64; ensure the error returned is descriptive so callers of the
conversion function can handle the overflow.

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.

1 participant