From b92a73172ef479aa6d8fa5af5f313c989adea4e3 Mon Sep 17 00:00:00 2001 From: kchoudhary Date: Thu, 17 Sep 2026 14:56:36 +0530 Subject: [PATCH 1/2] feat(agent-runtime): run postgres schema setup job --- config/manager/kustomization.yaml | 1 + .../ai_v1_aiplatform_agentruntime.yaml | 5 + .../templates/deployment.yaml | 2 + helm-chart/splunk-ai-operator/values.yaml | 1 + internal/controller/aiservice_controller.go | 49 ++++ pkg/ai/features/agentruntime/impl.go | 277 +++++++++++++++++- pkg/ai/features/agentruntime/impl_test.go | 53 +++- scripts/generate-bom.sh | 1 + .../install_minio_ec2.sh | 10 +- tools/cluster_setup/K0S_README.md | 12 + tools/cluster_setup/artifacts.yaml | 2 + tools/cluster_setup/k0s_cluster_with_stack.sh | 16 +- 12 files changed, 414 insertions(+), 15 deletions(-) diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index a8b55b7e..5341c9da 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -15,6 +15,7 @@ patches: \ - name: RELATED_IMAGE_SAIA_API\n value: \"667741767953.dkr.ecr.us-west-2.amazonaws.com/ml-platform/saia/saia-api:build-1\"\n \ - name: RELATED_IMAGE_SLIM_API\n value: \"667741767953.dkr.ecr.us-west-2.amazonaws.com/ml-platform/slim/slim-api:build-1\"\n \ - name: RELATED_IMAGE_AGENT_RUNTIME_BASE\n value: \"docker.io/splunk/agent-runtime:latest\"\n + \ - name: RELATED_IMAGE_AGENT_RUNTIME_SCHEMA_SETUP\n value: \"docker.io/splunk/agent-runtime-schema-setup:latest\"\n \ - name: RELATED_IMAGE_AGENT_RUNTIME_PROVIDER_MLTK\n value: \"docker.io/splunk/agent-runtime-provider-mltk:latest\"\n \ - name: RELATED_AGENT_RUNTIME_MODULE_PROVIDER_MLTK\n value: \"agentcore_operations.loader:MLTKAgentLoader\"\n \ - name: RELATED_IMAGE_POST_INSTALL_HOOK\n value: \"667741767953.dkr.ecr.us-west-2.amazonaws.com/ml-platform/saia/saia-data-loader:build-1\"\n diff --git a/config/samples/ai_v1_aiplatform_agentruntime.yaml b/config/samples/ai_v1_aiplatform_agentruntime.yaml index f77e647f..0a0a15c0 100644 --- a/config/samples/ai_v1_aiplatform_agentruntime.yaml +++ b/config/samples/ai_v1_aiplatform_agentruntime.yaml @@ -5,6 +5,11 @@ metadata: type: Opaque stringData: DATABASE_URL: postgresql://user:password@postgres.default.svc.cluster.local:5432/checkpoints + PG_HOST: postgres.default.svc.cluster.local + PG_PORT: "5432" + PG_USER: user + PG_PASSWORD: password + PG_DBNAME: checkpoints --- apiVersion: ai.splunk.com/v1 kind: AIPlatform diff --git a/helm-chart/splunk-ai-operator/templates/deployment.yaml b/helm-chart/splunk-ai-operator/templates/deployment.yaml index ca0e5255..04ee4baf 100644 --- a/helm-chart/splunk-ai-operator/templates/deployment.yaml +++ b/helm-chart/splunk-ai-operator/templates/deployment.yaml @@ -82,6 +82,8 @@ spec: value: {{ .Values.slimApiImage }} - name: RELATED_IMAGE_AGENT_RUNTIME_BASE value: {{ .Values.agentRuntimeBaseImage | quote }} + - name: RELATED_IMAGE_AGENT_RUNTIME_SCHEMA_SETUP + value: {{ .Values.agentRuntimeSchemaSetupImage | quote }} {{- range $version, $image := .Values.agentRuntimeBaseImages }} - name: RELATED_IMAGE_AGENT_RUNTIME_BASE_{{ $version | upper | replace "." "_" | replace "-" "_" }} value: {{ $image | quote }} diff --git a/helm-chart/splunk-ai-operator/values.yaml b/helm-chart/splunk-ai-operator/values.yaml index 5adc5d9a..1b60ccda 100644 --- a/helm-chart/splunk-ai-operator/values.yaml +++ b/helm-chart/splunk-ai-operator/values.yaml @@ -116,6 +116,7 @@ slimApiImage: "docker.io/splunk/slim-api:1.0.0" # Agent Runtime images. The base image is shared; provider images are carrier # payloads keyed by feature.provider. agentRuntimeBaseImage: "docker.io/splunk/agent-runtime:latest" +agentRuntimeSchemaSetupImage: "docker.io/splunk/agent-runtime-schema-setup:latest" agentRuntimeBaseImages: {} # Example: # v2.0.0: "docker.io/splunk/agent-runtime:v2.0.0" diff --git a/internal/controller/aiservice_controller.go b/internal/controller/aiservice_controller.go index 8bf8a2ee..c0b25cd0 100644 --- a/internal/controller/aiservice_controller.go +++ b/internal/controller/aiservice_controller.go @@ -27,6 +27,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" @@ -228,11 +229,19 @@ func (r *AIServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { common.AnnotationChangedPredicate(), )), ). + // Watch the referenced checkpoint Secret because it is user-owned and not + // an AIService child resource. + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(r.findAIServicesForCheckpointSecret), + builder.WithPredicates(checkpointSecretChangedPredicate()), + ). // Add predicates to filter events and avoid unnecessary reconciliations WithEventFilter(predicate.Or( common.GenerationChangedPredicate(), common.AnnotationChangedPredicate(), common.LabelChangedPredicate(), + checkpointSecretChangedPredicate(), )). // Configure concurrency control WithOptions(controller.Options{ @@ -326,6 +335,46 @@ func (r *AIServiceReconciler) findAIServicesForPlatform(ctx context.Context, pla return requests } +// findAIServicesForCheckpointSecret maps a checkpoint Secret to AgentRuntime +// AIService objects that reference it in the same namespace. +func (r *AIServiceReconciler) findAIServicesForCheckpointSecret(ctx context.Context, secret client.Object) []reconcile.Request { + log := logf.FromContext(ctx) + var services aiv1.AIServiceList + if err := r.List(ctx, &services, client.InNamespace(secret.GetNamespace())); err != nil { + log.Error(err, "failed to list AIServices for checkpoint Secret", "secret", secret.GetName()) + return nil + } + + requests := make([]reconcile.Request, 0) + for _, svc := range services.Items { + if svc.Spec.Feature.Name == "agentruntime" && svc.Spec.CheckpointDbSecretRef == secret.GetName() { + requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{ + Name: svc.Name, + Namespace: svc.Namespace, + }}) + } + } + return requests +} + +func checkpointSecretChangedPredicate() predicate.Predicate { + return predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + _, ok := e.Object.(*corev1.Secret) + return ok + }, + UpdateFunc: func(e event.UpdateEvent) bool { + oldSecret, oldOK := e.ObjectOld.(*corev1.Secret) + newSecret, newOK := e.ObjectNew.(*corev1.Secret) + return oldOK && newOK && oldSecret.ResourceVersion != newSecret.ResourceVersion + }, + DeleteFunc: func(e event.DeleteEvent) bool { + _, ok := e.Object.(*corev1.Secret) + return ok + }, + } +} + func containsString(slice []string, s string) bool { for _, x := range slice { if x == s { diff --git a/pkg/ai/features/agentruntime/impl.go b/pkg/ai/features/agentruntime/impl.go index 94524ac4..00dc90a5 100644 --- a/pkg/ai/features/agentruntime/impl.go +++ b/pkg/ai/features/agentruntime/impl.go @@ -2,6 +2,8 @@ package agentruntime import ( "context" + "crypto/sha256" + "encoding/json" "fmt" "hash/fnv" "os" @@ -16,7 +18,9 @@ import ( "github.com/splunk/splunk-ai-operator/pkg/ai/features/common" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -29,14 +33,20 @@ import ( ) const ( - defaultMinReplicas int32 = 1 - defaultMaxReplicas int32 = 4 - defaultTargetCPUUtilization int32 = 60 - defaultAgentRuntimeHTTPPort int32 = 8080 - defaultAgentRuntimeMetrics int32 = 9090 - maxDNSLabelLength = 63 - mtlsTerminationOperator = "operator" - sharedPackagesPath = "/shared-packages" + defaultMinReplicas int32 = 1 + defaultMaxReplicas int32 = 4 + defaultTargetCPUUtilization int32 = 60 + defaultAgentRuntimeHTTPPort int32 = 8080 + defaultAgentRuntimeMetrics int32 = 9090 + maxDNSLabelLength = 63 + mtlsTerminationOperator = "operator" + sharedPackagesPath = "/shared-packages" + schemaSetupImageEnv = "RELATED_IMAGE_AGENT_RUNTIME_SCHEMA_SETUP" + schemaJobDeploymentHashAnnotation = "ai.splunk.com/agentruntime-deployment-hash" + schemaJobInputHashAnnotation = "ai.splunk.com/agentruntime-schema-input-hash" + schemaJobManagedAnnotation = "ai.splunk.com/agentruntime-schema-job" + schemaJobManagedValue = "true" + schemaJobBackoffLimit int32 = 1 ) var agentModulePattern = regexp.MustCompile( @@ -74,6 +84,7 @@ func (r *AgentRuntimeReconciler) Reconcile(ctx context.Context, aiservice *aiv1. {"ServiceAccount", r.reconcileServiceAccount}, {"AgentRuntimeConfigMap", r.reconcileConfigMap}, {"Certificate", r.reconcileCertificate}, + {"PostgresSchemaSetup", r.reconcilePostgresSchemaSetup}, {"AgentRuntimeDeployment", r.reconcileDeployment}, {"AgentRuntimeService", r.reconcileService}, {"AgentRuntimeHPA", r.reconcileHPA}, @@ -122,6 +133,10 @@ func (r *AgentRuntimeReconciler) validateAIService(ctx context.Context, ai *aiv1 r.Recorder.Event(ai, corev1.EventTypeWarning, "InvalidSpec", err.Error()) return err } + if _, err := resolveSchemaSetupImage(); err != nil { + r.Recorder.Event(ai, corev1.EventTypeWarning, "InvalidSpec", err.Error()) + return err + } if ai.Spec.CheckpointDbSecretRef == "" { r.Recorder.Event(ai, corev1.EventTypeWarning, "InvalidSpec", "checkpointDbSecretRef must be set") return fmt.Errorf("checkpointDbSecretRef must be set") @@ -283,6 +298,244 @@ func (r *AgentRuntimeReconciler) reconcileCertificate(ctx context.Context, ai *a return err } +func (r *AgentRuntimeReconciler) reconcilePostgresSchemaSetup(ctx context.Context, ai *aiv1.AIService) error { + config, err := resolveAgentRuntimeConfig(ai) + if err != nil { + return err + } + schemaImage, err := resolveSchemaSetupImage() + if err != nil { + return err + } + + checkpointSecret := &corev1.Secret{} + if err := r.Get(ctx, types.NamespacedName{ + Name: ai.Spec.CheckpointDbSecretRef, + Namespace: ai.Namespace, + }, checkpointSecret); err != nil { + return fmt.Errorf("get checkpoint DB Secret %q: %w", ai.Spec.CheckpointDbSecretRef, err) + } + if err := validateSchemaSetupCredentials(checkpointSecret, ai.Spec.Feature.Env); err != nil { + return err + } + + deploymentHash := agentRuntimeDeploymentHash(ai, config) + inputHash := agentRuntimeSchemaInputHash(ai, schemaImage, checkpointSecret) + job := &batchv1.Job{} + if ai.Status.SchemaJobId != "" { + err := r.Get(ctx, types.NamespacedName{Name: ai.Status.SchemaJobId, Namespace: ai.Namespace}, job) + if err == nil { + if !metav1.IsControlledBy(job, ai) { + return fmt.Errorf("postgres schema setup Job %q is not owned by AIService %q", job.Name, ai.Name) + } + if !schemaJobNeedsRerun(job, deploymentHash, inputHash) { + if schemaJobSucceeded(job) { + return nil + } + if schemaJobFailed(job) { + return fmt.Errorf("postgres schema setup Job %q failed", job.Name) + } + return fmt.Errorf("postgres schema setup Job %q is still running", job.Name) + } + } else if !apierrors.IsNotFound(err) { + return fmt.Errorf("get postgres schema setup Job %q: %w", ai.Status.SchemaJobId, err) + } + ai.Status.SchemaJobId = "" + } + + labels, annotations := labelsAndAnnotations(ai) + labels["component"] = "agentruntime-schema-setup" + annotations[schemaJobDeploymentHashAnnotation] = deploymentHash + annotations[schemaJobInputHashAnnotation] = inputHash + annotations[schemaJobManagedAnnotation] = schemaJobManagedValue + job = &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: dnsLabelName(ai.Name, "schema-"+shortHash(deploymentHash+"-"+inputHash)), + Namespace: ai.Namespace, + Labels: labels, + Annotations: annotations, + }, + } + if err := controllerutil.SetControllerReference(ai, job, r.Scheme); err != nil { + return fmt.Errorf("ownerref on postgres schema setup Job: %w", err) + } + + featureEnv := buildSchemaSetupEnv(ai.Spec.Feature.Env) + job.Spec = batchv1.JobSpec{ + BackoffLimit: ptr(schemaJobBackoffLimit), + TTLSecondsAfterFinished: ptr(int32(86400)), + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: labels}, + Spec: corev1.PodSpec{ + ServiceAccountName: ai.Spec.ServiceAccountName, + RestartPolicy: corev1.RestartPolicyNever, + Containers: []corev1.Container{{ + Name: "schema-setup", + Image: schemaImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Env: featureEnv, + EnvFrom: []corev1.EnvFromSource{{ + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: ai.Spec.CheckpointDbSecretRef}, + }, + }}, + }}, + Affinity: &ai.Spec.Affinity, + Tolerations: ai.Spec.Tolerations, + NodeSelector: ai.Spec.NodeSelector, + ImagePullSecrets: ai.Spec.ImagePullSecrets, + }, + }, + } + if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, job, func() error { + return nil + }); err != nil { + return fmt.Errorf("create postgres schema setup Job: %w", err) + } + ai.Status.SchemaJobId = job.Name + return fmt.Errorf("created postgres schema setup Job %q, waiting for completion", job.Name) +} + +func validateSchemaSetupCredentials(secret *corev1.Secret, featureEnv map[string]string) error { + for _, name := range []string{"PG_HOST", "PG_USER", "PG_DBNAME", "PG_PASSWORD"} { + if strings.TrimSpace(featureEnv[name]) == "" && len(secret.Data[name]) == 0 { + return fmt.Errorf("postgres schema setup requires %s in checkpoint DB Secret or agentruntime feature env", name) + } + } + return nil +} + +func buildFeatureEnv(values map[string]string) []corev1.EnvVar { + names := make([]string, 0, len(values)) + for name := range values { + names = append(names, name) + } + sort.Strings(names) + env := make([]corev1.EnvVar, 0, len(names)) + for _, name := range names { + env = append(env, corev1.EnvVar{Name: name, Value: values[name]}) + } + return env +} + +func buildSchemaSetupEnv(values map[string]string) []corev1.EnvVar { + schemaValues := make(map[string]string, len(values)+1) + for name, value := range values { + schemaValues[name] = value + } + // The runtime contract uses PG_SSLMODE, while libpq/psql consumes the + // standard PGSSLMODE variable. Keep the existing runtime key compatible and + // bridge it for the schema-setup image when the caller has not supplied the + // standard name explicitly. + if schemaValues["PGSSLMODE"] == "" && schemaValues["PG_SSLMODE"] != "" { + schemaValues["PGSSLMODE"] = schemaValues["PG_SSLMODE"] + } + return buildFeatureEnv(schemaValues) +} + +func agentRuntimeDeploymentHash(ai *aiv1.AIService, config agentRuntimeConfig) string { + return stableHash(struct { + BaseImage string + ProviderImage string + AgentModule string + Replicas int32 + ServiceAccount string + Resources corev1.ResourceRequirements + RuntimeEnv []corev1.EnvVar + CheckpointSecret string + NodeSelector map[string]string + Tolerations []corev1.Toleration + Affinity corev1.Affinity + ImagePullSecrets []corev1.LocalObjectReference + }{ + BaseImage: config.BaseImage, + ProviderImage: config.ProviderImage, + AgentModule: config.AgentModule, + Replicas: ai.Spec.Replicas, + ServiceAccount: ai.Spec.ServiceAccountName, + Resources: ai.Spec.Resources, + RuntimeEnv: buildRuntimeEnvFingerprint(ai, config.AgentModule), + CheckpointSecret: ai.Spec.CheckpointDbSecretRef, + NodeSelector: ai.Spec.NodeSelector, + Tolerations: ai.Spec.Tolerations, + Affinity: ai.Spec.Affinity, + ImagePullSecrets: ai.Spec.ImagePullSecrets, + }) +} + +func agentRuntimeSchemaInputHash(ai *aiv1.AIService, schemaImage string, secret *corev1.Secret) string { + postgresEnv := map[string]string{} + for _, name := range []string{"PG_HOST", "PG_PORT", "PG_USER", "PG_DBNAME", "PG_SSLMODE", "PGSSLMODE", "PGSSLROOTCERT"} { + if value := ai.Spec.Feature.Env[name]; value != "" { + postgresEnv[name] = value + } + } + return stableHash(struct { + Image string + SchemaSecretRef string + SecretVersion string + PostgresEnv map[string]string + }{ + Image: schemaImage, + SchemaSecretRef: ai.Spec.CheckpointDbSecretRef, + SecretVersion: secret.ResourceVersion, + PostgresEnv: postgresEnv, + }) +} + +func buildRuntimeEnvFingerprint(ai *aiv1.AIService, agentModule string) []corev1.EnvVar { + env := buildAgentRuntimeEnv(ai, agentModule) + for i := range env { + if isSensitiveEnvName(env[i].Name) { + env[i].Value = "" + } + } + return env +} + +func isSensitiveEnvName(name string) bool { + name = strings.ToUpper(name) + return strings.Contains(name, "PASSWORD") || + strings.Contains(name, "TOKEN") || + strings.Contains(name, "SECRET") || + strings.Contains(name, "PRIVATE_KEY") +} + +func stableHash(value any) string { + encoded, err := json.Marshal(value) + if err != nil { + return "invalid" + } + digest := sha256.Sum256(encoded) + return fmt.Sprintf("%x", digest[:8]) +} + +func schemaJobNeedsRerun(job *batchv1.Job, deploymentHash, inputHash string) bool { + if job.Annotations[schemaJobManagedAnnotation] != schemaJobManagedValue { + return false + } + return job.Annotations[schemaJobDeploymentHashAnnotation] != deploymentHash && + job.Annotations[schemaJobInputHashAnnotation] != inputHash +} + +func schemaJobSucceeded(job *batchv1.Job) bool { + for _, condition := range job.Status.Conditions { + if condition.Type == batchv1.JobComplete && condition.Status == corev1.ConditionTrue { + return true + } + } + return false +} + +func schemaJobFailed(job *batchv1.Job) bool { + for _, condition := range job.Status.Conditions { + if condition.Type == batchv1.JobFailed && condition.Status == corev1.ConditionTrue { + return true + } + } + return false +} + func (r *AgentRuntimeReconciler) reconcileDeployment(ctx context.Context, ai *aiv1.AIService) error { config, err := resolveAgentRuntimeConfig(ai) if err != nil { @@ -654,6 +907,10 @@ func resolveBaseImage(ai *aiv1.AIService) (string, error) { return resolveRequiredRuntimeEnv("RELATED_IMAGE_AGENT_RUNTIME_BASE", "") } +func resolveSchemaSetupImage() (string, error) { + return resolveRequiredRuntimeEnv(schemaSetupImageEnv, "") +} + func resolveProviderImage(ai *aiv1.AIService) (string, error) { envName := "RELATED_IMAGE_AGENT_RUNTIME_PROVIDER_" + normalizeEnvKeySegment(ai.Spec.Feature.Provider) return resolveRequiredRuntimeEnv(envName, fmt.Sprintf(" for provider %q", ai.Spec.Feature.Provider)) @@ -719,3 +976,7 @@ func normalizeEnvKeySegment(value string) string { } return strings.Trim(b.String(), "_") } + +func ptr[T any](value T) *T { + return &value +} diff --git a/pkg/ai/features/agentruntime/impl_test.go b/pkg/ai/features/agentruntime/impl_test.go index 92b5af6f..5ccdb1b6 100644 --- a/pkg/ai/features/agentruntime/impl_test.go +++ b/pkg/ai/features/agentruntime/impl_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -29,6 +30,7 @@ func buildAgentRuntimeTestScheme(t *testing.T) *runtime.Scheme { require.NoError(t, corev1.AddToScheme(s)) require.NoError(t, appsv1.AddToScheme(s)) require.NoError(t, autoscalingv2.AddToScheme(s)) + require.NoError(t, batchv1.AddToScheme(s)) require.NoError(t, monitoringv1.AddToScheme(s)) require.NoError(t, certmanagerv1.AddToScheme(s)) return s @@ -38,6 +40,7 @@ func TestAgentRuntimeFactoryHandler_ReconcilesLifecycle(t *testing.T) { t.Setenv("RELATED_IMAGE_AGENT_RUNTIME_BASE", "docker.io/splunk/agent-runtime:test") t.Setenv("RELATED_IMAGE_AGENT_RUNTIME_PROVIDER_MLTK", "docker.io/splunk/agent-runtime-provider-mltk:test") t.Setenv("RELATED_AGENT_RUNTIME_MODULE_PROVIDER_MLTK", "agentcore_operations.loader:MLTKAgentLoader") + t.Setenv(schemaSetupImageEnv, "docker.io/splunk/agent-runtime-schema-setup:test") scheme := buildAgentRuntimeTestScheme(t) ai := &aiv1.AIService{ @@ -57,12 +60,31 @@ func TestAgentRuntimeFactoryHandler_ReconcilesLifecycle(t *testing.T) { }, } - fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ai).Build() + checkpointSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "mltk-postgres", Namespace: "default", ResourceVersion: "1"}, + Data: map[string][]byte{ + "PG_HOST": []byte("postgres.default.svc.cluster.local"), + "PG_PORT": []byte("5432"), + "PG_USER": []byte("user"), + "PG_PASSWORD": []byte("password"), + "PG_DBNAME": []byte("checkpoints"), + }, + } + fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ai, checkpointSecret).Build() factory := &AgentRuntimeFactory{} handler, err := factory.New(context.Background(), fakeClient, scheme, ai, record.NewFakeRecorder(20)) require.NoError(t, err) - require.NoError(t, handler.Reconcile(context.Background(), ai)) + require.ErrorContains(t, handler.Reconcile(context.Background(), ai), "waiting for completion") + job := &batchv1.Job{} + require.NoError(t, fakeClient.Get(context.Background(), types.NamespacedName{ + Name: ai.Status.SchemaJobId, Namespace: ai.Namespace, + }, job)) + assert.Error(t, fakeClient.Get(context.Background(), types.NamespacedName{ + Name: ai.Name + "-agentruntime-deployment", Namespace: ai.Namespace, + }, &appsv1.Deployment{})) + job.Status.Conditions = []batchv1.JobCondition{{Type: batchv1.JobComplete, Status: corev1.ConditionTrue}} + require.NoError(t, fakeClient.Status().Update(context.Background(), job)) require.NoError(t, handler.Reconcile(context.Background(), ai)) for _, object := range []ctrlclient.Object{ @@ -92,6 +114,7 @@ func TestAgentRuntimeFactoryHandler_ReconcilesLifecycle(t *testing.T) { assertConditionTrue(t, ai.Status.Conditions, "ValidateReady") assertConditionTrue(t, ai.Status.Conditions, "ServiceAccountReady") assertConditionTrue(t, ai.Status.Conditions, "AgentRuntimeConfigMapReady") + assertConditionTrue(t, ai.Status.Conditions, "PostgresSchemaSetupReady") assertConditionTrue(t, ai.Status.Conditions, "AgentRuntimeDeploymentReady") assertConditionTrue(t, ai.Status.Conditions, "AgentRuntimeServiceReady") assertConditionTrue(t, ai.Status.Conditions, "AgentRuntimeHPAReady") @@ -443,6 +466,7 @@ func TestAgentRuntimeReconcileStopsBeforeResourcesWhenValidationFails(t *testing t.Setenv("RELATED_IMAGE_AGENT_RUNTIME_BASE", "docker.io/splunk/agent-runtime:test") t.Setenv("RELATED_IMAGE_AGENT_RUNTIME_PROVIDER_MLTK", "") t.Setenv("RELATED_AGENT_RUNTIME_MODULE_PROVIDER_MLTK", "agentcore_operations.loader:MLTKAgentLoader") + t.Setenv(schemaSetupImageEnv, "docker.io/splunk/agent-runtime-schema-setup:test") scheme := buildAgentRuntimeTestScheme(t) ai := &aiv1.AIService{ @@ -483,6 +507,7 @@ func TestAgentRuntimeReconcileStopsBeforeResourcesWhenAgentModuleIsInvalid(t *te t.Setenv("RELATED_IMAGE_AGENT_RUNTIME_BASE", "docker.io/splunk/agent-runtime:test") t.Setenv("RELATED_IMAGE_AGENT_RUNTIME_PROVIDER_MLTK", "docker.io/splunk/agent-runtime-provider-mltk:test") t.Setenv("RELATED_AGENT_RUNTIME_MODULE_PROVIDER_MLTK", "agentcore_operations.loader:MLTK-AgentLoader") + t.Setenv(schemaSetupImageEnv, "docker.io/splunk/agent-runtime-schema-setup:test") scheme := buildAgentRuntimeTestScheme(t) ai := &aiv1.AIService{ @@ -546,6 +571,30 @@ func assertEnv(t *testing.T, env []corev1.EnvVar, name, value string) { t.Fatalf("missing env %s", name) } +func TestSchemaJobNeedsRerunRequiresBothFingerprintsToChange(t *testing.T) { + job := &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ + schemaJobManagedAnnotation: schemaJobManagedValue, + schemaJobDeploymentHashAnnotation: "deployment-old", + schemaJobInputHashAnnotation: "input-old", + }}} + + assert.False(t, schemaJobNeedsRerun(job, "deployment-new", "input-old")) + assert.False(t, schemaJobNeedsRerun(job, "deployment-old", "input-new")) + assert.True(t, schemaJobNeedsRerun(job, "deployment-new", "input-new")) +} + +func TestValidateSchemaSetupCredentialsAllowsFeatureEnvFallback(t *testing.T) { + secret := &corev1.Secret{Data: map[string][]byte{"DATABASE_URL": []byte("postgresql://legacy")}} + featureEnv := map[string]string{ + "PG_HOST": "postgres", + "PG_USER": "user", + "PG_PASSWORD": "password", + "PG_DBNAME": "checkpoints", + } + + require.NoError(t, validateSchemaSetupCredentials(secret, featureEnv)) +} + func TestAgentRuntimeReconcileCertificate_UsesCommonReconciler(t *testing.T) { scheme := buildAgentRuntimeTestScheme(t) ai := &aiv1.AIService{ diff --git a/scripts/generate-bom.sh b/scripts/generate-bom.sh index 4f38424e..a44a404b 100755 --- a/scripts/generate-bom.sh +++ b/scripts/generate-bom.sh @@ -48,6 +48,7 @@ declare -A IMAGES=( # environment variables. Keep the BOM aligned with that contract rather than # maintaining a second hard-coded provider list here. IMAGES["agent-runtime-base"]="${RELATED_IMAGE_AGENT_RUNTIME_BASE:-docker.io/splunk/agent-runtime:latest}" +IMAGES["agent-runtime-schema-setup"]="${RELATED_IMAGE_AGENT_RUNTIME_SCHEMA_SETUP:-docker.io/splunk/agent-runtime-schema-setup:latest}" # Include any runtime-version-specific base images that are configured in the # operator environment, for example RELATED_IMAGE_AGENT_RUNTIME_BASE_V2_0_0. diff --git a/tools/artifacts_download_upload_scripts/install_minio_ec2.sh b/tools/artifacts_download_upload_scripts/install_minio_ec2.sh index fb680af5..0b1538c6 100755 --- a/tools/artifacts_download_upload_scripts/install_minio_ec2.sh +++ b/tools/artifacts_download_upload_scripts/install_minio_ec2.sh @@ -166,7 +166,8 @@ if [[ -z "${MINIO_ROOT_PASSWORD}" ]]; then log "Generated MINIO_ROOT_PASSWORD (save it for cluster-config.yaml)" fi -# Install MinIO binary (use stable "latest" URL; archive URLs can 404 and return HTML) +# Install MinIO AIStor binaries. The community-edition download endpoint was +# retired and now returns HTTP 410; AIStor is the current supported binary path. install_minio_binary() { local arch arch="$(uname -m)" @@ -175,10 +176,10 @@ install_minio_binary() { aarch64|arm64) arch=arm64 ;; *) err "Unsupported arch: $arch"; exit 1 ;; esac - local url="https://dl.min.io/server/minio/release/linux-${arch}/minio" + local url="https://dl.min.io/aistor/minio/release/linux-${arch}/minio" local tmp="/tmp/minio.$$" log "Downloading MinIO (linux-${arch})..." - if ! curl -sSL -o "$tmp" "$url"; then + if ! curl -fsSL --retry 3 --connect-timeout 20 -o "$tmp" "$url"; then err "Download failed. Check network or try: curl -sSL -o /tmp/minio '$url'" rm -f "$tmp" exit 1 @@ -207,7 +208,8 @@ install_mc() { esac local tmp="/tmp/mc.$$" log "Downloading MinIO Client (mc)..." - if ! curl -sSL -o "$tmp" "https://dl.min.io/client/mc/release/linux-${arch}/mc"; then + local url="https://dl.min.io/aistor/mc/release/linux-${arch}/mc" + if ! curl -fsSL --retry 3 --connect-timeout 20 -o "$tmp" "$url"; then err "Download failed for mc." rm -f "$tmp" exit 1 diff --git a/tools/cluster_setup/K0S_README.md b/tools/cluster_setup/K0S_README.md index 19c444c9..fadee056 100644 --- a/tools/cluster_setup/K0S_README.md +++ b/tools/cluster_setup/K0S_README.md @@ -439,6 +439,7 @@ Short image paths (without a FQDN) are automatically prefixed with `images.regis | `images.saia.apiV2Image` | **Yes** | — | SAIA API v2 image | | `images.saia.dataLoaderImage` | **Yes** | — | SAIA data loader / post-install hook image | | `images.agentRuntime.baseImage` | When `agentruntime` enabled | — | Shared agent-runtime base image | +| `images.agentRuntime.schemaSetupImage` | When `agentruntime` enabled | — | Published image containing the Agent Runtime PostgreSQL schema and setup entrypoint | | `images.agentRuntime.baseImages.` | No | — | Optional runtime-version-specific base image override | | `images.agentRuntime.providerImages.` | When `agentruntime` enabled | — | Provider package image for each agent-runtime provider | | `images.agentRuntime.providerModules.` | No | provider default | Python loader module for each provider | @@ -471,6 +472,7 @@ Short image paths (without a FQDN) are automatically prefixed with `images.regis | `images.saia.apiV2Image` | `RELATED_IMAGE_SAIA_API_V2` | `artifacts.yaml` | | `images.saia.dataLoaderImage` | `RELATED_IMAGE_POST_INSTALL_HOOK` | `artifacts.yaml` | | `images.agentRuntime.baseImage` | `RELATED_IMAGE_AGENT_RUNTIME_BASE` | `artifacts.yaml` | +| `images.agentRuntime.schemaSetupImage` | `RELATED_IMAGE_AGENT_RUNTIME_SCHEMA_SETUP` | `artifacts.yaml` | | `images.agentRuntime.baseImages.` | `RELATED_IMAGE_AGENT_RUNTIME_BASE_` | `artifacts.yaml` | | `images.agentRuntime.providerImages.` | `RELATED_IMAGE_AGENT_RUNTIME_PROVIDER_` | `artifacts.yaml` | | `images.agentRuntime.providerModules.` | `RELATED_AGENT_RUNTIME_MODULE_PROVIDER_` | `artifacts.yaml` | @@ -496,6 +498,7 @@ Short image paths (without a FQDN) are automatically prefixed with `images.regis | `aiPlatform.features[].maxReplicas` | No | operator default | Maximum feature replicas | | `aiPlatform.features[].targetCPUUtilization` | No | operator default | HPA target CPU utilization | | `aiPlatform.features[].checkpointDbSecretRef` | Agent Runtime | — | Secret name containing checkpoint DB connection details | +| `aiPlatform.features[].env` | Agent Runtime | — | Optional runtime environment overrides; the schema image requires `PG_HOST`, `PG_USER`, `PG_PASSWORD`, and `PG_DBNAME` (with optional `PG_PORT`) from this map or the referenced Secret | | `aiPlatform.features[].serviceAccountName` | No | `""` | Service account override | | `aiPlatform.cpuScheduling.nodeSelector` | No | auto-generated | Node selector for CPU workloads | | `aiPlatform.cpuScheduling.tolerations` | No | `[]` | Tolerations for CPU workloads | @@ -504,6 +507,15 @@ Short image paths (without a FQDN) are automatically prefixed with `images.regis | `aiPlatform.serviceTemplate.type` | No | — | Service type for SAIA exposure: `NodePort` or `LoadBalancer` | | `aiPlatform.serviceTemplate.nodePort` | No | — | Node port number (only when type=NodePort) | +Agent Runtime schema preparation runs as an AIService-owned Job before the +AgentRuntime Deployment is created. The setup image is built from the +AgentRuntime repository's `cicd/docker/schema-setup/Dockerfile` and must contain +the compatible `schema.sql`. `DATABASE_URL` alone is not consumed by that image; +use the structured `PG_*` keys in the referenced Secret or AgentRuntime feature +environment. The operator also maps `PG_SSLMODE` to libpq's `PGSSLMODE` for the +schema Job. SAIA does not require these keys and its existing data-loader flow +is unchanged. + #### Optional Component Gates The `components` section controls which add-ons the installer deploys. Omitted diff --git a/tools/cluster_setup/artifacts.yaml b/tools/cluster_setup/artifacts.yaml index b8ab5867..1ec1de69 100644 --- a/tools/cluster_setup/artifacts.yaml +++ b/tools/cluster_setup/artifacts.yaml @@ -5879,6 +5879,8 @@ spec: value: 667741767953.dkr.ecr.us-west-2.amazonaws.com/ml-platform/slim/slim-api:build-1 - name: RELATED_IMAGE_AGENT_RUNTIME_BASE value: docker.io/splunk/agent-runtime:latest + - name: RELATED_IMAGE_AGENT_RUNTIME_SCHEMA_SETUP + value: docker.io/splunk/agent-runtime-schema-setup:latest - name: RELATED_IMAGE_AGENT_RUNTIME_PROVIDER_MLTK value: docker.io/splunk/agent-runtime-provider-mltk:latest - name: RELATED_AGENT_RUNTIME_MODULE_PROVIDER_MLTK diff --git a/tools/cluster_setup/k0s_cluster_with_stack.sh b/tools/cluster_setup/k0s_cluster_with_stack.sh index 1c1e67f9..1b1e5bd6 100755 --- a/tools/cluster_setup/k0s_cluster_with_stack.sh +++ b/tools/cluster_setup/k0s_cluster_with_stack.sh @@ -780,6 +780,7 @@ Run 'yq eval . ${CONFIG_FILE}' for details, then fix the line and retry." SAIA_DATALOADER_IMAGE="$(yq eval '.images.saia.dataLoaderImage' "$CONFIG_FILE" 2>/dev/null || echo "")" SLIM_API_IMAGE="$(yq eval '.images.slim.apiImage' "$CONFIG_FILE" 2>/dev/null || echo "")" AGENT_RUNTIME_BASE_IMAGE="$(yq eval '.images.agentRuntime.baseImage // ""' "$CONFIG_FILE" 2>/dev/null || echo "")" + AGENT_RUNTIME_SCHEMA_SETUP_IMAGE="$(yq eval '.images.agentRuntime.schemaSetupImage // ""' "$CONFIG_FILE" 2>/dev/null || echo "")" FLUENT_BIT_IMAGE="$(yq eval '.images.fluentBit.image' "$CONFIG_FILE" 2>/dev/null || echo "")" OTEL_COLLECTOR_IMAGE="$(yq eval '.images.otelCollector.image' "$CONFIG_FILE" 2>/dev/null || echo "")" NGINX_IMAGE="$(yq eval '.images.nginx.image' "$CONFIG_FILE" 2>/dev/null || echo "")" @@ -938,6 +939,9 @@ validate_image_config() { if [[ -z "$AGENT_RUNTIME_BASE_IMAGE" || "$AGENT_RUNTIME_BASE_IMAGE" == "null" ]]; then err "REQUIRED: images.agentRuntime.baseImage must be specified in k0s-cluster-config.yaml when the 'agentruntime' feature is enabled" fi + if [[ -z "$AGENT_RUNTIME_SCHEMA_SETUP_IMAGE" || "$AGENT_RUNTIME_SCHEMA_SETUP_IMAGE" == "null" ]]; then + err "REQUIRED: images.agentRuntime.schemaSetupImage must be specified in k0s-cluster-config.yaml when the 'agentruntime' feature is enabled" + fi local agent_feature_count agent_i agent_provider agent_provider_image agent_feature_count=$(yq eval '.aiPlatform.features | length' "${CONFIG_FILE}" 2>/dev/null || echo "0") for ((agent_i=0; agent_i/dev/null || true)" @@ -1188,6 +1195,13 @@ configure_images() { log " ✓ Updated RELATED_IMAGE_AGENT_RUNTIME_BASE: $agent_runtime_base_full" fi + if [[ -n "$AGENT_RUNTIME_SCHEMA_SETUP_IMAGE" && "$AGENT_RUNTIME_SCHEMA_SETUP_IMAGE" != "null" ]]; then + local agent_runtime_schema_setup_full + agent_runtime_schema_setup_full=$(build_image_url "$IMAGE_REGISTRY" "$AGENT_RUNTIME_SCHEMA_SETUP_IMAGE") + upsert_ai_operator_env "RELATED_IMAGE_AGENT_RUNTIME_SCHEMA_SETUP" "$agent_runtime_schema_setup_full" + log " ✓ Updated RELATED_IMAGE_AGENT_RUNTIME_SCHEMA_SETUP: $agent_runtime_schema_setup_full" + fi + local agent_runtime_versions agent_runtime_version agent_runtime_version_image agent_runtime_version_full agent_runtime_env agent_runtime_versions="$(yq eval '.images.agentRuntime.baseImages // {} | keys | .[]' "${CONFIG_FILE}" 2>/dev/null || true)" while IFS= read -r agent_runtime_version; do From 7be7a7147ca6a9775460b2e94086e879c00b9fd1 Mon Sep 17 00:00:00 2001 From: kchoudhary Date: Thu, 17 Sep 2026 16:30:36 +0530 Subject: [PATCH 2/2] fix(agent-runtime): address schema setup review findings --- pkg/ai/features/agentruntime/impl.go | 31 ++++++++++++++++++++- pkg/ai/features/agentruntime/impl_test.go | 16 +++++++++-- tools/cluster_setup/K0S_README.md | 12 ++++---- tools/cluster_setup/k0s-cluster-config.yaml | 1 + 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/pkg/ai/features/agentruntime/impl.go b/pkg/ai/features/agentruntime/impl.go index 00dc90a5..687a8580 100644 --- a/pkg/ai/features/agentruntime/impl.go +++ b/pkg/ai/features/agentruntime/impl.go @@ -49,6 +49,28 @@ const ( schemaJobBackoffLimit int32 = 1 ) +const schemaSetupCommand = `set -eu +export PGSSLMODE="${PGSSLMODE:-${PG_SSLMODE:-}}" + +if [ -n "${PG_HOST:-}" ] && [ -n "${PG_USER:-}" ] && [ -n "${PG_DBNAME:-}" ] && [ -n "${PG_PASSWORD:-}" ]; then + until pg_isready -h "$PG_HOST" -p "${PG_PORT:-5432}" -U "$PG_USER" -d "$PG_DBNAME"; do + echo 'waiting for postgres...' + sleep 2 + done + PGPASSWORD="$PG_PASSWORD" psql -h "$PG_HOST" -p "${PG_PORT:-5432}" -U "$PG_USER" -d "$PG_DBNAME" -v ON_ERROR_STOP=1 -f /schema.sql +elif [ -n "${DATABASE_URL:-}" ]; then + until pg_isready -d "$DATABASE_URL"; do + echo 'waiting for postgres...' + sleep 2 + done + psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f /schema.sql +else + echo 'schema setup requires either PG_HOST/PG_USER/PG_DBNAME/PG_PASSWORD or DATABASE_URL' >&2 + exit 1 +fi + +echo 'Schema setup complete.'` + var agentModulePattern = regexp.MustCompile( `^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*:` + `[A-Za-z_][A-Za-z0-9_]*$`, @@ -373,6 +395,8 @@ func (r *AgentRuntimeReconciler) reconcilePostgresSchemaSetup(ctx context.Contex Name: "schema-setup", Image: schemaImage, ImagePullPolicy: corev1.PullIfNotPresent, + Command: []string{"sh", "-c"}, + Args: []string{schemaSetupCommand}, Env: featureEnv, EnvFrom: []corev1.EnvFromSource{{ SecretRef: &corev1.SecretEnvSource{ @@ -397,6 +421,9 @@ func (r *AgentRuntimeReconciler) reconcilePostgresSchemaSetup(ctx context.Contex } func validateSchemaSetupCredentials(secret *corev1.Secret, featureEnv map[string]string) error { + if strings.TrimSpace(featureEnv["DATABASE_URL"]) != "" || len(secret.Data["DATABASE_URL"]) > 0 { + return nil + } for _, name := range []string{"PG_HOST", "PG_USER", "PG_DBNAME", "PG_PASSWORD"} { if strings.TrimSpace(featureEnv[name]) == "" && len(secret.Data[name]) == 0 { return fmt.Errorf("postgres schema setup requires %s in checkpoint DB Secret or agentruntime feature env", name) @@ -514,7 +541,9 @@ func schemaJobNeedsRerun(job *batchv1.Job, deploymentHash, inputHash string) boo if job.Annotations[schemaJobManagedAnnotation] != schemaJobManagedValue { return false } - return job.Annotations[schemaJobDeploymentHashAnnotation] != deploymentHash && + // A change to either the desired AgentRuntime deployment or schema input + // invalidates the completed schema preparation result. + return job.Annotations[schemaJobDeploymentHashAnnotation] != deploymentHash || job.Annotations[schemaJobInputHashAnnotation] != inputHash } diff --git a/pkg/ai/features/agentruntime/impl_test.go b/pkg/ai/features/agentruntime/impl_test.go index 5ccdb1b6..90eed4c3 100644 --- a/pkg/ai/features/agentruntime/impl_test.go +++ b/pkg/ai/features/agentruntime/impl_test.go @@ -80,6 +80,8 @@ func TestAgentRuntimeFactoryHandler_ReconcilesLifecycle(t *testing.T) { require.NoError(t, fakeClient.Get(context.Background(), types.NamespacedName{ Name: ai.Status.SchemaJobId, Namespace: ai.Namespace, }, job)) + assert.Equal(t, []string{"sh", "-c"}, job.Spec.Template.Spec.Containers[0].Command) + assert.Contains(t, job.Spec.Template.Spec.Containers[0].Args[0], "DATABASE_URL") assert.Error(t, fakeClient.Get(context.Background(), types.NamespacedName{ Name: ai.Name + "-agentruntime-deployment", Namespace: ai.Namespace, }, &appsv1.Deployment{})) @@ -571,16 +573,17 @@ func assertEnv(t *testing.T, env []corev1.EnvVar, name, value string) { t.Fatalf("missing env %s", name) } -func TestSchemaJobNeedsRerunRequiresBothFingerprintsToChange(t *testing.T) { +func TestSchemaJobNeedsRerunWhenEitherFingerprintChanges(t *testing.T) { job := &batchv1.Job{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{ schemaJobManagedAnnotation: schemaJobManagedValue, schemaJobDeploymentHashAnnotation: "deployment-old", schemaJobInputHashAnnotation: "input-old", }}} - assert.False(t, schemaJobNeedsRerun(job, "deployment-new", "input-old")) - assert.False(t, schemaJobNeedsRerun(job, "deployment-old", "input-new")) + assert.True(t, schemaJobNeedsRerun(job, "deployment-new", "input-old")) + assert.True(t, schemaJobNeedsRerun(job, "deployment-old", "input-new")) assert.True(t, schemaJobNeedsRerun(job, "deployment-new", "input-new")) + assert.False(t, schemaJobNeedsRerun(job, "deployment-old", "input-old")) } func TestValidateSchemaSetupCredentialsAllowsFeatureEnvFallback(t *testing.T) { @@ -593,6 +596,13 @@ func TestValidateSchemaSetupCredentialsAllowsFeatureEnvFallback(t *testing.T) { } require.NoError(t, validateSchemaSetupCredentials(secret, featureEnv)) + require.NoError(t, validateSchemaSetupCredentials(secret, nil)) + require.NoError(t, validateSchemaSetupCredentials(&corev1.Secret{Data: map[string][]byte{ + "PG_HOST": []byte("postgres"), + "PG_USER": []byte("user"), + "PG_PASSWORD": []byte("password"), + "PG_DBNAME": []byte("checkpoints"), + }}, nil)) } func TestAgentRuntimeReconcileCertificate_UsesCommonReconciler(t *testing.T) { diff --git a/tools/cluster_setup/K0S_README.md b/tools/cluster_setup/K0S_README.md index fadee056..e46c0e84 100644 --- a/tools/cluster_setup/K0S_README.md +++ b/tools/cluster_setup/K0S_README.md @@ -498,7 +498,7 @@ Short image paths (without a FQDN) are automatically prefixed with `images.regis | `aiPlatform.features[].maxReplicas` | No | operator default | Maximum feature replicas | | `aiPlatform.features[].targetCPUUtilization` | No | operator default | HPA target CPU utilization | | `aiPlatform.features[].checkpointDbSecretRef` | Agent Runtime | — | Secret name containing checkpoint DB connection details | -| `aiPlatform.features[].env` | Agent Runtime | — | Optional runtime environment overrides; the schema image requires `PG_HOST`, `PG_USER`, `PG_PASSWORD`, and `PG_DBNAME` (with optional `PG_PORT`) from this map or the referenced Secret | +| `aiPlatform.features[].env` | Agent Runtime | — | Optional runtime environment overrides; schema setup accepts either `DATABASE_URL` or `PG_HOST`, `PG_USER`, `PG_PASSWORD`, and `PG_DBNAME` (with optional `PG_PORT`) from this map or the referenced Secret | | `aiPlatform.features[].serviceAccountName` | No | `""` | Service account override | | `aiPlatform.cpuScheduling.nodeSelector` | No | auto-generated | Node selector for CPU workloads | | `aiPlatform.cpuScheduling.tolerations` | No | `[]` | Tolerations for CPU workloads | @@ -510,11 +510,11 @@ Short image paths (without a FQDN) are automatically prefixed with `images.regis Agent Runtime schema preparation runs as an AIService-owned Job before the AgentRuntime Deployment is created. The setup image is built from the AgentRuntime repository's `cicd/docker/schema-setup/Dockerfile` and must contain -the compatible `schema.sql`. `DATABASE_URL` alone is not consumed by that image; -use the structured `PG_*` keys in the referenced Secret or AgentRuntime feature -environment. The operator also maps `PG_SSLMODE` to libpq's `PGSSLMODE` for the -schema Job. SAIA does not require these keys and its existing data-loader flow -is unchanged. +the compatible `schema.sql`. The Job accepts either a `DATABASE_URL` in the +referenced Secret or structured `PG_*` keys from the Secret/AgentRuntime feature +environment, with structured keys taking precedence when both are present. The +operator also maps `PG_SSLMODE` to libpq's `PGSSLMODE` for the schema Job. SAIA +does not require these keys and its existing data-loader flow is unchanged. #### Optional Component Gates diff --git a/tools/cluster_setup/k0s-cluster-config.yaml b/tools/cluster_setup/k0s-cluster-config.yaml index d1ebfe2b..1467c6d9 100644 --- a/tools/cluster_setup/k0s-cluster-config.yaml +++ b/tools/cluster_setup/k0s-cluster-config.yaml @@ -144,6 +144,7 @@ images: # a pinned base image; providerImages are keyed by feature.provider. agentRuntime: baseImage: "docker.io/splunk/agent-runtime:latest" + schemaSetupImage: "docker.io/splunk/agent-runtime-schema-setup:latest" baseImages: # v2.0.0: "docker.io/splunk/agent-runtime:v2.0.0" providerImages: