Skip to content
This repository was archived by the owner on May 28, 2021. It is now read-only.

Commit 7cad242

Browse files
committed
Test MySQL upgrades
1 parent f59a361 commit 7cad242

File tree

4 files changed

+250
-23
lines changed

4 files changed

+250
-23
lines changed

pkg/controllers/cluster/controller.go

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -427,29 +427,34 @@ func (m *MySQLController) syncHandler(key string) error {
427427
return nil
428428
}
429429

430-
func (m *MySQLController) ensureMySQLVersion(c *v1alpha1.Cluster, ss *apps.StatefulSet) error {
431-
var index int
432-
{
433-
var found bool
434-
for i, c := range ss.Spec.Template.Spec.Containers {
435-
if c.Name == statefulsets.MySQLServerName {
436-
index = i
437-
found = true
438-
break
439-
}
440-
}
441-
442-
if !found {
443-
return errors.Errorf("no %q container found for StatefulSet %q", statefulsets.MySQLServerName, ss.Name)
430+
func getMySQLContainerIndex(containers []corev1.Container) (int, error) {
431+
for i, c := range containers {
432+
if c.Name == statefulsets.MySQLServerName {
433+
return i, nil
444434
}
445435
}
446436

447-
image := ss.Spec.Template.Spec.Containers[index].Image
437+
return 0, errors.Errorf("no %q container found", statefulsets.MySQLServerName)
438+
}
439+
440+
// splitImage splits an image into its name and version.
441+
func splitImage(image string) (string, string, error) {
448442
parts := strings.Split(image, ":")
449443
if len(parts) < 2 {
450-
return errors.Errorf("invalid image %q for StatefulSet %q", image, ss.Name)
444+
return "", "", errors.Errorf("invalid image %q", image)
445+
}
446+
return strings.Join(parts[:len(parts)-1], ""), parts[len(parts)-1], nil
447+
}
448+
449+
func (m *MySQLController) ensureMySQLVersion(c *v1alpha1.Cluster, ss *apps.StatefulSet) error {
450+
index, err := getMySQLContainerIndex(ss.Spec.Template.Spec.Containers)
451+
if err != nil {
452+
return errors.Wrapf(err, "getting MySQL container for StatefulSet %q", ss.Name)
453+
}
454+
imageName, actualVersion, err := splitImage(ss.Spec.Template.Spec.Containers[index].Image)
455+
if err != nil {
456+
return errors.Wrapf(err, "getting MySQL version for StatefulSet %q", ss.Name)
451457
}
452-
actualVersion := parts[len(parts)-1]
453458

454459
actual, err := semver.NewVersion(actualVersion)
455460
if err != nil {
@@ -468,10 +473,7 @@ func (m *MySQLController) ensureMySQLVersion(c *v1alpha1.Cluster, ss *apps.State
468473
}
469474

470475
updated := ss.DeepCopy()
471-
472-
updated.Spec.Template.Spec.Containers[index].Image = fmt.Sprintf(
473-
"%s:%s", strings.Join(parts[:len(parts)-1], ""), expected.String(),
474-
)
476+
updated.Spec.Template.Spec.Containers[index].Image = fmt.Sprintf("%s:%s", imageName, c.Spec.Version)
475477
// NOTE: We do this as previously we defaulted to the OnDelete strategy
476478
// so clusters created with previous versions would not support upgrades.
477479
updated.Spec.UpdateStrategy = apps.StatefulSetUpdateStrategy{

pkg/controllers/cluster/controller_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ import (
3030
"k8s.io/client-go/kubernetes/fake"
3131
cache "k8s.io/client-go/tools/cache"
3232

33+
"github.com/stretchr/testify/assert"
34+
"github.com/stretchr/testify/require"
35+
3336
"github.com/oracle/mysql-operator/pkg/apis/mysql/v1alpha1"
3437
"github.com/oracle/mysql-operator/pkg/constants"
3538
"github.com/oracle/mysql-operator/pkg/controllers/util"
@@ -42,6 +45,76 @@ import (
4245
buildversion "github.com/oracle/mysql-operator/pkg/version"
4346
)
4447

48+
func TestGetMySQLContainerIndex(t *testing.T) {
49+
testCases := map[string]struct {
50+
containers []v1.Container
51+
index int
52+
errors bool
53+
}{
54+
"empty_errors": {
55+
containers: []v1.Container{},
56+
errors: true,
57+
},
58+
"mysql_server_only": {
59+
containers: []v1.Container{{Name: "mysql"}},
60+
index: 0,
61+
},
62+
"mysql_server_and_agent": {
63+
containers: []v1.Container{{Name: "mysql-agent"}, {Name: "mysql"}},
64+
index: 1,
65+
},
66+
"mysql_agent_only": {
67+
containers: []v1.Container{{Name: "mysql-agent"}},
68+
errors: true,
69+
},
70+
}
71+
72+
for name, tc := range testCases {
73+
t.Run(name, func(t *testing.T) {
74+
index, err := getMySQLContainerIndex(tc.containers)
75+
if tc.errors {
76+
require.Error(t, err)
77+
} else {
78+
require.NoError(t, err)
79+
assert.Equal(t, index, tc.index)
80+
}
81+
})
82+
}
83+
}
84+
85+
func TestSplitImage(t *testing.T) {
86+
testCases := map[string]struct {
87+
image string
88+
name string
89+
version string
90+
errors bool
91+
}{
92+
"8.0.11": {
93+
image: "mysql/mysql-server:8.0.11",
94+
name: "mysql/mysql-server",
95+
version: "8.0.11",
96+
errors: false,
97+
},
98+
"invalid": {
99+
image: "mysql/mysql-server",
100+
errors: true,
101+
},
102+
}
103+
104+
for name, tc := range testCases {
105+
t.Run(name, func(t *testing.T) {
106+
name, version, err := splitImage(tc.image)
107+
if tc.errors {
108+
require.Error(t, err)
109+
} else {
110+
require.NoError(t, err)
111+
assert.Equal(t, name, tc.name)
112+
assert.Equal(t, version, tc.version)
113+
}
114+
})
115+
}
116+
}
117+
45118
func mockOperatorConfig() operatoropts.MySQLOperatorOpts {
46119
opts := operatoropts.MySQLOperatorOpts{}
47120
opts.EnsureDefaults()

test/e2e/framework/cluster.go

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
"strings"
2020
"time"
2121

22+
apps "k8s.io/api/apps/v1beta1"
23+
"k8s.io/api/core/v1"
2224
apierrors "k8s.io/apimachinery/pkg/api/errors"
2325
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2426
"k8s.io/apimachinery/pkg/labels"
@@ -35,6 +37,7 @@ import (
3537
"github.com/oracle/mysql-operator/pkg/controllers/cluster/labeler"
3638
mysqlclientset "github.com/oracle/mysql-operator/pkg/generated/clientset/versioned"
3739
"github.com/oracle/mysql-operator/pkg/resources/secrets"
40+
"github.com/oracle/mysql-operator/pkg/resources/statefulsets"
3841
)
3942

4043
// TestDBName is the name of database to use when executing test SQL queries.
@@ -112,7 +115,7 @@ func (j *ClusterTestJig) CreateAndAwaitClusterOrFail(namespace string, members i
112115
return cluster
113116
}
114117

115-
func (j *ClusterTestJig) waitForConditionOrFail(namespace, name string, timeout time.Duration, message string, conditionFn func(*v1alpha1.Cluster) bool) *v1alpha1.Cluster {
118+
func (j *ClusterTestJig) WaitForConditionOrFail(namespace, name string, timeout time.Duration, message string, conditionFn func(*v1alpha1.Cluster) bool) *v1alpha1.Cluster {
116119
var cluster *v1alpha1.Cluster
117120
pollFunc := func() (bool, error) {
118121
c, err := j.MySQLClient.MySQLV1alpha1().Clusters(namespace).Get(name, metav1.GetOptions{})
@@ -135,12 +138,99 @@ func (j *ClusterTestJig) waitForConditionOrFail(namespace, name string, timeout
135138
// the running phase.
136139
func (j *ClusterTestJig) WaitForClusterReadyOrFail(namespace, name string, timeout time.Duration) *v1alpha1.Cluster {
137140
Logf("Waiting up to %v for Cluster \"%s/%s\" to be ready", timeout, namespace, name)
138-
cluster := j.waitForConditionOrFail(namespace, name, timeout, "have all nodes ready", func(cluster *v1alpha1.Cluster) bool {
141+
cluster := j.WaitForConditionOrFail(namespace, name, timeout, "have all nodes ready", func(cluster *v1alpha1.Cluster) bool {
139142
return clusterutil.IsClusterReady(cluster)
140143
})
141144
return cluster
142145
}
143146

147+
// WaitForClusterUpgradedOrFail waits for a MySQL cluster to be upgraded to the
148+
// given version or fails.
149+
func (j *ClusterTestJig) WaitForClusterUpgradedOrFail(namespace, name, version string, timeout time.Duration) *v1alpha1.Cluster {
150+
Logf("Waiting up to %v for Cluster \"%s/%s\" to be upgraded", timeout, namespace, name)
151+
152+
cluster := j.WaitForConditionOrFail(namespace, name, timeout, "be upgraded ", func(cluster *v1alpha1.Cluster) bool {
153+
set, err := j.KubeClient.AppsV1beta1().StatefulSets(cluster.Namespace).Get(cluster.Name, metav1.GetOptions{})
154+
if err != nil {
155+
Failf("Failed to get StatefulSet %[1]q for Cluster %[1]q: %[2]v", name, err)
156+
}
157+
158+
set = j.waitForSSRollingUpdate(set)
159+
160+
var actualVersion string
161+
{
162+
var found bool
163+
var index int
164+
for i, c := range set.Spec.Template.Spec.Containers {
165+
if c.Name == statefulsets.MySQLServerName {
166+
index = i
167+
found = true
168+
break
169+
}
170+
}
171+
172+
if !found {
173+
Failf("no %q container found for StatefulSet %q", statefulsets.MySQLServerName, set.Name)
174+
}
175+
image := set.Spec.Template.Spec.Containers[index].Image
176+
parts := strings.Split(image, ":")
177+
if len(parts) < 2 {
178+
Failf("invalid image %q for StatefulSet %q", image, set.Name)
179+
}
180+
actualVersion = parts[len(parts)-1]
181+
}
182+
183+
return actualVersion == version
184+
})
185+
return cluster
186+
}
187+
188+
// waitForSSState periodically polls for the ss and its pods until the until function returns either true or an error
189+
func (j *ClusterTestJig) waitForSSState(ss *apps.StatefulSet, until func(*apps.StatefulSet, *v1.PodList) (bool, error)) {
190+
pollErr := wait.PollImmediate(Poll, DefaultTimeout,
191+
func() (bool, error) {
192+
ssGet, err := j.KubeClient.AppsV1beta1().StatefulSets(ss.Namespace).Get(ss.Name, metav1.GetOptions{})
193+
if err != nil {
194+
return false, err
195+
}
196+
197+
selector, err := metav1.LabelSelectorAsSelector(ss.Spec.Selector)
198+
ExpectNoError(err)
199+
podList, err := j.KubeClient.CoreV1().Pods(ss.Namespace).List(metav1.ListOptions{LabelSelector: selector.String()})
200+
ExpectNoError(err)
201+
202+
return until(ssGet, podList)
203+
})
204+
if pollErr != nil {
205+
Failf("Failed waiting for state update: %v", pollErr)
206+
}
207+
}
208+
209+
// waitForRollingUpdate waits for all Pods in set to exist and have the correct revision and for the RollingUpdate to
210+
// complete. set must have a RollingUpdateStatefulSetStrategyType.
211+
func (j *ClusterTestJig) waitForSSRollingUpdate(set *apps.StatefulSet) *apps.StatefulSet {
212+
var pods *v1.PodList
213+
if set.Spec.UpdateStrategy.Type != apps.RollingUpdateStatefulSetStrategyType {
214+
Failf("StatefulSet %s/%s attempt to wait for rolling update with updateStrategy %s",
215+
set.Namespace,
216+
set.Name,
217+
set.Spec.UpdateStrategy.Type)
218+
}
219+
Logf("Waiting for StatefulSet %s/%s to complete update", set.Namespace, set.Name)
220+
j.waitForSSState(set, func(set2 *apps.StatefulSet, pods2 *v1.PodList) (bool, error) {
221+
set = set2
222+
pods = pods2
223+
if len(pods.Items) < int(*set.Spec.Replicas) {
224+
return false, nil
225+
}
226+
if set.Status.UpdateRevision != set.Status.CurrentRevision {
227+
return false, nil
228+
}
229+
return true, nil
230+
})
231+
return set
232+
}
233+
144234
// SanityCheckCluster checks basic properties of a given Cluster match
145235
// our expectations.
146236
func (j *ClusterTestJig) SanityCheckCluster(cluster *v1alpha1.Cluster) {

test/e2e/upgrade.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2018 Oracle and/or its affiliates. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package e2e
16+
17+
import (
18+
. "github.com/onsi/ginkgo"
19+
. "github.com/onsi/gomega"
20+
21+
"github.com/oracle/mysql-operator/pkg/apis/mysql/v1alpha1"
22+
mysqlclientset "github.com/oracle/mysql-operator/pkg/generated/clientset/versioned"
23+
"github.com/oracle/mysql-operator/test/e2e/framework"
24+
)
25+
26+
var _ = Describe("MySQL Upgrade", func() {
27+
f := framework.NewDefaultFramework("upgrade")
28+
29+
var mcs mysqlclientset.Interface
30+
BeforeEach(func() {
31+
mcs = f.MySQLClientSet
32+
})
33+
34+
It("should be possible to upgrade a cluster from 8.0.11 to 8.0.12", func() {
35+
jig := framework.NewClusterTestJig(mcs, f.ClientSet, "upgrade-test")
36+
37+
By("creating an 8.0.11 cluster")
38+
39+
cluster := jig.CreateAndAwaitClusterOrFail(f.Namespace.Name, 3, func(c *v1alpha1.Cluster) {
40+
c.Spec.Version = "8.0.11"
41+
}, framework.DefaultTimeout)
42+
43+
expected, err := framework.WriteSQLTest(cluster, cluster.Name+"-0")
44+
Expect(err).NotTo(HaveOccurred())
45+
46+
By("triggering an upgrade to 8.0.12")
47+
48+
cluster.Spec.Version = "8.0.12"
49+
cluster, err = mcs.MySQLV1alpha1().Clusters(cluster.Namespace).Update(cluster)
50+
Expect(err).NotTo(HaveOccurred())
51+
52+
By("waiting for the upgrade to complete")
53+
54+
cluster = jig.WaitForClusterUpgradedOrFail(cluster.Namespace, cluster.Name, "8.0.12", framework.DefaultTimeout)
55+
56+
By("testing we can read from the upgraded database")
57+
58+
actual, err := framework.ReadSQLTest(cluster, cluster.Name+"-0")
59+
Expect(err).NotTo(HaveOccurred())
60+
Expect(actual).To(Equal(expected))
61+
})
62+
})

0 commit comments

Comments
 (0)