Skip to content

Commit d5b63fd

Browse files
authored
feat(story,runs,impulse,cel,config): request manifest metadata and wire secret artifacts, reorganize engram execution and artifact plumbing (#21)
- extend StepRun API/CRDs with manifest request/response fields, regenerated deepcopy, and loop bound config - upgrade step executor/DAG to compute manifest specs, resolve loop inputs safely, and add unit coverage - introduce secret artifact builder and plumb resolver/controller changes for Engram, Impulse, Story, and hybrid runs - align operator config, webhooks, and manager manifests with new knobs; refresh go.mod/sum and workflow glue
1 parent a148056 commit d5b63fd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+4809
-863
lines changed

.github/workflows/docker.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,10 @@ jobs:
3535
with:
3636
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
3737
tags: |
38-
# PRs
3938
type=ref,event=pr
40-
# Default branch builds
4139
type=raw,value=latest,enable={{is_default_branch}}
4240
type=ref,event=branch
4341
type=sha
44-
# Semver tags
4542
type=semver,pattern={{version}}
4643
type=semver,pattern={{major}}.{{minor}}
4744
type=semver,pattern={{major}}
@@ -85,4 +82,3 @@ jobs:
8582
if: always()
8683
with:
8784
sarif_file: 'trivy-results.sarif'
88-

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ go.work
2626
*.swo
2727
*~
2828

29-
.DS_Store
29+
.DS_Store
30+
.gocache/

api/runs/v1alpha1/steprun_types.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ type StepRunSpec struct {
112112
// Can be a list to support fanning out to multiple parallel steps.
113113
// +optional
114114
DownstreamTargets []DownstreamTarget `json:"downstreamTargets,omitempty"`
115+
116+
// RequestedManifest lists the metadata fields the controller expects the SDK
117+
// to materialize alongside the offloaded output. These are derived from CEL expressions
118+
// that reference this step's outputs (e.g., len(steps.foo.output.bar)).
119+
// +optional
120+
RequestedManifest []ManifestRequest `json:"requestedManifest,omitempty"`
115121
}
116122

117123
// DownstreamTarget defines the destination for an Engram's output in real-time execution mode.
@@ -140,7 +146,50 @@ type TerminateTarget struct {
140146
StopMode enums.StopMode `json:"stopMode"`
141147
}
142148

149+
// ManifestOperation enumerates the metadata operations supported for step manifests.
150+
type ManifestOperation string
151+
152+
const (
153+
// ManifestOperationExists records whether the referenced field exists/non-nil.
154+
ManifestOperationExists ManifestOperation = "exists"
155+
// ManifestOperationLength records the length of the referenced field when it is an array, map, or string.
156+
ManifestOperationLength ManifestOperation = "length"
157+
)
158+
159+
// ManifestRequest describes a single output field and the metadata operations required for it.
160+
type ManifestRequest struct {
161+
// Path is the dot/bracket notation path relative to the step output root.
162+
// Examples: "result.items", "tools", "items[0].id".
163+
// +kubebuilder:validation:MinLength=1
164+
Path string `json:"path"`
165+
// Operations lists the metadata operations that should be computed for this path.
166+
// Defaults to ["exists"] when omitted.
167+
// +optional
168+
Operations []ManifestOperation `json:"operations,omitempty"`
169+
}
170+
171+
// StepManifestData captures the metadata emitted by the SDK for a single manifest path.
172+
type StepManifestData struct {
173+
// Exists indicates whether the referenced field was present and non-nil.
174+
// +optional
175+
Exists *bool `json:"exists,omitempty"`
176+
// Length contains the computed length when requested and applicable.
177+
// +optional
178+
Length *int64 `json:"length,omitempty"`
179+
// Truncated signals that the SDK could not compute the full metadata due to limits.
180+
// +optional
181+
Truncated bool `json:"truncated,omitempty"`
182+
// Error contains a warning message emitted by the SDK when it cannot honour the manifest request.
183+
// +optional
184+
Error string `json:"error,omitempty"`
185+
// Sample holds an optional representative slice of the data (implementation-defined).
186+
// +optional
187+
Sample *runtime.RawExtension `json:"sample,omitempty"`
188+
}
189+
143190
// StepRunStatus tracks the detailed execution state of this individual step
191+
// +kubebuilder:validation:XValidation:message="status.conditions reason field must be <= 64 characters",rule="!has(self.conditions) || self.conditions.all(c, !has(c.reason) || size(c.reason) <= 64)"
192+
// +kubebuilder:validation:XValidation:message="status.conditions message field must be <= 2048 characters",rule="!has(self.conditions) || self.conditions.all(c, !has(c.message) || size(c.message) <= 2048)"
144193
type StepRunStatus struct {
145194
// observedGeneration is the most recent generation observed for this StepRun. It corresponds to the
146195
// StepRun's generation, which is updated on mutation by the API Server.
@@ -198,6 +247,16 @@ type StepRunStatus struct {
198247
// Step coordination - which steps must complete before this one can start
199248
// Uses the same "needs" terminology as our Story API for consistency
200249
Needs []string `json:"needs,omitempty"` // StepRun names that must complete first
250+
251+
// Manifest contains metadata captured for this step's output that enables CEL expressions
252+
// to execute without hydrating large blobs from storage.
253+
// The map key matches the ManifestRequest path.
254+
// +optional
255+
Manifest map[string]StepManifestData `json:"manifest,omitempty"`
256+
257+
// ManifestWarnings contains any warnings produced while computing manifest data (e.g., unsupported operations).
258+
// +optional
259+
ManifestWarnings []string `json:"manifestWarnings,omitempty"`
201260
}
202261

203262
// +kubebuilder:object:root=true

api/runs/v1alpha1/storyrun_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ type StoryRunSpec struct {
8080
}
8181

8282
// StoryRunStatus tracks the current state and results of this story execution
83+
// +kubebuilder:validation:XValidation:rule="!has(self.conditions) || self.conditions.exists(c, c.type == 'Ready')",message="status.conditions must include Ready when conditions are set"
84+
// +kubebuilder:validation:XValidation:rule="!has(self.conditions) || self.conditions.all(c, has(c.lastTransitionTime))",message="status.conditions entries must set lastTransitionTime"
85+
// +kubebuilder:validation:XValidation:message="status.conditions reason field must be <= 64 characters",rule="!has(self.conditions) || self.conditions.all(c, !has(c.reason) || size(c.reason) <= 64)"
86+
// +kubebuilder:validation:XValidation:message="status.conditions message field must be <= 2048 characters",rule="!has(self.conditions) || self.conditions.all(c, !has(c.message) || size(c.message) <= 2048)"
8387
type StoryRunStatus struct {
8488
// observedGeneration is the most recent generation observed for this StoryRun. It corresponds to the
8589
// StoryRun's generation, which is updated on mutation by the API Server.

api/runs/v1alpha1/zz_generated.deepcopy.go

Lines changed: 69 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v1alpha1/shared_types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ type ExecutionOverrides struct {
114114
ServiceAccountName *string `json:"serviceAccountName,omitempty"`
115115

116116
// AutomountServiceAccountToken controls whether a service account token should be automatically mounted.
117-
// Defaults to false.
117+
// +kubebuilder:default=true
118118
// +optional
119119
AutomountServiceAccountToken *bool `json:"automountServiceAccountToken,omitempty"`
120120

api/v1alpha1/story_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ type Story struct {
5353
}
5454

5555
// StorySpec defines what the workflow does and how it should run
56+
// +kubebuilder:validation:XValidation:rule="self.steps.all(step, has(step.ref) != has(step.type))",message="each step must set exactly one of ref or type"
57+
// +kubebuilder:validation:XValidation:rule="self.steps.all(step, self.steps.exists_one(other, other.name == step.name))",message="step names must be unique"
5658
type StorySpec struct {
5759
// Pattern specifies the execution model for the Story.
5860
// "batch" stories are run to completion via a StoryRun.

cmd/main.go

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@ limitations under the License.
1717
package main
1818

1919
import (
20+
"context"
2021
"crypto/tls"
2122
"flag"
2223
"os"
24+
"time"
2325

2426
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
2527
// to ensure that exec-entrypoint and run can make use of them.
2628
_ "k8s.io/client-go/plugin/pkg/client/auth"
2729

30+
apierrors "k8s.io/apimachinery/pkg/api/errors"
2831
"k8s.io/apimachinery/pkg/runtime"
2932
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
3033
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
@@ -40,6 +43,7 @@ import (
4043
setup "github.com/bubustack/bobrapet/internal/setup"
4144
"github.com/bubustack/bobrapet/pkg/cel"
4245
"github.com/bubustack/bobrapet/pkg/logging"
46+
"github.com/bubustack/bobrapet/pkg/observability"
4347

4448
catalogv1alpha1 "github.com/bubustack/bobrapet/api/catalog/v1alpha1"
4549
runsv1alpha1 "github.com/bubustack/bobrapet/api/runs/v1alpha1"
@@ -75,6 +79,11 @@ func main() {
7579
var secureMetrics bool
7680
var enableHTTP2 bool
7781
var tlsOpts []func(*tls.Config)
82+
83+
// Operator configuration flags
84+
var operatorConfigNamespace string
85+
var operatorConfigName string
86+
7887
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
7988
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
8089
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
@@ -92,6 +101,13 @@ func main() {
92101
flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
93102
flag.BoolVar(&enableHTTP2, "enable-http2", false,
94103
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
104+
105+
// Operator configuration flags (similar to kube-controller-manager --kubeconfig pattern)
106+
flag.StringVar(&operatorConfigNamespace, "config-namespace", "bobrapet-system",
107+
"The namespace where the operator configuration ConfigMap resides.")
108+
flag.StringVar(&operatorConfigName, "config-name", "bobrapet-operator-config",
109+
"The name of the operator configuration ConfigMap.")
110+
95111
opts := zap.Options{
96112
Development: true,
97113
}
@@ -140,7 +156,12 @@ func main() {
140156
managerCtx := ctrl.SetupSignalHandler()
141157
setup.SetupIndexers(managerCtx, mgr)
142158

143-
operatorConfigManager, controllerConfig, configResolver, celEvaluator := mustInitOperatorServices(mgr)
159+
operatorConfigManager, controllerConfig, configResolver, celEvaluator := mustInitOperatorServices(
160+
mgr,
161+
managerCtx,
162+
operatorConfigNamespace,
163+
operatorConfigName,
164+
)
144165

145166
deps := config.ControllerDependencies{
146167
Client: mgr.GetClient(),
@@ -214,19 +235,51 @@ func buildMetricsServerOptions(
214235

215236
func mustInitOperatorServices(
216237
mgr ctrl.Manager,
238+
startupCtx context.Context,
239+
configNamespace string,
240+
configName string,
217241
) (*config.OperatorConfigManager, *config.ControllerConfig, *config.Resolver, *cel.Evaluator) {
218242
operatorConfigManager := config.NewOperatorConfigManager(
219243
mgr.GetClient(),
220-
"bobrapet-system",
221-
"bobrapet-operator-config",
244+
configNamespace,
245+
configName,
222246
)
223-
setupLog.Info("Operator configuration manager initialized")
224-
if err := mgr.Add(operatorConfigManager); err != nil {
225-
setupLog.Error(err, "unable to add operator config manager to manager")
247+
operatorConfigManager.SetAPIReader(mgr.GetAPIReader())
248+
249+
setupLog.Info("Operator configuration manager initialized",
250+
"configNamespace", configNamespace,
251+
"configName", configName)
252+
253+
// Setup the config manager as a reconciler (event-driven)
254+
if err := operatorConfigManager.SetupWithManager(mgr); err != nil {
255+
setupLog.Error(err, "unable to setup operator config manager controller")
226256
os.Exit(1)
227257
}
258+
setupLog.Info("Operator config manager controller registered")
259+
260+
loadCtx, cancel := context.WithTimeout(startupCtx, 15*time.Second)
261+
defer cancel()
262+
if err := operatorConfigManager.LoadInitial(loadCtx); err != nil {
263+
if apierrors.IsNotFound(err) {
264+
setupLog.Info("Operator config map not found; continuing with defaults",
265+
"configNamespace", configNamespace,
266+
"configName", configName)
267+
} else {
268+
setupLog.Error(err, "failed to load operator configuration during startup",
269+
"configNamespace", configNamespace,
270+
"configName", configName)
271+
os.Exit(1)
272+
}
273+
} else {
274+
setupLog.Info("Operator configuration loaded from ConfigMap",
275+
"configNamespace", configNamespace,
276+
"configName", configName)
277+
}
278+
228279
controllerConfig := operatorConfigManager.GetControllerConfig()
229280
setupLog.Info("Controller configuration loaded")
281+
config.EnableTelemetry(controllerConfig.TelemetryEnabled)
282+
observability.EnableTracing(controllerConfig.TelemetryEnabled)
230283
configResolver := config.NewResolver(mgr.GetClient(), operatorConfigManager)
231284
setupLog.Info("Configuration resolver initialized")
232285
celLogger := logging.NewCELLogger(ctrl.Log)
@@ -261,6 +314,12 @@ func mustSetupControllers(
261314
setupLog.Error(err, "unable to create controller", "controller", "Impulse")
262315
os.Exit(1)
263316
}
317+
if err := (&controller.RealtimeEngramReconciler{
318+
ControllerDependencies: deps,
319+
}).SetupWithManager(mgr, controllerConfig.BuildEngramControllerOptions()); err != nil {
320+
setupLog.Error(err, "unable to create controller", "controller", "RealtimeEngram")
321+
os.Exit(1)
322+
}
264323
if err := (&runscontroller.StoryRunReconciler{
265324
ControllerDependencies: deps,
266325
}).SetupWithManager(mgr, controllerConfig.BuildStoryRunControllerOptions()); err != nil {
@@ -293,7 +352,8 @@ func setupWebhooksIfEnabled(mgr ctrl.Manager, operatorConfigManager *config.Oper
293352
}
294353
setupLog.Info("setting up webhooks")
295354
if err := (&webhookv1alpha1.StoryWebhook{
296-
Config: operatorConfigManager.GetControllerConfig(),
355+
Config: operatorConfigManager.GetControllerConfig(),
356+
ConfigManager: operatorConfigManager,
297357
}).SetupWebhookWithManager(mgr); err != nil {
298358
setupLog.Error(err, "unable to create webhook", "webhook", "Story")
299359
os.Exit(1)
@@ -311,12 +371,16 @@ func setupWebhooksIfEnabled(mgr ctrl.Manager, operatorConfigManager *config.Oper
311371
os.Exit(1)
312372
}
313373
if err := (&webhookrunsv1alpha1.StoryRunWebhook{
314-
Config: operatorConfigManager.GetControllerConfig(),
374+
Config: operatorConfigManager.GetControllerConfig(),
375+
ConfigManager: operatorConfigManager,
315376
}).SetupWebhookWithManager(mgr); err != nil {
316377
setupLog.Error(err, "unable to create webhook", "webhook", "StoryRun")
317378
os.Exit(1)
318379
}
319-
if err := (&webhookrunsv1alpha1.StepRunWebhook{}).SetupWebhookWithManager(mgr); err != nil {
380+
if err := (&webhookrunsv1alpha1.StepRunWebhook{
381+
Config: operatorConfigManager.GetControllerConfig(),
382+
ConfigManager: operatorConfigManager,
383+
}).SetupWebhookWithManager(mgr); err != nil {
320384
setupLog.Error(err, "unable to create webhook", "webhook", "StepRun")
321385
os.Exit(1)
322386
}

0 commit comments

Comments
 (0)