Skip to content
Draft
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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
This branch is an experiment to simplify the adapter DSL

Check the new adapter config format in: adapter-step-config-template.yaml

Things to note:

- Now all operations are under steps
- There are no phases (params, preconditions, resources, post)
- steps like the `clusterStatus` `apiCall` execute and generate as ouputs
- `clusterStatus.results`
- `clusterStatus.error`
- which can be used in other expressions, or `when` conditions
- e.g. the clusterNamespace resource action uses a `when` to execute only when clusterPhase == NotReady
- The `reportStatus` is simply another `apiCall` at the end of the adapterConfig

# HyperFleet Adapter

HyperFleet Adapter Framework - Event-driven adapter services for HyperFleet cluster provisioning. Handles CloudEvents consumption, AdapterConfig CRD integration, precondition evaluation, Kubernetes Job creation/monitoring, and status reporting via API. Supports GCP Pub/Sub, RabbitMQ broker abstraction.
Expand Down Expand Up @@ -123,10 +138,12 @@ hyperfleet-adapter/
HyperFleet Adapter uses [bingo](https://github.com/bwplotka/bingo) to manage Go tool dependencies with pinned versions.

**Managed tools**:

- `goimports` - Code formatting and import organization
- `golangci-lint` - Code linting

**Common operations**:

```bash
# Install all tools
bingo get
Expand Down Expand Up @@ -219,11 +236,13 @@ make test
```

Unit tests include:

- Logger functionality and context handling
- Error handling and error codes
- Operation ID middleware
- Template rendering and parsing
- Kubernetes client logic

### Integration Tests

Integration tests use **Testcontainers** with **dynamically installed envtest** - works in any CI/CD platform without requiring privileged containers.
Expand Down Expand Up @@ -269,6 +288,7 @@ The first run will download golang:alpine and install envtest (~20-30 seconds).
**Performance**: ~30-40 seconds for complete test suite (10 suites, 24 test cases).

**Alternative**: Use K3s (`make test-integration-k3s`) for 2x faster tests if privileged containers are available.

- ⚠️ Requires Docker or rootful Podman
- ✅ Makefile automatically checks Podman mode and provides helpful instructions if incompatible

Expand Down
283 changes: 283 additions & 0 deletions configs/adapter-step-config-template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
# HyperFleet Adapter Step-Based Configuration Template
#
# This configuration uses the simplified step-based execution model where
# all operations are expressed as sequential steps with optional 'when' clauses.
#
# EXECUTION MODEL:
# ================
# Steps execute sequentially in order. Each step has:
# - name: Unique identifier (used to reference results in later steps)
# - when: (optional) CEL expression - if false, step is skipped (soft failure)
# - One of: param, apiCall, resource, payload, log
#
# STEP TYPES:
# ===========
# param - Extract/define a value (from env, event, literal, or expression)
# apiCall - Execute HTTP request with optional captures
# resource - Create/update Kubernetes resource
# payload - Build JSON structure for API calls
# log - Emit log message
#
# REFERENCING RESULTS:
# ====================
# Each step's result is accessible by its name:
# - {{ .stepName }} in Go templates
# - stepName in CEL expressions
# - stepName.error != null to check for errors
# - stepName.skipped to check if step was skipped
#
# CEL EXPRESSIONS:
# ================
# Use CEL for 'when' clauses and param expressions:
# when: "clusterPhase == 'NotReady'"
# when: "previousStep.error == null"
# when: "!hasError && clusterPhase == 'Ready'"
#
# SOFT FAILURES:
# ==============
# When 'when' evaluates to false, the step is skipped (not an error).
# Execution continues to subsequent steps.
# Use stepName.error in later 'when' clauses to conditionally skip steps.

apiVersion: hyperfleet.redhat.com/v1alpha1
kind: AdapterConfig
metadata:
name: example-adapter
namespace: hyperfleet-system
labels:
hyperfleet.io/adapter-type: example
hyperfleet.io/component: adapter

spec:
adapter:
version: "0.1.0"

hyperfleetApi:
timeout: 2s
retryAttempts: 3
retryBackoff: exponential

kubernetes:
apiVersion: "v1"

steps:
# =========================================================================
# CONFIGURATION PARAMETERS
# Use param steps with 'value' for configuration that would otherwise be
# in spec.adapter or spec.hyperfleetApi
# =========================================================================
- name: "adapterVersion"
param:
value: "0.1.0"

- name: "apiTimeout"
param:
value: "10s"

- name: "apiRetryAttempts"
param:
value: 3

- name: "apiRetryBackoff"
param:
value: "exponential"

# =========================================================================
# ENVIRONMENT PARAMETERS
# Extract values from environment variables
# =========================================================================
- name: "hyperfleetApiBaseUrl"
param:
source: "env.HYPERFLEET_API_BASE_URL"

- name: "hyperfleetApiVersion"
param:
source: "env.HYPERFLEET_API_VERSION"
default: "v1"

# =========================================================================
# EVENT PARAMETERS
# Extract values from CloudEvent data
# =========================================================================
- name: "clusterId"
param:
source: "event.id"

- name: "resourceType"
param:
source: "event.kind"

- name: "eventGeneration"
param:
source: "event.generation"
default: "1"

- name: "eventHref"
param:
source: "event.href"

# =========================================================================
# COMPUTED PARAMETERS
# Use CEL expressions to compute values from other parameters
# =========================================================================
- name: "clusterApiUrl"
param:
expression: "hyperfleetApiBaseUrl + '/api/hyperfleet/' + hyperfleetApiVersion + '/clusters/' + clusterId"

# =========================================================================
# API CALLS
# Execute HTTP requests to external services
# Use 'capture' to extract fields from the response to top-level variables
# =========================================================================
- name: "clusterStatus"
apiCall:
method: GET
url: "{{ .clusterApiUrl }}"
timeout: "{{ .apiTimeout }}"
retryAttempts: 3
retryBackoff: "exponential"
capture:
- name: "clusterPhase"
field: "status.phase"
- name: "clusterName"
field: "name"
- name: "generationId"
field: "generation"

# =========================================================================
# ERROR TRACKING
# Use param steps with expressions to track errors from previous steps
# =========================================================================
- name: "hasError"
param:
expression: "clusterStatus.error != null"

# =========================================================================
# LOGGING
# Emit log messages for debugging and monitoring
# =========================================================================
- name: "logClusterStatus"
log:
level: info
message: "Processing cluster {{ .clusterId }}: phase={{ .clusterPhase }}, hasError={{ .hasError }}"

# =========================================================================
# RESOURCES
# Create/update Kubernetes resources
# Use 'when' to conditionally create resources based on state
# =========================================================================
- name: "clusterNamespace"
when: "!hasError && clusterPhase == 'NotReady'"
resource:
manifest:
apiVersion: v1
kind: Namespace
metadata:
name: "{{ .clusterId | lower }}"
labels:
hyperfleet.io/cluster-id: "{{ .clusterId }}"
hyperfleet.io/managed-by: "{{ .metadata.name }}"
hyperfleet.io/resource-type: "namespace"
annotations:
hyperfleet.io/created-by: "hyperfleet-adapter"
hyperfleet.io/generation: "{{ .generationId }}"
discovery:
bySelectors:
labelSelector:
hyperfleet.io/resource-type: "namespace"
hyperfleet.io/cluster-id: "{{ .clusterId }}"
hyperfleet.io/managed-by: "{{ .metadata.name }}"

# =========================================================================
# EXECUTION STATUS
# Compute final execution status for reporting
# =========================================================================
- name: "executionError"
param:
expression: |
clusterStatus.error != null ? clusterStatus.error :
clusterNamespace.error != null ? clusterNamespace.error :
null

- name: "executionStatus"
param:
expression: "executionError == null ? 'success' : 'failed'"

# =========================================================================
# PAYLOAD BUILDING
# Build complex JSON structures for API calls
# Supports nested structures with CEL expressions
# =========================================================================
- name: "statusPayload"
payload:
adapter: "{{ .metadata.name }}"
conditions:
- type: "Applied"
status:
expression: |
clusterNamespace.?status.?phase.orValue("") == "Active" ? "True" : "False"
reason:
expression: |
clusterNamespace.?status.?phase.orValue("") == "Active"
? "NamespaceCreated"
: "NamespacePending"
message:
expression: |
clusterNamespace.?status.?phase.orValue("") == "Active"
? "Namespace created successfully"
: "Namespace creation in progress"

- type: "Available"
status:
expression: |
clusterNamespace.?status.?phase.orValue("") == "Active" ? "True" : "False"
reason:
expression: |
clusterNamespace.?status.?phase.orValue("") == "Active" ? "NamespaceReady" : "NamespaceNotReady"
message:
expression: |
clusterNamespace.?status.?phase.orValue("") == "Active" ? "Namespace is active and ready" : "Namespace is not active and ready"

- type: "Health"
status:
expression: "executionStatus == 'success' ? 'True' : 'False'"
reason:
expression: |
executionError.?reason.orValue("") != "" ? executionError.?reason.orValue("") : "Healthy"
message:
expression: |
executionError.?message.orValue("") != "" ? executionError.?message.orValue("") : "All adapter operations completed successfully"

observed_generation:
expression: "generationId"

observed_time: "{{ now | date \"2006-01-02T15:04:05Z07:00\" }}"

data:
namespace:
name:
expression: |
clusterNamespace.?metadata.?name.orValue("")
status:
expression: |
clusterNamespace.?status.?phase.orValue("")

- name: "logPayload"
log:
level: info
message: "{{ .statusPayload }}"
# =========================================================================
# STATUS REPORTING
# Report status back to HyperFleet API (always runs - no when clause)
# =========================================================================
- name: "reportStatus"
apiCall:
method: POST
url: "{{ .clusterApiUrl }}/statuses"
body: "{{ .statusPayload }}"
timeout: "30s"
retryAttempts: 3
retryBackoff: "exponential"
headers:
- name: "Content-Type"
value: "application/json"
Loading