Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 4 additions & 60 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ linters:

misspell:
locale: US
ignore-rules:
- cancelled # British spelling is acceptable

lll:
line-length: 120
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ The first run will download golang:alpine and install envtest (~20-30 seconds).

</details>

📖 **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

Expand Down
156 changes: 99 additions & 57 deletions cmd/adapter/main.go

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package config_loader
package configloader

import (
"fmt"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package config_loader
package configloader

import (
"os"
Expand All @@ -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()
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package config_loader
package configloader

import (
"fmt"
Expand Down Expand Up @@ -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{})
Expand Down Expand Up @@ -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.") {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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:]
}
Expand Down
Loading