Skip to content

feat: maestro client implemented in adapter framework#37

Open
xueli181114 wants to merge 3 commits intoopenshift-hyperfleet:mainfrom
xueli181114:HYPERFLEET-564
Open

feat: maestro client implemented in adapter framework#37
xueli181114 wants to merge 3 commits intoopenshift-hyperfleet:mainfrom
xueli181114:HYPERFLEET-564

Conversation

@xueli181114
Copy link
Contributor

@xueli181114 xueli181114 commented Feb 3, 2026

Summary by CodeRabbit

  • New Features

    • Maestro client for ManifestWork management and generation-based resource tracking.
    • New version package and configurable User-Agent; build/version embedding updated.
    • Added logging/context keys and helper to attach Maestro consumer to contexts.
    • New public error types for Maestro and configuration errors.
    • Expanded Google Pub/Sub broker options (retry, dead-letter, receive, behavior flags).
  • Documentation

    • Consolidated Environment Variables section with deployment examples and sample adapter deployment config.
  • Tests

    • New unit and integration tests covering generation logic and Maestro client flows.

@openshift-ci openshift-ci bot requested review from rh-amarin and yasun1 February 3, 2026 12:44
@coderabbitai
Copy link

coderabbitai bot commented Feb 3, 2026

Walkthrough

This PR adds a generation-based resource tracking package and integrates it into the executor (new applyResource flow), removes legacy generation helpers, and updates ResourceResult to use generation.Operation. It introduces a Maestro client (gRPC + OpenAPI) with ManifestWork CRUD/upsert and an OCM logger adapter, plus Maestro integration tests. It adds a sample AdapterDeploymentConfig, removes the default adapter config, centralizes build/version info in pkg/version, migrates logging/context keys to string constants, updates README/config docs, and bumps multiple dependencies and build/linker settings.

Sequence Diagram(s)

sequenceDiagram
    participant Executor as ResourceExecutor
    participant Apply as applyResource
    participant Gen as generation.Package
    participant K8s as K8sClient
    participant ExecCtx as ExecutionContext

    Executor->>Apply: executeResource(resource, manifest)
    Apply->>K8s: discover existing resource (selector/name)
    alt resource exists
        K8s-->>Apply: existing resource
        Apply->>Gen: GetGenerationFromUnstructured(existing)
        Gen-->>Apply: existingGen
    else not found
        K8s-->>Apply: NotFound
        Note right of Apply: existingGen = 0
    end
    Apply->>Gen: GetGenerationFromUnstructured(manifest)
    Gen-->>Apply: newGen
    Apply->>Gen: CompareGenerations(newGen, existingGen, exists)
    Gen-->>Apply: ApplyDecision(operation, reason)
    alt create
        Apply->>K8s: CreateResource(manifest)
    else update
        Apply->>K8s: PatchResource(manifest)
    else recreate
        Apply->>K8s: DeleteResource(existing)
        Apply->>K8s: CreateResource(manifest)
    else skip
        Apply-->>Executor: return existing (skip)
    end
    Apply-->>ExecCtx: store ResourceResult(operation, resource)
Loading
sequenceDiagram
    participant Caller as Adapter
    participant Maestro as MaestroClient
    participant Gen as generation.Package
    participant OCM as WorkV1Client
    participant OpenAPI as MaestroOpenAPI

    Caller->>Maestro: ApplyManifestWork(consumer, work)
    Maestro->>Gen: ValidateManifestWorkGeneration(work)
    Maestro->>OCM: GetManifestWork(consumer, name)
    alt exists
        OCM-->>Maestro: existing work
        Maestro->>Gen: CompareGenerations(newGen, existingGen, true)
        Gen-->>Maestro: ApplyDecision
    else not found
        Note right of Maestro: decision = create
    end
    alt create
        Maestro->>OCM: CreateManifestWork(consumer, work)
    else update
        Maestro->>Maestro: createManifestWorkPatch(work)
        Maestro->>OCM: PatchManifestWork(consumer, name, patch)
    else skip
        Maestro-->>Caller: return existing
    end
    Maestro->>OpenAPI: (optional) register consumer / health checks
    Maestro-->>Caller: result
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

lgtm

Suggested reviewers

  • yasun1
  • rh-amarin
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.78% 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 and specifically describes the main change: implementing a Maestro client in the adapter framework. It is concise, direct, and accurately reflects the primary objective of the PR.

✏️ 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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pkg/errors/error_test.go (1)

74-102: ⚠️ Potential issue | 🟡 Minor

Missing new error codes in requiredCodes validation.

The expected count was updated to 17, but requiredCodes still only contains 15 codes. Add ErrorMaestroError and ErrorConfigurationError to ensure these new error codes are explicitly validated.

Proposed fix
 		requiredCodes := []ServiceErrorCode{
 			ErrorNotFound,
 			ErrorValidation,
 			ErrorConflict,
 			ErrorForbidden,
 			ErrorUnauthorized,
 			ErrorUnauthenticated,
 			ErrorBadRequest,
 			ErrorMalformedRequest,
 			ErrorNotImplemented,
 			ErrorGeneral,
 			ErrorAdapterConfigNotFound,
 			ErrorBrokerConnectionError,
 			ErrorKubernetesError,
 			ErrorHyperFleetAPIError,
 			ErrorInvalidCloudEvent,
+			ErrorMaestroError,
+			ErrorConfigurationError,
 		}
internal/k8s_client/mock.go (1)

3-10: ⚠️ Potential issue | 🟠 Major

Populate ApplyResourceResult.Operation and avoid silent nil on unknown ops.

ApplyResources currently appends results without Operation, so callers inspecting create/update/skip always see the zero value. Also, ApplyResource falls through to return nil, nil on an unexpected operation, which can mask bugs. Consider centralizing apply logic into a helper that returns ApplyResourceResult with Operation set and returns an explicit error for unknown operations.

✅ Suggested refactor to preserve Operation and return explicit errors
 import (
 	"context"
+	"fmt"

 	"github.com/openshift-hyperfleet/hyperfleet-adapter/internal/generation"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 )
@@
-// ApplyResource implements K8sClient.ApplyResource
-// It creates or updates a resource based on generation comparison
-func (m *MockK8sClient) ApplyResource(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
-	if m.ApplyResourceError != nil {
-		return nil, m.ApplyResourceError
-	}
-	if m.ApplyResourceResult != nil {
-		return m.ApplyResourceResult, nil
-	}
-
-	gvk := obj.GroupVersionKind()
-	namespace := obj.GetNamespace()
-	name := obj.GetName()
-	newGeneration := generation.GetGenerationFromUnstructured(obj)
-
-	// Check if resource exists
-	existingObj, err := m.GetResource(ctx, gvk, namespace, name)
-	exists := err == nil
-	if err != nil && !apierrors.IsNotFound(err) {
-		return nil, err
-	}
-
-	// Get existing generation (0 if not found)
-	var existingGeneration int64
-	if exists {
-		existingGeneration = generation.GetGenerationFromUnstructured(existingObj)
-	}
-
-	// Compare generations to determine operation
-	compareResult := generation.CompareGenerations(newGeneration, existingGeneration, exists)
-
-	// Execute operation based on comparison result
-	switch compareResult.Operation {
-	case generation.OperationCreate:
-		return m.CreateResource(ctx, obj)
-	case generation.OperationSkip:
-		return existingObj, nil
-	case generation.OperationUpdate:
-		obj.SetResourceVersion(existingObj.GetResourceVersion())
-		return m.UpdateResource(ctx, obj)
-	}
-
-	return nil, nil
-}
+// applyResourceResult centralizes apply logic for ApplyResource/ApplyResources.
+func (m *MockK8sClient) applyResourceResult(ctx context.Context, obj *unstructured.Unstructured) (ApplyResourceResult, error) {
+	if m.ApplyResourceError != nil {
+		return ApplyResourceResult{Error: m.ApplyResourceError}, m.ApplyResourceError
+	}
+	if m.ApplyResourceResult != nil {
+		return ApplyResourceResult{Resource: m.ApplyResourceResult}, nil
+	}
+
+	gvk := obj.GroupVersionKind()
+	namespace := obj.GetNamespace()
+	name := obj.GetName()
+	newGeneration := generation.GetGenerationFromUnstructured(obj)
+
+	existingObj, err := m.GetResource(ctx, gvk, namespace, name)
+	exists := err == nil
+	if err != nil && !apierrors.IsNotFound(err) {
+		return ApplyResourceResult{Error: err}, err
+	}
+
+	var existingGeneration int64
+	if exists {
+		existingGeneration = generation.GetGenerationFromUnstructured(existingObj)
+	}
+
+	compareResult := generation.CompareGenerations(newGeneration, existingGeneration, exists)
+
+	switch compareResult.Operation {
+	case generation.OperationCreate:
+		res, err := m.CreateResource(ctx, obj)
+		return ApplyResourceResult{Resource: res, Operation: compareResult.Operation, Error: err}, err
+	case generation.OperationSkip:
+		return ApplyResourceResult{Resource: existingObj, Operation: compareResult.Operation}, nil
+	case generation.OperationUpdate:
+		obj.SetResourceVersion(existingObj.GetResourceVersion())
+		res, err := m.UpdateResource(ctx, obj)
+		return ApplyResourceResult{Resource: res, Operation: compareResult.Operation, Error: err}, err
+	default:
+		err := fmt.Errorf("unsupported operation %q", compareResult.Operation)
+		return ApplyResourceResult{Error: err}, err
+	}
+}
+
+// ApplyResource implements K8sClient.ApplyResource
+func (m *MockK8sClient) ApplyResource(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
+	result, err := m.applyResourceResult(ctx, obj)
+	return result.Resource, err
+}
@@
 func (m *MockK8sClient) ApplyResources(ctx context.Context, objs []*unstructured.Unstructured) ([]ApplyResourceResult, error) {
 	results := make([]ApplyResourceResult, 0, len(objs))

 	for _, obj := range objs {
-		resource, err := m.ApplyResource(ctx, obj)
-		if err != nil {
-			results = append(results, ApplyResourceResult{Error: err})
-			return results, err
-		}
-		results = append(results, ApplyResourceResult{Resource: resource})
+		result, err := m.applyResourceResult(ctx, obj)
+		results = append(results, result)
+		if err != nil {
+			return results, err
+		}
 	}

 	return results, nil
 }

