Skip to content
Merged
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
10 changes: 8 additions & 2 deletions charts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,17 @@ helm install hyperfleet-adapter oci://REGISTRY/hyperfleet-adapter \

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| adapterConfig | object | `{"create":true,"hyperfleetApi":{"baseUrl":"http://hyperfleet-api:8000","version":"v1"},"log":{"level":"info"}}` | Adapter deployment configuration. Controls how the adapter-config ConfigMap is created. Use `adapterConfig.yaml` for inline YAML, `adapterConfig.files` for chart-packaged files, or set `create: false` and provide `configMapName` to reference an existing ConfigMap. |
| adapterConfig | object | `{"create":true,"hyperfleetApi":{"auth":{"audience":"hyperfleet-api","enabled":false,"expirationSeconds":3600,"tokenCacheTtl":"30s","tokenPath":"/var/run/secrets/hyperfleet/token"},"baseUrl":"http://hyperfleet-api:8000","version":"v1"},"log":{"level":"info"}}` | Adapter deployment configuration. Controls how the adapter-config ConfigMap is created. Use `adapterConfig.yaml` for inline YAML, `adapterConfig.files` for chart-packaged files, or set `create: false` and provide `configMapName` to reference an existing ConfigMap. |
| adapterConfig.create | bool | `true` | Create the adapter-config ConfigMap |
| adapterConfig.hyperfleetApi | object | `{"baseUrl":"http://hyperfleet-api:8000","version":"v1"}` | HyperFleet API connection settings injected as environment variables |
| adapterConfig.hyperfleetApi | object | `{"auth":{"audience":"hyperfleet-api","enabled":false,"expirationSeconds":3600,"tokenCacheTtl":"30s","tokenPath":"/var/run/secrets/hyperfleet/token"},"baseUrl":"http://hyperfleet-api:8000","version":"v1"}` | HyperFleet API connection settings injected as environment variables |
| adapterConfig.hyperfleetApi.baseUrl | string | `"http://hyperfleet-api:8000"` | API base URL (`HYPERFLEET_API_BASE_URL`) |
| adapterConfig.hyperfleetApi.version | string | `"v1"` | API version (`HYPERFLEET_API_VERSION`) |
| adapterConfig.hyperfleetApi.auth | object | `{"audience":"hyperfleet-api","enabled":false,"expirationSeconds":3600,"tokenCacheTtl":"30s","tokenPath":"/var/run/secrets/hyperfleet/token"}` | JWT bearer token authentication via Kubernetes projected ServiceAccount token |
| adapterConfig.hyperfleetApi.auth.enabled | bool | `false` | Enable bearer token auth (`HYPERFLEET_API_AUTH_TOKEN_PATH`) |
| adapterConfig.hyperfleetApi.auth.audience | string | `"hyperfleet-api"` | ServiceAccount token audience (used for the projected volume) |
| adapterConfig.hyperfleetApi.auth.tokenPath | string | `"/var/run/secrets/hyperfleet/token"` | Absolute path where the token file is mounted |
| adapterConfig.hyperfleetApi.auth.expirationSeconds | int | `3600` | Token lifetime in seconds for the projected ServiceAccount token |
| adapterConfig.hyperfleetApi.auth.tokenCacheTtl | string | `"30s"` | How long the token is cached in memory (`HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL`). Zero means re-read on every request. |
| adapterConfig.log | object | `{"level":"info"}` | Log level for the adapter |
| adapterConfig.log.level | string | `"info"` | Log level (`debug`, `info`, `warn`, `error`) |
| adapterTaskConfig | object | `{"create":true}` | Adapter task configuration. Controls how the adapter-task-config ConfigMap is created. Supports inline YAML, chart-packaged files, or external content via `--set-file`. |
Expand Down
8 changes: 8 additions & 0 deletions charts/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,14 @@ Validate required values that must not remain as placeholders.
{{- if not (trim (toString .Values.image.tag)) -}}
{{- fail "image.tag must be set (e.g. --set image.tag=abc1234)" -}}
{{- end -}}
{{- if .Values.adapterConfig.hyperfleetApi.auth.enabled -}}
{{- if not (hasPrefix "/" .Values.adapterConfig.hyperfleetApi.auth.tokenPath) -}}
{{- fail "adapterConfig.hyperfleetApi.auth.tokenPath must be an absolute path (start with /)" -}}
{{- end -}}
{{- if eq (.Values.adapterConfig.hyperfleetApi.auth.tokenPath | base) "" -}}
{{- fail "adapterConfig.hyperfleetApi.auth.tokenPath must include a filename, not just a directory" -}}
{{- end -}}
{{- end -}}
{{- end }}

