Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 8 additions & 5 deletions api/v1/weightsandbiases_conversion_overrides.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,18 +141,21 @@ func mapLegacyOverrides(values map[string]interface{}, dst *appsv2.WeightsAndBia
return nil
}

// mapPerAppLegacyOverrides is best-effort: a manifest fetch failure must never
// make v1 objects unservable, so it logs and skips instead of erroring.
// mapPerAppLegacyOverrides fails when the server manifest can't be resolved.
// The manifest is the only authority on which values sections are applications,
// so continuing would silently discard every per-application env and resource
// override while reporting a successful conversion — the failure mode that gets
// discovered in production. Rejecting the write is recoverable; a silently
// gutted CR is not.
func mapPerAppLegacyOverrides(values map[string]interface{}, version, globalSize string, overrides map[string]appsv2.LegacyOverrides) error {
if version == "" {
logger.Info("no version derived from v1 values; skipping per-application legacy overrides")
return nil
}
apps, err := legacyManifestApps(version)
if err != nil {
logger.Error(err, "failed to resolve server manifest; skipping per-application legacy overrides",
"version", version)
return nil
return fmt.Errorf("resolve server manifest for version %q (required to map per-application "+
"env/resources overrides): %w", version, err)
}

appNames := make([]string, 0, len(apps))
Expand Down
53 changes: 46 additions & 7 deletions api/v1/weightsandbiases_conversion_overrides_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -401,13 +401,52 @@ func TestConvertTo_LegacyOverridesManifestUnavailable(t *testing.T) {
"env": map[string]interface{}{"API_VAR": "1"},
},
}))
// A manifest fetch failure must never fail conversion: global env still
// converts, per-app extraction is skipped.
// Converting anyway would drop api's env while reporting success, so the
// write is rejected instead.
err := src.ConvertTo(dst)
require.Error(t, err)
require.Contains(t, err.Error(), "resolve server manifest")
require.Contains(t, err.Error(), testLegacyVersion)
require.Contains(t, err.Error(), "registry unreachable")
}

// TestConvertTo_LegacyOverridesManifestUnavailableNoAppSections: with nothing
// per-application to lose, an unresolvable manifest is still fatal — the
// manifest decides what counts as an application, so we can't know there was
// nothing to map.
func TestConvertTo_LegacyOverridesManifestUnavailableNoAppSections(t *testing.T) {
SetConversionManifestGetter(func(_ context.Context, _, _ string) (serverManifest.Manifest, error) {
return serverManifest.Manifest{}, errors.New("registry unreachable")
})
t.Cleanup(disableConversionManifestFetch)

dst := &appsv2.WeightsAndBiases{}
src := newV1(withVersion(map[string]interface{}{
"global": map[string]interface{}{"host": "http://wandb.example.com"},
}))
require.Error(t, src.ConvertTo(dst))
}

// TestConvertTo_NoVersionSkipsManifestFetch: without a version there is nothing
// to resolve, so conversion proceeds and global env still converts.
func TestConvertTo_NoVersionSkipsManifestFetch(t *testing.T) {
var calls atomic.Int32
SetConversionManifestGetter(func(_ context.Context, _, _ string) (serverManifest.Manifest, error) {
calls.Add(1)
return serverManifest.Manifest{}, errors.New("registry unreachable")
})
t.Cleanup(disableConversionManifestFetch)

dst := &appsv2.WeightsAndBiases{}
src := newV1(map[string]interface{}{
"global": map[string]interface{}{
"env": map[string]interface{}{"HTTP_PROXY": "http://proxy"},
},
})
require.NoError(t, src.ConvertTo(dst))

overrides := dst.Spec.Wandb.LegacyOverrides
require.Contains(t, overrides, appsv2.LegacyOverridesGlobalKey)
require.NotContains(t, overrides, "api")
require.Contains(t, dst.Spec.Wandb.LegacyOverrides, appsv2.LegacyOverridesGlobalKey)
require.Equal(t, int32(0), calls.Load(), "no version means no manifest fetch")
Comment on lines +417 to +449

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

fd -a 'suite_test.go' .
rg -n -C 2 'github.com/onsi/ginkgo|github.com/onsi/gomega|RunSpecs|Describe\(|It\(' \
  -g '*_test.go' -g 'go.mod' .

Repository: wandb/operator

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- target file outline ---\n'
ast-grep outline api/v1/weightsandbiases_conversion_overrides_suite_test.go api/v1/weightsandbiases_conversion_overrides_test.go 2>/dev/null || true

printf '\n--- api/v1 suite files ---\n'
fd '_suite_test.go|test.go|weightsandbiases_conversion_overrides' api/v1 -t f | sort | sed -n '1,120p'

printf '\n--- target test imports and functions ---\n'
sed -n '1,90p' api/v1/weightsandbiases_conversion_overrides_test.go
rg -n 'func Test(Check|ConvertTo|.*LegacyOverrides|NoVersionSkips|DisableConversionManifest|SetConversionManifest)' api/v1/weightsandbiases_conversion_overrides_test.go

printf '\n--- ginkgo assertion usages in target if present ---\n'
rg -n 'g\\.' api/v1/weightsandbiases_conversion_overrides_test.go || true
rg -n 'require\\.|assert\\.|func test|var _ = Describe|var _ = DescribeTable|It\\(' api/v1/weightsandbiases_conversion_overrides_test.go || true

printf '\n--- deterministic parser-style check for assertion frameworks in target ---\n'
python3 - <<'PY'
from pathlib import Path
p=Path('api/v1/weightsandbiases_conversion_overrides_test.go')
s=p.read_text()
checks = {
    'imports_ginkgo': '. "github.com/onsi/ginkgo/v2"' in s,
    'imports_gomega': '. "github.com/onsi/gomega"' in s,
    'imports_testing': 'testing' in s,
    'has_require_nearest': True if 'require' in s else False,
    'has_test_func': True if 'func Test' in s else False,
    'uses_ginkgo_it': bool(re.search(r'\bIt\s*\(', s)) if (re := __import__('re')) else False,
    'uses_gomega_expect': bool(re.search(r'\bExpect\s*\(', s)) if (re := __import__('re')) else False,
}
for k,v in checks.items():
    print(f'{k}={v}')
print('require_count=', s.count('require.'))
print('ginkgo_functionlike_count=', s.count('var _ = '))
PY

Repository: wandb/operator

Length of output: 243


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- target file imports ---\n'
sed -n '1,80p' api/v1/weightsandbiases_conversion_overrides_test.go

printf '\n--- target test bodies around added scenarios ---\n'
sed -n '390,460p' api/v1/weightsandbiases_conversion_overrides_test.go

printf '\n--- nearby suite imports for model ---\n'
for f in api/v1/*_suite_test.go api/v1/*conversion*suite*; do
  [ -e "$f" ] || continue
  echo "--- $f ---"
  sed -n '1,40p' "$f"
done

Repository: wandb/operator

Length of output: 243


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat -n api/v1/weightsandbiases_conversion_overrides_test.go | sed -n '1,120p'
printf '\n--- target lines 390-460 ---\n'
cat -n api/v1/weightsandbiases_conversion_overrides_test.go | sed -n '390,460p'

printf '\n--- api/v1 related suite files ---\n'
fd '_suite_test.go|weightsandbiases_conversion_overrides' api/v1 -t f | sort

printf '\n--- framework usage in target test ---\n'
python3 - <<'PY'
from pathlib import Path
s = Path('api/v1/weightsandbiases_conversion_overrides_test.go').read_text()
print('imports_ginkgo=', '. "github.com/onsi/ginkgo/v2"' in s)
print('imports_gomega=', '. "github.com/onsi/gomega"' in s)
print('imports_testing=', 'testing' in s)
print('has_require=', s.count('require.') > 0)
print('has_func_test=', 'func Test' in s)
print('has_ginkgo_it=', 'var _ = Describe(' in s or 'It(' in s)
print('has_gomega_expect=', 'Expect(' in s)
PY

Repository: wandb/operator

Length of output: 8675


Use the repository suite pattern for conversion tests.

api/v1/weightsandbiases_conversion_overrides_test.go still defines func TestConvertTo_... and uses testing.T/Testify require, while the package has no Ginkgo suite. Rewrite these tests with func TestV1ConversionSuites(t *testing.T) plus RunSpecs, and replace assertions with Ginkgo/Gomega.

🤖 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_overrides_test.go` around lines 417 - 449,
Rewrite the conversion tests in weightsandbiases_conversion_overrides_test.go to
follow the repository’s suite pattern: consolidate them under
TestV1ConversionSuites(t *testing.T), initialize RunSpecs, and organize cases as
Ginkgo specs. Replace testing.T/Testify require assertions with Ginkgo/Gomega
assertions while preserving the existing manifest-fetch and conversion behavior
checks.

Source: Coding guidelines

Comment on lines 446 to +449

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 | 🟡 Minor | ⚡ Quick win

Assert the converted global environment value.

The key-presence assertion passes if conversion creates an empty global override. Assert that HTTP_PROXY and http://proxy exist in dst.Spec.Wandb.LegacyOverrides[appsv2.LegacyOverridesGlobalKey].

🤖 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_overrides_test.go` around lines 446 - 449,
Update the conversion test assertions after ConvertTo to verify the global
override value under LegacyOverridesGlobalKey contains the HTTP_PROXY entry
mapped to http://proxy, rather than only asserting that the global key exists;
retain the existing no-manifest-fetch call count assertion.

}

func TestConvertTo_LegacyOverridesManifestFailureCooldown(t *testing.T) {
Expand All @@ -426,8 +465,8 @@ func TestConvertTo_LegacyOverridesManifestFailureCooldown(t *testing.T) {
"env": map[string]interface{}{"API_VAR": "1"},
},
}))
require.NoError(t, src.ConvertTo(dst))
require.NotContains(t, dst.Spec.Wandb.LegacyOverrides, "api")
// Every attempt fails, but from the cached failure rather than a refetch.
require.Error(t, src.ConvertTo(dst))
}

require.Equal(t, int32(1), calls.Load(), "repeat conversions within the cooldown must not retry the fetch")
Expand Down
12 changes: 12 additions & 0 deletions api/v1/weightsandbiases_conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ func TestConvertTo_CustomCACerts(t *testing.T) {
}

func TestConvertTo_VersionFromAppImageTag(t *testing.T) {
// A resolvable manifest: these assert version mapping, not per-app overrides.
withConversionManifestApps(t)
dst := &appsv2.WeightsAndBiases{}
src := newV1(map[string]interface{}{
"app": map[string]interface{}{
Expand All @@ -171,6 +173,8 @@ func TestConvertTo_VersionFromAppImageTag(t *testing.T) {
}

func TestConvertTo_VersionFallsBackToApiImageTag(t *testing.T) {
// A resolvable manifest: these assert version mapping, not per-app overrides.
withConversionManifestApps(t)
dst := &appsv2.WeightsAndBiases{}
src := newV1(map[string]interface{}{
"api": map[string]interface{}{
Expand All @@ -182,6 +186,8 @@ func TestConvertTo_VersionFallsBackToApiImageTag(t *testing.T) {
}

func TestConvertTo_VersionAppWinsOverApi(t *testing.T) {
// A resolvable manifest: these assert version mapping, not per-app overrides.
withConversionManifestApps(t)
dst := &appsv2.WeightsAndBiases{}
src := newV1(map[string]interface{}{
"app": map[string]interface{}{
Expand All @@ -196,6 +202,8 @@ func TestConvertTo_VersionAppWinsOverApi(t *testing.T) {
}

func TestConvertTo_VersionEmptyAppFallsBackToApi(t *testing.T) {
// A resolvable manifest: these assert version mapping, not per-app overrides.
withConversionManifestApps(t)
dst := &appsv2.WeightsAndBiases{}
src := newV1(map[string]interface{}{
"app": map[string]interface{}{
Expand All @@ -219,6 +227,8 @@ func TestConvertTo_VersionAbsent(t *testing.T) {
}

func TestConvertTo_VersionWithoutGlobal(t *testing.T) {
// A resolvable manifest: these assert version mapping, not per-app overrides.
withConversionManifestApps(t)
dst := &appsv2.WeightsAndBiases{}
src := newV1(map[string]interface{}{
"app": map[string]interface{}{
Expand Down Expand Up @@ -1687,6 +1697,8 @@ func TestConvertFrom_NoAnnotations(t *testing.T) {
}

func TestConvertTo_ActiveSpecSecretOverridesCRValues(t *testing.T) {
// A resolvable manifest: these assert version mapping, not per-app overrides.
withConversionManifestApps(t)
withConversionReader(t, activeSpecSecret(t, "default", "wandb", map[string]interface{}{
"global": map[string]interface{}{
"host": "http://wandb.from-active-spec",
Expand Down
12 changes: 12 additions & 0 deletions internal/webhook/v2/weightsandbiases_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,18 @@ func validateWandbSpec(wandb *appsv2.WeightsAndBiases) field.ErrorList {
))
}

// v1 installs commonly ran app.image.tag=latest, which conversion copies
// verbatim. No server-manifest artifact is published under a mutable tag, so
// the manifest lookup fails and the whole reconcile aborts. Reject it at
// admission, where the message is actionable, instead of at reconcile time.
if strings.TrimSpace(wandb.Spec.Wandb.Version) == "latest" {
errors = append(errors, field.Invalid(
field.NewPath("spec").Child("wandb").Child("version"),
wandb.Spec.Wandb.Version,
"must be pinned to a published server version; no server-manifest is published for the \"latest\" tag",
))
}
Comment on lines +452 to +458

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject or normalize whitespace around server versions.

The condition trims wandb.Spec.Wandb.Version only for comparison. It preserves the raw value. Therefore, " " and " 0.83.1 " can pass admission and reach manifest resolution as invalid tags. Reject surrounding whitespace or normalize the value before conversion. Add regression tests for both cases.

Proposed validation
+	version := strings.TrimSpace(wandb.Spec.Wandb.Version)
-	if strings.TrimSpace(wandb.Spec.Wandb.Version) == "latest" {
+	if version == "latest" {
 		errors = append(errors, field.Invalid(
 			field.NewPath("spec").Child("wandb").Child("version"),
 			wandb.Spec.Wandb.Version,
 			"must be pinned to a published server version; no server-manifest is published for the \"latest\" tag",
 		))
+	} else if version != wandb.Spec.Wandb.Version {
+		errors = append(errors, field.Invalid(
+			field.NewPath("spec").Child("wandb").Child("version"),
+			wandb.Spec.Wandb.Version,
+			"must not contain leading or trailing whitespace",
+		))
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if strings.TrimSpace(wandb.Spec.Wandb.Version) == "latest" {
errors = append(errors, field.Invalid(
field.NewPath("spec").Child("wandb").Child("version"),
wandb.Spec.Wandb.Version,
"must be pinned to a published server version; no server-manifest is published for the \"latest\" tag",
))
}
version := strings.TrimSpace(wandb.Spec.Wandb.Version)
if version == "latest" {
errors = append(errors, field.Invalid(
field.NewPath("spec").Child("wandb").Child("version"),
wandb.Spec.Wandb.Version,
"must be pinned to a published server version; no server-manifest is published for the \"latest\" tag",
))
} else if version != wandb.Spec.Wandb.Version {
errors = append(errors, field.Invalid(
field.NewPath("spec").Child("wandb").Child("version"),
wandb.Spec.Wandb.Version,
"must not contain leading or trailing whitespace",
))
}
🤖 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/webhook/v2/weightsandbiases_webhook.go` around lines 455 - 461,
Update the version validation around the existing latest-tag check to reject
values with surrounding whitespace, or normalize the version before manifest
conversion so values such as " 0.83.1 " cannot reach resolution unchanged;
ensure whitespace-only values are also invalid. Add regression tests covering
both surrounding whitespace and whitespace-only server versions.


return errors
}

Expand Down
39 changes: 39 additions & 0 deletions internal/webhook/v2/weightsandbiases_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,45 @@ var _ = Describe("WeightsAndBiases Webhook", func() {
Expect(err.Error()).To(ContainSubstring("spec.wandb.hostname"))
})

It("rejects create when version is the mutable latest tag", func() {
obj.Spec.Wandb.Version = "latest"

_, err := validator.ValidateCreate(ctx, obj)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("spec.wandb.version"))
Expect(err.Error()).To(ContainSubstring("must be pinned"))
})

It("rejects a whitespace-padded latest", func() {
obj.Spec.Wandb.Version = " latest "

_, err := validator.ValidateCreate(ctx, obj)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("spec.wandb.version"))
})

It("rejects update to the latest tag", func() {
obj.Spec.Wandb.Version = "latest"

_, err := validator.ValidateUpdate(ctx, oldObj, obj)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("spec.wandb.version"))
})

It("accepts a pinned version", func() {
obj.Spec.Wandb.Version = "0.83.1"

_, err := validator.ValidateCreate(ctx, obj)
Expect(err).ToNot(HaveOccurred())
})

It("accepts an empty version (nothing pinned in v1 values)", func() {
obj.Spec.Wandb.Version = ""

_, err := validator.ValidateCreate(ctx, obj)
Expect(err).ToNot(HaveOccurred())
})

It("rejects update when hostname is missing", func() {
obj.Spec.Wandb.Hostname = ""

Expand Down
Loading