Also applies to: 130-190

🤖 Fix all issues with AI agents
In `@configs/broker-configmap-pubsub-template.yaml`:
- Around line 89-94: The template's documented default ("Default: false")
contradicts the actual flag values; update the behavior flags so
`create_topic_if_missing` and `create_subscription_if_missing` either default to
false (recommended for production) or keep true but add a clarifying comment
that these permissive settings are intentional for development/demo usage;
modify the block containing `create_topic_if_missing` and
`create_subscription_if_missing` accordingly and ensure the comment above the
flags reflects the chosen default behavior.

In `@examples/maestro_client/adapter-deployment-config.yaml`:
- Around line 72-76: The example uses the key "httpAPI" but the main config uses
"hyperfleetApi", so update the example YAML to use "hyperfleetApi" (rename the
"httpAPI:" mapping to "hyperfleetApi:") while keeping the same nested fields
(timeout, retryAttempts, retryBackoff) and values; ensure any documentation or
nearby comments in the example are consistent with the new key so users
referencing hyperfleetApi see the same structure as the main config.

In `@internal/k8s_client/client.go`:
- Around line 479-486: The ApplyResources loop never sets
ApplyResourceResult.Operation, so callers cannot know what action was taken;
update either ApplyResource to return an operation string/enum alongside the
resource (e.g., change ApplyResource(ctx, obj) to return (resource, operation,
error)) or fetch the operation value returned by the existing ApplyResource call
and populate ApplyResourceResult.Operation before appending; modify the
ApplyResources function to capture that operation and set
ApplyResourceResult{Resource: resource, Operation: operation} for each
iteration, and update any callers/tests to handle the new return signature of
ApplyResource if you choose that route.

In `@internal/maestro_client/client.go`:
- Around line 229-335: The configureTLS function can silently return nil (no
TLS) when config.Insecure is false but no CA/cert/token files are provided;
change it to fail fast: after handling the three TLS branches (mutual TLS,
token+CA, CA-only) detect the case where config.Insecure is false and
grpcOptions.Dialer.TLSConfig is still nil and return a descriptive error (e.g.,
"no TLS configuration provided: provide CAFile or client certs or set
Insecure=true"); update configureTLS to reference Config fields (Insecure,
CAFile, ClientCertFile, ClientKeyFile, TokenFile) and
grpcOptions.Dialer.TLSConfig so callers get a clear failure instead of silently
falling back to plaintext; alternatively (optional) implement a fallback using
system root CAs if you prefer that behavior instead of returning an error.

In `@internal/maestro_client/ocm_logger_adapter.go`:
- Around line 77-84: The Fatal method on ocmLoggerAdapter currently calls
a.log.Errorf and does not terminate the process; replace that call with
a.log.Fatal and pass a single formatted message (use fmt.Sprintf(format,
args...)) so the adapter's Fatal(ctx, ...) uses a.log.Fatal(ctx, formattedMsg)
and triggers the underlying logger's os.Exit behavior instead of just logging an
error.

In `@internal/maestro_client/operations.go`:
- Around line 253-264: createManifestWorkPatch currently always includes
"labels" and "annotations" in the JSON patch which will set them to null (and
delete server-side values) if work.Labels or work.Annotations are nil; change
createManifestWorkPatch so it only adds the "labels" and "annotations" keys to
the patch map when work.Labels != nil and work.Annotations != nil respectively
(leave them out when nil) while still including "spec" (refer to
createManifestWorkPatch, work.Labels, work.Annotations, work.Spec); then marshal
and return the patch as before.
🧹 Nitpick comments (11)
configs/adapter-deployment-config.yaml (1)

85-95: Minor formatting inconsistency in YAML values.

The hyperfleetApi section uses unquoted values (2s, exponential) while the maestro section uses quoted values ("30s", "exponential"). While YAML parsers accept both, consistent quoting improves readability.

Suggested fix for consistency
     # HyperFleet HTTP API client
     hyperfleetApi:
-      timeout: 2s
+      timeout: "2s"
       retryAttempts: 3
-      retryBackoff: exponential
+      retryBackoff: "exponential"
examples/maestro_client/adapter-deployment-config.yaml (1)

46-59: Missing httpCaFile in TLS config.

The main deployment config includes httpCaFile for HTTP API TLS configuration (allowing different CA than gRPC), but this example omits it. Consider adding it for completeness, or document when it's optional.

Suggested addition
           # Server name for TLS verification
           # Environment variable: HYPERFLEET_MAESTRO_SERVER_NAME
           serverName: "maestro-grpc.maestro.svc.cluster.local"
+          
+          # HTTP API TLS configuration (may use different CA than gRPC)
+          # If not set, falls back to caFile for backwards compatibility
+          # Environment variable: HYPERFLEET_MAESTRO_HTTP_CA_FILE
+          httpCaFile: "/etc/maestro/certs/https/ca.crt"
internal/generation/generation.go (1)

232-255: Avoid mutating the input list during sort.
Sorting list.Items in place can surprise callers that reuse the list order. Consider copying before sorting.

