-
Notifications
You must be signed in to change notification settings - Fork 8
HYPERFLEET-569 - feat: Implement maestro DSL loader #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -11,7 +11,9 @@ spec: | |||||
| version: "0.1.0" | ||||||
|
|
||||||
| # Log the full merged configuration after load (default: false) | ||||||
| debugConfig: false | ||||||
| debugConfig: true | ||||||
| log: | ||||||
| level: debug | ||||||
|
|
||||||
| clients: | ||||||
| hyperfleetApi: | ||||||
|
|
@@ -22,8 +24,51 @@ spec: | |||||
| retryBackoff: exponential | ||||||
|
|
||||||
| broker: | ||||||
| subscriptionId: "example-clusters-subscription" | ||||||
| topic: "example-clusters" | ||||||
| subscriptionId: "CHANGE_ME" | ||||||
| topic: "CHANGE_ME" | ||||||
|
|
||||||
| kubernetes: | ||||||
| apiVersion: "v1" | ||||||
| #kubeConfigPath: PATH_TO_KUBECONFIG # for local development | ||||||
|
|
||||||
| maestro: | ||||||
| grpcServerAddress: "maestro-grpc.maestro.svc.cluster.local:8090" | ||||||
|
|
||||||
| # HTTPS server address for REST API operations (optional) | ||||||
| # Environment variable: HYPERFLEET_MAESTRO_HTTP_SERVER_ADDRESS | ||||||
| httpServerAddress: "http://maestro.maestro.svc.cluster.local:8000" | ||||||
|
|
||||||
| # Source identifier for CloudEvents routing (must be unique across adapters) | ||||||
| # Environment variable: HYPERFLEET_MAESTRO_SOURCE_ID | ||||||
| sourceId: "hyperfleet-adapter" | ||||||
|
|
||||||
| # Client identifier (defaults to sourceId if not specified) | ||||||
| # Environment variable: HYPERFLEET_MAESTRO_CLIENT_ID | ||||||
| clientId: "hyperfleet-adapter-client" | ||||||
| insecure: true | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prefer secure defaults for Maestro in the example.
🔧 Suggested change- insecure: true
+ insecure: false📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| # Authentication configuration | ||||||
| #auth: | ||||||
| # type: "tls" # TLS certificate-based mTLS | ||||||
| # | ||||||
| # tlsConfig: | ||||||
| # # gRPC TLS configuration | ||||||
| # # Certificate paths (mounted from Kubernetes secrets) | ||||||
| # # Environment variable: HYPERFLEET_MAESTRO_CA_FILE | ||||||
| # caFile: "/etc/maestro/certs/grpc/ca.crt" | ||||||
| # | ||||||
| # # Environment variable: HYPERFLEET_MAESTRO_CERT_FILE | ||||||
| # certFile: "/etc/maestro/certs/grpc/client.crt" | ||||||
| # | ||||||
| # # Environment variable: HYPERFLEET_MAESTRO_KEY_FILE | ||||||
| # keyFile: "/etc/maestro/certs/grpc/client.key" | ||||||
| # | ||||||
| # # Server name for TLS verification | ||||||
| # # Environment variable: HYPERFLEET_MAESTRO_SERVER_NAME | ||||||
| # serverName: "maestro-grpc.maestro.svc.cluster.local" | ||||||
| # | ||||||
| # # HTTP API TLS configuration (may use different CA than gRPC) | ||||||
| # # If not set, falls back to caFile for backwards compatibility | ||||||
| # # Environment variable: HYPERFLEET_MAESTRO_HTTP_CA_FILE | ||||||
| # httpCaFile: "/etc/maestro/certs/https/ca.crt" | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,7 +76,31 @@ spec: | |
| # Resources with valid K8s manifests | ||
| resources: | ||
| - name: "maestro" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This kind of configuration lose the manifestwork ref. And for there should be multiple manifests in the manifestwork.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And we should put manifests in same manifestwork together. Manifests number cannot be changed once the manifestwork created. |
||
| transport: | ||
| client: "maestro" | ||
| maestro: | ||
| targetCluster: cluster1 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. manifestwork.ref here should be something required and have a validation |
||
| manifest: | ||
| apiVersion: v1 | ||
| kind: Namespace | ||
| metadata: | ||
| name: "maestro-{{ .clusterId }}" | ||
| labels: | ||
| hyperfleet.io/cluster-id: "{{ .clusterId }}" | ||
| hyperfleet.io/cluster-name: "{{ .clusterName }}" | ||
| annotations: | ||
| hyperfleet.io/generation: "{{ .generationSpec }}" | ||
| discovery: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Discovery is manifest level not manifestwork.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the discovery can help us locate the manifest attributes with jsonPath |
||
| namespace: "*" # Cluster-scoped resource (Namespace) | ||
| bySelectors: | ||
| labelSelector: | ||
| hyperfleet.io/cluster-id: "{{ .clusterId }}" | ||
| hyperfleet.io/cluster-name: "{{ .clusterName }}" | ||
|
|
||
| - name: "clusterNamespace" | ||
| transport: | ||
| client: "kubernetes" | ||
| manifest: | ||
| apiVersion: v1 | ||
| kind: Namespace | ||
|
|
@@ -98,6 +122,8 @@ spec: | |
| # in the namespace created above | ||
| # it will require a service account to be created in that namespace as well as a role and rolebinding | ||
| - name: "jobServiceAccount" | ||
| transport: | ||
| client: "kubernetes" | ||
| manifest: | ||
| ref: "/etc/adapter/job-serviceaccount.yaml" | ||
| discovery: | ||
|
|
@@ -107,6 +133,8 @@ spec: | |
| hyperfleet.io/cluster-id: "{{ .clusterId }}" | ||
|
|
||
| - name: "job_role" | ||
| transport: | ||
| client: "kubernetes" | ||
| manifest: | ||
| ref: "/etc/adapter/job-role.yaml" | ||
| discovery: | ||
|
|
@@ -118,6 +146,8 @@ spec: | |
| hyperfleet.io/resource-type: "role" | ||
|
|
||
| - name: "job_rolebinding" | ||
| transport: | ||
| client: "kubernetes" | ||
| manifest: | ||
| ref: "/etc/adapter/job-rolebinding.yaml" | ||
| discovery: | ||
|
|
@@ -129,6 +159,8 @@ spec: | |
| hyperfleet.io/resource-type: "rolebinding" | ||
|
|
||
| - name: "jobNamespace" | ||
| transport: | ||
| client: "kubernetes" | ||
| manifest: | ||
| ref: "/etc/adapter/job.yaml" | ||
| discovery: | ||
|
|
@@ -143,6 +175,8 @@ spec: | |
| # and using the same service account as the adapter | ||
|
|
||
| - name: "deploymentNamespace" | ||
| transport: | ||
| client: "kubernetes" | ||
| manifest: | ||
| ref: "/etc/adapter/deployment.yaml" | ||
| discovery: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,10 +9,10 @@ import ( | |
| "syscall" | ||
| "time" | ||
|
|
||
| "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/client_factory" | ||
| "github.com/openshift-hyperfleet/hyperfleet-adapter/internal/config_loader" | ||
| "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/pkg/health" | ||
| "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/logger" | ||
| "github.com/openshift-hyperfleet/hyperfleet-adapter/pkg/otel" | ||
|
|
@@ -50,7 +50,6 @@ const ( | |
| ) | ||
|
|
||
| func main() { | ||
|
|
||
| // Root command | ||
| rootCmd := &cobra.Command{ | ||
| Use: "adapter", | ||
|
|
@@ -269,7 +268,7 @@ func runServe() error { | |
|
|
||
| // Create HyperFleet API client from config | ||
| log.Info(ctx, "Creating HyperFleet API client...") | ||
| apiClient, err := createAPIClient(config.Spec.Clients.HyperfleetAPI, log) | ||
| apiClient, err := client_factory.CreateAPIClient(config.Spec.Clients.HyperfleetAPI, log) | ||
| if err != nil { | ||
| errCtx := logger.WithErrorField(ctx, err) | ||
| log.Errorf(errCtx, "Failed to create HyperFleet API client") | ||
|
|
@@ -278,21 +277,39 @@ func runServe() error { | |
|
|
||
| // Create Kubernetes client | ||
| log.Info(ctx, "Creating Kubernetes client...") | ||
| k8sClient, err := createK8sClient(ctx, config.Spec.Clients.Kubernetes, log) | ||
| k8sClient, err := client_factory.CreateK8sClient(ctx, config.Spec.Clients.Kubernetes, log) | ||
| if err != nil { | ||
| errCtx := logger.WithErrorField(ctx, err) | ||
| log.Errorf(errCtx, "Failed to create Kubernetes client") | ||
| return fmt.Errorf("failed to create Kubernetes client: %w", err) | ||
| } | ||
|
|
||
| // Create Maestro client if configured | ||
| var maestroClient maestro_client.ManifestWorkClient | ||
| if config.Spec.Clients.Maestro != nil { | ||
| log.Info(ctx, "Creating Maestro client...") | ||
| maestroClient, err = client_factory.CreateMaestroClient(ctx, config.Spec.Clients.Maestro, log) | ||
| if err != nil { | ||
| errCtx := logger.WithErrorField(ctx, err) | ||
| log.Errorf(errCtx, "Failed to create Maestro client") | ||
| return fmt.Errorf("failed to create Maestro client: %w", err) | ||
| } | ||
| log.Info(ctx, "Maestro client created successfully") | ||
| } | ||
|
|
||
| // Create the executor using the builder pattern | ||
| log.Info(ctx, "Creating event executor...") | ||
| exec, err := executor.NewBuilder(). | ||
| execBuilder := executor.NewBuilder(). | ||
| WithConfig(config). | ||
| WithAPIClient(apiClient). | ||
| WithK8sClient(k8sClient). | ||
| WithLogger(log). | ||
| Build() | ||
| WithLogger(log) | ||
|
|
||
| if maestroClient != nil { | ||
| execBuilder = execBuilder.WithMaestroClient(maestroClient) | ||
| } | ||
|
|
||
| exec, err := execBuilder.Build() | ||
| if err != nil { | ||
|
Comment on lines
+287
to
313
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search for Maestro client validation logic
rg -n "WithMaestroClient|MaestroClient|TransportClientMaestro" internal/executor internal/config_loader -C 3Repository: openshift-hyperfleet/hyperfleet-adapter Length of output: 8184 🏁 Script executed: #!/bin/bash
# Check executor Build() method for validation
rg -n "func.*Build\(\)" internal/executor/executor.go -A 20Repository: openshift-hyperfleet/hyperfleet-adapter Length of output: 181 🏁 Script executed: #!/bin/bash
# Check applyResourceMaestro implementation
rg -n "func.*applyResourceMaestro" internal/executor -A 15Repository: openshift-hyperfleet/hyperfleet-adapter Length of output: 1449 🏁 Script executed: #!/bin/bash
# Check NewExecutor function for validation logic
rg -n "func NewExecutor" internal/executor/executor.go -A 30Repository: openshift-hyperfleet/hyperfleet-adapter Length of output: 999 🏁 Script executed: #!/bin/bash
# Get the full validateExecutorConfig function
rg -n "func validateExecutorConfig" internal/executor/executor.go -A 50Repository: openshift-hyperfleet/hyperfleet-adapter Length of output: 2081 🏁 Script executed: #!/bin/bash
# Check if there's any config-level validation linking transport requirements to client initialization
rg -n "TransportClientMaestro" internal/config_loader/validator.go -B 5 -A 10Repository: openshift-hyperfleet/hyperfleet-adapter Length of output: 759 Add build-time validation to fail fast when resources require Maestro transport but no Maestro client is configured. The executor currently has a runtime guard in 🤖 Prompt for AI Agents |
||
| errCtx := logger.WithErrorField(ctx, err) | ||
| log.Errorf(errCtx, "Failed to create executor") | ||
|
|
@@ -437,60 +454,3 @@ func runServe() error { | |
|
|
||
| return nil | ||
| } | ||
|
|
||
| // 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 | ||
|
|
||
| // Set base URL if configured (env fallback handled in NewClient) | ||
| if apiConfig.BaseURL != "" { | ||
| opts = append(opts, hyperfleet_api.WithBaseURL(apiConfig.BaseURL)) | ||
| } | ||
|
|
||
| // Set timeout if configured (0 means use default) | ||
| if apiConfig.Timeout > 0 { | ||
| opts = append(opts, hyperfleet_api.WithTimeout(apiConfig.Timeout)) | ||
| } | ||
|
|
||
| // Set retry attempts | ||
| if apiConfig.RetryAttempts > 0 { | ||
| opts = append(opts, hyperfleet_api.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)) | ||
| default: | ||
| 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)) | ||
| } | ||
|
|
||
| // Set retry max delay | ||
| if apiConfig.MaxDelay > 0 { | ||
| opts = append(opts, hyperfleet_api.WithMaxDelay(apiConfig.MaxDelay)) | ||
| } | ||
|
|
||
| // Set default headers | ||
| for key, value := range apiConfig.DefaultHeaders { | ||
| opts = append(opts, hyperfleet_api.WithDefaultHeader(key, value)) | ||
| } | ||
|
|
||
| return hyperfleet_api.NewClient(log, opts...) | ||
| } | ||
|
|
||
| // 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{ | ||
| KubeConfigPath: k8sConfig.KubeConfigPath, | ||
| QPS: k8sConfig.QPS, | ||
| Burst: k8sConfig.Burst, | ||
| } | ||
| return k8s_client.NewClient(ctx, clientConfig, log) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid enabling full config debug logging in the example.
debugConfig: truelogs the full merged config and can leak secrets when users copy this example into real deployments. Consider defaulting it to false and leaving a comment for troubleshooting.🔒 Suggested change
📝 Committable suggestion
🤖 Prompt for AI Agents