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..afef3e1 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) @@ -136,15 +137,19 @@ 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 - } 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 +202,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 +370,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..29271e7 100644 --- a/internal/executor/executor_test.go +++ b/internal/executor/executor_test.go @@ -3,13 +3,14 @@ package executor import ( "context" "encoding/json" + "fmt" "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 +20,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 +47,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 +55,7 @@ func TestNewExecutor(t *testing.T) { { name: "missing logger", config: &ExecutorConfig{ - Config: &config_loader.Config{}, + Config: &configloader.Config{}, APIClient: newMockAPIClient(), }, expectError: true, @@ -62,9 +63,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 +85,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 +95,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 +240,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 +262,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 +306,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 +319,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 +327,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 +335,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 +343,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 +358,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 +366,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 +374,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 +387,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 +475,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 +494,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 +506,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 +518,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 +532,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 +543,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 +581,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 +603,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 +649,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 +672,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 +692,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 +703,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 +731,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 +751,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 +772,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 +802,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 +829,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 +840,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 +872,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 +895,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 +943,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 +953,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 +986,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) @@ -1009,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 { 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..42289bd 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,31 @@ 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) + 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, + "duplicate resource context key", + collisionErr, + ) } execCtx.Resources[nestedName] = nestedObj } @@ -177,7 +203,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 +247,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 +320,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 +368,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 +411,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 +497,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