{{/*
Expand Down
20 changes: 20 additions & 0 deletions charts/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ spec:
value: {{ .Values.adapterConfig.hyperfleetApi.baseUrl | quote }}
- name: HYPERFLEET_API_VERSION
value: {{ .Values.adapterConfig.hyperfleetApi.version | quote }}
{{- if .Values.adapterConfig.hyperfleetApi.auth.enabled }}
- name: HYPERFLEET_API_AUTH_TOKEN_PATH
value: {{ .Values.adapterConfig.hyperfleetApi.auth.tokenPath | quote }}
- name: HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL
value: {{ .Values.adapterConfig.hyperfleetApi.auth.tokenCacheTtl | quote }}
{{- end }}
- name: BROKER_CONFIG_FILE
value: /etc/broker/broker.yaml
{{- $brokerType := include "hyperfleet-adapter.brokerType" . }}
Expand Down Expand Up @@ -154,6 +160,11 @@ spec:
mountPath: /etc/broker/broker.yaml
subPath: broker.yaml
readOnly: true
{{- if .Values.adapterConfig.hyperfleetApi.auth.enabled }}
- name: hyperfleet-api-token
mountPath: {{ .Values.adapterConfig.hyperfleetApi.auth.tokenPath | dir }}
readOnly: true
{{- end }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
Expand All @@ -176,6 +187,15 @@ spec:
items:
- key: broker.yaml
path: broker.yaml
{{- if .Values.adapterConfig.hyperfleetApi.auth.enabled }}
- name: hyperfleet-api-token
projected:
sources:
- serviceAccountToken:
audience: {{ .Values.adapterConfig.hyperfleetApi.auth.audience }}
expirationSeconds: {{ .Values.adapterConfig.hyperfleetApi.auth.expirationSeconds }}
path: {{ .Values.adapterConfig.hyperfleetApi.auth.tokenPath | base }}
{{- end }}
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
Expand Down
13 changes: 13 additions & 0 deletions charts/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ adapterConfig:
baseUrl: http://hyperfleet-api:8000
# -- API version (`HYPERFLEET_API_VERSION`)
version: v1
# -- JWT bearer token authentication via Kubernetes projected ServiceAccount token
auth:
# -- Enable bearer token auth (`HYPERFLEET_API_AUTH_TOKEN_PATH`)
enabled: false
# -- ServiceAccount token audience (used for the projected volume)
audience: hyperfleet-api
# -- Absolute path where the token file is mounted
tokenPath: /var/run/secrets/hyperfleet/token
# -- Token lifetime in seconds for the projected ServiceAccount token
expirationSeconds: 3600
# -- How long the token is cached in memory (`HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL`).
# Zero means re-read on every request.
tokenCacheTtl: 30s
# -- Log level for the adapter
log:
# -- Log level (`debug`, `info`, `warn`, `error`)
Expand Down
5 changes: 5 additions & 0 deletions cmd/adapter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,11 @@ func createAPIClient(apiConfig configloader.HyperfleetAPIConfig, log logger.Logg
opts = append(opts, hyperfleetapi.WithDefaultHeader(key, value))
}

// Configure bearer token auth if set
if apiConfig.Auth != nil {
opts = append(opts, hyperfleetapi.WithAuth(apiConfig.Auth))
}

return hyperfleetapi.NewClient(log, opts...)
}

Expand Down
7 changes: 7 additions & 0 deletions configs/adapter-config-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ clients:
timeout: 2s
retry_attempts: 3
retry_backoff: exponential
# Optional JWT bearer token authentication via a file (e.g. Kubernetes projected ServiceAccount token).
# When configured, the token is read from token_path and attached as Authorization: Bearer <token>.
# token_path must be an absolute path.
# Environment variables: HYPERFLEET_API_AUTH_TOKEN_PATH, HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL
# auth:
# token_path: "/var/run/secrets/hyperfleet/token"
# token_cache_ttl: "30s" # 0 = re-read on every request

# Broker consumer configuration (adapter-level)
broker:
Expand Down
7 changes: 7 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ clients:
max_delay: "30s"
default_headers:
X-Example: "value"
auth:
token_path: "/var/run/secrets/hyperfleet/token"
token_cache_ttl: "30s"
broker:
subscription_id: "example-subscription"
topic: "example-topic"
Expand Down Expand Up @@ -112,6 +115,8 @@ clients:
- `base_delay` (duration string): Initial retry delay. Default: `1s`.
- `max_delay` (duration string): Maximum retry delay. Default: `30s`.
- `default_headers` (map[string]string): Headers added to all API requests.
- `auth.token_path` (string): Absolute path to a file containing a JWT bearer token. When set, the token is read from this file and attached as `Authorization: Bearer <token>` on every request. Typically a Kubernetes projected ServiceAccount token. Must be an absolute path.
- `auth.token_cache_ttl` (duration string): How long the token is cached in memory before re-reading the file. Zero (default) means re-read on every request.

### Broker (`clients.broker`)

Expand Down Expand Up @@ -287,6 +292,8 @@ All deployment overrides use the `HYPERFLEET_` prefix unless noted.
- `HYPERFLEET_API_RETRY_BACKOFF` -> `clients.hyperfleet_api.retry_backoff`
- `HYPERFLEET_API_BASE_DELAY` -> `clients.hyperfleet_api.base_delay`
- `HYPERFLEET_API_MAX_DELAY` -> `clients.hyperfleet_api.max_delay`
- `HYPERFLEET_API_AUTH_TOKEN_PATH` -> `clients.hyperfleet_api.auth.token_path`
- `HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL` -> `clients.hyperfleet_api.auth.token_cache_ttl`

**Broker**

Expand Down
33 changes: 33 additions & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ These fields have first-class Helm values that the chart injects as environment
| `adapterConfig.log.level` | Log level (`debug`, `info`, `warn`, `error`) | `LOG_LEVEL` | `info` |
| `adapterConfig.hyperfleetApi.baseUrl` | HyperFleet API base URL | `HYPERFLEET_API_BASE_URL` | `http://hyperfleet-api:8000` |
| `adapterConfig.hyperfleetApi.version` | API version | `HYPERFLEET_API_VERSION` | `v1` |
| `adapterConfig.hyperfleetApi.auth.enabled` | Enable JWT bearer token auth | — (controls volume + env vars) | `false` |
| `adapterConfig.hyperfleetApi.auth.tokenPath` | Absolute path to the token file | `HYPERFLEET_API_AUTH_TOKEN_PATH` | `/var/run/secrets/hyperfleet/token` |
| `adapterConfig.hyperfleetApi.auth.tokenCacheTtl` | In-memory token cache TTL | `HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL` | `30s` |
| `adapterConfig.hyperfleetApi.auth.audience` | ServiceAccount token audience | — (used in projected volume) | `hyperfleet-api` |
| `adapterConfig.hyperfleetApi.auth.expirationSeconds` | ServiceAccount token lifetime (seconds) | — (used in projected volume) | `3600` |

### Fields settable via the `env` list

Expand Down Expand Up @@ -140,6 +145,32 @@ When using individual properties, `broker.type` must be set to `googlepubsub` or

---

## HyperFleet API Authentication

The adapter can authenticate to the HyperFleet API using a Kubernetes projected ServiceAccount token (JWT bearer token). Authentication is **disabled by default** — existing deployments are unaffected.

When enabled, the Helm chart:
1. Mounts a projected `serviceAccountToken` volume at the configured `tokenPath` directory.
2. Sets `HYPERFLEET_API_AUTH_TOKEN_PATH` and `HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL` env vars.
3. The adapter reads the token file and attaches `Authorization: Bearer <token>` to every HyperFleet API request.

```yaml
adapterConfig:
hyperfleetApi:
auth:
enabled: true
audience: hyperfleet-api # token audience claimed by the API server
tokenPath: /var/run/secrets/hyperfleet/token
expirationSeconds: 3600 # kubelet rotates the token before expiry
tokenCacheTtl: 30s # re-read file every 30s; 0 = re-read per request
```

`tokenPath` must be an absolute path (validated at `helm install`/`upgrade` time).

The token file is managed by the kubelet and rotated automatically before `expirationSeconds` elapses. Setting `tokenCacheTtl` shorter than the rotation interval ensures the adapter picks up a fresh token before the old one expires.

---

## Tracing

OpenTelemetry distributed tracing. The Helm chart defaults to tracing **disabled** (the binary defaults to enabled). When no endpoint is configured, traces are written to stdout.
Expand All @@ -159,6 +190,8 @@ The chart automatically sets these environment variables from Helm values:
| `LOG_LEVEL` | `adapterConfig.log.level` | Always |
| `HYPERFLEET_API_BASE_URL` | `adapterConfig.hyperfleetApi.baseUrl` | Always |
| `HYPERFLEET_API_VERSION` | `adapterConfig.hyperfleetApi.version` | Always |
| `HYPERFLEET_API_AUTH_TOKEN_PATH` | `adapterConfig.hyperfleetApi.auth.tokenPath` | When `auth.enabled` is `true` |
| `HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL` | `adapterConfig.hyperfleetApi.auth.tokenCacheTtl` | When `auth.enabled` is `true` |
| `BROKER_CONFIG_FILE` | Hardcoded `/etc/broker/broker.yaml` | Always |
| `HYPERFLEET_BROKER_SUBSCRIPTION_ID` | `broker.googlepubsub.subscriptionId` | When broker type is `googlepubsub` |
| `HYPERFLEET_BROKER_TOPIC` | `broker.googlepubsub.topic` | When broker type is `googlepubsub` |
Expand Down
3 changes: 3 additions & 0 deletions internal/configloader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ func LoadConfig(opts ...LoadOption) (*Config, error) {
if err := taskValidator.ValidateSemantic(); err != nil {
return nil, fmt.Errorf("task config semantic validation failed: %w", err)
}
for _, w := range taskValidator.Warnings() {
o.logger.Warn(o.ctx, w)
}
}

// 3. Merge into unified Config
Expand Down
4 changes: 4 additions & 0 deletions internal/configloader/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ type LogConfig struct {
// Alias to hyperfleetapi.ClientConfig to ensure shared schema.
type HyperfleetAPIConfig = hyperfleetapi.ClientConfig

// HyperfleetAPIAuthConfig is the HyperFleet API auth configuration.
// Alias to hyperfleetapi.AuthConfig to ensure shared schema.
type HyperfleetAPIAuthConfig = hyperfleetapi.AuthConfig

// BrokerConfig contains broker consumer configuration
type BrokerConfig struct {
SubscriptionID string `yaml:"subscription_id,omitempty" mapstructure:"subscription_id"`
Expand Down
38 changes: 33 additions & 5 deletions internal/configloader/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,38 @@ func (v *AdapterConfigValidator) ValidateStructure() error {
return fmt.Errorf("%s", errs.First())
}

if err := v.validateHyperfleetAuth(); err != nil {
return err
}

return nil
}

func (v *AdapterConfigValidator) validateHyperfleetAuth() error {
auth := v.config.Clients.HyperfleetAPI.Auth
if auth == nil {
return nil
}
if auth.TokenPath == "" {
return fmt.Errorf("clients.hyperfleet_api.auth.token_path must be set when auth is configured")
}
if !filepath.IsAbs(auth.TokenPath) {
return fmt.Errorf("clients.hyperfleet_api.auth.token_path must be an absolute path, got %q", auth.TokenPath)
}
if auth.TokenCacheTTL < 0 {
return fmt.Errorf("clients.hyperfleet_api.auth.token_cache_ttl must not be negative")
}
return nil
}

// TaskConfigValidator validates AdapterTaskConfig (task configuration)
type TaskConfigValidator struct {
config *AdapterTaskConfig
errors *ValidationErrors
definedVars map[string]bool
celEnv *cel.Env
definedVars map[string]bool
baseDir string
warnings []string
}

// NewTaskConfigValidator creates a validator for AdapterTaskConfig
Expand All @@ -72,6 +94,11 @@ func NewTaskConfigValidator(config *AdapterTaskConfig, baseDir string) *TaskConf
}
}

// Warnings returns deprecation warnings collected during validation.
func (v *TaskConfigValidator) Warnings() []string {
return v.warnings
}

// ValidateStructure validates the structural requirements of AdapterTaskConfig
func (v *TaskConfigValidator) ValidateStructure() error {
if v.config == nil {
Expand Down Expand Up @@ -182,15 +209,16 @@ func (v *TaskConfigValidator) validatePreconditionAPICallForbidden() {
for i, precond := range v.config.Preconditions {
if precond.APICall != nil {
path := fmt.Sprintf("%s[%d].%s", FieldPreconditions, i, FieldAPICall)
v.errors.Add(path, fmt.Sprintf(
"precondition %q contains api_call. api_call is no longer valid in the precondition phase.\n"+
"Move the api_call block to a params entry:\n"+
v.warnings = append(v.warnings, fmt.Sprintf(
"%s: DEPRECATED: precondition %q uses api_call directly. "+
"Move the api_call block to a params entry with source.api_call instead. "+
"Direct api_call on preconditions will be removed in a future release.\n"+
" params:\n"+
" - name: %q\n"+
" source:\n"+
" api_call:\n"+
" ...",
precond.Name, precond.Name))
path, precond.Name, precond.Name))
}
}
}
Expand Down
66 changes: 61 additions & 5 deletions internal/configloader/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1228,7 +1228,7 @@ func TestValidateParamsAPICallSource(t *testing.T) {
}

func TestValidatePreconditionAPICallForbidden(t *testing.T) {
t.Run("precondition with api_call produces migration error", func(t *testing.T) {
t.Run("precondition with api_call produces deprecation warning not error", func(t *testing.T) {
cfg := baseTaskConfig()
cfg.Preconditions = []Precondition{{
ActionBase: ActionBase{
Expand All @@ -1239,12 +1239,15 @@ func TestValidatePreconditionAPICallForbidden(t *testing.T) {
v := newTaskValidator(cfg)
_ = v.ValidateStructure()
err := v.ValidateSemantic()
require.Error(t, err)
assert.Contains(t, err.Error(), "api_call is no longer valid in the precondition phase")
assert.Contains(t, err.Error(), "fetchCluster")
require.NoError(t, err)
warnings := v.Warnings()
require.Len(t, warnings, 1)
assert.Contains(t, warnings[0], "DEPRECATED")
assert.Contains(t, warnings[0], "fetchCluster")
assert.Contains(t, warnings[0], "source.api_call")
})

t.Run("precondition without api_call passes", func(t *testing.T) {
t.Run("precondition without api_call passes with no warnings", func(t *testing.T) {
cfg := baseTaskConfig()
cfg.Preconditions = []Precondition{{
ActionBase: ActionBase{Name: "check"},
Expand All @@ -1253,5 +1256,58 @@ func TestValidatePreconditionAPICallForbidden(t *testing.T) {
v := newTaskValidator(cfg)
require.NoError(t, v.ValidateStructure())
require.NoError(t, v.ValidateSemantic())
assert.Empty(t, v.Warnings())
})
}

func TestAdapterConfigValidator_HyperfleetAuth(t *testing.T) {
baseAdapterConfig := func() *AdapterConfig {
return &AdapterConfig{
Adapter: AdapterInfo{Name: "test-adapter"},
}
}
newValidator := func(cfg *AdapterConfig) *AdapterConfigValidator {
return NewAdapterConfigValidator(cfg, "")
}

t.Run("nil auth is valid", func(t *testing.T) {
cfg := baseAdapterConfig()
require.NoError(t, newValidator(cfg).ValidateStructure())
})

t.Run("valid absolute token path", func(t *testing.T) {
cfg := baseAdapterConfig()
cfg.Clients.HyperfleetAPI.Auth = &HyperfleetAPIAuthConfig{
TokenPath: "/var/run/secrets/token",
TokenCacheTTL: 30,
}
require.NoError(t, newValidator(cfg).ValidateStructure())
})

t.Run("empty token_path is an error", func(t *testing.T) {
cfg := baseAdapterConfig()
cfg.Clients.HyperfleetAPI.Auth = &HyperfleetAPIAuthConfig{TokenPath: ""}
err := newValidator(cfg).ValidateStructure()
require.Error(t, err)
assert.Contains(t, err.Error(), "token_path must be set")
})

t.Run("relative token_path is an error", func(t *testing.T) {
cfg := baseAdapterConfig()
cfg.Clients.HyperfleetAPI.Auth = &HyperfleetAPIAuthConfig{TokenPath: "relative/path/token"}
err := newValidator(cfg).ValidateStructure()
require.Error(t, err)
assert.Contains(t, err.Error(), "must be an absolute path")
})

t.Run("negative token_cache_ttl is an error", func(t *testing.T) {
cfg := baseAdapterConfig()
cfg.Clients.HyperfleetAPI.Auth = &HyperfleetAPIAuthConfig{
TokenPath: "/var/run/secrets/token",
TokenCacheTTL: -1,
}
err := newValidator(cfg).ValidateStructure()
require.Error(t, err)
assert.Contains(t, err.Error(), "token_cache_ttl must not be negative")
})
}
2 changes: 2 additions & 0 deletions internal/configloader/viper_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ var viperKeyMappings = map[string]string{
"clients::hyperfleet_api::retry_backoff": "API_RETRY_BACKOFF",
"clients::hyperfleet_api::base_delay": "API_BASE_DELAY",
"clients::hyperfleet_api::max_delay": "API_MAX_DELAY",
"clients::hyperfleet_api::auth::token_path": "API_AUTH_TOKEN_PATH",
"clients::hyperfleet_api::auth::token_cache_ttl": "API_AUTH_TOKEN_CACHE_TTL",
"clients::broker::subscription_id": "BROKER_SUBSCRIPTION_ID",
"clients::broker::topic": "BROKER_TOPIC",
"clients::kubernetes::kube_config_path": "KUBERNETES_KUBE_CONFIG_PATH",
Expand Down
Loading