Skip to content
Open
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
75 changes: 51 additions & 24 deletions components/operator/internal/trigger/trigger.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import (
"strings"
"time"

"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/util/retry"

"ambient-code-operator/internal/config"
"ambient-code-operator/internal/types"
)

Expand All @@ -33,39 +35,28 @@ func RunSessionTrigger() {
log.Fatalf("Required environment variables SESSION_TEMPLATE, PROJECT_NAMESPACE, and SCHEDULED_SESSION_NAME must be set")
}

// Init K8s client
cfg, err := rest.InClusterConfig()
if err != nil {
log.Fatalf("Failed to get in-cluster config: %v", err)
}

dynamicClient, err := dynamic.NewForConfig(cfg)
if err != nil {
log.Fatalf("Failed to create dynamic client: %v", err)
if err := config.InitK8sClients(); err != nil {
log.Fatalf("Failed to initialize Kubernetes clients: %v", err)
}

// Parse session template
var template map[string]interface{}
if err := json.Unmarshal([]byte(sessionTemplate), &template); err != nil {
log.Fatalf("Failed to parse SESSION_TEMPLATE JSON: %v", err)
}

// Check if reuse mode is enabled
reuseLastSession := strings.EqualFold(strings.TrimSpace(os.Getenv("REUSE_LAST_SESSION")), "true")

if reuseLastSession {
reused, err := tryReuseLastSession(dynamicClient, projectNamespace, scheduledSessionName, template)
reused, err := tryReuseLastSession(config.DynamicClient, projectNamespace, scheduledSessionName, template)
if err != nil {
// Don't fall through to create — the reuse may have partially succeeded
log.Fatalf("Failed to reuse last session for %s: %v", scheduledSessionName, err)
}
if reused {
return
}
// No reusable session found — fall through to create a new one
}

createNewSession(dynamicClient, projectNamespace, scheduledSessionName, template)
createNewSession(config.DynamicClient, projectNamespace, scheduledSessionName, template)
}

// tryReuseLastSession finds the most recent session for this scheduled session and either
Expand Down Expand Up @@ -244,19 +235,56 @@ func resumeSessionWithPrompt(dynamicClient dynamic.Interface, namespace, session
})
}

// createNewSession creates a new AgenticSession CR (original behavior).
// applyFeatureFlagOverrides reads workspace feature flag overrides from ConfigMap
// and applies them to the session template. Non-fatal: logs warnings but continues
// if ConfigMap is missing or read fails (degraded operation acceptable).
func applyFeatureFlagOverrides(ctx context.Context, k8sClient kubernetes.Interface, namespace string, template map[string]interface{}) error {
cm, err := k8sClient.CoreV1().ConfigMaps(namespace).Get(ctx, types.FeatureFlagOverridesConfigMap, metav1.GetOptions{})
if errors.IsNotFound(err) {
return nil
}
if err != nil {
log.Printf("WARNING: failed to read feature flag overrides for namespace %s: %v", namespace, err)
return nil
}

val, exists := cm.Data[types.JiraWriteFlagKey]
if !exists || val != "true" {
return nil
}

spec, ok := template["spec"].(map[string]interface{})
if !ok {
spec = map[string]interface{}{}
template["spec"] = spec
}

envVars, ok := spec["environmentVariables"].(map[string]interface{})
if !ok {
envVars = map[string]interface{}{}
}

envVars[types.JiraReadOnlyModeEnvVar] = "false"
spec["environmentVariables"] = envVars

log.Printf("Applied jira-write feature flag: %s=false", types.JiraReadOnlyModeEnvVar)
return nil
}

func createNewSession(dynamicClient dynamic.Interface, namespace, scheduledSessionName string, template map[string]interface{}) {
// Build session name and display name.
// The most restrictive derived K8s resource name is the Service:
// "session-" (8 chars) + sessionName ≤ 63 → sessionName ≤ 55
// sanitizeName caps at 40 chars, so namePrefix + "-" + timestamp (10)
// yields at most 51 chars — well within the 55-char budget.
if config.K8sClient != nil {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := applyFeatureFlagOverrides(ctx, config.K8sClient, namespace, template); err != nil {
log.Printf("WARNING: failed to apply feature flag overrides: %v", err)
}
}

now := time.Now()
ts := strconv.FormatInt(now.Unix(), 10)
namePrefix := sanitizeName(scheduledSessionName)
if dn, ok := template["displayName"].(string); ok && dn != "" {
namePrefix = sanitizeName(dn)
// Set display name with human-readable timestamp, e.g. "Daily Jira Summary (Jan 1, 2026 - 00:00:00)"
template["displayName"] = fmt.Sprintf("%s (%s)", dn, now.UTC().Format("Jan 2, 2006 - 15:04:05"))
}
sessionName := fmt.Sprintf("%s-%s", namePrefix, ts)
Expand All @@ -277,7 +305,6 @@ func createNewSession(dynamicClient dynamic.Interface, namespace, scheduledSessi
},
}

// Create via dynamic client
gvr := types.GetAgenticSessionResource()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
Expand Down
124 changes: 124 additions & 0 deletions components/operator/internal/trigger/trigger_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package trigger

import (
"context"
"testing"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"

"ambient-code-operator/internal/types"
)

func TestSanitizeName(t *testing.T) {
Expand Down Expand Up @@ -104,3 +111,120 @@ func TestSanitizeName_TruncationPreservesValidSuffix(t *testing.T) {
t.Errorf("sanitizeName(%q) ends with hyphen: %q", input, result)
}
}

