Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 114 additions & 24 deletions api/v1/weightsandbiases_conversion_mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func applyValueMappings(src *WeightsAndBiases, dst *appsv2.WeightsAndBiases) err
if err := mapVersion(values, dst); err != nil {
return err
}
if err := mapServiceAccountAnnotations(values, dst); err != nil {
if err := mapServiceAccount(values, dst); err != nil {
return err
}
if err := mapInternalJWTIssuer(values, dst); err != nil {
Expand Down Expand Up @@ -166,26 +166,128 @@ func mapVersion(values map[string]interface{}, dst *appsv2.WeightsAndBiases) err
return nil
}

// mapServiceAccountAnnotations maps v1's per-sub-chart ServiceAccount
// annotations to v2's single spec.wandb.serviceAccount.annotations,
// preferring `app` and falling back to `api`.
func mapServiceAccountAnnotations(values map[string]interface{}, dst *appsv2.WeightsAndBiases) error {
anns, err := readServiceAccountAnnotations(values, "app")
// v1ServiceAccountSubcharts are the v1 subcharts whose serviceAccount block
// describes the W&B application identity, in precedence order. Infra subcharts
// (mysql, redis, …) are deliberately excluded: their serviceAccount blocks
// configure those workloads, and v2 models them separately as
// ManagedServiceAccountSpec.
var v1ServiceAccountSubcharts = []string{"app", "api"}

// v1ServiceAccount is one subchart's serviceAccount block.
type v1ServiceAccount struct {
subchart string
create *bool
name string
annotations map[string]string
}

// mapServiceAccount maps v1's per-subchart ServiceAccount blocks to v2's single
// spec.wandb.serviceAccount.
//
// create and name must be carried together: v2's CRD defaults create=true and
// serviceAccountName=wandb, so dropping either one makes the operator stand up
// its own ServiceAccount and orphan the identity the deployment's cloud IAM
// binding is attached to. Carrying create without name is worse still — pods
// would reference a ServiceAccount nothing creates and fail admission.
func mapServiceAccount(values map[string]interface{}, dst *appsv2.WeightsAndBiases) error {
blocks, err := readV1ServiceAccounts(values)
if err != nil {
return err
}
if len(anns) == 0 {
anns, err = readServiceAccountAnnotations(values, "api")
if err != nil {
return err
if len(blocks) == 0 {
return nil
}
if err := assertServiceAccountAgreement(blocks); err != nil {
return err
}

// First non-empty wins, so earlier subcharts take precedence.
sa := &dst.Spec.Wandb.ServiceAccount
for _, block := range blocks {
if block.create != nil && sa.Create == nil {
sa.Create = ptr.To(*block.create)
}
if block.name != "" && sa.ServiceAccountName == "" {
sa.ServiceAccountName = block.name
}
if len(block.annotations) > 0 && len(sa.Annotations) == 0 {
sa.Annotations = block.annotations
}
}
Comment on lines +206 to 216

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Pair create with name instead of applying them independently.

The loop applies create and name as separate fields. A v1 block that sets create: false and omits name therefore produces Create=false with an empty ServiceAccountName, and the v2 CRD then defaults the name to wandb. Pods reference a wandb ServiceAccount that nothing creates. The function doc comment describes this exact failure mode.

The same independence lets create come from app while name comes from api, which combines two subcharts into one identity that neither declared.

Reject or ignore an incomplete identity instead. Example: fail conversion when a block sets create=false without a name, so the operator asks for an explicit v2 value.

🐛 Proposed fix: carry create and name from the same block
 	// First non-empty wins, so earlier subcharts take precedence.
 	sa := &dst.Spec.Wandb.ServiceAccount
 	for _, block := range blocks {
+		// create=false without a name is unrepresentable in v2: the CRD would
+		// default the name to wandb and pods would reference a ServiceAccount
+		// nothing creates.
+		if block.create != nil && !*block.create && block.name == "" {
+			return fmt.Errorf(
+				"spec.values.%s.serviceAccount: create=false without name; "+
+					"set spec.wandb.serviceAccount.serviceAccountName explicitly",
+				block.subchart)
+		}
 		if block.create != nil && sa.Create == nil {
 			sa.Create = ptr.To(*block.create)
 		}

Add a test for create: false with no name, and a test for create in app with name only in api.

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

In `@api/v1/weightsandbiases_conversion_mapping.go` around lines 206 - 216, Update
the block-processing logic in the conversion function so create and name are
treated as one identity from the same block, rather than merged independently
across blocks. Reject or ignore incomplete identities, especially create=false
without a name, so conversion requires an explicit v2 value instead of producing
a default ServiceAccount name; preserve valid paired values from a single block.
Add coverage for create=false without name and for create in app with name only
in api.

if len(anns) > 0 {
dst.Spec.Wandb.ServiceAccount.Annotations = anns
return nil
}

// assertServiceAccountAgreement fails when subcharts disagree on the identity.
// v2 has one application ServiceAccount, so silently picking a winner would
// point pods at the wrong cloud identity — the exact failure this mapper
// exists to prevent.
func assertServiceAccountAgreement(blocks []v1ServiceAccount) error {
var nameOwner, createOwner *v1ServiceAccount
for i := range blocks {
block := &blocks[i]
if block.name != "" {
if nameOwner != nil && nameOwner.name != block.name {
return fmt.Errorf(
"spec.values: %s.serviceAccount.name=%q conflicts with %s.serviceAccount.name=%q; "+
"v2 has a single spec.wandb.serviceAccount, so set it explicitly",
nameOwner.subchart, nameOwner.name, block.subchart, block.name)
}
if nameOwner == nil {
nameOwner = block
}
}
if block.create != nil {
if createOwner != nil && *createOwner.create != *block.create {
return fmt.Errorf(
"spec.values: %s.serviceAccount.create=%v conflicts with %s.serviceAccount.create=%v; "+
"v2 has a single spec.wandb.serviceAccount, so set it explicitly",
createOwner.subchart, *createOwner.create, block.subchart, *block.create)
}
if createOwner == nil {
createOwner = block
}
}
}
return nil
}

// readV1ServiceAccounts collects the serviceAccount block from each known
// subchart that sets one, preserving v1ServiceAccountSubcharts order.
func readV1ServiceAccounts(values map[string]interface{}) ([]v1ServiceAccount, error) {
var out []v1ServiceAccount
for _, subchart := range v1ServiceAccountSubcharts {
saMap, found, err := unstructured.NestedMap(values, subchart, "serviceAccount")
if err != nil {
return nil, fmt.Errorf("spec.values.%s.serviceAccount: %w", subchart, err)
}
if !found || len(saMap) == 0 {
continue
}

block := v1ServiceAccount{subchart: subchart}

// Non-boolean create is treated as unset rather than failing, so a
// stringly-typed helm value can't make a v1 object unservable.
if raw, ok := saMap["create"]; ok {
if s, isScalar := scalarToString(raw); isScalar {
if parsed, parseErr := strconv.ParseBool(s); parseErr == nil {
block.create = ptr.To(parsed)
}
}
}

if block.name, _, err = unstructured.NestedString(saMap, "name"); err != nil {
return nil, fmt.Errorf("spec.values.%s.serviceAccount.name: %w", subchart, err)
}
if block.annotations, _, err = unstructured.NestedStringMap(saMap, "annotations"); err != nil {
return nil, fmt.Errorf("spec.values.%s.serviceAccount.annotations: %w", subchart, err)
}

out = append(out, block)
}
return out, nil
}

// mapInternalJWTIssuer pulls the first entry from global.internalJWTMap (or
// app as fallback) into spec.wandb.internalServiceAuth.oidcIssuer.
func mapInternalJWTIssuer(values map[string]interface{}, dst *appsv2.WeightsAndBiases) error {
Expand Down Expand Up @@ -322,18 +424,6 @@ func readFirstInternalJWTIssuer(values map[string]interface{}, service string) (
return issuer, nil
}

// readServiceAccountAnnotations reads values.<service>.serviceAccount.annotations.
func readServiceAccountAnnotations(values map[string]interface{}, service string) (map[string]string, error) {
anns, found, err := unstructured.NestedStringMap(values, service, "serviceAccount", "annotations")
if err != nil {
return nil, fmt.Errorf("spec.values.%s.serviceAccount.annotations: %w", service, err)
}
if !found {
return nil, nil
}
return anns, nil
}

func mapHostnameLicense(globalMap map[string]interface{}, dst *appsv2.WeightsAndBiases) error {
host, _, err := unstructured.NestedString(globalMap, "host")
if err != nil {
Expand Down
179 changes: 179 additions & 0 deletions api/v1/weightsandbiases_conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,185 @@ func TestConvertTo_ServiceAccountAnnotationsEmptyAppFallsBackToApi(t *testing.T)
"empty app annotations should not consume the slot; api fallback applies")
}

// TestConvertTo_ServiceAccountCreateFalseAndName: v2 defaults create=true and
// serviceAccountName=wandb, so both must be carried or the operator stands up
// its own ServiceAccount and orphans the v1 cloud identity.
func TestConvertTo_ServiceAccountCreateFalseAndName(t *testing.T) {
dst := &appsv2.WeightsAndBiases{}
src := newV1(map[string]interface{}{
"app": map[string]interface{}{
"serviceAccount": map[string]interface{}{
"create": false,
"name": "my-existing-sa",
"annotations": map[string]interface{}{
"eks.amazonaws.com/role-arn": "arn:aws:iam::123456789012:role/wandb",
},
},
},
})
require.NoError(t, src.ConvertTo(dst))

sa := dst.Spec.Wandb.ServiceAccount
require.NotNil(t, sa.Create, "create must be set explicitly; nil defaults to true")
require.False(t, *sa.Create)
require.Equal(t, "my-existing-sa", sa.ServiceAccountName)
require.Equal(t, "arn:aws:iam::123456789012:role/wandb",
sa.Annotations["eks.amazonaws.com/role-arn"])
}

func TestConvertTo_ServiceAccountCreateTrueWithName(t *testing.T) {
dst := &appsv2.WeightsAndBiases{}
src := newV1(map[string]interface{}{
"app": map[string]interface{}{
"serviceAccount": map[string]interface{}{
"create": true,
"name": "wandb-custom",
},
},
})
require.NoError(t, src.ConvertTo(dst))

require.NotNil(t, dst.Spec.Wandb.ServiceAccount.Create)
require.True(t, *dst.Spec.Wandb.ServiceAccount.Create)
require.Equal(t, "wandb-custom", dst.Spec.Wandb.ServiceAccount.ServiceAccountName)
}

// TestConvertTo_ServiceAccountFallsBackToApi: api supplies the identity when
// app has no serviceAccount block at all.
func TestConvertTo_ServiceAccountFallsBackToApi(t *testing.T) {
dst := &appsv2.WeightsAndBiases{}
src := newV1(map[string]interface{}{
"api": map[string]interface{}{
"serviceAccount": map[string]interface{}{
"create": false,
"name": "api-sa",
},
},
})
require.NoError(t, src.ConvertTo(dst))

require.False(t, *dst.Spec.Wandb.ServiceAccount.Create)
require.Equal(t, "api-sa", dst.Spec.Wandb.ServiceAccount.ServiceAccountName)
}

// TestConvertTo_ServiceAccountAgreeingSubchartsOK: identical blocks are not a
// conflict.
func TestConvertTo_ServiceAccountAgreeingSubchartsOK(t *testing.T) {
dst := &appsv2.WeightsAndBiases{}
src := newV1(map[string]interface{}{
"app": map[string]interface{}{
"serviceAccount": map[string]interface{}{"create": false, "name": "shared-sa"},
},
"api": map[string]interface{}{
"serviceAccount": map[string]interface{}{"create": false, "name": "shared-sa"},
},
})
require.NoError(t, src.ConvertTo(dst))

require.False(t, *dst.Spec.Wandb.ServiceAccount.Create)
require.Equal(t, "shared-sa", dst.Spec.Wandb.ServiceAccount.ServiceAccountName)
}

// TestConvertTo_ServiceAccountConflictingNamesFails: v2 has one application
// ServiceAccount, so two names are unrepresentable.
func TestConvertTo_ServiceAccountConflictingNamesFails(t *testing.T) {
dst := &appsv2.WeightsAndBiases{}
src := newV1(map[string]interface{}{
"app": map[string]interface{}{
"serviceAccount": map[string]interface{}{"name": "app-sa"},
},
"api": map[string]interface{}{
"serviceAccount": map[string]interface{}{"name": "api-sa"},
},
})
err := src.ConvertTo(dst)
require.Error(t, err)
require.Contains(t, err.Error(), "app-sa")
require.Contains(t, err.Error(), "api-sa")
require.Contains(t, err.Error(), "single spec.wandb.serviceAccount")
}

func TestConvertTo_ServiceAccountConflictingCreateFails(t *testing.T) {
dst := &appsv2.WeightsAndBiases{}
src := newV1(map[string]interface{}{
"app": map[string]interface{}{
"serviceAccount": map[string]interface{}{"create": false, "name": "shared-sa"},
},
"api": map[string]interface{}{
"serviceAccount": map[string]interface{}{"create": true, "name": "shared-sa"},
},
})
err := src.ConvertTo(dst)
require.Error(t, err)
require.Contains(t, err.Error(), "serviceAccount.create")
}

// TestConvertTo_ServiceAccountInfraSubchartsIgnored: infra serviceAccount
// blocks configure those workloads, not the application identity.
func TestConvertTo_ServiceAccountInfraSubchartsIgnored(t *testing.T) {
dst := &appsv2.WeightsAndBiases{}
src := newV1(map[string]interface{}{
"mysql": map[string]interface{}{
"serviceAccount": map[string]interface{}{"create": false, "name": "mysql-sa"},
},
"redis": map[string]interface{}{
"serviceAccount": map[string]interface{}{"create": false, "name": "redis-sa"},
},
})
require.NoError(t, src.ConvertTo(dst), "infra subcharts must not trigger a conflict")

require.Nil(t, dst.Spec.Wandb.ServiceAccount.Create)
require.Empty(t, dst.Spec.Wandb.ServiceAccount.ServiceAccountName)
}

// TestConvertTo_ServiceAccountCreateStringBool: helm values are frequently
// stringly-typed.
func TestConvertTo_ServiceAccountCreateStringBool(t *testing.T) {
dst := &appsv2.WeightsAndBiases{}
src := newV1(map[string]interface{}{
"app": map[string]interface{}{
"serviceAccount": map[string]interface{}{"create": "false", "name": "my-sa"},
},
})
require.NoError(t, src.ConvertTo(dst))

require.NotNil(t, dst.Spec.Wandb.ServiceAccount.Create)
require.False(t, *dst.Spec.Wandb.ServiceAccount.Create)
}

// TestConvertTo_ServiceAccountCreateNonBoolIsUnset: an uninterpretable flag
// must not make a v1 object unservable.
func TestConvertTo_ServiceAccountCreateNonBoolIsUnset(t *testing.T) {
dst := &appsv2.WeightsAndBiases{}
src := newV1(map[string]interface{}{
"app": map[string]interface{}{
"serviceAccount": map[string]interface{}{
"create": map[string]interface{}{"nested": "nonsense"},
"name": "my-sa",
},
},
})
require.NoError(t, src.ConvertTo(dst))

require.Nil(t, dst.Spec.Wandb.ServiceAccount.Create, "unparseable create is unset")
require.Equal(t, "my-sa", dst.Spec.Wandb.ServiceAccount.ServiceAccountName)
}

// TestConvertTo_ServiceAccountNameOnlyNoCreate: name without create leaves
// create unset so the v2 default (true) applies — v1's own chart default.
func TestConvertTo_ServiceAccountNameOnlyNoCreate(t *testing.T) {
dst := &appsv2.WeightsAndBiases{}
src := newV1(map[string]interface{}{
"app": map[string]interface{}{
"serviceAccount": map[string]interface{}{"name": "named-sa"},
},
})
require.NoError(t, src.ConvertTo(dst))

require.Nil(t, dst.Spec.Wandb.ServiceAccount.Create)
require.Equal(t, "named-sa", dst.Spec.Wandb.ServiceAccount.ServiceAccountName)
}

func TestConvertTo_ServiceAccountAnnotationsAbsent(t *testing.T) {
dst := &appsv2.WeightsAndBiases{}
src := newV1(map[string]interface{}{
Expand Down
37 changes: 37 additions & 0 deletions internal/controller/reconciler/internal_service_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package reconciler

import (
v2 "github.com/wandb/operator/api/v2"
"github.com/wandb/operator/pkg/utils"
)

// serviceAccountIssuerUnknownReason marks a CR that can't be reconciled because
// the cluster's service-account issuer is neither configured nor discoverable.
const serviceAccountIssuerUnknownReason = "ServiceAccountIssuerUnknown"

const serviceAccountIssuerUnknownMessage = "could not determine the cluster service-account issuer: " +
"set spec.wandb.internalServiceAuth.oidcIssuer to the value of " +
"`kubectl get --raw /.well-known/openid-configuration`, or grant the operator get on that URL " +
"and restart it"
Comment on lines +8 to +15

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg '(^|/)internal/controller/reconciler/internal_service_auth\.go$|(^|/)suite_test\.go$|(^|/)Makefile$' || true

echo "== file excerpt =="
sed -n '1,160p' internal/controller/reconciler/internal_service_auth.go

echo "== references in tracked files =="
rg -n "serviceAccountIssuerUnknownReason|serviceAccountIssuerUnknownMessage|ServiceAccountIssuerUnknown|internalServiceAuthIssuerUnknown|internalServiceAuthIssuer" . --glob '!vendor/**' --glob '!node_modules/**' || true

echo "== lint config excerpt =="
for f in .golangci.yml .golangci.yaml golangci.yml golangci.yaml; do
  if [ -f "$f" ]; then
    echo "-- $f --"
    sed -n '1,220p' "$f"
  fi
done

echo "== golangci linters list if present =="
python3 - <<'PY'
from pathlib import Path
import re, yaml, json, os
for p in [Path(".golangci.yml"), Path(".golangci.yaml"), Path("golangci.yml"), Path("golangci.yaml")]:
    if p.exists():
        data=yaml.safe_load(p.read_text())
        print("golangci.yml linters configuration:", data.get("linters") if data else None)
        print("linters.enable:", (data.get("linters") or {}).get("enable"))
        print("linters.disble:", (data.get("linters") or {}).get("disable"))
        print("linters.defaults.enable:", ((data.get("linters") or {}).get("defaults") or {}).get("enable"))
PY

Repository: wandb/operator

Length of output: 2998


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg '(^|/)internal/controller/reconciler/internal_service_auth\.go$|(^|/)suite_test\.go$|(^|/)Makefile$' || true

echo "== file excerpt =="
sed -n '1,160p' internal/controller/reconciler/internal_service_auth.go

echo "== references in tracked files =="
rg -n "serviceAccountIssuerUnknownReason|serviceAccountIssuerUnknownMessage|ServiceAccountIssuerUnknown|internalServiceAuthIssuerUnknown|internalServiceAuthIssuer" . --glob '!vendor/**' --glob '!node_modules/**' || true

echo "== lint config excerpt =="
for f in .golangci.yml .golangci.yaml golangci.yml golangci.yaml; do
  if [ -f "$f" ]; then
    echo "-- $f --"
    sed -n '1,220p' "$f"
  fi
done

echo "== golangci linters list if present =="
python3 - <<'PY'
from pathlib import Path
import yaml
for p in [Path(".golangci.yml"), Path(".golangci.yaml"), Path("golangci.yml"), Path("golangci.yaml")]:
    if p.exists():
        data=yaml.safe_load(p.read_text())
        print("golangci.yml linters configuration:", data.get("linters") if data else None)
        print("linters.enable:", (data.get("linters") or {}).get("enable"))
        print("linters.disable:", (data.get("linters") or {}).get("disable"))
        print("linters.defaults.enable:", ((data.get("linters") or {}).get("defaults") or {}).get("enable"))
PY

Repository: wandb/operator

Length of output: 2998


🌐 Web query:

golangci-lint unused declarations error default linters unused

💡 Result:

The unused linter is included in the default set of linters enabled by golangci-lint [1][2]. It is responsible for checking Go code for unused constants, variables, functions, and types [1][3]. If you are encountering errors related to "unused declarations," it is likely because this linter is enabled by default [1]. You do not need to explicitly enable it in your configuration file. Key points regarding the unused linter: 1. Enabled by Default: It is part of the standard/default set of linters [1][2]. You can verify this by running the command: golangci-lint help linters [1][4]. 2. Functionality: It replaces older, now-deprecated linters such as varcheck, deadcode, and structcheck, consolidating their functionality into a single, maintained tool [5]. 3. Managing Errors: - If you wish to disable it, you can do so in your.golangci.yml configuration file: linters: disable: - unused - Alternatively, you can use a //nolint:unused comment to suppress the error for a specific line or block of code [6]. For a complete list of all currently enabled linters, you can execute golangci-lint help linters in your terminal [1][4].

Citations:


Remove or use the unused issuer-error constants.

serviceAccountIssuerUnknownReason and serviceAccountIssuerUnknownMessage are only declared. golangci-lint flags unused declarations by default, so make lint fails unless they are wired into the reconciliation error path or removed.

🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 10-10: const serviceAccountIssuerUnknownReason is unused

(unused)


[error] 12-12: const serviceAccountIssuerUnknownMessage is unused

(unused)

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

In `@internal/controller/reconciler/internal_service_auth.go` around lines 8 - 15,
Remove the unused serviceAccountIssuerUnknownReason and
serviceAccountIssuerUnknownMessage declarations, or wire both into the
reconciliation error path that handles an undiscoverable service-account issuer;
ensure no unused constants remain and preserve the existing issuer error
behavior.

Sources: Coding guidelines, Linters/SAST tools


// internalServiceAuthEnabled reports whether W&B services validate each other's
// projected ServiceAccount tokens.
func internalServiceAuthEnabled(wandb *v2.WeightsAndBiases) bool {
return wandb.Spec.Wandb.InternalServiceAuth.Enabled != nil &&
*wandb.Spec.Wandb.InternalServiceAuth.Enabled
}

// resolveInternalServiceAuthIssuer returns the issuer W&B services must validate
// projected ServiceAccount tokens against: the explicit CR value when set, else
// the issuer discovered from the cluster at start-up.
//
// Empty means unknown, and callers must not substitute a guess. The API server
// stamps its own --service-account-issuer as the token's `iss`, so any other
// value fails validation — which surfaces as a 401 and panics the API rather
// than degrading.
func resolveInternalServiceAuthIssuer(wandb *v2.WeightsAndBiases) string {
if wandb.Spec.Wandb.InternalServiceAuth.OIDCIssuer != "" {
return wandb.Spec.Wandb.InternalServiceAuth.OIDCIssuer
}
return utils.ServiceAccountIssuer()
}
Loading
Loading