From b236c7674ef25dbf87a49e4953e645db186cb4aa Mon Sep 17 00:00:00 2001 From: Rafael Benevides Date: Wed, 18 Mar 2026 23:33:59 -0300 Subject: [PATCH 1/3] HYPERFLEET-769 - fix: align golangci.yml with architecture standard Align .golangci.yml with the architecture standard by removing all repo-specific exclusions and fixing the underlying code: - Remove misspell, gocritic, gosec, and revive exclusions - Fix all lint violations: filepath.Clean, crypto/rand, line length, if/else->switch, exitAfterDefer, var-naming, errcheck, goconst - Rename underscore packages to comply with Go naming conventions: config_loader->configloader, hyperfleet_api->hyperfleetapi, k8s_client->k8sclient, maestro_client->maestroclient, transport_client->transportclient - Rename integration test packages to remove underscores - Fix k8s_client integration tests failing in -short mode - Reject nested discovery key collisions instead of silently overwriting - Use context-aware exec.CommandContext with timeouts for cleanup commands - Add forceCleanupContainerNoTest fallback on container.Terminate failure - Check RegisterValidation errors at startup (fail fast) - Add clusterID assertion in executor K8s integration test - Replace chained type assertions with checked extractions in maestro test - Use errors.Is(readErr, io.EOF) instead of string comparison - Add YAML fallback in MockK8sClient.ApplyResource - Fix DiscoverResources example to match 4-param signature - Remove unused apiBaseURL param from createK8sTestConfig Only remaining exclusions: pkg/errors and pkg/utils naming (renaming would break the public API surface). --- .golangci.yml | 64 +--- README.md | 2 +- cmd/adapter/main.go | 156 +++++--- docs/runbook.md | 4 +- .../{config_loader => configloader}/README.md | 4 +- .../accessors.go | 2 +- .../constants.go | 2 +- .../{config_loader => configloader}/loader.go | 5 +- .../loader_test.go | 74 +--- .../struct_validator.go | 40 +- .../{config_loader => configloader}/types.go | 42 ++- .../validator.go | 5 +- .../validator_test.go | 67 +++- .../viper_loader.go | 31 +- internal/criteria/README.md | 6 +- internal/criteria/evaluator_test.go | 3 +- internal/dryrun/discovery_overrides.go | 3 +- internal/dryrun/dryrun_api_client.go | 38 +- internal/dryrun/dryrun_api_client_test.go | 26 +- internal/dryrun/dryrun_responses.go | 3 +- internal/dryrun/event_loader.go | 3 +- internal/dryrun/event_loader_test.go | 3 +- internal/dryrun/recording_transport_client.go | 32 +- .../dryrun/recording_transport_client_test.go | 4 +- internal/dryrun/trace.go | 9 +- internal/executor/executor.go | 23 +- internal/executor/executor_test.go | 193 +++++----- internal/executor/param_extractor.go | 18 +- internal/executor/post_action_executor.go | 64 +++- .../executor/post_action_executor_test.go | 112 +++--- internal/executor/precondition_executor.go | 31 +- internal/executor/resource_executor.go | 68 +++- internal/executor/resource_executor_test.go | 51 +-- internal/executor/types.go | 39 +- internal/executor/utils.go | 41 +- internal/executor/utils_test.go | 162 ++++---- internal/generation/generation.go | 25 +- .../README.md | 2 +- .../client.go | 32 +- .../client_test.go | 26 +- .../{hyperfleet_api => hyperfleetapi}/mock.go | 2 +- .../types.go | 2 +- internal/{k8s_client => k8sclient}/README.md | 6 +- internal/{k8s_client => k8sclient}/apply.go | 20 +- internal/{k8s_client => k8sclient}/client.go | 39 +- .../{k8s_client => k8sclient}/client_test.go | 2 +- .../{k8s_client => k8sclient}/discovery.go | 17 +- .../{k8s_client => k8sclient}/interface.go | 28 +- internal/{k8s_client => k8sclient}/mock.go | 66 +++- .../test_helpers_test.go | 52 ++- internal/{k8s_client => k8sclient}/types.go | 2 +- .../{k8s_client => k8sclient}/types_test.go | 2 +- .../client.go | 86 +++-- .../client_test.go | 2 +- .../interface.go | 33 +- .../ocm_logger_adapter.go | 2 +- .../operations.go | 4 +- .../operations_test.go | 12 +- internal/manifest/generation.go | 75 ++-- internal/manifest/manifestwork.go | 3 +- .../interface.go | 37 +- .../types.go | 8 +- pkg/errors/api_error.go | 10 +- pkg/errors/error.go | 90 ++++- pkg/errors/error_test.go | 3 +- pkg/logger/with_error_field_test.go | 95 +++-- pkg/otel/tracer.go | 19 +- test/integration/README.md | 6 +- .../config_criteria_integration_test.go | 14 +- .../config-loader/loader_template_test.go | 34 +- .../executor/executor_integration_test.go | 349 +++++++++--------- .../executor/executor_k8s_integration_test.go | 269 +++++++------- test/integration/executor/main_test.go | 9 +- test/integration/executor/setup_test.go | 6 +- .../{k8s_client => k8sclient}/README.md | 18 +- .../client_integration_test.go | 8 +- .../helper_envtest_prebuilt.go | 41 +- .../helper_selector.go | 8 +- .../{k8s_client => k8sclient}/main_test.go | 8 +- .../client_integration_test.go | 49 ++- .../client_tls_config_integration_test.go | 103 +++--- .../client_tls_integration_test.go | 26 +- .../main_test.go | 20 +- .../setup_test.go | 2 +- .../tls_helper_test.go | 2 +- test/integration/testutil/container.go | 141 +++++-- test/integration/testutil/mock_api_server.go | 33 +- 87 files changed, 2020 insertions(+), 1358 deletions(-) rename internal/{config_loader => configloader}/README.md (99%) rename internal/{config_loader => configloader}/accessors.go (99%) rename internal/{config_loader => configloader}/constants.go (99%) rename internal/{config_loader => configloader}/loader.go (98%) rename internal/{config_loader => configloader}/loader_test.go (97%) rename internal/{config_loader => configloader}/struct_validator.go (88%) rename internal/{config_loader => configloader}/types.go (93%) rename internal/{config_loader => configloader}/validator.go (99%) rename internal/{config_loader => configloader}/validator_test.go (93%) rename internal/{config_loader => configloader}/viper_loader.go (93%) rename internal/{hyperfleet_api => hyperfleetapi}/README.md (99%) rename internal/{hyperfleet_api => hyperfleetapi}/client.go (93%) rename internal/{hyperfleet_api => hyperfleetapi}/client_test.go (95%) rename internal/{hyperfleet_api => hyperfleetapi}/mock.go (99%) rename internal/{hyperfleet_api => hyperfleetapi}/types.go (99%) rename internal/{k8s_client => k8sclient}/README.md (98%) rename internal/{k8s_client => k8sclient}/apply.go (92%) rename internal/{k8s_client => k8sclient}/client.go (92%) rename internal/{k8s_client => k8sclient}/client_test.go (99%) rename internal/{k8s_client => k8sclient}/discovery.go (79%) rename internal/{k8s_client => k8sclient}/interface.go (74%) rename internal/{k8s_client => k8sclient}/mock.go (69%) rename internal/{k8s_client => k8sclient}/test_helpers_test.go (58%) rename internal/{k8s_client => k8sclient}/types.go (98%) rename internal/{k8s_client => k8sclient}/types_test.go (99%) rename internal/{maestro_client => maestroclient}/client.go (89%) rename internal/{maestro_client => maestroclient}/client_test.go (99%) rename internal/{maestro_client => maestroclient}/interface.go (68%) rename internal/{maestro_client => maestroclient}/ocm_logger_adapter.go (99%) rename internal/{maestro_client => maestroclient}/operations.go (99%) rename internal/{maestro_client => maestroclient}/operations_test.go (94%) rename internal/{transport_client => transportclient}/interface.go (64%) rename internal/{transport_client => transportclient}/types.go (81%) rename test/integration/{k8s_client => k8sclient}/README.md (92%) rename test/integration/{k8s_client => k8sclient}/client_integration_test.go (99%) rename test/integration/{k8s_client => k8sclient}/helper_envtest_prebuilt.go (88%) rename test/integration/{k8s_client => k8sclient}/helper_selector.go (88%) rename test/integration/{k8s_client => k8sclient}/main_test.go (94%) rename test/integration/{maestro_client => maestroclient}/client_integration_test.go (85%) rename test/integration/{maestro_client => maestroclient}/client_tls_config_integration_test.go (80%) rename test/integration/{maestro_client => maestroclient}/client_tls_integration_test.go (94%) rename test/integration/{maestro_client => maestroclient}/main_test.go (92%) rename test/integration/{maestro_client => maestroclient}/setup_test.go (99%) rename test/integration/{maestro_client => maestroclient}/tls_helper_test.go (99%) diff --git a/.golangci.yml b/.golangci.yml index aea6390..c693573 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -43,8 +43,6 @@ linters: misspell: locale: US - ignore-rules: - - cancelled # British spelling is acceptable lll: line-length: 120 @@ -67,79 +65,25 @@ linters: exhaustive: default-signifies-exhaustive: true - gocritic: - disabled-checks: - - ifElseChain - - singleCaseSwitch - - elseif - - exitAfterDefer exclusions: generated: lax - presets: - - comments - - common-false-positives - - legacy - - std-error-handling rules: # Relaxed rules for test files - linters: - gosec - errcheck - unparam - - goconst - - lll - - gocritic - - exhaustive path: _test\.go - # Relaxed rules for integration tests - - linters: - - gosec - - errcheck - - unparam - - goconst - - lll - - gocritic - - revive - - exhaustive - path: test/ - # Relaxed rules for cmd/ - - linters: - - revive - - lll - path: cmd/ - # Relaxed rules for internal packages - - linters: - - lll - path: internal/ - # Relaxed rules for pkg/ - - linters: - - lll - path: pkg/ - # G304: file inclusion via variable - acceptable for config loader - - linters: - - gosec - path: internal/config_loader/ - text: 'G304' - # G404: weak random - acceptable for jitter in retry logic - - linters: - - gosec - path: internal/hyperfleet_api/client\.go - text: 'G404' - # Package naming: underscore packages are structural and renaming is out of scope - # Scoped to legacy underscore packages only - - linters: - - revive - path: internal/(config_loader|hyperfleet_api|maestro_client|k8s_client|transport_client)/ - text: "don't use an underscore in package name" - # Package naming: allow existing package names (scoped to legacy packages) + # pkg/utils and pkg/errors: names conflict with Go conventions but renaming + # would break the public API surface - linters: - revive - path: (internal/(config_loader|hyperfleet_api|maestro_client|k8s_client|transport_client)|pkg/utils)/ + path: pkg/utils/ text: "avoid meaningless package names" - linters: - revive - path: (internal/(config_loader|hyperfleet_api|maestro_client|k8s_client|transport_client)|pkg/errors)/ + path: pkg/errors/ text: "avoid package names that conflict with" # Standard exclusion paths paths: diff --git a/README.md b/README.md index 3ce3da4..c2dfb98 100644 --- a/README.md +++ b/README.md @@ -466,7 +466,7 @@ The first run will download golang:alpine and install envtest (~20-30 seconds). -๐Ÿ“– **Full guide:** [`test/integration/k8s_client/README.md`](test/integration/k8s_client/README.md) +๐Ÿ“– **Full guide:** [`test/integration/k8sclient/README.md`](test/integration/k8sclient/README.md) ### Test Coverage diff --git a/cmd/adapter/main.go b/cmd/adapter/main.go index ce01118..beb542c 100644 --- a/cmd/adapter/main.go +++ b/cmd/adapter/main.go @@ -10,13 +10,13 @@ import ( "syscall" "time" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/dryrun" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/executor" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleet_api" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/k8s_client" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/maestro_client" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transport_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleetapi" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/k8sclient" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/maestroclient" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transportclient" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/health" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/metrics" @@ -113,7 +113,7 @@ Dry-run mode: serveCmd.Flags().StringVar(&dryRunAPIResponses, "dry-run-api-responses", "", "Path to mock API responses JSON file for dry-run mode (defaults to 200 OK)") serveCmd.Flags().StringVar(&dryRunDiscovery, "dry-run-discovery", "", - "Path to mock discovery responses JSON file for dry-run mode (overrides applied resources by name)") + "Path to mock discovery responses JSON file for dry-run mode (overrides applied resources)") serveCmd.Flags().BoolVar(&dryRunVerbose, "dry-run-verbose", false, "Show rendered manifests, API request/response bodies in dry-run output") serveCmd.Flags().StringVar(&dryRunOutput, "dry-run-output", "text", @@ -181,7 +181,7 @@ func isDryRun() bool { // buildLoggerConfig creates a logger configuration with the following priority // (lowest to highest): config file < LOG_* env vars < --log-* CLI flags. // Pass logCfg=nil for the bootstrap logger (before config is loaded). -func buildLoggerConfig(component string, logCfg *config_loader.LogConfig) logger.Config { +func buildLoggerConfig(component string, logCfg *configloader.LogConfig) logger.Config { cfg := logger.DefaultConfig() // Apply config file values (lowest priority) @@ -226,13 +226,13 @@ func buildLoggerConfig(component string, logCfg *config_loader.LogConfig) logger } // loadConfig loads the unified adapter configuration from both config files. -func loadConfig(ctx context.Context, log logger.Logger, flags *pflag.FlagSet) (*config_loader.Config, error) { +func loadConfig(ctx context.Context, log logger.Logger, flags *pflag.FlagSet) (*configloader.Config, error) { log.Info(ctx, "Loading adapter configuration...") - config, err := config_loader.LoadConfig( - config_loader.WithAdapterConfigPath(configPath), - config_loader.WithTaskConfigPath(taskConfigPath), - config_loader.WithAdapterVersion(version.Version), - config_loader.WithFlags(flags), + config, err := configloader.LoadConfig( + configloader.WithAdapterConfigPath(configPath), + configloader.WithTaskConfigPath(taskConfigPath), + configloader.WithAdapterVersion(version.Version), + configloader.WithFlags(flags), ) if err != nil { errCtx := logger.WithErrorField(ctx, err) @@ -247,54 +247,61 @@ func loadConfig(ctx context.Context, log logger.Logger, flags *pflag.FlagSet) (* // ----------------------------------------------------------------------------- // createAPIClient creates a HyperFleet API client from the config -func createAPIClient(apiConfig config_loader.HyperfleetAPIConfig, log logger.Logger) (hyperfleet_api.Client, error) { - var opts []hyperfleet_api.ClientOption +func createAPIClient(apiConfig configloader.HyperfleetAPIConfig, log logger.Logger) (hyperfleetapi.Client, error) { + var opts []hyperfleetapi.ClientOption // Set base URL if configured (env fallback handled in NewClient) if apiConfig.BaseURL != "" { - opts = append(opts, hyperfleet_api.WithBaseURL(apiConfig.BaseURL)) + opts = append(opts, hyperfleetapi.WithBaseURL(apiConfig.BaseURL)) } // Set timeout if configured (0 means use default) if apiConfig.Timeout > 0 { - opts = append(opts, hyperfleet_api.WithTimeout(apiConfig.Timeout)) + opts = append(opts, hyperfleetapi.WithTimeout(apiConfig.Timeout)) } // Set retry attempts if apiConfig.RetryAttempts > 0 { - opts = append(opts, hyperfleet_api.WithRetryAttempts(apiConfig.RetryAttempts)) + opts = append(opts, hyperfleetapi.WithRetryAttempts(apiConfig.RetryAttempts)) } // Set retry backoff strategy if apiConfig.RetryBackoff != "" { switch apiConfig.RetryBackoff { - case hyperfleet_api.BackoffExponential, hyperfleet_api.BackoffLinear, hyperfleet_api.BackoffConstant: - opts = append(opts, hyperfleet_api.WithRetryBackoff(apiConfig.RetryBackoff)) + case hyperfleetapi.BackoffExponential, hyperfleetapi.BackoffLinear, hyperfleetapi.BackoffConstant: + opts = append(opts, hyperfleetapi.WithRetryBackoff(apiConfig.RetryBackoff)) default: - return nil, fmt.Errorf("invalid retry backoff strategy %q (supported: exponential, linear, constant)", apiConfig.RetryBackoff) + return nil, fmt.Errorf( + "invalid retry backoff strategy %q (supported: exponential, linear, constant)", + apiConfig.RetryBackoff, + ) } } // Set retry base delay if apiConfig.BaseDelay > 0 { - opts = append(opts, hyperfleet_api.WithBaseDelay(apiConfig.BaseDelay)) + opts = append(opts, hyperfleetapi.WithBaseDelay(apiConfig.BaseDelay)) } // Set retry max delay if apiConfig.MaxDelay > 0 { - opts = append(opts, hyperfleet_api.WithMaxDelay(apiConfig.MaxDelay)) + opts = append(opts, hyperfleetapi.WithMaxDelay(apiConfig.MaxDelay)) } // Set default headers for key, value := range apiConfig.DefaultHeaders { - opts = append(opts, hyperfleet_api.WithDefaultHeader(key, value)) + opts = append(opts, hyperfleetapi.WithDefaultHeader(key, value)) } - return hyperfleet_api.NewClient(log, opts...) + return hyperfleetapi.NewClient(log, opts...) } // createTransportClient creates the appropriate transport client based on config. -func createTransportClient(ctx context.Context, config *config_loader.Config, log logger.Logger) (transport_client.TransportClient, error) { +func createTransportClient( + ctx context.Context, + config *configloader.Config, + log logger.Logger, +) (transportclient.TransportClient, error) { if config.Clients.Maestro != nil { log.Info(ctx, "Creating Maestro transport client...") client, err := createMaestroClient(ctx, config.Clients.Maestro, log) @@ -315,18 +322,26 @@ func createTransportClient(ctx context.Context, config *config_loader.Config, lo } // createK8sClient creates a Kubernetes client from the config -func createK8sClient(ctx context.Context, k8sConfig config_loader.KubernetesConfig, log logger.Logger) (*k8s_client.Client, error) { - clientConfig := k8s_client.ClientConfig{ +func createK8sClient( + ctx context.Context, + k8sConfig configloader.KubernetesConfig, + log logger.Logger, +) (*k8sclient.Client, error) { + clientConfig := k8sclient.ClientConfig{ KubeConfigPath: k8sConfig.KubeConfigPath, QPS: k8sConfig.QPS, Burst: k8sConfig.Burst, } - return k8s_client.NewClient(ctx, clientConfig, log) + return k8sclient.NewClient(ctx, clientConfig, log) } // createMaestroClient creates a Maestro client from the config -func createMaestroClient(ctx context.Context, maestroConfig *config_loader.MaestroClientConfig, log logger.Logger) (*maestro_client.Client, error) { - config := &maestro_client.Config{ +func createMaestroClient( + ctx context.Context, + maestroConfig *configloader.MaestroClientConfig, + log logger.Logger, +) (*maestroclient.Client, error) { + config := &maestroclient.Config{ MaestroServerAddr: maestroConfig.HTTPServerAddress, GRPCServerAddr: maestroConfig.GRPCServerAddress, SourceID: maestroConfig.SourceID, @@ -344,7 +359,11 @@ func createMaestroClient(ctx context.Context, maestroConfig *config_loader.Maest if maestroConfig.ServerHealthinessTimeout != "" { d, err := time.ParseDuration(maestroConfig.ServerHealthinessTimeout) if err != nil { - return nil, fmt.Errorf("invalid maestro serverHealthinessTimeout %q: %w", maestroConfig.ServerHealthinessTimeout, err) + return nil, fmt.Errorf( + "invalid maestro serverHealthinessTimeout %q: %w", + maestroConfig.ServerHealthinessTimeout, + err, + ) } config.ServerHealthinessTimeout = d } @@ -356,11 +375,17 @@ func createMaestroClient(ctx context.Context, maestroConfig *config_loader.Maest config.HTTPCAFile = maestroConfig.Auth.TLSConfig.HTTPCAFile } - return maestro_client.NewMaestroClient(ctx, config, log) + return maestroclient.NewMaestroClient(ctx, config, log) } // buildExecutor creates the executor with the given clients. -func buildExecutor(config *config_loader.Config, apiClient hyperfleet_api.Client, tc transport_client.TransportClient, log logger.Logger, recorder *metrics.Recorder) (*executor.Executor, error) { +func buildExecutor( + config *configloader.Config, + apiClient hyperfleetapi.Client, + tc transportclient.TransportClient, + log logger.Logger, + recorder *metrics.Recorder, +) (*executor.Executor, error) { return executor.NewBuilder(). WithConfig(config). WithAPIClient(apiClient). @@ -386,7 +411,8 @@ func runServe(flags *pflag.FlagSet) error { return fmt.Errorf("failed to create logger: %w", err) } - log.Infof(ctx, "Starting Hyperfleet Adapter version=%s commit=%s built=%s", version.Version, version.Commit, version.BuildDate) + log.Infof(ctx, "Starting Hyperfleet Adapter version=%s commit=%s built=%s", + version.Version, version.Commit, version.BuildDate) // Load unified configuration (deployment + task configs) config, err := loadConfig(ctx, log, flags) @@ -400,11 +426,9 @@ func runServe(flags *pflag.FlagSet) error { return fmt.Errorf("failed to create logger with adapter config: %w", err) } - log.Infof(ctx, "Adapter configuration loaded successfully: name=%s ", - config.Adapter.Name) + log.Infof(ctx, "Adapter configuration loaded successfully: name=%s ", config.Adapter.Name) log.Infof(ctx, "HyperFleet API client configured: timeout=%s retry_attempts=%d", - config.Clients.HyperfleetAPI.Timeout.String(), - config.Clients.HyperfleetAPI.RetryAttempts) + config.Clients.HyperfleetAPI.Timeout.String(), config.Clients.HyperfleetAPI.RetryAttempts) var redactedConfigBytes []byte if config.DebugConfig { var data []byte @@ -585,7 +609,7 @@ func runServe(flags *pflag.FlagSet) error { // Wait for shutdown signal or fatal subscription error select { case <-ctx.Done(): - log.Info(ctx, "Context cancelled, shutting down...") + log.Info(ctx, "Context canceled, shutting down...") case err := <-fatalErrCh: errCtx := logger.WithErrorField(ctx, err) log.Errorf(errCtx, "Fatal subscription error, shutting down") @@ -595,7 +619,9 @@ func runServe(flags *pflag.FlagSet) error { // Close subscriber gracefully log.Info(ctx, "Closing broker subscriber...") - shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) + shutdownCtx, shutdownCancel := context.WithTimeout( + context.Background(), 30*time.Second, + ) defer shutdownCancel() closeDone := make(chan error, 1) @@ -754,39 +780,55 @@ func runConfigDump(flags *pflag.FlagSet) error { // addConfigPathFlags registers the --config and --task-config path flags. func addConfigPathFlags(cmd *cobra.Command) { cmd.Flags().StringVarP(&configPath, "config", "c", "", - fmt.Sprintf("Path to adapter deployment config file (can also use %s env var)", config_loader.EnvAdapterConfig)) + fmt.Sprintf("Path to adapter deployment config file (can also use %s env var)", + configloader.EnvAdapterConfig)) cmd.Flags().StringVarP(&taskConfigPath, "task-config", "t", "", - fmt.Sprintf("Path to adapter task config file (can also use %s env var)", config_loader.EnvTaskConfigPath)) + fmt.Sprintf("Path to adapter task config file (can also use %s env var)", + configloader.EnvTaskConfigPath)) } // addOverrideFlags registers all configuration override flags (Maestro, API, broker, Kubernetes). // These flags are available on both the serve and config-dump commands. func addOverrideFlags(cmd *cobra.Command) { // Maestro override flags - cmd.Flags().String("maestro-grpc-server-address", "", "Maestro gRPC server address. Env: HYPERFLEET_MAESTRO_GRPC_SERVER_ADDRESS") - cmd.Flags().String("maestro-http-server-address", "", "Maestro HTTP server address. Env: HYPERFLEET_MAESTRO_HTTP_SERVER_ADDRESS") + cmd.Flags().String("maestro-grpc-server-address", "", + "Maestro gRPC server address. Env: HYPERFLEET_MAESTRO_GRPC_SERVER_ADDRESS") + cmd.Flags().String("maestro-http-server-address", "", + "Maestro HTTP server address. Env: HYPERFLEET_MAESTRO_HTTP_SERVER_ADDRESS") cmd.Flags().String("maestro-source-id", "", "Maestro source ID. Env: HYPERFLEET_MAESTRO_SOURCE_ID") cmd.Flags().String("maestro-client-id", "", "Maestro client ID. Env: HYPERFLEET_MAESTRO_CLIENT_ID") cmd.Flags().String("maestro-auth-type", "", "Maestro auth type (tls, none). Env: HYPERFLEET_MAESTRO_AUTH_TYPE") cmd.Flags().String("maestro-ca-file", "", "Maestro gRPC CA certificate file. Env: HYPERFLEET_MAESTRO_CA_FILE") cmd.Flags().String("maestro-cert-file", "", "Maestro gRPC client certificate file. Env: HYPERFLEET_MAESTRO_CERT_FILE") cmd.Flags().String("maestro-key-file", "", "Maestro gRPC client key file. Env: HYPERFLEET_MAESTRO_KEY_FILE") - cmd.Flags().String("maestro-http-ca-file", "", "Maestro HTTP CA certificate file. Env: HYPERFLEET_MAESTRO_HTTP_CA_FILE") - cmd.Flags().String("maestro-timeout", "", "Maestro client timeout (e.g. 10s). Env: HYPERFLEET_MAESTRO_TIMEOUT") - cmd.Flags().String("maestro-server-healthiness-timeout", "", "Maestro server healthiness check timeout (e.g. 20s). Env: HYPERFLEET_MAESTRO_SERVER_HEALTHINESS_TIMEOUT") - cmd.Flags().Int("maestro-retry-attempts", 0, "Maestro retry attempts. Env: HYPERFLEET_MAESTRO_RETRY_ATTEMPTS") - cmd.Flags().String("maestro-keepalive-time", "", "Maestro gRPC keepalive ping interval (e.g. 30s). Env: HYPERFLEET_MAESTRO_KEEPALIVE_TIME") - cmd.Flags().String("maestro-keepalive-timeout", "", "Maestro gRPC keepalive ping timeout (e.g. 10s). Env: HYPERFLEET_MAESTRO_KEEPALIVE_TIMEOUT") - cmd.Flags().Bool("maestro-insecure", false, "Use insecure connection to Maestro. Env: HYPERFLEET_MAESTRO_INSECURE") + cmd.Flags().String("maestro-http-ca-file", "", + "Maestro HTTP CA certificate file. Env: HYPERFLEET_MAESTRO_HTTP_CA_FILE") + cmd.Flags().String("maestro-timeout", "", + "Maestro client timeout (e.g. 10s). Env: HYPERFLEET_MAESTRO_TIMEOUT") + cmd.Flags().String("maestro-server-healthiness-timeout", "", + "Maestro server healthiness check timeout (e.g. 20s). Env: HYPERFLEET_MAESTRO_SERVER_HEALTHINESS_TIMEOUT") + cmd.Flags().Int("maestro-retry-attempts", 0, + "Maestro retry attempts. Env: HYPERFLEET_MAESTRO_RETRY_ATTEMPTS") + cmd.Flags().String("maestro-keepalive-time", "", + "Maestro gRPC keepalive ping interval (e.g. 30s). Env: HYPERFLEET_MAESTRO_KEEPALIVE_TIME") + cmd.Flags().String("maestro-keepalive-timeout", "", + "Maestro gRPC keepalive ping timeout (e.g. 10s). Env: HYPERFLEET_MAESTRO_KEEPALIVE_TIMEOUT") + cmd.Flags().Bool("maestro-insecure", false, + "Use insecure connection to Maestro. Env: HYPERFLEET_MAESTRO_INSECURE") // HyperFleet API override flags cmd.Flags().String("hyperfleet-api-base-url", "", "HyperFleet API base URL. Env: HYPERFLEET_API_BASE_URL") cmd.Flags().String("hyperfleet-api-version", "", "HyperFleet API version (e.g. v1). Env: HYPERFLEET_API_VERSION") - cmd.Flags().String("hyperfleet-api-timeout", "", "HyperFleet API timeout (e.g. 10s). Env: HYPERFLEET_API_TIMEOUT") - cmd.Flags().Int("hyperfleet-api-retry", 0, "HyperFleet API retry attempts. Env: HYPERFLEET_API_RETRY_ATTEMPTS") - cmd.Flags().String("hyperfleet-api-retry-backoff", "", "HyperFleet API retry backoff strategy (exponential, linear, constant). Env: HYPERFLEET_API_RETRY_BACKOFF") - cmd.Flags().String("hyperfleet-api-base-delay", "", "HyperFleet API retry base delay (e.g. 1s). Env: HYPERFLEET_API_BASE_DELAY") - cmd.Flags().String("hyperfleet-api-max-delay", "", "HyperFleet API retry max delay (e.g. 30s). Env: HYPERFLEET_API_MAX_DELAY") + cmd.Flags().String("hyperfleet-api-timeout", "", + "HyperFleet API timeout (e.g. 10s). Env: HYPERFLEET_API_TIMEOUT") + cmd.Flags().Int("hyperfleet-api-retry", 0, + "HyperFleet API retry attempts. Env: HYPERFLEET_API_RETRY_ATTEMPTS") + cmd.Flags().String("hyperfleet-api-retry-backoff", "", + "HyperFleet API retry backoff strategy (exponential, linear, constant). Env: HYPERFLEET_API_RETRY_BACKOFF") + cmd.Flags().String("hyperfleet-api-base-delay", "", + "HyperFleet API retry base delay (e.g. 1s). Env: HYPERFLEET_API_BASE_DELAY") + cmd.Flags().String("hyperfleet-api-max-delay", "", + "HyperFleet API retry max delay (e.g. 30s). Env: HYPERFLEET_API_MAX_DELAY") // Broker override flags cmd.Flags().String("broker-subscription-id", "", "Broker subscription ID. Env: HYPERFLEET_BROKER_SUBSCRIPTION_ID") diff --git a/docs/runbook.md b/docs/runbook.md index 55bd77e..bda9750 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -170,7 +170,7 @@ All event failures are ACKed (not retried) to avoid infinite loops on non-transi |-------------|-------|------------| | `"HyperFleet API request returned retryable status"` | API returning 5xx, 408, or 429 | Check HyperFleet API health | | `"API request failed: ... after N attempt(s)"` | All retries exhausted | API may be down or overloaded | -| `"context cancelled"` | Request timed out | Increase `spec.clients.hyperfleetApi.timeout` or check API latency | +| `"context canceled"` | Request timed out | Increase `spec.clients.hyperfleetApi.timeout` or check API latency | The adapter retries on 5xx, 408 (Request Timeout), and 429 (Too Many Requests) with configurable backoff (exponential, linear, or constant). @@ -219,7 +219,7 @@ The adapter retries on 5xx, 408 (Request Timeout), and 429 (Too Many Requests) w | `"failed to create kubernetes client"` | RBAC or kubeconfig issues | Check ServiceAccount and RBAC permissions | | `K8sOperationError{Operation:"create",...}` | Resource creation failed | Check RBAC, resource quotas, namespace existence | | `K8sOperationError{Operation:"update",...}` | Conflict on update | Usually transient; retried automatically | -| `"context cancelled while waiting for resource deletion"` | Recreate timed out waiting for deletion | Resource may have finalizers preventing deletion | +| `"context canceled while waiting for resource deletion"` | Recreate timed out waiting for deletion | Resource may have finalizers preventing deletion | Retryable K8s errors (automatically retried): timeouts, server unavailable, internal errors, rate limiting, network errors (connection refused, connection reset). Non-retryable: forbidden, unauthorized, bad request, invalid, gone, method not supported, not acceptable. diff --git a/internal/config_loader/README.md b/internal/configloader/README.md similarity index 99% rename from internal/config_loader/README.md rename to internal/configloader/README.md index 328ac6a..5befa69 100644 --- a/internal/config_loader/README.md +++ b/internal/configloader/README.md @@ -24,7 +24,7 @@ The `config_loader` package loads and validates HyperFleet Adapter configuration ## Usage ```go -import "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" +import "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" // Load from file (or set ADAPTER_CONFIG_PATH env var) config, err := config_loader.Load("path/to/config.yaml") @@ -230,4 +230,4 @@ See `types.go` for complete definitions. ## Related - `internal/criteria` - Evaluates conditions -- `internal/k8s_client` - Manages K8s resources +- `internal/k8sclient` - Manages K8s resources diff --git a/internal/config_loader/accessors.go b/internal/configloader/accessors.go similarity index 99% rename from internal/config_loader/accessors.go rename to internal/configloader/accessors.go index 65bdfc7..687a0cf 100644 --- a/internal/config_loader/accessors.go +++ b/internal/configloader/accessors.go @@ -1,4 +1,4 @@ -package config_loader +package configloader import ( "fmt" diff --git a/internal/config_loader/constants.go b/internal/configloader/constants.go similarity index 99% rename from internal/config_loader/constants.go rename to internal/configloader/constants.go index ac4189e..c710a88 100644 --- a/internal/config_loader/constants.go +++ b/internal/configloader/constants.go @@ -1,4 +1,4 @@ -package config_loader +package configloader // Field path constants for configuration structure. // These constants define the known field names used in adapter configuration diff --git a/internal/config_loader/loader.go b/internal/configloader/loader.go similarity index 98% rename from internal/config_loader/loader.go rename to internal/configloader/loader.go index 72db147..0958be9 100644 --- a/internal/config_loader/loader.go +++ b/internal/configloader/loader.go @@ -1,8 +1,9 @@ -package config_loader +package configloader import ( "fmt" "os" + "path/filepath" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/utils" "gopkg.in/yaml.v3" @@ -210,7 +211,7 @@ func loadYAMLFile(baseDir, refPath string) (map[string]interface{}, error) { return nil, err } - data, err := os.ReadFile(fullPath) + data, err := os.ReadFile(filepath.Clean(fullPath)) if err != nil { return nil, fmt.Errorf("failed to read file %q: %w", fullPath, err) } diff --git a/internal/config_loader/loader_test.go b/internal/configloader/loader_test.go similarity index 97% rename from internal/config_loader/loader_test.go rename to internal/configloader/loader_test.go index 9a2ea84..24d648f 100644 --- a/internal/config_loader/loader_test.go +++ b/internal/configloader/loader_test.go @@ -1,4 +1,4 @@ -package config_loader +package configloader import ( "os" @@ -11,6 +11,18 @@ import ( "gopkg.in/yaml.v3" ) +const testAdapterConfigYAML = ` +adapter: + name: test-adapter + version: "0.1.0" +clients: + hyperfleet_api: + base_url: "https://test.example.com" + timeout: 2s + kubernetes: + api_version: "v1" +` + // createTestConfigFiles creates temporary adapter and task config files for testing func createTestConfigFiles(t *testing.T, tmpDir string, adapterYAML, taskYAML string) (adapterPath, taskPath string) { t.Helper() @@ -716,17 +728,7 @@ status: "{{ .status }}" `), 0644)) // Create adapter config file - adapterYAML := ` -adapter: - name: test-adapter - version: "0.1.0" -clients: - hyperfleet_api: - base_url: "https://test.example.com" - timeout: 2s - kubernetes: - api_version: "v1" -` + adapterYAML := testAdapterConfigYAML adapterPath := filepath.Join(tmpDir, "adapter-config.yaml") require.NoError(t, os.WriteFile(adapterPath, []byte(adapterYAML), 0644)) @@ -828,17 +830,7 @@ spec: `), 0644)) // Create adapter config - adapterYAML := ` -adapter: - name: test-adapter - version: "0.1.0" -clients: - hyperfleet_api: - base_url: "https://test.example.com" - timeout: 2s - kubernetes: - api_version: "v1" -` + adapterYAML := testAdapterConfigYAML adapterPath := filepath.Join(tmpDir, "adapter-config.yaml") require.NoError(t, os.WriteFile(adapterPath, []byte(adapterYAML), 0644)) @@ -1308,17 +1300,7 @@ spec: manifests: [] `), 0644)) - adapterYAML := ` -adapter: - name: test-adapter - version: "0.1.0" -clients: - hyperfleet_api: - base_url: "https://test.example.com" - timeout: 2s - kubernetes: - api_version: "v1" -` + adapterYAML := testAdapterConfigYAML taskYAML := ` params: @@ -1365,17 +1347,7 @@ resources: func TestLoadConfigWithManifestWorkRefNotFound(t *testing.T) { tmpDir := t.TempDir() - adapterYAML := ` -adapter: - name: test-adapter - version: "0.1.0" -clients: - hyperfleet_api: - base_url: "https://test.example.com" - timeout: 2s - kubernetes: - api_version: "v1" -` + adapterYAML := testAdapterConfigYAML taskYAML := ` resources: @@ -1407,17 +1379,7 @@ resources: func TestLoadConfigWithInlineManifestWork(t *testing.T) { tmpDir := t.TempDir() - adapterYAML := ` -adapter: - name: test-adapter - version: "0.1.0" -clients: - hyperfleet_api: - base_url: "https://test.example.com" - timeout: 2s - kubernetes: - api_version: "v1" -` + adapterYAML := testAdapterConfigYAML taskYAML := ` params: diff --git a/internal/config_loader/struct_validator.go b/internal/configloader/struct_validator.go similarity index 88% rename from internal/config_loader/struct_validator.go rename to internal/configloader/struct_validator.go index f1bb3ae..cc73657 100644 --- a/internal/config_loader/struct_validator.go +++ b/internal/configloader/struct_validator.go @@ -1,4 +1,4 @@ -package config_loader +package configloader import ( "fmt" @@ -66,10 +66,16 @@ func getStructValidator() *validator.Validate { structValidator = validator.New() // Register custom field-level validations - //nolint:errcheck // these validations are known-good, errors would only occur on invalid config - _ = structValidator.RegisterValidation("resourcename", validateResourceName) - //nolint:errcheck // these validations are known-good, errors would only occur on invalid config - _ = structValidator.RegisterValidation("validoperator", validateOperator) + if err := structValidator.RegisterValidation( + "resourcename", validateResourceName); err != nil { + panic(fmt.Sprintf( + "failed to register resourcename validation: %v", err)) + } + if err := structValidator.RegisterValidation( + "validoperator", validateOperator); err != nil { + panic(fmt.Sprintf( + "failed to register validoperator validation: %v", err)) + } // Register custom struct-level validations structValidator.RegisterStructValidation(validateParameterEnvRequired, Parameter{}) @@ -99,7 +105,9 @@ func validateOperator(fl validator.FieldLevel) bool { // validateParameterEnvRequired is a struct-level validator for Parameter. // Checks that required env params have their environment variables set. func validateParameterEnvRequired(sl validator.StructLevel) { - param := sl.Current().Interface().(Parameter) //nolint:errcheck // type is guaranteed by RegisterStructValidation + // type is guaranteed by RegisterStructValidation + //nolint:errcheck + param := sl.Current().Interface().(Parameter) // Only validate if Required=true and Source starts with "env." if !param.Required || !strings.HasPrefix(param.Source, "env.") { @@ -159,14 +167,20 @@ func formatFullErrorMessage(e validator.FieldError) string { case "eq": return fmt.Sprintf("invalid %s %q (expected: %q)", path, e.Value(), e.Param()) case "oneof": - return fmt.Sprintf("%s %q is invalid (allowed: %s)", path, e.Value(), strings.ReplaceAll(e.Param(), " ", ", ")) + return fmt.Sprintf("%s %q is invalid (allowed: %s)", + path, e.Value(), strings.ReplaceAll(e.Param(), " ", ", ")) case "resourcename": - return fmt.Sprintf("%s %q: must start with lowercase letter and contain only letters, numbers, underscores (no hyphens)", path, e.Value()) + return fmt.Sprintf( + "%s %q: must start with lowercase letter and contain only letters, numbers, underscores (no hyphens)", + path, e.Value(), + ) case "validoperator": - return fmt.Sprintf("%s: invalid operator %q, must be one of: %s", path, e.Value(), strings.Join(criteria.OperatorStrings(), ", ")) + return fmt.Sprintf("%s: invalid operator %q, must be one of: %s", + path, e.Value(), strings.Join(criteria.OperatorStrings(), ", ")) case "required_without_all": // e.g., "must specify apiCall, expression, or conditions" - // Convert params like "ActionBase.APICall Expression Conditions" to "apiCall, expression, or conditions" + // Convert params like "ActionBase.APICall Expression Conditions" + // to "apiCall, expression, or conditions" params := strings.Split(e.Param(), " ") cleanParams := make([]string, 0, len(params)) for _, p := range params { @@ -227,7 +241,8 @@ var embeddedStructNames = map[string]bool{ // formatFieldPath converts validator namespace to our path format // e.g., "AdapterConfig.Spec.Resources[0].Name" -> "spec.resources[0].name" // Also handles embedded structs by removing the embedded type name -// e.g., "AdapterConfig.Spec.Preconditions[0].ActionBase.Name" -> "spec.preconditions[0].name" +// e.g., "AdapterConfig.Spec.Preconditions[0].ActionBase.Name" +// becomes "spec.preconditions[0].name" func formatFieldPath(namespace string) string { // Remove the root struct name (e.g., "AdapterConfig.") parts := strings.SplitN(namespace, ".", 2) @@ -245,7 +260,8 @@ func formatFieldPath(namespace string) string { if embeddedStructNames[part] { continue } - // Convert array-indexed parts to lowercase (e.g., "Preconditions[0]" -> "preconditions[0]") + // Convert array-indexed parts to lowercase + // e.g., "Preconditions[0]" -> "preconditions[0]" if idx := strings.Index(part, "["); idx > 0 { part = strings.ToLower(part[:idx]) + part[idx:] } diff --git a/internal/config_loader/types.go b/internal/configloader/types.go similarity index 93% rename from internal/config_loader/types.go rename to internal/configloader/types.go index 6abe09a..ac9212a 100644 --- a/internal/config_loader/types.go +++ b/internal/configloader/types.go @@ -1,10 +1,10 @@ -package config_loader +package configloader import ( "fmt" "strings" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleet_api" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleetapi" "gopkg.in/yaml.v3" ) @@ -89,8 +89,8 @@ type FieldExpressionDef struct { } // ValueDef represents a dynamic value definition in payload builds. -// Used when a payload field should be computed via field extraction (JSONPath) or CEL expression. -// Only one of Field or Expression should be set. +// Used when a payload field should be computed via field extraction (JSONPath) +// or CEL expression. Only one of Field or Expression should be set. // // Example YAML with field (JSONPath): // @@ -152,8 +152,8 @@ type LogConfig struct { } // HyperfleetAPIConfig is the HyperFleet API client configuration. -// Alias to hyperfleet_api.ClientConfig to ensure shared schema. -type HyperfleetAPIConfig = hyperfleet_api.ClientConfig +// Alias to hyperfleetapi.ClientConfig to ensure shared schema. +type HyperfleetAPIConfig = hyperfleetapi.ClientConfig // BrokerConfig contains broker consumer configuration type BrokerConfig struct { @@ -231,7 +231,8 @@ type Precondition struct { ActionBase `yaml:",inline"` Expression string `yaml:"expression,omitempty" validate:"required_without_all=ActionBase.APICall Conditions"` Capture []CaptureField `yaml:"capture,omitempty" validate:"dive"` - Conditions []Condition `yaml:"conditions,omitempty" validate:"dive,required_without_all=ActionBase.APICall Expression"` + //nolint:lll + Conditions []Condition `yaml:"conditions,omitempty" validate:"dive,required_without_all=ActionBase.APICall Expression"` } // APICall represents an API call configuration @@ -255,7 +256,8 @@ type Header struct { // // Supports two modes (mutually exclusive): // - Field: JSONPath expression for simple field extraction (e.g., "{.items[0].name}") -// - Expression: CEL expression for complex transformations (e.g., "response.items.filter(i, i.adapter == 'x')") +// - Expression: CEL expression for complex transformations +// (e.g., "response.items.filter(i, i.adapter == 'x')") type CaptureField struct { Name string `yaml:"name" validate:"required"` FieldExpressionDef `yaml:",inline"` @@ -326,8 +328,9 @@ type Resource struct { Transport *TransportConfig `yaml:"transport,omitempty"` Manifest interface{} `yaml:"manifest,omitempty"` Discovery *DiscoveryConfig `yaml:"discovery,omitempty" validate:"required"` - // NestedDiscoveries defines how to discover individual sub-resources within the applied manifest. - // For example, discovering resources inside a ManifestWork's workload. + // NestedDiscoveries defines how to discover individual sub-resources + // within the applied manifest. For example, discovering resources + // inside a ManifestWork's workload. NestedDiscoveries []NestedDiscovery `yaml:"nested_discoveries,omitempty" validate:"dive"` RecreateOnChange bool `yaml:"recreate_on_change,omitempty"` } @@ -340,9 +343,10 @@ type NestedDiscovery struct { // DiscoveryConfig represents resource discovery configuration type DiscoveryConfig struct { - BySelectors *SelectorConfig `yaml:"by_selectors,omitempty" validate:"required_without=ByName,excluded_with=ByName,omitempty"` + BySelectors *SelectorConfig `yaml:"by_selectors,omitempty" validate:"required_without=ByName,excluded_with=ByName"` Namespace string `yaml:"namespace,omitempty"` - ByName string `yaml:"by_name,omitempty" validate:"required_without=BySelectors,excluded_with=BySelectors"` + //nolint:lll + ByName string `yaml:"by_name,omitempty" validate:"required_without=BySelectors,excluded_with=BySelectors"` } // SelectorConfig represents label selector configuration @@ -399,7 +403,8 @@ func (ve *ValidationErrors) Error() string { for _, e := range ve.Errors { msgs = append(msgs, e.Error()) } - return fmt.Sprintf("validation failed with %d error(s):\n - %s", len(ve.Errors), strings.Join(msgs, "\n - ")) + return fmt.Sprintf("validation failed with %d error(s):\n - %s", + len(ve.Errors), strings.Join(msgs, "\n - ")) } func (ve *ValidationErrors) Add(path, message string) { @@ -450,11 +455,12 @@ type ClientsConfig struct { // MaestroClientConfig contains Maestro client configuration type MaestroClientConfig struct { - GRPCServerAddress string `yaml:"grpc_server_address" mapstructure:"grpc_server_address"` - HTTPServerAddress string `yaml:"http_server_address" mapstructure:"http_server_address"` - SourceID string `yaml:"source_id" mapstructure:"source_id"` - ClientID string `yaml:"client_id" mapstructure:"client_id"` - Timeout string `yaml:"timeout" mapstructure:"timeout"` + GRPCServerAddress string `yaml:"grpc_server_address" mapstructure:"grpc_server_address"` + HTTPServerAddress string `yaml:"http_server_address" mapstructure:"http_server_address"` + SourceID string `yaml:"source_id" mapstructure:"source_id"` + ClientID string `yaml:"client_id" mapstructure:"client_id"` + Timeout string `yaml:"timeout" mapstructure:"timeout"` + //nolint:lll ServerHealthinessTimeout string `yaml:"server_healthiness_timeout,omitempty" mapstructure:"server_healthiness_timeout"` Keepalive *KeepaliveConfig `yaml:"keepalive,omitempty" mapstructure:"keepalive"` Auth MaestroAuthConfig `yaml:"auth" mapstructure:"auth"` diff --git a/internal/config_loader/validator.go b/internal/configloader/validator.go similarity index 99% rename from internal/config_loader/validator.go rename to internal/configloader/validator.go index f91babb..558296b 100644 --- a/internal/config_loader/validator.go +++ b/internal/configloader/validator.go @@ -1,8 +1,9 @@ -package config_loader +package configloader import ( "fmt" "os" + "path/filepath" "reflect" "regexp" "strings" @@ -129,7 +130,7 @@ func (v *TaskConfigValidator) validateFileExists(refPath, configPath string) err return fmt.Errorf("%s: %w", configPath, err) } - info, err := os.Stat(fullPath) + info, err := os.Stat(filepath.Clean(fullPath)) if err != nil { if os.IsNotExist(err) { return fmt.Errorf("%s: referenced file %q does not exist (resolved to %q)", configPath, refPath, fullPath) diff --git a/internal/config_loader/validator_test.go b/internal/configloader/validator_test.go similarity index 93% rename from internal/config_loader/validator_test.go rename to internal/configloader/validator_test.go index 436a68f..45144d2 100644 --- a/internal/config_loader/validator_test.go +++ b/internal/configloader/validator_test.go @@ -1,4 +1,4 @@ -package config_loader +package configloader import ( "os" @@ -141,8 +141,11 @@ func TestValidateTemplateVariables(t *testing.T) { } cfg.Preconditions = []Precondition{{ ActionBase: ActionBase{ - Name: "checkCluster", - APICall: &APICall{Method: "GET", URL: "{{ .apiUrl }}/clusters/{{ .clusterId }}"}, + Name: "checkCluster", + APICall: &APICall{ + Method: "GET", + URL: "{{ .apiUrl }}/clusters/{{ .clusterId }}", + }, }, }} v := newTaskValidator(cfg) @@ -155,8 +158,11 @@ func TestValidateTemplateVariables(t *testing.T) { cfg.Params = []Parameter{{Name: "clusterId", Source: "event.id"}} cfg.Preconditions = []Precondition{{ ActionBase: ActionBase{ - Name: "checkCluster", - APICall: &APICall{Method: "GET", URL: "{{ .undefinedVar }}/clusters/{{ .clusterId }}"}, + Name: "checkCluster", + APICall: &APICall{ + Method: "GET", + URL: "{{ .undefinedVar }}/clusters/{{ .clusterId }}", + }, }, }} v := newTaskValidator(cfg) @@ -193,7 +199,10 @@ func TestValidateTemplateVariables(t *testing.T) { Name: "getCluster", APICall: &APICall{Method: "GET", URL: "{{ .apiUrl }}/clusters"}, }, - Capture: []CaptureField{{Name: "clusterName", FieldExpressionDef: FieldExpressionDef{Field: "name"}}}, + Capture: []CaptureField{{ + Name: "clusterName", + FieldExpressionDef: FieldExpressionDef{Field: "name"}, + }}, }} cfg.Resources = []Resource{{ Name: "testNs", @@ -235,7 +244,10 @@ func TestValidateCELExpressions(t *testing.T) { }) t.Run("valid CEL with has() function", func(t *testing.T) { - cfg := withExpression(`has(cluster.status) && cluster.status.conditions.exists(c, c.type == "Ready" && c.status == "True")`) + cfg := withExpression( + `has(cluster.status) && ` + + `cluster.status.conditions.exists(c, c.type == "Ready" && c.status == "True")`, + ) v := newTaskValidator(cfg) require.NoError(t, v.ValidateStructure()) require.NoError(t, v.ValidateSemantic()) @@ -258,7 +270,10 @@ func TestValidateK8sManifests(t *testing.T) { validManifest := map[string]interface{}{ "apiVersion": "v1", "kind": "Namespace", - "metadata": map[string]interface{}{"name": "test-namespace", "labels": map[string]interface{}{"app": "test"}}, + "metadata": map[string]interface{}{ + "name": "test-namespace", + "labels": map[string]interface{}{"app": "test"}, + }, } t.Run("valid K8s manifest", func(t *testing.T) { @@ -308,7 +323,9 @@ func TestValidateK8sManifests(t *testing.T) { cfg := withResource(map[string]interface{}{ "apiVersion": "v1", "kind": "Namespace", - "metadata": map[string]interface{}{"labels": map[string]interface{}{"app": "test"}}, + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{"app": "test"}, + }, }) v := newTaskValidator(cfg) _ = v.ValidateStructure() @@ -362,7 +379,10 @@ func TestValidateSemantic(t *testing.T) { // Test that ValidateSemantic catches multiple errors cfg := baseTaskConfig() cfg.Preconditions = []Precondition{ - {ActionBase: ActionBase{Name: "check1"}, Conditions: []Condition{{Field: "status", Operator: "badOperator", Value: "Ready"}}}, + { + ActionBase: ActionBase{Name: "check1"}, + Conditions: []Condition{{Field: "status", Operator: "badOperator", Value: "Ready"}}, + }, {ActionBase: ActionBase{Name: "check2"}, Expression: "invalid ))) syntax"}, } cfg.Resources = []Resource{{ @@ -451,10 +471,8 @@ func TestPayloadValidate(t *testing.T) { require.NotNil(t, errs) require.True(t, errs.HasErrors()) assert.Contains(t, errs.Error(), tt.errorMsg) - } else { - if errs != nil { - assert.False(t, errs.HasErrors(), "unexpected error: %v", errs) - } + } else if errs != nil { + assert.False(t, errs.HasErrors(), "unexpected error: %v", errs) } }) } @@ -477,7 +495,10 @@ func TestValidateCaptureFields(t *testing.T) { t.Run("valid capture with field only", func(t *testing.T) { cfg := withCapture([]CaptureField{ {Name: "clusterName", FieldExpressionDef: FieldExpressionDef{Field: "name"}}, - {Name: "clusterPhase", FieldExpressionDef: FieldExpressionDef{Field: "status.phase"}}, + { + Name: "clusterPhase", + FieldExpressionDef: FieldExpressionDef{Field: "status.phase"}, + }, }) v := newTaskValidator(cfg) require.NoError(t, v.ValidateStructure()) @@ -485,14 +506,20 @@ func TestValidateCaptureFields(t *testing.T) { }) t.Run("valid capture with expression only", func(t *testing.T) { - cfg := withCapture([]CaptureField{{Name: "activeCount", FieldExpressionDef: FieldExpressionDef{Expression: "1 + 1"}}}) + cfg := withCapture([]CaptureField{{ + Name: "activeCount", + FieldExpressionDef: FieldExpressionDef{Expression: "1 + 1"}, + }}) v := newTaskValidator(cfg) require.NoError(t, v.ValidateStructure()) require.NoError(t, v.ValidateSemantic()) }) t.Run("invalid - both field and expression set", func(t *testing.T) { - cfg := withCapture([]CaptureField{{Name: "conflicting", FieldExpressionDef: FieldExpressionDef{Field: "name", Expression: "1 + 1"}}}) + cfg := withCapture([]CaptureField{{ + Name: "conflicting", + FieldExpressionDef: FieldExpressionDef{Field: "name", Expression: "1 + 1"}, + }}) err := newTaskValidator(cfg).ValidateStructure() require.Error(t, err) assert.Contains(t, err.Error(), "mutually exclusive") @@ -788,7 +815,8 @@ func TestValidateTransportConfig(t *testing.T) { }) t.Run("maestro transport skips K8s manifest validation", func(t *testing.T) { - // Maestro resources use manifest for ManifestWork content - should skip K8s apiVersion/kind validation + // Maestro resources use manifest for ManifestWork content + // should skip K8s apiVersion/kind validation cfg := baseTaskConfig() cfg.Resources = []Resource{{ Name: "testMW", @@ -821,7 +849,8 @@ func TestValidateFileReferencesManifestRef(t *testing.T) { manifestDir := filepath.Join(tmpDir, "templates") require.NoError(t, os.MkdirAll(manifestDir, 0o755)) manifestFile := filepath.Join(manifestDir, "manifestwork.yaml") - require.NoError(t, os.WriteFile(manifestFile, []byte("apiVersion: work.open-cluster-management.io/v1\nkind: ManifestWork"), 0o644)) + manifestContent := []byte("apiVersion: work.open-cluster-management.io/v1\nkind: ManifestWork") + require.NoError(t, os.WriteFile(manifestFile, manifestContent, 0o644)) tests := []struct { name string diff --git a/internal/config_loader/viper_loader.go b/internal/configloader/viper_loader.go similarity index 93% rename from internal/config_loader/viper_loader.go rename to internal/configloader/viper_loader.go index 8222821..4cb43d2 100644 --- a/internal/config_loader/viper_loader.go +++ b/internal/configloader/viper_loader.go @@ -1,4 +1,4 @@ -package config_loader +package configloader import ( "bytes" @@ -97,7 +97,10 @@ var standardConfigPaths = []string{ // with environment variable and CLI flag overrides using Viper. // Priority: CLI flags > Environment variables > Config file > Defaults // Returns the resolved config file path alongside the loaded config. -func loadAdapterConfigWithViper(filePath string, flags *pflag.FlagSet) (string, *AdapterConfig, error) { +func loadAdapterConfigWithViper( + filePath string, + flags *pflag.FlagSet, +) (string, *AdapterConfig, error) { // Use "::" as key delimiter to avoid conflicts with dots in YAML keys // (e.g., "hyperfleet.io/component" in metadata.labels) v := viper.NewWithOptions(viper.KeyDelimiter("::")) @@ -118,12 +121,14 @@ func loadAdapterConfigWithViper(filePath string, flags *pflag.FlagSet) (string, } if filePath == "" { - return "", nil, fmt.Errorf("adapter config file path is required (use --config flag or %s env var)", - EnvAdapterConfig) + return "", nil, fmt.Errorf( + "adapter config file path is required (use --config flag or %s env var)", + EnvAdapterConfig, + ) } // Read the YAML file first to get base configuration - data, err := os.ReadFile(filePath) + data, err := os.ReadFile(filepath.Clean(filePath)) if err != nil { return "", nil, fmt.Errorf("failed to read adapter config file %q: %w", filePath, err) } @@ -211,11 +216,13 @@ func loadTaskConfig(filePath string) (*AdapterTaskConfig, error) { } if filePath == "" { - return nil, fmt.Errorf("task config file path is required (use --task-config flag or %s env var)", - EnvTaskConfigPath) + return nil, fmt.Errorf( + "task config file path is required (use --task-config flag or %s env var)", + EnvTaskConfigPath, + ) } - data, err := os.ReadFile(filePath) + data, err := os.ReadFile(filepath.Clean(filePath)) if err != nil { return nil, fmt.Errorf("failed to read task config file %q: %w", filePath, err) } @@ -239,9 +246,13 @@ func getBaseDir(filePath string) (string, error) { return filepath.Dir(absPath), nil } -// loadAdapterConfigWithViperGeneric wraps loadAdapterConfigWithViper, binding CLI flags if provided and of correct type. +// loadAdapterConfigWithViperGeneric wraps loadAdapterConfigWithViper, +// binding CLI flags if provided and of correct type. // Returns the resolved config file path alongside the loaded config. -func loadAdapterConfigWithViperGeneric(filePath string, flags interface{}) (string, *AdapterConfig, error) { +func loadAdapterConfigWithViperGeneric( + filePath string, + flags interface{}, +) (string, *AdapterConfig, error) { if pflags, ok := flags.(*pflag.FlagSet); ok && pflags != nil { return loadAdapterConfigWithViper(filePath, pflags) } diff --git a/internal/criteria/README.md b/internal/criteria/README.md index 433c17e..da0fc1b 100644 --- a/internal/criteria/README.md +++ b/internal/criteria/README.md @@ -209,7 +209,7 @@ The criteria package is designed to work seamlessly with conditions defined in a ```go import ( - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/criteria" ) @@ -309,8 +309,8 @@ go test -v ./internal/criteria/... -run Integration ## Related Packages -- `internal/config_loader`: Parses adapter configurations with condition definitions -- `internal/k8s_client`: Uses criteria for resource status evaluation +- `internal/configloader`: Parses adapter configurations with condition definitions +- `internal/k8sclient`: Uses criteria for resource status evaluation ## Configuration Template Examples diff --git a/internal/criteria/evaluator_test.go b/internal/criteria/evaluator_test.go index 88fabf6..fe8c798 100644 --- a/internal/criteria/evaluator_test.go +++ b/internal/criteria/evaluator_test.go @@ -901,7 +901,8 @@ func TestEvaluationError(t *testing.T) { func TestNewEvaluatorErrorsWithNilParams(t *testing.T) { t.Run("errors with nil ctx", func(t *testing.T) { - _, err := NewEvaluator(nil, NewEvaluationContext(), logger.NewTestLogger()) //nolint:staticcheck // intentionally testing nil ctx + //nolint:staticcheck // intentionally testing nil ctx + _, err := NewEvaluator(nil, NewEvaluationContext(), logger.NewTestLogger()) assert.Error(t, err) assert.Contains(t, err.Error(), "ctx is required") }) diff --git a/internal/dryrun/discovery_overrides.go b/internal/dryrun/discovery_overrides.go index 53c3ec9..ae40188 100644 --- a/internal/dryrun/discovery_overrides.go +++ b/internal/dryrun/discovery_overrides.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" ) // DiscoveryOverrides maps rendered Kubernetes resource names to complete resource @@ -15,7 +16,7 @@ type DiscoveryOverrides map[string]map[string]interface{} // Each top-level key is a rendered metadata.name, and each value is a complete // Kubernetes-like resource object that must contain at least apiVersion and kind. func LoadDiscoveryOverrides(path string) (DiscoveryOverrides, error) { - data, err := os.ReadFile(path) + data, err := os.ReadFile(filepath.Clean(path)) if err != nil { return nil, fmt.Errorf("failed to read discovery overrides file: %w", err) } diff --git a/internal/dryrun/dryrun_api_client.go b/internal/dryrun/dryrun_api_client.go index dbe5309..1578ad8 100644 --- a/internal/dryrun/dryrun_api_client.go +++ b/internal/dryrun/dryrun_api_client.go @@ -8,7 +8,7 @@ import ( "regexp" "sync" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleet_api" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleetapi" ) // RequestRecord stores details of an API request made through the dryrun client. @@ -21,7 +21,7 @@ type RequestRecord struct { StatusCode int } -// DryrunAPIClient implements hyperfleet_api.Client backed by file-defined dryrun responses. +// DryrunAPIClient implements hyperfleetapi.Client backed by file-defined dryrun responses. // It matches requests by HTTP method and URL regex pattern, returning responses // sequentially from a configured array per endpoint. All requests are recorded. type DryrunAPIClient struct { @@ -86,7 +86,7 @@ func (c *DryrunAPIClient) nextResponse(ep *compiledEndpoint) DryrunResponse { } // Do executes a dryrun HTTP request, matching against configured endpoints. -func (c *DryrunAPIClient) Do(ctx context.Context, req *hyperfleet_api.Request) (*hyperfleet_api.Response, error) { +func (c *DryrunAPIClient) Do(ctx context.Context, req *hyperfleetapi.Request) (*hyperfleetapi.Response, error) { c.mu.Lock() defer c.mu.Unlock() @@ -127,7 +127,7 @@ func (c *DryrunAPIClient) Do(ctx context.Context, req *hyperfleet_api.Request) ( } c.Requests = append(c.Requests, record) - return &hyperfleet_api.Response{ + return &hyperfleetapi.Response{ StatusCode: statusCode, Status: fmt.Sprintf("%d %s", statusCode, http.StatusText(statusCode)), Body: respBody, @@ -137,8 +137,10 @@ func (c *DryrunAPIClient) Do(ctx context.Context, req *hyperfleet_api.Request) ( } // Get performs a dryrun GET request. -func (c *DryrunAPIClient) Get(ctx context.Context, url string, opts ...hyperfleet_api.RequestOption) (*hyperfleet_api.Response, error) { - req := &hyperfleet_api.Request{Method: http.MethodGet, URL: url} +func (c *DryrunAPIClient) Get( + ctx context.Context, url string, opts ...hyperfleetapi.RequestOption, +) (*hyperfleetapi.Response, error) { + req := &hyperfleetapi.Request{Method: http.MethodGet, URL: url} for _, opt := range opts { opt(req) } @@ -146,8 +148,10 @@ func (c *DryrunAPIClient) Get(ctx context.Context, url string, opts ...hyperflee } // Post performs a dryrun POST request. -func (c *DryrunAPIClient) Post(ctx context.Context, url string, body []byte, opts ...hyperfleet_api.RequestOption) (*hyperfleet_api.Response, error) { - req := &hyperfleet_api.Request{Method: http.MethodPost, URL: url, Body: body} +func (c *DryrunAPIClient) Post( + ctx context.Context, url string, body []byte, opts ...hyperfleetapi.RequestOption, +) (*hyperfleetapi.Response, error) { + req := &hyperfleetapi.Request{Method: http.MethodPost, URL: url, Body: body} for _, opt := range opts { opt(req) } @@ -155,8 +159,10 @@ func (c *DryrunAPIClient) Post(ctx context.Context, url string, body []byte, opt } // Put performs a dryrun PUT request. -func (c *DryrunAPIClient) Put(ctx context.Context, url string, body []byte, opts ...hyperfleet_api.RequestOption) (*hyperfleet_api.Response, error) { - req := &hyperfleet_api.Request{Method: http.MethodPut, URL: url, Body: body} +func (c *DryrunAPIClient) Put( + ctx context.Context, url string, body []byte, opts ...hyperfleetapi.RequestOption, +) (*hyperfleetapi.Response, error) { + req := &hyperfleetapi.Request{Method: http.MethodPut, URL: url, Body: body} for _, opt := range opts { opt(req) } @@ -164,8 +170,10 @@ func (c *DryrunAPIClient) Put(ctx context.Context, url string, body []byte, opts } // Patch performs a dryrun PATCH request. -func (c *DryrunAPIClient) Patch(ctx context.Context, url string, body []byte, opts ...hyperfleet_api.RequestOption) (*hyperfleet_api.Response, error) { - req := &hyperfleet_api.Request{Method: http.MethodPatch, URL: url, Body: body} +func (c *DryrunAPIClient) Patch( + ctx context.Context, url string, body []byte, opts ...hyperfleetapi.RequestOption, +) (*hyperfleetapi.Response, error) { + req := &hyperfleetapi.Request{Method: http.MethodPatch, URL: url, Body: body} for _, opt := range opts { opt(req) } @@ -173,8 +181,10 @@ func (c *DryrunAPIClient) Patch(ctx context.Context, url string, body []byte, op } // Delete performs a dryrun DELETE request. -func (c *DryrunAPIClient) Delete(ctx context.Context, url string, opts ...hyperfleet_api.RequestOption) (*hyperfleet_api.Response, error) { - req := &hyperfleet_api.Request{Method: http.MethodDelete, URL: url} +func (c *DryrunAPIClient) Delete( + ctx context.Context, url string, opts ...hyperfleetapi.RequestOption, +) (*hyperfleetapi.Response, error) { + req := &hyperfleetapi.Request{Method: http.MethodDelete, URL: url} for _, opt := range opts { opt(req) } diff --git a/internal/dryrun/dryrun_api_client_test.go b/internal/dryrun/dryrun_api_client_test.go index 5b42da7..bc8d064 100644 --- a/internal/dryrun/dryrun_api_client_test.go +++ b/internal/dryrun/dryrun_api_client_test.go @@ -6,7 +6,7 @@ import ( "net/http" "testing" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleet_api" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleetapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -64,7 +64,7 @@ func TestDo_MatchesEndpoint(t *testing.T) { require.NoError(t, err) ctx := context.Background() - req := &hyperfleet_api.Request{ + req := &hyperfleetapi.Request{ Method: "GET", URL: "/api/v1/tasks/123", } @@ -106,7 +106,7 @@ func TestDo_NoMatchDefaultOK(t *testing.T) { require.NoError(t, err) ctx := context.Background() - req := &hyperfleet_api.Request{ + req := &hyperfleetapi.Request{ Method: "GET", URL: "/unmatched-path", } @@ -136,7 +136,7 @@ func TestDo_MethodFiltering(t *testing.T) { require.NoError(t, err) ctx := context.Background() - req := &hyperfleet_api.Request{ + req := &hyperfleetapi.Request{ Method: "GET", URL: "/api/v1/tasks", } @@ -180,7 +180,7 @@ func TestDo_WildcardMethod(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - req := &hyperfleet_api.Request{ + req := &hyperfleetapi.Request{ Method: tc.method, URL: "/api/v1/anything", } @@ -212,7 +212,7 @@ func TestDo_SequentialResponses(t *testing.T) { require.NoError(t, err) ctx := context.Background() - req := &hyperfleet_api.Request{ + req := &hyperfleetapi.Request{ Method: "GET", URL: "/api/v1/resource", } @@ -262,7 +262,7 @@ func TestDo_StatusCodeZeroDefaultsOK(t *testing.T) { require.NoError(t, err) ctx := context.Background() - req := &hyperfleet_api.Request{ + req := &hyperfleetapi.Request{ Method: "GET", URL: "/api/v1/zero", } @@ -276,40 +276,40 @@ func TestDo_StatusCodeZeroDefaultsOK(t *testing.T) { func TestConvenienceMethods(t *testing.T) { tests := []struct { name string - call func(ctx context.Context, client *DryrunAPIClient) (*hyperfleet_api.Response, error) + call func(ctx context.Context, client *DryrunAPIClient) (*hyperfleetapi.Response, error) expectedMethod string }{ { name: "Get", - call: func(ctx context.Context, c *DryrunAPIClient) (*hyperfleet_api.Response, error) { + call: func(ctx context.Context, c *DryrunAPIClient) (*hyperfleetapi.Response, error) { return c.Get(ctx, "/test") }, expectedMethod: "GET", }, { name: "Post", - call: func(ctx context.Context, c *DryrunAPIClient) (*hyperfleet_api.Response, error) { + call: func(ctx context.Context, c *DryrunAPIClient) (*hyperfleetapi.Response, error) { return c.Post(ctx, "/test", []byte(`{"data":"post"}`)) }, expectedMethod: "POST", }, { name: "Put", - call: func(ctx context.Context, c *DryrunAPIClient) (*hyperfleet_api.Response, error) { + call: func(ctx context.Context, c *DryrunAPIClient) (*hyperfleetapi.Response, error) { return c.Put(ctx, "/test", []byte(`{"data":"put"}`)) }, expectedMethod: "PUT", }, { name: "Patch", - call: func(ctx context.Context, c *DryrunAPIClient) (*hyperfleet_api.Response, error) { + call: func(ctx context.Context, c *DryrunAPIClient) (*hyperfleetapi.Response, error) { return c.Patch(ctx, "/test", []byte(`{"data":"patch"}`)) }, expectedMethod: "PATCH", }, { name: "Delete", - call: func(ctx context.Context, c *DryrunAPIClient) (*hyperfleet_api.Response, error) { + call: func(ctx context.Context, c *DryrunAPIClient) (*hyperfleetapi.Response, error) { return c.Delete(ctx, "/test") }, expectedMethod: "DELETE", diff --git a/internal/dryrun/dryrun_responses.go b/internal/dryrun/dryrun_responses.go index cef3d88..3c323e0 100644 --- a/internal/dryrun/dryrun_responses.go +++ b/internal/dryrun/dryrun_responses.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" ) // DryrunResponsesFile represents the top-level structure of a dryrun API responses JSON file. @@ -32,7 +33,7 @@ type DryrunResponse struct { // LoadDryrunResponses reads and parses a dryrun API responses JSON file. func LoadDryrunResponses(path string) (*DryrunResponsesFile, error) { - data, err := os.ReadFile(path) + data, err := os.ReadFile(filepath.Clean(path)) if err != nil { return nil, fmt.Errorf("failed to read dryrun responses file %q: %w", path, err) } diff --git a/internal/dryrun/event_loader.go b/internal/dryrun/event_loader.go index 5be2c93..1ad6098 100644 --- a/internal/dryrun/event_loader.go +++ b/internal/dryrun/event_loader.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" cloudevents "github.com/cloudevents/sdk-go/v2/event" ) @@ -11,7 +12,7 @@ import ( // LoadCloudEvent reads a CloudEvent from a JSON file in standard CloudEvents // JSON format and returns the parsed event. func LoadCloudEvent(path string) (*cloudevents.Event, error) { - data, err := os.ReadFile(path) + data, err := os.ReadFile(filepath.Clean(path)) if err != nil { return nil, fmt.Errorf("failed to read event file %q: %w", path, err) } diff --git a/internal/dryrun/event_loader_test.go b/internal/dryrun/event_loader_test.go index b2c765c..484a48e 100644 --- a/internal/dryrun/event_loader_test.go +++ b/internal/dryrun/event_loader_test.go @@ -20,7 +20,8 @@ func writeEventFile(t *testing.T, dir, name, content string) string { func TestLoadCloudEvent_ValidFile(t *testing.T) { t.Run("loads a valid CloudEvent from file", func(t *testing.T) { dir := t.TempDir() - path := writeEventFile(t, dir, "event.json", `{"specversion":"1.0","id":"test-123","type":"com.example.test","source":"/test"}`) + eventJSON := `{"specversion":"1.0","id":"test-123","type":"com.example.test","source":"/test"}` + path := writeEventFile(t, dir, "event.json", eventJSON) evt, err := LoadCloudEvent(path) diff --git a/internal/dryrun/recording_transport_client.go b/internal/dryrun/recording_transport_client.go index ebc10de..cc6c469 100644 --- a/internal/dryrun/recording_transport_client.go +++ b/internal/dryrun/recording_transport_client.go @@ -7,7 +7,7 @@ import ( "sync" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/manifest" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transport_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transportclient" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -21,7 +21,7 @@ const ( // TransportRecord stores details of a transport client operation. type TransportRecord struct { Error error - Result *transport_client.ApplyResult + Result *transportclient.ApplyResult Namespace string Name string GVK schema.GroupVersionKind @@ -29,7 +29,7 @@ type TransportRecord struct { Manifest []byte } -// DryrunTransportClient implements transport_client.TransportClient by recording +// DryrunTransportClient implements transportclient.TransportClient by recording // all operations in-memory without executing real Kubernetes calls. // Applied resources are stored for subsequent discovery/get operations. type DryrunTransportClient struct { @@ -64,7 +64,12 @@ func resourceKey(gvk schema.GroupVersionKind, namespace, name string) string { } // ApplyResource parses the manifest JSON, stores it in-memory, and records the operation. -func (c *DryrunTransportClient) ApplyResource(ctx context.Context, manifestBytes []byte, opts *transport_client.ApplyOptions, target transport_client.TransportContext) (*transport_client.ApplyResult, error) { +func (c *DryrunTransportClient) ApplyResource( + ctx context.Context, + manifestBytes []byte, + opts *transportclient.ApplyOptions, + target transportclient.TransportContext, +) (*transportclient.ApplyResult, error) { c.mu.Lock() defer c.mu.Unlock() @@ -109,7 +114,7 @@ func (c *DryrunTransportClient) ApplyResource(ctx context.Context, manifestBytes c.resources[key] = obj } - result := &transport_client.ApplyResult{ + result := &transportclient.ApplyResult{ Operation: operation, Reason: fmt.Sprintf("dry-run %s", operation), } @@ -127,7 +132,12 @@ func (c *DryrunTransportClient) ApplyResource(ctx context.Context, manifestBytes } // GetResource returns a resource from the in-memory store or a NotFound error. -func (c *DryrunTransportClient) GetResource(ctx context.Context, gvk schema.GroupVersionKind, namespace, name string, target transport_client.TransportContext) (*unstructured.Unstructured, error) { +func (c *DryrunTransportClient) GetResource( + ctx context.Context, + gvk schema.GroupVersionKind, + namespace, name string, + target transportclient.TransportContext, +) (*unstructured.Unstructured, error) { c.mu.Lock() defer c.mu.Unlock() @@ -142,14 +152,20 @@ func (c *DryrunTransportClient) GetResource(ctx context.Context, gvk schema.Grou }) if !exists { - return nil, fmt.Errorf("resource %s/%s %s/%s not found (dry-run)", gvk.Kind, gvk.Version, namespace, name) + return nil, fmt.Errorf("resource %s/%s %s/%s not found (dry-run)", + gvk.Kind, gvk.Version, namespace, name) } return obj.DeepCopy(), nil } // DiscoverResources returns resources from the in-memory store filtered by discovery config. -func (c *DryrunTransportClient) DiscoverResources(ctx context.Context, gvk schema.GroupVersionKind, discovery manifest.Discovery, target transport_client.TransportContext) (*unstructured.UnstructuredList, error) { +func (c *DryrunTransportClient) DiscoverResources( + ctx context.Context, + gvk schema.GroupVersionKind, + discovery manifest.Discovery, + target transportclient.TransportContext, +) (*unstructured.UnstructuredList, error) { c.mu.Lock() defer c.mu.Unlock() diff --git a/internal/dryrun/recording_transport_client_test.go b/internal/dryrun/recording_transport_client_test.go index 3f3e201..8b2c02d 100644 --- a/internal/dryrun/recording_transport_client_test.go +++ b/internal/dryrun/recording_transport_client_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/manifest" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transport_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transportclient" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime/schema" @@ -98,7 +98,7 @@ func TestApplyResource_RecreateOnChange(t *testing.T) { require.NoError(t, err) // Second apply with RecreateOnChange. - opts := &transport_client.ApplyOptions{RecreateOnChange: true} + opts := &transportclient.ApplyOptions{RecreateOnChange: true} result, err := client.ApplyResource(ctx, manifestBytes, opts, nil) require.NoError(t, err) assert.Equal(t, manifest.OperationRecreate, result.Operation) diff --git a/internal/dryrun/trace.go b/internal/dryrun/trace.go index d23ae2a..072cda6 100644 --- a/internal/dryrun/trace.go +++ b/internal/dryrun/trace.go @@ -188,7 +188,8 @@ func (t *ExecutionTrace) FormatText() string { if t.Verbose { for _, tr := range t.Transport.Records { - if tr.Operation == operationApply && tr.GVK.Kind == rr.Kind && tr.Name == rr.ResourceName && tr.Namespace == rr.Namespace { + if tr.Operation == operationApply && tr.GVK.Kind == rr.Kind && + tr.Name == rr.ResourceName && tr.Namespace == rr.Namespace { fmt.Fprintf(&b, " [verbose] Rendered manifest:\n %s\n", prettyJSON(tr.Manifest)) break } @@ -202,8 +203,10 @@ func (t *ExecutionTrace) FormatText() string { } // Discovery results (resources available for payload CEL: resources.) - if result.ExecutionContext != nil && result.ExecutionContext.Resources != nil && len(result.ExecutionContext.Resources) > 0 { - b.WriteString("\nPhase 3.5: Discovery Results ................. (available as resources.* in payload)\n") + if result.ExecutionContext != nil && result.ExecutionContext.Resources != nil && + len(result.ExecutionContext.Resources) > 0 { + msg := "\nPhase 3.5: Discovery Results ................. (available as resources.* in payload)\n" + b.WriteString(msg) celVars := result.ExecutionContext.GetCELVariables() if r, ok := celVars["resources"].(map[string]interface{}); ok { for name, val := range r { diff --git a/internal/executor/executor.go b/internal/executor/executor.go index 94d5c57..c384fb8 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -10,9 +10,9 @@ import ( "time" "github.com/cloudevents/sdk-go/v2/event" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleet_api" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transport_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleetapi" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transportclient" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/metrics" pkgotel "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/otel" @@ -126,7 +126,8 @@ func (e *Executor) Execute(ctx context.Context, data interface{}) *ExecutionResu precondOutcome := e.precondExecutor.ExecuteAll(ctx, preconditions, execCtx) result.PreconditionResults = precondOutcome.Results - if precondOutcome.Error != nil { + switch { + case precondOutcome.Error != nil: // Process execution error: precondition evaluation failed result.Status = StatusFailed precondErr := fmt.Errorf("precondition evaluation failed: error=%w", precondOutcome.Error) @@ -138,13 +139,13 @@ func (e *Executor) Execute(ctx context.Context, data interface{}) *ExecutionResu result.SkipReason = "PreconditionFailed" execCtx.SetSkipped("PreconditionFailed", precondOutcome.Error.Error()) // Continue to post actions for error reporting - } else if !precondOutcome.AllMatched { + case !precondOutcome.AllMatched: // Business outcome: precondition not satisfied result.ResourcesSkipped = true result.SkipReason = precondOutcome.NotMetReason execCtx.SetSkipped("PreconditionNotMet", precondOutcome.NotMetReason) e.log.Infof(ctx, "Phase %s: SUCCESS - NOT_MET - %s", result.CurrentPhase, precondOutcome.NotMetReason) - } else { + default: // All preconditions matched e.log.Infof(ctx, "Phase %s: SUCCESS - MET - %d passed", result.CurrentPhase, len(precondOutcome.Results)) } @@ -197,7 +198,9 @@ func (e *Executor) Execute(ctx context.Context, data interface{}) *ExecutionResu result.ExecutionContext = execCtx if result.Status == StatusSuccess { - e.log.Infof(ctx, "Event execution finished: event_execution_status=success resources_skipped=%t reason=%s", result.ResourcesSkipped, result.SkipReason) + e.log.Infof(ctx, + "Event execution finished: event_execution_status=success resources_skipped=%t reason=%s", + result.ResourcesSkipped, result.SkipReason) } else { // Combine all errors into a single error for logging var errMsgs []string @@ -363,19 +366,19 @@ func NewBuilder() *ExecutorBuilder { } // WithConfig sets the unified configuration -func (b *ExecutorBuilder) WithConfig(config *config_loader.Config) *ExecutorBuilder { +func (b *ExecutorBuilder) WithConfig(config *configloader.Config) *ExecutorBuilder { b.config.Config = config return b } // WithAPIClient sets the HyperFleet API client -func (b *ExecutorBuilder) WithAPIClient(client hyperfleet_api.Client) *ExecutorBuilder { +func (b *ExecutorBuilder) WithAPIClient(client hyperfleetapi.Client) *ExecutorBuilder { b.config.APIClient = client return b } // WithTransportClient sets the transport client for resource application (kubernetes or maestro) -func (b *ExecutorBuilder) WithTransportClient(client transport_client.TransportClient) *ExecutorBuilder { +func (b *ExecutorBuilder) WithTransportClient(client transportclient.TransportClient) *ExecutorBuilder { b.config.TransportClient = client return b } diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go index 9d758e2..e08168b 100644 --- a/internal/executor/executor_test.go +++ b/internal/executor/executor_test.go @@ -6,10 +6,10 @@ import ( "testing" "github.com/cloudevents/sdk-go/v2/event" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/criteria" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleet_api" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/k8s_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleetapi" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/k8sclient" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/metrics" "github.com/prometheus/client_golang/prometheus" @@ -19,8 +19,8 @@ import ( ) // newMockAPIClient creates a new mock API client for convenience -func newMockAPIClient() *hyperfleet_api.MockClient { - return hyperfleet_api.NewMockClient() +func newMockAPIClient() *hyperfleetapi.MockClient { + return hyperfleetapi.NewMockClient() } // TestNewExecutor tests the NewExecutor function @@ -46,7 +46,7 @@ func TestNewExecutor(t *testing.T) { { name: "missing API client", config: &ExecutorConfig{ - Config: &config_loader.Config{}, + Config: &configloader.Config{}, Logger: logger.NewTestLogger(), }, expectError: true, @@ -54,7 +54,7 @@ func TestNewExecutor(t *testing.T) { { name: "missing logger", config: &ExecutorConfig{ - Config: &config_loader.Config{}, + Config: &configloader.Config{}, APIClient: newMockAPIClient(), }, expectError: true, @@ -62,9 +62,9 @@ func TestNewExecutor(t *testing.T) { { name: "valid config", config: &ExecutorConfig{ - Config: &config_loader.Config{}, + Config: &configloader.Config{}, APIClient: newMockAPIClient(), - TransportClient: k8s_client.NewMockK8sClient(), + TransportClient: k8sclient.NewMockK8sClient(), Logger: logger.NewTestLogger(), }, expectError: false, @@ -84,8 +84,8 @@ func TestNewExecutor(t *testing.T) { } func TestExecutorBuilder(t *testing.T) { - config := &config_loader.Config{ - Adapter: config_loader.AdapterInfo{ + config := &configloader.Config{ + Adapter: configloader.AdapterInfo{ Name: "test-adapter", Version: "1.0.0", }, @@ -94,7 +94,7 @@ func TestExecutorBuilder(t *testing.T) { exec, err := NewBuilder(). WithConfig(config). WithAPIClient(newMockAPIClient()). - WithTransportClient(k8s_client.NewMockK8sClient()). + WithTransportClient(k8sclient.NewMockK8sClient()). WithLogger(logger.NewTestLogger()). Build() @@ -239,12 +239,12 @@ func TestExecute_ParamExtraction(t *testing.T) { // Set up environment variable for test t.Setenv("TEST_VAR", "test-value") - config := &config_loader.Config{ - Adapter: config_loader.AdapterInfo{ + config := &configloader.Config{ + Adapter: configloader.AdapterInfo{ Name: "test-adapter", Version: "1.0.0", }, - Params: []config_loader.Parameter{ + Params: []configloader.Parameter{ { Name: "testParam", Source: "env.TEST_VAR", @@ -261,7 +261,7 @@ func TestExecute_ParamExtraction(t *testing.T) { exec, err := NewBuilder(). WithConfig(config). WithAPIClient(newMockAPIClient()). - WithTransportClient(k8s_client.NewMockK8sClient()). + WithTransportClient(k8sclient.NewMockK8sClient()). WithLogger(logger.NewTestLogger()). Build() if err != nil { @@ -305,12 +305,12 @@ func TestParamExtractor(t *testing.T) { expectValue interface{} name string expectKey string - params []config_loader.Parameter + params []configloader.Parameter expectError bool }{ { name: "extract from env", - params: []config_loader.Parameter{ + params: []configloader.Parameter{ {Name: "envVar", Source: "env.TEST_ENV"}, }, expectKey: "envVar", @@ -318,7 +318,7 @@ func TestParamExtractor(t *testing.T) { }, { name: "extract from event", - params: []config_loader.Parameter{ + params: []configloader.Parameter{ {Name: "clusterId", Source: "event.id"}, }, expectKey: "clusterId", @@ -326,7 +326,7 @@ func TestParamExtractor(t *testing.T) { }, { name: "extract nested from event", - params: []config_loader.Parameter{ + params: []configloader.Parameter{ {Name: "nestedVal", Source: "event.nested.value"}, }, expectKey: "nestedVal", @@ -334,7 +334,7 @@ func TestParamExtractor(t *testing.T) { }, { name: "use default for missing optional", - params: []config_loader.Parameter{ + params: []configloader.Parameter{ {Name: "optional", Source: "env.MISSING", Default: "default-val"}, }, expectKey: "optional", @@ -342,14 +342,14 @@ func TestParamExtractor(t *testing.T) { }, { name: "fail on missing required", - params: []config_loader.Parameter{ + params: []configloader.Parameter{ {Name: "required", Source: "env.MISSING", Required: true}, }, expectError: true, }, { name: "extract from config", - params: []config_loader.Parameter{ + params: []configloader.Parameter{ {Name: "adapterName", Source: "config.adapter.name"}, }, expectKey: "adapterName", @@ -357,7 +357,7 @@ func TestParamExtractor(t *testing.T) { }, { name: "extract nested from config", - params: []config_loader.Parameter{ + params: []configloader.Parameter{ {Name: "adapterVersion", Source: "config.adapter.version"}, }, expectKey: "adapterVersion", @@ -365,7 +365,7 @@ func TestParamExtractor(t *testing.T) { }, { name: "use default for missing optional config field", - params: []config_loader.Parameter{ + params: []configloader.Parameter{ {Name: "optional", Source: "config.nonexistent", Default: "fallback"}, }, expectKey: "optional", @@ -373,7 +373,7 @@ func TestParamExtractor(t *testing.T) { }, { name: "fail on missing required config field", - params: []config_loader.Parameter{ + params: []configloader.Parameter{ {Name: "required", Source: "config.nonexistent", Required: true}, }, expectError: true, @@ -386,8 +386,8 @@ func TestParamExtractor(t *testing.T) { execCtx := NewExecutionContext(context.Background(), eventData, nil) // Create config with test params - config := &config_loader.Config{ - Adapter: config_loader.AdapterInfo{ + config := &configloader.Config{ + Adapter: configloader.AdapterInfo{ Name: "test", Version: "1.0.0", }, @@ -474,17 +474,17 @@ func TestSequentialExecution_Preconditions(t *testing.T) { tests := []struct { name string expectedLastName string - preconditions []config_loader.Precondition + preconditions []configloader.Precondition expectedResults int // number of results before stopping expectError bool expectNotMet bool }{ { name: "all pass - all executed", - preconditions: []config_loader.Precondition{ - {ActionBase: config_loader.ActionBase{Name: "precond1"}, Expression: "true"}, - {ActionBase: config_loader.ActionBase{Name: "precond2"}, Expression: "true"}, - {ActionBase: config_loader.ActionBase{Name: "precond3"}, Expression: "true"}, + preconditions: []configloader.Precondition{ + {ActionBase: configloader.ActionBase{Name: "precond1"}, Expression: "true"}, + {ActionBase: configloader.ActionBase{Name: "precond2"}, Expression: "true"}, + {ActionBase: configloader.ActionBase{Name: "precond3"}, Expression: "true"}, }, expectedResults: 3, expectError: false, @@ -493,10 +493,10 @@ func TestSequentialExecution_Preconditions(t *testing.T) { }, { name: "first fails - stops immediately", - preconditions: []config_loader.Precondition{ - {ActionBase: config_loader.ActionBase{Name: "precond1"}, Expression: "false"}, - {ActionBase: config_loader.ActionBase{Name: "precond2"}, Expression: "true"}, - {ActionBase: config_loader.ActionBase{Name: "precond3"}, Expression: "true"}, + preconditions: []configloader.Precondition{ + {ActionBase: configloader.ActionBase{Name: "precond1"}, Expression: "false"}, + {ActionBase: configloader.ActionBase{Name: "precond2"}, Expression: "true"}, + {ActionBase: configloader.ActionBase{Name: "precond3"}, Expression: "true"}, }, expectedResults: 1, expectError: false, @@ -505,10 +505,10 @@ func TestSequentialExecution_Preconditions(t *testing.T) { }, { name: "second fails - first executes, stops at second", - preconditions: []config_loader.Precondition{ - {ActionBase: config_loader.ActionBase{Name: "precond1"}, Expression: "true"}, - {ActionBase: config_loader.ActionBase{Name: "precond2"}, Expression: "false"}, - {ActionBase: config_loader.ActionBase{Name: "precond3"}, Expression: "true"}, + preconditions: []configloader.Precondition{ + {ActionBase: configloader.ActionBase{Name: "precond1"}, Expression: "true"}, + {ActionBase: configloader.ActionBase{Name: "precond2"}, Expression: "false"}, + {ActionBase: configloader.ActionBase{Name: "precond3"}, Expression: "true"}, }, expectedResults: 2, expectError: false, @@ -517,10 +517,10 @@ func TestSequentialExecution_Preconditions(t *testing.T) { }, { name: "third fails - first two execute, stops at third", - preconditions: []config_loader.Precondition{ - {ActionBase: config_loader.ActionBase{Name: "precond1"}, Expression: "true"}, - {ActionBase: config_loader.ActionBase{Name: "precond2"}, Expression: "true"}, - {ActionBase: config_loader.ActionBase{Name: "precond3"}, Expression: "false"}, + preconditions: []configloader.Precondition{ + {ActionBase: configloader.ActionBase{Name: "precond1"}, Expression: "true"}, + {ActionBase: configloader.ActionBase{Name: "precond2"}, Expression: "true"}, + {ActionBase: configloader.ActionBase{Name: "precond3"}, Expression: "false"}, }, expectedResults: 3, expectError: false, @@ -531,8 +531,8 @@ func TestSequentialExecution_Preconditions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - config := &config_loader.Config{ - Adapter: config_loader.AdapterInfo{ + config := &configloader.Config{ + Adapter: configloader.AdapterInfo{ Name: "test-adapter", Version: "1.0.0", }, @@ -542,7 +542,7 @@ func TestSequentialExecution_Preconditions(t *testing.T) { exec, err := NewBuilder(). WithConfig(config). WithAPIClient(newMockAPIClient()). - WithTransportClient(k8s_client.NewMockK8sClient()). + WithTransportClient(k8sclient.NewMockK8sClient()). WithLogger(logger.NewTestLogger()). Build() if err != nil { @@ -580,7 +580,8 @@ func TestSequentialExecution_Preconditions(t *testing.T) { } } -// TestPrecondition_CustomCELFunctions tests that custom CEL functions (like now()) are available in precondition expressions +// TestPrecondition_CustomCELFunctions tests that custom CEL functions +// (like now()) are available in precondition expressions func TestPrecondition_CustomCELFunctions(t *testing.T) { tests := []struct { name string @@ -601,20 +602,20 @@ func TestPrecondition_CustomCELFunctions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - config := &config_loader.Config{ - Adapter: config_loader.AdapterInfo{ + config := &configloader.Config{ + Adapter: configloader.AdapterInfo{ Name: "test-adapter", Version: "1.0.0", }, - Preconditions: []config_loader.Precondition{ - {ActionBase: config_loader.ActionBase{Name: "test-custom-function"}, Expression: tt.expression}, + Preconditions: []configloader.Precondition{ + {ActionBase: configloader.ActionBase{Name: "test-custom-function"}, Expression: tt.expression}, }, } exec, err := NewBuilder(). WithConfig(config). WithAPIClient(newMockAPIClient()). - WithTransportClient(k8s_client.NewMockK8sClient()). + WithTransportClient(k8sclient.NewMockK8sClient()). WithLogger(logger.NewTestLogger()). Build() require.NoError(t, err, "failed to create executor") @@ -647,13 +648,13 @@ func TestSequentialExecution_Resources(t *testing.T) { tests := []struct { name string - resources []config_loader.Resource + resources []configloader.Resource expectedResults int expectFailure bool }{ { name: "single resource with valid manifest", - resources: []config_loader.Resource{ + resources: []configloader.Resource{ { Name: "resource1", Manifest: map[string]interface{}{ @@ -670,7 +671,7 @@ func TestSequentialExecution_Resources(t *testing.T) { }, { name: "first resource has no manifest - stops immediately", - resources: []config_loader.Resource{ + resources: []configloader.Resource{ {Name: "resource1"}, // No manifest at all { Name: "resource2", @@ -690,8 +691,8 @@ func TestSequentialExecution_Resources(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - config := &config_loader.Config{ - Adapter: config_loader.AdapterInfo{ + config := &configloader.Config{ + Adapter: configloader.AdapterInfo{ Name: "test-adapter", Version: "1.0.0", }, @@ -701,7 +702,7 @@ func TestSequentialExecution_Resources(t *testing.T) { exec, err := NewBuilder(). WithConfig(config). WithAPIClient(newMockAPIClient()). - WithTransportClient(k8s_client.NewMockK8sClient()). + WithTransportClient(k8sclient.NewMockK8sClient()). WithLogger(logger.NewTestLogger()). Build() if err != nil { @@ -729,18 +730,18 @@ func TestSequentialExecution_Resources(t *testing.T) { func TestSequentialExecution_PostActions(t *testing.T) { tests := []struct { mockError error - mockResponse *hyperfleet_api.Response + mockResponse *hyperfleetapi.Response name string - postActions []config_loader.PostAction + postActions []configloader.PostAction expectedResults int expectError bool }{ { name: "all log actions succeed", - postActions: []config_loader.PostAction{ - {ActionBase: config_loader.ActionBase{Name: "log1", Log: &config_loader.LogAction{Message: "msg1"}}}, - {ActionBase: config_loader.ActionBase{Name: "log2", Log: &config_loader.LogAction{Message: "msg2"}}}, - {ActionBase: config_loader.ActionBase{Name: "log3", Log: &config_loader.LogAction{Message: "msg3"}}}, + postActions: []configloader.PostAction{ + {ActionBase: configloader.ActionBase{Name: "log1", Log: &configloader.LogAction{Message: "msg1"}}}, + {ActionBase: configloader.ActionBase{Name: "log2", Log: &configloader.LogAction{Message: "msg2"}}}, + {ActionBase: configloader.ActionBase{Name: "log3", Log: &configloader.LogAction{Message: "msg3"}}}, }, expectedResults: 3, expectError: false, @@ -749,12 +750,12 @@ func TestSequentialExecution_PostActions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - postConfig := &config_loader.PostConfig{ + postConfig := &configloader.PostConfig{ PostActions: tt.postActions, } - config := &config_loader.Config{ - Adapter: config_loader.AdapterInfo{ + config := &configloader.Config{ + Adapter: configloader.AdapterInfo{ Name: "test-adapter", Version: "1.0.0", }, @@ -770,7 +771,7 @@ func TestSequentialExecution_PostActions(t *testing.T) { exec, err := NewBuilder(). WithConfig(config). WithAPIClient(mockClient). - WithTransportClient(k8s_client.NewMockK8sClient()). + WithTransportClient(k8sclient.NewMockK8sClient()). WithLogger(logger.NewTestLogger()). Build() if err != nil { @@ -800,25 +801,25 @@ func TestSequentialExecution_SkipReasonCapture(t *testing.T) { tests := []struct { name string expectedStatus ExecutionStatus - preconditions []config_loader.Precondition + preconditions []configloader.Precondition expectSkipped bool }{ { name: "first precondition not met", - preconditions: []config_loader.Precondition{ - {ActionBase: config_loader.ActionBase{Name: "check1"}, Expression: "false"}, - {ActionBase: config_loader.ActionBase{Name: "check2"}, Expression: "true"}, - {ActionBase: config_loader.ActionBase{Name: "check3"}, Expression: "true"}, + preconditions: []configloader.Precondition{ + {ActionBase: configloader.ActionBase{Name: "check1"}, Expression: "false"}, + {ActionBase: configloader.ActionBase{Name: "check2"}, Expression: "true"}, + {ActionBase: configloader.ActionBase{Name: "check3"}, Expression: "true"}, }, expectedStatus: StatusSuccess, // Successful execution, just resources skipped expectSkipped: true, }, { name: "second precondition not met", - preconditions: []config_loader.Precondition{ - {ActionBase: config_loader.ActionBase{Name: "check1"}, Expression: "true"}, - {ActionBase: config_loader.ActionBase{Name: "check2"}, Expression: "false"}, - {ActionBase: config_loader.ActionBase{Name: "check3"}, Expression: "true"}, + preconditions: []configloader.Precondition{ + {ActionBase: configloader.ActionBase{Name: "check1"}, Expression: "true"}, + {ActionBase: configloader.ActionBase{Name: "check2"}, Expression: "false"}, + {ActionBase: configloader.ActionBase{Name: "check3"}, Expression: "true"}, }, expectedStatus: StatusSuccess, // Successful execution, just resources skipped expectSkipped: true, @@ -827,8 +828,8 @@ func TestSequentialExecution_SkipReasonCapture(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - config := &config_loader.Config{ - Adapter: config_loader.AdapterInfo{ + config := &configloader.Config{ + Adapter: configloader.AdapterInfo{ Name: "test-adapter", Version: "1.0.0", }, @@ -838,7 +839,7 @@ func TestSequentialExecution_SkipReasonCapture(t *testing.T) { exec, err := NewBuilder(). WithConfig(config). WithAPIClient(newMockAPIClient()). - WithTransportClient(k8s_client.NewMockK8sClient()). + WithTransportClient(k8sclient.NewMockK8sClient()). WithLogger(logger.NewTestLogger()). Build() if err != nil { @@ -870,19 +871,19 @@ func TestSequentialExecution_SkipReasonCapture(t *testing.T) { func TestCreateHandler_MetricsRecording(t *testing.T) { tests := []struct { name string - preconditions []config_loader.Precondition + preconditions []configloader.Precondition expectedStatus string // "success", "skipped", or "failed" expectedErrors []string }{ { name: "success records success metric", - preconditions: []config_loader.Precondition{}, + preconditions: []configloader.Precondition{}, expectedStatus: "success", }, { name: "skipped records skipped metric", - preconditions: []config_loader.Precondition{ - {ActionBase: config_loader.ActionBase{Name: "check"}, Expression: "false"}, + preconditions: []configloader.Precondition{ + {ActionBase: configloader.ActionBase{Name: "check"}, Expression: "false"}, }, expectedStatus: "skipped", }, @@ -893,15 +894,15 @@ func TestCreateHandler_MetricsRecording(t *testing.T) { registry := prometheus.NewRegistry() recorder := metrics.NewRecorder("test-adapter", "v0.1.0", registry) - config := &config_loader.Config{ - Adapter: config_loader.AdapterInfo{Name: "test-adapter", Version: "v0.1.0"}, + config := &configloader.Config{ + Adapter: configloader.AdapterInfo{Name: "test-adapter", Version: "v0.1.0"}, Preconditions: tt.preconditions, } exec, err := NewBuilder(). WithConfig(config). WithAPIClient(newMockAPIClient()). - WithTransportClient(k8s_client.NewMockK8sClient()). + WithTransportClient(k8sclient.NewMockK8sClient()). WithLogger(logger.NewTestLogger()). WithMetricsRecorder(recorder). Build() @@ -941,9 +942,9 @@ func TestCreateHandler_MetricsRecording_Failed(t *testing.T) { registry := prometheus.NewRegistry() recorder := metrics.NewRecorder("test-adapter", "v0.1.0", registry) - config := &config_loader.Config{ - Adapter: config_loader.AdapterInfo{Name: "test-adapter", Version: "v0.1.0"}, - Params: []config_loader.Parameter{ + config := &configloader.Config{ + Adapter: configloader.AdapterInfo{Name: "test-adapter", Version: "v0.1.0"}, + Params: []configloader.Parameter{ {Name: "required", Source: "env.MISSING_VAR", Required: true}, }, } @@ -951,7 +952,7 @@ func TestCreateHandler_MetricsRecording_Failed(t *testing.T) { exec, err := NewBuilder(). WithConfig(config). WithAPIClient(newMockAPIClient()). - WithTransportClient(k8s_client.NewMockK8sClient()). + WithTransportClient(k8sclient.NewMockK8sClient()). WithLogger(logger.NewTestLogger()). WithMetricsRecorder(recorder). Build() @@ -984,14 +985,14 @@ func TestCreateHandler_MetricsRecording_Failed(t *testing.T) { // TestCreateHandler_NilMetricsRecorder verifies handler works without a metrics recorder func TestCreateHandler_NilMetricsRecorder(t *testing.T) { - config := &config_loader.Config{ - Adapter: config_loader.AdapterInfo{Name: "test-adapter", Version: "v0.1.0"}, + config := &configloader.Config{ + Adapter: configloader.AdapterInfo{Name: "test-adapter", Version: "v0.1.0"}, } exec, err := NewBuilder(). WithConfig(config). WithAPIClient(newMockAPIClient()). - WithTransportClient(k8s_client.NewMockK8sClient()). + WithTransportClient(k8sclient.NewMockK8sClient()). WithLogger(logger.NewTestLogger()). Build() require.NoError(t, err) diff --git a/internal/executor/param_extractor.go b/internal/executor/param_extractor.go index 8f8779f..80a4840 100644 --- a/internal/executor/param_extractor.go +++ b/internal/executor/param_extractor.go @@ -6,13 +6,17 @@ import ( "strings" "github.com/go-viper/mapstructure/v2" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/utils" ) // extractConfigParams extracts all configured parameters and populates execCtx.Params // This is a pure function that directly modifies execCtx for simplicity -func extractConfigParams(config *config_loader.Config, execCtx *ExecutionContext, configMap map[string]interface{}) error { +func extractConfigParams( + config *configloader.Config, + execCtx *ExecutionContext, + configMap map[string]interface{}, +) error { for _, param := range config.Params { value, err := extractParam(param, execCtx.EventData, configMap) if err != nil { @@ -62,7 +66,11 @@ func extractConfigParams(config *config_loader.Config, execCtx *ExecutionContext } // extractParam extracts a single parameter based on its source -func extractParam(param config_loader.Parameter, eventData map[string]interface{}, configMap map[string]interface{}) (interface{}, error) { +func extractParam( + param configloader.Parameter, + eventData map[string]interface{}, + configMap map[string]interface{}, +) (interface{}, error) { source := param.Source // Handle different source types @@ -85,7 +93,7 @@ func extractParam(param config_loader.Parameter, eventData map[string]interface{ // configToMap converts a Config to map[string]interface{} using the yaml struct tags for key names. // mapstructure reads the "yaml" tag for key names but ignores the omitempty option, so zero-valued // fields like debug_config=false are preserved in the resulting map. -func configToMap(cfg *config_loader.Config) (map[string]interface{}, error) { +func configToMap(cfg *configloader.Config) (map[string]interface{}, error) { var m map[string]interface{} decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ TagName: "yaml", @@ -110,7 +118,7 @@ func extractFromEnv(envVar string) (interface{}, error) { } // addAdapterParams adds adapter info and the full config map to execCtx.Params -func addAdapterParams(config *config_loader.Config, execCtx *ExecutionContext, configMap map[string]interface{}) { +func addAdapterParams(config *configloader.Config, execCtx *ExecutionContext, configMap map[string]interface{}) { execCtx.Params["adapter"] = map[string]interface{}{ "name": config.Adapter.Name, "version": config.Adapter.Version, diff --git a/internal/executor/post_action_executor.go b/internal/executor/post_action_executor.go index 85b9638..254cd6b 100644 --- a/internal/executor/post_action_executor.go +++ b/internal/executor/post_action_executor.go @@ -5,15 +5,15 @@ import ( "encoding/json" "fmt" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/criteria" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleet_api" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleetapi" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" ) // PostActionExecutor executes post-processing actions type PostActionExecutor struct { - apiClient hyperfleet_api.Client + apiClient hyperfleetapi.Client log logger.Logger } @@ -28,7 +28,11 @@ func newPostActionExecutor(config *ExecutorConfig) *PostActionExecutor { // ExecuteAll executes all post-processing actions // First builds payloads from post.payloads, then executes post.postActions -func (pae *PostActionExecutor) ExecuteAll(ctx context.Context, postConfig *config_loader.PostConfig, execCtx *ExecutionContext) ([]PostActionResult, error) { +func (pae *PostActionExecutor) ExecuteAll( + ctx context.Context, + postConfig *configloader.PostConfig, + execCtx *ExecutionContext, +) ([]PostActionResult, error) { if postConfig == nil { return []PostActionResult{}, nil } @@ -44,7 +48,8 @@ func (pae *PostActionExecutor) ExecuteAll(ctx context.Context, postConfig *confi Step: "build_payloads", Message: err.Error(), } - return []PostActionResult{}, NewExecutorError(PhasePostActions, "build_payloads", "failed to build post payloads", err) + return []PostActionResult{}, NewExecutorError( + PhasePostActions, "build_payloads", "failed to build post payloads", err) } for _, payload := range postConfig.Payloads { pae.log.Debugf(ctx, "payload[%s] built successfully", payload.Name) @@ -79,7 +84,11 @@ func (pae *PostActionExecutor) ExecuteAll(ctx context.Context, postConfig *confi // buildPostPayloads builds all post payloads and stores them in execCtx.Params // Payloads are complex structures built from CEL expressions and templates -func (pae *PostActionExecutor) buildPostPayloads(ctx context.Context, payloads []config_loader.Payload, execCtx *ExecutionContext) error { +func (pae *PostActionExecutor) buildPostPayloads( + ctx context.Context, + payloads []configloader.Payload, + execCtx *ExecutionContext, +) error { // Create evaluation context with all CEL variables (params, adapter, resources) evalCtx := criteria.NewEvaluationContext() evalCtx.SetVariablesFromMap(execCtx.GetCELVariables()) @@ -92,11 +101,12 @@ func (pae *PostActionExecutor) buildPostPayloads(ctx context.Context, payloads [ for _, payload := range payloads { // Determine build source (inline Build or BuildRef) var buildDef any - if payload.Build != nil { + switch { + case payload.Build != nil: buildDef = payload.Build - } else if payload.BuildRefContent != nil { + case payload.BuildRefContent != nil: buildDef = payload.BuildRefContent - } else { + default: return fmt.Errorf("payload '%s' has neither Build nor BuildRefContent", payload.Name) } @@ -121,7 +131,12 @@ func (pae *PostActionExecutor) buildPostPayloads(ctx context.Context, payloads [ // buildPayload builds a payload from a build definition // The build definition can contain expressions that need to be evaluated -func (pae *PostActionExecutor) buildPayload(ctx context.Context, build any, evaluator *criteria.Evaluator, params map[string]any) (any, error) { +func (pae *PostActionExecutor) buildPayload( + ctx context.Context, + build any, + evaluator *criteria.Evaluator, + params map[string]any, +) (any, error) { switch v := build.(type) { case map[string]any: return pae.buildMapPayload(ctx, v, evaluator, params) @@ -134,7 +149,12 @@ func (pae *PostActionExecutor) buildPayload(ctx context.Context, build any, eval } // buildMapPayload builds a map payload, evaluating expressions as needed -func (pae *PostActionExecutor) buildMapPayload(ctx context.Context, m map[string]any, evaluator *criteria.Evaluator, params map[string]any) (map[string]any, error) { +func (pae *PostActionExecutor) buildMapPayload( + ctx context.Context, + m map[string]any, + evaluator *criteria.Evaluator, + params map[string]any, +) (map[string]any, error) { result := make(map[string]any) for k, v := range m { @@ -157,11 +177,16 @@ func (pae *PostActionExecutor) buildMapPayload(ctx context.Context, m map[string } // processValue processes a value, evaluating expressions as needed -func (pae *PostActionExecutor) processValue(ctx context.Context, v any, evaluator *criteria.Evaluator, params map[string]any) (any, error) { +func (pae *PostActionExecutor) processValue( + ctx context.Context, + v any, + evaluator *criteria.Evaluator, + params map[string]any, +) (any, error) { switch val := v.(type) { case map[string]any: // Check if this is a value definition: { field: "...", default: ... } or { expression: "...", default: ... } - if valueDef, ok := config_loader.ParseValueDef(val); ok { + if valueDef, ok := configloader.ParseValueDef(val); ok { result, err := evaluator.ExtractValue(valueDef.Field, valueDef.Expression) // err indicates parse error - fail fast (bug in config) if err != nil { @@ -204,7 +229,11 @@ func (pae *PostActionExecutor) processValue(ctx context.Context, v any, evaluato } // executePostAction executes a single post-action -func (pae *PostActionExecutor) executePostAction(ctx context.Context, action config_loader.PostAction, execCtx *ExecutionContext) (PostActionResult, error) { +func (pae *PostActionExecutor) executePostAction( + ctx context.Context, + action configloader.PostAction, + execCtx *ExecutionContext, +) (PostActionResult, error) { result := PostActionResult{ Name: action.Name, Status: StatusSuccess, @@ -226,7 +255,12 @@ func (pae *PostActionExecutor) executePostAction(ctx context.Context, action con } // executeAPICall executes an API call and populates the result with response details -func (pae *PostActionExecutor) executeAPICall(ctx context.Context, apiCall *config_loader.APICall, execCtx *ExecutionContext, result *PostActionResult) error { +func (pae *PostActionExecutor) executeAPICall( + ctx context.Context, + apiCall *configloader.APICall, + execCtx *ExecutionContext, + result *PostActionResult, +) error { resp, url, err := ExecuteAPICall(ctx, apiCall, execCtx, pae.apiClient, pae.log) result.APICallMade = true diff --git a/internal/executor/post_action_executor_test.go b/internal/executor/post_action_executor_test.go index 362ffa5..7cc1950 100644 --- a/internal/executor/post_action_executor_test.go +++ b/internal/executor/post_action_executor_test.go @@ -7,9 +7,9 @@ import ( "testing" "github.com/cloudevents/sdk-go/v2/event" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/criteria" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleet_api" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleetapi" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -304,8 +304,8 @@ func TestProcessValue(t *testing.T) { func TestPostActionExecutor_ExecuteAll(t *testing.T) { tests := []struct { - postConfig *config_loader.PostConfig - mockResponse *hyperfleet_api.Response + postConfig *configloader.PostConfig + mockResponse *hyperfleetapi.Response name string expectedResults int expectError bool @@ -318,20 +318,20 @@ func TestPostActionExecutor_ExecuteAll(t *testing.T) { }, { name: "empty post actions", - postConfig: &config_loader.PostConfig{ - PostActions: []config_loader.PostAction{}, + postConfig: &configloader.PostConfig{ + PostActions: []configloader.PostAction{}, }, expectedResults: 0, expectError: false, }, { name: "single log action", - postConfig: &config_loader.PostConfig{ - PostActions: []config_loader.PostAction{ + postConfig: &configloader.PostConfig{ + PostActions: []configloader.PostAction{ { - ActionBase: config_loader.ActionBase{ + ActionBase: configloader.ActionBase{ Name: "log-status", - Log: &config_loader.LogAction{Message: "Processing complete", Level: "info"}, + Log: &configloader.LogAction{Message: "Processing complete", Level: "info"}, }, }, }, @@ -341,11 +341,20 @@ func TestPostActionExecutor_ExecuteAll(t *testing.T) { }, { name: "multiple log actions", - postConfig: &config_loader.PostConfig{ - PostActions: []config_loader.PostAction{ - {ActionBase: config_loader.ActionBase{Name: "log1", Log: &config_loader.LogAction{Message: "Step 1", Level: "info"}}}, - {ActionBase: config_loader.ActionBase{Name: "log2", Log: &config_loader.LogAction{Message: "Step 2", Level: "info"}}}, - {ActionBase: config_loader.ActionBase{Name: "log3", Log: &config_loader.LogAction{Message: "Step 3", Level: "info"}}}, + postConfig: &configloader.PostConfig{ + PostActions: []configloader.PostAction{ + {ActionBase: configloader.ActionBase{ + Name: "log1", + Log: &configloader.LogAction{Message: "Step 1", Level: "info"}, + }}, + {ActionBase: configloader.ActionBase{ + Name: "log2", + Log: &configloader.LogAction{Message: "Step 2", Level: "info"}, + }}, + {ActionBase: configloader.ActionBase{ + Name: "log3", + Log: &configloader.LogAction{Message: "Step 3", Level: "info"}, + }}, }, }, expectedResults: 3, @@ -353,8 +362,8 @@ func TestPostActionExecutor_ExecuteAll(t *testing.T) { }, { name: "with payloads", - postConfig: &config_loader.PostConfig{ - Payloads: []config_loader.Payload{ + postConfig: &configloader.PostConfig{ + Payloads: []configloader.Payload{ { Name: "statusPayload", Build: map[string]interface{}{ @@ -362,8 +371,11 @@ func TestPostActionExecutor_ExecuteAll(t *testing.T) { }, }, }, - PostActions: []config_loader.PostAction{ - {ActionBase: config_loader.ActionBase{Name: "log1", Log: &config_loader.LogAction{Message: "Done", Level: "info"}}}, + PostActions: []configloader.PostAction{ + {ActionBase: configloader.ActionBase{ + Name: "log1", + Log: &configloader.LogAction{Message: "Done", Level: "info"}, + }}, }, }, expectedResults: 1, @@ -373,7 +385,7 @@ func TestPostActionExecutor_ExecuteAll(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockClient := hyperfleet_api.NewMockClient() + mockClient := hyperfleetapi.NewMockClient() if tt.mockResponse != nil { mockClient.DoResponse = tt.mockResponse } @@ -407,9 +419,9 @@ func TestPostActionExecutor_ExecuteAll(t *testing.T) { func TestExecuteAPICall(t *testing.T) { tests := []struct { mockError error - apiCall *config_loader.APICall + apiCall *configloader.APICall params map[string]interface{} - mockResponse *hyperfleet_api.Response + mockResponse *hyperfleetapi.Response name string expectedURL string expectedBody string // optional: for POST/PUT/PATCH, assert last request body (rendered payload) @@ -423,12 +435,12 @@ func TestExecuteAPICall(t *testing.T) { }, { name: "simple GET request", - apiCall: &config_loader.APICall{ + apiCall: &configloader.APICall{ Method: "GET", URL: "http://api.example.com/clusters", }, params: map[string]interface{}{}, - mockResponse: &hyperfleet_api.Response{ + mockResponse: &hyperfleetapi.Response{ StatusCode: http.StatusOK, Status: "200 OK", Body: []byte(`{"status":"ok"}`), @@ -438,14 +450,14 @@ func TestExecuteAPICall(t *testing.T) { }, { name: "GET request with URL template", - apiCall: &config_loader.APICall{ + apiCall: &configloader.APICall{ Method: "GET", URL: "http://api.example.com/clusters/{{ .clusterId }}", }, params: map[string]interface{}{ "clusterId": "cluster-123", }, - mockResponse: &hyperfleet_api.Response{ + mockResponse: &hyperfleetapi.Response{ StatusCode: http.StatusOK, Status: "200 OK", Body: []byte(`{}`), @@ -455,7 +467,7 @@ func TestExecuteAPICall(t *testing.T) { }, { name: "POST request with body", - apiCall: &config_loader.APICall{ + apiCall: &configloader.APICall{ Method: "POST", URL: "http://api.example.com/clusters", Body: `{"name": "{{ .name }}"}`, @@ -463,7 +475,7 @@ func TestExecuteAPICall(t *testing.T) { params: map[string]interface{}{ "name": "new-cluster", }, - mockResponse: &hyperfleet_api.Response{ + mockResponse: &hyperfleetapi.Response{ StatusCode: http.StatusCreated, Status: "201 Created", }, @@ -473,13 +485,13 @@ func TestExecuteAPICall(t *testing.T) { }, { name: "PUT request", - apiCall: &config_loader.APICall{ + apiCall: &configloader.APICall{ Method: "PUT", URL: "http://api.example.com/clusters/{{ .id }}", Body: `{"status": "updated"}`, }, params: map[string]interface{}{"id": "123"}, - mockResponse: &hyperfleet_api.Response{ + mockResponse: &hyperfleetapi.Response{ StatusCode: http.StatusOK, Status: "200 OK", }, @@ -489,13 +501,13 @@ func TestExecuteAPICall(t *testing.T) { }, { name: "PATCH request", - apiCall: &config_loader.APICall{ + apiCall: &configloader.APICall{ Method: "PATCH", URL: "http://api.example.com/clusters/123", Body: `{"field": "value"}`, }, params: map[string]interface{}{}, - mockResponse: &hyperfleet_api.Response{ + mockResponse: &hyperfleetapi.Response{ StatusCode: http.StatusOK, Status: "200 OK", }, @@ -505,13 +517,13 @@ func TestExecuteAPICall(t *testing.T) { }, { name: "POST with empty body", - apiCall: &config_loader.APICall{ + apiCall: &configloader.APICall{ Method: "POST", URL: "http://api.example.com/clusters", Body: "", }, params: map[string]interface{}{}, - mockResponse: &hyperfleet_api.Response{ + mockResponse: &hyperfleetapi.Response{ StatusCode: http.StatusOK, Status: "200 OK", }, @@ -521,12 +533,12 @@ func TestExecuteAPICall(t *testing.T) { }, { name: "DELETE request", - apiCall: &config_loader.APICall{ + apiCall: &configloader.APICall{ Method: "DELETE", URL: "http://api.example.com/clusters/123", }, params: map[string]interface{}{}, - mockResponse: &hyperfleet_api.Response{ + mockResponse: &hyperfleetapi.Response{ StatusCode: http.StatusNoContent, Status: "204 No Content", }, @@ -535,7 +547,7 @@ func TestExecuteAPICall(t *testing.T) { }, { name: "unsupported HTTP method", - apiCall: &config_loader.APICall{ + apiCall: &configloader.APICall{ Method: "INVALID", URL: "http://api.example.com/test", }, @@ -544,10 +556,10 @@ func TestExecuteAPICall(t *testing.T) { }, { name: "request with headers", - apiCall: &config_loader.APICall{ + apiCall: &configloader.APICall{ Method: "GET", URL: "http://api.example.com/clusters", - Headers: []config_loader.Header{ + Headers: []configloader.Header{ {Name: "Authorization", Value: "Bearer {{ .token }}"}, {Name: "X-Request-ID", Value: "{{ .requestId }}"}, }, @@ -556,7 +568,7 @@ func TestExecuteAPICall(t *testing.T) { "token": "secret-token", "requestId": "req-123", }, - mockResponse: &hyperfleet_api.Response{ + mockResponse: &hyperfleetapi.Response{ StatusCode: http.StatusOK, Status: "200 OK", }, @@ -565,13 +577,13 @@ func TestExecuteAPICall(t *testing.T) { }, { name: "request with timeout", - apiCall: &config_loader.APICall{ + apiCall: &configloader.APICall{ Method: "GET", URL: "http://api.example.com/slow", Timeout: "30s", }, params: map[string]interface{}{}, - mockResponse: &hyperfleet_api.Response{ + mockResponse: &hyperfleetapi.Response{ StatusCode: http.StatusOK, Status: "200 OK", }, @@ -580,14 +592,14 @@ func TestExecuteAPICall(t *testing.T) { }, { name: "request with retry config", - apiCall: &config_loader.APICall{ + apiCall: &configloader.APICall{ Method: "GET", URL: "http://api.example.com/flaky", RetryAttempts: 3, RetryBackoff: "exponential", }, params: map[string]interface{}{}, - mockResponse: &hyperfleet_api.Response{ + mockResponse: &hyperfleetapi.Response{ StatusCode: http.StatusOK, Status: "200 OK", }, @@ -596,7 +608,7 @@ func TestExecuteAPICall(t *testing.T) { }, { name: "URL template error", - apiCall: &config_loader.APICall{ + apiCall: &configloader.APICall{ Method: "GET", URL: "http://api.example.com/{{ .missing }}", }, @@ -607,7 +619,7 @@ func TestExecuteAPICall(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mockClient := hyperfleet_api.NewMockClient() + mockClient := hyperfleetapi.NewMockClient() if tt.mockResponse != nil { mockClient.DoResponse = tt.mockResponse } @@ -636,7 +648,11 @@ func TestExecuteAPICall(t *testing.T) { assert.Equal(t, tt.expectedURL, url) // For body-based methods, verify the rendered payload sent to the client - if tt.apiCall != nil && (tt.apiCall.Method == http.MethodPost || tt.apiCall.Method == http.MethodPut || tt.apiCall.Method == http.MethodPatch) { + isBodyMethod := tt.apiCall != nil && + (tt.apiCall.Method == http.MethodPost || + tt.apiCall.Method == http.MethodPut || + tt.apiCall.Method == http.MethodPatch) + if isBodyMethod { lastReq := mockClient.GetLastRequest() require.NotNil(t, lastReq, "expected a request for %s", tt.apiCall.Method) assert.Equal(t, tt.expectedBody, string(lastReq.Body), "request body should match rendered payload") @@ -659,7 +675,7 @@ func TestBuildPostPayloads_WithResourceDiscoveryCELHelpers(t *testing.T) { }, } - payloads := []config_loader.Payload{ + payloads := []configloader.Payload{ { Name: "inspectPayload", Build: map[string]interface{}{ diff --git a/internal/executor/precondition_executor.go b/internal/executor/precondition_executor.go index cd55527..064dd44 100644 --- a/internal/executor/precondition_executor.go +++ b/internal/executor/precondition_executor.go @@ -6,15 +6,15 @@ import ( "fmt" "strings" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/criteria" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleet_api" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleetapi" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" ) // PreconditionExecutor evaluates preconditions type PreconditionExecutor struct { - apiClient hyperfleet_api.Client + apiClient hyperfleetapi.Client log logger.Logger } @@ -29,7 +29,11 @@ func newPreconditionExecutor(config *ExecutorConfig) *PreconditionExecutor { // ExecuteAll executes all preconditions in sequence // Returns a high-level outcome with match status and individual results -func (pe *PreconditionExecutor) ExecuteAll(ctx context.Context, preconditions []config_loader.Precondition, execCtx *ExecutionContext) *PreconditionsOutcome { +func (pe *PreconditionExecutor) ExecuteAll( + ctx context.Context, + preconditions []configloader.Precondition, + execCtx *ExecutionContext, +) *PreconditionsOutcome { results := make([]PreconditionResult, 0, len(preconditions)) for _, precond := range preconditions { @@ -70,7 +74,11 @@ func (pe *PreconditionExecutor) ExecuteAll(ctx context.Context, preconditions [] } // executePrecondition executes a single precondition -func (pe *PreconditionExecutor) executePrecondition(ctx context.Context, precond config_loader.Precondition, execCtx *ExecutionContext) (PreconditionResult, error) { +func (pe *PreconditionExecutor) executePrecondition( + ctx context.Context, + precond configloader.Precondition, + execCtx *ExecutionContext, +) (PreconditionResult, error) { result := PreconditionResult{ Name: precond.Name, Status: StatusSuccess, @@ -166,7 +174,8 @@ func (pe *PreconditionExecutor) executePrecondition(ctx context.Context, precond } // Evaluate using structured conditions or CEL expression - if len(precond.Conditions) > 0 { + switch { + case len(precond.Conditions) > 0: pe.log.Debugf(ctx, "Evaluating %d structured conditions", len(precond.Conditions)) condDefs := ToConditionDefs(precond.Conditions) @@ -195,7 +204,7 @@ func (pe *PreconditionExecutor) executePrecondition(ctx context.Context, precond fieldResults[cr.Field] = cr } execCtx.AddConditionsEvaluation(PhasePreconditions, precond.Name, condResult.Matched, fieldResults) - } else if precond.Expression != "" { + case precond.Expression != "": // Evaluate CEL expression pe.log.Debugf(ctx, "Evaluating CEL expression: %s", strings.TrimSpace(precond.Expression)) celResult, err := evaluator.EvaluateCEL(strings.TrimSpace(precond.Expression)) @@ -211,7 +220,7 @@ func (pe *PreconditionExecutor) executePrecondition(ctx context.Context, precond // Record CEL evaluation in execution context execCtx.AddCELEvaluation(PhasePreconditions, precond.Name, precond.Expression, celResult.Matched) - } else { + default: // No conditions specified - consider it matched pe.log.Debugf(ctx, "No conditions specified, auto-matched") result.Matched = true @@ -221,7 +230,11 @@ func (pe *PreconditionExecutor) executePrecondition(ctx context.Context, precond } // executeAPICall executes an API call and returns the response body for field capture -func (pe *PreconditionExecutor) executeAPICall(ctx context.Context, apiCall *config_loader.APICall, execCtx *ExecutionContext) ([]byte, error) { +func (pe *PreconditionExecutor) executeAPICall( + ctx context.Context, + apiCall *configloader.APICall, + execCtx *ExecutionContext, +) ([]byte, error) { resp, url, err := ExecuteAPICall(ctx, apiCall, execCtx, pe.apiClient, pe.log) // Validate response - returns APIError with full metadata if validation fails diff --git a/internal/executor/resource_executor.go b/internal/executor/resource_executor.go index a056b24..4cbe705 100644 --- a/internal/executor/resource_executor.go +++ b/internal/executor/resource_executor.go @@ -6,10 +6,10 @@ import ( "fmt" "github.com/mitchellh/copystructure" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/maestro_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/maestroclient" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/manifest" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transport_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transportclient" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -18,7 +18,7 @@ import ( // ResourceExecutor creates and updates Kubernetes resources type ResourceExecutor struct { - client transport_client.TransportClient + client transportclient.TransportClient log logger.Logger } @@ -33,7 +33,11 @@ func newResourceExecutor(config *ExecutorConfig) *ResourceExecutor { // ExecuteAll creates/updates all resources in sequence // Returns results for each resource and updates the execution context -func (re *ResourceExecutor) ExecuteAll(ctx context.Context, resources []config_loader.Resource, execCtx *ExecutionContext) ([]ResourceResult, error) { +func (re *ResourceExecutor) ExecuteAll( + ctx context.Context, + resources []configloader.Resource, + execCtx *ExecutionContext, +) ([]ResourceResult, error) { if execCtx.Resources == nil { execCtx.Resources = make(map[string]interface{}) } @@ -54,7 +58,11 @@ func (re *ResourceExecutor) ExecuteAll(ctx context.Context, resources []config_l // executeResource creates or updates a single resource via the transport client. // For k8s transport: renders manifest template โ†’ marshals to JSON โ†’ calls ApplyResource(bytes) // For maestro transport: renders manifestWork template โ†’ marshals to JSON โ†’ calls ApplyResource(bytes) -func (re *ResourceExecutor) executeResource(ctx context.Context, resource config_loader.Resource, execCtx *ExecutionContext) (ResourceResult, error) { +func (re *ResourceExecutor) executeResource( + ctx context.Context, + resource configloader.Resource, + execCtx *ExecutionContext, +) (ResourceResult, error) { result := ResourceResult{ Name: resource.Name, Status: StatusSuccess, @@ -85,13 +93,13 @@ func (re *ResourceExecutor) executeResource(ctx context.Context, resource config } // Step 3: Prepare apply options - var applyOpts *transport_client.ApplyOptions + var applyOpts *transportclient.ApplyOptions if resource.RecreateOnChange { - applyOpts = &transport_client.ApplyOptions{RecreateOnChange: true} + applyOpts = &transportclient.ApplyOptions{RecreateOnChange: true} } - // Step 4: Build transport context (nil for k8s, *maestro_client.TransportContext for maestro) - var transportTarget transport_client.TransportContext + // Step 4: Build transport context (nil for k8s, *maestroclient.TransportContext for maestro) + var transportTarget transportclient.TransportContext if resource.IsMaestroTransport() && resource.Transport.Maestro != nil { targetCluster, tplErr := renderTemplate(resource.Transport.Maestro.TargetCluster, execCtx.Params) if tplErr != nil { @@ -99,7 +107,7 @@ func (re *ResourceExecutor) executeResource(ctx context.Context, resource config result.Error = tplErr return result, NewExecutorError(PhaseResources, resource.Name, "failed to render targetCluster template", tplErr) } - transportTarget = &maestro_client.TransportContext{ + transportTarget = &maestroclient.TransportContext{ ConsumerName: targetCluster, } } @@ -142,7 +150,8 @@ func (re *ResourceExecutor) executeResource(ctx context.Context, resource config errCtx := logger.WithK8sResult(ctx, "FAILED") errCtx = logger.WithErrorField(errCtx, discoverErr) re.log.Errorf(errCtx, "Resource[%s] discovery after apply failed: %v", resource.Name, discoverErr) - return result, NewExecutorError(PhaseResources, resource.Name, "failed to discover resource after apply", discoverErr) + return result, NewExecutorError( + PhaseResources, resource.Name, "failed to discover resource after apply", discoverErr) } if discovered != nil { // Always store the discovered top-level resource by resource name. @@ -155,14 +164,21 @@ func (re *ResourceExecutor) executeResource(ctx context.Context, resource config nestedResults := re.discoverNestedResources(ctx, resource, execCtx, discovered) for nestedName, nestedObj := range nestedResults { if nestedName == resource.Name { - re.log.Warnf(ctx, "Nested discovery %q has the same name as parent resource; skipping to avoid overwriting parent", nestedName) + re.log.Warnf(ctx, + "Nested discovery %q has the same name as parent resource; skipping to avoid overwriting parent", + nestedName) continue } if nestedObj == nil { continue } if _, exists := execCtx.Resources[nestedName]; exists { - re.log.Warnf(ctx, "Nested discovery key collision for %q; overriding previous value", nestedName) + return result, NewExecutorError( + PhaseResources, resource.Name, + fmt.Sprintf( + "nested discovery key collision: %q already exists in context", + nestedName), + nil) } execCtx.Resources[nestedName] = nestedObj } @@ -177,7 +193,11 @@ func (re *ResourceExecutor) executeResource(ctx context.Context, resource config // renderToBytes renders the resource's manifest template to JSON bytes. // The manifest holds either a K8s resource or a ManifestWork depending on transport type. -func (re *ResourceExecutor) renderToBytes(ctx context.Context, resource config_loader.Resource, execCtx *ExecutionContext) ([]byte, error) { +func (re *ResourceExecutor) renderToBytes( + ctx context.Context, + resource configloader.Resource, + execCtx *ExecutionContext, +) ([]byte, error) { if resource.Manifest == nil { return nil, fmt.Errorf("no manifest specified for resource %s", resource.Name) } @@ -217,7 +237,12 @@ func (re *ResourceExecutor) renderToBytes(ctx context.Context, resource config_l // For k8s transport: discovers the K8s resource by name or label selector. // For maestro transport: discovers the ManifestWork by name or label selector. // The discovered resource is stored in execCtx.Resources for post-action CEL evaluation. -func (re *ResourceExecutor) discoverResource(ctx context.Context, resource config_loader.Resource, execCtx *ExecutionContext, transportTarget transport_client.TransportContext) (*unstructured.Unstructured, error) { +func (re *ResourceExecutor) discoverResource( + ctx context.Context, + resource configloader.Resource, + execCtx *ExecutionContext, + transportTarget transportclient.TransportContext, +) (*unstructured.Unstructured, error) { discovery := resource.Discovery if discovery == nil { return nil, nil @@ -285,7 +310,7 @@ func (re *ResourceExecutor) discoverResource(ctx context.Context, resource confi // Each nestedDiscovery is matched against the parent's nested manifests using manifest.DiscoverNestedManifest. func (re *ResourceExecutor) discoverNestedResources( ctx context.Context, - resource config_loader.Resource, + resource configloader.Resource, execCtx *ExecutionContext, parent *unstructured.Unstructured, ) map[string]*unstructured.Unstructured { @@ -333,7 +358,7 @@ func (re *ResourceExecutor) discoverNestedResources( // buildNestedDiscoveryConfig renders templates in a discovery config and returns a manifest.DiscoveryConfig. func (re *ResourceExecutor) buildNestedDiscoveryConfig( - discovery *config_loader.DiscoveryConfig, + discovery *configloader.DiscoveryConfig, params map[string]interface{}, ) (*manifest.DiscoveryConfig, error) { namespace, err := renderTemplate(discovery.Namespace, params) @@ -376,7 +401,7 @@ func (re *ResourceExecutor) buildNestedDiscoveryConfig( // resolveGVK extracts the GVK from the resource's manifest. // Works for both K8s resources and ManifestWorks since both have apiVersion and kind. -func (re *ResourceExecutor) resolveGVK(resource config_loader.Resource) schema.GroupVersionKind { +func (re *ResourceExecutor) resolveGVK(resource configloader.Resource) schema.GroupVersionKind { manifestData, ok := resource.Manifest.(map[string]interface{}) if !ok { return schema.GroupVersionKind{} @@ -462,7 +487,10 @@ func deepCopyMap(ctx context.Context, m map[string]interface{}, log logger.Logge } // renderManifestTemplates recursively renders all template strings in a manifest -func renderManifestTemplates(data map[string]interface{}, params map[string]interface{}) (map[string]interface{}, error) { +func renderManifestTemplates( + data map[string]interface{}, + params map[string]interface{}, +) (map[string]interface{}, error) { result := make(map[string]interface{}) for k, v := range data { diff --git a/internal/executor/resource_executor_test.go b/internal/executor/resource_executor_test.go index 37ea8de..845455b 100644 --- a/internal/executor/resource_executor_test.go +++ b/internal/executor/resource_executor_test.go @@ -5,16 +5,20 @@ import ( "errors" "testing" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/k8s_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/k8sclient" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/manifest" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transport_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transportclient" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) +const ( + testStringModified = "modified" +) + func TestDeepCopyMap_BasicTypes(t *testing.T) { original := map[string]interface{}{ "string": "hello", @@ -36,7 +40,7 @@ func TestDeepCopyMap_BasicTypes(t *testing.T) { // Verify no warnings logged // Verify mutation doesn't affect original - copied["string"] = "modified" + copied["string"] = testStringModified assert.Equal(t, "hello", original["string"], "Original should not be modified") } @@ -57,7 +61,7 @@ func TestDeepCopyMap_NestedMaps(t *testing.T) { // Modify the copied nested map level1 := copied["level1"].(map[string]interface{}) level2 := level1["level2"].(map[string]interface{}) - level2["value"] = "modified" + level2["value"] = testStringModified // Verify original is NOT modified (deep copy worked) originalLevel1 := original["level1"].(map[string]interface{}) @@ -78,7 +82,7 @@ func TestDeepCopyMap_Slices(t *testing.T) { // Modify copied slice copiedItems := copied["items"].([]interface{}) - copiedItems[0] = "modified" + copiedItems[0] = testStringModified // Verify original is NOT modified originalItems := original["items"].([]interface{}) @@ -176,7 +180,7 @@ func TestDeepCopyMap_DeepCopyVerification(t *testing.T) { // Verify deep copy works copiedNested := copied["nested"].(map[string]interface{}) - copiedNested["key"] = "modified" + copiedNested["key"] = testStringModified originalNested := original["nested"].(map[string]interface{}) assert.Equal(t, "nested_value", originalNested["key"], "Original should not be modified") @@ -213,7 +217,7 @@ func TestDeepCopyMap_KubernetesManifest(t *testing.T) { // Modify copied manifest copiedMetadata := copied["metadata"].(map[string]interface{}) copiedLabels := copiedMetadata["labels"].(map[string]interface{}) - copiedLabels["app"] = "modified" + copiedLabels["app"] = testStringModified // Verify original is NOT modified originalMetadata := original["metadata"].(map[string]interface{}) @@ -245,13 +249,14 @@ func TestDeepCopyMap_RealWorldContext(t *testing.T) { } // TestResourceExecutor_ExecuteAll_DiscoveryFailure verifies that when discovery fails after a successful apply, -// the error is logged and notified: ExecuteAll returns an error, result is failed, and execCtx.Adapter.ExecutionError is set. +// the error is logged and notified: ExecuteAll returns an error, result is failed, +// and execCtx.Adapter.ExecutionError is set. func TestResourceExecutor_ExecuteAll_DiscoveryFailure(t *testing.T) { discoveryErr := errors.New("discovery failed: resource not found") - mock := k8s_client.NewMockK8sClient() + mock := k8sclient.NewMockK8sClient() mock.GetResourceError = discoveryErr // Apply succeeds so we reach discovery - mock.ApplyResourceResult = &transport_client.ApplyResult{ + mock.ApplyResourceResult = &transportclient.ApplyResult{ Operation: manifest.OperationCreate, Reason: "mock", } @@ -262,9 +267,9 @@ func TestResourceExecutor_ExecuteAll_DiscoveryFailure(t *testing.T) { } re := newResourceExecutor(config) - resource := config_loader.Resource{ + resource := configloader.Resource{ Name: "test-resource", - Transport: &config_loader.TransportConfig{Client: "kubernetes"}, + Transport: &configloader.TransportConfig{Client: "kubernetes"}, Manifest: map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", @@ -273,12 +278,12 @@ func TestResourceExecutor_ExecuteAll_DiscoveryFailure(t *testing.T) { "namespace": "default", }, }, - Discovery: &config_loader.DiscoveryConfig{ + Discovery: &configloader.DiscoveryConfig{ Namespace: "default", ByName: "test-cm", }, } - resources := []config_loader.Resource{resource} + resources := []configloader.Resource{resource} execCtx := NewExecutionContext(context.Background(), map[string]interface{}{}, nil) results, err := re.ExecuteAll(context.Background(), resources, execCtx) @@ -295,8 +300,8 @@ func TestResourceExecutor_ExecuteAll_DiscoveryFailure(t *testing.T) { } func TestResourceExecutor_ExecuteAll_StoresNestedDiscoveriesByName(t *testing.T) { - mock := k8s_client.NewMockK8sClient() - mock.ApplyResourceResult = &transport_client.ApplyResult{ + mock := k8sclient.NewMockK8sClient() + mock.ApplyResourceResult = &transportclient.ApplyResult{ Operation: manifest.OperationCreate, Reason: "mock", } @@ -364,9 +369,9 @@ func TestResourceExecutor_ExecuteAll_StoresNestedDiscoveriesByName(t *testing.T) Logger: logger.NewTestLogger(), }) - resource := config_loader.Resource{ + resource := configloader.Resource{ Name: "resource0", - Transport: &config_loader.TransportConfig{ + Transport: &configloader.TransportConfig{ Client: "kubernetes", }, Manifest: map[string]interface{}{ @@ -377,14 +382,14 @@ func TestResourceExecutor_ExecuteAll_StoresNestedDiscoveriesByName(t *testing.T) "namespace": "default", }, }, - Discovery: &config_loader.DiscoveryConfig{ + Discovery: &configloader.DiscoveryConfig{ Namespace: "default", ByName: "cluster-1-adapter2", }, - NestedDiscoveries: []config_loader.NestedDiscovery{ + NestedDiscoveries: []configloader.NestedDiscovery{ { Name: "configmap0", - Discovery: &config_loader.DiscoveryConfig{ + Discovery: &configloader.DiscoveryConfig{ Namespace: "default", ByName: "cluster-1-adapter2-configmap", }, @@ -393,7 +398,7 @@ func TestResourceExecutor_ExecuteAll_StoresNestedDiscoveriesByName(t *testing.T) } execCtx := NewExecutionContext(context.Background(), map[string]interface{}{}, nil) - results, err := re.ExecuteAll(context.Background(), []config_loader.Resource{resource}, execCtx) + results, err := re.ExecuteAll(context.Background(), []configloader.Resource{resource}, execCtx) require.NoError(t, err) require.Len(t, results, 1) assert.Equal(t, StatusSuccess, results[0].Status) diff --git a/internal/executor/types.go b/internal/executor/types.go index 1ddae69..14deb90 100644 --- a/internal/executor/types.go +++ b/internal/executor/types.go @@ -5,11 +5,11 @@ import ( "fmt" "time" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/criteria" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleet_api" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleetapi" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/manifest" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transport_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transportclient" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/metrics" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -58,11 +58,11 @@ type EventData struct { // ExecutorConfig holds configuration for the executor type ExecutorConfig struct { // Config is the unified configuration (merged from deployment and task configs) - Config *config_loader.Config + Config *configloader.Config // APIClient is the HyperFleet API client - APIClient hyperfleet_api.Client + APIClient hyperfleetapi.Client // TransportClient is the transport client for applying resources (kubernetes or maestro) - TransportClient transport_client.TransportClient + TransportClient transportclient.TransportClient // Logger is the logger instance Logger logger.Logger // MetricsRecorder records adapter-level Prometheus metrics (nil disables recording) @@ -137,7 +137,8 @@ type ResourceResult struct { // ResourceName is the actual K8s resource name ResourceName string // OperationReason explains why this operation was performed - // Examples: "resource not found", "generation changed from 1 to 2", "generation 1 unchanged", "recreate_on_change=true" + // Examples: "resource not found", "generation changed from 1 to 2", + // "generation 1 unchanged", "recreate_on_change=true" OperationReason string // Status is the result status Status ExecutionStatus @@ -170,7 +171,7 @@ type ExecutionContext struct { // Ctx is the Go context Ctx context.Context // Config is the unified adapter configuration - Config *config_loader.Config + Config *configloader.Config // EventData is the parsed event data payload EventData map[string]interface{} // Params holds extracted parameters and captured fields @@ -243,7 +244,11 @@ type ExecutionError struct { } // NewExecutionContext creates a new execution context -func NewExecutionContext(ctx context.Context, eventData map[string]interface{}, config *config_loader.Config) *ExecutionContext { +func NewExecutionContext( + ctx context.Context, + eventData map[string]interface{}, + config *configloader.Config, +) *ExecutionContext { return &ExecutionContext{ Ctx: ctx, Config: config, @@ -258,7 +263,14 @@ func NewExecutionContext(ctx context.Context, eventData map[string]interface{}, } // AddEvaluation records a condition evaluation result -func (ec *ExecutionContext) AddEvaluation(phase ExecutionPhase, name string, evalType EvaluationType, expression string, matched bool, fieldResults map[string]criteria.EvaluationResult) { +func (ec *ExecutionContext) AddEvaluation( + phase ExecutionPhase, + name string, + evalType EvaluationType, + expression string, + matched bool, + fieldResults map[string]criteria.EvaluationResult, +) { ec.Evaluations = append(ec.Evaluations, EvaluationRecord{ Phase: phase, Name: name, @@ -276,7 +288,12 @@ func (ec *ExecutionContext) AddCELEvaluation(phase ExecutionPhase, name, express } // AddConditionsEvaluation is a convenience method for recording structured conditions evaluations -func (ec *ExecutionContext) AddConditionsEvaluation(phase ExecutionPhase, name string, matched bool, fieldResults map[string]criteria.EvaluationResult) { +func (ec *ExecutionContext) AddConditionsEvaluation( + phase ExecutionPhase, + name string, + matched bool, + fieldResults map[string]criteria.EvaluationResult, +) { ec.AddEvaluation(phase, name, EvaluationTypeConditions, "", matched, fieldResults) } diff --git a/internal/executor/utils.go b/internal/executor/utils.go index 450ce99..29694df 100644 --- a/internal/executor/utils.go +++ b/internal/executor/utils.go @@ -12,18 +12,18 @@ import ( "text/template" "time" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/criteria" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleet_api" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleetapi" apierrors "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/errors" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" "golang.org/x/text/cases" "golang.org/x/text/language" ) -// ToConditionDefs converts config_loader.Condition slice to criteria.ConditionDef slice. +// ToConditionDefs converts configloader.Condition slice to criteria.ConditionDef slice. // This centralizes the conversion logic that was previously repeated in multiple places. -func ToConditionDefs(conditions []config_loader.Condition) []criteria.ConditionDef { +func ToConditionDefs(conditions []configloader.Condition) []criteria.ConditionDef { defs := make([]criteria.ConditionDef, len(conditions)) for i, cond := range conditions { defs[i] = criteria.ConditionDef{ @@ -38,7 +38,12 @@ func ToConditionDefs(conditions []config_loader.Condition) []criteria.ConditionD // ExecuteLogAction executes a log action with the given context // The message is rendered as a Go template with access to all params // This is a shared utility function used by both PreconditionExecutor and PostActionExecutor -func ExecuteLogAction(ctx context.Context, logAction *config_loader.LogAction, execCtx *ExecutionContext, log logger.Logger) { +func ExecuteLogAction( + ctx context.Context, + logAction *configloader.LogAction, + execCtx *ExecutionContext, + log logger.Logger, +) { if logAction == nil || logAction.Message == "" { return } @@ -76,7 +81,13 @@ func ExecuteLogAction(ctx context.Context, logAction *config_loader.LogAction, e // This is a shared utility function used by both PreconditionExecutor and PostActionExecutor // On error, it returns an APIError with full context (method, URL, status, body, attempts, duration) // Returns: response, renderedURL, error -func ExecuteAPICall(ctx context.Context, apiCall *config_loader.APICall, execCtx *ExecutionContext, apiClient hyperfleet_api.Client, log logger.Logger) (*hyperfleet_api.Response, string, error) { +func ExecuteAPICall( + ctx context.Context, + apiCall *configloader.APICall, + execCtx *ExecutionContext, + apiClient hyperfleetapi.Client, + log logger.Logger, +) (*hyperfleetapi.Response, string, error) { if apiCall == nil { return nil, "", fmt.Errorf("apiCall is nil") } @@ -93,7 +104,7 @@ func ExecuteAPICall(ctx context.Context, apiCall *config_loader.APICall, execCtx log.Infof(ctx, "Making API call: %s %s", apiCall.Method, url) // Build request options - opts := make([]hyperfleet_api.RequestOption, 0) + opts := make([]hyperfleetapi.RequestOption, 0) // Add headers headers := make(map[string]string) @@ -105,14 +116,14 @@ func ExecuteAPICall(ctx context.Context, apiCall *config_loader.APICall, execCtx headers[h.Name] = headerValue } if len(headers) > 0 { - opts = append(opts, hyperfleet_api.WithHeaders(headers)) + opts = append(opts, hyperfleetapi.WithHeaders(headers)) } // Set timeout if specified if apiCall.Timeout != "" { timeout, timeoutErr := time.ParseDuration(apiCall.Timeout) if timeoutErr == nil { - opts = append(opts, hyperfleet_api.WithRequestTimeout(timeout)) + opts = append(opts, hyperfleetapi.WithRequestTimeout(timeout)) } else { log.Warnf(ctx, "failed to parse timeout '%s': %v, using default timeout", apiCall.Timeout, timeoutErr) } @@ -120,15 +131,15 @@ func ExecuteAPICall(ctx context.Context, apiCall *config_loader.APICall, execCtx // Set retry configuration if apiCall.RetryAttempts > 0 { - opts = append(opts, hyperfleet_api.WithRequestRetryAttempts(apiCall.RetryAttempts)) + opts = append(opts, hyperfleetapi.WithRequestRetryAttempts(apiCall.RetryAttempts)) } if apiCall.RetryBackoff != "" { - backoff := hyperfleet_api.BackoffStrategy(apiCall.RetryBackoff) - opts = append(opts, hyperfleet_api.WithRequestRetryBackoff(backoff)) + backoff := hyperfleetapi.BackoffStrategy(apiCall.RetryBackoff) + opts = append(opts, hyperfleetapi.WithRequestRetryBackoff(backoff)) } // Execute request based on method - var resp *hyperfleet_api.Response + var resp *hyperfleetapi.Response switch strings.ToUpper(apiCall.Method) { case http.MethodGet: resp, err = apiClient.Get(ctx, url, opts...) @@ -223,7 +234,7 @@ func ExecuteAPICall(ctx context.Context, apiCall *config_loader.APICall, execCtx // buildHyperfleetAPICallURL builds a full HyperFleet API URL when a relative path is provided. // It uses hyperfleet API client settings from execution context config. -// Since the hyperfleet_api.Client always prepends its baseURL to the path, +// Since the hyperfleetapi.Client always prepends its baseURL to the path, // this function returns a relative path that the client can use correctly. // If the URL is absolute and contains the baseURL, the relative path is extracted. func buildHyperfleetAPICallURL(apiCallURL string, execCtx *ExecutionContext) string { @@ -298,7 +309,7 @@ func buildHyperfleetAPICallURL(apiCallURL string, execCtx *ExecutionContext) str // ValidateAPIResponse checks if an API response is valid and successful // Returns an APIError with full context if response is nil or unsuccessful // method and url are used to construct APIError with proper context -func ValidateAPIResponse(resp *hyperfleet_api.Response, err error, method, url string) error { +func ValidateAPIResponse(resp *hyperfleetapi.Response, err error, method, url string) error { if err != nil { // If it's already an APIError, return it as-is if _, ok := apierrors.IsAPIError(err); ok { //nolint:errcheck // checking type only, not using the value diff --git a/internal/executor/utils_test.go b/internal/executor/utils_test.go index 0156080..379a285 100644 --- a/internal/executor/utils_test.go +++ b/internal/executor/utils_test.go @@ -7,9 +7,9 @@ import ( "testing" "time" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/criteria" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleet_api" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleetapi" apierrors "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/errors" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" "github.com/stretchr/testify/assert" @@ -18,7 +18,7 @@ import ( ) func TestValidateAPIResponse_NilError_SuccessResponse(t *testing.T) { - resp := &hyperfleet_api.Response{ + resp := &hyperfleetapi.Response{ StatusCode: 200, Status: "200 OK", Body: []byte(`{"status":"ok"}`), @@ -171,7 +171,7 @@ func TestValidateAPIResponse_NonSuccessStatusCodes(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - resp := &hyperfleet_api.Response{ + resp := &hyperfleetapi.Response{ StatusCode: tt.statusCode, Status: tt.status, Body: tt.body, @@ -233,7 +233,7 @@ func TestValidateAPIResponse_SuccessStatusCodes(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - resp := &hyperfleet_api.Response{ + resp := &hyperfleetapi.Response{ StatusCode: tt.statusCode, Status: tt.status, Body: nil, @@ -249,7 +249,7 @@ func TestValidateAPIResponse_SuccessStatusCodes(t *testing.T) { } func TestValidateAPIResponse_PreservesAttempts(t *testing.T) { - resp := &hyperfleet_api.Response{ + resp := &hyperfleetapi.Response{ StatusCode: 500, Status: "500 Internal Server Error", Body: []byte("error"), @@ -272,7 +272,7 @@ func TestValidateAPIResponse_AllHTTPMethods(t *testing.T) { for _, method := range methods { t.Run(method, func(t *testing.T) { - resp := &hyperfleet_api.Response{ + resp := &hyperfleetapi.Response{ StatusCode: 404, Status: "404 Not Found", } @@ -297,7 +297,7 @@ func TestValidateAPIResponse_URLPreserved(t *testing.T) { for _, url := range urls { t.Run(url, func(t *testing.T) { - resp := &hyperfleet_api.Response{ + resp := &hyperfleetapi.Response{ StatusCode: 500, Status: "500 Internal Server Error", } @@ -331,7 +331,7 @@ func TestValidateAPIResponse_WrappedErrorChain(t *testing.T) { } func TestValidateAPIResponse_ErrorMessageContainsContext(t *testing.T) { - resp := &hyperfleet_api.Response{ + resp := &hyperfleetapi.Response{ StatusCode: 503, Status: "503 Service Unavailable", Body: []byte(`{"message":"database connection failed","retry_after":30}`), @@ -352,7 +352,7 @@ func TestValidateAPIResponse_ErrorMessageContainsContext(t *testing.T) { func TestValidateAPIResponse_APIErrorHelpers(t *testing.T) { t.Run("IsServerError", func(t *testing.T) { - resp := &hyperfleet_api.Response{StatusCode: 500, Status: "500 Internal Server Error"} + resp := &hyperfleetapi.Response{StatusCode: 500, Status: "500 Internal Server Error"} err := ValidateAPIResponse(resp, nil, "GET", "http://example.com") apiErr, _ := apierrors.IsAPIError(err) @@ -361,7 +361,7 @@ func TestValidateAPIResponse_APIErrorHelpers(t *testing.T) { }) t.Run("IsClientError", func(t *testing.T) { - resp := &hyperfleet_api.Response{StatusCode: 400, Status: "400 Bad Request"} + resp := &hyperfleetapi.Response{StatusCode: 400, Status: "400 Bad Request"} err := ValidateAPIResponse(resp, nil, "GET", "http://example.com") apiErr, _ := apierrors.IsAPIError(err) @@ -370,7 +370,7 @@ func TestValidateAPIResponse_APIErrorHelpers(t *testing.T) { }) t.Run("IsNotFound", func(t *testing.T) { - resp := &hyperfleet_api.Response{StatusCode: 404, Status: "404 Not Found"} + resp := &hyperfleetapi.Response{StatusCode: 404, Status: "404 Not Found"} err := ValidateAPIResponse(resp, nil, "GET", "http://example.com") apiErr, _ := apierrors.IsAPIError(err) @@ -378,7 +378,7 @@ func TestValidateAPIResponse_APIErrorHelpers(t *testing.T) { }) t.Run("IsUnauthorized", func(t *testing.T) { - resp := &hyperfleet_api.Response{StatusCode: 401, Status: "401 Unauthorized"} + resp := &hyperfleetapi.Response{StatusCode: 401, Status: "401 Unauthorized"} err := ValidateAPIResponse(resp, nil, "GET", "http://example.com") apiErr, _ := apierrors.IsAPIError(err) @@ -386,7 +386,7 @@ func TestValidateAPIResponse_APIErrorHelpers(t *testing.T) { }) t.Run("IsForbidden", func(t *testing.T) { - resp := &hyperfleet_api.Response{StatusCode: 403, Status: "403 Forbidden"} + resp := &hyperfleetapi.Response{StatusCode: 403, Status: "403 Forbidden"} err := ValidateAPIResponse(resp, nil, "GET", "http://example.com") apiErr, _ := apierrors.IsAPIError(err) @@ -394,7 +394,7 @@ func TestValidateAPIResponse_APIErrorHelpers(t *testing.T) { }) t.Run("IsRateLimited", func(t *testing.T) { - resp := &hyperfleet_api.Response{StatusCode: 429, Status: "429 Too Many Requests"} + resp := &hyperfleetapi.Response{StatusCode: 429, Status: "429 Too Many Requests"} err := ValidateAPIResponse(resp, nil, "GET", "http://example.com") apiErr, _ := apierrors.IsAPIError(err) @@ -402,7 +402,7 @@ func TestValidateAPIResponse_APIErrorHelpers(t *testing.T) { }) t.Run("IsBadRequest", func(t *testing.T) { - resp := &hyperfleet_api.Response{StatusCode: 400, Status: "400 Bad Request"} + resp := &hyperfleetapi.Response{StatusCode: 400, Status: "400 Bad Request"} err := ValidateAPIResponse(resp, nil, "GET", "http://example.com") apiErr, _ := apierrors.IsAPIError(err) @@ -410,7 +410,7 @@ func TestValidateAPIResponse_APIErrorHelpers(t *testing.T) { }) t.Run("IsConflict", func(t *testing.T) { - resp := &hyperfleet_api.Response{StatusCode: 409, Status: "409 Conflict"} + resp := &hyperfleetapi.Response{StatusCode: 409, Status: "409 Conflict"} err := ValidateAPIResponse(resp, nil, "POST", "http://example.com") apiErr, _ := apierrors.IsAPIError(err) @@ -419,7 +419,7 @@ func TestValidateAPIResponse_APIErrorHelpers(t *testing.T) { } func TestValidateAPIResponse_ResponseBodyString(t *testing.T) { - resp := &hyperfleet_api.Response{ + resp := &hyperfleetapi.Response{ StatusCode: 500, Status: "500 Internal Server Error", Body: []byte(`{"error":"database timeout","code":"DB_TIMEOUT"}`), @@ -433,21 +433,21 @@ func TestValidateAPIResponse_ResponseBodyString(t *testing.T) { assert.Equal(t, `{"error":"database timeout","code":"DB_TIMEOUT"}`, apiErr.ResponseBodyString()) } -// TestToConditionDefs tests the conversion of config_loader conditions to criteria definitions +// TestToConditionDefs tests the conversion of configloader conditions to criteria definitions func TestToConditionDefs(t *testing.T) { tests := []struct { name string expected []criteria.ConditionDef - conditions []config_loader.Condition + conditions []configloader.Condition }{ { name: "empty conditions", - conditions: []config_loader.Condition{}, + conditions: []configloader.Condition{}, expected: []criteria.ConditionDef{}, }, { name: "single condition", - conditions: []config_loader.Condition{ + conditions: []configloader.Condition{ {Field: "status.phase", Operator: "equals", Value: "Running"}, }, expected: []criteria.ConditionDef{ @@ -456,7 +456,7 @@ func TestToConditionDefs(t *testing.T) { }, { name: "multiple conditions with camelCase operators", - conditions: []config_loader.Condition{ + conditions: []configloader.Condition{ {Field: "status.phase", Operator: "equals", Value: "Running"}, {Field: "replicas", Operator: "greaterThan", Value: 0}, {Field: "metadata.labels.app", Operator: "notEquals", Value: ""}, @@ -469,7 +469,7 @@ func TestToConditionDefs(t *testing.T) { }, { name: "all operator types", - conditions: []config_loader.Condition{ + conditions: []configloader.Condition{ {Field: "f1", Operator: "equals", Value: "v1"}, {Field: "f2", Operator: "notEquals", Value: "v2"}, {Field: "f3", Operator: "greaterThan", Value: 10}, @@ -715,7 +715,7 @@ func TestAdapterMetadataToMap(t *testing.T) { // TestExecuteLogAction tests log action execution func TestExecuteLogAction(t *testing.T) { tests := []struct { - logAction *config_loader.LogAction + logAction *configloader.LogAction params map[string]interface{} name string expectCall bool @@ -728,43 +728,43 @@ func TestExecuteLogAction(t *testing.T) { }, { name: "empty message", - logAction: &config_loader.LogAction{Message: ""}, + logAction: &configloader.LogAction{Message: ""}, params: map[string]interface{}{}, expectCall: false, }, { name: "simple message", - logAction: &config_loader.LogAction{Message: "Hello World", Level: "info"}, + logAction: &configloader.LogAction{Message: "Hello World", Level: "info"}, params: map[string]interface{}{}, expectCall: true, }, { name: "template message", - logAction: &config_loader.LogAction{Message: "Processing cluster {{ .clusterId }}", Level: "info"}, + logAction: &configloader.LogAction{Message: "Processing cluster {{ .clusterId }}", Level: "info"}, params: map[string]interface{}{"clusterId": "cluster-123"}, expectCall: true, }, { name: "debug level", - logAction: &config_loader.LogAction{Message: "Debug info", Level: "debug"}, + logAction: &configloader.LogAction{Message: "Debug info", Level: "debug"}, params: map[string]interface{}{}, expectCall: true, }, { name: "warning level", - logAction: &config_loader.LogAction{Message: "Warning message", Level: "warning"}, + logAction: &configloader.LogAction{Message: "Warning message", Level: "warning"}, params: map[string]interface{}{}, expectCall: true, }, { name: "error level", - logAction: &config_loader.LogAction{Message: "Error occurred", Level: "error"}, + logAction: &configloader.LogAction{Message: "Error occurred", Level: "error"}, params: map[string]interface{}{}, expectCall: true, }, { name: "default level (empty)", - logAction: &config_loader.LogAction{Message: "Default level", Level: ""}, + logAction: &configloader.LogAction{Message: "Default level", Level: ""}, params: map[string]interface{}{}, expectCall: true, }, @@ -1103,7 +1103,7 @@ func TestBuildHyperfleetAPICallURL(t *testing.T) { { name: "empty URL returns empty", url: "", - execCtx: &ExecutionContext{Config: &config_loader.Config{}}, + execCtx: &ExecutionContext{Config: &configloader.Config{}}, expected: "", }, { @@ -1122,9 +1122,9 @@ func TestBuildHyperfleetAPICallURL(t *testing.T) { name: "absolute URL matching baseURL extracts relative path", url: "http://localhost:8000/api/hyperfleet/v1/clusters/abc123", execCtx: &ExecutionContext{ - Config: &config_loader.Config{ - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Config: &configloader.Config{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ BaseURL: "http://localhost:8000", Version: "v1", }, @@ -1137,9 +1137,9 @@ func TestBuildHyperfleetAPICallURL(t *testing.T) { name: "absolute URL with baseURL containing path extracts relative path", url: "http://localhost:8000/api/hyperfleet/v1/clusters/abc123", execCtx: &ExecutionContext{ - Config: &config_loader.Config{ - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Config: &configloader.Config{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ BaseURL: "http://localhost:8000/api/hyperfleet/v1", Version: "v1", }, @@ -1152,9 +1152,9 @@ func TestBuildHyperfleetAPICallURL(t *testing.T) { name: "absolute URL with trailing slash in baseURL", url: "http://localhost:8000/api/hyperfleet/v1/clusters/abc123", execCtx: &ExecutionContext{ - Config: &config_loader.Config{ - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Config: &configloader.Config{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ BaseURL: "http://localhost:8000/api/hyperfleet/v1/", Version: "v1", }, @@ -1167,9 +1167,9 @@ func TestBuildHyperfleetAPICallURL(t *testing.T) { name: "absolute URL with different host returns as-is", url: "http://other-host:9000/api/clusters/abc123", execCtx: &ExecutionContext{ - Config: &config_loader.Config{ - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Config: &configloader.Config{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ BaseURL: "http://localhost:8000", Version: "v1", }, @@ -1182,9 +1182,9 @@ func TestBuildHyperfleetAPICallURL(t *testing.T) { name: "absolute URL with different scheme returns as-is", url: "https://localhost:8000/api/clusters/abc123", execCtx: &ExecutionContext{ - Config: &config_loader.Config{ - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Config: &configloader.Config{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ BaseURL: "http://localhost:8000", Version: "v1", }, @@ -1197,9 +1197,9 @@ func TestBuildHyperfleetAPICallURL(t *testing.T) { name: "relative path with api/ prefix returns with leading slash", url: "api/hyperfleet/v1/clusters/abc123", execCtx: &ExecutionContext{ - Config: &config_loader.Config{ - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Config: &configloader.Config{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ BaseURL: "http://localhost:8000", Version: "v1", }, @@ -1212,9 +1212,9 @@ func TestBuildHyperfleetAPICallURL(t *testing.T) { name: "relative path with /api/ prefix returns as-is", url: "/api/hyperfleet/v1/clusters/abc123", execCtx: &ExecutionContext{ - Config: &config_loader.Config{ - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Config: &configloader.Config{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ BaseURL: "http://localhost:8000", Version: "v1", }, @@ -1227,9 +1227,9 @@ func TestBuildHyperfleetAPICallURL(t *testing.T) { name: "simple relative path gets full API path added", url: "clusters/abc123", execCtx: &ExecutionContext{ - Config: &config_loader.Config{ - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Config: &configloader.Config{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ BaseURL: "http://localhost:8000", Version: "v1", }, @@ -1242,9 +1242,9 @@ func TestBuildHyperfleetAPICallURL(t *testing.T) { name: "simple relative path with leading slash gets full API path added", url: "/clusters/abc123", execCtx: &ExecutionContext{ - Config: &config_loader.Config{ - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Config: &configloader.Config{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ BaseURL: "http://localhost:8000", Version: "v1", }, @@ -1257,9 +1257,9 @@ func TestBuildHyperfleetAPICallURL(t *testing.T) { name: "relative path with custom version", url: "clusters/abc123", execCtx: &ExecutionContext{ - Config: &config_loader.Config{ - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Config: &configloader.Config{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ BaseURL: "http://localhost:8000", Version: "v2", }, @@ -1272,9 +1272,9 @@ func TestBuildHyperfleetAPICallURL(t *testing.T) { name: "relative path with empty version defaults to v1", url: "clusters/abc123", execCtx: &ExecutionContext{ - Config: &config_loader.Config{ - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Config: &configloader.Config{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ BaseURL: "http://localhost:8000", Version: "", }, @@ -1287,9 +1287,9 @@ func TestBuildHyperfleetAPICallURL(t *testing.T) { name: "relative path with empty baseURL returns as-is", url: "clusters/abc123", execCtx: &ExecutionContext{ - Config: &config_loader.Config{ - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Config: &configloader.Config{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ BaseURL: "", Version: "v1", }, @@ -1302,9 +1302,9 @@ func TestBuildHyperfleetAPICallURL(t *testing.T) { name: "path with trailing slashes gets cleaned", url: "clusters/abc123/", execCtx: &ExecutionContext{ - Config: &config_loader.Config{ - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Config: &configloader.Config{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ BaseURL: "http://localhost:8000", Version: "v1", }, @@ -1317,9 +1317,9 @@ func TestBuildHyperfleetAPICallURL(t *testing.T) { name: "path with dot segments gets cleaned", url: "/clusters/../clusters/abc123", execCtx: &ExecutionContext{ - Config: &config_loader.Config{ - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Config: &configloader.Config{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ BaseURL: "http://localhost:8000", Version: "v1", }, @@ -1332,9 +1332,9 @@ func TestBuildHyperfleetAPICallURL(t *testing.T) { name: "absolute URL with empty baseURL returns as-is", url: "http://localhost:8000/api/hyperfleet/v1/clusters/abc123", execCtx: &ExecutionContext{ - Config: &config_loader.Config{ - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Config: &configloader.Config{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ BaseURL: "", Version: "v1", }, @@ -1347,9 +1347,9 @@ func TestBuildHyperfleetAPICallURL(t *testing.T) { name: "complex nested path", url: "clusters/abc123/statuses", execCtx: &ExecutionContext{ - Config: &config_loader.Config{ - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Config: &configloader.Config{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ BaseURL: "http://localhost:8000", Version: "v1", }, diff --git a/internal/generation/generation.go b/internal/generation/generation.go index 6814f2b..5f083e0 100644 --- a/internal/generation/generation.go +++ b/internal/generation/generation.go @@ -1,7 +1,7 @@ // Package generation provides utilities for generation-based resource tracking. // // This package handles generation annotation validation, comparison, and extraction -// for both k8s_client (Kubernetes resources) and maestro_client (ManifestWork). +// for both k8sclient (Kubernetes resources) and maestroclient (ManifestWork). package generation import ( @@ -52,7 +52,7 @@ type ApplyDecision struct { // - If generations differ: Update (apply changes) // // This function encapsulates the generation comparison logic used by both -// resource_executor (for k8s resources) and maestro_client (for ManifestWorks). +// resource_executor (for k8s resources) and maestroclient (for ManifestWorks). func CompareGenerations(newGen, existingGen int64, exists bool) ApplyDecision { if !exists { return ApplyDecision{ @@ -150,11 +150,13 @@ func ValidateGeneration(meta metav1.ObjectMeta) error { gen, err := strconv.ParseInt(genStr, 10, 64) if err != nil { - return apperrors.Validation("invalid %s annotation value %q: %v", constants.AnnotationGeneration, genStr, err).AsError() + return apperrors.Validation("invalid %s annotation value %q: %v", + constants.AnnotationGeneration, genStr, err).AsError() } if gen <= 0 { - return apperrors.Validation("%s annotation must be > 0, got %d", constants.AnnotationGeneration, gen).AsError() + return apperrors.Validation("%s annotation must be > 0, got %d", + constants.AnnotationGeneration, gen).AsError() } return nil @@ -180,21 +182,24 @@ func ValidateManifestWorkGeneration(work *workv1.ManifestWork) error { for i, m := range work.Spec.Workload.Manifests { obj := &unstructured.Unstructured{} if err := obj.UnmarshalJSON(m.Raw); err != nil { - return apperrors.Validation("ManifestWork %q manifest[%d]: failed to unmarshal: %v", work.Name, i, err).AsError() + return apperrors.Validation("ManifestWork %q manifest[%d]: failed to unmarshal: %v", + work.Name, i, err).AsError() } // Validate generation annotation exists if err := ValidateGenerationFromUnstructured(obj); err != nil { kind := obj.GetKind() name := obj.GetName() - return apperrors.Validation("ManifestWork %q manifest[%d] %s/%s: %v", work.Name, i, kind, name, err).AsError() + return apperrors.Validation("ManifestWork %q manifest[%d] %s/%s: %v", + work.Name, i, kind, name, err).AsError() } } return nil } -// ValidateGenerationFromUnstructured validates that the generation annotation exists and is valid on an Unstructured object. +// ValidateGenerationFromUnstructured validates that the generation annotation exists and is valid on an +// Unstructured object. // Returns error if: // - Object is nil // - Annotation is missing @@ -222,11 +227,13 @@ func ValidateGenerationFromUnstructured(obj *unstructured.Unstructured) error { gen, err := strconv.ParseInt(genStr, 10, 64) if err != nil { - return apperrors.Validation("invalid %s annotation value %q: %v", constants.AnnotationGeneration, genStr, err).AsError() + return apperrors.Validation("invalid %s annotation value %q: %v", + constants.AnnotationGeneration, genStr, err).AsError() } if gen <= 0 { - return apperrors.Validation("%s annotation must be > 0, got %d", constants.AnnotationGeneration, gen).AsError() + return apperrors.Validation("%s annotation must be > 0, got %d", + constants.AnnotationGeneration, gen).AsError() } return nil diff --git a/internal/hyperfleet_api/README.md b/internal/hyperfleetapi/README.md similarity index 99% rename from internal/hyperfleet_api/README.md rename to internal/hyperfleetapi/README.md index a3db631..24b0f05 100644 --- a/internal/hyperfleet_api/README.md +++ b/internal/hyperfleetapi/README.md @@ -16,7 +16,7 @@ Pure HTTP client for communicating with the HyperFleet API. Supports configurabl ### Basic Usage ```go -import "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleet_api" +import "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleetapi" // Create a client with defaults client := hyperfleet_api.NewClient() diff --git a/internal/hyperfleet_api/client.go b/internal/hyperfleetapi/client.go similarity index 93% rename from internal/hyperfleet_api/client.go rename to internal/hyperfleetapi/client.go index ba5e8e7..5625d23 100644 --- a/internal/hyperfleet_api/client.go +++ b/internal/hyperfleetapi/client.go @@ -1,13 +1,14 @@ -package hyperfleet_api +package hyperfleetapi import ( "bytes" "context" + "crypto/rand" "errors" "fmt" "io" "math" - "math/rand" + "math/big" "net/http" "os" "strings" @@ -137,7 +138,9 @@ func NewClient(log logger.Logger, opts ...ClientOption) (Client, error) { // Validate base URL is configured (either via option or env var) if c.config.BaseURL == "" { - return nil, fmt.Errorf("HyperFleet API base URL not configured: set via WithBaseURL option or %s environment variable", EnvBaseURL) + return nil, fmt.Errorf( + "HyperFleet API base URL not configured: set via WithBaseURL option or %s environment variable", + EnvBaseURL) } // Create HTTP client if not provided @@ -196,7 +199,8 @@ func (c *httpClient) Do(ctx context.Context, req *Request) (*Response, error) { for attempt := 1; attempt <= retryAttempts; attempt++ { // Check context before each attempt if err := ctx.Err(); err != nil { - return nil, apierrors.NewAPIError(req.Method, req.URL, 0, "", nil, attempt, time.Since(startTime), fmt.Errorf("context cancelled: %w", err)) + return nil, apierrors.NewAPIError(req.Method, req.URL, 0, "", nil, attempt, + time.Since(startTime), fmt.Errorf("context canceled: %w", err)) } resp, err := c.doRequest(ctx, req) @@ -225,7 +229,8 @@ func (c *httpClient) Do(ctx context.Context, req *Request) (*Response, error) { select { case <-ctx.Done(): - return nil, apierrors.NewAPIError(req.Method, req.URL, 0, "", nil, attempt, time.Since(startTime), fmt.Errorf("context cancelled during retry: %w", ctx.Err())) + return nil, apierrors.NewAPIError(req.Method, req.URL, 0, "", nil, attempt, + time.Since(startTime), fmt.Errorf("context canceled during retry: %w", ctx.Err())) case <-time.After(delay): // Continue to next attempt } @@ -336,7 +341,11 @@ func (c *httpClient) doRequest(ctx context.Context, req *Request) (*Response, er if err != nil { return nil, fmt.Errorf("HTTP request failed: %w", err) } - defer func() { _ = httpResp.Body.Close() }() + defer func() { + if closeErr := httpResp.Body.Close(); closeErr != nil { + c.log.Warnf(ctx, "Failed to close response body: %v", closeErr) + } + }() // Read response body respBody, err := io.ReadAll(httpResp.Body) @@ -378,8 +387,15 @@ func (c *httpClient) calculateBackoff(attempt int, strategy BackoffStrategy) tim } // Add jitter (ยฑ10%) to prevent thundering herd - // Using package-level rand.Float64() which is concurrency-safe (uses locked source) - jitter := time.Duration(rand.Float64()*0.2*float64(delay) - 0.1*float64(delay)) + // Using crypto/rand for secure random number generation + n, err := rand.Int(rand.Reader, big.NewInt(1000)) + if err != nil { + n = big.NewInt(500) // fallback to midpoint on error + } + jitterFrac := float64(n.Int64()) / 1000.0 // [0.0, 1.0) + jitter := time.Duration( + jitterFrac*0.2*float64(delay) - 0.1*float64(delay), + ) delay += jitter // Cap at max delay diff --git a/internal/hyperfleet_api/client_test.go b/internal/hyperfleetapi/client_test.go similarity index 95% rename from internal/hyperfleet_api/client_test.go rename to internal/hyperfleetapi/client_test.go index 9190ddc..7270d76 100644 --- a/internal/hyperfleet_api/client_test.go +++ b/internal/hyperfleetapi/client_test.go @@ -1,4 +1,4 @@ -package hyperfleet_api +package hyperfleetapi import ( "context" @@ -27,7 +27,8 @@ var ( func testLog() logger.Logger { loggerOnce.Do(func() { var err error - sharedTestLogger, err = logger.NewLogger(logger.Config{Level: "error", Format: "text", Output: "stdout", Component: "test", Version: "test"}) + cfg := logger.Config{Level: "error", Format: "text", Output: "stdout", Component: "test", Version: "test"} + sharedTestLogger, err = logger.NewLogger(cfg) if err != nil { panic(err) } @@ -224,7 +225,8 @@ func TestClientWithHeaders(t *testing.T) { })) defer server.Close() - client, err := NewClient(testLog(), WithBaseURL(server.URL), WithDefaultHeader("Authorization", "Bearer default-token")) + client, err := NewClient(testLog(), WithBaseURL(server.URL), + WithDefaultHeader("Authorization", "Bearer default-token")) require.NoError(t, err, "failed to create client") ctx := context.Background() @@ -545,9 +547,12 @@ func TestAPIError(t *testing.T) { // Test Error() method errStr := err.Error() - assert.Contains(t, errStr, "POST", "error string should contain method, got: %s", errStr) - assert.Contains(t, errStr, "503", "error string should contain status code, got: %s", errStr) - assert.Contains(t, errStr, "3 attempt", "error string should contain attempts, got: %s", errStr) + assert.Contains(t, errStr, "POST", + "error string should contain method, got: %s", errStr) + assert.Contains(t, errStr, "503", + "error string should contain status code, got: %s", errStr) + assert.Contains(t, errStr, "3 attempt", + "error string should contain attempts, got: %s", errStr) // Test helper methods if !err.IsServerError() { @@ -562,7 +567,8 @@ func TestAPIError(t *testing.T) { // Test ResponseBodyString bodyStr := err.ResponseBodyString() - assert.Contains(t, bodyStr, "backend is down", "expected response body string to contain error message, got: %s", bodyStr) + assert.Contains(t, bodyStr, "backend is down", + "expected response body string to contain error message, got: %s", bodyStr) } func TestAPIErrorStatusHelpers(t *testing.T) { @@ -640,7 +646,8 @@ func TestAPIErrorInRetryExhausted(t *testing.T) { atomic.AddInt32(&attemptCount, 1) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusServiceUnavailable) - _, _ = w.Write([]byte(`{"error":"service_unavailable","message":"backend overloaded"}`)) + body := []byte(`{"error":"service_unavailable","message":"backend overloaded"}`) + _, _ = w.Write(body) })) defer server.Close() @@ -671,7 +678,8 @@ func TestAPIErrorInRetryExhausted(t *testing.T) { if apiErr.Attempts != 2 { t.Errorf("expected 2 attempts, got %d", apiErr.Attempts) } - assert.Contains(t, apiErr.ResponseBodyString(), "backend overloaded", "expected response body to contain error message, got: %s", apiErr.ResponseBodyString()) + assert.Contains(t, apiErr.ResponseBodyString(), "backend overloaded", + "expected response body to contain error message, got: %s", apiErr.ResponseBodyString()) if !apiErr.IsServerError() { t.Error("expected IsServerError to return true") } diff --git a/internal/hyperfleet_api/mock.go b/internal/hyperfleetapi/mock.go similarity index 99% rename from internal/hyperfleet_api/mock.go rename to internal/hyperfleetapi/mock.go index 53bb940..2add0b1 100644 --- a/internal/hyperfleet_api/mock.go +++ b/internal/hyperfleetapi/mock.go @@ -1,4 +1,4 @@ -package hyperfleet_api +package hyperfleetapi import ( "context" diff --git a/internal/hyperfleet_api/types.go b/internal/hyperfleetapi/types.go similarity index 99% rename from internal/hyperfleet_api/types.go rename to internal/hyperfleetapi/types.go index 0e08152..0524be7 100644 --- a/internal/hyperfleet_api/types.go +++ b/internal/hyperfleetapi/types.go @@ -1,4 +1,4 @@ -package hyperfleet_api +package hyperfleetapi import ( "context" diff --git a/internal/k8s_client/README.md b/internal/k8sclient/README.md similarity index 98% rename from internal/k8s_client/README.md rename to internal/k8sclient/README.md index 17461ff..92c22ff 100644 --- a/internal/k8s_client/README.md +++ b/internal/k8sclient/README.md @@ -2,7 +2,7 @@ Kubernetes client wrapper for dynamic resource operations in the HyperFleet adapter framework. -**Package:** `github.com/openshift-hyperfleet/hyperfleet-adapter/internal/k8s_client` +**Package:** `github.com/openshift-hyperfleet/hyperfleet-adapter/internal/k8sclient` ## Overview @@ -308,7 +308,7 @@ Integration tests use **Testcontainers** with K3s for real Kubernetes cluster te - Fresh cluster for each test suite - Real networking and CRD support -See `test/integration/k8s_client/` for integration test examples and setup guide. +See `test/integration/k8sclient/` for integration test examples and setup guide. ## Best Practices @@ -321,6 +321,6 @@ See `test/integration/k8s_client/` for integration test examples and setup guide ## Related Packages - **`internal/executor`**: High-level resource management with templates and discovery -- **`internal/config_loader`**: Parses adapter configurations +- **`internal/configloader`**: Parses adapter configurations - **`pkg/errors`**: Error handling utilities - **`pkg/logger`**: Logging interface diff --git a/internal/k8s_client/apply.go b/internal/k8sclient/apply.go similarity index 92% rename from internal/k8s_client/apply.go rename to internal/k8sclient/apply.go index 56f23f2..f0d9d27 100644 --- a/internal/k8s_client/apply.go +++ b/internal/k8sclient/apply.go @@ -1,4 +1,4 @@ -package k8s_client +package k8sclient import ( "context" @@ -7,7 +7,7 @@ import ( "time" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/manifest" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transport_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transportclient" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -16,19 +16,19 @@ import ( // Type aliases for backward compatibility. type ( - ApplyOptions = transport_client.ApplyOptions - ApplyResult = transport_client.ApplyResult + ApplyOptions = transportclient.ApplyOptions + ApplyResult = transportclient.ApplyResult ) -// ApplyResource implements transport_client.TransportClient. +// ApplyResource implements transportclient.TransportClient. // It accepts rendered JSON/YAML bytes, parses them into an unstructured K8s resource, // discovers the existing resource by name, and applies with generation comparison. func (c *Client) ApplyResource( ctx context.Context, manifestBytes []byte, - opts *transport_client.ApplyOptions, - _ transport_client.TransportContext, -) (*transport_client.ApplyResult, error) { + opts *transportclient.ApplyOptions, + _ transportclient.TransportContext, +) (*transportclient.ApplyResult, error) { if len(manifestBytes) == 0 { return nil, fmt.Errorf("manifest bytes cannot be empty") } @@ -172,8 +172,8 @@ func (c *Client) waitForDeletion( 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()) + c.log.Warnf(ctx, "Context canceled/timed out while waiting for deletion of %s/%s", gvk.Kind, name) + return fmt.Errorf("context canceled while waiting for resource deletion: %w", ctx.Err()) case <-ticker.C: _, err := c.GetResource(ctx, gvk, namespace, name, nil) if err != nil { diff --git a/internal/k8s_client/client.go b/internal/k8sclient/client.go similarity index 92% rename from internal/k8s_client/client.go rename to internal/k8sclient/client.go index 5b6d613..d631e06 100644 --- a/internal/k8s_client/client.go +++ b/internal/k8sclient/client.go @@ -1,10 +1,10 @@ -package k8s_client +package k8sclient import ( "context" "encoding/json" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transport_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transportclient" apperrors "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/errors" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -115,7 +115,9 @@ func NewClientFromConfig(ctx context.Context, restConfig *rest.Config, log logge } // CreateResource creates a Kubernetes resource from an unstructured object -func (c *Client) CreateResource(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { +func (c *Client) CreateResource( + ctx context.Context, obj *unstructured.Unstructured, +) (*unstructured.Unstructured, error) { gvk := obj.GroupVersionKind() namespace := obj.GetNamespace() name := obj.GetName() @@ -142,7 +144,12 @@ func (c *Client) CreateResource(ctx context.Context, obj *unstructured.Unstructu } // GetResource retrieves a specific Kubernetes resource by GVK, namespace, and name -func (c *Client) GetResource(ctx context.Context, gvk schema.GroupVersionKind, namespace, name string, _ transport_client.TransportContext) (*unstructured.Unstructured, error) { +func (c *Client) GetResource( + ctx context.Context, + gvk schema.GroupVersionKind, + namespace, name string, + _ transportclient.TransportContext, +) (*unstructured.Unstructured, error) { c.log.Infof(ctx, "Getting resource: %s/%s (namespace: %s)", gvk.Kind, name, namespace) obj := &unstructured.Unstructured{} @@ -181,8 +188,14 @@ func (c *Client) GetResource(ctx context.Context, gvk schema.GroupVersionKind, n // - labelSelector: label selector string (e.g., "app=myapp,env=prod") - empty to skip // // For more flexible discovery (including by-name lookup), use DiscoverResources() instead. -func (c *Client) ListResources(ctx context.Context, gvk schema.GroupVersionKind, namespace string, labelSelector string) (*unstructured.UnstructuredList, error) { - c.log.Infof(ctx, "Listing resources: %s (namespace: %s, labelSelector: %s)", gvk.Kind, namespace, labelSelector) +func (c *Client) ListResources( + ctx context.Context, + gvk schema.GroupVersionKind, + namespace string, + labelSelector string, +) (*unstructured.UnstructuredList, error) { + c.log.Infof(ctx, "Listing resources: %s (namespace: %s, labelSelector: %s)", + gvk.Kind, namespace, labelSelector) list := &unstructured.UnstructuredList{} list.SetGroupVersionKind(gvk) @@ -215,7 +228,8 @@ func (c *Client) ListResources(ctx context.Context, gvk schema.GroupVersionKind, } } - c.log.Infof(ctx, "Successfully listed resources: %s (found %d items)", gvk.Kind, len(list.Items)) + c.log.Infof(ctx, "Successfully listed resources: %s (found %d items)", + gvk.Kind, len(list.Items)) return list, nil } @@ -240,7 +254,9 @@ func (c *Client) ListResources(ctx context.Context, gvk schema.GroupVersionKind, // resource, _ := client.GetResource(ctx, gvk, "default", "my-cm") // resource.SetLabels(map[string]string{"app": "myapp"}) // updated, err := client.UpdateResource(ctx, resource) -func (c *Client) UpdateResource(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { +func (c *Client) UpdateResource( + ctx context.Context, obj *unstructured.Unstructured, +) (*unstructured.Unstructured, error) { gvk := obj.GroupVersionKind() namespace := obj.GetNamespace() name := obj.GetName() @@ -320,7 +336,12 @@ func (c *Client) DeleteResource(ctx context.Context, gvk schema.GroupVersionKind // // patchData := []byte(`{"metadata":{"labels":{"new-label":"value"}}}`) // patched, err := client.PatchResource(ctx, gvk, "default", "my-cm", patchData) -func (c *Client) PatchResource(ctx context.Context, gvk schema.GroupVersionKind, namespace, name string, patchData []byte) (*unstructured.Unstructured, error) { +func (c *Client) PatchResource( + ctx context.Context, + gvk schema.GroupVersionKind, + namespace, name string, + patchData []byte, +) (*unstructured.Unstructured, error) { c.log.Infof(ctx, "Patching resource: %s/%s (namespace: %s)", gvk.Kind, name, namespace) // Parse patch data to validate JSON diff --git a/internal/k8s_client/client_test.go b/internal/k8sclient/client_test.go similarity index 99% rename from internal/k8s_client/client_test.go rename to internal/k8sclient/client_test.go index 756b873..47b2fd2 100644 --- a/internal/k8s_client/client_test.go +++ b/internal/k8sclient/client_test.go @@ -1,4 +1,4 @@ -package k8s_client +package k8sclient import ( "testing" diff --git a/internal/k8s_client/discovery.go b/internal/k8sclient/discovery.go similarity index 79% rename from internal/k8s_client/discovery.go rename to internal/k8sclient/discovery.go index ee01880..aefa893 100644 --- a/internal/k8s_client/discovery.go +++ b/internal/k8sclient/discovery.go @@ -1,16 +1,16 @@ -package k8s_client +package k8sclient import ( "context" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/manifest" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transport_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transportclient" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" ) // DiscoveryConfig is an alias to manifest.DiscoveryConfig for convenience. -// This allows k8s_client users to use k8s_client.DiscoveryConfig without importing manifest package. +// This allows k8sclient users to use k8sclient.DiscoveryConfig without importing manifest package. type DiscoveryConfig = manifest.DiscoveryConfig // BuildLabelSelector is an alias to manifest.BuildLabelSelector for convenience. @@ -25,12 +25,17 @@ var BuildLabelSelector = manifest.BuildLabelSelector // // Example: // -// discovery := &k8s_client.DiscoveryConfig{ +// discovery := &k8sclient.DiscoveryConfig{ // Namespace: "default", // LabelSelector: "app=myapp", // } -// list, err := client.DiscoverResources(ctx, gvk, discovery) -func (c *Client) DiscoverResources(ctx context.Context, gvk schema.GroupVersionKind, discovery manifest.Discovery, _ transport_client.TransportContext) (*unstructured.UnstructuredList, error) { +// list, err := client.DiscoverResources(ctx, gvk, discovery, nil) +func (c *Client) DiscoverResources( + ctx context.Context, + gvk schema.GroupVersionKind, + discovery manifest.Discovery, + _ transportclient.TransportContext, +) (*unstructured.UnstructuredList, error) { list := &unstructured.UnstructuredList{} list.SetGroupVersionKind(gvk) if discovery == nil { diff --git a/internal/k8s_client/interface.go b/internal/k8sclient/interface.go similarity index 74% rename from internal/k8s_client/interface.go rename to internal/k8sclient/interface.go index 7b77b69..cb885de 100644 --- a/internal/k8s_client/interface.go +++ b/internal/k8sclient/interface.go @@ -1,9 +1,9 @@ -package k8s_client +package k8sclient import ( "context" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transport_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transportclient" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -13,12 +13,12 @@ import ( // a real Kubernetes cluster or DryRun mode. // // K8sClient extends TransportClient with K8s-specific operations. -// Both k8s_client.Client and maestro_client.Client implement TransportClient, +// Both k8sclient.Client and maestroclient.Client implement TransportClient, // allowing the executor to use either as the transport backend. type K8sClient interface { // Embed TransportClient interface // This provides: ApplyResource([]byte), GetResource, DiscoverResources - transport_client.TransportClient + transportclient.TransportClient // ApplyManifest creates or updates a single Kubernetes resource based on generation comparison. // This is a K8sClient-specific method for applying parsed unstructured resources. @@ -28,7 +28,12 @@ type K8sClient interface { // If it exists and the generation matches, it skips the update (idempotent). // // The manifest must have the hyperfleet.io/generation annotation set. - ApplyManifest(ctx context.Context, newManifest *unstructured.Unstructured, existing *unstructured.Unstructured, opts *ApplyOptions) (*ApplyResult, error) + ApplyManifest( + ctx context.Context, + newManifest *unstructured.Unstructured, + existing *unstructured.Unstructured, + opts *ApplyOptions, + ) (*ApplyResult, error) // Resource CRUD operations @@ -38,14 +43,21 @@ type K8sClient interface { // UpdateResource updates an existing Kubernetes resource. // The resource must have resourceVersion set for optimistic concurrency. - UpdateResource(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) + UpdateResource( + ctx context.Context, + obj *unstructured.Unstructured, + ) (*unstructured.Unstructured, error) // DeleteResource deletes a Kubernetes resource by GVK, namespace, and name. - DeleteResource(ctx context.Context, gvk schema.GroupVersionKind, namespace, name string) error + DeleteResource( + ctx context.Context, + gvk schema.GroupVersionKind, + namespace, name string, + ) error } // Ensure Client implements K8sClient interface var _ K8sClient = (*Client)(nil) // Ensure Client implements TransportClient interface -var _ transport_client.TransportClient = (*Client)(nil) +var _ transportclient.TransportClient = (*Client)(nil) diff --git a/internal/k8s_client/mock.go b/internal/k8sclient/mock.go similarity index 69% rename from internal/k8s_client/mock.go rename to internal/k8sclient/mock.go index 0e4c6be..d9cad80 100644 --- a/internal/k8s_client/mock.go +++ b/internal/k8sclient/mock.go @@ -1,14 +1,16 @@ -package k8s_client +package k8sclient import ( "context" "encoding/json" + "fmt" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/manifest" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transport_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transportclient" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/yaml" ) // MockK8sClient implements K8sClient for testing. @@ -42,7 +44,12 @@ func NewMockK8sClient() *MockK8sClient { // GetResource implements K8sClient.GetResource // Returns a NotFound error when the resource doesn't exist, matching real K8s client behavior. -func (m *MockK8sClient) GetResource(ctx context.Context, gvk schema.GroupVersionKind, namespace, name string, _ transport_client.TransportContext) (*unstructured.Unstructured, error) { +func (m *MockK8sClient) GetResource( + ctx context.Context, + gvk schema.GroupVersionKind, + namespace, name string, + _ transportclient.TransportContext, +) (*unstructured.Unstructured, error) { // Check explicit error override first if m.GetResourceError != nil { return nil, m.GetResourceError @@ -62,7 +69,10 @@ func (m *MockK8sClient) GetResource(ctx context.Context, gvk schema.GroupVersion } // CreateResource implements K8sClient.CreateResource -func (m *MockK8sClient) CreateResource(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { +func (m *MockK8sClient) CreateResource( + ctx context.Context, + obj *unstructured.Unstructured, +) (*unstructured.Unstructured, error) { if m.CreateResourceError != nil { return nil, m.CreateResourceError } @@ -76,7 +86,10 @@ func (m *MockK8sClient) CreateResource(ctx context.Context, obj *unstructured.Un } // UpdateResource implements K8sClient.UpdateResource -func (m *MockK8sClient) UpdateResource(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { +func (m *MockK8sClient) UpdateResource( + ctx context.Context, + obj *unstructured.Unstructured, +) (*unstructured.Unstructured, error) { if m.UpdateResourceError != nil { return nil, m.UpdateResourceError } @@ -90,7 +103,11 @@ func (m *MockK8sClient) UpdateResource(ctx context.Context, obj *unstructured.Un } // DeleteResource implements K8sClient.DeleteResource -func (m *MockK8sClient) DeleteResource(ctx context.Context, gvk schema.GroupVersionKind, namespace, name string) error { +func (m *MockK8sClient) DeleteResource( + ctx context.Context, + gvk schema.GroupVersionKind, + namespace, name string, +) error { if m.DeleteResourceError != nil { return m.DeleteResourceError } @@ -101,7 +118,12 @@ func (m *MockK8sClient) DeleteResource(ctx context.Context, gvk schema.GroupVers } // ApplyManifest implements K8sClient.ApplyManifest -func (m *MockK8sClient) ApplyManifest(ctx context.Context, newManifest *unstructured.Unstructured, existing *unstructured.Unstructured, opts *ApplyOptions) (*ApplyResult, error) { +func (m *MockK8sClient) ApplyManifest( + ctx context.Context, + newManifest *unstructured.Unstructured, + existing *unstructured.Unstructured, + opts *ApplyOptions, +) (*ApplyResult, error) { if m.ApplyManifestError != nil { return nil, m.ApplyManifestError } @@ -117,29 +139,47 @@ func (m *MockK8sClient) ApplyManifest(ctx context.Context, newManifest *unstruct }, nil } -// ApplyResource implements transport_client.TransportClient.ApplyResource -func (m *MockK8sClient) ApplyResource(ctx context.Context, manifestBytes []byte, opts *transport_client.ApplyOptions, _ transport_client.TransportContext) (*transport_client.ApplyResult, error) { +// ApplyResource implements transportclient.TransportClient.ApplyResource +func (m *MockK8sClient) ApplyResource( + ctx context.Context, + manifestBytes []byte, + opts *transportclient.ApplyOptions, + _ transportclient.TransportContext, +) (*transportclient.ApplyResult, error) { if m.ApplyResourceError != nil { return nil, m.ApplyResourceError } if m.ApplyResourceResult != nil { return m.ApplyResourceResult, nil } - // Default behavior: parse and store + // Default behavior: parse and store (try JSON first, fall back to YAML) obj := &unstructured.Unstructured{} if err := json.Unmarshal(manifestBytes, &obj.Object); err != nil { - return nil, err + jsonBytes, yamlErr := yaml.YAMLToJSON(manifestBytes) + if yamlErr != nil { + return nil, fmt.Errorf( + "failed to parse manifest as JSON or YAML: %w", err) + } + if err := json.Unmarshal(jsonBytes, &obj.Object); err != nil { + return nil, fmt.Errorf( + "failed to parse manifest after YAML conversion: %w", err) + } } key := obj.GetNamespace() + "/" + obj.GetName() m.Resources[key] = obj - return &transport_client.ApplyResult{ + return &transportclient.ApplyResult{ Operation: manifest.OperationCreate, Reason: "mock apply", }, nil } // DiscoverResources implements K8sClient.DiscoverResources -func (m *MockK8sClient) DiscoverResources(ctx context.Context, gvk schema.GroupVersionKind, discovery manifest.Discovery, _ transport_client.TransportContext) (*unstructured.UnstructuredList, error) { +func (m *MockK8sClient) DiscoverResources( + ctx context.Context, + gvk schema.GroupVersionKind, + discovery manifest.Discovery, + _ transportclient.TransportContext, +) (*unstructured.UnstructuredList, error) { if m.DiscoverError != nil { return nil, m.DiscoverError } diff --git a/internal/k8s_client/test_helpers_test.go b/internal/k8sclient/test_helpers_test.go similarity index 58% rename from internal/k8s_client/test_helpers_test.go rename to internal/k8sclient/test_helpers_test.go index 9b1623c..7348fd4 100644 --- a/internal/k8s_client/test_helpers_test.go +++ b/internal/k8sclient/test_helpers_test.go @@ -1,4 +1,4 @@ -package k8s_client +package k8sclient // This file contains test-only helpers and constants. // DO NOT use these in production code - they are for testing purposes only. @@ -13,13 +13,13 @@ import ( // Production code must extract GVK from config using GVKFromKindAndAPIVersion(). // // Available only in test builds for: -// - Unit tests (internal/k8s_client/*_test.go) -// - Integration tests (test/integration/k8s_client/*_test.go) +// - Unit tests (internal/k8sclient/*_test.go) +// - Integration tests (test/integration/k8sclient/*_test.go) // // Example usage in tests: // // ns := &unstructured.Unstructured{...} -// ns.SetGroupVersionKind(k8s_client.CommonResourceKinds.Namespace) +// ns.SetGroupVersionKind(k8sclient.CommonResourceKinds.Namespace) // _, err := client.CreateResource(ctx, ns) var CommonResourceKinds = struct { // Core resources (v1) @@ -55,13 +55,15 @@ var CommonResourceKinds = struct { StorageClass schema.GroupVersionKind }{ // Core v1 - Namespace: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"}, - Pod: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, - Service: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"}, - ConfigMap: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"}, - Secret: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"}, - ServiceAccount: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ServiceAccount"}, - PersistentVolumeClaim: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "PersistentVolumeClaim"}, + Namespace: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"}, + Pod: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, + Service: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"}, + ConfigMap: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"}, + Secret: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"}, + ServiceAccount: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ServiceAccount"}, + PersistentVolumeClaim: schema.GroupVersionKind{ + Group: "", Version: "v1", Kind: "PersistentVolumeClaim", + }, // Apps v1 Deployment: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, @@ -74,15 +76,29 @@ var CommonResourceKinds = struct { CronJob: schema.GroupVersionKind{Group: "batch", Version: "v1", Kind: "CronJob"}, // RBAC v1 - Role: schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "Role"}, - RoleBinding: schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBinding"}, - ClusterRole: schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole"}, - ClusterRoleBinding: schema.GroupVersionKind{Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding"}, + Role: schema.GroupVersionKind{ + Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "Role", + }, + RoleBinding: schema.GroupVersionKind{ + Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "RoleBinding", + }, + ClusterRole: schema.GroupVersionKind{ + Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole", + }, + ClusterRoleBinding: schema.GroupVersionKind{ + Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRoleBinding", + }, // Networking v1 - Ingress: schema.GroupVersionKind{Group: "networking.k8s.io", Version: "v1", Kind: "Ingress"}, - NetworkPolicy: schema.GroupVersionKind{Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicy"}, + Ingress: schema.GroupVersionKind{ + Group: "networking.k8s.io", Version: "v1", Kind: "Ingress", + }, + NetworkPolicy: schema.GroupVersionKind{ + Group: "networking.k8s.io", Version: "v1", Kind: "NetworkPolicy", + }, // Storage v1 - StorageClass: schema.GroupVersionKind{Group: "storage.k8s.io", Version: "v1", Kind: "StorageClass"}, + StorageClass: schema.GroupVersionKind{ + Group: "storage.k8s.io", Version: "v1", Kind: "StorageClass", + }, } diff --git a/internal/k8s_client/types.go b/internal/k8sclient/types.go similarity index 98% rename from internal/k8s_client/types.go rename to internal/k8sclient/types.go index fdfa878..074be5b 100644 --- a/internal/k8s_client/types.go +++ b/internal/k8sclient/types.go @@ -1,4 +1,4 @@ -package k8s_client +package k8sclient import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" diff --git a/internal/k8s_client/types_test.go b/internal/k8sclient/types_test.go similarity index 99% rename from internal/k8s_client/types_test.go rename to internal/k8sclient/types_test.go index 79f59a0..b1e340c 100644 --- a/internal/k8s_client/types_test.go +++ b/internal/k8sclient/types_test.go @@ -1,4 +1,4 @@ -package k8s_client +package k8sclient import ( "testing" diff --git a/internal/maestro_client/client.go b/internal/maestroclient/client.go similarity index 89% rename from internal/maestro_client/client.go rename to internal/maestroclient/client.go index 46da488..486338b 100644 --- a/internal/maestro_client/client.go +++ b/internal/maestroclient/client.go @@ -1,4 +1,4 @@ -package maestro_client +package maestroclient import ( "context" @@ -9,11 +9,12 @@ import ( "net/http" "net/url" "os" + "path/filepath" "strings" "time" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/manifest" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transport_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/transportclient" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/constants" apperrors "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/errors" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" @@ -49,15 +50,18 @@ type Client struct { // Following the official Maestro client pattern: // https://github.com/openshift-online/maestro/blob/main/examples/manifestwork/client.go type Config struct { - // MaestroServerAddr is the Maestro HTTP API server address (e.g., "https://maestro.example.com:8000") + // MaestroServerAddr is the Maestro HTTP API server address + // (e.g., "https://maestro.example.com:8000") // This is used for the OpenAPI client to communicate with Maestro's REST API MaestroServerAddr string - // GRPCServerAddr is the Maestro gRPC server address (e.g., "maestro-grpc.example.com:8090") + // GRPCServerAddr is the Maestro gRPC server address + // (e.g., "maestro-grpc.example.com:8090") // This is used for CloudEvents communication GRPCServerAddr string - // SourceID is a unique identifier for this client (used for CloudEvents routing) + // SourceID is a unique identifier for this client + // (used for CloudEvents routing) // This identifies the source of ManifestWork operations SourceID string @@ -68,7 +72,8 @@ type Config struct { ClientCertFile string // ClientKeyFile is the path to the client key file for mutual TLS (gRPC) ClientKeyFile string - // TokenFile is the path to a token file for token-based authentication (alternative to cert auth) + // TokenFile is the path to a token file for token-based authentication + // (alternative to cert auth) TokenFile string // TLS Configuration for HTTP API (optional - may use different CA than gRPC) @@ -83,7 +88,8 @@ type Config struct { // HTTPTimeout is the timeout for HTTP requests to Maestro API (default: 10s) HTTPTimeout time.Duration - // ServerHealthinessTimeout is the timeout for gRPC server health checks (default: 20s) + // ServerHealthinessTimeout is the timeout for gRPC server health checks + // (default: 20s) ServerHealthinessTimeout time.Duration } @@ -117,7 +123,8 @@ func NewMaestroClient(ctx context.Context, config *Config, log logger.Logger) (* if err != nil { return nil, apperrors.ConfigurationError("invalid MaestroServerAddr URL: %v", err) } - // Require http or https scheme (reject schemeless or other schemes like ftp://, grpc://, etc.) + // Require http or https scheme + // (reject schemeless or other schemes like ftp://, grpc://, etc.) if serverURL.Scheme != "http" && serverURL.Scheme != "https" { return nil, apperrors.ConfigurationError( "MaestroServerAddr must use http:// or https:// scheme (got scheme %q in %q)", @@ -217,8 +224,8 @@ func NewMaestroClient(ctx context.Context, config *Config, log logger.Logger) (* } // createHTTPTransport creates an HTTP transport with appropriate TLS configuration. -// It clones http.DefaultTransport to preserve important defaults like ProxyFromEnvironment, -// connection pooling, timeouts, etc., and only overrides TLS settings. +// It clones http.DefaultTransport to preserve important defaults like +// ProxyFromEnvironment, connection pooling, timeouts, etc., and only overrides TLS settings. func createHTTPTransport(config *Config) (*http.Transport, error) { // Clone default transport to preserve ProxyFromEnvironment, DialContext, // MaxIdleConns, IdleConnTimeout, TLSHandshakeTimeout, etc. @@ -234,8 +241,9 @@ func createHTTPTransport(config *Config) (*http.Transport, error) { } if config.Insecure { - // Insecure mode: skip TLS verification (works for both http:// and https://) - tlsConfig.InsecureSkipVerify = true //nolint:gosec // Intentional: user explicitly set Insecure=true + // Insecure mode: skip TLS verification + // (works for both http:// and https://) + tlsConfig.InsecureSkipVerify = true //nolint:gosec // Intentional } else { // Secure mode: load CA certificate if provided // HTTPCAFile takes precedence, falls back to CAFile for backwards compatibility @@ -245,13 +253,14 @@ func createHTTPTransport(config *Config) (*http.Transport, error) { } if httpCAFile != "" { - caCert, err := os.ReadFile(httpCAFile) + caCert, err := os.ReadFile(filepath.Clean(httpCAFile)) if err != nil { return nil, err } 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", httpCAFile).AsError() } tlsConfig.RootCAs = caCertPool } @@ -371,13 +380,16 @@ func configureTLS(config *Config, grpcOptions *grpcopts.GRPCOptions) error { // Fail fast: Insecure=false but no TLS configuration was provided // This prevents silently falling back to plaintext connections - return fmt.Errorf("no TLS configuration provided: set CAFile (with optional ClientCertFile/ClientKeyFile or TokenFile) or set Insecure=true for plaintext connections") + return fmt.Errorf( + "no TLS configuration provided: set CAFile " + + "(with optional ClientCertFile/ClientKeyFile or TokenFile) " + + "or set Insecure=true for plaintext connections") } // readTokenFile reads a token from a file and trims whitespace. // Returns an error if the file is empty or contains only whitespace. func readTokenFile(path string) (string, error) { - token, err := os.ReadFile(path) + token, err := os.ReadFile(filepath.Clean(path)) if err != nil { return "", err } @@ -414,9 +426,12 @@ type TransportContext struct { ConsumerName string } -// resolveTransportContext extracts the maestro TransportContext from the generic transport context. +// resolveTransportContext extracts the maestro TransportContext +// from the generic transport context. // Returns nil if target is nil or wrong type. -func (c *Client) resolveTransportContext(target transport_client.TransportContext) *TransportContext { +func (c *Client) resolveTransportContext( + target transportclient.TransportContext, +) *TransportContext { if target == nil { return nil } @@ -431,18 +446,18 @@ func (c *Client) resolveTransportContext(target transport_client.TransportContex // TransportClient Interface Implementation // ============================================================================= -// Ensure Client implements transport_client.TransportClient -var _ transport_client.TransportClient = (*Client)(nil) +// Ensure Client implements transportclient.TransportClient +var _ transportclient.TransportClient = (*Client)(nil) // ApplyResource applies a rendered ManifestWork (JSON/YAML bytes) to the target cluster. // It parses the bytes into a ManifestWork, then applies it via Maestro gRPC. -// Requires a *maestro_client.TransportContext with ConsumerName. +// Requires a *maestroclient.TransportContext with ConsumerName. func (c *Client) ApplyResource( ctx context.Context, manifestBytes []byte, - opts *transport_client.ApplyOptions, - target transport_client.TransportContext, -) (*transport_client.ApplyResult, error) { + opts *transportclient.ApplyOptions, + target transportclient.TransportContext, +) (*transportclient.ApplyResult, error) { if len(manifestBytes) == 0 { return nil, fmt.Errorf("manifest bytes cannot be empty") } @@ -450,12 +465,16 @@ func (c *Client) ApplyResource( // Resolve maestro transport context transportCtx := c.resolveTransportContext(target) if transportCtx == nil { - return nil, fmt.Errorf("maestro TransportContext is required: pass *maestro_client.TransportContext as target") + return nil, fmt.Errorf( + "maestro TransportContext is required: " + + "pass *maestroclient.TransportContext as target") } consumerName := transportCtx.ConsumerName if consumerName == "" { - return nil, fmt.Errorf("consumer name (target cluster) is required: set TransportContext.ConsumerName") + return nil, fmt.Errorf( + "consumer name (target cluster) is required: " + + "set TransportContext.ConsumerName") } ctx = logger.WithMaestroConsumer(ctx, consumerName) @@ -477,7 +496,7 @@ func (c *Client) ApplyResource( return nil, fmt.Errorf("failed to apply ManifestWork: %w", err) } - return &transport_client.ApplyResult{ + return &transportclient.ApplyResult{ Operation: result.Operation, Reason: result.Reason, }, nil @@ -488,7 +507,7 @@ func (c *Client) GetResource( ctx context.Context, gvk schema.GroupVersionKind, namespace, name string, - target transport_client.TransportContext, + target transportclient.TransportContext, ) (*unstructured.Unstructured, error) { transportCtx := c.resolveTransportContext(target) consumerName := "" @@ -503,7 +522,8 @@ func (c *Client) GetResource( ctx = logger.WithMaestroConsumer(ctx, consumerName) // If the GVK is ManifestWork, get the ManifestWork object directly - if gvk.Kind == constants.ManifestWorkKind && gvk.Group == constants.ManifestWorkGroup { + if gvk.Kind == constants.ManifestWorkKind && + gvk.Group == constants.ManifestWorkGroup { work, err := c.GetManifestWork(ctx, consumerName, name) if err != nil { return nil, err @@ -544,7 +564,7 @@ func (c *Client) DiscoverResources( ctx context.Context, gvk schema.GroupVersionKind, discovery manifest.Discovery, - target transport_client.TransportContext, + target transportclient.TransportContext, ) (*unstructured.UnstructuredList, error) { transportCtx := c.resolveTransportContext(target) consumerName := "" @@ -565,8 +585,10 @@ func (c *Client) DiscoverResources( allItems := &unstructured.UnstructuredList{} - // If discovering ManifestWork objects themselves, match against the top-level objects - if gvk.Kind == constants.ManifestWorkKind && gvk.Group == constants.ManifestWorkGroup { + // If discovering ManifestWork objects themselves, + // match against the top-level objects + if gvk.Kind == constants.ManifestWorkKind && + gvk.Group == constants.ManifestWorkGroup { for i := range workList.Items { workUnstructured, err := workToUnstructured(&workList.Items[i]) if err != nil { diff --git a/internal/maestro_client/client_test.go b/internal/maestroclient/client_test.go similarity index 99% rename from internal/maestro_client/client_test.go rename to internal/maestroclient/client_test.go index 473c0df..8fdee45 100644 --- a/internal/maestro_client/client_test.go +++ b/internal/maestroclient/client_test.go @@ -1,4 +1,4 @@ -package maestro_client +package maestroclient import ( "encoding/json" diff --git a/internal/maestro_client/interface.go b/internal/maestroclient/interface.go similarity index 68% rename from internal/maestro_client/interface.go rename to internal/maestroclient/interface.go index 795ce8d..aca623c 100644 --- a/internal/maestro_client/interface.go +++ b/internal/maestroclient/interface.go @@ -1,4 +1,4 @@ -package maestro_client +package maestroclient import ( "context" @@ -21,23 +21,44 @@ type ApplyManifestWorkResult struct { // This interface enables easier testing through mocking. type ManifestWorkClient interface { // CreateManifestWork creates a new ManifestWork for a target cluster (consumer) - CreateManifestWork(ctx context.Context, consumerName string, work *workv1.ManifestWork) (*workv1.ManifestWork, error) + CreateManifestWork( + ctx context.Context, + consumerName string, + work *workv1.ManifestWork, + ) (*workv1.ManifestWork, error) // GetManifestWork retrieves a ManifestWork by name from a target cluster - GetManifestWork(ctx context.Context, consumerName string, workName string) (*workv1.ManifestWork, error) + GetManifestWork( + ctx context.Context, + consumerName string, + workName string, + ) (*workv1.ManifestWork, error) // ApplyManifestWork creates or updates a ManifestWork (upsert operation). // Returns the result including the actual operation performed (create, update, or skip). - ApplyManifestWork(ctx context.Context, consumerName string, work *workv1.ManifestWork) (*ApplyManifestWorkResult, error) + ApplyManifestWork( + ctx context.Context, + consumerName string, + work *workv1.ManifestWork, + ) (*ApplyManifestWorkResult, error) // DeleteManifestWork deletes a ManifestWork from a target cluster DeleteManifestWork(ctx context.Context, consumerName string, workName string) error // ListManifestWorks lists all ManifestWorks for a target cluster - ListManifestWorks(ctx context.Context, consumerName string, labelSelector string) (*workv1.ManifestWorkList, error) + ListManifestWorks( + ctx context.Context, + consumerName string, + labelSelector string, + ) (*workv1.ManifestWorkList, error) // PatchManifestWork patches an existing ManifestWork using JSON merge patch - PatchManifestWork(ctx context.Context, consumerName string, workName string, patchData []byte) (*workv1.ManifestWork, error) + PatchManifestWork( + ctx context.Context, + consumerName string, + workName string, + patchData []byte, + ) (*workv1.ManifestWork, error) } // Ensure Client implements ManifestWorkClient diff --git a/internal/maestro_client/ocm_logger_adapter.go b/internal/maestroclient/ocm_logger_adapter.go similarity index 99% rename from internal/maestro_client/ocm_logger_adapter.go rename to internal/maestroclient/ocm_logger_adapter.go index 7698d70..0f4c6ce 100644 --- a/internal/maestro_client/ocm_logger_adapter.go +++ b/internal/maestroclient/ocm_logger_adapter.go @@ -1,4 +1,4 @@ -package maestro_client +package maestroclient import ( "context" diff --git a/internal/maestro_client/operations.go b/internal/maestroclient/operations.go similarity index 99% rename from internal/maestro_client/operations.go rename to internal/maestroclient/operations.go index f1f5962..e5300fa 100644 --- a/internal/maestro_client/operations.go +++ b/internal/maestroclient/operations.go @@ -1,4 +1,4 @@ -package maestro_client +package maestroclient import ( "context" @@ -279,7 +279,7 @@ func createManifestWorkPatch(work *workv1.ManifestWork) ([]byte, error) { } // DiscoverManifest finds manifests within a ManifestWork that match the discovery criteria. -// This is the maestro_client equivalent of k8s_client.DiscoverResources. +// This is the maestroclient equivalent of k8sclient.DiscoverResources. // // Parameters: // - ctx: Context for the operation diff --git a/internal/maestro_client/operations_test.go b/internal/maestroclient/operations_test.go similarity index 94% rename from internal/maestro_client/operations_test.go rename to internal/maestroclient/operations_test.go index 8931a63..a5031ab 100644 --- a/internal/maestro_client/operations_test.go +++ b/internal/maestroclient/operations_test.go @@ -1,9 +1,9 @@ -// Package maestro_client tests +// Package maestroclient tests // // Note: Tests for manifest.ValidateGeneration, manifest.ValidateGenerationFromUnstructured, // and manifest.ValidateManifestWorkGeneration are in internal/generation/generation_test.go. -// This file contains tests specific to maestro_client functionality. -package maestro_client +// This file contains tests specific to maestroclient functionality. +package maestroclient import ( "fmt" @@ -32,8 +32,10 @@ func TestIsConsumerNotFoundError(t *testing.T) { expected: false, }, { - name: "FK constraint error is detected", - err: fmt.Errorf(`pq: insert or update on table "resources" violates foreign key constraint "fk_resources_consumers"`), + name: "FK constraint error is detected", + err: fmt.Errorf( + `pq: insert or update on table "resources" ` + + `violates foreign key constraint "fk_resources_consumers"`), expected: true, }, { diff --git a/internal/manifest/generation.go b/internal/manifest/generation.go index 2273316..825acfb 100644 --- a/internal/manifest/generation.go +++ b/internal/manifest/generation.go @@ -1,4 +1,5 @@ -// Package manifest provides utilities for Kubernetes manifest validation, generation tracking, and discovery. +// Package manifest provides utilities for Kubernetes manifest validation, +// generation tracking, and discovery. // // This package handles: // - Manifest validation (apiVersion, kind, name, generation annotation) @@ -6,7 +7,7 @@ // - ManifestWork validation for OCM // - Discovery interface for finding resources/manifests // -// Used by both k8s_client (Kubernetes resources) and maestro_client (ManifestWork). +// Used by both k8sclient (Kubernetes resources) and maestroclient (ManifestWork). package manifest import ( @@ -58,7 +59,7 @@ type ApplyDecision struct { // - If generations differ: Update (apply changes) // // This function encapsulates the generation comparison logic used by both -// resource_executor (for k8s resources) and maestro_client (for ManifestWorks). +// resource_executor (for k8s resources) and maestroclient (for ManifestWorks). func CompareGenerations(newGen, existingGen int64, exists bool) ApplyDecision { if !exists { return ApplyDecision{ @@ -111,7 +112,8 @@ func GetGeneration(meta metav1.ObjectMeta) int64 { return gen } -// GetGenerationFromUnstructured is a convenience wrapper for getting generation from unstructured.Unstructured. +// GetGenerationFromUnstructured is a convenience wrapper for getting generation +// from unstructured.Unstructured. // Returns 0 if the resource is nil, has no annotations, or the annotation cannot be parsed. func GetGenerationFromUnstructured(obj *unstructured.Unstructured) int64 { if obj == nil { @@ -132,7 +134,8 @@ func GetGenerationFromUnstructured(obj *unstructured.Unstructured) int64 { return gen } -// ValidateGeneration validates that the generation annotation exists and is valid on ObjectMeta. +// ValidateGeneration validates that the generation annotation exists and is valid +// on ObjectMeta. // Returns error if: // - Annotation is missing // - Annotation value is empty @@ -142,12 +145,14 @@ func GetGenerationFromUnstructured(obj *unstructured.Unstructured) int64 { // This is used to validate that templates properly set the generation annotation. func ValidateGeneration(meta metav1.ObjectMeta) error { if meta.Annotations == nil { - return apperrors.Validation("missing %s annotation", constants.AnnotationGeneration).AsError() + return apperrors.Validation( + "missing %s annotation", constants.AnnotationGeneration).AsError() } genStr, ok := meta.Annotations[constants.AnnotationGeneration] if !ok { - return apperrors.Validation("missing %s annotation", constants.AnnotationGeneration).AsError() + return apperrors.Validation( + "missing %s annotation", constants.AnnotationGeneration).AsError() } if genStr == "" { @@ -156,7 +161,9 @@ func ValidateGeneration(meta metav1.ObjectMeta) error { gen, err := strconv.ParseInt(genStr, 10, 64) if err != nil { - return apperrors.Validation("invalid %s annotation value %q: %v", constants.AnnotationGeneration, genStr, err).AsError() + return apperrors.Validation( + "invalid %s annotation value %q: %v", constants.AnnotationGeneration, genStr, err, + ).AsError() } if gen <= 0 { @@ -200,7 +207,8 @@ func ValidateManifestWorkGeneration(work *workv1.ManifestWork) error { return nil } -// ValidateGenerationFromUnstructured validates that the generation annotation exists and is valid on an Unstructured object. +// ValidateGenerationFromUnstructured validates that the generation annotation exists +// and is valid on an Unstructured object. // Returns error if: // - Object is nil // - Annotation is missing @@ -214,31 +222,39 @@ func ValidateGenerationFromUnstructured(obj *unstructured.Unstructured) error { annotations := obj.GetAnnotations() if annotations == nil { - return apperrors.Validation("missing %s annotation", constants.AnnotationGeneration).AsError() + return apperrors.Validation( + "missing %s annotation", constants.AnnotationGeneration).AsError() } genStr, ok := annotations[constants.AnnotationGeneration] if !ok { - return apperrors.Validation("missing %s annotation", constants.AnnotationGeneration).AsError() + return apperrors.Validation( + "missing %s annotation", constants.AnnotationGeneration).AsError() } if genStr == "" { - return apperrors.Validation("%s annotation is empty", constants.AnnotationGeneration).AsError() + return apperrors.Validation( + "%s annotation is empty", constants.AnnotationGeneration).AsError() } gen, err := strconv.ParseInt(genStr, 10, 64) if err != nil { - return apperrors.Validation("invalid %s annotation value %q: %v", constants.AnnotationGeneration, genStr, err).AsError() + return apperrors.Validation( + "invalid %s annotation value %q: %v", + constants.AnnotationGeneration, genStr, err).AsError() } if gen <= 0 { - return apperrors.Validation("%s annotation must be > 0, got %d", constants.AnnotationGeneration, gen).AsError() + return apperrors.Validation( + "%s annotation must be > 0, got %d", + constants.AnnotationGeneration, gen).AsError() } return nil } -// GetLatestGenerationFromList returns the resource with the highest generation annotation from a list. +// 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. @@ -273,7 +289,8 @@ func GetLatestGenerationFromList(list *unstructured.UnstructuredList) *unstructu // ============================================================================= // Discovery defines the interface for resource/manifest discovery configuration. -// This interface is used by both k8s_client (for K8s resources) and maestro_client (for ManifestWork manifests). +// This interface is used by both k8sclient (for K8s resources) +// and maestroclient (for ManifestWork manifests). type Discovery interface { // GetNamespace returns the namespace to search in. // Empty string means cluster-scoped or all namespaces. @@ -283,7 +300,8 @@ type Discovery interface { // Empty string means use selector-based discovery. GetName() string - // GetLabelSelector returns the label selector string (e.g., "app=myapp,env=prod"). + // GetLabelSelector returns the label selector string + // (e.g., "app=myapp,env=prod"). // Empty string means no label filtering. GetLabelSelector() string @@ -292,7 +310,7 @@ type Discovery interface { } // DiscoveryConfig is the default implementation of the Discovery interface. -// Used by both k8s_client and maestro_client for consistent discovery configuration. +// Used by both k8sclient and maestroclient for consistent discovery configuration. type DiscoveryConfig struct { // Namespace to search in (empty for cluster-scoped or all namespaces) Namespace string @@ -375,8 +393,8 @@ func MatchesLabels(obj *unstructured.Unstructured, labelSelector string) bool { return true } -// DiscoverNestedManifest finds manifests within a parent resource (e.g., ManifestWork) that match -// the discovery criteria. The parent is expected to contain nested manifests at +// DiscoverNestedManifest finds manifests within a parent resource (e.g., ManifestWork) +// that match the discovery criteria. The parent is expected to contain nested manifests at // spec.workload.manifests (OCM ManifestWork structure). // // Parameters: @@ -386,7 +404,10 @@ func MatchesLabels(obj *unstructured.Unstructured, labelSelector string) bool { // Returns: // - List of matching manifests as unstructured objects // - The manifest with the highest generation if multiple match -func DiscoverNestedManifest(parent *unstructured.Unstructured, discovery Discovery) (*unstructured.UnstructuredList, error) { +func DiscoverNestedManifest( + parent *unstructured.Unstructured, + discovery Discovery, +) (*unstructured.UnstructuredList, error) { list := &unstructured.UnstructuredList{} if parent == nil || discovery == nil { @@ -394,9 +415,11 @@ func DiscoverNestedManifest(parent *unstructured.Unstructured, discovery Discove } // Extract spec.workload.manifests from the unstructured parent - manifests, found, err := unstructured.NestedSlice(parent.Object, "spec", "workload", "manifests") + manifests, found, err := unstructured.NestedSlice( + parent.Object, "spec", "workload", "manifests") if err != nil { - return nil, apperrors.Validation("failed to extract spec.workload.manifests from %q: %v", + return nil, apperrors.Validation( + "failed to extract spec.workload.manifests from %q: %v", parent.GetName(), err) } if !found { @@ -431,7 +454,8 @@ func EnrichWithResourceStatus(parent, nested *unstructured.Unstructured) { return } - statusManifests, found, err := unstructured.NestedSlice(parent.Object, "status", "resourceStatus", "manifests") + statusManifests, found, err := unstructured.NestedSlice( + parent.Object, "status", "resourceStatus", "manifests") if err != nil || !found { return } @@ -471,7 +495,8 @@ func EnrichWithResourceStatus(parent, nested *unstructured.Unstructured) { } } -// MatchesDiscoveryCriteria checks if a resource matches the discovery criteria (namespace, name, or labels). +// MatchesDiscoveryCriteria checks if a resource matches the discovery criteria +// (namespace, name, or labels). func MatchesDiscoveryCriteria(obj *unstructured.Unstructured, discovery Discovery) bool { // Check namespace if specified if ns := discovery.GetNamespace(); ns != "" && obj.GetNamespace() != ns { diff --git a/internal/manifest/manifestwork.go b/internal/manifest/manifestwork.go index 8f5a7fe..e5de962 100644 --- a/internal/manifest/manifestwork.go +++ b/internal/manifest/manifestwork.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" workv1 "open-cluster-management.io/api/work/v1" "sigs.k8s.io/yaml" @@ -34,7 +35,7 @@ func ParseManifestWork(data []byte) (*workv1.ManifestWork, error) { // LoadManifestWork reads a ManifestWork from a file path (JSON or YAML). func LoadManifestWork(path string) (*workv1.ManifestWork, error) { - data, err := os.ReadFile(path) + data, err := os.ReadFile(filepath.Clean(path)) if err != nil { return nil, fmt.Errorf("failed to read ManifestWork file %s: %w", path, err) } diff --git a/internal/transport_client/interface.go b/internal/transportclient/interface.go similarity index 64% rename from internal/transport_client/interface.go rename to internal/transportclient/interface.go index 5a49920..80e4e79 100644 --- a/internal/transport_client/interface.go +++ b/internal/transportclient/interface.go @@ -1,4 +1,4 @@ -package transport_client +package transportclient import ( "context" @@ -11,8 +11,8 @@ import ( // TransportClient defines the interface for applying Kubernetes resources. // This interface abstracts the underlying implementation, allowing resources // to be applied via different backends: -// - Direct Kubernetes API (k8s_client) -// - Maestro/OCM ManifestWork (maestro_client) +// - Direct Kubernetes API (k8sclient) +// - Maestro/OCM ManifestWork (maestroclient) // // All implementations must support generation-aware apply operations: // - Create if resource doesn't exist @@ -21,8 +21,8 @@ import ( type TransportClient interface { // ApplyResource applies a rendered manifest (JSON/YAML bytes). // Each backend parses the bytes into its expected type: - // - k8s_client: parses as K8s resource (unstructured), applies to K8s API - // - maestro_client: parses as ManifestWork, applies via Maestro gRPC + // - k8sclient: parses as K8s resource (unstructured), applies to K8s API + // - maestroclient: parses as ManifestWork, applies via Maestro gRPC // // The backend handles discovery of existing resources internally for generation comparison. // @@ -30,17 +30,32 @@ type TransportClient interface { // - ctx: Context for the operation // - manifest: Rendered JSON/YAML bytes of the resource to apply // - opts: Apply options (e.g., RecreateOnChange). Nil uses defaults. - // - target: Per-request routing context (nil for k8s_client) - ApplyResource(ctx context.Context, manifest []byte, opts *ApplyOptions, target TransportContext) (*ApplyResult, error) + // - target: Per-request routing context (nil for k8sclient) + ApplyResource( + ctx context.Context, + manifest []byte, + opts *ApplyOptions, + target TransportContext, + ) (*ApplyResult, error) // GetResource retrieves a single Kubernetes resource by GVK, namespace, and name. - // The target parameter provides per-request routing context (nil for k8s_client). + // The target parameter provides per-request routing context (nil for k8sclient). // Returns the resource or an error if not found. - GetResource(ctx context.Context, gvk schema.GroupVersionKind, namespace, name string, target TransportContext) (*unstructured.Unstructured, error) + GetResource( + ctx context.Context, + gvk schema.GroupVersionKind, + namespace, name string, + target TransportContext, + ) (*unstructured.Unstructured, error) // DiscoverResources discovers Kubernetes resources based on the Discovery configuration. - // The target parameter provides per-request routing context (nil for k8s_client). + // The target parameter provides per-request routing context (nil for k8sclient). // If Discovery.IsSingleResource() is true, it fetches a single resource by name. // Otherwise, it lists resources matching the label selector. - DiscoverResources(ctx context.Context, gvk schema.GroupVersionKind, discovery manifest.Discovery, target TransportContext) (*unstructured.UnstructuredList, error) + DiscoverResources( + ctx context.Context, + gvk schema.GroupVersionKind, + discovery manifest.Discovery, + target TransportContext, + ) (*unstructured.UnstructuredList, error) } diff --git a/internal/transport_client/types.go b/internal/transportclient/types.go similarity index 81% rename from internal/transport_client/types.go rename to internal/transportclient/types.go index 1e267f4..3386296 100644 --- a/internal/transport_client/types.go +++ b/internal/transportclient/types.go @@ -1,6 +1,6 @@ -// Package transport_client provides a unified interface for applying Kubernetes resources +// Package transportclient provides a unified interface for applying Kubernetes resources // across different backends (direct K8s API, Maestro/OCM ManifestWork, etc.). -package transport_client +package transportclient import ( "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/manifest" @@ -24,8 +24,8 @@ type ApplyResult struct { // TransportContext carries per-request routing information for the transport backend. // Each transport client defines its own concrete context type and type-asserts: -// - k8s_client: ignores it (nil) -// - maestro_client: expects *maestro_client.TransportContext with ConsumerName +// - k8sclient: ignores it (nil) +// - maestroclient: expects *maestroclient.TransportContext with ConsumerName // // This is typed as `any` to allow each backend to define its own context shape. type TransportContext = any diff --git a/pkg/errors/api_error.go b/pkg/errors/api_error.go index 5a25330..b518d5a 100644 --- a/pkg/errors/api_error.go +++ b/pkg/errors/api_error.go @@ -120,7 +120,15 @@ func (e *APIError) HasResponseBody() bool { // ----------------------------------------------------------------------------- // NewAPIError creates a new APIError with all fields -func NewAPIError(method, url string, statusCode int, status string, body []byte, attempts int, duration time.Duration, err error) *APIError { +func NewAPIError( + method, url string, + statusCode int, + status string, + body []byte, + attempts int, + duration time.Duration, + err error, +) *APIError { return &APIError{ Method: method, URL: url, diff --git a/pkg/errors/error.go b/pkg/errors/error.go index df660f4..f419ce7 100644 --- a/pkg/errors/error.go +++ b/pkg/errors/error.go @@ -82,23 +82,75 @@ func Find(code ServiceErrorCode) (bool, *ServiceError) { func Errors() ServiceErrors { return ServiceErrors{ - ServiceError{Code: ErrorNotFound, Reason: "Resource not found", HTTPCode: http.StatusNotFound}, - ServiceError{Code: ErrorValidation, Reason: "General validation failure", HTTPCode: http.StatusBadRequest}, - ServiceError{Code: ErrorConflict, Reason: "An entity with the specified unique values already exists", HTTPCode: http.StatusConflict}, - ServiceError{Code: ErrorForbidden, Reason: "Forbidden to perform this action", HTTPCode: http.StatusForbidden}, - ServiceError{Code: ErrorUnauthorized, Reason: "Account is unauthorized to perform this action", HTTPCode: http.StatusForbidden}, - ServiceError{Code: ErrorUnauthenticated, Reason: "Account authentication could not be verified", HTTPCode: http.StatusUnauthorized}, - ServiceError{Code: ErrorBadRequest, Reason: "Bad request", HTTPCode: http.StatusBadRequest}, - ServiceError{Code: ErrorMalformedRequest, Reason: "Unable to read request body", HTTPCode: http.StatusBadRequest}, - ServiceError{Code: ErrorNotImplemented, Reason: "HTTP Method not implemented for this endpoint", HTTPCode: http.StatusMethodNotAllowed}, - ServiceError{Code: ErrorGeneral, Reason: "Unspecified error", HTTPCode: http.StatusInternalServerError}, - ServiceError{Code: ErrorAdapterConfigNotFound, Reason: "Adapter configuration not found", HTTPCode: http.StatusNotFound}, - ServiceError{Code: ErrorBrokerConnectionError, Reason: "Failed to connect to message broker", HTTPCode: http.StatusInternalServerError}, - ServiceError{Code: ErrorKubernetesError, Reason: "Kubernetes API error", HTTPCode: http.StatusInternalServerError}, - ServiceError{Code: ErrorHyperFleetAPIError, Reason: "HyperFleet API error", HTTPCode: http.StatusInternalServerError}, - ServiceError{Code: ErrorInvalidCloudEvent, Reason: "Invalid CloudEvent", HTTPCode: http.StatusBadRequest}, - ServiceError{Code: ErrorMaestroError, Reason: "Maestro API error", HTTPCode: http.StatusInternalServerError}, - ServiceError{Code: ErrorConfigurationError, Reason: "Configuration error", HTTPCode: http.StatusInternalServerError}, + ServiceError{ + Code: ErrorNotFound, Reason: "Resource not found", HTTPCode: http.StatusNotFound, + }, + ServiceError{ + Code: ErrorValidation, Reason: "General validation failure", HTTPCode: http.StatusBadRequest, + }, + ServiceError{ + Code: ErrorConflict, + Reason: "An entity with the specified unique values already exists", + HTTPCode: http.StatusConflict, + }, + ServiceError{ + Code: ErrorForbidden, Reason: "Forbidden to perform this action", HTTPCode: http.StatusForbidden, + }, + ServiceError{ + Code: ErrorUnauthorized, + Reason: "Account is unauthorized to perform this action", + HTTPCode: http.StatusForbidden, + }, + ServiceError{ + Code: ErrorUnauthenticated, + Reason: "Account authentication could not be verified", + HTTPCode: http.StatusUnauthorized, + }, + ServiceError{ + Code: ErrorBadRequest, Reason: "Bad request", HTTPCode: http.StatusBadRequest, + }, + ServiceError{ + Code: ErrorMalformedRequest, + Reason: "Unable to read request body", + HTTPCode: http.StatusBadRequest, + }, + ServiceError{ + Code: ErrorNotImplemented, + Reason: "HTTP Method not implemented for this endpoint", + HTTPCode: http.StatusMethodNotAllowed, + }, + ServiceError{ + Code: ErrorGeneral, Reason: "Unspecified error", HTTPCode: http.StatusInternalServerError, + }, + ServiceError{ + Code: ErrorAdapterConfigNotFound, + Reason: "Adapter configuration not found", + HTTPCode: http.StatusNotFound, + }, + ServiceError{ + Code: ErrorBrokerConnectionError, + Reason: "Failed to connect to message broker", + HTTPCode: http.StatusInternalServerError, + }, + ServiceError{ + Code: ErrorKubernetesError, Reason: "Kubernetes API error", HTTPCode: http.StatusInternalServerError, + }, + ServiceError{ + Code: ErrorHyperFleetAPIError, + Reason: "HyperFleet API error", + HTTPCode: http.StatusInternalServerError, + }, + ServiceError{ + Code: ErrorInvalidCloudEvent, Reason: "Invalid CloudEvent", HTTPCode: http.StatusBadRequest, + }, + ServiceError{ + Code: ErrorMaestroError, Reason: "Maestro API error", HTTPCode: http.StatusInternalServerError, + }, + ServiceError{ + Code: ErrorConfigurationError, + Reason: "Configuration error", + HTTPCode: http.StatusInternalServerError, + }, } } @@ -119,7 +171,9 @@ func New(code ServiceErrorCode, reason string, values ...interface{}) *ServiceEr if !exists { // Log undefined error code - using fmt.Printf as fallback since we don't have logger here fmt.Printf("Undefined error code used: %d\n", code) - err = &ServiceError{Code: ErrorGeneral, Reason: "Unspecified error", HTTPCode: http.StatusInternalServerError} + err = &ServiceError{ + Code: ErrorGeneral, Reason: "Unspecified error", HTTPCode: http.StatusInternalServerError, + } } // If the reason is specified, use it (with formatting) diff --git a/pkg/errors/error_test.go b/pkg/errors/error_test.go index ea98e80..3ea93ac 100644 --- a/pkg/errors/error_test.go +++ b/pkg/errors/error_test.go @@ -533,8 +533,7 @@ func TestHelperFunctions(t *testing.T) { errStr := err.Error() for _, arg := range tt.args { argStr := "" - switch v := arg.(type) { - case string: + if v, ok := arg.(string); ok { argStr = v } if argStr != "" && !containsString(errStr, argStr) { diff --git a/pkg/logger/with_error_field_test.go b/pkg/logger/with_error_field_test.go index d50b09c..d505b6c 100644 --- a/pkg/logger/with_error_field_test.go +++ b/pkg/logger/with_error_field_test.go @@ -126,8 +126,10 @@ func TestShouldCaptureStackTrace_K8sAPIErrors(t *testing.T) { expectCapture: false, }, { - name: "Conflict_skips_stack_trace", - err: apierrors.NewConflict(schema.GroupResource{Group: "", Resource: "pods"}, "my-pod", errors.New("conflict")), + name: "Conflict_skips_stack_trace", + err: apierrors.NewConflict( + schema.GroupResource{Group: "", Resource: "pods"}, "my-pod", errors.New("conflict"), + ), expectCapture: false, }, { @@ -136,8 +138,10 @@ func TestShouldCaptureStackTrace_K8sAPIErrors(t *testing.T) { expectCapture: false, }, { - name: "Forbidden_skips_stack_trace", - err: apierrors.NewForbidden(schema.GroupResource{Group: "", Resource: "pods"}, "my-pod", errors.New("forbidden")), + name: "Forbidden_skips_stack_trace", + err: apierrors.NewForbidden( + schema.GroupResource{Group: "", Resource: "pods"}, "my-pod", errors.New("forbidden"), + ), expectCapture: false, }, { @@ -184,18 +188,24 @@ func TestShouldCaptureStackTrace_K8sResourceDataErrors(t *testing.T) { expectCapture bool }{ { - name: "K8sResourceKeyNotFoundError_skips_stack_trace", - err: apperrors.NewK8sResourceKeyNotFoundError("Secret", "default", "my-secret", "password"), + name: "K8sResourceKeyNotFoundError_skips_stack_trace", + err: apperrors.NewK8sResourceKeyNotFoundError( + "Secret", "default", "my-secret", "password", + ), expectCapture: false, }, { - name: "K8sInvalidPathError_skips_stack_trace", - err: apperrors.NewK8sInvalidPathError("Secret", "invalid/path", "namespace.name.key"), + name: "K8sInvalidPathError_skips_stack_trace", + err: apperrors.NewK8sInvalidPathError( + "Secret", "invalid/path", "namespace.name.key", + ), expectCapture: false, }, { - name: "K8sResourceDataError_skips_stack_trace", - err: apperrors.NewK8sResourceDataError("ConfigMap", "default", "my-config", "data field missing", nil), + name: "K8sResourceDataError_skips_stack_trace", + err: apperrors.NewK8sResourceDataError( + "ConfigMap", "default", "my-config", "data field missing", nil, + ), expectCapture: false, }, } @@ -217,43 +227,59 @@ func TestShouldCaptureStackTrace_APIErrors(t *testing.T) { expectCapture bool }{ { - name: "APIError_NotFound_skips_stack_trace", - err: apperrors.NewAPIError("GET", "/api/v1/clusters/123", 404, "Not Found", nil, 1, 0, errors.New("not found")), + name: "APIError_NotFound_skips_stack_trace", + err: apperrors.NewAPIError( + "GET", "/api/v1/clusters/123", 404, "Not Found", nil, 1, 0, errors.New("not found"), + ), expectCapture: false, }, { - name: "APIError_Unauthorized_skips_stack_trace", - err: apperrors.NewAPIError("GET", "/api/v1/clusters", 401, "Unauthorized", nil, 1, 0, errors.New("unauthorized")), + name: "APIError_Unauthorized_skips_stack_trace", + err: apperrors.NewAPIError( + "GET", "/api/v1/clusters", 401, "Unauthorized", nil, 1, 0, errors.New("unauthorized"), + ), expectCapture: false, }, { - name: "APIError_Forbidden_skips_stack_trace", - err: apperrors.NewAPIError("POST", "/api/v1/clusters", 403, "Forbidden", nil, 1, 0, errors.New("forbidden")), + name: "APIError_Forbidden_skips_stack_trace", + err: apperrors.NewAPIError( + "POST", "/api/v1/clusters", 403, "Forbidden", nil, 1, 0, errors.New("forbidden"), + ), expectCapture: false, }, { - name: "APIError_BadRequest_skips_stack_trace", - err: apperrors.NewAPIError("POST", "/api/v1/clusters", 400, "Bad Request", nil, 1, 0, errors.New("bad request")), + name: "APIError_BadRequest_skips_stack_trace", + err: apperrors.NewAPIError( + "POST", "/api/v1/clusters", 400, "Bad Request", nil, 1, 0, errors.New("bad request"), + ), expectCapture: false, }, { - name: "APIError_Conflict_skips_stack_trace", - err: apperrors.NewAPIError("PUT", "/api/v1/clusters/123", 409, "Conflict", nil, 1, 0, errors.New("conflict")), + name: "APIError_Conflict_skips_stack_trace", + err: apperrors.NewAPIError( + "PUT", "/api/v1/clusters/123", 409, "Conflict", nil, 1, 0, errors.New("conflict"), + ), expectCapture: false, }, { - name: "APIError_RateLimited_skips_stack_trace", - err: apperrors.NewAPIError("GET", "/api/v1/clusters", 429, "Too Many Requests", nil, 1, 0, errors.New("rate limited")), + name: "APIError_RateLimited_skips_stack_trace", + err: apperrors.NewAPIError( + "GET", "/api/v1/clusters", 429, "Too Many Requests", nil, 1, 0, errors.New("rate limited"), + ), expectCapture: false, }, { - name: "APIError_Timeout_skips_stack_trace", - err: apperrors.NewAPIError("GET", "/api/v1/clusters", 408, "Request Timeout", nil, 1, 0, errors.New("timeout")), + name: "APIError_Timeout_skips_stack_trace", + err: apperrors.NewAPIError( + "GET", "/api/v1/clusters", 408, "Request Timeout", nil, 1, 0, errors.New("timeout"), + ), expectCapture: false, }, { - name: "APIError_ServerError_skips_stack_trace", - err: apperrors.NewAPIError("GET", "/api/v1/clusters", 503, "Service Unavailable", nil, 3, 0, errors.New("server error")), + name: "APIError_ServerError_skips_stack_trace", + err: apperrors.NewAPIError( + "GET", "/api/v1/clusters", 503, "Service Unavailable", nil, 3, 0, errors.New("server error"), + ), expectCapture: false, }, } @@ -342,15 +368,19 @@ func TestCaptureStackTrace(t *testing.T) { // (the first frame was skipped, but a new frame was captured at the end) for i := 0; i < len(stack1)-1; i++ { if stack1[i] != stack0[i+1] { - t.Errorf("Expected stack1[%d] to equal stack0[%d], got %q vs %q", - i, i+1, stack1[i], stack0[i+1]) + t.Errorf( + "Expected stack1[%d] to equal stack0[%d], got %q vs %q", + i, i+1, stack1[i], stack0[i+1], + ) } } return } - t.Errorf("Expected skip=1 to result in fewer frames or shifted stack, got len(stack0)=%d, len(stack1)=%d", - len(stack0), len(stack1)) + t.Errorf( + "Expected skip=1 to result in fewer frames or shifted stack, got len(stack0)=%d, len(stack1)=%d", + len(stack0), len(stack1), + ) }) t.Run("frames_contain_file_line_function", func(t *testing.T) { @@ -444,7 +474,10 @@ func TestWithErrorField_StackTraceIntegration(t *testing.T) { t.Run("api_error_server_error_no_stack_trace", func(t *testing.T) { ctx := context.Background() - err := apperrors.NewAPIError("GET", "/api/v1/clusters", 500, "Internal Server Error", nil, 1, 0, errors.New("server error")) + err := apperrors.NewAPIError( + "GET", "/api/v1/clusters", 500, "Internal Server Error", nil, 1, 0, + errors.New("server error"), + ) result := WithErrorField(ctx, err) fields := GetLogFields(result) diff --git a/pkg/otel/tracer.go b/pkg/otel/tracer.go index ee4ace2..218c4c9 100644 --- a/pkg/otel/tracer.go +++ b/pkg/otel/tracer.go @@ -35,22 +35,33 @@ const ( func GetTraceSampleRatio(log logger.Logger, ctx context.Context) float64 { ratioStr := os.Getenv(EnvTraceSampleRatio) if ratioStr == "" { - log.Infof(ctx, "Using default trace sample ratio: %.2f (set %s to override)", DefaultTraceSampleRatio, EnvTraceSampleRatio) + log.Infof( + ctx, "Using default trace sample ratio: %.2f (set %s to override)", + DefaultTraceSampleRatio, EnvTraceSampleRatio, + ) return DefaultTraceSampleRatio } ratio, err := strconv.ParseFloat(ratioStr, 64) if err != nil { - log.Warnf(ctx, "Invalid %s value %q, using default %.2f: %v", EnvTraceSampleRatio, ratioStr, DefaultTraceSampleRatio, err) + log.Warnf( + ctx, "Invalid %s value %q, using default %.2f: %v", + EnvTraceSampleRatio, ratioStr, DefaultTraceSampleRatio, err, + ) return DefaultTraceSampleRatio } if ratio < 0.0 || ratio > 1.0 { - log.Warnf(ctx, "Invalid %s value %.4f (must be 0.0-1.0), using default %.2f", EnvTraceSampleRatio, ratio, DefaultTraceSampleRatio) + log.Warnf( + ctx, "Invalid %s value %.4f (must be 0.0-1.0), using default %.2f", + EnvTraceSampleRatio, ratio, DefaultTraceSampleRatio, + ) return DefaultTraceSampleRatio } - log.Infof(ctx, "Trace sample ratio configured: %.4f (%.2f%% of traces will be sampled)", ratio, ratio*100) + log.Infof( + ctx, "Trace sample ratio configured: %.4f (%.2f%% of traces will be sampled)", ratio, ratio*100, + ) return ratio } diff --git a/test/integration/README.md b/test/integration/README.md index 1ebf370..5b89cab 100644 --- a/test/integration/README.md +++ b/test/integration/README.md @@ -219,7 +219,7 @@ INTEGRATION_STRATEGY=k3s \ The integration tests cover: -### K8s Client Tests (`test/integration/k8s_client/`) +### K8s Client Tests (`test/integration/k8sclient/`) - **CRUD Operations**: Create, Get, List, Update, Delete resources - **Patch Operations**: Strategic merge patch, JSON merge patch @@ -247,7 +247,7 @@ test/integration/ Tests use a unified interface (`TestEnv`) with automatic strategy selection: ```go -// test/integration/k8s_client/helper_selector.go +// test/integration/k8sclient/helper_selector.go func SetupTestEnv(t *testing.T) TestEnv { strategy := os.Getenv("INTEGRATION_STRATEGY") @@ -574,7 +574,7 @@ INTEGRATION_ENVTEST_IMAGE=quay.io/your-org/integration-test:v1 make test-integra Since each test gets a fresh container, you can run tests in parallel: ```bash -go test -v -tags=integration -parallel 4 ./test/integration/k8s_client/... +go test -v -tags=integration -parallel 4 ./test/integration/k8sclient/... ``` **Note**: This increases resource usage but can speed up total runtime. diff --git a/test/integration/config-loader/config_criteria_integration_test.go b/test/integration/config-loader/config_criteria_integration_test.go index af09910..cc54745 100644 --- a/test/integration/config-loader/config_criteria_integration_test.go +++ b/test/integration/config-loader/config_criteria_integration_test.go @@ -1,6 +1,6 @@ //go:build integration -package config_loader_integration +package configloaderintegration import ( "context" @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/criteria" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" ) @@ -42,15 +42,15 @@ func getTaskConfigPath() string { } // loadTestConfig loads split adapter and task configs and returns the merged Config. -func loadTestConfig(t *testing.T) *config_loader.Config { +func loadTestConfig(t *testing.T) *configloader.Config { t.Helper() adapterConfigPath := getAdapterConfigPath() taskConfigPath := getTaskConfigPath() - config, err := config_loader.LoadConfig( - config_loader.WithAdapterConfigPath(adapterConfigPath), - config_loader.WithTaskConfigPath(taskConfigPath), - config_loader.WithSkipSemanticValidation(), + config, err := configloader.LoadConfig( + configloader.WithAdapterConfigPath(adapterConfigPath), + configloader.WithTaskConfigPath(taskConfigPath), + configloader.WithSkipSemanticValidation(), ) require.NoError(t, err, "should load split config files from %s and %s", adapterConfigPath, taskConfigPath) require.NotNil(t, config) diff --git a/test/integration/config-loader/loader_template_test.go b/test/integration/config-loader/loader_template_test.go index c6fdc7d..363975c 100644 --- a/test/integration/config-loader/loader_template_test.go +++ b/test/integration/config-loader/loader_template_test.go @@ -1,4 +1,4 @@ -package config_loader_integration +package configloaderintegration import ( "os" @@ -7,8 +7,8 @@ import ( "testing" "time" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleet_api" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleetapi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -46,10 +46,10 @@ func TestLoadSplitConfig(t *testing.T) { adapterConfigPath := filepath.Join(projectRoot, "test/testdata/adapter-config.yaml") taskConfigPath := filepath.Join(projectRoot, "test/testdata/task-config.yaml") - config, err := config_loader.LoadConfig( - config_loader.WithAdapterConfigPath(adapterConfigPath), - config_loader.WithTaskConfigPath(taskConfigPath), - config_loader.WithSkipSemanticValidation(), + config, err := configloader.LoadConfig( + configloader.WithAdapterConfigPath(adapterConfigPath), + configloader.WithTaskConfigPath(taskConfigPath), + configloader.WithSkipSemanticValidation(), ) require.NoError(t, err, "should be able to load split config files") require.NotNil(t, config) @@ -62,17 +62,17 @@ func TestLoadSplitConfig(t *testing.T) { // Clients config comes from adapter config assert.Equal(t, 2*time.Second, config.Clients.HyperfleetAPI.Timeout) assert.Equal(t, 3, config.Clients.HyperfleetAPI.RetryAttempts) - assert.Equal(t, hyperfleet_api.BackoffExponential, config.Clients.HyperfleetAPI.RetryBackoff) + assert.Equal(t, hyperfleetapi.BackoffExponential, config.Clients.HyperfleetAPI.RetryBackoff) // Verify params exist (from task config) assert.NotEmpty(t, config.Params) assert.GreaterOrEqual(t, len(config.Params), 1, "should have at least 1 parameter") // Check specific params (using accessor method) - clusterIdParam := config.GetParamByName("clusterId") - require.NotNil(t, clusterIdParam, "clusterId parameter should exist") - assert.Equal(t, "event.id", clusterIdParam.Source) - assert.True(t, clusterIdParam.Required) + clusterIDParam := config.GetParamByName("clusterId") + require.NotNil(t, clusterIDParam, "clusterId parameter should exist") + assert.Equal(t, "event.id", clusterIDParam.Source) + assert.True(t, clusterIDParam.Required) // Verify preconditions (from task config) assert.NotEmpty(t, config.Preconditions) @@ -133,10 +133,10 @@ func TestLoadSplitConfigWithResourceByName(t *testing.T) { adapterConfigPath := filepath.Join(projectRoot, "test/testdata/adapter-config.yaml") taskConfigPath := filepath.Join(projectRoot, "test/testdata/task-config.yaml") - config, err := config_loader.LoadConfig( - config_loader.WithAdapterConfigPath(adapterConfigPath), - config_loader.WithTaskConfigPath(taskConfigPath), - config_loader.WithSkipSemanticValidation(), + config, err := configloader.LoadConfig( + configloader.WithAdapterConfigPath(adapterConfigPath), + configloader.WithTaskConfigPath(taskConfigPath), + configloader.WithSkipSemanticValidation(), ) require.NoError(t, err) require.NotNil(t, config) @@ -148,7 +148,7 @@ func TestLoadSplitConfigWithResourceByName(t *testing.T) { } // Helper function to find a capture field by name -func findCaptureByName(captures []config_loader.CaptureField, name string) *config_loader.CaptureField { +func findCaptureByName(captures []configloader.CaptureField, name string) *configloader.CaptureField { for i := range captures { if captures[i].Name == name { return &captures[i] diff --git a/test/integration/executor/executor_integration_test.go b/test/integration/executor/executor_integration_test.go index 09762c1..cf2e232 100644 --- a/test/integration/executor/executor_integration_test.go +++ b/test/integration/executor/executor_integration_test.go @@ -1,4 +1,4 @@ -package executor_integration_test +package executorintegrationtest import ( "context" @@ -10,15 +10,20 @@ import ( "time" "github.com/cloudevents/sdk-go/v2/event" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/executor" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleet_api" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleetapi" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" "github.com/openshift-hyperfleet/hyperfleet-adapter/test/integration/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +const ( + healthStatusHealthy = "Healthy" + healthMessageAllOperationsCompleted = "All adapter operations completed successfully" +) + // getK8sEnvForTest returns the K8s environment for integration testing. // Uses real K8s from testcontainers. Skips tests if testcontainers are unavailable. func getK8sEnvForTest(t *testing.T) *K8sTestEnv { @@ -33,18 +38,18 @@ func getK8sEnvForTest(t *testing.T) *K8sTestEnv { } // createTestEvent creates a CloudEvent for testing -func createTestEvent(clusterId string) *event.Event { +func createTestEvent(clusterID string) *event.Event { evt := event.New() - evt.SetID("test-event-" + clusterId) + evt.SetID("test-event-" + clusterID) evt.SetType("com.redhat.hyperfleet.cluster.provision") evt.SetSource("test") evt.SetTime(time.Now()) eventData := map[string]interface{}{ - "id": clusterId, + "id": clusterID, "resource_type": "cluster", "generation": "gen-001", - "href": "/api/v1/clusters/" + clusterId, + "href": "/api/v1/clusters/" + clusterID, } eventDataBytes, _ := json.Marshal(eventData) _ = evt.SetData(event.ApplicationJSON, eventDataBytes) @@ -53,58 +58,58 @@ func createTestEvent(clusterId string) *event.Event { } // createTestConfig creates a unified Config for executor integration tests. -func createTestConfig(apiBaseURL string) *config_loader.Config { +func createTestConfig(apiBaseURL string) *configloader.Config { _ = apiBaseURL // Kept for compatibility; base URL comes from env params. - return &config_loader.Config{ - Adapter: config_loader.AdapterInfo{ + return &configloader.Config{ + Adapter: configloader.AdapterInfo{ Name: "test-adapter", Version: "1.0.0", }, - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ Timeout: 10 * time.Second, RetryAttempts: 1, - RetryBackoff: hyperfleet_api.BackoffConstant, + RetryBackoff: hyperfleetapi.BackoffConstant, }, }, - Params: []config_loader.Parameter{ + Params: []configloader.Parameter{ {Name: "hyperfleetApiBaseUrl", Source: "env.HYPERFLEET_API_BASE_URL", Required: true}, {Name: "hyperfleetApiVersion", Source: "env.HYPERFLEET_API_VERSION", Default: "v1", Required: false}, - {Name: "clusterId", Source: "event.id", Required: true}, + {Name: "clusterID", Source: "event.id", Required: true}, }, - Preconditions: []config_loader.Precondition{ + Preconditions: []configloader.Precondition{ { - ActionBase: config_loader.ActionBase{ + ActionBase: configloader.ActionBase{ Name: "clusterStatus", - APICall: &config_loader.APICall{ + APICall: &configloader.APICall{ Method: "GET", - URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterId }}", + URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterID }}", Timeout: "5s", }, }, - Capture: []config_loader.CaptureField{ - {Name: "clusterName", FieldExpressionDef: config_loader.FieldExpressionDef{Field: "name"}}, + Capture: []configloader.CaptureField{ + {Name: "clusterName", FieldExpressionDef: configloader.FieldExpressionDef{Field: "name"}}, { Name: "readyConditionStatus", - FieldExpressionDef: config_loader.FieldExpressionDef{ + FieldExpressionDef: configloader.FieldExpressionDef{ Expression: `status.conditions.filter(c, c.type == "Ready").size() > 0 ? status.conditions.filter(c, c.type == "Ready")[0].status : "False"`, }, }, - {Name: "region", FieldExpressionDef: config_loader.FieldExpressionDef{Field: "spec.region"}}, - {Name: "cloudProvider", FieldExpressionDef: config_loader.FieldExpressionDef{Field: "spec.provider"}}, - {Name: "vpcId", FieldExpressionDef: config_loader.FieldExpressionDef{Field: "spec.vpc_id"}}, + {Name: "region", FieldExpressionDef: configloader.FieldExpressionDef{Field: "spec.region"}}, + {Name: "cloudProvider", FieldExpressionDef: configloader.FieldExpressionDef{Field: "spec.provider"}}, + {Name: "vpcId", FieldExpressionDef: configloader.FieldExpressionDef{Field: "spec.vpc_id"}}, }, - Conditions: []config_loader.Condition{ + Conditions: []configloader.Condition{ {Field: "readyConditionStatus", Operator: "equals", Value: "True"}, {Field: "cloudProvider", Operator: "in", Value: []interface{}{"aws", "gcp", "azure"}}, }, }, }, - Resources: []config_loader.Resource{}, - Post: &config_loader.PostConfig{ - Payloads: []config_loader.Payload{ + Resources: []configloader.Resource{}, + Post: &configloader.PostConfig{ + Payloads: []configloader.Payload{ { Name: "clusterStatusPayload", Build: map[string]interface{}{ @@ -114,15 +119,17 @@ func createTestConfig(apiBaseURL string) *config_loader.Config { "expression": `adapter.executionStatus == "success" && !adapter.resourcesSkipped`, }, "reason": map[string]interface{}{ - "expression": `adapter.resourcesSkipped ? "PreconditionNotMet" : (adapter.errorReason != "" ? adapter.errorReason : "Healthy")`, + "expression": `adapter.resourcesSkipped ? "PreconditionNotMet" : ` + + `(adapter.errorReason != "" ? adapter.errorReason : "Healthy")`, }, "message": map[string]interface{}{ - "expression": `adapter.skipReason != "" ? adapter.skipReason : (adapter.errorMessage != "" ? adapter.errorMessage : "All adapter operations completed successfully")`, + "expression": `adapter.skipReason != "" ? adapter.skipReason : ` + + `(adapter.errorMessage != "" ? adapter.errorMessage : "All adapter operations completed successfully")`, }, }, }, - "clusterId": map[string]interface{}{ - "value": "{{ .clusterId }}", + "clusterID": map[string]interface{}{ + "value": "{{ .clusterID }}", }, "clusterName": map[string]interface{}{ "expression": `clusterName != "" ? clusterName : "unknown"`, @@ -130,13 +137,13 @@ func createTestConfig(apiBaseURL string) *config_loader.Config { }, }, }, - PostActions: []config_loader.PostAction{ + PostActions: []configloader.PostAction{ { - ActionBase: config_loader.ActionBase{ + ActionBase: configloader.ActionBase{ Name: "reportClusterStatus", - APICall: &config_loader.APICall{ + APICall: &configloader.APICall{ Method: "POST", - URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterId }}/statuses", + URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterID }}/statuses", Body: "{{ .clusterStatusPayload }}", Timeout: "5s", }, @@ -161,9 +168,9 @@ func TestExecutor_FullFlow_Success(t *testing.T) { // Create config and executor config := createTestConfig(mockAPI.URL()) - apiClient, err := hyperfleet_api.NewClient(testLog(), - hyperfleet_api.WithTimeout(10*time.Second), - hyperfleet_api.WithRetryAttempts(1), + apiClient, err := hyperfleetapi.NewClient(testLog(), + hyperfleetapi.WithTimeout(10*time.Second), + hyperfleetapi.WithRetryAttempts(1), ) require.NoError(t, err, "failed to create API client") @@ -190,8 +197,8 @@ func TestExecutor_FullFlow_Success(t *testing.T) { require.Equal(t, executor.StatusSuccess, result.Status, "Expected success status; errors=%v", result.Errors) // Verify params were extracted - if result.Params["clusterId"] != "cluster-123" { - t.Errorf("Expected clusterId 'cluster-123', got '%v'", result.Params["clusterId"]) + if result.Params["clusterID"] != "cluster-123" { + t.Errorf("Expected clusterID 'cluster-123', got '%v'", result.Params["clusterID"]) } // Verify preconditions passed @@ -244,8 +251,8 @@ func TestExecutor_FullFlow_Success(t *testing.T) { // Reason should be "Healthy" (default since no adapter.errorReason) if reason, ok := health["reason"].(string); ok { - if reason != "Healthy" { - t.Errorf("Expected health.reason to be 'Healthy', got '%s'", reason) + if reason != healthStatusHealthy { + t.Errorf("Expected health.reason to be '%s', got '%s'", healthStatusHealthy, reason) } } else { t.Error("Expected health.reason to be a string") @@ -253,7 +260,7 @@ func TestExecutor_FullFlow_Success(t *testing.T) { // Message should be default success message (no adapter.errorMessage) if message, ok := health["message"].(string); ok { - if message != "All adapter operations completed successfully" { + if message != healthMessageAllOperationsCompleted { t.Errorf("Expected health.message to be default success message, got '%s'", message) } } else { @@ -299,7 +306,7 @@ func TestExecutor_PreconditionNotMet(t *testing.T) { // Create config and executor config := createTestConfig(mockAPI.URL()) - apiClient, err := hyperfleet_api.NewClient(k8sEnv.Log) + apiClient, err := hyperfleetapi.NewClient(k8sEnv.Log) assert.NoError(t, err) exec, err := executor.NewBuilder(). WithConfig(config). @@ -327,10 +334,8 @@ func TestExecutor_PreconditionNotMet(t *testing.T) { // Verify precondition was not matched if len(result.PreconditionResults) != 1 { t.Errorf("Expected 1 precondition result, got %d", len(result.PreconditionResults)) - } else { - if result.PreconditionResults[0].Matched { - t.Error("Expected precondition to NOT match") - } + } else if result.PreconditionResults[0].Matched { + t.Error("Expected precondition to NOT match") } // Post actions should still execute (to report the error) @@ -353,15 +358,15 @@ func TestExecutor_PreconditionNotMet(t *testing.T) { // Reason should contain error (from adapter.errorReason, not "Healthy") if reason, ok := health["reason"].(string); ok { - if reason == "Healthy" { - t.Error("Expected health.reason to indicate precondition not met, got 'Healthy'") + if reason == healthStatusHealthy { + t.Errorf("Expected health.reason to indicate precondition not met, got '%s'", healthStatusHealthy) } t.Logf("Health reason: %s", reason) } // Message should contain error explanation (from adapter.errorMessage) if message, ok := health["message"].(string); ok { - if message == "All adapter operations completed successfully" { + if message == healthMessageAllOperationsCompleted { t.Error("Expected health.message to explain precondition not met, got default success message") } t.Logf("Health message: %s", message) @@ -401,8 +406,8 @@ func TestExecutor_PreconditionAPIFailure(t *testing.T) { // Create config and executor config := createTestConfig(mockAPI.URL()) - apiClient, err := hyperfleet_api.NewClient(testLog(), - hyperfleet_api.WithRetryAttempts(1), + apiClient, err := hyperfleetapi.NewClient(testLog(), + hyperfleetapi.WithRetryAttempts(1), ) assert.NoError(t, err) @@ -456,8 +461,8 @@ func TestExecutor_PreconditionAPIFailure(t *testing.T) { // Reason should contain error reason (not "Healthy") if reason, ok := health["reason"].(string); ok { - if reason == "Healthy" { - t.Error("Expected health.reason to contain error, got 'Healthy'") + if reason == healthStatusHealthy { + t.Errorf("Expected health.reason to contain error, got '%s'", healthStatusHealthy) } t.Logf("Health reason: %s", reason) } else { @@ -466,7 +471,7 @@ func TestExecutor_PreconditionAPIFailure(t *testing.T) { // Message should contain error message (not default success message) if message, ok := health["message"].(string); ok { - if message == "All adapter operations completed successfully" { + if message == healthMessageAllOperationsCompleted { t.Error("Expected health.message to contain error, got default success message") } t.Logf("Health message: %s", message) @@ -495,32 +500,33 @@ func TestExecutor_CELExpressionEvaluation(t *testing.T) { // Create config with CEL expression precondition config := createTestConfig(mockAPI.URL()) - config.Preconditions = []config_loader.Precondition{ + config.Preconditions = []configloader.Precondition{ { - ActionBase: config_loader.ActionBase{ + ActionBase: configloader.ActionBase{ Name: "clusterStatus", - APICall: &config_loader.APICall{ + APICall: &configloader.APICall{ Method: "GET", - URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterId }}", + URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterID }}", Timeout: "5s", }, }, - Capture: []config_loader.CaptureField{ - {Name: "clusterName", FieldExpressionDef: config_loader.FieldExpressionDef{Field: "name"}}, + Capture: []configloader.CaptureField{ + {Name: "clusterName", FieldExpressionDef: configloader.FieldExpressionDef{Field: "name"}}, { Name: "readyConditionStatus", - FieldExpressionDef: config_loader.FieldExpressionDef{ - Expression: `status.conditions.filter(c, c.type == "Ready").size() > 0 ? status.conditions.filter(c, c.type == "Ready")[0].status : "False"`, + FieldExpressionDef: configloader.FieldExpressionDef{ + Expression: `status.conditions.filter(c, c.type == "Ready").size() > 0 ? ` + + `status.conditions.filter(c, c.type == "Ready")[0].status : "False"`, }, }, - {Name: "nodeCount", FieldExpressionDef: config_loader.FieldExpressionDef{Field: "spec.node_count"}}, + {Name: "nodeCount", FieldExpressionDef: configloader.FieldExpressionDef{Field: "spec.node_count"}}, }, // Use CEL expression instead of structured conditions Expression: `readyConditionStatus == "True" && nodeCount >= 3`, }, } - apiClient, err := hyperfleet_api.NewClient(testLog()) + apiClient, err := hyperfleetapi.NewClient(testLog()) assert.NoError(t, err) @@ -568,7 +574,7 @@ func TestExecutor_MultipleMessages(t *testing.T) { t.Setenv("HYPERFLEET_API_VERSION", "v1") config := createTestConfig(mockAPI.URL()) - apiClient, err := hyperfleet_api.NewClient(testLog()) + apiClient, err := hyperfleetapi.NewClient(testLog()) assert.NoError(t, err) exec, err := executor.NewBuilder(). WithConfig(config). @@ -581,11 +587,11 @@ func TestExecutor_MultipleMessages(t *testing.T) { } // Process multiple messages - clusterIds := []string{"cluster-a", "cluster-b", "cluster-c"} - results := make([]*executor.ExecutionResult, len(clusterIds)) + clusterIDs := []string{"cluster-a", "cluster-b", "cluster-c"} + results := make([]*executor.ExecutionResult, len(clusterIDs)) - for i, clusterId := range clusterIds { - evt := createTestEvent(clusterId) + for i, clusterID := range clusterIDs { + evt := createTestEvent(clusterID) results[i] = exec.Execute(context.Background(), evt) } @@ -596,20 +602,20 @@ func TestExecutor_MultipleMessages(t *testing.T) { continue } - // Verify each message had its own clusterId - expectedClusterId := clusterIds[i] - if result.Params["clusterId"] != expectedClusterId { - t.Errorf("Message %d: expected clusterId '%s', got '%v'", i, expectedClusterId, result.Params["clusterId"]) + // Verify each message had its own clusterID + expectedClusterID := clusterIDs[i] + if result.Params["clusterID"] != expectedClusterID { + t.Errorf("Message %d: expected clusterID '%s', got '%v'", i, expectedClusterID, result.Params["clusterID"]) } } // Verify we got separate status posts for each message statusResponses := mockAPI.GetStatusResponses() - if len(statusResponses) != len(clusterIds) { - t.Errorf("Expected %d status responses, got %d", len(clusterIds), len(statusResponses)) + if len(statusResponses) != len(clusterIDs) { + t.Errorf("Expected %d status responses, got %d", len(clusterIDs), len(statusResponses)) } - t.Logf("Successfully processed %d messages with isolated contexts", len(clusterIds)) + t.Logf("Successfully processed %d messages with isolated contexts", len(clusterIDs)) } func TestExecutor_Handler_Integration(t *testing.T) { @@ -621,7 +627,7 @@ func TestExecutor_Handler_Integration(t *testing.T) { t.Setenv("HYPERFLEET_API_VERSION", "v1") config := createTestConfig(mockAPI.URL()) - apiClient, err := hyperfleet_api.NewClient(testLog()) + apiClient, err := hyperfleetapi.NewClient(testLog()) assert.NoError(t, err) exec, err := executor.NewBuilder(). WithConfig(config). @@ -673,7 +679,7 @@ func TestExecutor_Handler_PreconditionNotMet_ReturnsNil(t *testing.T) { t.Setenv("HYPERFLEET_API_VERSION", "v1") config := createTestConfig(mockAPI.URL()) - apiClient, err := hyperfleet_api.NewClient(testLog()) + apiClient, err := hyperfleetapi.NewClient(testLog()) assert.NoError(t, err) exec, err := executor.NewBuilder(). WithConfig(config). @@ -706,7 +712,7 @@ func TestExecutor_ContextCancellation(t *testing.T) { t.Setenv("HYPERFLEET_API_VERSION", "v1") config := createTestConfig(mockAPI.URL()) - apiClient, err := hyperfleet_api.NewClient(testLog()) + apiClient, err := hyperfleetapi.NewClient(testLog()) assert.NoError(t, err) exec, err := executor.NewBuilder(). WithConfig(config). @@ -718,17 +724,17 @@ func TestExecutor_ContextCancellation(t *testing.T) { t.Fatalf("Failed to create executor: %v", err) } - // Create already cancelled context + // Create already canceled context ctx, cancel := context.WithCancel(context.Background()) cancel() // Cancel immediately - evt := createTestEvent("cluster-cancelled") + evt := createTestEvent("cluster-canceled") result := exec.Execute(ctx, evt) // Should fail due to context cancellation // Note: The exact behavior depends on where cancellation is checked - require.NotEmpty(t, result.Errors, "Expected error for cancelled context") - t.Logf("Result with cancelled context: status=%s, errors=%v", result.Status, result.Errors) + require.NotEmpty(t, result.Errors, "Expected error for canceled context") + t.Logf("Result with canceled context: status=%s, errors=%v", result.Status, result.Errors) } func TestExecutor_MissingRequiredParam(t *testing.T) { @@ -741,7 +747,7 @@ func TestExecutor_MissingRequiredParam(t *testing.T) { t.Setenv("HYPERFLEET_API_VERSION", "v1") config := createTestConfig(mockAPI.URL()) - apiClient, err := hyperfleet_api.NewClient(testLog()) + apiClient, err := hyperfleetapi.NewClient(testLog()) assert.NoError(t, err) exec, err := executor.NewBuilder(). @@ -807,7 +813,7 @@ func TestExecutor_InvalidEventJSON(t *testing.T) { t.Setenv("HYPERFLEET_API_VERSION", "v1") config := createTestConfig(mockAPI.URL()) - apiClient, err := hyperfleet_api.NewClient(testLog()) + apiClient, err := hyperfleetapi.NewClient(testLog()) assert.NoError(t, err) exec, err := executor.NewBuilder(). @@ -860,7 +866,7 @@ func TestExecutor_MissingEventFields(t *testing.T) { t.Setenv("HYPERFLEET_API_VERSION", "v1") config := createTestConfig(mockAPI.URL()) - apiClient, err := hyperfleet_api.NewClient(testLog()) + apiClient, err := hyperfleetapi.NewClient(testLog()) assert.NoError(t, err) exec, err := executor.NewBuilder(). WithConfig(config). @@ -893,7 +899,7 @@ func TestExecutor_MissingEventFields(t *testing.T) { // Expect failure in param extraction phase errPhase := result.Errors[executor.PhaseParamExtraction] require.Error(t, errPhase) - assert.Contains(t, errPhase.Error(), "clusterId", "Error should mention missing clusterId") + assert.Contains(t, errPhase.Error(), "clusterID", "Error should mention missing clusterID") t.Logf("Missing field error: %v", errPhase) // All phases should be skipped for events with missing required fields @@ -921,81 +927,82 @@ func TestExecutor_LogAction(t *testing.T) { log, logCapture := logger.NewCaptureLogger() // Create config with log actions in preconditions and post-actions - config := &config_loader.Config{ - Adapter: config_loader.AdapterInfo{ + config := &configloader.Config{ + Adapter: configloader.AdapterInfo{ Name: "log-test-adapter", Version: "1.0.0", }, - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ Timeout: 10 * time.Second, RetryAttempts: 1, }, }, - Params: []config_loader.Parameter{ + Params: []configloader.Parameter{ {Name: "hyperfleetApiBaseUrl", Source: "env.HYPERFLEET_API_BASE_URL", Required: true}, {Name: "hyperfleetApiVersion", Default: "v1"}, - {Name: "clusterId", Source: "event.id", Required: true}, + {Name: "clusterID", Source: "event.id", Required: true}, }, - Preconditions: []config_loader.Precondition{ + Preconditions: []configloader.Precondition{ { // Log action only - no API call or conditions - ActionBase: config_loader.ActionBase{ + ActionBase: configloader.ActionBase{ Name: "logStart", - Log: &config_loader.LogAction{ - Message: "Starting processing for cluster {{ .clusterId }}", + Log: &configloader.LogAction{ + Message: "Starting processing for cluster {{ .clusterID }}", Level: "info", }, }, }, { // Log action before API call - ActionBase: config_loader.ActionBase{ + ActionBase: configloader.ActionBase{ Name: "logBeforeAPICall", - Log: &config_loader.LogAction{ - Message: "About to check cluster status for {{ .clusterId }}", + Log: &configloader.LogAction{ + Message: "About to check cluster status for {{ .clusterID }}", Level: "debug", }, }, }, { - ActionBase: config_loader.ActionBase{ + ActionBase: configloader.ActionBase{ Name: "checkCluster", - APICall: &config_loader.APICall{ + APICall: &configloader.APICall{ Method: "GET", - URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterId }}", + URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterID }}", }, }, - Capture: []config_loader.CaptureField{ + Capture: []configloader.CaptureField{ { Name: "readyConditionStatus", - FieldExpressionDef: config_loader.FieldExpressionDef{ - Expression: `status.conditions.filter(c, c.type == "Ready").size() > 0 ? status.conditions.filter(c, c.type == "Ready")[0].status : "False"`, + FieldExpressionDef: configloader.FieldExpressionDef{ + Expression: `status.conditions.filter(c, c.type == "Ready").size() > 0 ? ` + + `status.conditions.filter(c, c.type == "Ready")[0].status : "False"`, }, }, }, - Conditions: []config_loader.Condition{ + Conditions: []configloader.Condition{ {Field: "readyConditionStatus", Operator: "equals", Value: "True"}, }, }, }, - Post: &config_loader.PostConfig{ - PostActions: []config_loader.PostAction{ + Post: &configloader.PostConfig{ + PostActions: []configloader.PostAction{ { // Log action in post-actions - ActionBase: config_loader.ActionBase{ + ActionBase: configloader.ActionBase{ Name: "logCompletion", - Log: &config_loader.LogAction{ - Message: "Completed processing cluster {{ .clusterId }} with resource {{ .resourceId }}", + Log: &configloader.LogAction{ + Message: "Completed processing cluster {{ .clusterID }} with resource {{ .resourceId }}", Level: "info", }, }, }, { // Log with warning level - ActionBase: config_loader.ActionBase{ + ActionBase: configloader.ActionBase{ Name: "logWarning", - Log: &config_loader.LogAction{ - Message: "This is a warning for cluster {{ .clusterId }}", + Log: &configloader.LogAction{ + Message: "This is a warning for cluster {{ .clusterID }}", Level: "warning", }, }, @@ -1004,7 +1011,7 @@ func TestExecutor_LogAction(t *testing.T) { }, } - apiClient, err := hyperfleet_api.NewClient(testLog()) + apiClient, err := hyperfleetapi.NewClient(testLog()) assert.NoError(t, err) exec, err := executor.NewBuilder(). WithConfig(config). @@ -1071,8 +1078,8 @@ func TestExecutor_PostActionAPIFailure(t *testing.T) { // Create config and executor config := createTestConfig(mockAPI.URL()) - apiClient, err := hyperfleet_api.NewClient(testLog(), - hyperfleet_api.WithRetryAttempts(1), + apiClient, err := hyperfleetapi.NewClient(testLog(), + hyperfleetapi.WithRetryAttempts(1), ) assert.NoError(t, err) exec, err := executor.NewBuilder(). @@ -1168,47 +1175,48 @@ func TestExecutor_ExecutionError_CELAccess(t *testing.T) { t.Setenv("HYPERFLEET_API_VERSION", "v1") // Create config with CEL expressions that access adapter.executionError - config := &config_loader.Config{ - Adapter: config_loader.AdapterInfo{ + config := &configloader.Config{ + Adapter: configloader.AdapterInfo{ Name: "executionError-cel-test", Version: "1.0.0", }, - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ - Timeout: 10 * time.Second, RetryAttempts: 1, RetryBackoff: hyperfleet_api.BackoffConstant, + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ + Timeout: 10 * time.Second, RetryAttempts: 1, RetryBackoff: hyperfleetapi.BackoffConstant, }, }, - Params: []config_loader.Parameter{ + Params: []configloader.Parameter{ {Name: "hyperfleetApiBaseUrl", Source: "env.HYPERFLEET_API_BASE_URL", Required: true}, {Name: "hyperfleetApiVersion", Default: "v1"}, - {Name: "clusterId", Source: "event.id", Required: true}, + {Name: "clusterID", Source: "event.id", Required: true}, }, - Preconditions: []config_loader.Precondition{ + Preconditions: []configloader.Precondition{ { - ActionBase: config_loader.ActionBase{ + ActionBase: configloader.ActionBase{ Name: "clusterStatus", - APICall: &config_loader.APICall{ + APICall: &configloader.APICall{ Method: "GET", - URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterId }}", + URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterID }}", Timeout: "5s", }, }, - Capture: []config_loader.CaptureField{ + Capture: []configloader.CaptureField{ { Name: "readyConditionStatus", - FieldExpressionDef: config_loader.FieldExpressionDef{ - Expression: `status.conditions.filter(c, c.type == "Ready").size() > 0 ? status.conditions.filter(c, c.type == "Ready")[0].status : "False"`, + FieldExpressionDef: configloader.FieldExpressionDef{ + Expression: `status.conditions.filter(c, c.type == "Ready").size() > 0 ? ` + + `status.conditions.filter(c, c.type == "Ready")[0].status : "False"`, }, }, }, - Conditions: []config_loader.Condition{ + Conditions: []configloader.Condition{ {Field: "readyConditionStatus", Operator: "equals", Value: "True"}, }, }, }, - Resources: []config_loader.Resource{}, - Post: &config_loader.PostConfig{ - Payloads: []config_loader.Payload{ + Resources: []configloader.Resource{}, + Post: &configloader.PostConfig{ + Payloads: []configloader.Payload{ { Name: "errorReportPayload", Build: map[string]interface{}{ @@ -1217,13 +1225,16 @@ func TestExecutor_ExecutionError_CELAccess(t *testing.T) { "expression": "has(adapter.executionError) && adapter.executionError != null", }, "errorPhase": map[string]interface{}{ - "expression": "has(adapter.executionError) && adapter.executionError != null ? adapter.executionError.phase : \"no_error\"", + "expression": "has(adapter.executionError) && adapter.executionError != null ? " + + `adapter.executionError.phase : "no_error"`, }, "errorStep": map[string]interface{}{ - "expression": "has(adapter.executionError) && adapter.executionError != null ? adapter.executionError.step : \"no_step\"", + "expression": "has(adapter.executionError) && adapter.executionError != null ? " + + `adapter.executionError.step : "no_step"`, }, "errorMessage": map[string]interface{}{ - "expression": "has(adapter.executionError) && adapter.executionError != null ? adapter.executionError.message : \"no_message\"", + "expression": "has(adapter.executionError) && adapter.executionError != null ? " + + `adapter.executionError.message : "no_message"`, }, // Also test that other adapter fields still work "executionStatus": map[string]interface{}{ @@ -1232,19 +1243,19 @@ func TestExecutor_ExecutionError_CELAccess(t *testing.T) { "errorReason": map[string]interface{}{ "expression": "adapter.errorReason", }, - "clusterId": map[string]interface{}{ - "value": "{{ .clusterId }}", + "clusterID": map[string]interface{}{ + "value": "{{ .clusterID }}", }, }, }, }, - PostActions: []config_loader.PostAction{ + PostActions: []configloader.PostAction{ { - ActionBase: config_loader.ActionBase{ + ActionBase: configloader.ActionBase{ Name: "reportError", - APICall: &config_loader.APICall{ + APICall: &configloader.APICall{ Method: "POST", - URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterId }}/error-report", + URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterID }}/error-report", Body: "{{ .errorReportPayload }}", Timeout: "5s", }, @@ -1254,7 +1265,7 @@ func TestExecutor_ExecutionError_CELAccess(t *testing.T) { }, } - apiClient, err := hyperfleet_api.NewClient(testLog(), hyperfleet_api.WithRetryAttempts(1)) + apiClient, err := hyperfleetapi.NewClient(testLog(), hyperfleetapi.WithRetryAttempts(1)) assert.NoError(t, err) exec, err := executor.NewBuilder(). WithConfig(config). @@ -1313,7 +1324,7 @@ func TestExecutor_ExecutionError_CELAccess(t *testing.T) { // Verify other adapter fields still accessible assert.Equal(t, "failed", reportPayload["executionStatus"], "executionStatus should be 'failed'") assert.NotEmpty(t, reportPayload["errorReason"], "errorReason should be populated") - assert.Equal(t, "cluster-cel-error-test", reportPayload["clusterId"], "clusterId should match") + assert.Equal(t, "cluster-cel-error-test", reportPayload["clusterID"], "clusterID should match") t.Logf("CEL expressions successfully accessed executionError:") t.Logf(" hasError: %v", reportPayload["hasError"]) @@ -1339,32 +1350,32 @@ func TestExecutor_PayloadBuildFailure(t *testing.T) { t.Setenv("HYPERFLEET_API_VERSION", "v1") // Create config with invalid CEL expression in payload build (will cause build failure) - config := &config_loader.Config{ - Adapter: config_loader.AdapterInfo{ + config := &configloader.Config{ + Adapter: configloader.AdapterInfo{ Name: "payload-build-fail-test", Version: "1.0.0", }, - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ Timeout: 10 * time.Second, RetryAttempts: 1, }, }, - Params: []config_loader.Parameter{ + Params: []configloader.Parameter{ {Name: "hyperfleetApiBaseUrl", Source: "env.HYPERFLEET_API_BASE_URL", Required: true}, {Name: "hyperfleetApiVersion", Default: "v1"}, - {Name: "clusterId", Source: "event.id", Required: true}, + {Name: "clusterID", Source: "event.id", Required: true}, }, - Preconditions: []config_loader.Precondition{ + Preconditions: []configloader.Precondition{ { - ActionBase: config_loader.ActionBase{Name: "simpleCheck"}, - Conditions: []config_loader.Condition{ - {Field: "clusterId", Operator: "equals", Value: "test-cluster"}, + ActionBase: configloader.ActionBase{Name: "simpleCheck"}, + Conditions: []configloader.Condition{ + {Field: "clusterID", Operator: "equals", Value: "test-cluster"}, }, }, }, - Resources: []config_loader.Resource{}, - Post: &config_loader.PostConfig{ - Payloads: []config_loader.Payload{ + Resources: []configloader.Resource{}, + Post: &configloader.PostConfig{ + Payloads: []configloader.Payload{ { Name: "badPayload", Build: map[string]interface{}{ @@ -1375,13 +1386,13 @@ func TestExecutor_PayloadBuildFailure(t *testing.T) { }, }, }, - PostActions: []config_loader.PostAction{ + PostActions: []configloader.PostAction{ { - ActionBase: config_loader.ActionBase{ + ActionBase: configloader.ActionBase{ Name: "shouldNotExecute", - APICall: &config_loader.APICall{ + APICall: &configloader.APICall{ Method: "POST", - URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterId }}/statuses", + URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterID }}/statuses", Body: "{{ .badPayload }}", Timeout: "5s", }, @@ -1391,7 +1402,7 @@ func TestExecutor_PayloadBuildFailure(t *testing.T) { }, } - apiClient, err := hyperfleet_api.NewClient(testLog()) + apiClient, err := hyperfleetapi.NewClient(testLog()) assert.NoError(t, err) // Use capture logger to verify error logging log, logCapture := logger.NewCaptureLogger() diff --git a/test/integration/executor/executor_k8s_integration_test.go b/test/integration/executor/executor_k8s_integration_test.go index 9508357..b151b1b 100644 --- a/test/integration/executor/executor_k8s_integration_test.go +++ b/test/integration/executor/executor_k8s_integration_test.go @@ -1,4 +1,4 @@ -package executor_integration_test +package executorintegrationtest import ( "context" @@ -12,9 +12,9 @@ import ( "time" "github.com/cloudevents/sdk-go/v2/event" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/executor" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleet_api" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/hyperfleetapi" "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/manifest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -122,18 +122,18 @@ func (m *k8sTestAPIServer) GetStatusResponses() []map[string]interface{} { } // createK8sTestEvent creates a CloudEvent for K8s integration testing -func createK8sTestEvent(clusterId string) *event.Event { +func createK8sTestEvent(clusterID string) *event.Event { evt := event.New() - evt.SetID("k8s-test-event-" + clusterId) + evt.SetID("k8s-test-event-" + clusterID) evt.SetType("com.redhat.hyperfleet.cluster.provision") evt.SetSource("k8s-integration-test") evt.SetTime(time.Now()) eventData := map[string]interface{}{ - "id": clusterId, + "id": clusterID, "resource_type": "cluster", "generation": "gen-001", - "href": "/api/v1/clusters/" + clusterId, + "href": "/api/v1/clusters/" + clusterID, } eventDataBytes, _ := json.Marshal(eventData) _ = evt.SetData(event.ApplicationJSON, eventDataBytes) @@ -142,21 +142,20 @@ func createK8sTestEvent(clusterId string) *event.Event { } // createK8sTestConfig creates a unified Config with K8s resources -func createK8sTestConfig(apiBaseURL, testNamespace string) *config_loader.Config { - _ = apiBaseURL // Base URL is pulled from env params - return &config_loader.Config{ - Adapter: config_loader.AdapterInfo{ +func createK8sTestConfig(testNamespace string) *configloader.Config { + return &configloader.Config{ + Adapter: configloader.AdapterInfo{ Name: "k8s-test-adapter", Version: "1.0.0", }, - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ Timeout: 10 * time.Second, RetryAttempts: 1, - RetryBackoff: hyperfleet_api.BackoffConstant, + RetryBackoff: hyperfleetapi.BackoffConstant, }, }, - Params: []config_loader.Parameter{ + Params: []configloader.Parameter{ { Name: "hyperfleetApiBaseUrl", Source: "env.HYPERFLEET_API_BASE_URL", @@ -169,7 +168,7 @@ func createK8sTestConfig(apiBaseURL, testNamespace string) *config_loader.Config Required: false, }, { - Name: "clusterId", + Name: "clusterID", Source: "event.id", Required: true, }, @@ -179,59 +178,62 @@ func createK8sTestConfig(apiBaseURL, testNamespace string) *config_loader.Config Required: false, }, }, - Preconditions: []config_loader.Precondition{ + Preconditions: []configloader.Precondition{ { - ActionBase: config_loader.ActionBase{ + ActionBase: configloader.ActionBase{ Name: "clusterStatus", - APICall: &config_loader.APICall{ - Method: "GET", - URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterId }}", + APICall: &configloader.APICall{ + Method: "GET", + URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/" + + "{{ .clusterID }}", Timeout: "5s", }, }, - Capture: []config_loader.CaptureField{ - {Name: "clusterName", FieldExpressionDef: config_loader.FieldExpressionDef{Field: "name"}}, + Capture: []configloader.CaptureField{ + {Name: "clusterName", FieldExpressionDef: configloader.FieldExpressionDef{Field: "name"}}, { Name: "readyConditionStatus", - FieldExpressionDef: config_loader.FieldExpressionDef{ - Expression: `status.conditions.filter(c, c.type == "Ready").size() > 0 ? status.conditions.filter(c, c.type == "Ready")[0].status : "False"`, + FieldExpressionDef: configloader.FieldExpressionDef{ + Expression: `status.conditions.filter(c, c.type == "Ready").size() > 0 ` + + `? status.conditions.filter(c, c.type == "Ready")[0].status ` + + `: "False"`, }, }, - {Name: "region", FieldExpressionDef: config_loader.FieldExpressionDef{Field: "spec.region"}}, - {Name: "cloudProvider", FieldExpressionDef: config_loader.FieldExpressionDef{Field: "spec.provider"}}, + {Name: "region", FieldExpressionDef: configloader.FieldExpressionDef{Field: "spec.region"}}, + {Name: "cloudProvider", FieldExpressionDef: configloader.FieldExpressionDef{Field: "spec.provider"}}, }, - Conditions: []config_loader.Condition{ + Conditions: []configloader.Condition{ {Field: "readyConditionStatus", Operator: "equals", Value: "True"}, }, }, }, // K8s Resources to create - Resources: []config_loader.Resource{ + Resources: []configloader.Resource{ { Name: "clusterConfigMap", Manifest: map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ - "name": "cluster-config-{{ .clusterId }}", + "name": "cluster-config-{{ .clusterID }}", "namespace": testNamespace, "labels": map[string]interface{}{ - "hyperfleet.io/cluster-id": "{{ .clusterId }}", + "hyperfleet.io/cluster-id": "{{ .clusterID }}", "hyperfleet.io/managed-by": "{{ .adapter.name }}", "test": "executor-integration", }, }, "data": map[string]interface{}{ - "cluster-id": "{{ .clusterId }}", + "cluster-id": "{{ .clusterID }}", "cluster-name": "{{ .clusterName }}", "region": "{{ .region }}", "provider": "{{ .cloudProvider }}", "readyStatus": "{{ .readyConditionStatus }}", }, }, - Discovery: &config_loader.DiscoveryConfig{ + Discovery: &configloader.DiscoveryConfig{ Namespace: testNamespace, - ByName: "cluster-config-{{ .clusterId }}", + ByName: "cluster-config-{{ .clusterID }}", }, }, { @@ -240,28 +242,28 @@ func createK8sTestConfig(apiBaseURL, testNamespace string) *config_loader.Config "apiVersion": "v1", "kind": "Secret", "metadata": map[string]interface{}{ - "name": "cluster-secret-{{ .clusterId }}", + "name": "cluster-secret-{{ .clusterID }}", "namespace": testNamespace, "labels": map[string]interface{}{ - "hyperfleet.io/cluster-id": "{{ .clusterId }}", + "hyperfleet.io/cluster-id": "{{ .clusterID }}", "hyperfleet.io/managed-by": "{{ .adapter.name }}", "test": "executor-integration", }, }, "type": "Opaque", "stringData": map[string]interface{}{ - "cluster-id": "{{ .clusterId }}", - "api-token": "test-token-{{ .clusterId }}", + "cluster-id": "{{ .clusterID }}", + "api-token": "test-token-{{ .clusterID }}", }, }, - Discovery: &config_loader.DiscoveryConfig{ + Discovery: &configloader.DiscoveryConfig{ Namespace: testNamespace, - ByName: "cluster-secret-{{ .clusterId }}", + ByName: "cluster-secret-{{ .clusterID }}", }, }, }, - Post: &config_loader.PostConfig{ - Payloads: []config_loader.Payload{ + Post: &configloader.PostConfig{ + Payloads: []configloader.Payload{ { Name: "clusterStatusPayload", Build: map[string]interface{}{ @@ -274,12 +276,13 @@ func createK8sTestConfig(apiBaseURL, testNamespace string) *config_loader.Config "expression": "has(adapter.errorReason) ? adapter.errorReason : \"ResourcesCreated\"", }, "message": map[string]interface{}{ - "expression": "has(adapter.errorMessage) ? adapter.errorMessage : \"ConfigMap and Secret created successfully\"", + "expression": "has(adapter.errorMessage) ? adapter.errorMessage : " + + `"ConfigMap and Secret created successfully"`, }, }, }, - "clusterId": map[string]interface{}{ - "value": "{{ .clusterId }}", + "clusterID": map[string]interface{}{ + "value": "{{ .clusterID }}", }, "resourcesCreated": map[string]interface{}{ "value": "2", @@ -287,13 +290,14 @@ func createK8sTestConfig(apiBaseURL, testNamespace string) *config_loader.Config }, }, }, - PostActions: []config_loader.PostAction{ + PostActions: []configloader.PostAction{ { - ActionBase: config_loader.ActionBase{ + ActionBase: configloader.ActionBase{ Name: "reportClusterStatus", - APICall: &config_loader.APICall{ - Method: "POST", - URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/{{ .clusterId }}/statuses", + APICall: &configloader.APICall{ + Method: "POST", + URL: "{{ .hyperfleetApiBaseUrl }}/api/{{ .hyperfleetApiVersion }}/clusters/" + + "{{ .clusterID }}/statuses", Body: "{{ .clusterStatusPayload }}", Timeout: "5s", }, @@ -324,10 +328,10 @@ func TestExecutor_K8s_CreateResources(t *testing.T) { t.Setenv("HYPERFLEET_API_VERSION", "v1") // Create config with K8s resources - config := createK8sTestConfig(mockAPI.URL(), testNamespace) - apiClient, err := hyperfleet_api.NewClient(testLog(), - hyperfleet_api.WithTimeout(10*time.Second), - hyperfleet_api.WithRetryAttempts(1), + config := createK8sTestConfig(testNamespace) + apiClient, err := hyperfleetapi.NewClient(testLog(), + hyperfleetapi.WithTimeout(10*time.Second), + hyperfleetapi.WithRetryAttempts(1), ) require.NoError(t, err) @@ -341,8 +345,8 @@ func TestExecutor_K8s_CreateResources(t *testing.T) { require.NoError(t, err) // Create test event - clusterId := fmt.Sprintf("cluster-%d", time.Now().UnixNano()) - evt := createK8sTestEvent(clusterId) + clusterID := fmt.Sprintf("cluster-%d", time.Now().UnixNano()) + evt := createK8sTestEvent(clusterID) // Execute ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) @@ -374,11 +378,14 @@ func TestExecutor_K8s_CreateResources(t *testing.T) { assert.Equal(t, executor.StatusSuccess, secretResult.Status, "Secret creation should succeed") assert.Equal(t, manifest.OperationCreate, secretResult.Operation) assert.Equal(t, "Secret", secretResult.Kind) - t.Logf("Secret created: %s/%s (operation: %s)", secretResult.Namespace, secretResult.ResourceName, secretResult.Operation) + t.Logf( + "Secret created: %s/%s (operation: %s)", + secretResult.Namespace, secretResult.ResourceName, secretResult.Operation, + ) // Verify ConfigMap exists in K8s cmGVK := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"} - cmName := fmt.Sprintf("cluster-config-%s", clusterId) + cmName := fmt.Sprintf("cluster-config-%s", clusterID) cm, err := k8sEnv.Client.GetResource(ctx, cmGVK, testNamespace, cmName, nil) require.NoError(t, err, "ConfigMap should exist in K8s") assert.Equal(t, cmName, cm.GetName()) @@ -387,7 +394,7 @@ func TestExecutor_K8s_CreateResources(t *testing.T) { cmData, found, err := unstructured.NestedStringMap(cm.Object, "data") require.NoError(t, err) require.True(t, found, "ConfigMap should have data") - assert.Equal(t, clusterId, cmData["cluster-id"]) + assert.Equal(t, clusterID, cmData["cluster-id"]) assert.Equal(t, "test-cluster", cmData["cluster-name"]) assert.Equal(t, "us-east-1", cmData["region"]) assert.Equal(t, "aws", cmData["provider"]) @@ -396,12 +403,12 @@ func TestExecutor_K8s_CreateResources(t *testing.T) { // Verify ConfigMap labels cmLabels := cm.GetLabels() - assert.Equal(t, clusterId, cmLabels["hyperfleet.io/cluster-id"]) + assert.Equal(t, clusterID, cmLabels["hyperfleet.io/cluster-id"]) assert.Equal(t, "k8s-test-adapter", cmLabels["hyperfleet.io/managed-by"]) // Verify Secret exists in K8s secretGVK := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"} - secretName := fmt.Sprintf("cluster-secret-%s", clusterId) + secretName := fmt.Sprintf("cluster-secret-%s", clusterID) secret, err := k8sEnv.Client.GetResource(ctx, secretGVK, testNamespace, secretName, nil) require.NoError(t, err, "Secret should exist in K8s") assert.Equal(t, secretName, secret.GetName()) @@ -413,17 +420,20 @@ func TestExecutor_K8s_CreateResources(t *testing.T) { status := statusResponses[0] t.Logf("Status reported: %+v", status) + // Verify clusterID is correctly templated in status payload + assert.Equal(t, clusterID, status["clusterID"], + "Status payload should contain the correct clusterID") + if conditions, ok := status["conditions"].(map[string]interface{}); ok { if applied, ok := conditions["applied"].(map[string]interface{}); ok { - // Status should be true (adapter.executionStatus == "success") - assert.Equal(t, true, applied["status"], "Applied status should be true") - - // Reason should be "ResourcesCreated" (default, no adapter.errorReason) - assert.Equal(t, "ResourcesCreated", applied["reason"], "Should use default reason for success") - - // Message should be success message (default, no adapter.errorMessage) + assert.Equal(t, true, applied["status"], + "Applied status should be true") + assert.Equal(t, "ResourcesCreated", applied["reason"], + "Should use default reason for success") if message, ok := applied["message"].(string); ok { - assert.Equal(t, "ConfigMap and Secret created successfully", message, "Should use default success message") + assert.Equal(t, + "ConfigMap and Secret created successfully", + message, "Should use default success message") } } } @@ -444,7 +454,7 @@ func TestExecutor_K8s_UpdateExistingResource(t *testing.T) { t.Setenv("HYPERFLEET_API_BASE_URL", mockAPI.URL()) t.Setenv("HYPERFLEET_API_VERSION", "v1") - clusterId := fmt.Sprintf("update-cluster-%d", time.Now().UnixNano()) + clusterID := fmt.Sprintf("update-cluster-%d", time.Now().UnixNano()) // Pre-create the ConfigMap existingCM := &unstructured.Unstructured{ @@ -452,16 +462,16 @@ func TestExecutor_K8s_UpdateExistingResource(t *testing.T) { "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ - "name": fmt.Sprintf("cluster-config-%s", clusterId), + "name": fmt.Sprintf("cluster-config-%s", clusterID), "namespace": testNamespace, "labels": map[string]interface{}{ - "hyperfleet.io/cluster-id": clusterId, + "hyperfleet.io/cluster-id": clusterID, "hyperfleet.io/managed-by": "k8s-test-adapter", "test": "executor-integration", }, }, "data": map[string]interface{}{ - "cluster-id": clusterId, + "cluster-id": clusterID, "readyStatus": "False", // Old value }, }, @@ -474,11 +484,11 @@ func TestExecutor_K8s_UpdateExistingResource(t *testing.T) { t.Logf("Pre-created ConfigMap with readyStatus=False") // Create executor - config := createK8sTestConfig(mockAPI.URL(), testNamespace) + config := createK8sTestConfig(testNamespace) // Only include ConfigMap resource for this test config.Resources = config.Resources[:1] - apiClient, err := hyperfleet_api.NewClient(testLog()) + apiClient, err := hyperfleetapi.NewClient(testLog()) require.NoError(t, err) exec, err := executor.NewBuilder(). WithConfig(config). @@ -489,7 +499,7 @@ func TestExecutor_K8s_UpdateExistingResource(t *testing.T) { require.NoError(t, err) // Execute - should update existing resource - evt := createK8sTestEvent(clusterId) + evt := createK8sTestEvent(clusterID) result := exec.Execute(ctx, evt) require.Equal(t, executor.StatusSuccess, result.Status, "Execution should succeed: errors=%v", result.Errors) @@ -502,7 +512,7 @@ func TestExecutor_K8s_UpdateExistingResource(t *testing.T) { // Verify ConfigMap was updated with new data cmGVK := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"} - cmName := fmt.Sprintf("cluster-config-%s", clusterId) + cmName := fmt.Sprintf("cluster-config-%s", clusterID) updatedCM, err := k8sEnv.Client.GetResource(ctx, cmGVK, testNamespace, cmName, nil) require.NoError(t, err) @@ -549,35 +559,35 @@ func TestExecutor_K8s_DiscoveryByLabels(t *testing.T) { t.Setenv("HYPERFLEET_API_BASE_URL", mockAPI.URL()) t.Setenv("HYPERFLEET_API_VERSION", "v1") - clusterId := fmt.Sprintf("discovery-cluster-%d", time.Now().UnixNano()) + clusterID := fmt.Sprintf("discovery-cluster-%d", time.Now().UnixNano()) // Create config with label-based discovery - config := createK8sTestConfig(mockAPI.URL(), testNamespace) + config := createK8sTestConfig(testNamespace) // Modify to use label selector instead of byName - config.Resources = []config_loader.Resource{ + config.Resources = []configloader.Resource{ { Name: "clusterConfigMap", Manifest: map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ - "name": "cluster-config-{{ .clusterId }}", + "name": "cluster-config-{{ .clusterID }}", "namespace": testNamespace, "labels": map[string]interface{}{ - "hyperfleet.io/cluster-id": "{{ .clusterId }}", + "hyperfleet.io/cluster-id": "{{ .clusterID }}", "hyperfleet.io/managed-by": "{{ .adapter.name }}", "app": "cluster-config", }, }, "data": map[string]interface{}{ - "cluster-id": "{{ .clusterId }}", + "cluster-id": "{{ .clusterID }}", }, }, - Discovery: &config_loader.DiscoveryConfig{ + Discovery: &configloader.DiscoveryConfig{ Namespace: testNamespace, - BySelectors: &config_loader.SelectorConfig{ + BySelectors: &configloader.SelectorConfig{ LabelSelector: map[string]string{ - "hyperfleet.io/cluster-id": "{{ .clusterId }}", + "hyperfleet.io/cluster-id": "{{ .clusterID }}", "app": "cluster-config", }, }, @@ -585,7 +595,7 @@ func TestExecutor_K8s_DiscoveryByLabels(t *testing.T) { }, } - apiClient, err := hyperfleet_api.NewClient(testLog()) + apiClient, err := hyperfleetapi.NewClient(testLog()) require.NoError(t, err) exec, err := executor.NewBuilder(). WithConfig(config). @@ -598,14 +608,14 @@ func TestExecutor_K8s_DiscoveryByLabels(t *testing.T) { ctx := context.Background() // First execution - should create - evt := createK8sTestEvent(clusterId) + evt := createK8sTestEvent(clusterID) result1 := exec.Execute(ctx, evt) require.Equal(t, executor.StatusSuccess, result1.Status) assert.Equal(t, manifest.OperationCreate, result1.ResourceResults[0].Operation) t.Logf("First execution: %s", result1.ResourceResults[0].Operation) // Second execution - should find by labels and update - evt2 := createK8sTestEvent(clusterId) + evt2 := createK8sTestEvent(clusterID) result2 := exec.Execute(ctx, evt2) require.Equal(t, executor.StatusSuccess, result2.Status) assert.Equal(t, manifest.OperationUpdate, result2.ResourceResults[0].Operation) @@ -627,11 +637,11 @@ func TestExecutor_K8s_RecreateOnChange(t *testing.T) { t.Setenv("HYPERFLEET_API_BASE_URL", mockAPI.URL()) t.Setenv("HYPERFLEET_API_VERSION", "v1") - clusterId := fmt.Sprintf("recreate-cluster-%d", time.Now().UnixNano()) + clusterID := fmt.Sprintf("recreate-cluster-%d", time.Now().UnixNano()) // Create config with recreateOnChange - config := createK8sTestConfig(mockAPI.URL(), testNamespace) - config.Resources = []config_loader.Resource{ + config := createK8sTestConfig(testNamespace) + config.Resources = []configloader.Resource{ { Name: "clusterConfigMap", RecreateOnChange: true, // Enable recreate @@ -639,24 +649,24 @@ func TestExecutor_K8s_RecreateOnChange(t *testing.T) { "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ - "name": "cluster-config-{{ .clusterId }}", + "name": "cluster-config-{{ .clusterID }}", "namespace": testNamespace, "labels": map[string]interface{}{ - "hyperfleet.io/cluster-id": "{{ .clusterId }}", + "hyperfleet.io/cluster-id": "{{ .clusterID }}", }, }, "data": map[string]interface{}{ - "cluster-id": "{{ .clusterId }}", + "cluster-id": "{{ .clusterID }}", }, }, - Discovery: &config_loader.DiscoveryConfig{ + Discovery: &configloader.DiscoveryConfig{ Namespace: testNamespace, - ByName: "cluster-config-{{ .clusterId }}", + ByName: "cluster-config-{{ .clusterID }}", }, }, } - apiClient, err := hyperfleet_api.NewClient(testLog()) + apiClient, err := hyperfleetapi.NewClient(testLog()) require.NoError(t, err) exec, err := executor.NewBuilder(). WithConfig(config). @@ -669,21 +679,21 @@ func TestExecutor_K8s_RecreateOnChange(t *testing.T) { ctx := context.Background() // First execution - create - evt := createK8sTestEvent(clusterId) + evt := createK8sTestEvent(clusterID) result1 := exec.Execute(ctx, evt) require.Equal(t, executor.StatusSuccess, result1.Status) assert.Equal(t, manifest.OperationCreate, result1.ResourceResults[0].Operation) // Get the original UID cmGVK := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"} - cmName := fmt.Sprintf("cluster-config-%s", clusterId) + cmName := fmt.Sprintf("cluster-config-%s", clusterID) originalCM, err := k8sEnv.Client.GetResource(ctx, cmGVK, testNamespace, cmName, nil) require.NoError(t, err) originalUID := originalCM.GetUID() t.Logf("Original ConfigMap UID: %s", originalUID) // Second execution - should recreate (delete + create) - evt2 := createK8sTestEvent(clusterId) + evt2 := createK8sTestEvent(clusterID) result2 := exec.Execute(ctx, evt2) require.Equal(t, executor.StatusSuccess, result2.Status) assert.Equal(t, manifest.OperationRecreate, result2.ResourceResults[0].Operation) @@ -713,8 +723,8 @@ func TestExecutor_K8s_MultipleResourceTypes(t *testing.T) { t.Setenv("HYPERFLEET_API_VERSION", "v1") // Execute with default config (ConfigMap + Secret) - config := createK8sTestConfig(mockAPI.URL(), testNamespace) - apiClient, err := hyperfleet_api.NewClient(testLog()) + config := createK8sTestConfig(testNamespace) + apiClient, err := hyperfleetapi.NewClient(testLog()) require.NoError(t, err) exec, err := executor.NewBuilder(). WithConfig(config). @@ -724,8 +734,8 @@ func TestExecutor_K8s_MultipleResourceTypes(t *testing.T) { Build() require.NoError(t, err) - clusterId := fmt.Sprintf("multi-cluster-%d", time.Now().UnixNano()) - evt := createK8sTestEvent(clusterId) + clusterID := fmt.Sprintf("multi-cluster-%d", time.Now().UnixNano()) + evt := createK8sTestEvent(clusterID) result := exec.Execute(context.Background(), evt) @@ -741,7 +751,7 @@ func TestExecutor_K8s_MultipleResourceTypes(t *testing.T) { // Verify we can list resources by labels cmGVK := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"} - selector := fmt.Sprintf("hyperfleet.io/cluster-id=%s", clusterId) + selector := fmt.Sprintf("hyperfleet.io/cluster-id=%s", clusterID) list, err := k8sEnv.Client.ListResources(context.Background(), cmGVK, testNamespace, selector) require.NoError(t, err) assert.Len(t, list.Items, 1, "Should find 1 ConfigMap with cluster label") @@ -761,8 +771,8 @@ func TestExecutor_K8s_ResourceCreationFailure(t *testing.T) { t.Setenv("HYPERFLEET_API_BASE_URL", mockAPI.URL()) t.Setenv("HYPERFLEET_API_VERSION", "v1") - config := createK8sTestConfig(mockAPI.URL(), nonExistentNamespace) - apiClient, err := hyperfleet_api.NewClient(testLog()) + config := createK8sTestConfig(nonExistentNamespace) + apiClient, err := hyperfleetapi.NewClient(testLog()) require.NoError(t, err) exec, err := executor.NewBuilder(). WithConfig(config). @@ -844,7 +854,7 @@ func TestExecutor_K8s_MultipleMatchingResources(t *testing.T) { t.Setenv("HYPERFLEET_API_BASE_URL", mockAPI.URL()) t.Setenv("HYPERFLEET_API_VERSION", "v1") - clusterId := fmt.Sprintf("multi-match-%d", time.Now().UnixNano()) + clusterID := fmt.Sprintf("multi-match-%d", time.Now().UnixNano()) ctx := context.Background() // Pre-create multiple ConfigMaps with the same labels but different names @@ -854,10 +864,10 @@ func TestExecutor_K8s_MultipleMatchingResources(t *testing.T) { "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ - "name": fmt.Sprintf("config-%s-%d", clusterId, i), + "name": fmt.Sprintf("config-%s-%d", clusterID, i), "namespace": testNamespace, "labels": map[string]interface{}{ - "hyperfleet.io/cluster-id": clusterId, + "hyperfleet.io/cluster-id": clusterID, "app": "multi-match-test", }, }, @@ -874,34 +884,34 @@ func TestExecutor_K8s_MultipleMatchingResources(t *testing.T) { // Create config WITHOUT discovery - just create a new resource // Discovery-based update logic is not yet implemented - config := &config_loader.Config{ - Adapter: config_loader.AdapterInfo{Name: "multi-match-test", Version: "1.0.0"}, - Clients: config_loader.ClientsConfig{ - HyperfleetAPI: config_loader.HyperfleetAPIConfig{ + config := &configloader.Config{ + Adapter: configloader.AdapterInfo{Name: "multi-match-test", Version: "1.0.0"}, + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ Timeout: 10 * time.Second, RetryAttempts: 1, }, }, - Params: []config_loader.Parameter{ + Params: []configloader.Parameter{ {Name: "hyperfleetApiBaseUrl", Source: "env.HYPERFLEET_API_BASE_URL", Required: true}, {Name: "hyperfleetApiVersion", Default: "v1"}, - {Name: "clusterId", Source: "event.id", Required: true}, + {Name: "clusterID", Source: "event.id", Required: true}, }, // No preconditions - this test focuses on resource creation - Resources: []config_loader.Resource{ + Resources: []configloader.Resource{ { Name: "clusterConfig", Manifest: map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ - "name": "config-{{ .clusterId }}-new", + "name": "config-{{ .clusterID }}-new", "labels": map[string]interface{}{ - "hyperfleet.io/cluster-id": "{{ .clusterId }}", + "hyperfleet.io/cluster-id": "{{ .clusterID }}", "app": "multi-match-test", }, }, "data": map[string]interface{}{ - "cluster-id": "{{ .clusterId }}", + "cluster-id": "{{ .clusterID }}", "created": "true", }, }, @@ -910,7 +920,7 @@ func TestExecutor_K8s_MultipleMatchingResources(t *testing.T) { }, } - apiClient, err := hyperfleet_api.NewClient(testLog()) + apiClient, err := hyperfleetapi.NewClient(testLog()) require.NoError(t, err) exec, err := executor.NewBuilder(). WithConfig(config). @@ -920,7 +930,7 @@ func TestExecutor_K8s_MultipleMatchingResources(t *testing.T) { Build() require.NoError(t, err) - evt := createK8sTestEvent(clusterId) + evt := createK8sTestEvent(clusterID) result := exec.Execute(ctx, evt) require.Equal(t, executor.StatusSuccess, result.Status, "Execution should succeed: errors=%v", result.Errors) @@ -935,7 +945,7 @@ func TestExecutor_K8s_MultipleMatchingResources(t *testing.T) { // Verify we now have 4 ConfigMaps (3 pre-created + 1 new) cmGVK := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"} - selector := fmt.Sprintf("hyperfleet.io/cluster-id=%s,app=multi-match-test", clusterId) + selector := fmt.Sprintf("hyperfleet.io/cluster-id=%s,app=multi-match-test", clusterID) list, err := k8sEnv.Client.ListResources(ctx, cmGVK, testNamespace, selector) require.NoError(t, err) assert.Len(t, list.Items, 4, "Should have 4 ConfigMaps (3 pre-created + 1 new)") @@ -952,7 +962,8 @@ func TestExecutor_K8s_MultipleMatchingResources(t *testing.T) { assert.Equal(t, 1, createdCount, "Exactly one ConfigMap should be created") } -// TestExecutor_K8s_PostActionsAfterPreconditionNotMet tests that post actions execute even when preconditions don't match +// TestExecutor_K8s_PostActionsAfterPreconditionNotMet tests that post actions execute even when preconditions +// don't match func TestExecutor_K8s_PostActionsAfterPreconditionNotMet(t *testing.T) { k8sEnv := SetupK8sTestEnv(t) defer k8sEnv.Cleanup(t) @@ -983,8 +994,8 @@ func TestExecutor_K8s_PostActionsAfterPreconditionNotMet(t *testing.T) { t.Setenv("HYPERFLEET_API_BASE_URL", mockAPI.URL()) t.Setenv("HYPERFLEET_API_VERSION", "v1") - config := createK8sTestConfig(mockAPI.URL(), testNamespace) - apiClient, err := hyperfleet_api.NewClient(testLog()) + config := createK8sTestConfig(testNamespace) + apiClient, err := hyperfleetapi.NewClient(testLog()) require.NoError(t, err) exec, err := executor.NewBuilder(). WithConfig(config). @@ -994,8 +1005,8 @@ func TestExecutor_K8s_PostActionsAfterPreconditionNotMet(t *testing.T) { Build() require.NoError(t, err) - clusterId := fmt.Sprintf("precond-fail-%d", time.Now().UnixNano()) - evt := createK8sTestEvent(clusterId) + clusterID := fmt.Sprintf("precond-fail-%d", time.Now().UnixNano()) + evt := createK8sTestEvent(clusterID) result := exec.Execute(context.Background(), evt) diff --git a/test/integration/executor/main_test.go b/test/integration/executor/main_test.go index 660184f..935955c 100644 --- a/test/integration/executor/main_test.go +++ b/test/integration/executor/main_test.go @@ -1,4 +1,4 @@ -package executor_integration_test +package executorintegrationtest import ( "context" @@ -10,7 +10,7 @@ import ( "testing" "time" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/k8s_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/k8sclient" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" "github.com/openshift-hyperfleet/hyperfleet-adapter/test/integration/testutil" "github.com/testcontainers/testcontainers-go" @@ -51,10 +51,10 @@ func TestMain(m *testing.M) { // Quick check if testcontainers can work ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() provider, err := testcontainers.NewDockerProvider() if err != nil { + cancel() setupErr = err println("โš ๏ธ Warning: Could not connect to container runtime:", err.Error()) println(" K8s tests will be skipped") @@ -63,6 +63,7 @@ func TestMain(m *testing.M) { info, err := provider.DaemonHost(ctx) _ = provider.Close() + cancel() if err != nil { setupErr = err @@ -147,7 +148,7 @@ func setupSharedK8sEnvtestEnv() (*K8sTestEnv, error) { println(" โœ… API server is ready!") // Create K8s client - client, err := k8s_client.NewClientFromConfig(ctx, restConfig, log) + client, err := k8sclient.NewClientFromConfig(ctx, restConfig, log) if err != nil { sharedContainer.Cleanup() return nil, fmt.Errorf("failed to create K8s client: %w", err) diff --git a/test/integration/executor/setup_test.go b/test/integration/executor/setup_test.go index 913d3df..6f0d80d 100644 --- a/test/integration/executor/setup_test.go +++ b/test/integration/executor/setup_test.go @@ -1,10 +1,10 @@ -package executor_integration_test +package executorintegrationtest import ( "context" "testing" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/k8s_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/k8sclient" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -14,7 +14,7 @@ import ( // K8sTestEnv wraps the K8s test environment for executor tests type K8sTestEnv struct { - Client *k8s_client.Client + Client *k8sclient.Client Config *rest.Config Ctx context.Context Log logger.Logger diff --git a/test/integration/k8s_client/README.md b/test/integration/k8sclient/README.md similarity index 92% rename from test/integration/k8s_client/README.md rename to test/integration/k8sclient/README.md index 57b3f5f..6d8caaa 100644 --- a/test/integration/k8s_client/README.md +++ b/test/integration/k8sclient/README.md @@ -1,6 +1,6 @@ # K8s Client Integration Tests -Integration tests for the Kubernetes client (`internal/k8s_client`) using real Kubernetes API servers. +Integration tests for the Kubernetes client (`internal/k8sclient`) using real Kubernetes API servers. ## Quick Start @@ -36,7 +36,7 @@ These integration tests verify: ## Test Files ``` -test/integration/k8s_client/ +test/integration/k8sclient/ โ”œโ”€โ”€ README.md # This file โ”œโ”€โ”€ helper_selector.go # Strategy selection โ”œโ”€โ”€ helper_envtest_prebuilt.go # Pre-built testing with envtest implementation @@ -69,11 +69,11 @@ If you need to run only k8s_client tests: ```bash # Pre-built envtest strategy INTEGRATION_ENVTEST_IMAGE=localhost/hyperfleet-integration-test:latest \ - go test -v -tags=integration ./test/integration/k8s_client/... -timeout 30m + go test -v -tags=integration ./test/integration/k8sclient/... -timeout 30m # K3s strategy INTEGRATION_STRATEGY=k3s \ - go test -v -tags=integration ./test/integration/k8s_client/... -timeout 30m + go test -v -tags=integration ./test/integration/k8sclient/... -timeout 30m ``` **Note**: Direct `go test` requires manual setup. Use `make test-integration` for proper configuration. @@ -81,17 +81,17 @@ INTEGRATION_STRATEGY=k3s \ ## Test Results **Pre-built Envtest Strategy:** -``` +```text PASS -ok github.com/openshift-hyperfleet/hyperfleet-adapter/test/integration/k8s_client 192.048s +ok github.com/openshift-hyperfleet/hyperfleet-adapter/test/integration/k8sclient 192.048s ``` - 10 test suites, each creating fresh containers - ~19s per test suite (container startup + API server initialization) **K3s Strategy:** -``` +```text PASS -ok github.com/openshift-hyperfleet/hyperfleet-adapter/test/integration/k8s_client 26.148s +ok github.com/openshift-hyperfleet/hyperfleet-adapter/test/integration/k8sclient 26.148s ``` - 10 test suites, each creating fresh K3s clusters - ~2-3s per test suite @@ -199,6 +199,6 @@ See the main documentation for detailed troubleshooting: ## Additional Resources - [Main Integration Test Documentation](../README.md) -- [k8s_client Package Documentation](../../../internal/k8s_client/README.md) +- [k8sclient Package Documentation](../../../internal/k8sclient/README.md) - [Testcontainers for Go](https://golang.testcontainers.org/) - [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) diff --git a/test/integration/k8s_client/client_integration_test.go b/test/integration/k8sclient/client_integration_test.go similarity index 99% rename from test/integration/k8s_client/client_integration_test.go rename to test/integration/k8sclient/client_integration_test.go index 926fbb8..48687e0 100644 --- a/test/integration/k8s_client/client_integration_test.go +++ b/test/integration/k8sclient/client_integration_test.go @@ -1,6 +1,6 @@ // This file contains integration tests for the K8s client. -package k8s_client_integration +package k8sclientintegration import ( "testing" @@ -14,7 +14,7 @@ import ( ) // gvk provides commonly used GroupVersionKinds for integration tests. -// This is a local copy to avoid depending on test-only exports from k8s_client. +// This is a local copy to avoid depending on test-only exports from k8sclient. var gvk = struct { Namespace schema.GroupVersionKind Pod schema.GroupVersionKind @@ -433,7 +433,9 @@ func TestIntegration_PatchResource(t *testing.T) { t.Run("patch non-existent resource returns error", func(t *testing.T) { patchData := []byte(`{"data": {"key": "value"}}`) - _, err := env.GetClient().PatchResource(env.GetContext(), gvk.ConfigMap, "default", "non-existent-cm-12345", patchData) + _, err := env.GetClient().PatchResource( + env.GetContext(), gvk.ConfigMap, "default", "non-existent-cm-12345", patchData, + ) require.Error(t, err) assert.True(t, k8serrors.IsNotFound(err), "Should return NotFound error") }) diff --git a/test/integration/k8s_client/helper_envtest_prebuilt.go b/test/integration/k8sclient/helper_envtest_prebuilt.go similarity index 88% rename from test/integration/k8s_client/helper_envtest_prebuilt.go rename to test/integration/k8sclient/helper_envtest_prebuilt.go index fbfdc5c..9689ac3 100644 --- a/test/integration/k8s_client/helper_envtest_prebuilt.go +++ b/test/integration/k8sclient/helper_envtest_prebuilt.go @@ -1,6 +1,6 @@ // This file contains helper functions for setting up a pre-built image integration test environment. -package k8s_client_integration +package k8sclientintegration import ( "context" @@ -17,7 +17,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/rest" - k8s_client "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/k8s_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/k8sclient" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" "github.com/openshift-hyperfleet/hyperfleet-adapter/test/integration/testutil" ) @@ -50,7 +50,10 @@ func waitForAPIServerReady(kubeAPIServer string, timeout time.Duration) error { deadline := time.Now().Add(timeout) backoff := 500 * time.Millisecond - for { + var lastErr error + var lastStatus int + + for time.Now().Before(deadline) { req, err := http.NewRequest(http.MethodGet, healthURL, nil) if err != nil { return fmt.Errorf("failed to create request: %w", err) @@ -58,28 +61,34 @@ func waitForAPIServerReady(kubeAPIServer string, timeout time.Duration) error { req.Header.Set("Authorization", "Bearer "+EnvtestBearerToken) resp, err := client.Do(req) - if err == nil { - _ = resp.Body.Close() - if resp.StatusCode == http.StatusOK { + if err != nil { + lastErr = err + } else { + lastStatus = resp.StatusCode + if closeErr := resp.Body.Close(); closeErr != nil { + lastErr = closeErr + } else if lastStatus == http.StatusOK { return nil + } else { + lastErr = nil } } - if time.Now().After(deadline) { - if err != nil { - return fmt.Errorf("API server not ready after %v: last error: %w", timeout, err) - } - return fmt.Errorf("API server not ready after %v: last status code: %d", timeout, resp.StatusCode) - } - time.Sleep(backoff) } + + if lastErr != nil { + return fmt.Errorf( + "API server not ready after %v: last error: %w", timeout, lastErr) + } + return fmt.Errorf( + "API server not ready after %v: last status code: %d", timeout, lastStatus) } // TestEnvPrebuilt holds the test environment for pre-built image integration tests type TestEnvPrebuilt struct { Container testcontainers.Container - Client *k8s_client.Client + Client *k8sclient.Client Config *rest.Config Ctx context.Context Log logger.Logger @@ -155,7 +164,7 @@ func setupSharedTestEnv() (*TestEnvPrebuilt, error) { } // Create client - client, err := k8s_client.NewClientFromConfig(ctx, restConfig, log) + client, err := k8sclient.NewClientFromConfig(ctx, restConfig, log) if err != nil { sharedContainer.Cleanup() return nil, fmt.Errorf("failed to create K8s client: %w", err) @@ -189,7 +198,7 @@ func (e *TestEnvPrebuilt) CleanupSharedEnv() { } // createDefaultNamespaceNoTest creates the default namespace without requiring *testing.T -func createDefaultNamespaceNoTest(client *k8s_client.Client, ctx context.Context) error { +func createDefaultNamespaceNoTest(client *k8sclient.Client, ctx context.Context) error { ns := &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", diff --git a/test/integration/k8s_client/helper_selector.go b/test/integration/k8sclient/helper_selector.go similarity index 88% rename from test/integration/k8s_client/helper_selector.go rename to test/integration/k8sclient/helper_selector.go index 81485e0..e7fc681 100644 --- a/test/integration/k8s_client/helper_selector.go +++ b/test/integration/k8sclient/helper_selector.go @@ -1,19 +1,19 @@ // This file contains helper functions for selecting the appropriate integration test environment. -package k8s_client_integration +package k8sclientintegration import ( "context" "testing" - k8s_client "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/k8s_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/k8sclient" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" "k8s.io/client-go/rest" ) // TestEnv is a common interface for all integration test environments type TestEnv interface { - GetClient() *k8s_client.Client + GetClient() *k8sclient.Client GetConfig() *rest.Config GetContext() context.Context GetLogger() logger.Logger @@ -24,7 +24,7 @@ type TestEnv interface { var _ TestEnv = (*TestEnvPrebuilt)(nil) // GetClient returns the k8s client -func (e *TestEnvPrebuilt) GetClient() *k8s_client.Client { +func (e *TestEnvPrebuilt) GetClient() *k8sclient.Client { return e.Client } diff --git a/test/integration/k8s_client/main_test.go b/test/integration/k8sclient/main_test.go similarity index 94% rename from test/integration/k8s_client/main_test.go rename to test/integration/k8sclient/main_test.go index ddecaaa..f5422ea 100644 --- a/test/integration/k8s_client/main_test.go +++ b/test/integration/k8sclient/main_test.go @@ -1,7 +1,7 @@ // main_test.go provides shared test setup for integration tests. // It starts a single envtest container that is reused across all test functions. -package k8s_client_integration +package k8sclientintegration import ( "context" @@ -37,7 +37,6 @@ func TestMain(m *testing.M) { // Quick check if testcontainers can work ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() provider, err := testcontainers.NewDockerProvider() @@ -69,6 +68,7 @@ func TestMain(m *testing.M) { } } } + cancel() println() // Run tests (they will skip if setupErr != nil) @@ -87,9 +87,13 @@ func TestMain(m *testing.M) { } // GetSharedEnv returns the shared test environment. +// If in short mode, the test is skipped. // If setup failed, the test will be failed with the setup error. func GetSharedEnv(t *testing.T) TestEnv { t.Helper() + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } require.NoError(t, setupErr, "Shared environment setup failed") require.NotNil(t, sharedEnv, "Shared test environment is not initialized") return sharedEnv diff --git a/test/integration/maestro_client/client_integration_test.go b/test/integration/maestroclient/client_integration_test.go similarity index 85% rename from test/integration/maestro_client/client_integration_test.go rename to test/integration/maestroclient/client_integration_test.go index f1f11cb..68d3c2c 100644 --- a/test/integration/maestro_client/client_integration_test.go +++ b/test/integration/maestroclient/client_integration_test.go @@ -1,4 +1,4 @@ -package maestro_client_integration +package maestroclientintegration import ( "context" @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/maestro_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/maestroclient" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/constants" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" "github.com/stretchr/testify/assert" @@ -18,7 +18,7 @@ import ( // testClient holds resources for a test client that need cleanup type testClient struct { - Client *maestro_client.Client + Client *maestroclient.Client Ctx context.Context Cancel context.CancelFunc } @@ -50,14 +50,14 @@ func createTestClient(t *testing.T, sourceID string, timeout time.Duration) *tes ctx, cancel := context.WithTimeout(context.Background(), timeout) - config := &maestro_client.Config{ + config := &maestroclient.Config{ MaestroServerAddr: env.MaestroServerAddr, GRPCServerAddr: env.MaestroGRPCAddr, SourceID: sourceID, Insecure: true, } - client, err := maestro_client.NewMaestroClient(ctx, config, log) + client, err := maestroclient.NewMaestroClient(ctx, config, log) if err != nil { cancel() require.NoError(t, err, "Should create Maestro client successfully") @@ -130,7 +130,9 @@ func TestMaestroClientCreateManifestWork(t *testing.T) { created, err := tc.Client.CreateManifestWork(tc.Ctx, consumerName, work) // Consumer should be registered during test setup, so this should succeed - require.NoError(t, err, "CreateManifestWork should succeed (consumer %s should be registered)", consumerName) + require.NoError( + t, err, "CreateManifestWork should succeed (consumer %s should be registered)", consumerName, + ) require.NotNil(t, created) assert.Equal(t, work.Name, created.Name) t.Logf("Created ManifestWork: %s/%s", created.Namespace, created.Name) @@ -147,7 +149,9 @@ func TestMaestroClientListManifestWorks(t *testing.T) { list, err := tc.Client.ListManifestWorks(tc.Ctx, consumerName, "") // Consumer should be registered during test setup, so this should succeed - require.NoError(t, err, "ListManifestWorks should succeed (consumer %s should be registered)", consumerName) + require.NoError( + t, err, "ListManifestWorks should succeed (consumer %s should be registered)", consumerName, + ) require.NotNil(t, list) t.Logf("Found %d ManifestWorks for consumer %s", len(list.Items), consumerName) } @@ -204,16 +208,27 @@ func TestMaestroClientApplyManifestWork(t *testing.T) { applied, err := tc.Client.ApplyManifestWork(tc.Ctx, consumerName, work) // Consumer should be registered during test setup, so this should succeed - require.NoError(t, err, "ApplyManifestWork should succeed (consumer %s should be registered)", consumerName) + require.NoError( + t, err, "ApplyManifestWork should succeed (consumer %s should be registered)", consumerName, + ) require.NotNil(t, applied) require.NotNil(t, applied.Work) - t.Logf("Applied ManifestWork: %s/%s (operation=%s)", applied.Work.Namespace, applied.Work.Name, applied.Operation) + t.Logf( + "Applied ManifestWork: %s/%s (operation=%s)", applied.Work.Namespace, applied.Work.Name, applied.Operation, + ) // Now apply again with updated generation (should update) work.Annotations[constants.AnnotationGeneration] = "2" - // Safe: manifest structure is defined above in this test with known nested maps - configMapManifest["metadata"].(map[string]interface{})["annotations"].(map[string]interface{})[constants.AnnotationGeneration] = "2" - configMapManifest["data"].(map[string]interface{})["key2"] = "value2" + + metadata, ok := configMapManifest["metadata"].(map[string]interface{}) + require.True(t, ok, "configMapManifest should have metadata map") + annotations, ok := metadata["annotations"].(map[string]interface{}) + require.True(t, ok, "metadata should have annotations map") + annotations[constants.AnnotationGeneration] = "2" + + data, ok := configMapManifest["data"].(map[string]interface{}) + require.True(t, ok, "configMapManifest should have data map") + data["key2"] = "value2" configMapJSON, _ = json.Marshal(configMapManifest) work.Spec.Workload.Manifests[0].Raw = configMapJSON @@ -221,7 +236,9 @@ func TestMaestroClientApplyManifestWork(t *testing.T) { require.NoError(t, err, "ApplyManifestWork (update) should succeed") require.NotNil(t, updated) require.NotNil(t, updated.Work) - t.Logf("Updated ManifestWork: %s/%s (operation=%s)", updated.Work.Namespace, updated.Work.Name, updated.Operation) + t.Logf( + "Updated ManifestWork: %s/%s (operation=%s)", updated.Work.Namespace, updated.Work.Name, updated.Operation, + ) } // TestMaestroClientGenerationSkip tests that apply skips when generation matches @@ -290,6 +307,8 @@ func TestMaestroClientGenerationSkip(t *testing.T) { "ManifestWork name should match when generation unchanged (skip)") assert.Equal(t, result1.Work.Namespace, result2.Work.Namespace, "ManifestWork namespace should match when generation unchanged (skip)") - t.Logf("Skip test passed - operation=%s, result1.ResourceVersion=%s, result2.ResourceVersion=%s", - result2.Operation, result1.Work.ResourceVersion, result2.Work.ResourceVersion) + t.Logf( + "Skip test passed - operation=%s, result1.ResourceVersion=%s, result2.ResourceVersion=%s", + result2.Operation, result1.Work.ResourceVersion, result2.Work.ResourceVersion, + ) } diff --git a/test/integration/maestro_client/client_tls_config_integration_test.go b/test/integration/maestroclient/client_tls_config_integration_test.go similarity index 80% rename from test/integration/maestro_client/client_tls_config_integration_test.go rename to test/integration/maestroclient/client_tls_config_integration_test.go index 0f0e457..4121d87 100644 --- a/test/integration/maestro_client/client_tls_config_integration_test.go +++ b/test/integration/maestroclient/client_tls_config_integration_test.go @@ -1,4 +1,4 @@ -package maestro_client_integration +package maestroclientintegration import ( "fmt" @@ -7,17 +7,23 @@ import ( "testing" "time" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/maestro_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/configloader" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/maestroclient" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// buildMaestroClientConfigFromLoaded reproduces the same mapping logic as -// createMaestroClient in cmd/adapter/main.go. This is intentionally duplicated -// so the test catches drift between main.go and the config structs. -func buildMaestroClientConfigFromLoaded(maestroConfig *config_loader.MaestroClientConfig) (*maestro_client.Config, error) { - config := &maestro_client.Config{ +const ( + testInsecureTrue = "true" +) + +// buildMaestroClientConfigFromLoaded reproduces the same mapping logic as createMaestroClient +// in cmd/adapter/main.go. This is intentionally duplicated so the test catches drift between +// main.go and the config structs. +func buildMaestroClientConfigFromLoaded( + maestroConfig *configloader.MaestroClientConfig, +) (*maestroclient.Config, error) { + config := &maestroclient.Config{ MaestroServerAddr: maestroConfig.HTTPServerAddress, GRPCServerAddr: maestroConfig.GRPCServerAddress, SourceID: maestroConfig.SourceID, @@ -27,7 +33,9 @@ func buildMaestroClientConfigFromLoaded(maestroConfig *config_loader.MaestroClie if maestroConfig.Timeout != "" { d, err := time.ParseDuration(maestroConfig.Timeout) if err != nil { - return nil, fmt.Errorf("invalid maestro timeout %q: %w", maestroConfig.Timeout, err) + return nil, fmt.Errorf( + "invalid maestro timeout %q: %w", maestroConfig.Timeout, err, + ) } config.HTTPTimeout = d } @@ -35,7 +43,10 @@ func buildMaestroClientConfigFromLoaded(maestroConfig *config_loader.MaestroClie if maestroConfig.ServerHealthinessTimeout != "" { d, err := time.ParseDuration(maestroConfig.ServerHealthinessTimeout) if err != nil { - return nil, fmt.Errorf("invalid maestro serverHealthinessTimeout %q: %w", maestroConfig.ServerHealthinessTimeout, err) + return nil, fmt.Errorf( + "invalid maestro serverHealthinessTimeout %q: %w", + maestroConfig.ServerHealthinessTimeout, err, + ) } config.ServerHealthinessTimeout = d } @@ -50,8 +61,8 @@ func buildMaestroClientConfigFromLoaded(maestroConfig *config_loader.MaestroClie return config, nil } -// writeTestAdapterConfig writes a minimal AdapterConfig YAML that references -// the given TLS cert paths and Maestro addresses. +// writeTestAdapterConfig writes a minimal AdapterConfig YAML that references the given TLS +// cert paths and Maestro addresses. func writeTestAdapterConfig(t *testing.T, dir string, opts map[string]string) string { t.Helper() @@ -67,8 +78,8 @@ func writeTestAdapterConfig(t *testing.T, dir string, opts map[string]string) st } insecure := "false" - if opts["insecure"] == "true" { - insecure = "true" + if opts["insecure"] == testInsecureTrue { + insecure = testInsecureTrue } yaml := fmt.Sprintf(`adapter: @@ -110,9 +121,9 @@ func writeMinimalTaskConfig(t *testing.T, dir string) string { return path } -// TestTLSConfigLoadAndConnect_MutualTLS loads an AdapterConfig YAML with mTLS -// settings, parses it through the config loader, maps it to maestro_client.Config -// (same logic as main.go), creates a real client, and connects to the TLS Maestro. +// TestTLSConfigLoadAndConnect_MutualTLS loads an AdapterConfig YAML with mTLS settings, +// parses it through the config loader, maps it to maestroclient.Config (same logic as +// main.go), creates a real client, and connects to the TLS Maestro. func TestTLSConfigLoadAndConnect_MutualTLS(t *testing.T) { env := GetSharedEnv(t) requireTLSEnv(t, env) @@ -130,10 +141,10 @@ func TestTLSConfigLoadAndConnect_MutualTLS(t *testing.T) { }) taskPath := writeMinimalTaskConfig(t, tmpDir) - cfg, err := config_loader.LoadConfig( - config_loader.WithAdapterConfigPath(adapterPath), - config_loader.WithTaskConfigPath(taskPath), - config_loader.WithSkipSemanticValidation(), + cfg, err := configloader.LoadConfig( + configloader.WithAdapterConfigPath(adapterPath), + configloader.WithTaskConfigPath(taskPath), + configloader.WithSkipSemanticValidation(), ) require.NoError(t, err, "Config loading should succeed") require.NotNil(t, cfg, "Config should not be nil") @@ -169,12 +180,14 @@ func TestTLSConfigLoadAndConnect_MutualTLS(t *testing.T) { defer tc.Close() list, err := tc.Client.ListManifestWorks(tc.Ctx, "test-cluster-list", "") - require.NoError(t, err, "ListManifestWorks over TLS (config-loaded mTLS) should succeed") + require.NoError( + t, err, "ListManifestWorks over TLS (config-loaded mTLS) should succeed", + ) t.Logf("Config-loaded mTLS: listed %d ManifestWorks", len(list.Items)) } -// TestTLSConfigLoadAndConnect_CAOnly loads config with only caFile (no client certs), -// verifying the CA-only TLS path works end-to-end from config file. +// TestTLSConfigLoadAndConnect_CAOnly loads config with only caFile (no client certs), verifying +// the CA-only TLS path works end-to-end from config file. func TestTLSConfigLoadAndConnect_CAOnly(t *testing.T) { env := GetSharedEnv(t) requireTLSEnv(t, env) @@ -192,10 +205,10 @@ func TestTLSConfigLoadAndConnect_CAOnly(t *testing.T) { }) taskPath := writeMinimalTaskConfig(t, tmpDir) - cfg, err := config_loader.LoadConfig( - config_loader.WithAdapterConfigPath(adapterPath), - config_loader.WithTaskConfigPath(taskPath), - config_loader.WithSkipSemanticValidation(), + cfg, err := configloader.LoadConfig( + configloader.WithAdapterConfigPath(adapterPath), + configloader.WithTaskConfigPath(taskPath), + configloader.WithSkipSemanticValidation(), ) require.NoError(t, err) require.NotNil(t, cfg, "Config should not be nil") @@ -212,12 +225,14 @@ func TestTLSConfigLoadAndConnect_CAOnly(t *testing.T) { defer tc.Close() list, err := tc.Client.ListManifestWorks(tc.Ctx, "test-cluster-list", "") - require.NoError(t, err, "ListManifestWorks over TLS (config-loaded CA-only) should succeed") + require.NoError( + t, err, "ListManifestWorks over TLS (config-loaded CA-only) should succeed", + ) t.Logf("Config-loaded CA-only: listed %d ManifestWorks", len(list.Items)) } -// TestTLSConfigLoadAndConnect_Insecure loads an insecure config (no TLS) -// and connects to the plaintext Maestro, verifying the insecure path from config. +// TestTLSConfigLoadAndConnect_Insecure loads an insecure config (no TLS) and connects to the +// plaintext Maestro, verifying the insecure path from config. func TestTLSConfigLoadAndConnect_Insecure(t *testing.T) { env := GetSharedEnv(t) @@ -227,14 +242,14 @@ func TestTLSConfigLoadAndConnect_Insecure(t *testing.T) { "grpcAddr": env.MaestroGRPCAddr, "httpAddr": env.MaestroServerAddr, "sourceId": "config-insecure", - "insecure": "true", + "insecure": testInsecureTrue, }) taskPath := writeMinimalTaskConfig(t, tmpDir) - cfg, err := config_loader.LoadConfig( - config_loader.WithAdapterConfigPath(adapterPath), - config_loader.WithTaskConfigPath(taskPath), - config_loader.WithSkipSemanticValidation(), + cfg, err := configloader.LoadConfig( + configloader.WithAdapterConfigPath(adapterPath), + configloader.WithTaskConfigPath(taskPath), + configloader.WithSkipSemanticValidation(), ) require.NoError(t, err) require.NotNil(t, cfg, "Config should not be nil") @@ -255,8 +270,8 @@ func TestTLSConfigLoadAndConnect_Insecure(t *testing.T) { t.Logf("Config-loaded insecure: listed %d ManifestWorks", len(list.Items)) } -// TestTLSConfigLoadAndConnect_EnvOverride verifies that environment variables -// override YAML config values, simulating the production Viper override path. +// TestTLSConfigLoadAndConnect_EnvOverride verifies that environment variables override YAML +// config values, simulating the production Viper override path. func TestTLSConfigLoadAndConnect_EnvOverride(t *testing.T) { env := GetSharedEnv(t) requireTLSEnv(t, env) @@ -281,10 +296,10 @@ func TestTLSConfigLoadAndConnect_EnvOverride(t *testing.T) { t.Setenv("HYPERFLEET_MAESTRO_SOURCE_ID", "config-tls-env-override") t.Setenv("HYPERFLEET_MAESTRO_INSECURE", "false") - cfg, err := config_loader.LoadConfig( - config_loader.WithAdapterConfigPath(adapterPath), - config_loader.WithTaskConfigPath(taskPath), - config_loader.WithSkipSemanticValidation(), + cfg, err := configloader.LoadConfig( + configloader.WithAdapterConfigPath(adapterPath), + configloader.WithTaskConfigPath(taskPath), + configloader.WithSkipSemanticValidation(), ) require.NoError(t, err) require.NotNil(t, cfg, "Config should not be nil") @@ -302,6 +317,8 @@ func TestTLSConfigLoadAndConnect_EnvOverride(t *testing.T) { defer tc.Close() list, err := tc.Client.ListManifestWorks(tc.Ctx, "test-cluster-list", "") - require.NoError(t, err, "ListManifestWorks with env-overridden TLS config should succeed") + require.NoError( + t, err, "ListManifestWorks with env-overridden TLS config should succeed", + ) t.Logf("Config-loaded with env override: listed %d ManifestWorks", len(list.Items)) } diff --git a/test/integration/maestro_client/client_tls_integration_test.go b/test/integration/maestroclient/client_tls_integration_test.go similarity index 94% rename from test/integration/maestro_client/client_tls_integration_test.go rename to test/integration/maestroclient/client_tls_integration_test.go index bbe1621..526fba5 100644 --- a/test/integration/maestro_client/client_tls_integration_test.go +++ b/test/integration/maestroclient/client_tls_integration_test.go @@ -1,4 +1,4 @@ -package maestro_client_integration +package maestroclientintegration import ( "context" @@ -15,14 +15,14 @@ import ( "testing" "time" - "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/maestro_client" + "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/maestroclient" "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // createTLSTestClient creates a Maestro client configured for TLS. -func createTLSTestClient(t *testing.T, config *maestro_client.Config, timeout time.Duration) *testClient { +func createTLSTestClient(t *testing.T, config *maestroclient.Config, timeout time.Duration) *testClient { t.Helper() log, err := logger.NewLogger(logger.Config{ @@ -34,7 +34,7 @@ func createTLSTestClient(t *testing.T, config *maestro_client.Config, timeout ti ctx, cancel := context.WithTimeout(context.Background(), timeout) - client, err := maestro_client.NewMaestroClient(ctx, config, log) + client, err := maestroclient.NewMaestroClient(ctx, config, log) if err != nil { cancel() require.NoError(t, err, "Should create TLS Maestro client successfully") @@ -54,7 +54,7 @@ func TestTLSClientWithCAOnly(t *testing.T) { env := GetSharedEnv(t) requireTLSEnv(t, env) - config := &maestro_client.Config{ + config := &maestroclient.Config{ MaestroServerAddr: env.TLSMaestroServerAddr, GRPCServerAddr: env.TLSMaestroGRPCAddr, SourceID: "tls-test-ca-only", @@ -75,7 +75,7 @@ func TestTLSClientWithMutualTLS(t *testing.T) { env := GetSharedEnv(t) requireTLSEnv(t, env) - config := &maestro_client.Config{ + config := &maestroclient.Config{ MaestroServerAddr: env.TLSMaestroServerAddr, GRPCServerAddr: env.TLSMaestroGRPCAddr, SourceID: "tls-test-mtls", @@ -103,7 +103,7 @@ func TestTLSClientWithToken(t *testing.T) { tokenFile := filepath.Join(tokenDir, "token") require.NoError(t, os.WriteFile(tokenFile, []byte("test-bearer-token"), 0o600)) - config := &maestro_client.Config{ + config := &maestroclient.Config{ MaestroServerAddr: env.TLSMaestroServerAddr, GRPCServerAddr: env.TLSMaestroGRPCAddr, SourceID: "tls-test-token", @@ -128,7 +128,7 @@ func TestTLSClientWithWrongCA(t *testing.T) { wrongCAFile := writeWrongCA(t) - config := &maestro_client.Config{ + config := &maestroclient.Config{ MaestroServerAddr: env.TLSMaestroServerAddr, GRPCServerAddr: env.TLSMaestroGRPCAddr, SourceID: "tls-test-wrong-ca", @@ -157,7 +157,7 @@ func TestTLSClientPlaintextHTTPToTLSServer(t *testing.T) { // The server expects a TLS handshake, so the plaintext HTTP request will fail. plaintextAddr := fmt.Sprintf("http://127.0.0.1:%s", env.TLSMaestroHTTPPort) - config := &maestro_client.Config{ + config := &maestroclient.Config{ MaestroServerAddr: plaintextAddr, GRPCServerAddr: env.TLSMaestroGRPCAddr, SourceID: "tls-test-plaintext", @@ -180,7 +180,7 @@ func TestTLSClientHTTPSWithCA(t *testing.T) { env := GetSharedEnv(t) requireTLSEnv(t, env) - config := &maestro_client.Config{ + config := &maestroclient.Config{ MaestroServerAddr: env.TLSMaestroServerAddr, GRPCServerAddr: env.TLSMaestroGRPCAddr, SourceID: "tls-test-https-ca", @@ -204,7 +204,7 @@ func TestTLSClientSeparateHTTPCA(t *testing.T) { env := GetSharedEnv(t) requireTLSEnv(t, env) - config := &maestro_client.Config{ + config := &maestroclient.Config{ MaestroServerAddr: env.TLSMaestroServerAddr, GRPCServerAddr: env.TLSMaestroGRPCAddr, SourceID: "tls-test-http-ca", @@ -228,7 +228,7 @@ func TestTLSNoConfigFails(t *testing.T) { env := GetSharedEnv(t) requireTLSEnv(t, env) - config := &maestro_client.Config{ + config := &maestroclient.Config{ MaestroServerAddr: env.TLSMaestroServerAddr, GRPCServerAddr: env.TLSMaestroGRPCAddr, SourceID: "tls-test-no-config", @@ -245,7 +245,7 @@ func TestTLSNoConfigFails(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - _, err = maestro_client.NewMaestroClient(ctx, config, log) + _, err = maestroclient.NewMaestroClient(ctx, config, log) require.Error(t, err, "Should fail when Insecure=false and no TLS config provided") assert.Contains(t, err.Error(), "no TLS configuration provided") t.Logf("No TLS config correctly rejected: %v", err) diff --git a/test/integration/maestro_client/main_test.go b/test/integration/maestroclient/main_test.go similarity index 92% rename from test/integration/maestro_client/main_test.go rename to test/integration/maestroclient/main_test.go index 7788f48..fdda397 100644 --- a/test/integration/maestro_client/main_test.go +++ b/test/integration/maestroclient/main_test.go @@ -1,7 +1,7 @@ -// main_test.go provides shared test setup for Maestro integration tests. -// It starts PostgreSQL and Maestro server containers that are reused across all test functions. +// main_test.go provides shared test setup for Maestro integration tests. It starts PostgreSQL +// and Maestro server containers that are reused across all test functions. -package maestro_client_integration +package maestroclientintegration import ( "context" @@ -82,21 +82,24 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } - // Skip on ARM64 Macs unless MAESTRO_ARM64_TEST is set (user has local ARM64 image) - // To run on ARM64, build a local image from the Maestro source and tag it as: + // Skip on ARM64 Macs unless MAESTRO_ARM64_TEST is set (user has local ARM64 image). To run + // on ARM64, build a local image from the Maestro source and tag it as: // quay.io/redhat-user-workloads/maestro-rhtap-tenant/maestro/maestro:latest if runtime.GOARCH == "arm64" && os.Getenv("MAESTRO_ARM64_TEST") != "true" { - skipReason = "ARM64 architecture without MAESTRO_ARM64_TEST=true (set this env if you have a local ARM64 Maestro image)" + skipReason = "ARM64 architecture without MAESTRO_ARM64_TEST=true " + + "(set this env if you have a local ARM64 Maestro image)" println("โš ๏ธ Skipping Maestro integration tests on ARM64") println(" The official Maestro image is amd64 only.") println(" To run locally, build from source and set MAESTRO_ARM64_TEST=true:") - println(" cd /path/to/maestro && podman build -t quay.io/redhat-user-workloads/maestro-rhtap-tenant/maestro/maestro:latest .") + println( + " cd /path/to/maestro && " + + "podman build -t quay.io/redhat-user-workloads/maestro-rhtap-tenant/maestro/maestro:latest .", + ) os.Exit(m.Run()) } // Quick check if testcontainers can work ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() provider, err := testcontainers.NewDockerProvider() if err != nil { @@ -142,6 +145,7 @@ func TestMain(m *testing.M) { } } } + cancel() println() // Run tests diff --git a/test/integration/maestro_client/setup_test.go b/test/integration/maestroclient/setup_test.go similarity index 99% rename from test/integration/maestro_client/setup_test.go rename to test/integration/maestroclient/setup_test.go index a021ccf..fd5173c 100644 --- a/test/integration/maestro_client/setup_test.go +++ b/test/integration/maestroclient/setup_test.go @@ -1,4 +1,4 @@ -package maestro_client_integration +package maestroclientintegration import ( "context" diff --git a/test/integration/maestro_client/tls_helper_test.go b/test/integration/maestroclient/tls_helper_test.go similarity index 99% rename from test/integration/maestro_client/tls_helper_test.go rename to test/integration/maestroclient/tls_helper_test.go index b5dabda..d3818f6 100644 --- a/test/integration/maestro_client/tls_helper_test.go +++ b/test/integration/maestroclient/tls_helper_test.go @@ -1,4 +1,4 @@ -package maestro_client_integration +package maestroclientintegration import ( "crypto/ecdsa" diff --git a/test/integration/testutil/container.go b/test/integration/testutil/container.go index b931a99..cdfd70a 100644 --- a/test/integration/testutil/container.go +++ b/test/integration/testutil/container.go @@ -33,7 +33,8 @@ type ContainerConfig struct { // StartupTimeout is the maximum time to wait for container to start (default: 180s) StartupTimeout time.Duration // CleanupTimeout is the maximum time to wait for container cleanup (default: 60s) - // Note: The cleanup path enforces a minimum of 60s to ensure containers have time to stop gracefully. + // Note: The cleanup path enforces a minimum of 60s to ensure containers have time to stop + // gracefully. CleanupTimeout time.Duration // RetryDelay is the base delay between retries (default: 1s, increases with attempt number) RetryDelay time.Duration @@ -125,7 +126,10 @@ func StartContainer(t *testing.T, config ContainerConfig) (*ContainerResult, err for attempt := 1; attempt <= config.MaxRetries; attempt++ { if attempt > 1 { delay := config.RetryDelay * time.Duration(attempt) - t.Logf("Retry attempt %d/%d for %s container (waiting %v)...", attempt, config.MaxRetries, config.Name, delay) + t.Logf( + "Retry attempt %d/%d for %s container (waiting %v)...", + attempt, config.MaxRetries, config.Name, delay, + ) time.Sleep(delay) } @@ -149,7 +153,9 @@ func StartContainer(t *testing.T, config ContainerConfig) (*ContainerResult, err t.Logf("Attempt %d failed but container was created. Terminating...", attempt) terminateCtx, cancel := context.WithTimeout(context.Background(), 60*time.Second) if termErr := container.Terminate(terminateCtx); termErr != nil { - t.Logf("Warning: Failed to terminate failed container from attempt %d: %v", attempt, termErr) + t.Logf( + "Warning: Failed to terminate failed container from attempt %d: %v", attempt, termErr, + ) // Try force cleanup if cid := container.GetContainerID(); cid != "" { forceCleanupContainer(t, cid) @@ -195,7 +201,9 @@ func StartContainer(t *testing.T, config ContainerConfig) (*ContainerResult, err } if err != nil { - return nil, fmt.Errorf("failed to start %s container after %d attempts: %w", config.Name, config.MaxRetries, err) + return nil, fmt.Errorf( + "failed to start %s container after %d attempts: %w", config.Name, config.MaxRetries, err, + ) } // Get container host @@ -209,7 +217,9 @@ func StartContainer(t *testing.T, config ContainerConfig) (*ContainerResult, err for _, portSpec := range config.ExposedPorts { port, err := container.MappedPort(ctx, nat.Port(portSpec)) if err != nil { - return nil, fmt.Errorf("failed to get mapped port %s for %s container: %w", portSpec, config.Name, err) + return nil, fmt.Errorf( + "failed to get mapped port %s for %s container: %w", portSpec, config.Name, err, + ) } ports[portSpec] = port.Port() } @@ -226,8 +236,8 @@ func StartContainer(t *testing.T, config ContainerConfig) (*ContainerResult, err // forceCleanupContainer attempts to force remove a specific container using docker/podman CLI. // This is a fallback when testcontainers' Terminate() fails. // -// Note: This function requires either 'docker' or 'podman' CLI to be available in PATH. -// If neither is available, cleanup will fail with a warning message suggesting manual cleanup. +// Note: This function requires either 'docker' or 'podman' CLI to be available in PATH. If +// neither is available, cleanup will fail with a warning message suggesting manual cleanup. func forceCleanupContainer(t *testing.T, containerID string) { t.Helper() @@ -238,35 +248,52 @@ func forceCleanupContainer(t *testing.T, containerID string) { // Try docker first, then podman runtimes := []string{"docker", "podman"} - for _, runtime := range runtimes { - rmCmd := exec.Command(runtime, "rm", "-f", containerID) - if output, err := rmCmd.CombinedOutput(); err == nil { - t.Logf("Force-removed container %s using %s", containerID, runtime) + for _, rt := range runtimes { + cmdCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + // #nosec G204 -- test infrastructure, commands constructed internally + rmCmd := exec.CommandContext(cmdCtx, rt, "rm", "-f", containerID) + output, err := rmCmd.CombinedOutput() + cancel() + if err == nil { + t.Logf("Force-removed container %s using %s", containerID, rt) return - } else { - // Log the error; some "not found" noise is acceptable for cleanup - t.Logf("Failed to force-remove with %s: %v (output: %s)", runtime, err, string(output)) } + // Log the error; some "not found" noise is acceptable for cleanup + t.Logf( + "Failed to force-remove with %s: %v (output: %s)", + rt, err, string(output)) } - t.Logf("WARNING: Could not force-remove container %s. It may already be removed or manual cleanup required.", containerID) - t.Logf("Run: docker rm -f %s OR podman rm -f %s", containerID, containerID) + t.Logf( + "WARNING: Could not force-remove container %s. "+ + "It may already be removed or manual cleanup required.", + containerID, + ) + t.Logf( + "Run: docker rm -f %s OR podman rm -f %s", + containerID, containerID) } // CleanupLeakedContainers removes any containers matching the given image pattern. // This can be called to clean up containers from previous failed test runs. // -// Note: This function requires either 'docker' or 'podman' CLI to be available in PATH. -// If neither is available, cleanup will silently skip (no containers found with either runtime). +// Note: This function requires either 'docker' or 'podman' CLI to be available in PATH. If +// neither is available, cleanup will silently skip (no containers found with either runtime). func CleanupLeakedContainers(t *testing.T, imagePattern string) { t.Helper() runtimes := []string{"docker", "podman"} - for _, runtime := range runtimes { + for _, rt := range runtimes { // List containers matching the image - listCmd := exec.Command(runtime, "ps", "-a", "-q", "--filter", fmt.Sprintf("ancestor=%s", imagePattern)) + listCtx, listCancel := context.WithTimeout( + context.Background(), 30*time.Second) + // #nosec G204 -- test infrastructure, commands constructed internally + listCmd := exec.CommandContext( + listCtx, rt, "ps", "-a", "-q", + "--filter", fmt.Sprintf("ancestor=%s", imagePattern)) output, err := listCmd.Output() + listCancel() if err != nil { continue // Try next runtime } @@ -282,12 +309,16 @@ func CleanupLeakedContainers(t *testing.T, imagePattern string) { if id == "" { continue } - rmCmd := exec.Command(runtime, "rm", "-f", id) + rmCtx, rmCancel := context.WithTimeout( + context.Background(), 30*time.Second) + // #nosec G204 -- test infrastructure, commands constructed internally + rmCmd := exec.CommandContext(rmCtx, rt, "rm", "-f", id) if rmErr := rmCmd.Run(); rmErr != nil { t.Logf("Warning: Failed to remove container %s: %v", id, rmErr) } else { t.Logf("Cleaned up leaked container: %s", id) } + rmCancel() } return // Success with this runtime } @@ -360,7 +391,9 @@ func (s *SharedContainer) Cleanup() { println(fmt.Sprintf("๐Ÿงน Cleaning up shared %s container...", s.Name)) if err := s.Container.Terminate(ctx); err != nil { - println(fmt.Sprintf("โš ๏ธ Warning: Failed to terminate shared %s container: %v", s.Name, err)) + println( + fmt.Sprintf("โš ๏ธ Warning: Failed to terminate shared %s container: %v", s.Name, err), + ) // Try force cleanup if cid := s.Container.GetContainerID(); cid != "" { forceCleanupContainerNoTest(cid) @@ -420,7 +453,9 @@ func StartSharedContainer(config ContainerConfig) (*SharedContainer, error) { var err error for attempt := 1; attempt <= config.MaxRetries; attempt++ { - println(fmt.Sprintf("๐Ÿš€ Starting shared %s container (attempt %d/%d)...", config.Name, attempt, config.MaxRetries)) + println( + fmt.Sprintf("๐Ÿš€ Starting shared %s container (attempt %d/%d)...", config.Name, attempt, config.MaxRetries), + ) // Create context with timeout for this attempt attemptCtx, cancel := context.WithTimeout(ctx, config.StartupTimeout) @@ -439,9 +474,14 @@ func StartSharedContainer(config ContainerConfig) (*SharedContainer, error) { // If container was created but failed to start fully, terminate it if container != nil { - terminateCtx, cancel := context.WithTimeout(context.Background(), 60*time.Second) - _ = container.Terminate(terminateCtx) - cancel() + terminateCtx, tCancel := context.WithTimeout( + context.Background(), 60*time.Second) + if termErr := container.Terminate(terminateCtx); termErr != nil { + println(fmt.Sprintf( + " Failed to terminate container: %v", termErr)) + forceCleanupContainerNoTest(container.GetContainerID()) + } + tCancel() } if attempt < config.MaxRetries { @@ -452,13 +492,22 @@ func StartSharedContainer(config ContainerConfig) (*SharedContainer, error) { } if err != nil { - return nil, fmt.Errorf("failed to start shared %s container after %d attempts: %w", config.Name, config.MaxRetries, err) + return nil, fmt.Errorf( + "failed to start shared %s container after %d attempts: %w", config.Name, config.MaxRetries, err, + ) } // Get container host host, err := container.Host(ctx) if err != nil { - _ = container.Terminate(ctx) + terminateCtx, tCancel := context.WithTimeout( + context.Background(), 60*time.Second) + if termErr := container.Terminate(terminateCtx); termErr != nil { + println(fmt.Sprintf( + " Failed to terminate container after host error: %v", termErr)) + forceCleanupContainerNoTest(container.GetContainerID()) + } + tCancel() return nil, fmt.Errorf("failed to get %s container host: %w", config.Name, err) } @@ -467,13 +516,26 @@ func StartSharedContainer(config ContainerConfig) (*SharedContainer, error) { for _, portSpec := range config.ExposedPorts { port, err := container.MappedPort(ctx, nat.Port(portSpec)) if err != nil { - _ = container.Terminate(ctx) - return nil, fmt.Errorf("failed to get mapped port %s for %s container: %w", portSpec, config.Name, err) + terminateCtx, tCancel := context.WithTimeout( + context.Background(), 60*time.Second) + if termErr := container.Terminate(terminateCtx); termErr != nil { + println(fmt.Sprintf( + " Failed to terminate container after port mapping error: %v", + termErr)) + forceCleanupContainerNoTest(container.GetContainerID()) + } + tCancel() + return nil, fmt.Errorf( + "failed to get mapped port %s for %s container: %w", + portSpec, config.Name, err, + ) } ports[portSpec] = port.Port() } - println(fmt.Sprintf("โœ… Shared %s container started successfully (host: %s)", config.Name, host)) + println( + fmt.Sprintf("โœ… Shared %s container started successfully (host: %s)", config.Name, host), + ) return &SharedContainer{ Container: container, @@ -492,13 +554,20 @@ func forceCleanupContainerNoTest(containerID string) { runtimes := []string{"docker", "podman"} - for _, runtime := range runtimes { - rmCmd := exec.Command(runtime, "rm", "-f", containerID) - if _, err := rmCmd.CombinedOutput(); err == nil { - println(fmt.Sprintf(" Force-removed container %s using %s", containerID, runtime)) + for _, rt := range runtimes { + cmdCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + // #nosec G204 -- test infrastructure, commands constructed internally + rmCmd := exec.CommandContext(cmdCtx, rt, "rm", "-f", containerID) + _, err := rmCmd.CombinedOutput() + cancel() + if err == nil { + println(fmt.Sprintf( + " Force-removed container %s using %s", containerID, rt)) return } } - println(fmt.Sprintf("โš ๏ธ Could not force-remove container %s. Manual cleanup may be required.", containerID)) + println( + fmt.Sprintf("โš ๏ธ Could not force-remove container %s. Manual cleanup may be required.", containerID), + ) } diff --git a/test/integration/testutil/mock_api_server.go b/test/integration/testutil/mock_api_server.go index 4136406..c148787 100644 --- a/test/integration/testutil/mock_api_server.go +++ b/test/integration/testutil/mock_api_server.go @@ -3,6 +3,8 @@ package testutil import ( "encoding/json" + "errors" + "io" "net/http" "net/http/httptest" "strings" @@ -79,7 +81,10 @@ func NewMockAPIServer(t *testing.T) *MockAPIServer { var bodyStr string if r.Body != nil { buf := make([]byte, 1024*1024) - n, _ := r.Body.Read(buf) + n, readErr := r.Body.Read(buf) + if readErr != nil && !errors.Is(readErr, io.EOF) { + t.Logf("Warning: error reading request body: %v", readErr) + } bodyStr = string(buf[:n]) } @@ -102,10 +107,12 @@ func NewMockAPIServer(t *testing.T) *MockAPIServer { // Check if we should fail the post action if mock.failPostAction { w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(map[string]string{ + if encodeErr := json.NewEncoder(w).Encode(map[string]string{ "error": "internal server error", "message": "failed to update cluster status", - }) + }); encodeErr != nil { + t.Logf("Warning: failed to encode error response: %v", encodeErr) + } return } @@ -114,7 +121,9 @@ func NewMockAPIServer(t *testing.T) *MockAPIServer { mock.statusResponses = append(mock.statusResponses, statusBody) } w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(map[string]string{"status": "accepted"}) + if encodeErr := json.NewEncoder(w).Encode(map[string]string{"status": "accepted"}); encodeErr != nil { + t.Logf("Warning: failed to encode status response: %v", encodeErr) + } return } @@ -123,24 +132,32 @@ func NewMockAPIServer(t *testing.T) *MockAPIServer { if r.Method == http.MethodGet { if mock.failPrecondition { w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(map[string]string{"error": "cluster not found"}) + if encodeErr := json.NewEncoder(w).Encode(map[string]string{"error": "cluster not found"}); encodeErr != nil { + t.Logf("Warning: failed to encode error response: %v", encodeErr) + } return } w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(mock.clusterResponse) + if encodeErr := json.NewEncoder(w).Encode(mock.clusterResponse); encodeErr != nil { + t.Logf("Warning: failed to encode cluster response: %v", encodeErr) + } return } case strings.Contains(r.URL.Path, "/validation/availability"): // GET validation availability w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode("available") + if encodeErr := json.NewEncoder(w).Encode("available"); encodeErr != nil { + t.Logf("Warning: failed to encode availability response: %v", encodeErr) + } return } // Default 404 w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(map[string]string{"error": "not found"}) + if encodeErr := json.NewEncoder(w).Encode(map[string]string{"error": "not found"}); encodeErr != nil { + t.Logf("Warning: failed to encode 404 response: %v", encodeErr) + } })) return mock From fdff37ad9f8e6e53ce0f0a867a67747e19948b59 Mon Sep 17 00:00:00 2001 From: Rafael Benevides Date: Thu, 19 Mar 2026 13:53:51 -0300 Subject: [PATCH 2/3] HYPERFLEET-769 - fix: set failure status on nested discovery key collision Align the collision error path with all other failure paths in executeResource by setting result.Status, result.Error, and execCtx.Adapter.ExecutionError before returning. --- internal/executor/resource_executor.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/internal/executor/resource_executor.go b/internal/executor/resource_executor.go index 4cbe705..42289bd 100644 --- a/internal/executor/resource_executor.go +++ b/internal/executor/resource_executor.go @@ -173,12 +173,22 @@ func (re *ResourceExecutor) executeResource( continue } if _, exists := execCtx.Resources[nestedName]; exists { + collisionErr := fmt.Errorf( + "nested discovery key collision: %q already exists in context", + nestedName, + ) + result.Status = StatusFailed + result.Error = collisionErr + execCtx.Adapter.ExecutionError = &ExecutionError{ + Phase: string(PhaseResources), + Step: resource.Name, + Message: collisionErr.Error(), + } return result, NewExecutorError( PhaseResources, resource.Name, - fmt.Sprintf( - "nested discovery key collision: %q already exists in context", - nestedName), - nil) + "duplicate resource context key", + collisionErr, + ) } execCtx.Resources[nestedName] = nestedObj } From 5d8798854de917915dc9a87c654f3ad2033453b2 Mon Sep 17 00:00:00 2001 From: Rafael Benevides Date: Thu, 19 Mar 2026 16:30:38 -0300 Subject: [PATCH 3/3] HYPERFLEET-702 - fix: preserve failed executionStatus on precondition error When a precondition API call fails, SetError() correctly sets adapter.executionStatus to "failed". However, SetSkipped() was called immediately after, overwriting executionStatus back to "success". This caused CEL expressions checking adapter.executionStatus (e.g., Health condition) to incorrectly evaluate as "True" instead of "False". Replace the SetSkipped() call in the precondition error path with direct field assignments that set resourcesSkipped and skipReason without overwriting the failed execution status. --- internal/executor/executor.go | 6 ++- internal/executor/executor_test.go | 72 ++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/internal/executor/executor.go b/internal/executor/executor.go index c384fb8..afef3e1 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -137,7 +137,11 @@ func (e *Executor) Execute(ctx context.Context, data interface{}) *ExecutionResu e.log.Errorf(errCtx, "Phase %s: FAILED", result.CurrentPhase) result.ResourcesSkipped = true result.SkipReason = "PreconditionFailed" - execCtx.SetSkipped("PreconditionFailed", precondOutcome.Error.Error()) + // Set skip metadata on adapter context without overwriting the failed execution status + // Note: SetSkipped() is NOT called here because it resets ExecutionStatus to "success", + // which would mask the precondition failure in CEL expressions (e.g., Health condition) + execCtx.Adapter.ResourcesSkipped = true + execCtx.Adapter.SkipReason = precondOutcome.Error.Error() // Continue to post actions for error reporting case !precondOutcome.AllMatched: // Business outcome: precondition not satisfied diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go index e08168b..29271e7 100644 --- a/internal/executor/executor_test.go +++ b/internal/executor/executor_test.go @@ -3,6 +3,7 @@ package executor import ( "context" "encoding/json" + "fmt" "testing" "github.com/cloudevents/sdk-go/v2/event" @@ -1010,6 +1011,77 @@ func TestCreateHandler_NilMetricsRecorder(t *testing.T) { }, "handler with nil MetricsRecorder should not panic") } +// TestPreconditionAPIFailure_ExecutionStatusRemainsFailed verifies that when a precondition +// API call fails, adapter.executionStatus stays "failed" and is not overwritten to "success". +// This is a regression test for a bug where SetSkipped() was called after SetError(), +// resetting executionStatus and causing Health CEL expressions to evaluate incorrectly. +func TestPreconditionAPIFailure_ExecutionStatusRemainsFailed(t *testing.T) { + // Configure mock to return an error on GET (simulating precondition API failure) + mockClient := newMockAPIClient() + mockClient.GetError = fmt.Errorf("connection refused") + mockClient.GetResponse = nil + + config := &configloader.Config{ + Adapter: configloader.AdapterInfo{ + Name: "test-adapter", + Version: "1.0.0", + }, + Clients: configloader.ClientsConfig{ + HyperfleetAPI: configloader.HyperfleetAPIConfig{ + BaseURL: "http://mock-api:8000", + Version: "v1", + }, + }, + Params: []configloader.Parameter{ + {Name: "clusterId", Source: "event.id", Required: true}, + }, + Preconditions: []configloader.Precondition{ + { + ActionBase: configloader.ActionBase{ + Name: "clusterStatus", + APICall: &configloader.APICall{ + Method: "GET", + URL: "/clusters/{{ .clusterId }}", + Timeout: "2s", + }, + }, + }, + }, + } + + exec, err := NewBuilder(). + WithConfig(config). + WithAPIClient(mockClient). + WithTransportClient(k8sclient.NewMockK8sClient()). + WithLogger(logger.NewTestLogger()). + Build() + require.NoError(t, err) + + ctx := logger.WithEventID(context.Background(), "test-precond-fail") + result := exec.Execute(ctx, map[string]interface{}{"id": "cluster-123"}) + + // Verify overall result status is failed + assert.Equal(t, StatusFailed, result.Status, "expected overall status to be failed") + assert.True(t, result.ResourcesSkipped, "resources should be skipped on precondition failure") + + // Critical assertion: verify adapter.executionStatus is "failed", not "success" + require.NotNil(t, result.ExecutionContext, "execution context should be present") + assert.Equal(t, string(StatusFailed), result.ExecutionContext.Adapter.ExecutionStatus, + "adapter.executionStatus must remain 'failed' after precondition API failure") + + // Verify error information is preserved + assert.Equal(t, "PreconditionFailed", result.ExecutionContext.Adapter.ErrorReason, + "adapter.errorReason should be 'PreconditionFailed'") + assert.NotEmpty(t, result.ExecutionContext.Adapter.ErrorMessage, + "adapter.errorMessage should contain the error details") + + // Verify skip metadata is also set (for CEL expressions that check resourcesSkipped) + assert.True(t, result.ExecutionContext.Adapter.ResourcesSkipped, + "adapter.resourcesSkipped should be true") + assert.NotEmpty(t, result.ExecutionContext.Adapter.SkipReason, + "adapter.skipReason should be set") +} + // helper functions for metrics assertions func findFamily(families []*dto.MetricFamily, name string) *dto.MetricFamily {