From 7fe797924ac07f2db90b899fea7553688b41a5c8 Mon Sep 17 00:00:00 2001 From: Jagat Thakkar Date: Fri, 14 Aug 2026 16:50:02 -0500 Subject: [PATCH 1/3] feat: Adding Notifications Configuration --- api/v2/weightsandbiases_types.go | 27 +++ api/v2/zz_generated.deepcopy.go | 96 ++++++++++ .../apps.wandb.com_weightsandbiases.yaml | 121 ++++++++++++ .../0.83.0-clickhouse-keeper.1/manifest.yaml | 10 + .../0.83.0-clickhouse-keeper.2/manifest.yaml | 10 + internal/controller/reconciler/email.go | 169 +++++++++++++++++ internal/controller/reconciler/email_test.go | 173 ++++++++++++++++++ .../controller/reconciler/reconcile_v2.go | 4 + .../controller/reconciler/slack_env_test.go | 54 ++++++ .../apps.wandb.com_weightsandbiases.yaml | 121 ++++++++++++ .../v2/weightsandbiases_notifications_test.go | 87 +++++++++ .../webhook/v2/weightsandbiases_webhook.go | 43 ++++- 12 files changed, 914 insertions(+), 1 deletion(-) create mode 100644 internal/controller/reconciler/email.go create mode 100644 internal/controller/reconciler/email_test.go create mode 100644 internal/controller/reconciler/slack_env_test.go create mode 100644 internal/webhook/v2/weightsandbiases_notifications_test.go diff --git a/api/v2/weightsandbiases_types.go b/api/v2/weightsandbiases_types.go index 1c898285..f0811dc3 100644 --- a/api/v2/weightsandbiases_types.go +++ b/api/v2/weightsandbiases_types.go @@ -381,6 +381,10 @@ type WandbAppSpec struct { // +optional OIDC OidcSpec `json:"oidc,omitempty"` + // Notification Configurations + // +optional + Notifications *NotificationsSpec `json:"notifications,omitempty"` + // LegacyOverrides holds env/resource overrides extracted from v1 // spec.values, keyed by manifest application name plus the reserved // "global" key (env only, applied to every application). Unknown keys are @@ -482,6 +486,28 @@ type OidcSpec struct { SessionLength string `json:"sessionLength,omitempty"` } +type NotificationsSpec struct { + Email *EmailSpec `json:"email,omitempty"` + Slack *SlackSpec `json:"slack,omitempty"` +} + +type EmailSMTPSpec struct { + Host corev1.SecretKeySelector `json:"host"` + Port corev1.SecretKeySelector `json:"port"` + Username corev1.SecretKeySelector `json:"username"` + Password corev1.SecretKeySelector `json:"password"` +} + +type EmailSpec struct { + Sink *corev1.SecretKeySelector `json:"sink,omitempty"` + SMTP *EmailSMTPSpec `json:"smtp,omitempty"` +} + +type SlackSpec struct { + ClientID corev1.SecretKeySelector `json:"clientId,omitempty"` + ClientSecret corev1.SecretKeySelector `json:"clientSecret,omitempty"` +} + type ManagedInfraSpec struct { RetentionPolicy *RetentionPolicy `json:"retentionPolicy,omitempty"` @@ -771,6 +797,7 @@ type WeightsAndBiasesStatus struct { ObjectStoreStatus map[string]ObjectStoreInfraStatus `json:"objectStoreStatus,omitempty"` ClickHouseStatus map[string]ClickHouseInfraStatus `json:"clickhouseStatus,omitempty"` TelemetryStatus TelemetryInfraStatus `json:"telemetryStatus,omitempty"` + EmailSink *corev1.SecretKeySelector `json:"emailSink,omitempty"` // GeneratedSecrets stores references to secrets generated by the operator // from the server manifest's generatedSecrets section. The key is the // logical secret name from the manifest, and the value is a SecretKeySelector diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index 9442d0db..ddb22263 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -356,6 +356,50 @@ func (in *ClickHouseSpec) DeepCopy() *ClickHouseSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EmailSMTPSpec) DeepCopyInto(out *EmailSMTPSpec) { + *out = *in + in.Host.DeepCopyInto(&out.Host) + in.Port.DeepCopyInto(&out.Port) + in.Username.DeepCopyInto(&out.Username) + in.Password.DeepCopyInto(&out.Password) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EmailSMTPSpec. +func (in *EmailSMTPSpec) DeepCopy() *EmailSMTPSpec { + if in == nil { + return nil + } + out := new(EmailSMTPSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EmailSpec) DeepCopyInto(out *EmailSpec) { + *out = *in + if in.Sink != nil { + in, out := &in.Sink, &out.Sink + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + if in.SMTP != nil { + in, out := &in.SMTP, &out.SMTP + *out = new(EmailSMTPSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EmailSpec. +func (in *EmailSpec) DeepCopy() *EmailSpec { + if in == nil { + return nil + } + out := new(EmailSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GatewayAPIConfig) DeepCopyInto(out *GatewayAPIConfig) { *out = *in @@ -1060,6 +1104,31 @@ func (in *NetworkingSpec) DeepCopy() *NetworkingSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NotificationsSpec) DeepCopyInto(out *NotificationsSpec) { + *out = *in + if in.Email != nil { + in, out := &in.Email, &out.Email + *out = new(EmailSpec) + (*in).DeepCopyInto(*out) + } + if in.Slack != nil { + in, out := &in.Slack, &out.Slack + *out = new(SlackSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NotificationsSpec. +func (in *NotificationsSpec) DeepCopy() *NotificationsSpec { + if in == nil { + return nil + } + out := new(NotificationsSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ObjectStoreConfig) DeepCopyInto(out *ObjectStoreConfig) { *out = *in @@ -1416,6 +1485,23 @@ func (in *ServiceAccountSpec) DeepCopy() *ServiceAccountSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SlackSpec) DeepCopyInto(out *SlackSpec) { + *out = *in + in.ClientID.DeepCopyInto(&out.ClientID) + in.ClientSecret.DeepCopyInto(&out.ClientSecret) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SlackSpec. +func (in *SlackSpec) DeepCopy() *SlackSpec { + if in == nil { + return nil + } + out := new(SlackSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TLSConfig) DeepCopyInto(out *TLSConfig) { *out = *in @@ -1524,6 +1610,11 @@ func (in *WandbAppSpec) DeepCopyInto(out *WandbAppSpec) { copy(*out, *in) } in.OIDC.DeepCopyInto(&out.OIDC) + if in.Notifications != nil { + in, out := &in.Notifications, &out.Notifications + *out = new(NotificationsSpec) + (*in).DeepCopyInto(*out) + } if in.LegacyOverrides != nil { in, out := &in.LegacyOverrides, &out.LegacyOverrides *out = make(map[string]LegacyOverrides, len(*in)) @@ -1789,6 +1880,11 @@ func (in *WeightsAndBiasesStatus) DeepCopyInto(out *WeightsAndBiasesStatus) { } } in.TelemetryStatus.DeepCopyInto(&out.TelemetryStatus) + if in.EmailSink != nil { + in, out := &in.EmailSink, &out.EmailSink + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } if in.GeneratedSecrets != nil { in, out := &in.GeneratedSecrets, &out.GeneratedSecrets *out = make(map[string]v1.SecretKeySelector, len(*in)) diff --git a/config/crd/bases/apps.wandb.com_weightsandbiases.yaml b/config/crd/bases/apps.wandb.com_weightsandbiases.yaml index 921d192e..1c42296d 100644 --- a/config/crd/bases/apps.wandb.com_weightsandbiases.yaml +++ b/config/crd/bases/apps.wandb.com_weightsandbiases.yaml @@ -4088,6 +4088,114 @@ spec: type: string manifestRepository: type: string + notifications: + properties: + email: + properties: + sink: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + smtp: + properties: + host: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + password: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + port: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + username: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + required: + - host + - password + - port + - username + type: object + type: object + slack: + properties: + clientId: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + clientSecret: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + type: object oidc: properties: authMethod: @@ -4599,6 +4707,19 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map + emailSink: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic gatewayStatus: properties: addresses: diff --git a/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/manifest.yaml b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/manifest.yaml index 9bbdaf76..0d7e56a3 100644 --- a/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/manifest.yaml +++ b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/manifest.yaml @@ -313,6 +313,16 @@ commonEnvvars: type: custom-resource field: spec.wandb.oidc.sessionLength defaultValue: "720h" + - name: GORILLA_SLACK_CLIENT_ID + sources: + - name: slack + type: custom-resource + field: spec.wandb.notifications.slack.clientId + - name: GORILLA_SLACK_SECRET + sources: + - name: slack + type: custom-resource + field: spec.wandb.notifications.slack.clientSecret gorillaCustomerSecrets: - name: GORILLA_CUSTOMER_SECRET_STORE_SOURCE value: "k8s-secretmanager://" diff --git a/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/manifest.yaml b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/manifest.yaml index 9bbdaf76..0d7e56a3 100644 --- a/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/manifest.yaml +++ b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/manifest.yaml @@ -313,6 +313,16 @@ commonEnvvars: type: custom-resource field: spec.wandb.oidc.sessionLength defaultValue: "720h" + - name: GORILLA_SLACK_CLIENT_ID + sources: + - name: slack + type: custom-resource + field: spec.wandb.notifications.slack.clientId + - name: GORILLA_SLACK_SECRET + sources: + - name: slack + type: custom-resource + field: spec.wandb.notifications.slack.clientSecret gorillaCustomerSecrets: - name: GORILLA_CUSTOMER_SECRET_STORE_SOURCE value: "k8s-secretmanager://" diff --git a/internal/controller/reconciler/email.go b/internal/controller/reconciler/email.go new file mode 100644 index 00000000..5675ebdc --- /dev/null +++ b/internal/controller/reconciler/email.go @@ -0,0 +1,169 @@ +package reconciler + +import ( + "context" + "fmt" + "net" + "net/url" + "strings" + + apiv2 "github.com/wandb/operator/api/v2" + "github.com/wandb/operator/internal/controller/common" + "github.com/wandb/operator/internal/controller/infra/external" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + emailSinkSecretSuffix = "email-sink" + emailSinkSecretKey = "sink" +) + +func emailSinkSecretName(wandb *apiv2.WeightsAndBiases) string { + return fmt.Sprintf("%s-%s", wandb.Name, emailSinkSecretSuffix) +} + +// reconcileEmailSink resolves the configured email mode into the single SecretKeySelector +func reconcileEmailSink( + ctx context.Context, + client ctrlClient.Client, + wandb *apiv2.WeightsAndBiases, +) error { + var emailSpec *apiv2.EmailSpec + if wandb.Spec.Wandb.Notifications != nil { + emailSpec = wandb.Spec.Wandb.Notifications.Email + } + + // No email configured, clean up the generated sink + if emailSpec == nil { + wandb.Status.EmailSink = nil + return deleteGeneratedEmailSink(ctx, client, wandb) + } + + // Use the sink given by the user + if emailSpec.Sink != nil { + wandb.Status.EmailSink = emailSpec.Sink.DeepCopy() + // The sink is already present under the same name, no need for changes + if emailSpec.Sink.Name == emailSinkSecretName(wandb) { + return nil + } + return deleteGeneratedEmailSink(ctx, client, wandb) + } + + // email spec exists, but neither sink nor smtp was supplied + if emailSpec.SMTP == nil { + wandb.Status.EmailSink = nil + return deleteGeneratedEmailSink(ctx, client, wandb) + } + + // Read the SMTP Secret values and build the sink URL + sink, err := resolveSMTPURL(ctx, client, wandb.Namespace, emailSpec.SMTP) + if err != nil { + return fmt.Errorf("resolve SMTP configuration: %w", err) + } + + secretName := emailSinkSecretName(wandb) + // Creates a new desired secret under the same namespace as the wandb application and under the `sink` key + // belonging to W&B + newSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: wandb.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "wandb-operator", + "app.kubernetes.io/instance": wandb.Name, + "app.kubernetes.io/part-of": "wandb", + }, + }, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{emailSinkSecretKey: sink}, + } + if err := controllerutil.SetControllerReference(wandb, newSecret, client.Scheme()); err != nil { + return fmt.Errorf("set email sink Secret owner: %w", err) + } + + actual := &corev1.Secret{} + err = client.Get(ctx, types.NamespacedName{Name: secretName, Namespace: wandb.Namespace}, actual) + if err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("get email sink Secret: %w", err) + } + if apierrors.IsNotFound(err) { + actual = nil + } else if !hasOwnerReference(actual, wandb) { + return fmt.Errorf("email sink Secret %s/%s already exists and is not owned by %s", wandb.Namespace, secretName, wandb.Name) + } + + if _, err := common.CrudResource(ctx, client, newSecret, actual); err != nil { + return fmt.Errorf("write email sink Secret: %w", err) + } + + wandb.Status.EmailSink = &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, + Key: emailSinkSecretKey, + } + return nil +} + +// resolveSMTPURL generates the URL from the EmailSMTPSpec +func resolveSMTPURL( + ctx context.Context, + client ctrlClient.Client, + namespace string, + smtp *apiv2.EmailSMTPSpec, +) (string, error) { + + host, err := external.ResolveSecretKey(ctx, client, namespace, smtp.Host) + if err != nil { + return "", fmt.Errorf("host: %w", err) + } + port, err := external.ResolveSecretKey(ctx, client, namespace, smtp.Port) + if err != nil { + return "", fmt.Errorf("port: %w", err) + } + + username, err := external.ResolveSecretKey(ctx, client, namespace, smtp.Username) + if err != nil { + return "", fmt.Errorf("username: %w", err) + } + password, err := external.ResolveSecretKey(ctx, client, namespace, smtp.Password) + if err != nil { + return "", fmt.Errorf("password: %w", err) + } + + host = strings.TrimSpace(host) + port = strings.TrimSpace(port) + if host == "" || port == "" { + return "", fmt.Errorf("host and port must not be empty") + } + // net.JoinHostPort expects an IPv6 host without surrounding brackets. + host = strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[") + sink := &url.URL{Scheme: "smtp", Host: net.JoinHostPort(host, port)} + sink.User = url.UserPassword(username, password) + return sink.String(), nil +} + +// Delete the generated email sink if it belongs to this W&B resource +func deleteGeneratedEmailSink(ctx context.Context, client ctrlClient.Client, wandb *apiv2.WeightsAndBiases) error { + secret := &corev1.Secret{} + err := client.Get(ctx, types.NamespacedName{ + Name: emailSinkSecretName(wandb), + Namespace: wandb.Namespace, + }, secret) + if apierrors.IsNotFound(err) { + return nil + } + if err != nil { + return fmt.Errorf("get generated email sink Secret: %w", err) + } + if !hasOwnerReference(secret, wandb) { + return nil + } + if err := client.Delete(ctx, secret); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("delete generated email sink Secret: %w", err) + } + return nil +} diff --git a/internal/controller/reconciler/email_test.go b/internal/controller/reconciler/email_test.go new file mode 100644 index 00000000..409cc408 --- /dev/null +++ b/internal/controller/reconciler/email_test.go @@ -0,0 +1,173 @@ +package reconciler + +import ( + "context" + "testing" + + apiv2 "github.com/wandb/operator/api/v2" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func emailTestSelector(name, key string) corev1.SecretKeySelector { + return corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + Key: key, + } +} + +func emailTestWandb(t *testing.T) (*runtime.Scheme, *apiv2.WeightsAndBiases) { + t.Helper() + scheme := runtime.NewScheme() + if err := corev1.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + if err := apiv2.AddToScheme(scheme); err != nil { + t.Fatal(err) + } + wandb := &apiv2.WeightsAndBiases{ + TypeMeta: metav1.TypeMeta{APIVersion: apiv2.GroupVersion.String(), Kind: "WeightsAndBiases"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb", + Namespace: "default", + UID: types.UID("wandb-uid"), + }, + } + wandb.Spec.Wandb.Notifications = &apiv2.NotificationsSpec{} + return scheme, wandb +} + +func TestReconcileEmailSinkUsesConfiguredSink(t *testing.T) { + scheme, wandb := emailTestWandb(t) + selector := emailTestSelector("existing-email", "url") + wandb.Spec.Wandb.Notifications.Email = &apiv2.EmailSpec{Sink: &selector} + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(wandb).Build() + + if err := reconcileEmailSink(context.Background(), client, wandb); err != nil { + t.Fatal(err) + } + if wandb.Status.EmailSink == nil || wandb.Status.EmailSink.Name != "existing-email" || wandb.Status.EmailSink.Key != "url" { + t.Fatalf("unexpected effective email sink: %+v", wandb.Status.EmailSink) + } +} + +func TestReconcileEmailSinkDoesNotDeleteConfiguredSink(t *testing.T) { + scheme, wandb := emailTestWandb(t) + selector := emailTestSelector("wandb-email-sink", "sink") + wandb.Spec.Wandb.Notifications.Email = &apiv2.EmailSpec{Sink: &selector} + configured := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: selector.Name, + Namespace: wandb.Namespace, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: apiv2.GroupVersion.String(), + Kind: "WeightsAndBiases", + Name: wandb.Name, + UID: wandb.UID, + }}, + }, + } + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(wandb, configured).Build() + + if err := reconcileEmailSink(context.Background(), client, wandb); err != nil { + t.Fatal(err) + } + if err := client.Get(context.Background(), types.NamespacedName{Name: configured.Name, Namespace: configured.Namespace}, &corev1.Secret{}); err != nil { + t.Fatalf("configured sink was deleted: %v", err) + } +} + +func TestResolveEnvvarsCustomResourceEmailSink(t *testing.T) { + _, wandb := emailTestWandb(t) + selector := emailTestSelector("effective-email", "sink") + wandb.Status.EmailSink = &selector + envs := []serverManifest.EnvVar{{ + Name: "GORILLA_EMAIL_SINK", + Sources: []serverManifest.EnvSource{{ + Type: "custom-resource", Field: "status.emailSink", + }}, + }} + + resolved, err := resolveEnvvars(context.Background(), fake.NewClientBuilder().Build(), wandb, serverManifest.Manifest{}, nil, envs) + if err != nil { + t.Fatal(err) + } + env := mustFindEnvVar(t, resolved, "GORILLA_EMAIL_SINK") + if env.ValueFrom == nil || env.ValueFrom.SecretKeyRef == nil { + t.Fatalf("email sink did not resolve to a SecretKeyRef: %+v", env) + } + if env.ValueFrom.SecretKeyRef.Name != "effective-email" || env.ValueFrom.SecretKeyRef.Key != "sink" { + t.Fatalf("unexpected email sink selector: %+v", env.ValueFrom.SecretKeyRef) + } +} + +func TestReconcileEmailSinkGeneratesAuthenticatedSMTPURL(t *testing.T) { + scheme, wandb := emailTestWandb(t) + username := emailTestSelector("smtp", "username") + password := emailTestSelector("smtp", "password") + wandb.Spec.Wandb.Notifications.Email = &apiv2.EmailSpec{SMTP: &apiv2.EmailSMTPSpec{ + Host: emailTestSelector("smtp", "host"), + Port: emailTestSelector("smtp", "port"), + Username: username, + Password: password, + }} + smtpSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "smtp", Namespace: "default"}, + Data: map[string][]byte{ + "host": []byte("smtp.example.com"), + "port": []byte("587"), + "username": []byte("user@example.com"), + "password": []byte("p@ss/word"), + }, + } + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(wandb, smtpSecret).Build() + + if err := reconcileEmailSink(context.Background(), client, wandb); err != nil { + t.Fatal(err) + } + + generated := &corev1.Secret{} + if err := client.Get(context.Background(), types.NamespacedName{Name: "wandb-email-sink", Namespace: "default"}, generated); err != nil { + t.Fatal(err) + } + value := generated.StringData[emailSinkSecretKey] + if value == "" { + value = string(generated.Data[emailSinkSecretKey]) + } + if want := "smtp://user%40example.com:p%40ss%2Fword@smtp.example.com:587"; value != want { + t.Fatalf("unexpected SMTP URL: got %q want %q", value, want) + } + if wandb.Status.EmailSink == nil || wandb.Status.EmailSink.Name != "wandb-email-sink" || wandb.Status.EmailSink.Key != emailSinkSecretKey { + t.Fatalf("unexpected effective email sink: %+v", wandb.Status.EmailSink) + } +} + +func TestReconcileEmailSinkRemovesGeneratedSecretWhenDisabled(t *testing.T) { + scheme, wandb := emailTestWandb(t) + generated := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "wandb-email-sink", + Namespace: "default", + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: apiv2.GroupVersion.String(), + Kind: "WeightsAndBiases", + Name: wandb.Name, + UID: wandb.UID, + }}, + }, + } + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(wandb, generated).Build() + + if err := reconcileEmailSink(context.Background(), client, wandb); err != nil { + t.Fatal(err) + } + err := client.Get(context.Background(), types.NamespacedName{Name: generated.Name, Namespace: generated.Namespace}, &corev1.Secret{}) + if !apierrors.IsNotFound(err) { + t.Fatalf("expected generated Secret to be deleted, got %v", err) + } +} diff --git a/internal/controller/reconciler/reconcile_v2.go b/internal/controller/reconciler/reconcile_v2.go index 545c49bf..dee5419a 100644 --- a/internal/controller/reconciler/reconcile_v2.go +++ b/internal/controller/reconciler/reconcile_v2.go @@ -343,6 +343,10 @@ func ReconcileWandbManifest( return result, err } + if err := reconcileEmailSink(ctx, client, wandb); err != nil { + return ctrl.Result{}, err + } + result, err = createKafkaTopics(ctx, client, wandb, manifest) if err != nil { return result, err diff --git a/internal/controller/reconciler/slack_env_test.go b/internal/controller/reconciler/slack_env_test.go new file mode 100644 index 00000000..761dc7f6 --- /dev/null +++ b/internal/controller/reconciler/slack_env_test.go @@ -0,0 +1,54 @@ +package reconciler + +import ( + "context" + "testing" + + apiv2 "github.com/wandb/operator/api/v2" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestResolveEnvvarsCustomResourceSlack(t *testing.T) { + wandb := &apiv2.WeightsAndBiases{ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "default"}} + wandb.Spec.Wandb.Notifications = &apiv2.NotificationsSpec{} + wandb.Spec.Wandb.Notifications.Slack = &apiv2.SlackSpec{ + ClientID: emailTestSelector("slack", "client-id"), + ClientSecret: emailTestSelector("slack", "client-secret"), + } + envs := []serverManifest.EnvVar{ + { + Name: "GORILLA_SLACK_CLIENT_ID", + Sources: []serverManifest.EnvSource{{ + Type: "custom-resource", Field: "spec.wandb.notifications.slack.clientId", + }}, + }, + { + Name: "GORILLA_SLACK_SECRET", + Sources: []serverManifest.EnvSource{{ + Type: "custom-resource", Field: "spec.wandb.notifications.slack.clientSecret", + }}, + }, + } + + resolved, err := resolveEnvvars(context.Background(), fake.NewClientBuilder().Build(), wandb, serverManifest.Manifest{}, nil, envs) + if err != nil { + t.Fatal(err) + } + for _, tc := range []struct { + name string + key string + }{ + {"GORILLA_SLACK_CLIENT_ID", "client-id"}, + {"GORILLA_SLACK_SECRET", "client-secret"}, + } { + env := mustFindEnvVar(t, resolved, tc.name) + if env.ValueFrom == nil || env.ValueFrom.SecretKeyRef == nil { + t.Fatalf("%s did not resolve to a SecretKeyRef: %+v", tc.name, env) + } + if env.ValueFrom.SecretKeyRef.Name != "slack" || env.ValueFrom.SecretKeyRef.Key != tc.key { + t.Fatalf("unexpected %s selector: %+v", tc.name, env.ValueFrom.SecretKeyRef) + } + } +} diff --git a/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml b/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml index 921d192e..1c42296d 100644 --- a/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml +++ b/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml @@ -4088,6 +4088,114 @@ spec: type: string manifestRepository: type: string + notifications: + properties: + email: + properties: + sink: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + smtp: + properties: + host: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + password: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + port: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + username: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + required: + - host + - password + - port + - username + type: object + type: object + slack: + properties: + clientId: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + clientSecret: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + type: object oidc: properties: authMethod: @@ -4599,6 +4707,19 @@ spec: x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map + emailSink: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic gatewayStatus: properties: addresses: diff --git a/internal/webhook/v2/weightsandbiases_notifications_test.go b/internal/webhook/v2/weightsandbiases_notifications_test.go new file mode 100644 index 00000000..7ae2b62a --- /dev/null +++ b/internal/webhook/v2/weightsandbiases_notifications_test.go @@ -0,0 +1,87 @@ +package v2 + +import ( + "strings" + "testing" + + appsv2 "github.com/wandb/operator/api/v2" + corev1 "k8s.io/api/core/v1" +) + +func notificationSelector(name, key string) corev1.SecretKeySelector { + return corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + Key: key, + } +} + +func TestValidateNotificationSpec(t *testing.T) { + sink := notificationSelector("email", "sink") + username := notificationSelector("smtp", "username") + password := notificationSelector("smtp", "password") + validSMTP := func() *appsv2.EmailSMTPSpec { + return &appsv2.EmailSMTPSpec{ + Host: notificationSelector("smtp", "host"), + Port: notificationSelector("smtp", "port"), + Username: username, + Password: password, + } + } + + cases := []struct { + name string + mutate func(*appsv2.WeightsAndBiases) + wantErr string + }{ + {"notifications omitted", func(*appsv2.WeightsAndBiases) {}, ""}, + {"complete Slack", func(w *appsv2.WeightsAndBiases) { + w.Spec.Wandb.Notifications = &appsv2.NotificationsSpec{} + w.Spec.Wandb.Notifications.Slack = &appsv2.SlackSpec{ + ClientID: notificationSelector("slack", "client-id"), ClientSecret: notificationSelector("slack", "client-secret"), + } + }, ""}, + {"Slack missing secret", func(w *appsv2.WeightsAndBiases) { + w.Spec.Wandb.Notifications = &appsv2.NotificationsSpec{} + w.Spec.Wandb.Notifications.Slack = &appsv2.SlackSpec{ClientID: notificationSelector("slack", "client-id")} + }, "secret name is required"}, + {"email sink", func(w *appsv2.WeightsAndBiases) { + w.Spec.Wandb.Notifications = &appsv2.NotificationsSpec{} + w.Spec.Wandb.Notifications.Email = &appsv2.EmailSpec{Sink: &sink} + }, ""}, + {"authenticated SMTP", func(w *appsv2.WeightsAndBiases) { + w.Spec.Wandb.Notifications = &appsv2.NotificationsSpec{} + w.Spec.Wandb.Notifications.Email = &appsv2.EmailSpec{SMTP: validSMTP()} + }, ""}, + {"sink and SMTP", func(w *appsv2.WeightsAndBiases) { + w.Spec.Wandb.Notifications = &appsv2.NotificationsSpec{} + w.Spec.Wandb.Notifications.Email = &appsv2.EmailSpec{Sink: &sink, SMTP: validSMTP()} + }, "exactly one"}, + {"neither email mode", func(w *appsv2.WeightsAndBiases) { + w.Spec.Wandb.Notifications = &appsv2.NotificationsSpec{} + w.Spec.Wandb.Notifications.Email = &appsv2.EmailSpec{} + }, "exactly one"}, + {"SMTP missing password", func(w *appsv2.WeightsAndBiases) { + smtp := validSMTP() + smtp.Password = corev1.SecretKeySelector{} + w.Spec.Wandb.Notifications = &appsv2.NotificationsSpec{} + w.Spec.Wandb.Notifications.Email = &appsv2.EmailSpec{SMTP: smtp} + }, "secret name is required"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + wandb := &appsv2.WeightsAndBiases{} + tc.mutate(wandb) + errs := validateNotificationSpec(wandb) + if tc.wantErr == "" { + if len(errs) != 0 { + t.Fatalf("expected no errors, got %v", errs) + } + return + } + if len(errs) == 0 || !strings.Contains(errs.ToAggregate().Error(), tc.wantErr) { + t.Fatalf("expected error containing %q, got %v", tc.wantErr, errs) + } + }) + } +} diff --git a/internal/webhook/v2/weightsandbiases_webhook.go b/internal/webhook/v2/weightsandbiases_webhook.go index 6a3d62ff..4a8ad8d1 100644 --- a/internal/webhook/v2/weightsandbiases_webhook.go +++ b/internal/webhook/v2/weightsandbiases_webhook.go @@ -112,7 +112,7 @@ func (d *WeightsAndBiasesCustomDefaulter) Default(ctx context.Context, obj runti wandb.Spec.Wandb.InternalServiceAuth.Enabled = ptr.To(true) } - if wandb.Spec.Wandb.InternalServiceAuth.OIDCIssuer == "" && wandb.Spec.Wandb.InternalServiceAuth.Enabled != nil && *wandb.Spec.Wandb.InternalServiceAuth.Enabled{ + if wandb.Spec.Wandb.InternalServiceAuth.OIDCIssuer == "" && wandb.Spec.Wandb.InternalServiceAuth.Enabled != nil && *wandb.Spec.Wandb.InternalServiceAuth.Enabled { wandb.Spec.Wandb.InternalServiceAuth.OIDCIssuer = "https://kubernetes.default.svc.cluster.local" } @@ -357,6 +357,7 @@ func validateSpec(_ context.Context, newWandb, oldWandb *appsv2.WeightsAndBiases allErrors = append(allErrors, validateRedisSpec(newWandb)...) allErrors = append(allErrors, validateObjectStoreSpec(newWandb)...) allErrors = append(allErrors, validateClickHouseSpec(newWandb)...) + allErrors = append(allErrors, validateNotificationSpec(newWandb)...) allErrors = append(allErrors, validateInfraNames(newWandb, oldWandb)...) networkingErrors, networkingWarnings := validateNetworkingSpec(newWandb) allErrors = append(allErrors, networkingErrors...) @@ -374,6 +375,46 @@ func validateSpec(_ context.Context, newWandb, oldWandb *appsv2.WeightsAndBiases ) } +func validateNotificationSpec(wandb *appsv2.WeightsAndBiases) field.ErrorList { + var errors field.ErrorList + notifications := wandb.Spec.Wandb.Notifications + if notifications == nil { + return errors + } + base := field.NewPath("spec").Child("wandb").Child("notifications") + + if slack := notifications.Slack; slack != nil { + slackPath := base.Child("slack") + errors = append(errors, validateRequiredSecretSelector(slack.ClientID, slackPath.Child("clientId"))...) + errors = append(errors, validateRequiredSecretSelector(slack.ClientSecret, slackPath.Child("clientSecret"))...) + } + + email := notifications.Email + if email == nil { + return errors + } + emailPath := base.Child("email") + if email.Sink == nil && email.SMTP == nil { + errors = append(errors, field.Invalid(emailPath, "", "configure exactly one of sink or smtp")) + return errors + } + if email.Sink != nil && email.SMTP != nil { + errors = append(errors, field.Invalid(emailPath, "", "configure exactly one of sink or smtp")) + return errors + } + if email.Sink != nil { + errors = append(errors, validateRequiredSecretSelector(*email.Sink, emailPath.Child("sink"))...) + return errors + } + + smtpPath := emailPath.Child("smtp") + errors = append(errors, validateRequiredSecretSelector(email.SMTP.Host, smtpPath.Child("host"))...) + errors = append(errors, validateRequiredSecretSelector(email.SMTP.Port, smtpPath.Child("port"))...) + errors = append(errors, validateRequiredSecretSelector(email.SMTP.Username, smtpPath.Child("username"))...) + errors = append(errors, validateRequiredSecretSelector(email.SMTP.Password, smtpPath.Child("password"))...) + return errors +} + func validateChanges(_ context.Context, newWandb *appsv2.WeightsAndBiases, oldWandb *appsv2.WeightsAndBiases) (admission.Warnings, error) { var allErrors field.ErrorList var warnings admission.Warnings From 7c35d02d53a20b8f8484171ba267937679882aa0 Mon Sep 17 00:00:00 2001 From: Jagat Thakkar Date: Mon, 17 Aug 2026 16:18:02 -0500 Subject: [PATCH 2/3] chore(test): Updating Manifests --- .../0.83.0-clickhouse-keeper.1/manifest.yaml | 10 - .../0.83.0-clickhouse-keeper.2/manifest.yaml | 10 - .../manifest.yaml | 1101 ++++++++++++ .../sizing.yaml | 1529 +++++++++++++++++ 4 files changed, 2630 insertions(+), 20 deletions(-) create mode 100644 hack/testing-manifests/server-manifest/0.84.0-notifications-security-flags.0/manifest.yaml create mode 100644 hack/testing-manifests/server-manifest/0.84.0-notifications-security-flags.0/sizing.yaml diff --git a/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/manifest.yaml b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/manifest.yaml index 0d7e56a3..9bbdaf76 100644 --- a/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/manifest.yaml +++ b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.1/manifest.yaml @@ -313,16 +313,6 @@ commonEnvvars: type: custom-resource field: spec.wandb.oidc.sessionLength defaultValue: "720h" - - name: GORILLA_SLACK_CLIENT_ID - sources: - - name: slack - type: custom-resource - field: spec.wandb.notifications.slack.clientId - - name: GORILLA_SLACK_SECRET - sources: - - name: slack - type: custom-resource - field: spec.wandb.notifications.slack.clientSecret gorillaCustomerSecrets: - name: GORILLA_CUSTOMER_SECRET_STORE_SOURCE value: "k8s-secretmanager://" diff --git a/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/manifest.yaml b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/manifest.yaml index 0d7e56a3..9bbdaf76 100644 --- a/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/manifest.yaml +++ b/hack/testing-manifests/server-manifest/0.83.0-clickhouse-keeper.2/manifest.yaml @@ -313,16 +313,6 @@ commonEnvvars: type: custom-resource field: spec.wandb.oidc.sessionLength defaultValue: "720h" - - name: GORILLA_SLACK_CLIENT_ID - sources: - - name: slack - type: custom-resource - field: spec.wandb.notifications.slack.clientId - - name: GORILLA_SLACK_SECRET - sources: - - name: slack - type: custom-resource - field: spec.wandb.notifications.slack.clientSecret gorillaCustomerSecrets: - name: GORILLA_CUSTOMER_SECRET_STORE_SOURCE value: "k8s-secretmanager://" diff --git a/hack/testing-manifests/server-manifest/0.84.0-notifications-security-flags.0/manifest.yaml b/hack/testing-manifests/server-manifest/0.84.0-notifications-security-flags.0/manifest.yaml new file mode 100644 index 00000000..b2d3e7f1 --- /dev/null +++ b/hack/testing-manifests/server-manifest/0.84.0-notifications-security-flags.0/manifest.yaml @@ -0,0 +1,1101 @@ +--- +requiredOperatorVersion: ^2.0.0 + +features: + filestreamQueue: false + +bucket: + default: + ingress: + paths: + - /bucket + servicePort: "http-minio" + pathType: Prefix + images: + seaweedfs: + registry: "docker.io" + repository: "chrislusf/seaweedfs" + tag: "4.35" + +clickhouse: + default: + images: + server: + registry: "docker.io" + repository: "altinity/clickhouse-server" + tag: "25.8.16.10002.altinitystable" + +clickhouseKeeper: + default: + images: + keeper: + registry: "docker.io" + repository: "altinity/clickhouse-keeper" + tag: "25.8.16.10002.altinitystable" + +generatedSecrets: + - name: session-key + length: 32 + type: password + - name: weave-worker-auth + length: 32 + type: password + useExactName: true + +kafka: + images: + bufstream: + registry: "us-docker.pkg.dev" + repository: "buf-images-1/buf/images/bufstream" + tag: "0.4.15" + etcd: + registry: "quay.io" + repository: "coreos/etcd" + tag: "v3.5.31" + bucketEnsure: + repository: "amazon/aws-cli" + tag: "2.35.10" + topics: + - name: filestream + features: + - filestreamQueue + topic: filestream + partitionCount: 96 + - name: flat-run-fields-updater + topic: flat-run-fields-updater + partitionCount: 96 + - name: weave-worker + topic: weave.call_ended + partitionCount: 16 + - name: weave-evaluate-model-worker + topic: weave.evaluate_model + partitionCount: 16 + +mysql: + default: + images: + mysql: + registry: "ghcr.io" + repository: "cybozu-go/moco/mysql" + tag: "8.4.8" + exporter: + registry: "docker.io" + repository: "prom/mysqld-exporter" + tag: "v0.15.1" + metadata: {} + runs: {} + usage: {} + +redis: + default: + images: + standalone: + registry: "quay.io" + repository: "opstree/redis" + tag: "v7.0.15" + replication: + registry: "quay.io" + repository: "opstree/redis" + tag: "v7.0.15" + sentinel: + registry: "quay.io" + repository: "opstree/redis-sentinel" + tag: "v7.0.12" + exporter: + registry: "quay.io" + repository: "opstree/redis-exporter" + tag: "v1.44.0" + +commonEnvvars: + gorillaMysql: + - name: MYSQL + sources: + - name: default + type: mysql + - name: GORILLA_ANALYTICS_SINK + sources: + - name: default + type: mysql + - name: GORILLA_FILE_STREAM_STORE_ADDRESS + sources: + - name: default + type: mysql + - name: GORILLA_METADATA_STORE + sources: + - name: metadata + type: mysql + - name: GORILLA_RUN_STORE + sources: + - name: runs + type: mysql + - name: GORILLA_USAGE_STORE + sources: + - name: usage + type: mysql + gorillaBucket: + - name: AWS_REGION + sources: + - name: default + type: bucket + field: region + defaultValue: "us-east-1" + - name: BUCKET + sources: + - name: default + type: bucket + - name: GORILLA_FILE_STORE + sources: + - name: default + type: bucket + - name: GORILLA_RUN_UPDATE_SHADOW_QUEUE_OVERFLOW_BUCKET_STORE + sources: + - name: default + type: bucket + - name: GORILLA_STORAGE_BUCKET + sources: + - name: default + type: bucket + gorillaRedis: + - name: REDIS + sources: + - name: default + type: redis + - name: GORILLA_ACTIVITY_STORE_CACHE_ADDRESS + sources: + - name: default + type: redis + - name: GORILLA_AUDITOR_CACHE + sources: + - name: default + type: redis + - name: GORILLA_CACHE + sources: + - name: default + type: redis + - name: GORILLA_FILE_METADATA_SOURCE + sources: + - name: default + type: redis + - name: GORILLA_LOCKER + sources: + - name: default + type: redis + - name: GORILLA_METADATA_CACHE + sources: + - name: default + type: redis + - name: GORILLA_SETTINGS_CACHE + sources: + - name: default + type: redis + - name: GORILLA_USAGE_METRICS_CACHE + sources: + - name: default + type: redis + gorillaService: + - name: GORILLA_SWEEP_PROVIDER + sources: + - name: anaconda2 + type: service + proto: "http" + path: "" + gorillaTaskQueueConsumer: + - name: GORILLA_TASK_QUEUE + sources: + - name: taskQueue + type: redis + params: + concurrency: 10 + - name: GORILLA_TASK_QUEUE_WORKER_ENABLED + value: "true" + gorillaTaskQueueProducer: + - name: GORILLA_TASK_QUEUE + sources: + - name: taskQueue + type: redis + - name: GORILLA_TASK_QUEUE_WORKER_ENABLED + value: "false" + gorillaHistoryStore: + - name: GORILLA_HISTORY_STORE + sources: + - name: parquet + type: service + port: parquet + proto: "http" + path: "/_goRPC_" + - name: default + type: mysql + - name: GORILLA_PARQUET_LIVE_HISTORY_STORE + sources: + - name: default + type: mysql + gorillaOnprem: + - name: GORILLA_LOCAL_SERVICE_BYPASS + value: "true" + - name: GORILLA_DEFAULT_REGION + value: "minio-local" + - name: GORILLA_EMAIL_SINK + sources: + - name: email + type: custom-resource + field: status.emailSink + defaultValue: "https://api.wandb.ai/email/dispatch" + - name: GORILLA_SLACK_CLIENT_ID + sources: + - name: slack + type: custom-resource + field: spec.wandb.notifications.slack.clientId + - name: GORILLA_SLACK_SECRET + sources: + - name: slack + type: custom-resource + field: spec.wandb.notifications.slack.clientSecret + - name: GORILLA_FILE_METADATA_SOURCE_IS_INTERNAL + value: "true" + - name: GORILLA_ONPREM + value: "true" + - name: GORILLA_ONPREM_API_KEY_PREFIX + value: "local" + - name: GORILLA_STATSD_PORT + value: "0" + - name: GORILLA_SESSION_KEY + sources: + - name: session-key + type: generatedSecret + - name: GORILLA_ALLOW_USER_TEAM_CREATION + sources: + - name: security + type: custom-resource + field: spec.wandb.security.allowUserTeamCreation + - name: GORILLA_DISABLE_CODE_SAVING + sources: + - name: security + type: custom-resource + field: spec.wandb.security.disableCodeSaving + - name: GORILLA_ALLOW_ANONYMOUS_PUBLIC_PROJECTS + sources: + - name: security + type: custom-resource + field: spec.wandb.security.allowAnonymousPublicProjects + - name: GORILLA_DISABLE_SSO_PROVISIONING + sources: + - name: security + type: custom-resource + field: spec.wandb.security.disableSSOProvisioning + - name: GORILLA_INSECURE_ALLOW_API_KEY_ADMIN_ACCESS + sources: + - name: security + type: custom-resource + field: spec.wandb.security.insecureAllowAPIKeyAdminAccess + - name: GORILLA_ONPREM_HIDE_UPGRADE_BANNER + sources: + - name: security + type: custom-resource + field: spec.wandb.security.hideUpgradeBanner + - name: GORILLA_BUCKET_ATTRIBUTION_DISABLED + sources: + - name: object-store + type: custom-resource + field: spec.objectStore.default.bucketAttributionDisabled + - name: BUCKET_PROXY + sources: + - name: bucket-proxy + type: custom-resource + field: spec.wandb.bucketProxy + - name: GORILLA_GLUE_FILE_STORE_IS_PROXIED + sources: + - name: bucket-proxy + type: custom-resource + field: spec.wandb.bucketProxy + - name: GORILLA_FILE_STORE_IS_PROXIED + sources: + - name: bucket-proxy + type: custom-resource + field: spec.wandb.bucketProxy + - name: GORILLA_FILE_HOST + sources: + - name: hostname + type: custom-resource + field: status.wandb.hostname + - name: GORILLA_FRONTEND_HOST + sources: + - name: hostname + type: custom-resource + field: status.wandb.hostname + - name: LICENSE + sources: + - name: license + type: custom-resource + field: spec.wandb.license + - name: GORILLA_LICENSE + sources: + - name: license + type: custom-resource + field: spec.wandb.license + - name: GORILLA_OIDC_CLIENT_ID + sources: + - name: oidc + type: custom-resource + field: spec.wandb.oidc.clientId + - name: GORILLA_OIDC_SECRET + sources: + - name: oidc + type: custom-resource + field: spec.wandb.oidc.clientSecret + - name: GORILLA_OIDC_ISSUER + sources: + - name: oidc + type: custom-resource + field: spec.wandb.oidc.issuerUrl + - name: GORILLA_AUTH_METHOD + sources: + - name: oidc + type: custom-resource + field: spec.wandb.oidc.authMethod + - name: GORILLA_SESSION_LENGTH + sources: + - name: oidc + type: custom-resource + field: spec.wandb.oidc.sessionLength + defaultValue: "720h" + gorillaCustomerSecrets: + - name: GORILLA_CUSTOMER_SECRET_STORE_SOURCE + value: "k8s-secretmanager://" + - name: GORILLA_CUSTOMER_SECRET_STORE_K8S_CONFIG_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: GORILLA_INTERNAL_JWT_SUBJECTS_TO_ISSUERS + sources: + - name: internal + type: jwt-issuer-map + clickhouse: + - name: WF_CLICKHOUSE_HOST + sources: + - name: default + type: clickhouse + field: host + - name: WF_CLICKHOUSE_PORT + sources: + - name: default + type: clickhouse + field: http-port + - name: WF_CLICKHOUSE_USER + sources: + - name: default + type: clickhouse + field: user + - name: WF_CLICKHOUSE_PASS + sources: + - name: default + type: clickhouse + field: password + - name: WF_CLICKHOUSE_DATABASE + sources: + - name: default + type: clickhouse + field: database + - name: WF_CLICKHOUSE_REPLICATED + defaultValue: "false" + sources: + - name: clickhouse-replicated + type: custom-resource + field: status.clickhouseStatus.default.replicated + - name: WF_CLICKHOUSE_REPLICATED_CLUSTER + sources: + - name: clickhouse-replicated-cluster + type: custom-resource + field: status.clickhouseStatus.default.clusterName + kafka: + - name: KAFKA_BROKER_HOST + sources: + - name: default + type: kafka + field: host + - name: KAFKA_BROKER_PORT + sources: + - name: default + type: kafka + field: port + - name: KAFKA_URL + sources: + - name: default + type: kafka + field: url + weaveTrace: + - name: WANDB_PUBLIC_BASE_URL + sources: + - name: hostname + type: custom-resource + field: status.wandb.hostname + - name: WANDB_BASE_URL + sources: + - name: api + type: service + proto: "http" + path: "/" + - name: WF_TRACE_SERVER_URL + sources: + - name: weave-trace + type: service + proto: "http" + path: "/traces" + - name: WEAVE_TRACE_SERVER_BASE_URL + sources: + - name: weave-trace + type: service + proto: "http" + path: "/traces" + - name: API_PATH_PREFIX + value: "/traces" + - name: WEAVE_ENABLE_ONLINE_EVAL + value: "true" + - name: WEAVE_ENABLE_AGENT_SCORING + value: "false" + - name: WEAVE_ENABLE_EVALUATE_MODEL_WORKER + value: "true" + - name: WANDB_INTERNAL_SERVICE_TOKEN + sources: + - name: weave-worker-auth + type: generatedSecret + - name: WANDB_INTERNAL_SERVICE_TOKEN_SECRET_NAME + value: "weave-worker-auth" + frontend: + - name: REACT_APP_HOST + source: + - name: hostname + type: custom-resource + field: status.wandb.hostname + - name: REACT_APP_ENVIRONMENT_NAME + value: "local" + - name: REACT_APP_ENVIRONMENT_IS_PRIVATE + value: "true" + - name: REACT_APP_ANALYTICS_DISABLED + value: "true" + - name: WEAVE_TRACES_ENABLED + value: "true" + - name: SERVER_FLAG_WEAVE_1_PERCENTAGE + value: "100" + flatRunsV2Producer: + - name: KAFKA_RUNS_V2_TOPIC_NAME + value: "flat-run-fields-updater" + - name: GORILLA_RUN_UPDATE_SHADOW_QUEUE_ADDR + value: "$(KAFKA_URL)/$(KAFKA_RUNS_V2_TOPIC_NAME)" + - name: GORILLA_RUN_STORE_ONPREM_MIGRATE_CREATE_RUN_TABLES + value: "true" + - name: GORILLA_RUN_STORE_ONPREM_MIGRATE_CREATE_RUN_STORE + value: "true" + - name: GORILLA_RUN_STORE_ONPREM_MIGRATE_SHADOW_RUN_UPDATES + value: "true" + - name: GORILLA_RUN_STORE_ONPREM_MIGRATE_DISABLE_READS + value: "false" + - name: GORILLA_RUN_STORE_ONPREM_MIGRATE_FLAT_RUNS_MIGRATOR + value: "true" + flatRunsV2Consumer: + - name: KAFKA_RUNS_V2_TOPIC_NAME + value: "flat-run-fields-updater" + telemetryOtel: + - name: OTEL_EXPORTER_OTLP_PROTOCOL + sources: + - type: telemetry + field: protocol + - name: OTEL_TRACES_EXPORTER + sources: + - type: telemetry + field: tracesExporter + - name: OTEL_METRICS_EXPORTER + sources: + - type: telemetry + field: metricsExporter + - name: OTEL_LOGS_EXPORTER + sources: + - type: telemetry + field: logsExporter + - name: OTEL_EXPORTER_OTLP_METRICS_ENDPOINT + sources: + - type: telemetry + field: metricsEndpoint + - name: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT + sources: + - type: telemetry + field: logsEndpoint + - name: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT + sources: + - type: telemetry + field: tracesEndpoint + - name: OTEL_SERVICE_NAME + sources: + - type: telemetry + field: serviceName + - name: OTEL_RESOURCE_ATTRIBUTES + sources: + - type: telemetry + field: resourceAttributes + - name: GORILLA_TRACER + sources: + - type: telemetry + field: gorillaTracer + runtimePaths: + - name: TMPDIR + value: "/tmp" + - name: HOME + value: "/tmp/wandb-home" + - name: XDG_CACHE_HOME + value: "/tmp/wandb-cache" + pythonRuntimePaths: + - name: PYTHONDONTWRITEBYTECODE + value: "1" + anacondaRuntimePaths: + - name: ANACONDA2_DISABLE_FILE_LOGGING + value: "true" + - name: ANACONDA2_LOG_DIR + value: "/tmp/anaconda2/log" + - name: GUNICORN_WORKER_TMP_DIR + value: "/tmp/gunicorn" + weaveRuntimePaths: + - name: WEAVE_GUNICORN_WORKER_TMP_DIR + value: "/tmp/gunicorn" +commonVolumeMounts: + runtimeTmp: + - mountPath: /tmp + name: runtime-tmp + source: + type: emptyDir + name: runtime-tmp + internalSigner: + - mountPath: /vol/env + name: wandb-internal-signer-root + source: + name: wandb-internal-signer + type: secret + +applications: + anaconda2: + name: anaconda2 + commonEnvs: + - runtimePaths + - pythonRuntimePaths + - anacondaRuntimePaths + commonVolumeMounts: + - runtimeTmp + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/anaconda2 + tag: 0.84.0-notifications-security-flags.0 + containers: + - name: anaconda2 + ports: + - containerPort: 8080 + name: anaconda2 + protocol: TCP + livenessProbe: + httpGet: + path: /ping + readinessProbe: + httpGet: + path: /ping + service: + ports: + - port: 8080 + protocol: TCP + name: anaconda2 + api: + name: api + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaHistoryStore + - gorillaTaskQueueProducer + - gorillaService + - gorillaOnprem + - gorillaCustomerSecrets + - kafka + - flatRunsV2Producer + - runtimePaths + commonVolumeMounts: + - internalSigner + - runtimeTmp + env: + - name: GORILLA_LICENSE_CERT_PATH + value: "/jwks.json" + - name: GORILLA_VIEW_SPEC_UPDATER_EXECUTABLE + value: "/view-spec-updater-linux" + - name: INTERNAL_SIGNER_KEY_PATH + value: "/vol/env" + - name: MIGRATE_RUNS_DB + sources: + - name: default + type: mysql + - name: MIGRATE_USAGE_DB + sources: + - name: default + type: mysql + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.84.0-notifications-security-flags.0 + containers: + - name: api + args: + - gorilla + ports: + - containerPort: 8080 + name: api + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + readinessProbe: + httpGet: + path: /ready + service: + ports: + - port: 8080 + protocol: TCP + name: api + ingress: + paths: + - /api + - /artifacts + - /artifactsV2 + - /files + - /graphql + - /graphql2 + - /oidc + servicePort: "8080" + pathType: Prefix + executor: + name: executor + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaHistoryStore + - gorillaTaskQueueConsumer + - gorillaService + - gorillaOnprem + - runtimePaths + commonVolumeMounts: + - runtimeTmp + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.84.0-notifications-security-flags.0 + containers: + - name: executor + args: + - executor + filemeta: + name: filemeta + args: + - filemeta + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaService + - gorillaOnprem + - runtimePaths + commonVolumeMounts: + - runtimeTmp + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.84.0-notifications-security-flags.0 + filestream: + name: filestream + features: + - filestreamQueue + args: + - filestream + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaService + - gorillaOnprem + - runtimePaths + commonVolumeMounts: + - runtimeTmp + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.84.0-notifications-security-flags.0 + containers: + - name: filestream + flat-run-fields-updater: + name: flat-run-fields-updater + args: + - flat-run-fields-updater + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaService + - gorillaOnprem + - kafka + - flatRunsV2Consumer + - runtimePaths + commonVolumeMounts: + - runtimeTmp + env: + - name: GORILLA_RUN_UPDATE_SHADOW_QUEUE_SUBSCRIPTIONS_FLAT_RUN_FIELDS_UPDATER + value: "$(KAFKA_URL)/$(KAFKA_RUNS_V2_TOPIC_NAME)?consumer_group_id=flat-run-fields-updater" + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.84.0-notifications-security-flags.0 + containers: + - name: flat-run-fields-updater + frontend: + name: frontend + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/frontend-nginx + tag: 0.84.0-notifications-security-flags.0 + commonEnvs: + - frontend + env: + - name: FRONTEND_APP_BACKEND + sources: + - name: api + type: service + proto: "" + path: "" + - name: FRONTEND_AUTH_BACKEND + sources: + - name: api + type: service + proto: "" + path: "" + - name: FRONTEND_LOCAL_BACKEND + sources: + - name: api + type: service + proto: "" + path: "" + - name: FRONTEND_WEAVE_BACKEND + sources: + - name: weave + type: service + proto: "" + path: "" + - name: WEAVE_ENABLED + value: "true" + - name: OPERATOR_ENABLED + value: "true" + containers: + - name: frontend + ports: + - containerPort: 8080 + name: frontend + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + readinessProbe: + httpGet: + path: /ready + service: + ports: + - port: 8080 + protocol: TCP + name: frontend + ingress: + paths: + - / + servicePort: "8080" + pathType: Prefix + volumeMounts: + - name: tmp + mountPath: /tmp/ + source: + type: emptyDir + name: tmp + - name: nginx-cache + mountPath: /var/cache/nginx + source: + type: emptyDir + name: nginx-cache + glue: + name: glue + args: + - glue + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaHistoryStore + - gorillaService + - gorillaOnprem + - runtimePaths + commonVolumeMounts: + - internalSigner + - runtimeTmp + env: + - name: GORILLA_LICENSE_CERT_PATH + value: "/jwks.json" + - name: GORILLA_VIEW_SPEC_UPDATER_EXECUTABLE + value: "/view-spec-updater-linux" + - name: GORILLA_GLUE_TASK_PROVIDER + value: "memory://" + - name: GORILLA_GLUE_TASK_CONFIG_PATH + value: "/gorilla_glue_tasks_local.yaml" + - name: GORILLA_GLUE_TASK_STORE + value: "memory://" + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.84.0-notifications-security-flags.0 + containers: + - name: glue + metric-observer: + name: metric-observer + args: + - metric-observer + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaService + - gorillaOnprem + - kafka + - flatRunsV2Consumer + - runtimePaths + commonVolumeMounts: + - runtimeTmp + env: + - name: GORILLA_RUN_UPDATE_SHADOW_QUEUE_SUBSCRIPTIONS_METRIC_OBSERVER + value: "$(KAFKA_URL)/$(KAFKA_RUNS_V2_TOPIC_NAME)?consumer_group_id=metric-observer" + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.84.0-notifications-security-flags.0 + containers: + - name: metric-observer + parquet: + name: parquet + args: + - parquet + commonEnvs: + - gorillaMysql + - gorillaBucket + - gorillaRedis + - gorillaHistoryStore + - gorillaTaskQueueProducer + - gorillaService + - gorillaOnprem + - runtimePaths + commonVolumeMounts: + - runtimeTmp + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.84.0-notifications-security-flags.0 + containers: + - name: parquet + ports: + - containerPort: 8080 + name: parquet + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + readinessProbe: + httpGet: + path: /ready + service: + ports: + - port: 8080 + protocol: TCP + name: parquet + weave: + name: weave + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/weave-python + tag: 0.84.0-notifications-security-flags.0 + commonEnvs: + - runtimePaths + - pythonRuntimePaths + - weaveRuntimePaths + env: + - name: DATADOG_TRACE_ENABLED + value: "false" + service: + ports: + - port: 9239 + protocol: TCP + name: weave + ingress: + paths: + - /weave/ + servicePort: "9239" + pathType: Prefix + containers: + - name: weave + ports: + - containerPort: 9239 + name: weave + protocol: TCP + livenessProbe: + httpGet: + path: /__weave/hello + readinessProbe: + httpGet: + path: /__weave/hello + - name: weave-cache-clear + command: + - python + - weave-public/weave_query/scripts/clear_cache.py + resources: + limits: + cpu: 2 + memory: 2Gi + requests: + cpu: 100m + memory: 128Mi + volumeMounts: + - name: temp-dir + mountPath: /tmp/ + source: + type: emptyDir + name: temp-dir + - name: cache + mountPath: /vol/weave/cache + source: + type: emptyDir + name: cache + weave-trace: + name: weave-trace + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/weave-trace + tag: 0.84.0-notifications-security-flags.0 + commonEnvs: + - clickhouse + - kafka + - weaveTrace + - runtimePaths + - pythonRuntimePaths + commonVolumeMounts: + - runtimeTmp + containers: + - name: weave-trace + args: + - "uvicorn" + - "src.trace_server:app" + - "--host" + - "0.0.0.0" + - "--port" + - "8080" + ports: + - containerPort: 8080 + name: weave-trace + protocol: TCP + livenessProbe: + httpGet: + path: /traces/health + readinessProbe: + httpGet: + path: /traces/health + service: + ports: + - port: 8080 + protocol: TCP + name: weave-trace + ingress: + paths: + - /traces + servicePort: "8080" + pathType: Prefix + jwtTokens: + - name: internal-jwt + mountPath: /tmp/weave-trace/internal-jwt + source: + kubernetesServiceAccount: + audience: internal-service + expirationSeconds: 600 + weave-trace-worker: + name: weave-trace-worker + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/weave-trace + tag: 0.84.0-notifications-security-flags.0 + env: + - name: DD_TRACE_ENABLED + value: "false" + commonEnvs: + - clickhouse + - kafka + - weaveTrace + - runtimePaths + - pythonRuntimePaths + commonVolumeMounts: + - runtimeTmp + containers: + - name: weave-trace-worker + args: + - "python" + - "-m" + - "src.workers.scoring_worker" + weave-trace-evaluate-model-worker: + name: weave-trace-evaluate-model-worker + legacyKey: weave-evaluate-model-worker + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/weave-trace + tag: 0.84.0-notifications-security-flags.0 + env: + - name: DD_TRACE_ENABLED + value: "false" + commonEnvs: + - clickhouse + - kafka + - weaveTrace + - runtimePaths + - pythonRuntimePaths + commonVolumeMounts: + - runtimeTmp + containers: + - name: weave-trace-evaluate-model-worker + args: + - "python" + - "-m" + - "src.workers.evaluate_model_worker.evaluate_model_worker" + +migrations: + gorilla: + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.84.0-notifications-security-flags.0 + args: + - "migrate" + - "--db=$(GORILLA_METADATA_STORE)" + - "--runs-db=$(GORILLA_RUN_STORE)" + - "--usage-db=$(GORILLA_USAGE_STORE)" + - "--squash" + - "true" + commonEnvs: + - gorillaMysql + - gorillaOnprem + - runtimePaths + commonVolumeMounts: + - runtimeTmp + internal-signer: + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/megabinary + tag: 0.84.0-notifications-security-flags.0 + args: + - "secret-generation-job" + env: + - name: INTERNAL_SIGNER_K8S_SECRET_NAME + value: "wandb-internal-signer" + - name: INTERNAL_SIGNER_K8S_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + commonEnvs: + - runtimePaths + commonVolumeMounts: + - runtimeTmp + weave-trace: + image: + repository: us-docker.pkg.dev/wandb-production/public/wandb/weave-trace + tag: 0.84.0-notifications-security-flags.0 + args: + - python + - migrator.py + commonEnvs: + - clickhouse + - runtimePaths + - pythonRuntimePaths + commonVolumeMounts: + - runtimeTmp diff --git a/hack/testing-manifests/server-manifest/0.84.0-notifications-security-flags.0/sizing.yaml b/hack/testing-manifests/server-manifest/0.84.0-notifications-security-flags.0/sizing.yaml new file mode 100644 index 00000000..34e04aaf --- /dev/null +++ b/hack/testing-manifests/server-manifest/0.84.0-notifications-security-flags.0/sizing.yaml @@ -0,0 +1,1529 @@ +bucket: + default: + sizing: + default: + replicas: 1 + volumeSize: 10Gi + metadataVolumeSize: 5Gi + micro: + replicas: 3 + copies: 1 + volumeSize: 50Gi + metadataVolumeSize: 10Gi + resources: + requests: + cpu: 1 + memory: 4Gi + limits: + cpu: 1 + memory: 4Gi + small: + replicas: 3 + copies: 1 + volumeSize: 100Gi + metadataVolumeSize: 15Gi + resources: + requests: + cpu: 2 + memory: 8Gi + limits: + cpu: 2 + memory: 8Gi + medium: + replicas: 3 + copies: 1 + volumeSize: 100Gi + metadataVolumeSize: 20Gi + resources: + requests: + cpu: 4 + memory: 16Gi + limits: + cpu: 4 + memory: 16Gi + large: + replicas: 3 + copies: 2 + volumeSize: 200Gi + metadataVolumeSize: 30Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi + xlarge: + replicas: 3 + copies: 2 + volumeSize: 200Gi + metadataVolumeSize: 40Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi + 2xlarge: + replicas: 3 + copies: 2 + volumeSize: 200Gi + metadataVolumeSize: 60Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi +clickhouse: + default: + sizing: + default: + shards: 1 + replicas: 1 + volumeSize: 10Gi + micro: + replicas: 2 + volumeSize: 30Gi + resources: + requests: + cpu: 1 + memory: 4Gi + limits: + cpu: 1 + memory: 4Gi + small: + shards: 1 + replicas: 3 + volumeSize: 50Gi + resources: + requests: + cpu: 2 + memory: 8Gi + limits: + cpu: 2 + memory: 8Gi + medium: + shards: 1 + replicas: 3 + volumeSize: 100Gi + resources: + requests: + cpu: 4 + memory: 16Gi + limits: + cpu: 4 + memory: 16Gi + large: + shards: 1 + replicas: 3 + volumeSize: 200Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi + xlarge: + shards: 2 + replicas: 3 + volumeSize: 200Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi + 2xlarge: + shards: 4 + replicas: 3 + volumeSize: 200Gi + resources: + requests: + cpu: 8 + memory: 32Gi + limits: + cpu: 8 + memory: 32Gi +# Keeper coordinates ReplicatedMergeTree replication; it stores only the Raft +# log + metadata snapshots, so its volume is small and independent of CH data. +# replicas must be odd (Raft quorum): 1 for single-node, 3 for replicated CH. +clickhouseKeeper: + default: + sizing: + default: + replicas: 1 + volumeSize: 10Gi + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 500m + memory: 1Gi + micro: + replicas: 3 + volumeSize: 10Gi + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 500m + memory: 1Gi + small: + replicas: 3 + volumeSize: 20Gi + resources: + requests: + cpu: 1 + memory: 2Gi + limits: + cpu: 1 + memory: 2Gi + medium: + replicas: 3 + volumeSize: 20Gi + resources: + requests: + cpu: 1 + memory: 2Gi + limits: + cpu: 1 + memory: 2Gi + large: + replicas: 3 + volumeSize: 20Gi + resources: + requests: + cpu: 2 + memory: 4Gi + limits: + cpu: 2 + memory: 4Gi + xlarge: + replicas: 3 + volumeSize: 20Gi + resources: + requests: + cpu: 2 + memory: 4Gi + limits: + cpu: 2 + memory: 4Gi + 2xlarge: + replicas: 3 + volumeSize: 20Gi + resources: + requests: + cpu: 2 + memory: 4Gi + limits: + cpu: 2 + memory: 4Gi +kafka: + sizing: + # Bufstream brokers are stateless; replicas = broker count (floored at 2), volumeSize = etcd metadata PVC, replicationFactor = topic RF (1 for object-store backed). + default: + replicas: 2 + volumeSize: 10Gi + replicationFactor: 1 + micro: + replicas: 2 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 1 + memory: 4Gi + limits: + cpu: 1 + memory: 4Gi + small: + replicas: 2 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 2 + memory: 8Gi + limits: + cpu: 2 + memory: 8Gi + medium: + replicas: 3 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 4 + memory: 16Gi + limits: + cpu: 4 + memory: 16Gi + large: + replicas: 3 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi + xlarge: + replicas: 4 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi + 2xlarge: + replicas: 6 + volumeSize: 10Gi + replicationFactor: 1 + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi +mysql: + default: + sizing: + default: + replicas: 1 + volumeSize: 10Gi + micro: + replicas: 3 + volumeSize: 50Gi + resources: + requests: + cpu: 1 + memory: 4Gi + limits: + cpu: 1 + memory: 4Gi + small: + replicas: 3 + volumeSize: 100Gi + resources: + requests: + cpu: 2 + memory: 8Gi + limits: + cpu: 2 + memory: 8Gi + medium: + replicas: 3 + volumeSize: 200Gi + resources: + requests: + cpu: 4 + memory: 16Gi + limits: + cpu: 4 + memory: 16Gi + large: + replicas: 3 + volumeSize: 500Gi + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi + xlarge: + replicas: 3 + volumeSize: 1Ti + resources: + requests: + cpu: 15 + memory: 60Gi + limits: + cpu: 15 + memory: 60Gi + 2xlarge: + replicas: 3 + volumeSize: 2Ti + resources: + requests: + cpu: 30 + memory: 120Gi + limits: + cpu: 30 + memory: 120Gi +redis: + default: + sizing: + default: + shards: 1 + replicas: 1 + volumeSize: 5Gi + micro: + replicas: 2 + volumeSize: 8Gi + resources: + requests: + cpu: 1 + memory: 4Gi + limits: + cpu: 1 + memory: 4Gi + small: + shards: 1 + replicas: 3 + volumeSize: 16Gi + resources: + requests: + cpu: 2 + memory: 8Gi + limits: + cpu: 2 + memory: 8Gi + medium: + shards: 1 + replicas: 3 + volumeSize: 32Gi + resources: + requests: + cpu: 4 + memory: 16Gi + limits: + cpu: 4 + memory: 16Gi + large: + shards: 1 + replicas: 3 + volumeSize: 64Gi + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi + xlarge: + shards: 2 + replicas: 3 + volumeSize: 128Gi + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi + 2xlarge: + shards: 4 + replicas: 3 + volumeSize: 256Gi + resources: + requests: + cpu: 8 + memory: 30Gi + limits: + cpu: 8 + memory: 30Gi +applications: + anaconda2: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 4Gi + requests: + cpu: "2" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + medium: + resources: + limits: + cpu: "4" + memory: 8Gi + requests: + cpu: "4" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + large: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 6 + minReplicas: 4 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 6 + minReplicas: 4 + api: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "4" + memory: 8Gi + requests: + cpu: "4" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + medium: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + large: + resources: + limits: + cpu: "16" + memory: 32Gi + requests: + cpu: "16" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 6 + minReplicas: 4 + xlarge: + resources: + limits: + cpu: "16" + memory: 32Gi + requests: + cpu: "16" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 12 + minReplicas: 6 + 2xlarge: + resources: + limits: + cpu: "16" + memory: 32Gi + requests: + cpu: "16" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 16 + minReplicas: 8 + executor: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 4Gi + requests: + cpu: "1" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "4" + memory: 16Gi + requests: + cpu: "4" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + medium: + resources: + limits: + cpu: "8" + memory: 32Gi + requests: + cpu: "8" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + large: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + xlarge: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 16 + minReplicas: 8 + 2xlarge: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 32 + minReplicas: 16 + filemeta: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 2Gi + requests: + cpu: "2" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + medium: + resources: + limits: + cpu: "4" + memory: 4Gi + requests: + cpu: "4" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + filestream: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 4Gi + requests: + cpu: "2" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + medium: + resources: + limits: + cpu: "4" + memory: 8Gi + requests: + cpu: "4" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + flat-run-fields-updater: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 4Gi + requests: + cpu: "2" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + medium: + resources: + limits: + cpu: "4" + memory: 8Gi + requests: + cpu: "4" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + frontend: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 2Gi + requests: + cpu: "2" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + medium: + resources: + limits: + cpu: "4" + memory: 4Gi + requests: + cpu: "4" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + large: + resources: + limits: + cpu: "8" + memory: 8Gi + requests: + cpu: "8" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + xlarge: + resources: + limits: + cpu: "8" + memory: 8Gi + requests: + cpu: "8" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 8Gi + requests: + cpu: "8" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + glue: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 3Gi + requests: + cpu: "1" + memory: 3Gi + small: + resources: + limits: + cpu: "2" + memory: 6Gi + requests: + cpu: "2" + memory: 6Gi + medium: + resources: + limits: + cpu: "4" + memory: 12Gi + requests: + cpu: "4" + memory: 12Gi + large: + resources: + limits: + cpu: "8" + memory: 24Gi + requests: + cpu: "8" + memory: 24Gi + xlarge: + resources: + limits: + cpu: "16" + memory: 48Gi + requests: + cpu: "16" + memory: 48Gi + 2xlarge: + resources: + limits: + cpu: "30" + memory: 64Gi + requests: + cpu: "30" + memory: 64Gi + metric-observer: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "2" + memory: 4Gi + requests: + cpu: "2" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + medium: + resources: + limits: + cpu: "4" + memory: 8Gi + requests: + cpu: "4" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + large: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "8" + memory: 16Gi + requests: + cpu: "8" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + parquet: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 8Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "4" + memory: 16Gi + requests: + cpu: "4" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 2 + medium: + resources: + limits: + cpu: "8" + memory: 32Gi + requests: + cpu: "8" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 2 + large: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + xlarge: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 6 + minReplicas: 4 + 2xlarge: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 8 + minReplicas: 4 + weave: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 4Gi + requests: + cpu: "1" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "4" + memory: 16Gi + requests: + cpu: "4" + memory: 16Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 2 + medium: + resources: + limits: + cpu: "8" + memory: 32Gi + requests: + cpu: "8" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 3 + minReplicas: 2 + large: + resources: + limits: + cpu: "8" + memory: 32Gi + requests: + cpu: "8" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + xlarge: + resources: + limits: + cpu: "8" + memory: 32Gi + requests: + cpu: "8" + memory: 32Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 5 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "16" + memory: 64Gi + requests: + cpu: "16" + memory: 64Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 5 + minReplicas: 2 + weave-trace: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 2 + medium: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 6 + minReplicas: 3 + large: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 6Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 10 + minReplicas: 4 + xlarge: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 6Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 15 + minReplicas: 5 + 2xlarge: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 6Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 20 + minReplicas: 6 + weave-trace-worker: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 1 + medium: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 6 + minReplicas: 1 + large: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 10 + minReplicas: 2 + xlarge: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 15 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 20 + minReplicas: 2 + weave-trace-evaluate-model-worker: + sizing: + micro: + resources: + limits: + cpu: "1" + memory: 2Gi + requests: + cpu: "1" + memory: 2Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 2 + minReplicas: 1 + small: + resources: + limits: + cpu: "1" + memory: 8Gi + requests: + cpu: "1" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 4 + minReplicas: 1 + medium: + resources: + limits: + cpu: "2" + memory: 8Gi + requests: + cpu: "2" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 6 + minReplicas: 1 + large: + resources: + limits: + cpu: "3" + memory: 8Gi + requests: + cpu: "3" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 10 + minReplicas: 2 + xlarge: + resources: + limits: + cpu: "3" + memory: 8Gi + requests: + cpu: "3" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 15 + minReplicas: 2 + 2xlarge: + resources: + limits: + cpu: "3" + memory: 8Gi + requests: + cpu: "3" + memory: 4Gi + autoscaling: + horizontal: + enabled: true + maxReplicas: 20 + minReplicas: 2 From 7031e8519219faf8110a6ef121d66bb729c5fe26 Mon Sep 17 00:00:00 2001 From: Jagat Thakkar Date: Tue, 18 Aug 2026 13:36:11 -0500 Subject: [PATCH 3/3] feat: Adding Security Flag Configuration --- api/v2/weightsandbiases_types.go | 32 +++++++- api/v2/zz_generated.deepcopy.go | 16 ++++ .../apps.wandb.com_weightsandbiases.yaml | 24 ++++++ .../reconciler/security_flags_env_test.go | 73 +++++++++++++++++++ .../apps.wandb.com_weightsandbiases.yaml | 24 ++++++ 5 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 internal/controller/reconciler/security_flags_env_test.go diff --git a/api/v2/weightsandbiases_types.go b/api/v2/weightsandbiases_types.go index f0811dc3..b64885bd 100644 --- a/api/v2/weightsandbiases_types.go +++ b/api/v2/weightsandbiases_types.go @@ -385,6 +385,10 @@ type WandbAppSpec struct { // +optional Notifications *NotificationsSpec `json:"notifications,omitempty"` + // Security Flag Configurations + // +optional + Security SecuritySpec `json:"security,omitempty"` + // LegacyOverrides holds env/resource overrides extracted from v1 // spec.values, keyed by manifest application name plus the reserved // "global" key (env only, applied to every application). Unknown keys are @@ -486,6 +490,26 @@ type OidcSpec struct { SessionLength string `json:"sessionLength,omitempty"` } +type SecuritySpec struct { + // +kubebuilder:default=false + AllowUserTeamCreation bool `json:"allowUserTeamCreation,omitempty"` + + // +kubebuilder:default=false + DisableCodeSaving bool `json:"disableCodeSaving,omitempty"` + + // +kubebuilder:default=false + AllowAnonymousPublicProjects bool `json:"allowAnonymousPublicProjects,omitempty"` + + // +kubebuilder:default=false + DisableSSOProvisioning bool `json:"disableSSOProvisioning,omitempty"` + + // +kubebuilder:default=false + InsecureAllowAPIKeyAdminAccess bool `json:"insecureAllowAPIKeyAdminAccess,omitempty"` + + // +kubebuilder:default=false + HideUpgradeBanner bool `json:"hideUpgradeBanner,omitempty"` +} + type NotificationsSpec struct { Email *EmailSpec `json:"email,omitempty"` Slack *SlackSpec `json:"slack,omitempty"` @@ -646,8 +670,10 @@ type KafkaReplicationConfig struct { // ObjectStoreSpec defines the desired state of the object store infrastructure component. type ObjectStoreSpec struct { - ManagedObjectStore *ManagedObjectStoreSpec `json:"managedObjectStore,omitempty"` - ExternalObjectStore *ObjectStoreConnection `json:"externalObjectStore,omitempty"` + // +kubebuilder:default=false + BucketAttributionDisabled bool `json:"bucketAttributionDisabled,omitempty"` + ManagedObjectStore *ManagedObjectStoreSpec `json:"managedObjectStore,omitempty"` + ExternalObjectStore *ObjectStoreConnection `json:"externalObjectStore,omitempty"` } type ManagedObjectStoreSpec struct { @@ -797,7 +823,7 @@ type WeightsAndBiasesStatus struct { ObjectStoreStatus map[string]ObjectStoreInfraStatus `json:"objectStoreStatus,omitempty"` ClickHouseStatus map[string]ClickHouseInfraStatus `json:"clickhouseStatus,omitempty"` TelemetryStatus TelemetryInfraStatus `json:"telemetryStatus,omitempty"` - EmailSink *corev1.SecretKeySelector `json:"emailSink,omitempty"` + EmailSink *corev1.SecretKeySelector `json:"emailSink,omitempty"` // GeneratedSecrets stores references to secrets generated by the operator // from the server manifest's generatedSecrets section. The key is the // logical secret name from the manifest, and the value is a SecretKeySelector diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index ddb22263..63b16718 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -1458,6 +1458,21 @@ func (in *SecretRef) DeepCopy() *SecretRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecuritySpec) DeepCopyInto(out *SecuritySpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecuritySpec. +func (in *SecuritySpec) DeepCopy() *SecuritySpec { + if in == nil { + return nil + } + out := new(SecuritySpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceAccountSpec) DeepCopyInto(out *ServiceAccountSpec) { *out = *in @@ -1615,6 +1630,7 @@ func (in *WandbAppSpec) DeepCopyInto(out *WandbAppSpec) { *out = new(NotificationsSpec) (*in).DeepCopyInto(*out) } + out.Security = in.Security if in.LegacyOverrides != nil { in, out := &in.LegacyOverrides, &out.LegacyOverrides *out = make(map[string]LegacyOverrides, len(*in)) diff --git a/config/crd/bases/apps.wandb.com_weightsandbiases.yaml b/config/crd/bases/apps.wandb.com_weightsandbiases.yaml index 1c42296d..6df9d6b8 100644 --- a/config/crd/bases/apps.wandb.com_weightsandbiases.yaml +++ b/config/crd/bases/apps.wandb.com_weightsandbiases.yaml @@ -2576,6 +2576,9 @@ spec: objectStore: additionalProperties: properties: + bucketAttributionDisabled: + default: false + type: boolean externalObjectStore: properties: accessKey: @@ -4499,6 +4502,27 @@ spec: type: integer type: object type: object + security: + properties: + allowAnonymousPublicProjects: + default: false + type: boolean + allowUserTeamCreation: + default: false + type: boolean + disableCodeSaving: + default: false + type: boolean + disableSSOProvisioning: + default: false + type: boolean + hideUpgradeBanner: + default: false + type: boolean + insecureAllowAPIKeyAdminAccess: + default: false + type: boolean + type: object serviceAccount: properties: annotations: diff --git a/internal/controller/reconciler/security_flags_env_test.go b/internal/controller/reconciler/security_flags_env_test.go new file mode 100644 index 00000000..5ec377c6 --- /dev/null +++ b/internal/controller/reconciler/security_flags_env_test.go @@ -0,0 +1,73 @@ +package reconciler + +import ( + "context" + "testing" + + apiv2 "github.com/wandb/operator/api/v2" + serverManifest "github.com/wandb/operator/pkg/wandb/manifest" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestResolveEnvvarsCustomResourceSecurityFlags(t *testing.T) { + wandb := &apiv2.WeightsAndBiases{ + ObjectMeta: metav1.ObjectMeta{Name: "wandb", Namespace: "default"}, + } + wandb.Spec.Wandb.Security = apiv2.SecuritySpec{ + AllowUserTeamCreation: true, + DisableCodeSaving: true, + AllowAnonymousPublicProjects: true, + DisableSSOProvisioning: true, + InsecureAllowAPIKeyAdminAccess: true, + HideUpgradeBanner: true, + } + wandb.Spec.ObjectStore = map[string]apiv2.ObjectStoreSpec{ + apiv2.DefaultInstanceName: {BucketAttributionDisabled: true}, + } + + tests := []struct { + name string + field string + }{ + {"GORILLA_ALLOW_USER_TEAM_CREATION", "spec.wandb.security.allowUserTeamCreation"}, + {"GORILLA_DISABLE_CODE_SAVING", "spec.wandb.security.disableCodeSaving"}, + {"GORILLA_ALLOW_ANONYMOUS_PUBLIC_PROJECTS", "spec.wandb.security.allowAnonymousPublicProjects"}, + {"GORILLA_DISABLE_SSO_PROVISIONING", "spec.wandb.security.disableSSOProvisioning"}, + {"GORILLA_INSECURE_ALLOW_API_KEY_ADMIN_ACCESS", "spec.wandb.security.insecureAllowAPIKeyAdminAccess"}, + {"GORILLA_BUCKET_ATTRIBUTION_DISABLED", "spec.objectStore.default.bucketAttributionDisabled"}, + {"GORILLA_ONPREM_HIDE_UPGRADE_BANNER", "spec.wandb.security.hideUpgradeBanner"}, + } + + envs := make([]serverManifest.EnvVar, 0, len(tests)) + for _, tc := range tests { + envs = append(envs, serverManifest.EnvVar{ + Name: tc.name, + Sources: []serverManifest.EnvSource{{ + Type: "custom-resource", Field: tc.field, + }}, + }) + } + + resolved, err := resolveEnvvars( + context.Background(), + fake.NewClientBuilder().Build(), + wandb, + serverManifest.Manifest{}, + nil, + envs, + ) + if err != nil { + t.Fatal(err) + } + + for _, tc := range tests { + env := mustFindEnvVar(t, resolved, tc.name) + if env.Value != "true" { + t.Errorf("expected %s=true, got %q", tc.name, env.Value) + } + if env.ValueFrom != nil { + t.Errorf("expected %s to be a plain value, got valueFrom %+v", tc.name, env.ValueFrom) + } + } +} diff --git a/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml b/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml index 1c42296d..6df9d6b8 100644 --- a/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml +++ b/internal/crdinstaller/crds/operator/apps.wandb.com_weightsandbiases.yaml @@ -2576,6 +2576,9 @@ spec: objectStore: additionalProperties: properties: + bucketAttributionDisabled: + default: false + type: boolean externalObjectStore: properties: accessKey: @@ -4499,6 +4502,27 @@ spec: type: integer type: object type: object + security: + properties: + allowAnonymousPublicProjects: + default: false + type: boolean + allowUserTeamCreation: + default: false + type: boolean + disableCodeSaving: + default: false + type: boolean + disableSSOProvisioning: + default: false + type: boolean + hideUpgradeBanner: + default: false + type: boolean + insecureAllowAPIKeyAdminAccess: + default: false + type: boolean + type: object serviceAccount: properties: annotations: