Skip to content

Commit 21ca66f

Browse files
committed
fix: reject incompatible mysqlsh before InnoDB Cluster deploy
MySQL Shell's AdminAPI refuses to manage a server whose major.minor is greater than the shell's own (e.g. mysqlsh 8.0.x + MySQL Server 8.4.x yields "Unsupported server version: AdminAPI operations in this version of MySQL Shell support MySQL Server up to version 8.0"). dbdeployer's findMysqlShell prefers <basedir>/bin/mysqlsh and falls back to $PATH; when the PATH mysqlsh is too old, the failure only surfaces deep inside init_cluster after sandboxes are already running. Parse `mysqlsh --version` up front, compare major.minor to the target server, and bail out early with a message pointing the user at https://dev.mysql.com/downloads/shell/. Unparseable --version output is logged and does not block deploy. Fixes #87.
1 parent 1e2b2e4 commit 21ca66f

3 files changed

Lines changed: 172 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
## Unreleased
22

3+
## BUGS FIXED
4+
5+
* Fail early when `mysqlsh` on PATH is too old for the target server
6+
version (e.g. mysqlsh 8.0.x + MySQL Server 8.4+), instead of letting
7+
the AdminAPI fail mid-deploy with "Unsupported server version"
8+
(issue #87). dbdeployer now parses `mysqlsh --version`, compares its
9+
major.minor to the server's, and emits an actionable error telling
10+
the user to install a matching mysqlsh.
11+
312
## DOCUMENTATION
413

514
* Recommend `sudo cp` instead of `sudo mv` in manual install steps, so

sandbox/innodb_cluster.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,56 @@ func findMysqlShell(basedir string) (string, error) {
5252
"Install it from https://dev.mysql.com/downloads/shell/", basedir)
5353
}
5454

55+
// mysqlShellVersionRegexp matches the "Ver X.Y.Z" token emitted by
56+
// `mysqlsh --version` (e.g. "mysqlsh Ver 8.0.36 for Linux on x86_64 ...").
57+
var mysqlShellVersionRegexp = regexp.MustCompile(`Ver\s+(\d+\.\d+\.\d+)`)
58+
59+
// getMysqlShellVersion runs `mysqlsh --version` and returns the parsed
60+
// X.Y.Z version string.
61+
func getMysqlShellVersion(mysqlshPath string) (string, error) {
62+
out, err := common.RunCmdCtrlWithArgs(mysqlshPath, []string{"--version"}, true)
63+
if err != nil {
64+
return "", fmt.Errorf("running '%s --version': %s", mysqlshPath, err)
65+
}
66+
m := mysqlShellVersionRegexp.FindStringSubmatch(out)
67+
if len(m) < 2 {
68+
return "", fmt.Errorf("could not parse mysqlsh version from: %s", out)
69+
}
70+
return m[1], nil
71+
}
72+
73+
// checkMysqlShellCompatibility verifies that a MySQL Shell at shellVersion
74+
// can drive a MySQL Server at serverVersion via AdminAPI. The rule is:
75+
// mysqlsh's major.minor must be >= the server's major.minor. In particular,
76+
// MySQL Shell 8.0.x refuses any server > 8.0 with
77+
// "Unsupported server version: AdminAPI operations in this version of
78+
// MySQL Shell support MySQL Server up to version 8.0".
79+
func checkMysqlShellCompatibility(shellVersion, serverVersion string) error {
80+
shellList, err := common.VersionToList(shellVersion)
81+
if err != nil {
82+
return fmt.Errorf("invalid mysqlsh version '%s': %s", shellVersion, err)
83+
}
84+
serverList, err := common.VersionToList(serverVersion)
85+
if err != nil {
86+
return fmt.Errorf("invalid server version '%s': %s", serverVersion, err)
87+
}
88+
shellMajor, shellMinor := shellList[0], shellList[1]
89+
serverMajor, serverMinor := serverList[0], serverList[1]
90+
if shellMajor > serverMajor ||
91+
(shellMajor == serverMajor && shellMinor >= serverMinor) {
92+
return nil
93+
}
94+
return fmt.Errorf(
95+
"MySQL Shell %s is too old for MySQL Server %s: "+
96+
"the AdminAPI in mysqlsh %d.%d only supports MySQL Server up to %d.%d. "+
97+
"Install mysqlsh >= %d.%d (see https://dev.mysql.com/downloads/shell/) "+
98+
"and make sure it is the one found on $PATH, "+
99+
"or place its 'mysqlsh' binary under <basedir>/bin/",
100+
shellVersion, serverVersion,
101+
shellMajor, shellMinor, shellMajor, shellMinor,
102+
serverMajor, serverMinor)
103+
}
104+
55105
// findMysqlRouter locates the mysqlrouter binary. It first checks the basedir/bin
56106
// directory, then falls back to the system PATH.
57107
func findMysqlRouter(basedir string) (string, error) {
@@ -116,6 +166,20 @@ func CreateInnoDBCluster(sandboxDef SandboxDef, origin string, nodes int, master
116166
}
117167
logger.Printf("Using MySQL Shell: %s\n", mysqlshPath)
118168

169+
// Pre-flight: fail early if the mysqlsh AdminAPI can't manage this
170+
// server version (e.g. mysqlsh 8.0.x + server 8.4.x). If parsing the
171+
// shell's own --version fails for any reason, proceed — we don't want
172+
// unknown output to block a valid deployment.
173+
shellVersion, err := getMysqlShellVersion(mysqlshPath)
174+
if err != nil {
175+
logger.Printf("Warning: could not determine mysqlsh version: %s\n", err)
176+
} else {
177+
logger.Printf("Detected MySQL Shell version: %s\n", shellVersion)
178+
if err := checkMysqlShellCompatibility(shellVersion, sandboxDef.Version); err != nil {
179+
return err
180+
}
181+
}
182+
119183
// Find mysqlrouter - optional if --skip-router is set
120184
var mysqlrouterPath string
121185
if !skipRouter {

sandbox/innodb_cluster_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// DBDeployer - The MySQL Sandbox
2+
// Copyright © 2006-2026 Giuseppe Maxia
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package sandbox
17+
18+
import (
19+
"strings"
20+
"testing"
21+
)
22+
23+
func TestCheckMysqlShellCompatibility(t *testing.T) {
24+
cases := []struct {
25+
label string
26+
shell string
27+
server string
28+
wantErr bool
29+
wantMatch string // substring expected in error when wantErr is true
30+
}{
31+
// The exact scenario from issue #87
32+
{"shell 8.0.36 rejects server 8.4.8", "8.0.36", "8.4.8", true, "too old"},
33+
{"shell 8.0.36 rejects server 9.1.0", "8.0.36", "9.1.0", true, "too old"},
34+
35+
// Matching major.minor — allowed
36+
{"shell 8.4.8 accepts server 8.4.8", "8.4.8", "8.4.8", false, ""},
37+
{"shell 8.0.36 accepts server 8.0.42", "8.0.36", "8.0.42", false, ""},
38+
{"shell 9.5.0 accepts server 9.5.0", "9.5.0", "9.5.0", false, ""},
39+
40+
// Shell newer than server — allowed
41+
{"shell 8.4.0 accepts server 8.0.42", "8.4.0", "8.0.42", false, ""},
42+
{"shell 9.5.0 accepts server 8.4.8", "9.5.0", "8.4.8", false, ""},
43+
44+
// Shell older than server by minor — rejected
45+
{"shell 8.3.0 rejects server 8.4.0", "8.3.0", "8.4.0", true, "too old"},
46+
47+
// Malformed inputs
48+
{"invalid shell version", "not-a-version", "8.4.8", true, "invalid mysqlsh version"},
49+
{"invalid server version", "8.4.8", "nope", true, "invalid server version"},
50+
}
51+
for _, c := range cases {
52+
err := checkMysqlShellCompatibility(c.shell, c.server)
53+
if c.wantErr {
54+
if err == nil {
55+
t.Errorf("%s: expected error, got nil", c.label)
56+
continue
57+
}
58+
if c.wantMatch != "" && !strings.Contains(err.Error(), c.wantMatch) {
59+
t.Errorf("%s: error %q does not contain %q", c.label, err.Error(), c.wantMatch)
60+
}
61+
} else if err != nil {
62+
t.Errorf("%s: unexpected error: %s", c.label, err)
63+
}
64+
}
65+
}
66+
67+
func TestMysqlShellVersionRegexp(t *testing.T) {
68+
cases := []struct {
69+
label string
70+
out string
71+
want string
72+
}{
73+
{
74+
"classic 8.0.36 output",
75+
"mysqlsh Ver 8.0.36 for Linux on x86_64 - for MySQL 8.0.36 (Source distribution)",
76+
"8.0.36",
77+
},
78+
{
79+
"8.4.8 output",
80+
"mysqlsh Ver 8.4.8 for Linux on x86_64 - for MySQL 8.4.8 (MySQL Community Server)",
81+
"8.4.8",
82+
},
83+
{
84+
"9.5.0 output",
85+
"MySQL Shell Ver 9.5.0 for Linux on x86_64 - for MySQL 9.5.0",
86+
"9.5.0",
87+
},
88+
}
89+
for _, c := range cases {
90+
m := mysqlShellVersionRegexp.FindStringSubmatch(c.out)
91+
if len(m) < 2 {
92+
t.Errorf("%s: no match in %q", c.label, c.out)
93+
continue
94+
}
95+
if m[1] != c.want {
96+
t.Errorf("%s: got %q, want %q", c.label, m[1], c.want)
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)