Skip to content
This repository was archived by the owner on Apr 4, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 7 commits
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
8 changes: 7 additions & 1 deletion Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,7 @@ required = [
[[override]]
name = "k8s.io/gengo"
revision = "b58fc7edb82e0c6ffc9b8aef61813c7261b785d4"

[[constraint]]
branch = "master"
name = "github.com/hashicorp/go-version"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think this needs to be added as a constraint given we are constraining it to master anyway. A simple dep ensure without adding this block should yield the same result, sans this change

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

5 changes: 5 additions & 0 deletions docs/cassandra.rst
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,8 @@ Navigator will add C* nodes, one at a time, until the desired number of nodes is
and `Best way to add multiple nodes to existing cassandra cluster <https://stackoverflow.com/questions/37283424/best-way-to-add-multiple-nodes-to-existing-cassandra-cluster>`_.

You can look at ``CassandraCluster.Status.NodePools[<nodepoolname>].ReadyReplicas`` to see the current number of healthy C* nodes in each ``nodepool``.

Supported Versions
------------------

Navigator only supports Cassandra major version 3.
8 changes: 8 additions & 0 deletions hack/update-client-gen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@ ${CODEGEN_PKG}/generate-internal-groups.sh all \
navigator:v1alpha1 \
--output-base "${GOPATH}/src/" \
--go-header-file ${SCRIPT_ROOT}/hack/boilerplate.go.txt

echo "Generating other deepcopy funcs"
${GOPATH}/bin/deepcopy-gen \
--input-dirs github.com/jetstack/navigator/pkg/cassandra/version \
-O zz_generated.deepcopy \
--bounding-dirs github.com/jetstack/navigator/pkg/cassandra/version \
--output-base "${GOPATH}/src/" \
--go-header-file ${SCRIPT_ROOT}/hack/boilerplate.go.txt
4 changes: 4 additions & 0 deletions internal/test/util/generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/jetstack/navigator/pkg/apis/navigator/v1alpha1"
"github.com/jetstack/navigator/pkg/cassandra/version"
)

type PilotConfig struct {
Expand Down Expand Up @@ -129,6 +130,9 @@ func CassandraCluster(c CassandraClusterConfig) *v1alpha1.CassandraCluster {
Name: c.Name,
Namespace: c.Namespace,
},
Spec: v1alpha1.CassandraClusterSpec{
Version: *version.New("3.11.2"),
},
}
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/apis/navigator/v1alpha1/zz_generated.deepcopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ func (in *CassandraClusterSpec) DeepCopyInto(out *CassandraClusterSpec) {
**out = **in
}
}
out.Version = in.Version
out.Version = in.Version.DeepCopy()
return
}

Expand Down Expand Up @@ -231,7 +231,7 @@ func (in *CassandraPilotStatus) DeepCopyInto(out *CassandraPilotStatus) {
*out = nil
} else {
*out = new(version.Version)
**out = **in
**out = (*in).DeepCopy()
}
}
return
Expand Down
42 changes: 42 additions & 0 deletions pkg/apis/navigator/validation/cassandra.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
package validation

import (
"fmt"
"reflect"

apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"

version "github.com/hashicorp/go-version"

"github.com/jetstack/navigator/pkg/apis/navigator"
)

var supportedCassandraVersions version.Constraints

func init() {
var err error
supportedCassandraVersions, err = version.NewConstraint(">= 3, < 4")
if err != nil {
panic(err)
}
}