♻️ Suggested change to avoid in-place mutation
-	// Sort by generation annotation (descending) to return the one with the latest generation
-	// Secondary sort by metadata.name for consistency when generations are equal
-	sort.Slice(list.Items, func(i, j int) bool {
-		genI := GetGenerationFromUnstructured(&list.Items[i])
-		genJ := GetGenerationFromUnstructured(&list.Items[j])
+	// Sort by generation annotation (descending) to return the one with the latest generation
+	// Secondary sort by metadata.name for consistency when generations are equal
+	items := append([]unstructured.Unstructured(nil), list.Items...)
+	sort.Slice(items, func(i, j int) bool {
+		genI := GetGenerationFromUnstructured(&items[i])
+		genJ := GetGenerationFromUnstructured(&items[j])
 		if genI != genJ {
 			return genI > genJ // Descending order - latest generation first
 		}
 		// Fall back to metadata.name for deterministic ordering when generations are equal
-		return list.Items[i].GetName() < list.Items[j].GetName()
+		return items[i].GetName() < items[j].GetName()
 	})
 
-	return &list.Items[0]
+	return &items[0]
internal/maestro_client/operations_test.go (2)

100-104: Error message assertion is incomplete when expectError is true.

When tt.expectError is true and tt.errorMsg is set, the test doesn't verify that the actual error contains the expected message. This reduces test effectiveness for validating specific error conditions.

🔧 Proposed fix to verify error messages
 		t.Run(tt.name, func(t *testing.T) {
 			err := generation.ValidateGeneration(tt.meta)
 
 			if tt.expectError {
 				if err == nil {
 					t.Errorf("expected error containing %q, got nil", tt.errorMsg)
 				}
+				if tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) {
+					t.Errorf("expected error containing %q, got %q", tt.errorMsg, err.Error())
+				}
 				return
 			}

Add "strings" to the imports.


205-206: Ignored MarshalJSON errors in test helpers could mask issues.

While unlikely to fail for these simple objects, silently ignoring errors can hide bugs during test development.

🔧 Proposed fix to handle errors
 	createManifest := func(kind, name, generation string) workv1.Manifest {
+		t.Helper()
 		obj := &unstructured.Unstructured{
 			Object: map[string]interface{}{
 				"apiVersion": "v1",
 				"kind":       kind,
 				"metadata": map[string]interface{}{
 					"name": name,
 					"annotations": map[string]interface{}{
 						constants.AnnotationGeneration: generation,
 					},
 				},
 			},
 		}
-		raw, _ := obj.MarshalJSON()
+		raw, err := obj.MarshalJSON()
+		if err != nil {
+			t.Fatalf("failed to marshal manifest: %v", err)
+		}
 		return workv1.Manifest{RawExtension: runtime.RawExtension{Raw: raw}}
 	}

Apply similar changes to createManifestNoGeneration.

Also applies to: 220-221

test/integration/maestro_client/main_test.go (3)

19-19: Using :latest tag reduces test reproducibility.

The MaestroImage uses :latest which can lead to non-reproducible test results as the image content may change between runs. Consider pinning to a specific version or digest.

🔧 Proposed fix
-	MaestroImage = "quay.io/redhat-user-workloads/maestro-rhtap-tenant/maestro/maestro:latest"
+	// TODO: Pin to a specific version for reproducible tests
+	// Example: MaestroImage = "quay.io/redhat-user-workloads/maestro-rhtap-tenant/maestro/maestro:v0.1.0"
+	MaestroImage = "quay.io/redhat-user-workloads/maestro-rhtap-tenant/maestro/maestro:latest"

100-101: Redundant fmt.Sprintf with println.

Using fmt.Sprintf with println is verbose. Consider using fmt.Printf or fmt.Println directly for consistency.

🔧 Proposed fix
-			println(fmt.Sprintf("   HTTP API: %s", env.MaestroServerAddr))
-			println(fmt.Sprintf("   gRPC:     %s", env.MaestroGRPCAddr))
+			fmt.Printf("   HTTP API: %s\n", env.MaestroServerAddr)
+			fmt.Printf("   gRPC:     %s\n", env.MaestroGRPCAddr)

Add "fmt" to imports if not already present.


80-81: Consider logging provider.Close() errors.

While the error from provider.Close() may not be critical, silently ignoring it could hide resource cleanup issues during debugging.

🔧 Proposed fix
-		_ = provider.Close()
+		if closeErr := provider.Close(); closeErr != nil {
+			println("⚠️  Warning: Failed to close provider:", closeErr.Error())
+		}
test/integration/maestro_client/client_integration_test.go (2)

249-254: Unsafe type assertions could cause test panic.

The chained type assertions on lines 251-252 can panic if the map structure doesn't match expectations. While this is test code, a panic provides less informative failure messages than explicit checks.

🔧 Proposed fix for safer assertions
 		// Now apply again with updated generation (should update)
 		work.Annotations[constants.AnnotationGeneration] = "2"
-		configMapManifest["metadata"].(map[string]interface{})["annotations"].(map[string]interface{})[constants.AnnotationGeneration] = "2"
-		configMapManifest["data"].(map[string]interface{})["key2"] = "value2"
-		configMapJSON, _ = json.Marshal(configMapManifest)
+		metadata := configMapManifest["metadata"].(map[string]interface{})
+		annotations := metadata["annotations"].(map[string]interface{})
+		annotations[constants.AnnotationGeneration] = "2"
+		data := configMapManifest["data"].(map[string]interface{})
+		data["key2"] = "value2"
+		configMapJSON, err = json.Marshal(configMapManifest)
+		require.NoError(t, err, "Failed to marshal updated configmap")
 		work.Spec.Workload.Manifests[0].Raw = configMapJSON

33-38: Minor formatting inconsistency with Insecure field.

The Insecure: true field is not aligned with other struct fields in the Config initialization. While not affecting functionality, consistent formatting improves readability.

🔧 Example fix
 	config := &maestro_client.Config{
 		MaestroServerAddr:  env.MaestroServerAddr,
 		GRPCServerAddr:     env.MaestroGRPCAddr,
 		SourceID:           "integration-test-source",
-		Insecure: true,
+		Insecure:           true,
 	}

Also applies to: 62-67, 147-152, 186-191, 280-285

internal/maestro_client/client.go (1)

217-221: Silent failure if CA certificate PEM is malformed.

AppendCertsFromPEM returns false if no valid certificates were found, but the error message doesn't indicate whether the file was empty, contained invalid PEM data, or had valid PEM but invalid certificate content.

🔧 Proposed improvement for better diagnostics
 	if httpCAFile != "" {
 		caCert, err := os.ReadFile(httpCAFile)
 		if err != nil {
 			return nil, err
 		}
+		if len(caCert) == 0 {
+			return nil, apperrors.ConfigurationError("CA certificate file %s is empty", httpCAFile).AsError()
+		}
 		caCertPool := x509.NewCertPool()
 		if !caCertPool.AppendCertsFromPEM(caCert) {
-			return nil, apperrors.ConfigurationError("failed to parse CA certificate from %s", httpCAFile).AsError()
+			return nil, apperrors.ConfigurationError("failed to parse CA certificate from %s: file may not contain valid PEM-encoded certificates", httpCAFile).AsError()
 		}
 		tlsConfig.RootCAs = caCertPool
 	}

Comment on lines +77 to +84
// Fatal logs at error level with formatting.
// Note: Does not exit - the underlying logger handles that behavior.
func (a *ocmLoggerAdapter) Fatal(ctx context.Context, format string, args ...interface{}) {
if ctx == nil {
ctx = context.Background()
}
a.log.Errorf(ctx, "FATAL: "+format, args...)
}
Copy link

@coderabbitai coderabbitai bot Feb 3, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

OCM SDK go logging.Logger Fatal method interface expectations

💡 Result:

In ocm-sdk-go, the logger you pass to ConnectionBuilder.Logger(...) must implement the logging.Logger interface. That interface expects a printf-style Fatal method with this exact shape:

Fatal(ctx context.Context, format string, args ...interface{})

Along with the other methods:

  • Debug(ctx, format, args...), Info(...), Warn(...), Error(...)
  • DebugEnabled() bool, InfoEnabled() bool, WarnEnabled() bool, ErrorEnabled() bool (there is no FatalEnabled) [1]
    and it’s wired into the SDK via ConnectionBuilder.Logger(logger logging.Logger) [2].

Two key “expectations” to be aware of:

  1. format is a format string (like fmt.Sprintf). The project even provides an analyzer (OcmLoggerLinter) that checks calls like logger.Warn(ctx, "x=%s y=%v", a, b) have the right number of formatting args, and it only applies when the receiver is github.com/openshift-online/ocm-sdk-go/logging.Logger (or pointer to it) [3].

  2. ctx may be nil in SDK calls (explicitly called out for Debug/Info/Warn/Error), so your implementation should handle nil contexts without panicking [1]. (In practice, it’s safest to accept nil for Fatal too.)

Sources: [1] [2] [3]


🏁 Script executed:

# First, let's understand the underlying logger type and the adapter structure
head -100 internal/maestro_client/ocm_logger_adapter.go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 2517


🏁 Script executed:

# Search for the logger type definition and Fatal method implementations in OCM SDK
rg -t go "logging\.Logger" internal/maestro_client/

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 434


🏁 Script executed:

# Check if there are any imports from the OCM SDK
rg -t go "import.*ocm\|import.*logging" internal/maestro_client/ocm_logger_adapter.go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 65


🏁 Script executed:

# Find the logger package files
fd -t f "\.go$" pkg/logger

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 240


🏁 Script executed:

# Look at the logger interface definition
rg -t go "interface.*Logger\|func.*Fatal" pkg/logger --context=3

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 65


🏁 Script executed:

# Check if Fatal is called elsewhere in the codebase
rg -t go "\.Fatal\(" --context=2

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 6195


🏁 Script executed:

# Read the logger interface definition
cat -n pkg/logger/logger.go

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 10248


🏁 Script executed:

# Also check for Fatal specifically in logger implementations
rg -t go "func.*Fatal" pkg/logger/

Repository: openshift-hyperfleet/hyperfleet-adapter

Length of output: 165


Fix Fatal to exit the process as expected by OCM SDK callers.

The Fatal method should call a.log.Fatal() instead of a.log.Errorf() to ensure the process terminates. The underlying logger's Fatal implementation already calls os.Exit(1), but the adapter currently bypasses it by logging at error level without exiting. Update to:

a.log.Fatal(ctx, fmt.Sprintf(format, args...))

This ensures the adapter's Fatal method matches OCM SDK expectations for fatal error handling.

🤖 Prompt for AI Agents
In `@internal/maestro_client/ocm_logger_adapter.go` around lines 77 - 84, The
Fatal method on ocmLoggerAdapter currently calls a.log.Errorf and does not
terminate the process; replace that call with a.log.Fatal and pass a single
formatted message (use fmt.Sprintf(format, args...)) so the adapter's Fatal(ctx,
...) uses a.log.Fatal(ctx, formattedMsg) and triggers the underlying logger's
os.Exit behavior instead of just logging an error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maestro failure will reflect in runtime error. I don't want maestro client failure cause my adapter exit unexpectedly.

Copy link

Choose a reason for hiding this comment

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

@xueli181114, understood! Your design decision makes sense—keeping the adapter resilient by treating Maestro client failures as runtime errors instead of fatal exits is appropriate for your use case. The current implementation is correct for your requirements.


✏️ Learnings added
Learnt from: xueli181114
Repo: openshift-hyperfleet/hyperfleet-adapter PR: 37
File: internal/maestro_client/ocm_logger_adapter.go:77-84
Timestamp: 2026-02-04T06:51:47.082Z
Learning: In the hyperfleet-adapter repository, the ocmLoggerAdapter's Fatal method intentionally calls a.log.Errorf instead of a.log.Fatal to prevent Maestro client failures from causing the adapter process to exit unexpectedly. Maestro failures should be reflected as runtime errors, not fatal exits.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: xueli181114
Repo: openshift-hyperfleet/hyperfleet-adapter PR: 13
File: internal/executor/post_action_executor.go:198-205
Timestamp: 2025-12-04T14:06:51.656Z
Learning: Logger refactor is tracked in HYPERFLEET-304 for the hyperfleet-adapter repository, which will address how CEL evaluation failures and similar errors are handled and logged.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
go.mod (1)

30-171: ⚠️ Potential issue | 🟡 Minor

Correct the vulnerability scanning approach for indirect dependency updates.

Verifying security posture for indirect dependency updates is appropriate given the volume of changes. However, the referenced tool (sonatard/go-vulncheck) does not exist. Use the official golang.org/x/vuln/cmd/govulncheck instead:

go install golang.org/x/vuln/cmd/govulncheck@latest && govulncheck ./...

Spot checks of key packages (golang.org/x/crypto v0.47.0, google.golang.org/grpc v1.78.0, getsentry/sentry-go v0.20.0) show no active published vulnerabilities, though running the above scan on the complete dependency tree remains a best practice.

🤖 Fix all issues with AI agents
In `@configs/broker-configmap-pubsub-template.yaml`:
- Around line 131-135: The example deployment comments out the required
environment variable BROKER_TOPIC, causing copy-paste deployments to fail;
update the template so the BROKER_TOPIC env var block (the configMapKeyRef
referencing name: hyperfleet-broker-config key: BROKER_TOPIC) is uncommented in
the example, or alternatively change the top-level label that marks BROKER_TOPIC
as REQUIRED to OPTIONAL and document the optionality—ensure the configMapKeyRef
stanza for BROKER_TOPIC is present and consistent with the documentation.

In `@go.mod`:
- Line 12: The go.mod entry for the Maestro dependency
(github.com/openshift-online/maestro) is using a pseudo-version
(v0.0.0-20260202062555-48b47506a254); replace it with a stable tagged release
(e.g., vX.Y.Z) if one exists, or coordinate with the Maestro maintainers to
obtain a proper semver tag; if a tagged release is not available and you must
proceed, document the rationale in a comment and consider adding a go.mod
replace directive or using a forked module with a tag to provide stability —
update the go.mod entry for github.com/openshift-online/maestro accordingly and
run go mod tidy to ensure module graph consistency.

In `@internal/k8s_client/mock.go`:
- Around line 162-172: The switch over compareResult.Operation currently falls
through to silently returning nil, nil; add a default case that returns a clear
error instead of falling through. Update the switch in the function that calls
m.CreateResource/m.UpdateResource (referencing compareResult.Operation,
generation.OperationCreate, generation.OperationSkip,
generation.OperationUpdate, m.CreateResource, m.UpdateResource, existingObj) to
handle unexpected operation values by returning an error that includes the
unknown operation value so callers can detect and surface this bug.

In `@internal/maestro_client/operations_test.go`:
- Around line 84-93: The test case "negative generation" in operations_test.go
expects the wrong error text; ValidateGeneration in generation.go returns "must
be > 0" for non-positive generations. Update the test case's errorMsg from "must
be >= 0" to "must be > 0" and strengthen the assertion in the test (around the
error checks) to explicitly compare the returned error string to errorMsg so the
message mismatch will fail the test if it regresses; reference the "negative
generation" test entry and the ValidateGeneration function when making the
change.
🧹 Nitpick comments (7)
examples/maestro_client/1.manifestwork-prams-manifests.yaml (1)

18-18: Typo in the filename reference.

The filename manifestwork-prams-manifests.yaml appears to have a typo - should likely be manifestwork-params-manifests.yaml to match the intended "parameters" meaning. This also affects the actual filename of this file.

internal/generation/generation.go (1)

238-256: In-place sort mutates the caller's list.

GetLatestGenerationFromList uses sort.Slice which sorts the list.Items slice in-place. This is a side effect that may surprise callers who don't expect their input to be modified.

Consider either:

  1. Documenting this behavior explicitly in the function comment
  2. Creating a copy of the slice before sorting
Option 1: Document the side effect
 // GetLatestGenerationFromList returns the resource with the highest generation annotation from a list.
 // It sorts by generation annotation (descending) and uses metadata.name as a secondary sort key
 // for deterministic behavior when generations are equal.
 // Returns nil if the list is nil or empty.
 //
+// Note: This function sorts the list.Items slice in-place, modifying the input list.
+//
 // Useful for finding the most recent version of a resource when multiple versions exist.
 func GetLatestGenerationFromList(list *unstructured.UnstructuredList) *unstructured.Unstructured {
Option 2: Sort a copy to avoid mutation
 func GetLatestGenerationFromList(list *unstructured.UnstructuredList) *unstructured.Unstructured {
 	if list == nil || len(list.Items) == 0 {
 		return nil
 	}

+	// Create a copy to avoid mutating the input
+	items := make([]unstructured.Unstructured, len(list.Items))
+	copy(items, list.Items)
+
 	// Sort by generation annotation (descending) to return the one with the latest generation
 	// Secondary sort by metadata.name for consistency when generations are equal
-	sort.Slice(list.Items, func(i, j int) bool {
-		genI := GetGenerationFromUnstructured(&list.Items[i])
-		genJ := GetGenerationFromUnstructured(&list.Items[j])
+	sort.Slice(items, func(i, j int) bool {
+		genI := GetGenerationFromUnstructured(&items[i])
+		genJ := GetGenerationFromUnstructured(&items[j])
 		if genI != genJ {
 			return genI > genJ // Descending order - latest generation first
 		}
 		// Fall back to metadata.name for deterministic ordering when generations are equal
-		return list.Items[i].GetName() < list.Items[j].GetName()
+		return items[i].GetName() < items[j].GetName()
 	})

-	return &list.Items[0]
+	return &items[0]
 }
internal/maestro_client/operations_test.go (1)

14-112: Consider asserting on error message content.

The errorMsg field is defined in the test struct but only used in the error output when an expected error is nil. Consider adding assertions to verify the error message actually contains the expected substring, which would catch mismatches like the one above.

Example assertion
 		if tt.expectError {
 			if err == nil {
 				t.Errorf("expected error containing %q, got nil", tt.errorMsg)
 			}
+			if tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) {
+				t.Errorf("expected error containing %q, got %q", tt.errorMsg, err.Error())
+			}
 			return
 		}

Note: This would require adding "strings" to the imports.

internal/k8s_client/mock.go (1)

175-190: ApplyResources doesn't populate the Operation field in results.

The ApplyResourceResult struct has an Operation field (per the relevant code snippets), but this mock implementation only sets Resource or Error. Tests relying on operation tracking won't get accurate results.

♻️ Proposed fix to track operations
 func (m *MockK8sClient) ApplyResources(ctx context.Context, objs []*unstructured.Unstructured) ([]ApplyResourceResult, error) {
 	results := make([]ApplyResourceResult, 0, len(objs))

 	for _, obj := range objs {
-		resource, err := m.ApplyResource(ctx, obj)
-		if err != nil {
-			results = append(results, ApplyResourceResult{Error: err})
-			return results, err
+		// Determine operation for result tracking
+		gvk := obj.GroupVersionKind()
+		existingObj, getErr := m.GetResource(ctx, gvk, obj.GetNamespace(), obj.GetName())
+		exists := getErr == nil
+		
+		newGen := generation.GetGenerationFromUnstructured(obj)
+		var existingGen int64
+		if exists {
+			existingGen = generation.GetGenerationFromUnstructured(existingObj)
 		}
-		results = append(results, ApplyResourceResult{Resource: resource})
+		compareResult := generation.CompareGenerations(newGen, existingGen, exists)
+		
+		resource, err := m.ApplyResource(ctx, obj)
+		results = append(results, ApplyResourceResult{
+			Resource:  resource,
+			Operation: compareResult.Operation,
+			Error:     err,
+		})
+		if err != nil {
+			return results, err
+		}
 	}

 	return results, nil
 }
test/integration/maestro_client/setup_test.go (2)

134-146: Consider extracting duplicated PostgreSQL IP extraction logic.

The code for getting the PostgreSQL container IP from network settings (with host.docker.internal fallback) is duplicated in both runMaestroMigration and startMaestroServer. Extracting this to a helper function would reduce duplication.

♻️ Proposed helper function
+// getContainerIP extracts the IP address from a container's network settings
+// Falls back to host.docker.internal for Docker Desktop compatibility
+func getContainerIP(ctx context.Context, container testcontainers.Container) (string, error) {
+	inspect, err := container.Inspect(ctx)
+	if err != nil {
+		return "", fmt.Errorf("failed to inspect container: %w", err)
+	}
+	
+	for _, network := range inspect.NetworkSettings.Networks {
+		if network.IPAddress != "" {
+			return network.IPAddress, nil
+		}
+	}
+	
+	// Fallback to host.docker.internal for Docker Desktop
+	return "host.docker.internal", nil
+}

Then use it in both functions:

pgIP, err := getContainerIP(ctx, env.PostgresContainer)
if err != nil {
    return nil, err
}

Also applies to: 205-215


182-192: Log reading may be truncated for debugging failures.

The single Read call with a fixed 4096-byte buffer may not capture all migration logs on failure. Consider using io.ReadAll for complete log output when debugging.

♻️ Proposed fix for complete log reading
 	if state.ExitCode != 0 {
 		// Get logs for debugging
 		logs, _ := container.Logs(ctx)
 		if logs != nil {
 			defer logs.Close() //nolint:errcheck
-			buf := make([]byte, 4096)
-			n, _ := logs.Read(buf)
-			println(fmt.Sprintf("      Migration logs: %s", string(buf[:n])))
+			logBytes, _ := io.ReadAll(logs)
+			println(fmt.Sprintf("      Migration logs: %s", string(logBytes)))
 		}
 		return fmt.Errorf("migration failed with exit code %d", state.ExitCode)
 	}
test/integration/maestro_client/client_integration_test.go (1)

249-254: Unchecked type assertions could panic on unexpected structure.

The chained type assertions on configMapManifest could panic if the map structure differs from expected. While safe here since you control the structure, using checked assertions would be more defensive.

🛡️ Safer alternative using helper or checked assertions
 		// Now apply again with updated generation (should update)
 		work.Annotations[constants.AnnotationGeneration] = "2"
-		configMapManifest["metadata"].(map[string]interface{})["annotations"].(map[string]interface{})[constants.AnnotationGeneration] = "2"
-		configMapManifest["data"].(map[string]interface{})["key2"] = "value2"
+		if meta, ok := configMapManifest["metadata"].(map[string]interface{}); ok {
+			if ann, ok := meta["annotations"].(map[string]interface{}); ok {
+				ann[constants.AnnotationGeneration] = "2"
+			}
+		}
+		if data, ok := configMapManifest["data"].(map[string]interface{}); ok {
+			data["key2"] = "value2"
+		}
 		configMapJSON, _ = json.Marshal(configMapManifest)

Or simply rebuild the manifest from scratch for clarity.

Comment on lines +131 to +135
# - name: BROKER_TOPIC
# valueFrom:
# configMapKeyRef:
# name: hyperfleet-broker-config
# key: BROKER_TOPIC
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

Example deployment omits a required env var.
BROKER_TOPIC is labeled REQUIRED above, but the example deployment comments it out, which can lead to a broken copy‑paste deployment. Consider uncommenting it or marking it optional above.

✅ Suggested fix (uncomment in example)
-#         - name: BROKER_TOPIC
-#           valueFrom:
-#             configMapKeyRef:
-#               name: hyperfleet-broker-config
-#               key: BROKER_TOPIC
+         - name: BROKER_TOPIC
+           valueFrom:
+             configMapKeyRef:
+               name: hyperfleet-broker-config
+               key: BROKER_TOPIC
📝 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
# - name: BROKER_TOPIC
# valueFrom:
# configMapKeyRef:
# name: hyperfleet-broker-config
# key: BROKER_TOPIC
- name: BROKER_TOPIC
valueFrom:
configMapKeyRef:
name: hyperfleet-broker-config
key: BROKER_TOPIC
🤖 Prompt for AI Agents
In `@configs/broker-configmap-pubsub-template.yaml` around lines 131 - 135, The
example deployment comments out the required environment variable BROKER_TOPIC,
causing copy-paste deployments to fail; update the template so the BROKER_TOPIC
env var block (the configMapKeyRef referencing name: hyperfleet-broker-config
key: BROKER_TOPIC) is uncommented in the example, or alternatively change the
top-level label that marks BROKER_TOPIC as REQUIRED to OPTIONAL and document the
optionality—ensure the configMapKeyRef stanza for BROKER_TOPIC is present and
consistent with the documentation.

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

🤖 Fix all issues with AI agents
In `@configs/adapter-config-template.yaml`:
- Around line 36-37: The Kind rename is incomplete: update all occurrences of
the old AdapterConfig type/name to the new AdapterWorkflowConfig across code and
tests; specifically rename the Go struct type AdapterConfig in
internal/config_loader/types.go to AdapterWorkflowConfig (and update its
constructor/usage sites), update any JSON/YAML type/kind fields in testdata
files (e.g., adapter_config_valid.yaml and adapter-config-template.yaml) from
"AdapterConfig" to "AdapterWorkflowConfig", and update test code references in
internal/config_loader/loader_test.go and validator_test.go to use the new
struct/type name and Kind string so all symbol names (AdapterWorkflowConfig, any
constructors or methods previously named for AdapterConfig) match consistently.

In `@pkg/version/version.go`:
- Around line 26-33: The comment for UserAgent incorrectly references
"USER_AGENT" while the code checks EnvUserAgent (HYPERFLEET_USER_AGENT); update
the comment above func UserAgent() to mention HYPERFLEET_USER_AGENT (or
EnvUserAgent) so it matches the actual environment variable used, keeping the
rest of the description unchanged and referencing the UserAgent function and
EnvUserAgent constant to locate the fix.

In `@README.md`:
- Around line 155-167: Add a missing environment variable row for
HYPERFLEET_API_TOKEN to the Environment Variables table so the README documents
that API auth token; update the table near the existing entries like
ADAPTER_CONFIG_PATH and HYPERFLEET_API_BASE_URL to include
`HYPERFLEET_API_TOKEN` with a short description such as "API authentication
token for HyperFleet" and mark its default as "(required)" or provide the actual
default if applicable; ensure the new row follows the same column ordering and
formatting as the other rows (Variable | Description | Default).

In `@test/integration/config-loader/config_criteria_integration_test.go`:
- Around line 50-51: The test currently sets ctx.Set("clusterPhase", "Ready")
but the "Ready should fail" case now contradicts the updated allowed phases;
update the failing-preconditions test case so it uses a phase outside the
allowed list (for example change the test input that sets clusterPhase from
"Ready" to "Terminating") or alternatively adjust the expected outcome for the
"Ready should fail" case to expect success; locate the test case in
test/integration/config-loader/config_criteria_integration_test.go where
ctx.Set("clusterPhase", ...) and the "Ready should fail" assertion occur and
make the corresponding value or expectation change.
🧹 Nitpick comments (6)
internal/hyperfleet_api/client.go (1)

324-325: Don’t clobber an explicit User-Agent header.
Right now this overwrites any per-request or default User-Agent the caller set. Consider only setting it when absent.

♻️ Suggested adjustment
-// Set User-Agent header
-httpReq.Header.Set("User-Agent", version.UserAgent())
+// Set User-Agent header (respect explicit caller override)
+if httpReq.Header.Get("User-Agent") == "" {
+	httpReq.Header.Set("User-Agent", version.UserAgent())
+}
internal/maestro_client/operations_test.go (2)

15-117: Consider consolidating duplicate test coverage.

This TestValidateGeneration test duplicates coverage already present in internal/generation/generation_test.go (Lines 215-317). Both test the same generation.ValidateGeneration function with similar test cases. Consider whether this duplication is intentional (testing from consumer perspective) or if it should be consolidated.


119-193: Duplicate test coverage noted.

This test also duplicates TestValidateGenerationFromUnstructured in internal/generation/generation_test.go (Lines 319-442). Same consolidation consideration applies.

test/integration/maestro_client/client_integration_test.go (1)

164-236: Type assertions could panic if manifest structure changes.

Lines 223-224 use chained type assertions that will panic if the manifest structure doesn't match expectations:

configMapManifest["metadata"].(map[string]interface{})["annotations"].(map[string]interface{})[...]

While this is test code with controlled data, consider using safer access or adding a guard comment.

Safer alternative using helper or explicit checks
// Option 1: Use a helper function
func setNestedAnnotation(manifest map[string]interface{}, key, value string) {
    metadata := manifest["metadata"].(map[string]interface{})
    annotations := metadata["annotations"].(map[string]interface{})
    annotations[key] = value
}

// Option 2: Add comment acknowledging the risk
// Safe: manifest structure is defined above in this test
configMapManifest["metadata"].(map[string]interface{})["annotations"].(map[string]interface{})[constants.AnnotationGeneration] = "2"
test/integration/maestro_client/setup_test.go (1)

213-221: Consider increasing log buffer size for debugging.

The 4096-byte buffer may truncate migration error logs, making debugging difficult. Consider reading the full output or using a larger buffer.

♻️ Proposed fix to read full logs
 		// Get logs for debugging
 		logs, _ := container.Logs(ctx)
 		if logs != nil {
 			defer logs.Close() //nolint:errcheck
-			buf := make([]byte, 4096)
-			n, _ := logs.Read(buf)
-			println(fmt.Sprintf("      Migration logs: %s", string(buf[:n])))
+			logBytes, _ := io.ReadAll(logs)
+			println(fmt.Sprintf("      Migration logs: %s", string(logBytes)))
 		}

Note: You'll need to add "io" to the imports.

internal/generation/generation.go (1)

235-259: Consider documenting or avoiding the in-place sort side effect.

sort.Slice modifies list.Items in-place. Callers may not expect their list to be reordered after calling this function. Consider either documenting this behavior or making a copy before sorting.

♻️ Option 1: Document the side effect
 // GetLatestGenerationFromList returns the resource with the highest generation annotation from a list.
 // It sorts by generation annotation (descending) and uses metadata.name as a secondary sort key
 // for deterministic behavior when generations are equal.
 // Returns nil if the list is nil or empty.
 //
+// Note: This function sorts list.Items in-place. Callers should be aware that
+// the order of items in the list will be modified.
+//
 // Useful for finding the most recent version of a resource when multiple versions exist.
♻️ Option 2: Avoid modifying input by copying
 func GetLatestGenerationFromList(list *unstructured.UnstructuredList) *unstructured.Unstructured {
 	if list == nil || len(list.Items) == 0 {
 		return nil
 	}

-	// Sort by generation annotation (descending) to return the one with the latest generation
-	// Secondary sort by metadata.name for consistency when generations are equal
-	sort.Slice(list.Items, func(i, j int) bool {
+	// Copy items to avoid modifying input
+	items := make([]unstructured.Unstructured, len(list.Items))
+	copy(items, list.Items)
+
+	// Sort by generation annotation (descending)
+	sort.Slice(items, func(i, j int) bool {
-		genI := GetGenerationFromUnstructured(&list.Items[i])
-		genJ := GetGenerationFromUnstructured(&list.Items[j])
+		genI := GetGenerationFromUnstructured(&items[i])
+		genJ := GetGenerationFromUnstructured(&items[j])
 		if genI != genJ {
 			return genI > genJ
 		}
-		return list.Items[i].GetName() < list.Items[j].GetName()
+		return items[i].GetName() < items[j].GetName()
 	})

-	return &list.Items[0]
+	return &items[0]
 }

Comment on lines +155 to +177
### Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `ADAPTER_CONFIG_PATH` | Path to adapter configuration file | `/etc/adapter/config/adapter-deployment-config.yaml` |
| `HYPERFLEET_USER_AGENT` | Custom User-Agent string for HTTP clients (Maestro, HyperFleet API) | `hyperfleet-adapter/{version}` |
| `HYPERFLEET_API_BASE_URL` | Base URL for HyperFleet API | (from config) |
| `HYPERFLEET_API_VERSION` | API version for HyperFleet API | (from config) |
| `BROKER_SUBSCRIPTION_ID` | Message broker subscription ID | (required) |
| `BROKER_TOPIC` | Message broker topic | (required) |
| `LOG_LEVEL` | Log level (debug, info, warn, error) | `info` |
| `LOG_FORMAT` | Log format (text, json) | `json` |
| `LOG_OUTPUT` | Log output (stdout, stderr) | `stdout` |
Copy link

@coderabbitai coderabbitai bot Feb 4, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Document HYPERFLEET_API_TOKEN here if the template requires it.
Integration tests now set this variable; if it’s required for API auth, the env var table should include it (with default/required status).

📌 Suggested doc update
 | `HYPERFLEET_API_VERSION` | API version for HyperFleet API | (from config) |
+| `HYPERFLEET_API_TOKEN` | API token for HyperFleet API authentication | (required) |
📝 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
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `ADAPTER_CONFIG_PATH` | Path to adapter configuration file | `/etc/adapter/config/adapter-deployment-config.yaml` |
| `HYPERFLEET_USER_AGENT` | Custom User-Agent string for HTTP clients (Maestro, HyperFleet API) | `hyperfleet-adapter/{version}` |
| `HYPERFLEET_API_BASE_URL` | Base URL for HyperFleet API | (from config) |
| `HYPERFLEET_API_VERSION` | API version for HyperFleet API | (from config) |
| `BROKER_SUBSCRIPTION_ID` | Message broker subscription ID | (required) |
| `BROKER_TOPIC` | Message broker topic | (required) |
| `LOG_LEVEL` | Log level (debug, info, warn, error) | `info` |
| `LOG_FORMAT` | Log format (text, json) | `json` |
| `LOG_OUTPUT` | Log output (stdout, stderr) | `stdout` |
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `ADAPTER_CONFIG_PATH` | Path to adapter configuration file | `/etc/adapter/config/adapter-deployment-config.yaml` |
| `HYPERFLEET_USER_AGENT` | Custom User-Agent string for HTTP clients (Maestro, HyperFleet API) | `hyperfleet-adapter/{version}` |
| `HYPERFLEET_API_BASE_URL` | Base URL for HyperFleet API | (from config) |
| `HYPERFLEET_API_VERSION` | API version for HyperFleet API | (from config) |
| `HYPERFLEET_API_TOKEN` | API token for HyperFleet API authentication | (required) |
| `BROKER_SUBSCRIPTION_ID` | Message broker subscription ID | (required) |
| `BROKER_TOPIC` | Message broker topic | (required) |
| `LOG_LEVEL` | Log level (debug, info, warn, error) | `info` |
| `LOG_FORMAT` | Log format (text, json) | `json` |
| `LOG_OUTPUT` | Log output (stdout, stderr) | `stdout` |
🤖 Prompt for AI Agents
In `@README.md` around lines 155 - 167, Add a missing environment variable row for
HYPERFLEET_API_TOKEN to the Environment Variables table so the README documents
that API auth token; update the table near the existing entries like
ADAPTER_CONFIG_PATH and HYPERFLEET_API_BASE_URL to include
`HYPERFLEET_API_TOKEN` with a short description such as "API authentication
token for HyperFleet" and mark its default as "(required)" or provide the actual
default if applicable; ensure the new row follows the same column ordering and
formatting as the other rows (Variable | Description | Default).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No need it.

Copy link

Choose a reason for hiding this comment

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

@xueli181114, understood! I'll skip that suggestion.


🧠 Learnings used
Learnt from: xueli181114
Repo: openshift-hyperfleet/hyperfleet-adapter PR: 17
File: internal/config_loader/validator_schema.go:126-133
Timestamp: 2025-12-11T02:06:33.975Z
Learning: In the hyperfleet-adapter repository (Go), environment variable validation for required parameters (those with `env.*` source, `Required=true`, and no default) occurs at config load time in `internal/config_loader/validator_schema.go`. This is intentional design - environment variables must be set before deployment, and the validation ensures fail-fast behavior with clear error messages.

@rh-amarin
Copy link
Contributor

/lgtm

@openshift-ci
Copy link

openshift-ci bot commented Feb 4, 2026

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: rh-amarin

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

The pull request process is described 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

@openshift-ci openshift-ci bot added the approved label Feb 4, 2026
@rh-amarin
Copy link
Contributor

/retest

@rh-amarin
Copy link
Contributor

Claude suggests adding in internal/maestro_client/client.go

   return &http.Transport{
      194        TLSClientConfig: &tls.Config{
      195          MinVersion:         tls.VersionTLS12,
      196 -        InsecureSkipVerify: true,
      196 +        InsecureSkipVerify: true, //nolint:gosec // G402: Intentional for local testing when Insecure=true
      197        },
      198      }, nil
      199    }

Would we like to configure this setting by some external config?

@openshift-ci openshift-ci bot removed the lgtm label Feb 5, 2026
@openshift-ci
Copy link

openshift-ci bot commented Feb 5, 2026

New changes are detected. LGTM label has been removed.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
go.mod (1)

1-171: ⚠️ Potential issue | 🔴 Critical

Resolve unresolved merge conflicts blocking build validation.

Multiple unresolved merge conflicts must be resolved before the go.mod changes can be validated. The following files contain conflict markers that prevent compilation:

  • test/integration/config-loader/loader_template_test.go: lines 46-50 and 100-106
  • test/integration/config-loader/config_criteria_integration_test.go: lines 35-39 and 54-60
Example conflict in loader_template_test.go (lines 46-50)
<<<<<<< HEAD
	configPath := filepath.Join(projectRoot, "configs/adapterconfig-template.yaml")
=======
	configPath := filepath.Join(projectRoot, "test/integration/config-loader/testdata/adapter-config-template.yaml")
>>>>>>> 1e51a34 (fix: Moved version to a package version and fixed maestro integration running failure)

Choose the correct path for each conflict location based on your intended file structure, then re-run validation to ensure the dependency changes are compatible and the module builds successfully.

🤖 Fix all issues with AI agents
In `@configs/adapter-deployment-config.yaml`:
- Around line 8-12: The header comment in configs/adapter-deployment-config.yaml
incorrectly states the file is packaged with the container image; update the
comment to clarify that this file is a sample/default and may not be included in
the image unless an explicit build step adds it, and that in production the
configuration should be provided via the HYPERFLEET_ADAPTER_DEPLOYMENT_CONFIG
environment variable (highest priority) or a ConfigMap mounted at
/etc/adapter/adapter-deployment-config.yaml; if this repository does include a
build step that packages the file, mention that build artifact (or Dockerfile
step) explicitly in the comment so the delivery mechanism is unambiguous.

In `@Dockerfile`:
- Around line 31-48: The Dockerfile contains unresolved Git merge conflict
markers (<<<<<<<, =======, >>>>>>>) around the adapter config comment block;
remove the conflict markers and retain the intended comment text about mounting
config via ConfigMap and setting ADAPTER_CONFIG_PATH, ensuring only one clean
commented block remains and no stray conflict lines exist so the Dockerfile is
valid.

In `@internal/maestro_client/ocm_logger_adapter.go`:
- Around line 77-83: The comment for ocmLoggerAdapter.Fatal is misleading about
exit behavior; update the doc for the Fatal method to state explicitly that this
adapter intentionally logs at error level (using a.log.Errorf in Fatal) and does
NOT call the underlying logger's exit behavior so the adapter process won't
terminate on Maestro client failures; reference the ocmLoggerAdapter type and
its Fatal method and replace the existing comment lines to clearly reflect this
intentional non-exit behavior.

In `@test/integration/config-loader/config_criteria_integration_test.go`:
- Around line 35-39: The file has unresolved Git conflict markers in
getConfigPath and TestConfigLoadAndCriteriaEvaluation; remove the markers and
choose the intended branch content (use the updated test path under
test/integration/config-loader/testdata — e.g., getConfigPath should return
filepath.Join(getProjectRoot(),
"test/integration/config-loader/testdata/adapter-config-template.yaml") and any
references in TestConfigLoadAndCriteriaEvaluation should use the same testdata
path), then ensure the code compiles by deleting the <<<<<<<, =======, and
>>>>>>> lines so the Go file contains only valid code.
- Around line 54-60: Resolve the merge conflicts by making the tests
consistently use the template's readyConditionStatus: update getConfigPath() to
return the test template (adapter-config-template.yaml) used by these
integration tests, replace any ctx.Set("clusterPhase", ...) with
ctx.Set("readyConditionStatus", "True") in config_criteria_integration_test.go,
and modify loader_template_test.go assertions to expect a condition named
"readyConditionStatus" with operator "equals" and value "True" (remove/replace
any assertions checking clusterPhase or a different operator/value).

In `@test/integration/config-loader/loader_template_test.go`:
- Around line 46-50: Remove the unresolved merge conflict markers and pick the
correct config path: eliminate the lines starting with <<<<<<<, =======, and
>>>>>>> so the assignment to configPath becomes a single filepath.Join call;
replace the conflicting path with the intended value (use configPath :=
filepath.Join(projectRoot,
"test/integration/config-loader/testdata/adapter-config-template.yaml") or the
projectRoot/ "configs/adapterconfig-template.yaml" variant as decided) and
ensure only one filepath.Join assignment to the configPath variable remains in
loader_template_test.go.
- Around line 93-95: The test currently expects the clusterName capture to
reference "metadata.name" (Kubernetes style) but the HyperFleet API uses a
root-level "name" field; update the assertion in the test that checks
clusterNameCapture.Field (located where findCaptureByName(firstPrecond.Capture,
"clusterName") and clusterNameCapture are used) to assert equality with "name"
instead of "metadata.name" so the precondition capture matches the HyperFleet
API response schema.
- Around line 100-106: Remove the git conflict markers and fix the two
assertions in loader_template_test.go so they assert the intended values for
firstCondition: change the conflicting lines to assert.Equal(t, "clusterPhase",
firstCondition.Field) and assert.Equal(t, "in", firstCondition.Operator), and
delete the <<<<<<<, =======, and >>>>>>> markers so the test compiles and
reflects the intended precondition checks.
🧹 Nitpick comments (6)
pkg/errors/error_test.go (1)

74-102: Cover new error codes explicitly in requiredCodes.

expectedCount is now 17, but requiredCodes still omits ErrorMaestroError and ErrorConfigurationError, so missing mappings could slip by. Consider adding them (and optionally extending the other tables) to keep coverage aligned.

♻️ Proposed update
 requiredCodes := []ServiceErrorCode{
 	ErrorNotFound,
 	ErrorValidation,
 	ErrorConflict,
 	ErrorForbidden,
 	ErrorUnauthorized,
 	ErrorUnauthenticated,
 	ErrorBadRequest,
 	ErrorMalformedRequest,
 	ErrorNotImplemented,
 	ErrorGeneral,
 	ErrorAdapterConfigNotFound,
 	ErrorBrokerConnectionError,
 	ErrorKubernetesError,
 	ErrorHyperFleetAPIError,
 	ErrorInvalidCloudEvent,
+	ErrorMaestroError,
+	ErrorConfigurationError,
 }
internal/maestro_client/operations_test.go (1)

195-333: Use errorMsg in ManifestWork validation assertions.

The table includes errorMsg but the test doesn’t assert it, so message regressions won’t be caught.

♻️ Suggested assertion
 		if tt.expectError {
 			if err == nil {
 				t.Errorf("expected error containing %q, got nil", tt.errorMsg)
 				return
 			}
+			if tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) {
+				t.Errorf("expected error containing %q, got %q", tt.errorMsg, err.Error())
+			}
 			return
 		}
test/integration/maestro_client/client_integration_test.go (2)

221-226: Marshal error silently discarded.

Line 225 ignores the error from json.Marshal. While unlikely to fail for a simple map, this could mask issues during test debugging.

🔧 Proposed fix
-		configMapJSON, _ = json.Marshal(configMapManifest)
+		configMapJSON, err = json.Marshal(configMapManifest)
+		require.NoError(t, err, "failed to marshal updated configmap")

286-289: Consider using t.Fatal instead of t.Skip for first apply failure.

If the first apply fails due to a consumer not being registered, the test skips rather than fails. However, per the test setup in setup_test.go, consumers should be pre-registered. A failure here might indicate a real setup problem that should surface as a test failure.

internal/generation/generation.go (1)

241-259: Sort modifies the input list in place.

sort.Slice sorts the slice in place, which mutates list.Items. This could cause unexpected side effects if the caller expects the list to remain unchanged after calling GetLatestGenerationFromList.

🔧 Proposed fix to avoid mutating input
 func GetLatestGenerationFromList(list *unstructured.UnstructuredList) *unstructured.Unstructured {
 	if list == nil || len(list.Items) == 0 {
 		return nil
 	}

+	// Make a copy to avoid mutating the original list
+	items := make([]unstructured.Unstructured, len(list.Items))
+	copy(items, list.Items)
+
 	// Sort by generation annotation (descending) to return the one with the latest generation
 	// Secondary sort by metadata.name for consistency when generations are equal
-	sort.Slice(list.Items, func(i, j int) bool {
-		genI := GetGenerationFromUnstructured(&list.Items[i])
-		genJ := GetGenerationFromUnstructured(&list.Items[j])
+	sort.Slice(items, func(i, j int) bool {
+		genI := GetGenerationFromUnstructured(&items[i])
+		genJ := GetGenerationFromUnstructured(&items[j])
 		if genI != genJ {
 			return genI > genJ // Descending order - latest generation first
 		}
 		// Fall back to metadata.name for deterministic ordering when generations are equal
-		return list.Items[i].GetName() < list.Items[j].GetName()
+		return items[i].GetName() < items[j].GetName()
 	})

-	return &list.Items[0]
+	return &items[0]
 }
test/integration/maestro_client/setup_test.go (1)

170-186: Database password embedded in shell script.

The password is embedded directly in the shell script passed to the entrypoint. While this is acceptable for local testing, be aware that:

  1. The password may appear in container logs if verbose logging is enabled
  2. Container inspection (docker inspect) would reveal the entrypoint with credentials

For integration tests with hardcoded test credentials, this is a minor concern but worth noting.

Comment on lines +77 to +83
// Fatal logs at error level with formatting.
// Note: Does not exit - the underlying logger handles that behavior.
func (a *ocmLoggerAdapter) Fatal(ctx context.Context, format string, args ...interface{}) {
if ctx == nil {
ctx = context.Background()
}
a.log.Errorf(ctx, "FATAL: "+format, args...)
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

Clarify the Fatal comment to reflect intentional non-exit.

The comment currently implies the underlying logger handles exiting, but this adapter intentionally avoids exit; the doc should match the intended behavior.

✏️ Suggested fix
-// Note: Does not exit - the underlying logger handles that behavior.
+// Note: intentionally does not exit to avoid terminating the adapter on Maestro client failures.

Based on learnings: In the hyperfleet-adapter repository, the ocmLoggerAdapter's Fatal method intentionally calls a.log.Errorf instead of a.log.Fatal to prevent Maestro client failures from causing the adapter process to exit unexpectedly. Maestro failures should be reflected as runtime errors, not fatal exits.

🤖 Prompt for AI Agents
In `@internal/maestro_client/ocm_logger_adapter.go` around lines 77 - 83, The
comment for ocmLoggerAdapter.Fatal is misleading about exit behavior; update the
doc for the Fatal method to state explicitly that this adapter intentionally
logs at error level (using a.log.Errorf in Fatal) and does NOT call the
underlying logger's exit behavior so the adapter process won't terminate on
Maestro client failures; reference the ocmLoggerAdapter type and its Fatal
method and replace the existing comment lines to clearly reflect this
intentional non-exit behavior.

@xueli181114
Copy link
Contributor Author

Claude suggests adding in internal/maestro_client/client.go

   return &http.Transport{
      194        TLSClientConfig: &tls.Config{
      195          MinVersion:         tls.VersionTLS12,
      196 -        InsecureSkipVerify: true,
      196 +        InsecureSkipVerify: true, //nolint:gosec // G402: Intentional for local testing when Insecure=true
      197        },
      198      }, nil
      199    }

Would we like to configure this setting by some external config?

I fixed this lint issue. How about wait for customer feedback? I make Insecure also handle InsecureSkipVerify. But we may get requirements from service offering team. Till what I know ARO is using token for verify instead of TLS not sure about ROSA and GCP.

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

🤖 Fix all issues with AI agents
In `@internal/maestro_client/client.go`:
- Around line 341-348: The readTokenFile function should fail fast when the file
is empty or contains only whitespace: after reading and trimming the token in
readTokenFile(path string), validate that the resulting string is non-empty and
return a descriptive error (e.g., "empty token file" or "token file contains
only whitespace") instead of returning an empty token; update callers to handle
that error as needed so authentication won't proceed with an empty token.
- Around line 101-109: Validate config.MaestroServerAddr to ensure it uses https
when the client is not allowed to be insecure: in the client initialization
(check around config.MaestroServerAddr validation) parse the URL and if
config.Insecure is false and the scheme is "http" (or missing/invalid but not
"https") return an apperrors.ConfigurationError explaining that
MaestroServerAddr must be an https URL unless Insecure is true; otherwise allow
http only when config.Insecure == true. Use the existing
config.MaestroServerAddr and config.Insecure fields to locate and enforce this
check.
- Around line 189-229: The current createHTTPTransport builds a bare
http.Transport and loses important defaults (ProxyFromEnvironment, DialContext,
ForceAttemptHTTP2, pooling and timeouts); instead, clone the default transport
and only override TLS settings: get a clone via
http.DefaultTransport.(*http.Transport).Clone(), set its TLSClientConfig to the
tls.Config you construct (respecting config.Insecure to set InsecureSkipVerify
when needed), and keep the code that loads HTTPCAFile/CAFile and appends to
tls.Config.RootCAs; return the cloned transport so ProxyFromEnvironment,
DialContext, MaxIdleConns, IdleConnTimeout, TLSHandshakeTimeout, etc. are
preserved.

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

🤖 Fix all issues with AI agents
In `@test/integration/maestro_client/client_integration_test.go`:
- Around line 129-138: The test currently treats any error from
tc.Client.CreateManifestWork as acceptable; change it to only accept the
specific "consumer not registered" (or equivalent Maestro-specific) error and
fail for all other errors: after calling CreateManifestWork (created, err :=
tc.Client.CreateManifestWork(tc.Ctx, consumerName, work)), inspect err and if
nil continue as success, if err matches the Maestro consumer-not-registered
sentinel/type/message (e.g., check for a specific error type, error.Is(err,
maestro.ErrConsumerNotRegistered) or strings.Contains(err.Error(), "consumer not
registered") or an expected HTTP status), log that as the expected path and
return, otherwise call t.Fatalf or t.Fatalf-like assertion to fail the test with
the unexpected error; update the test code around the CreateManifestWork call
accordingly so only the known consumer-not-registered error is allowed.
🧹 Nitpick comments (2)
test/integration/maestro_client/setup_test.go (2)

24-26: Consider adding an overall timeout for test environment setup.

The function creates a context.Background() without a timeout. While individual container waits have timeouts, an overall timeout would prevent the test from hanging indefinitely if something goes wrong during setup.

♻️ Suggested improvement
 func setupMaestroTestEnv() (*MaestroTestEnv, error) {
-	ctx := context.Background()
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+	defer cancel()
 	env := &MaestroTestEnv{}

171-187: Consider extracting the common shell script setup to reduce duplication.

The database secrets file creation logic is duplicated between runMaestroMigration (lines 171-187) and startMaestroServer (lines 235-257). Both generate identical /secrets/db.* files.

♻️ Suggested helper function
// buildMaestroSetupScript creates a shell script that sets up database secrets
// and executes the provided Maestro command with common DB flags.
func buildMaestroSetupScript(pgIP, dbUser, dbPassword, dbName, maestroCmd string) string {
	return fmt.Sprintf(`#!/bin/sh
mkdir -p /secrets
echo -n '%s' > /secrets/db.host
echo -n '5432' > /secrets/db.port
echo -n '%s' > /secrets/db.user
echo -n '%s' > /secrets/db.password
echo -n '%s' > /secrets/db.name
exec /usr/local/bin/maestro %s \
  --db-host-file=/secrets/db.host \
  --db-port-file=/secrets/db.port \
  --db-user-file=/secrets/db.user \
  --db-password-file=/secrets/db.password \
  --db-name-file=/secrets/db.name \
  --db-sslmode=disable \
  --alsologtostderr \
  -v=2
`, pgIP, dbUser, dbPassword, dbName, maestroCmd)
}

Then use it in both functions:

// In runMaestroMigration:
setupScript := buildMaestroSetupScript(pgIP, dbUser, dbPassword, dbName, "migration")

// In startMaestroServer:
serverCmd := `server \
  --server-hostname=0.0.0.0 \
  --enable-grpc-server=true \
  --grpc-server-bindport=8090 \
  --http-server-bindport=8000 \
  --health-check-server-bindport=8083 \
  --message-broker-type=grpc`
setupScript := buildMaestroSetupScript(pgIP, dbUser, dbPassword, dbName, serverCmd)

Also applies to: 235-257

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

🤖 Fix all issues with AI agents
In `@internal/maestro_client/client.go`:
- Around line 250-253: The gRPC branch currently sets
grpcOptions.Dialer.TLSConfig = nil when config.Insecure is true (forcing
plaintext), which mismatches the HTTP path's InsecureSkipVerify behavior; change
the gRPC handling in the client initialization (the block referencing
config.Insecure and grpcOptions.Dialer.TLSConfig) to create a tls.Config with
InsecureSkipVerify=true when Insecure is set (unless a CAFile is provided) so
self-signed certs work the same as HTTP, e.g. set grpcOptions.Dialer.TLSConfig =
&tls.Config{InsecureSkipVerify: true} (and continue to honor config.CAFile when
present), or alternatively update the docstring for config.Insecure to state
that for gRPC it currently enables plaintext only and CAFile must be used for
self-signed certs—implement the first option to preserve parity with the HTTP
path.
- Around line 106-116: The current validation allows schemeless or non-HTTP
schemes when config.Insecure is true; update the validation in the
MaestroServerAddr handling (around url.Parse, serverURL.Scheme and
config.Insecure) to always require serverURL.Scheme be either "http" or "https"
and return an apperrors.ConfigurationError if not, then additionally enforce
that serverURL.Scheme == "https" when config.Insecure == false (with a clear
error message referencing MaestroServerAddr and the received scheme). Ensure you
adjust the error messages returned from this block so they mention the invalid
scheme and the expected "http/https" values and still use
apperrors.ConfigurationError for consistency.

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

🤖 Fix all issues with AI agents
In `@test/integration/config-loader/loader_template_test.go`:
- Around line 41-44: TestLoadValidTestConfig is missing the required
HYPERFLEET_API_TOKEN env var causing validation to fail; before calling
config_loader.Load() in the TestLoadValidTestConfig test, add
t.Setenv("HYPERFLEET_API_TOKEN", "test-token-for-integration-tests") alongside
the existing t.Setenv("HYPERFLEET_API_BASE_URL", ...) so the
env.HYPERFLEET_API_TOKEN required field is present during config_loader.Load().
🧹 Nitpick comments (3)
configs/adapter-deployment-config.yaml (2)

34-37: Consider annotating the sample version to avoid staleness.

A short note helps future updates keep this aligned with releases.

📝 Suggested tweak
   adapter:
+    # Update to match the released adapter version to avoid staleness in samples.
     version: "0.1.0"

95-105: Optional: quote scalar strings for consistency with the rest of the file.

Purely stylistic, but it keeps duration/backoff values visually consistent.

📝 Suggested tweak
     hyperfleetApi:
-      timeout: 2s
+      timeout: "2s"
       retryAttempts: 3
-      retryBackoff: exponential
+      retryBackoff: "exponential"
internal/maestro_client/operations_test.go (1)

125-169: Prefer exercising generation.CompareGenerations to avoid test drift.

This test reimplements decision logic; using the helper keeps behavior aligned as logic evolves.

♻️ Proposed refactor
-		// Logic from ApplyManifestWork:
-		// if existingGeneration == generation { return existing }
-		shouldSkipUpdate := tt.existingGeneration == tt.newGeneration
-		shouldUpdate := !shouldSkipUpdate
+		decision := generation.CompareGenerations(tt.newGeneration, tt.existingGeneration, true)
+		shouldUpdate := decision.Operation != generation.OperationSkip

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants