diff --git a/docs/release-notes/2_18_0_summary.md b/docs/release-notes/2_18_0_summary.md new file mode 100644 index 000000000..a0b2b184d --- /dev/null +++ b/docs/release-notes/2_18_0_summary.md @@ -0,0 +1,13 @@ +## Major Changes + +### Table of Contents + +- **[VTOrc `--cell` flag auto-applied on Vitess v24+](#vtorc-cell-flag)** + +### VTOrc `--cell` flag auto-applied on Vitess v24+ + +VTOrc deployments now receive `--cell=` automatically when the configured image tag parses to Vitess v24 or newer. This is required for Vitess v25+ where `--cell` is a hard requirement ([vitessio/vitess#20048](https://github.com/vitessio/vitess/pull/20048)) and was introduced in v24 ([vitessio/vitess#19047](https://github.com/vitessio/vitess/pull/19047)). + +The flag is **only** emitted when the version is parseable from the image tag (e.g. `vitess/lite:v24.0.0-mysql80`). Rolling tags such as `vitess/lite:mysql80` or `vitess/lite:latest`, or digest-only references, do not get the flag — pin a versioned tag, or set `cell` explicitly via `vitessOrchestrator.extraFlags` if you need it. + +Pre-v24 users are unaffected. diff --git a/go.mod b/go.mod index e1c38f1b8..2be1392c7 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.26.2 require ( github.com/ahmetb/gen-crd-api-reference-docs v0.3.0 + github.com/blang/semver/v4 v4.0.0 github.com/google/uuid v1.6.0 github.com/planetscale/operator-sdk-libs v0.0.0-20220216002626-1af183733234 github.com/prometheus/client_golang v1.23.2 @@ -85,7 +86,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect github.com/aws/smithy-go v1.24.3 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/blang/semver/v4 v4.0.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect diff --git a/pkg/operator/vitess/version.go b/pkg/operator/vitess/version.go new file mode 100644 index 000000000..f66a57336 --- /dev/null +++ b/pkg/operator/vitess/version.go @@ -0,0 +1,46 @@ +/* +Copyright 2026 PlanetScale Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vitess + +import ( + "strings" + + "github.com/blang/semver/v4" +) + +// MajorVersionFromImage parses the Vitess major version from a Docker image +// reference like "vitess/lite:v24.0.0-mysql80". Returns (major, true) when +// the image carries a SemVer-compatible tag and (0, false) otherwise +// (rolling tags such as "mysql80" or "latest", digests, or empty input). +func MajorVersionFromImage(image string) (int, bool) { + if image == "" { + return 0, false + } + // Drop digest portion if present (e.g. "repo:tag@sha256:..."). + if at := strings.IndexByte(image, '@'); at >= 0 { + image = image[:at] + } + colon := strings.LastIndexByte(image, ':') + if colon < 0 { + return 0, false + } + v, err := semver.ParseTolerant(image[colon+1:]) + if err != nil { + return 0, false + } + return int(v.Major), true +} diff --git a/pkg/operator/vitess/version_test.go b/pkg/operator/vitess/version_test.go new file mode 100644 index 000000000..471586fa0 --- /dev/null +++ b/pkg/operator/vitess/version_test.go @@ -0,0 +1,119 @@ +/* +Copyright 2026 PlanetScale Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vitess + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMajorVersionFromImage(t *testing.T) { + tests := []struct { + name string + image string + want int + wantOK bool + }{ + { + name: "clean v24 tag", + image: "vitess/lite:v24.0.0", + want: 24, + wantOK: true, + }, + { + name: "v24 rc with mysql suffix", + image: "vitess/lite:v24.0.0-rc1-mysql80", + want: 24, + wantOK: true, + }, + { + name: "v23 with mysql suffix", + image: "vitess/lite:v23.0.5-mysql80", + want: 23, + wantOK: true, + }, + { + name: "large major version", + image: "vitess/lite:v100.2.3", + want: 100, + wantOK: true, + }, + { + name: "tag and digest both present", + image: "vitess/lite:v24.0.0@sha256:abc", + want: 24, + wantOK: true, + }, + { + name: "registry prefix with version", + image: "registry.example.com/vitess/lite:v25.0.0-mysql80", + want: 25, + wantOK: true, + }, + { + name: "rolling mysql tag", + image: "vitess/lite:mysql80", + want: 0, + wantOK: false, + }, + { + name: "latest tag", + image: "vitess/lite:latest", + want: 0, + wantOK: false, + }, + { + name: "digest only", + image: "vitess/lite@sha256:abc123", + want: 0, + wantOK: false, + }, + { + name: "no tag", + image: "vitess/lite", + want: 0, + wantOK: false, + }, + { + name: "empty", + image: "", + want: 0, + wantOK: false, + }, + { + name: "tag without v prefix still parses as semver", + image: "vitess/lite:24.0.0", + want: 24, + wantOK: true, + }, + { + name: "tag with only major still parses (tolerant)", + image: "vitess/lite:v24", + want: 24, + wantOK: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, ok := MajorVersionFromImage(tc.image) + assert.Equal(t, tc.wantOK, ok) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/pkg/operator/vtorc/deployment.go b/pkg/operator/vtorc/deployment.go index 013328171..26ad54143 100644 --- a/pkg/operator/vtorc/deployment.go +++ b/pkg/operator/vtorc/deployment.go @@ -210,7 +210,7 @@ func UpdateDeployment(obj *appsv1.Deployment, spec *Spec) { } func (spec *Spec) flags() vitess.Flags { - return vitess.Flags{ + flags := vitess.Flags{ "topo_implementation": spec.GlobalLockserver.Implementation, "topo_global_server_address": spec.GlobalLockserver.Address, "topo_global_root": spec.GlobalLockserver.RootPath, @@ -220,4 +220,11 @@ func (spec *Spec) flags() vitess.Flags { "logtostderr": true, } + // --cell was introduced in Vitess v24 and is required in v25+. Only emit + // it when we can confirm the image is on a supported version; on rolling + // or unparseable tags users can still force it via ExtraFlags. + if major, ok := vitess.MajorVersionFromImage(spec.Image); ok && major >= 24 { + flags["cell"] = spec.Cell + } + return flags } diff --git a/pkg/operator/vtorc/deployment_test.go b/pkg/operator/vtorc/deployment_test.go new file mode 100644 index 000000000..35a2f584a --- /dev/null +++ b/pkg/operator/vtorc/deployment_test.go @@ -0,0 +1,122 @@ +/* +Copyright 2026 PlanetScale Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package vtorc + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + planetscalev2 "planetscale.dev/vitess-operator/pkg/apis/planetscale/v2" +) + +func newSpecForTest(image, cell string) *Spec { + return &Spec{ + GlobalLockserver: planetscalev2.VitessLockserverParams{ + Implementation: "etcd2", + Address: "etcd:2379", + RootPath: "/vitess/global", + }, + Keyspace: "commerce", + Shard: "-", + Cell: cell, + Image: image, + } +} + +func TestSpecFlagsAlwaysIncludesBaseFlags(t *testing.T) { + spec := newSpecForTest("vitess/lite:v24.0.0-mysql80", "zone1") + flags := spec.flags() + + for _, key := range []string{ + "topo_implementation", + "topo_global_server_address", + "topo_global_root", + "port", + "clusters_to_watch", + "logtostderr", + } { + _, ok := flags[key] + assert.Truef(t, ok, "expected base flag %q to be present", key) + } + + assert.Equal(t, "commerce/-", flags["clusters_to_watch"]) +} + +func TestSpecFlagsCellGatedByVitessVersion(t *testing.T) { + tests := []struct { + name string + image string + wantCell bool + }{ + { + name: "v24 emits cell", + image: "vitess/lite:v24.0.0-mysql80", + wantCell: true, + }, + { + name: "v25 emits cell", + image: "vitess/lite:v25.0.0-mysql80", + wantCell: true, + }, + { + name: "v24 rc emits cell", + image: "vitess/lite:v24.0.0-rc1-mysql80", + wantCell: true, + }, + { + name: "v23 does not emit cell", + image: "vitess/lite:v23.0.5-mysql80", + wantCell: false, + }, + { + name: "v22 does not emit cell", + image: "vitess/lite:v22.0.0-mysql80", + wantCell: false, + }, + { + name: "rolling mysql tag does not emit cell", + image: "vitess/lite:mysql80", + wantCell: false, + }, + { + name: "latest tag does not emit cell", + image: "vitess/lite:latest", + wantCell: false, + }, + { + name: "empty image does not emit cell", + image: "", + wantCell: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + spec := newSpecForTest(tc.image, "zone1") + flags := spec.flags() + + got, present := flags["cell"] + if tc.wantCell { + assert.True(t, present, "expected --cell flag to be present") + assert.Equal(t, "zone1", got) + } else { + assert.False(t, present, "expected --cell flag to be absent, got %v", got) + } + }) + } +} diff --git a/test/endtoend/upgrade_test.sh b/test/endtoend/upgrade_test.sh index ad469212f..f2d1809fd 100755 --- a/test/endtoend/upgrade_test.sh +++ b/test/endtoend/upgrade_test.sh @@ -271,6 +271,9 @@ checkSemiSyncSetup checkMysqldExporterMetrics # Initially too durability policy should be specified verifyDurabilityPolicy "commerce" "semi_sync" +# VTOrc --cell is gated on Vitess v24+ in the operator. The initial cluster +# pins v24.0.0-rc1, so the flag must be on the vtorc pod. +checkPodSpecBySelectorWithTimeout example "planetscale.com/component=vtorc" 1 "--cell=zone1" upgradeToLatest verifyVtGateVersion "25.0.0" verifyResourceSpec