func ValidateCassandraClusterNodePool(np *navigator.CassandraClusterNodePool, fldPath *field.Path) field.ErrorList {
el := field.ErrorList{}
if np.Persistence != nil {
Expand All @@ -31,6 +44,20 @@ func ValidateCassandraClusterUpdate(old, new *navigator.CassandraCluster) field.

fldPath := field.NewPath("spec")

if !new.Spec.Version.Equal(&old.Spec.Version) {
allErrs = append(
allErrs,
field.Forbidden(
fldPath.Child("version"),
fmt.Sprintf(
"cannot change the version of an existing cluster. "+
"old version: %s, new version: %s",
old.Spec.Version, new.Spec.Version,
),
),
)
}

npPath := fldPath.Child("nodePools")
for i, newNp := range new.Spec.NodePools {
idxPath := npPath.Index(i)
Expand Down Expand Up @@ -63,7 +90,22 @@ func ValidateCassandraClusterUpdate(old, new *navigator.CassandraCluster) field.
}

func ValidateCassandraClusterSpec(spec *navigator.CassandraClusterSpec, fldPath *field.Path) field.ErrorList {

allErrs := ValidateNavigatorClusterConfig(&spec.NavigatorClusterConfig, fldPath)

if !supportedCassandraVersions.Check(spec.Version.Semver()) {
allErrs = append(
allErrs,
field.Forbidden(
fldPath.Child("version"),
fmt.Sprintf(
"%s is not supported. Supported versions are: %s",
spec.Version, supportedCassandraVersions,
),
),
)
}

npPath := fldPath.Child("nodePools")
allNames := sets.String{}
for i, np := range spec.NodePools {
Expand Down
40 changes: 39 additions & 1 deletion pkg/apis/navigator/validation/cassandra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ var (
Namespace: "bar",
},
Spec: navigator.CassandraClusterSpec{
Version: *version.New("5.6.2"),
Version: *version.New("3.11.2"),
Image: &validImageSpec,
NavigatorClusterConfig: validNavigatorClusterConfig,
NodePools: []navigator.CassandraClusterNodePool{
Expand All @@ -31,6 +31,8 @@ var (
},
},
}
lowerVersion = version.New("3.11.1")
higherVersion = version.New("3.12")
)

func TestValidateCassandraCluster(t *testing.T) {
Expand All @@ -39,10 +41,27 @@ func TestValidateCassandraCluster(t *testing.T) {
errorExpected bool
}

setVersion := func(
c *navigator.CassandraCluster,
v *version.Version,
) *navigator.CassandraCluster {
c = c.DeepCopy()
c.Spec.Version = *v
return c
}

tests := map[string]testT{
"valid cluster": {
cluster: validCassCluster,
},
"version too low": {
cluster: setVersion(validCassCluster, version.New("2.0.0")),
errorExpected: true,
},
"version too high": {
cluster: setVersion(validCassCluster, version.New("4.0.0")),
errorExpected: true,
},
}

setNavigatorClusterConfig := func(
Expand Down Expand Up @@ -130,6 +149,15 @@ func TestValidateCassandraClusterUpdate(t *testing.T) {
return c
}

setVersion := func(
c *navigator.CassandraCluster,
v *version.Version,
) *navigator.CassandraCluster {
c = c.DeepCopy()
c.Spec.Version = *v
return c
}

tests := map[string]testT{
"unchanged cluster": {
old: validCassCluster,
Expand All @@ -149,6 +177,16 @@ func TestValidateCassandraClusterUpdate(t *testing.T) {
old: setPersistence(validCassCluster, &navigator.PersistenceConfig{Size: resource.MustParse("10Gi")}),
new: validCassCluster,
},
"downgrade not allowed": {
old: setVersion(validCassCluster, lowerVersion),
new: validCassCluster,
errorExpected: true,
},
"upgrade not allowed": {
old: validCassCluster,
new: setVersion(validCassCluster, higherVersion),
errorExpected: true,
},
}

for title, persistence := range persistenceErrorCases {
Expand Down
4 changes: 2 additions & 2 deletions pkg/apis/navigator/zz_generated.deepcopy.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ func (in *CassandraClusterSpec) DeepCopyInto(out *CassandraClusterSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
out.Version = in.Version
out.Version = in.Version.DeepCopy()
if in.Image != nil {
in, out := &in.Image, &out.Image
if *in == nil {
Expand Down Expand Up @@ -231,7 +231,7 @@ func (in *CassandraPilotStatus) DeepCopyInto(out *CassandraPilotStatus) {
*out = nil
} else {
*out = new(version.Version)
**out = **in
**out = (*in).DeepCopy()
}
}
return
Expand Down
84 changes: 30 additions & 54 deletions pkg/cassandra/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ package version

import (
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/coreos/go-semver/semver"
semver "github.com/hashicorp/go-version"
)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this file out of pkg/cassandra and into something like pkg/api/util? This is an API type, but it isn't by default part of an API group (hence it isn't in apis).

It'd be good to denote it as such in the file structure, to make it clear this isn't specific to just cassandra (and also denotes that this type may be exposed in an api)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// Version represents a Cassandra database server version.
Expand All @@ -23,9 +21,11 @@ import (
// This also fixes the missing Patch number and stores the version internally as a semver.
// It also keeps a reference to the original version string so that we can report that in our API.
// So that the version reported in our API matches the version that an administrator expects.
//
// +k8s:deepcopy-gen=true
type Version struct {
versionString string
semver semver.Version
semver *semver.Version
}

func New(s string) *Version {
Expand All @@ -37,10 +37,28 @@ func New(s string) *Version {
return v
}

func (v *Version) set(s string) error {
sv, err := semver.NewVersion(s)
if err != nil {
return err
}
v.versionString = s
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to keep the versionString in memory? Surely semver.String will be more up to date?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need it because we want to remember the format of the originally supplied version string.
So if the Cassandra node reports version 3.11 via nodetool and a JMX query, we want to report that exact version in the Pilot.Status.Cassandra.Version rather than the semver 3.11.0.

We also don't want a user to submit CassandraCluster.Spec.Version: 3.11 and then have that change to 3.11.0 when they do kubectl get CassandraCluster -o yaml.

However, when we infer the Docker image tag, we use the semver, because we don't want docker to simply pull the latest 3.11 image from Dockerhub.

v.semver = sv
return nil
}

func (v *Version) Equal(versionB *Version) bool {
return v.semver.Equal(versionB.semver)
}

func (v Version) String() string {
return v.versionString
}

func (v *Version) Semver() *semver.Version {
return v.semver
}

func (v *Version) UnmarshalJSON(data []byte) error {
s, err := strconv.Unquote(string(data))
if err != nil {
Expand All @@ -49,62 +67,20 @@ func (v *Version) UnmarshalJSON(data []byte) error {
return v.set(s)
}

func (v *Version) set(cassVersionString string) error {
var versionsTried []string
var errorsEncountered []string

errorWhileParsingOriginalVersion := v.semver.Set(cassVersionString)
if errorWhileParsingOriginalVersion == nil {
v.versionString = cassVersionString
return nil
}

versionsTried = append(versionsTried, cassVersionString)
errorsEncountered = append(errorsEncountered, errorWhileParsingOriginalVersion.Error())

semverString := maybeAddMissingPatchVersion(cassVersionString)
if semverString != cassVersionString {
errorWhileParsingSemverVersion := v.semver.Set(semverString)
if errorWhileParsingSemverVersion == nil {
v.versionString = cassVersionString
return nil
}
versionsTried = append(versionsTried, semverString)
errorsEncountered = append(errorsEncountered, errorWhileParsingSemverVersion.Error())
}

return fmt.Errorf(
"unable to parse Cassandra version as semver. "+
"Versions tried: '%s'. "+
"Errors encountered: '%s'.",
strings.Join(versionsTried, "','"),
strings.Join(errorsEncountered, "','"),
)
}

var _ json.Unmarshaler = &Version{}

func maybeAddMissingPatchVersion(v string) string {
mmpAndLabels := strings.SplitN(v, "-", 2)
mmp := mmpAndLabels[0]
mmpParts := strings.SplitN(mmp, ".", 3)
if len(mmpParts) == 2 {
mmp = mmp + ".0"
}
mmpAndLabels[0] = mmp
return strings.Join(mmpAndLabels, "-")
}

func (v Version) String() string {
return v.versionString
}

func (v Version) MarshalJSON() ([]byte, error) {
return []byte(strconv.Quote(v.String())), nil
}

var _ json.Marshaler = &Version{}

func (v Version) Semver() string {
return v.semver.String()
// DeepCopy returns a deep-copy of the Version value.
// If the underlying semver is a nil pointer, assume that the zero value is being copied,
// and return that.
func (v Version) DeepCopy() Version {
if v.semver == nil {
return Version{}
}
return *New(v.String())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}
Loading