Skip to content
This repository was archived by the owner on Apr 4, 2023. It is now read-only.

Commit 1f18a93

Browse files
committed
Add a Cassandra upgrade mechanism
* Add a NodePool.Status.Version attribute which has the lowest reported version from all the pilots in the pool. * Or nil, if the pilot failed to query its Cassandra database for its version. * Calculate an UpdateVersion action if the desired Cassandra version is higher than the version reported for a NodePool. * NodePools are upgraded one at a time. * Pods within a Nodepool are upgraded one at a time, using a rolling update strategy without any partitioning. * Upgrades are performed after Nodepools are created and after scale out. Fixes: #257
1 parent ab9881b commit 1f18a93

File tree

27 files changed

+729
-77
lines changed

27 files changed

+729
-77
lines changed

docs/cassandra.rst

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ Supported Configuration Changes
191191
Navigator supports the following changes to a Cassandra cluster:
192192

193193
* :ref:`create-cluster-cassandra`: Add all initially configured node pools and nodes.
194+
* :ref:`minor-upgrade-cassandra`: Trigger a rolling upgrade of Cassandra nodes by increasing the minor and / or patch components of ``CassandraCluster.Spec.Version``.
194195
* :ref:`scale-out-cassandra`: Increase ``CassandraCluster.Spec.NodePools[0].Replicas`` to add more C* nodes to a ``nodepool``.
195196

196197
Navigator does not currently support any other changes to the Cassandra cluster configuration.
@@ -200,7 +201,6 @@ Unsupported Configuration Changes
200201

201202
The following configuration changes are not currently supported but will be supported in the near future:
202203

203-
* Minor Upgrade: Trigger a rolling Cassandra upgrade by increasing the minor and / or patch components of ``CassandraCluster.Spec.Version``.
204204
* Scale In: Decrease ``CassandraCluster.Spec.NodePools[0].Replicas`` to remove C* nodes from a ``nodepool``.
205205

206206
The following configuration changes are not currently supported:
@@ -220,6 +220,19 @@ in order of ``NodePool`` and according to the process described in :ref:`scale-o
220220
The order of node creation is determined by the order of the entries in the ``CassandraCluster.Spec.NodePools`` list.
221221
You can look at ``CassandraCluster.Status.NodePools`` to see the current state.
222222

223+
.. _minor-upgrade-cassandra:
224+
225+
Minor Upgrade
226+
~~~~~~~~~~~~~
227+
228+
If you increment the minor or patch number in ``CassandraCluster.Spec.Version``, Navigator will trigger a rolling update of the existing C* nodes.
229+
230+
C* nodes are upgraded serially, in order of NodePool and Pod ordinal, starting with the pod with the highest ordinal in the first NodePool.
231+
232+
`StatefulSet Rolling Updates <https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#rolling-updates>`_ describes the update process in more detail.
233+
234+
.. note:: Major version upgrades are not yet supported.
235+
223236
.. _scale-out-cassandra:
224237

225238
Scale Out

hack/e2e.sh

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,12 +266,23 @@ function test_cassandracluster() {
266266
stdout_equals "${CASS_VERSION}" \
267267
kubectl --namespace "${namespace}" \
268268
get pilots \
269-
--output 'jsonpath={.items[0].status.cassandra.version}'
269+
--selector "navigator.jetstack.io/cassandra-cluster-name=${CASS_NAME}" \
270+
--output 'jsonpath={.items[*].status.cassandra.version}'
270271
then
271272
kubectl --namespace "${namespace}" get pilots -o yaml
272273
fail_test "Pilots failed to report the expected version"
273274
fi
274275

276+
if ! retry TIMEOUT=300 \
277+
stdout_equals "${CASS_VERSION}" \
278+
kubectl --namespace "${namespace}" \
279+
get cassandracluster "${CASS_NAME}" \
280+
--output 'jsonpath={.status.nodePools.*.version}'
281+
then
282+
kubectl --namespace "${namespace}" get cassandracluster -o yaml
283+
fail_test "NodePools failed to report the expected version"
284+
fi
285+
275286
# Wait 5 minutes for cassandra to start and listen for CQL queries.
276287
if ! retry TIMEOUT=300 cql_connect \
277288
"${namespace}" \
@@ -303,6 +314,43 @@ function test_cassandracluster() {
303314
--debug \
304315
--execute="INSERT INTO space1.testtable1(key, value) VALUES('testkey1', 'testvalue1')"
305316

317+
# Upgrade to newer patch version
318+
export CASS_VERSION="3.11.2"
319+
kubectl apply \
320+
--namespace "${namespace}" \
321+
--filename \
322+
<(envsubst \
323+
'$NAVIGATOR_IMAGE_REPOSITORY:$NAVIGATOR_IMAGE_TAG:$NAVIGATOR_IMAGE_PULLPOLICY:$CASS_NAME:$CASS_REPLICAS:$CASS_CQL_PORT:$CASS_VERSION' \
324+
< "${SCRIPT_DIR}/testdata/cass-cluster-test.template.yaml")
325+
326+
# The cluster is upgraded
327+
if ! retry TIMEOUT=300 kube_event_exists "${namespace}" \
328+
"navigator-controller:CassandraCluster:Normal:UpdateVersion"
329+
then
330+
fail_test "An UpdateVersion event was not recorded"
331+
fi
332+
333+
if ! retry TIMEOUT=300 \
334+
stdout_equals "${CASS_VERSION}" \
335+
kubectl --namespace "${namespace}" \
336+
get pilots \
337+
--selector "navigator.jetstack.io/cassandra-cluster-name=${CASS_NAME}" \
338+
--output 'jsonpath={.items[*].status.cassandra.version}'
339+
then
340+
kubectl --namespace "${namespace}" get pilots -o yaml
341+
fail_test "Pilots failed to report the expected version"
342+
fi
343+
344+
if ! retry TIMEOUT=300 \
345+
stdout_equals "${CASS_VERSION}" \
346+
kubectl --namespace "${namespace}" \
347+
get cassandracluster "${CASS_NAME}" \
348+
--output 'jsonpath={.status.nodePools.*.version}'
349+
then
350+
kubectl --namespace "${namespace}" get cassandracluster -o yaml
351+
fail_test "NodePools failed to report the expected version"
352+
fi
353+
306354
# Delete the Cassandra pod and wait for the CQL service to become
307355
# unavailable (readiness probe fails)
308356

hack/testdata/values.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ controller:
3131
repository: quay.io/jetstack/navigator-controller
3232
tag: build
3333
pullPolicy: Never
34+
logLevel: 4

internal/test/util/generate/generate.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,18 +122,24 @@ func StatefulSet(c StatefulSetConfig) *apps.StatefulSet {
122122

123123
type CassandraClusterConfig struct {
124124
Name, Namespace string
125+
Version *version.Version
125126
}
126127

127128
func CassandraCluster(c CassandraClusterConfig) *v1alpha1.CassandraCluster {
128-
return &v1alpha1.CassandraCluster{
129+
o := &v1alpha1.CassandraCluster{
129130
ObjectMeta: metav1.ObjectMeta{
130131
Name: c.Name,
131132
Namespace: c.Namespace,
132133
},
133-
Spec: v1alpha1.CassandraClusterSpec{
134-
Version: *version.New("3.11.2"),
135-
},
134+
Spec: v1alpha1.CassandraClusterSpec{},
135+
}
136+
if c.Version == nil {
137+
o.Spec.Version = *version.New("3.11.2")
138+
} else {
139+
o.Spec.Version = *c.Version
136140
}
141+
142+
return o
137143
}
138144

139145
type CassandraClusterNodePoolConfig struct {

pkg/api/version/version.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package version
33
import (
44
"encoding/json"
55
"strconv"
6+
"strings"
67

78
semver "github.com/hashicorp/go-version"
89
)
@@ -59,6 +60,11 @@ func (v *Version) Semver() *semver.Version {
5960
return v.semver
6061
}
6162

63+
// TODO: Add tests for this
64+
func (v *Version) LessThan(versionB *Version) bool {
65+
return v.semver.LessThan(versionB.semver)
66+
}
67+
6268
func (v *Version) UnmarshalJSON(data []byte) error {
6369
s, err := strconv.Unquote(string(data))
6470
if err != nil {
@@ -84,3 +90,31 @@ func (v Version) DeepCopy() Version {
8490
}
8591
return *New(v.String())
8692
}
93+
94+
func (v *Version) bump(i int) *Version {
95+
v2 := v.DeepCopy()
96+
parts := strings.Split(v2.Semver().String(), ".")
97+
part, err := strconv.Atoi(parts[i])
98+
if err != nil {
99+
panic(err)
100+
}
101+
part++
102+
parts[i] = strconv.Itoa(part)
103+
return New(strings.Join(parts, "."))
104+
}
105+
106+
func (v *Version) BumpMajor() *Version {
107+
return v.bump(0)
108+
}
109+
110+
func (v *Version) BumpMinor() *Version {
111+
return v.bump(1)
112+
}
113+
114+
func (v *Version) BumpPatch() *Version {
115+
return v.bump(2)
116+
}
117+
118+
func (v *Version) Major() int64 {
119+
return v.semver.Segments64()[0]
120+
}

pkg/apis/navigator/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ type CassandraClusterStatus struct {
5151

5252
type CassandraClusterNodePoolStatus struct {
5353
ReadyReplicas int32
54+
Version *version.Version
5455
}
5556

5657
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

pkg/apis/navigator/v1alpha1/types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ type CassandraClusterStatus struct {
115115
type CassandraClusterNodePoolStatus struct {
116116
// The number of replicas in the node pool that are currently 'Ready'.
117117
ReadyReplicas int32 `json:"readyReplicas"`
118+
// The lowest version of Cassandra found to be running in this nodepool,
119+
// as reported by the Cassandra process.
120+
// nil or empty if the lowest version can not be determined,
121+
// or if the lowest version has not yet been determined
122+
// +optional
123+
Version *version.Version `json:"version,omitempty"`
118124
}
119125

120126
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

pkg/apis/navigator/v1alpha1/zz_generated.conversion.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ func Convert_navigator_CassandraClusterNodePool_To_v1alpha1_CassandraClusterNode
197197

198198
func autoConvert_v1alpha1_CassandraClusterNodePoolStatus_To_navigator_CassandraClusterNodePoolStatus(in *CassandraClusterNodePoolStatus, out *navigator.CassandraClusterNodePoolStatus, s conversion.Scope) error {
199199
out.ReadyReplicas = in.ReadyReplicas
200+
out.Version = (*version.Version)(unsafe.Pointer(in.Version))
200201
return nil
201202
}
202203

@@ -207,6 +208,7 @@ func Convert_v1alpha1_CassandraClusterNodePoolStatus_To_navigator_CassandraClust
207208

208209
func autoConvert_navigator_CassandraClusterNodePoolStatus_To_v1alpha1_CassandraClusterNodePoolStatus(in *navigator.CassandraClusterNodePoolStatus, out *CassandraClusterNodePoolStatus, s conversion.Scope) error {
209210
out.ReadyReplicas = in.ReadyReplicas
211+
out.Version = (*version.Version)(unsafe.Pointer(in.Version))
210212
return nil
211213
}
212214

pkg/apis/navigator/v1alpha1/zz_generated.deepcopy.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,15 @@ func (in *CassandraClusterNodePool) DeepCopy() *CassandraClusterNodePool {
152152
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
153153
func (in *CassandraClusterNodePoolStatus) DeepCopyInto(out *CassandraClusterNodePoolStatus) {
154154
*out = *in
155+
if in.Version != nil {
156+
in, out := &in.Version, &out.Version
157+
if *in == nil {
158+
*out = nil
159+
} else {
160+
*out = new(version.Version)
161+
**out = **in
162+
}
163+
}
155164
return
156165
}
157166

@@ -206,7 +215,9 @@ func (in *CassandraClusterStatus) DeepCopyInto(out *CassandraClusterStatus) {
206215
in, out := &in.NodePools, &out.NodePools
207216
*out = make(map[string]CassandraClusterNodePoolStatus, len(*in))
208217
for key, val := range *in {
209-
(*out)[key] = val
218+
newVal := new(CassandraClusterNodePoolStatus)
219+
val.DeepCopyInto(newVal)
220+
(*out)[key] = *newVal
210221
}
211222
}
212223
return

pkg/apis/navigator/validation/cassandra.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,30 @@ func ValidateCassandraClusterUpdate(old, new *navigator.CassandraCluster) field.
4444

4545
fldPath := field.NewPath("spec")
4646

47-
if !new.Spec.Version.Equal(&old.Spec.Version) {
47+
if new.Spec.Version.LessThan(&old.Spec.Version) {
4848
allErrs = append(
4949
allErrs,
5050
field.Forbidden(
5151
fldPath.Child("version"),
5252
fmt.Sprintf(
53-
"cannot change the version of an existing cluster. "+
53+
"cannot perform version downgrades. "+
5454
"old version: %s, new version: %s",
5555
old.Spec.Version, new.Spec.Version,
5656
),
5757
),
5858
)
5959
}
6060

61+
if new.Spec.Version.Major() != old.Spec.Version.Major() {
62+
allErrs = append(
63+
allErrs,
64+
field.Forbidden(
65+
fldPath.Child("version"),
66+
"cannot perform major version upgrades",
67+
),
68+
)
69+
}
70+
6171
npPath := fldPath.Child("nodePools")
6272
for i, newNp := range new.Spec.NodePools {
6373
idxPath := npPath.Index(i)

0 commit comments

Comments
 (0)