| 218 | } |
| 219 | |
| 220 | func (wb *WatcherBridge) createAnomaly(ctx context.Context, alert *pb.WatcherAlert) error { |
| 221 | signalType := MapAlertTypeToSignal(alert.Type) |
| 222 | ns := alert.Namespace |
| 223 | if ns == "" { |
| 224 | ns = "default" |
| 225 | } |
| 226 | |
| 227 | name := fmt.Sprintf("watcher-%s-%s-%d", strings.ToLower(alert.Type), alert.Deployment, alert.TimestampUnix) |
| 228 | // Sanitize name for K8s (lowercase, max 63 chars, no invalid chars) |
| 229 | name = sanitizeK8sName(name) |
| 230 | |
| 231 | labels := map[string]string{ |
| 232 | "platform.chatcli.io/source": "watcher", |
| 233 | "platform.chatcli.io/deployment": alert.Deployment, |
| 234 | } |
| 235 | // Link to the Instance that produced this anomaly (cross-namespace, so labels not OwnerRef) |
| 236 | if wb.connectedInstance != nil { |
| 237 | labels["platform.chatcli.io/instance"] = wb.connectedInstance.Name |
| 238 | labels["platform.chatcli.io/instance-namespace"] = wb.connectedInstance.Namespace |
| 239 | } |
| 240 | |
| 241 | anomaly := &platformv1alpha1.Anomaly{ |
| 242 | ObjectMeta: metav1.ObjectMeta{ |
| 243 | Name: name, |
| 244 | Namespace: ns, |
| 245 | Labels: labels, |
| 246 | }, |
| 247 | Spec: platformv1alpha1.AnomalySpec{ |
| 248 | Source: platformv1alpha1.AnomalySourceWatcher, |
| 249 | SignalType: signalType, |
| 250 | Resource: platformv1alpha1.ResourceRef{ |
| 251 | Kind: inferResourceKind(alert), |
| 252 | Name: alert.Deployment, |
| 253 | Namespace: ns, |
| 254 | }, |
| 255 | Value: alert.Message, |
| 256 | Threshold: "normal", |
| 257 | Description: alert.Message, |
| 258 | }, |
| 259 | } |
| 260 | |
| 261 | if err := wb.client.Create(ctx, anomaly); err != nil { |
| 262 | // Same (type, deployment, namespace, timestamp) produces a deterministic name. |
| 263 | // If the CR already exists (operator restart wiped the in-memory dedup map, |
| 264 | // or the server re-emits a still-active alert), treat it as a successful no-op |
| 265 | // so the caller marks the hash as seen and stops re-trying each poll. |
| 266 | if errors.IsAlreadyExists(err) { |
| 267 | wb.logger.Debug("Anomaly CR already exists, treating as idempotent success", |
| 268 | zap.String("name", name), |
| 269 | zap.String("signal", string(signalType)), |
| 270 | zap.String("deployment", alert.Deployment)) |
| 271 | return nil |
| 272 | } |
| 273 | return fmt.Errorf("creating anomaly %s: %w", name, err) |
| 274 | } |
| 275 | |
| 276 | wb.logger.Info("Created Anomaly CR", |
| 277 | zap.String("name", name), |