func TestApplyFeatureFlagOverrides(t *testing.T) {
tests := []struct {
name string
configMapData map[string]string
existingEnvVars map[string]interface{}
expectedEnvVars map[string]interface{}
}{
{
name: "jira-write enabled sets JIRA_READ_ONLY_MODE to false",
configMapData: map[string]string{"jira-write": "true"},
existingEnvVars: nil,
expectedEnvVars: map[string]interface{}{"JIRA_READ_ONLY_MODE": "false"},
},
{
name: "jira-write disabled does not set env var",
configMapData: map[string]string{"jira-write": "false"},
existingEnvVars: nil,
expectedEnvVars: nil,
},
{
name: "no overrides ConfigMap does not set env var",
configMapData: nil,
existingEnvVars: nil,
expectedEnvVars: nil,
},
{
name: "preserves existing env vars",
configMapData: map[string]string{"jira-write": "true"},
existingEnvVars: map[string]interface{}{"CUSTOM_VAR": "value"},
expectedEnvVars: map[string]interface{}{
"CUSTOM_VAR": "value",
"JIRA_READ_ONLY_MODE": "false",
},
},
{
name: "other flags do not affect env vars",
configMapData: map[string]string{"other-flag": "true"},
existingEnvVars: nil,
expectedEnvVars: nil,
},
{
name: "jira-write with non-true value does not set env var",
configMapData: map[string]string{"jira-write": "yes"},
existingEnvVars: nil,
expectedEnvVars: nil,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create fake K8s client
var k8sClient *fake.Clientset
if tt.configMapData != nil {
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: types.FeatureFlagOverridesConfigMap,
Namespace: "test-namespace",
},
Data: tt.configMapData,
}
k8sClient = fake.NewSimpleClientset(configMap)
} else {
k8sClient = fake.NewSimpleClientset()
}

// Create template with optional existing env vars
template := map[string]interface{}{
"spec": map[string]interface{}{},
}
if tt.existingEnvVars != nil {
spec := template["spec"].(map[string]interface{})
spec["environmentVariables"] = tt.existingEnvVars
}
Comment on lines +181 to +187
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Test fixture uses a nested spec shape that does not match runtime input.

applyFeatureFlagOverrides is called with the session spec map in production, but this test passes {"spec": {...}} and asserts inside template["spec"]. That can mask real regressions and currently validates the wrong contract.

Suggested fix
-			template := map[string]interface{}{
-				"spec": map[string]interface{}{},
-			}
+			template := map[string]interface{}{}
 			if tt.existingEnvVars != nil {
-				spec := template["spec"].(map[string]interface{})
-				spec["environmentVariables"] = tt.existingEnvVars
+				template["environmentVariables"] = tt.existingEnvVars
 			}
@@
-			spec, ok := template["spec"].(map[string]interface{})
-			if !ok {
-				t.Fatal("template[spec] is not a map")
-			}
-
-			envVars, ok := spec["environmentVariables"].(map[string]interface{})
+			envVars, ok := template["environmentVariables"].(map[string]interface{})
 			if tt.expectedEnvVars == nil {

Also applies to: 195-210

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/operator/internal/trigger/trigger_test.go` around lines 179 - 185,
The test fixture nests the session spec under "spec" which doesn't match the
real runtime input consumed by applyFeatureFlagOverrides; update the test to
pass the spec map directly (not wrapped as {"spec": ...}) and adjust assertions
to inspect top-level keys (e.g., check environmentVariables on the map passed to
applyFeatureFlagOverrides), replacing usages of template["spec"] and any other
occurrences in this file (also update the similar block around the other failing
assertions) so the test exercises the same contract as
applyFeatureFlagOverrides.


// Apply feature flag overrides
ctx := context.Background()
err := applyFeatureFlagOverrides(ctx, k8sClient, "test-namespace", template)
if err != nil {
t.Fatalf("applyFeatureFlagOverrides() unexpected error: %v", err)
}

// Verify environment variables
spec, ok := template["spec"].(map[string]interface{})
if !ok {
t.Fatal("template[spec] is not a map")
}

envVars, ok := spec["environmentVariables"].(map[string]interface{})
if tt.expectedEnvVars == nil {
if envVars != nil && len(envVars) > 0 {
t.Errorf("Expected no environmentVariables, got %v", envVars)
}
return
}

if !ok {
t.Fatal("spec[environmentVariables] is not a map")
}

if len(envVars) != len(tt.expectedEnvVars) {
t.Errorf("environmentVariables count = %d, want %d", len(envVars), len(tt.expectedEnvVars))
}

for key, expectedVal := range tt.expectedEnvVars {
actualVal, exists := envVars[key]
if !exists {
t.Errorf("environmentVariables[%q] missing, want %q", key, expectedVal)
continue
}
if actualVal != expectedVal {
t.Errorf("environmentVariables[%q] = %q, want %q", key, actualVal, expectedVal)
}
}
})
}
}
9 changes: 9 additions & 0 deletions components/operator/internal/types/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ const (
// manually on private-CA clusters. See issue #1247.
TrustedCABundleConfigMapName = "trusted-ca-bundle"

// FeatureFlagOverridesConfigMap is the ConfigMap containing workspace feature flag overrides
FeatureFlagOverridesConfigMap = "feature-flag-overrides"

// JiraWriteFlagKey enables write operations for Jira MCP tools
JiraWriteFlagKey = "jira-write"

// JiraReadOnlyModeEnvVar controls read-only mode for Jira integration
JiraReadOnlyModeEnvVar = "JIRA_READ_ONLY_MODE"

// CopiedFromAnnotation is the annotation key used to track secrets copied by the operator
CopiedFromAnnotation = "vteam.ambient-code/copied-from"
)
Expand Down