Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
53 changes: 53 additions & 0 deletions api/v1/autoconfig_defaults_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package v1

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestCheckNSetDefaultsAutoConfig(t *testing.T) {
tests := map[string]struct {
enabled *bool
loadType AutoConfigLoadType
wantEnabled bool
wantLoadType AutoConfigLoadType
}{
"unset stays disabled": {
wantEnabled: false,
wantLoadType: "",
},
"explicitly disabled keeps loadType unset": {
enabled: new(false),
wantEnabled: false,
wantLoadType: "",
},
"explicitly enabled without loadType gets default": {
enabled: new(true),
wantEnabled: true,
wantLoadType: AutoConfigLoadTypeSomeWrites,
},
"explicitly enabled with custom loadType is preserved": {
enabled: new(true),
loadType: AutoConfigLoadTypeHeavyWrites,
wantEnabled: true,
wantLoadType: AutoConfigLoadTypeHeavyWrites,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
cr := new(PerconaServerMySQL)
cr.Spec.MySQL.AutoConfig.Enabled = tc.enabled
cr.Spec.MySQL.AutoConfig.LoadType = tc.loadType

// CheckNSetDefaults returns an error later (nil volumeSpec), but the
// autoconfig defaulting runs before that, so the fields are set
// regardless.
_ = cr.CheckNSetDefaults(t.Context(), nil)

assert.Equal(t, tc.wantEnabled, cr.Spec.MySQL.AutoConfig.IsEnabled())
assert.Equal(t, tc.wantLoadType, cr.Spec.MySQL.AutoConfig.LoadType)
})
}
}
52 changes: 34 additions & 18 deletions api/v1/perconaservermysql_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/validation"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/percona/percona-server-mysql-operator/pkg/naming"
"github.com/percona/percona-server-mysql-operator/pkg/platform"
Expand Down Expand Up @@ -227,13 +226,6 @@ const (
ClusterTypeAsync ClusterType = "async"
)

const (
MinSafeProxySize = 2
MinSafeGRSize = 3
MaxSafeGRSize = 9
MinSafeAsyncSize = 2
)

// Checks if the provided ClusterType is valid.
func (t ClusterType) isValid() bool {
switch t {
Expand All @@ -246,11 +238,16 @@ func (t ClusterType) isValid() bool {

// +kubebuilder:validation:XValidation:rule="has(self.image) && size(self.image) > 0",message="mysql.image is required"
// +kubebuilder:validation:XValidation:rule="has(self.size) && self.size > 0",message="mysql.size must be greater than 0"
// +kubebuilder:validation:XValidation:rule="!(self.?autoconfig.?enabled.orValue(false)) || (has(self.resources) && ((has(self.resources.limits) && 'cpu' in self.resources.limits) || (has(self.resources.requests) && 'cpu' in self.resources.requests)) && ((has(self.resources.limits) && 'memory' in self.resources.limits) || (has(self.resources.requests) && 'memory' in self.resources.requests)))",message="mysql.resources must set cpu and memory (via limits or requests) when mysql.autoconfig.enabled is true"
// +kubebuilder:validation:XValidation:rule="!(self.?autoconfig.?enabled.orValue(false)) || sign(quantity(has(self.resources.limits) && 'cpu' in self.resources.limits ? self.resources.limits['cpu'] : self.resources.requests['cpu'])) == 1",message="mysql.resources cpu must be greater than 0 when mysql.autoconfig.enabled is true"
// +kubebuilder:validation:XValidation:rule="!(self.?autoconfig.?enabled.orValue(false)) || quantity(has(self.resources.limits) && 'memory' in self.resources.limits ? self.resources.limits['memory'] : self.resources.requests['memory']).compareTo(quantity('12Mi')) >= 0",message="mysql.resources memory must be at least 12Mi when mysql.autoconfig.enabled is true"
// +kubebuilder:validation:XValidation:rule="!(self.?autoconfig.?enabled.orValue(false)) || (has(self.autoconfig.version) && size(self.autoconfig.version) > 0)",message="mysql.autoconfig.version is required when mysql.autoconfig.enabled is true"
type MySQLSpec struct {
// +kubebuilder:validation:Enum=group-replication;async
// +kubebuilder:default=group-replication
ClusterType ClusterType `json:"clusterType,omitempty"`
Bootstrap BootstrapConfig `json:"bootstrap,omitempty"`
AutoConfig AutoConfigSpec `json:"autoconfig,omitempty"`
ExposePrimary ServiceExposeTogglable `json:"exposePrimary,omitempty"`
Expose ServiceExposeTogglable `json:"expose,omitempty"`
AutoRecovery bool `json:"autoRecovery,omitempty"`
Expand All @@ -266,6 +263,31 @@ type MySQLSpec struct {
PodSpec `json:",inline"`
}

type AutoConfigLoadType string

const (
AutoConfigLoadTypeMostlyReads AutoConfigLoadType = "mostlyReads"
AutoConfigLoadTypeSomeWrites AutoConfigLoadType = "someWrites"
AutoConfigLoadTypeEqualReadsWrites AutoConfigLoadType = "equalReadsWrites"
AutoConfigLoadTypeHeavyWrites AutoConfigLoadType = "heavyWrites"
)

type AutoConfigSpec struct {
Enabled *bool `json:"enabled,omitempty"`
// +kubebuilder:validation:Enum=mostlyReads;someWrites;equalReadsWrites;heavyWrites
LoadType AutoConfigLoadType `json:"loadType,omitempty"`
// Version is the MySQL version the configuration is calculated for. It is
// required when autoconfig is enabled and never changes which server is
// deployed; it only tells the calculator which parameters exist.
// +kubebuilder:validation:Pattern=`^\d+\.\d+(\.\d+)?$`
Version string `json:"version,omitempty"`
Comment on lines +282 to +283
}

// IsEnabled reports whether autoconfig is turned on.
func (s AutoConfigSpec) IsEnabled() bool {
return s.Enabled != nil && *s.Enabled
}

type BootstrapMode string

const (
Expand Down Expand Up @@ -1177,6 +1199,10 @@ func (cr *PerconaServerMySQL) CheckNSetDefaults(_ context.Context, serverVersion
return errors.Errorf("%s is not a valid clusterType, valid options are %s and %s", cr.Spec.MySQL.ClusterType, ClusterTypeGR, ClusterTypeAsync)
}

if cr.Spec.MySQL.AutoConfig.IsEnabled() && cr.Spec.MySQL.AutoConfig.LoadType == "" {
cr.Spec.MySQL.AutoConfig.LoadType = AutoConfigLoadTypeSomeWrites
}

if err := cr.validateStorageAutoscaling(); err != nil {
return errors.Wrap(err, "validate storage autoscaling")
}
Expand Down Expand Up @@ -1611,16 +1637,6 @@ func (cr *PerconaServerMySQL) ClusterHint() string {
return fmt.Sprintf("%s.%s", cr.Name, cr.Namespace)
}

// GetClusterNameFromObject retrieves the cluster's name from the given client object's labels.
func GetClusterNameFromObject(obj client.Object) (string, error) {
labels := obj.GetLabels()
instance, ok := labels[naming.LabelInstance]
if !ok {
return "", errors.Errorf("label %s doesn't exist", naming.LabelInstance)
}
return instance, nil
}

// FNVHash computes a hash of the provided byte slice using the FNV-1a algorithm.
func FNVHash(p []byte) string {
hash := fnv.New32()
Expand Down
21 changes: 21 additions & 0 deletions api/v1/zz_generated.deepcopy.go

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

23 changes: 23 additions & 0 deletions cmd/bootstrap/gr/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gr

import (
"fmt"
"os"
"strconv"
"strings"

Expand All @@ -10,6 +11,28 @@ import (
"github.com/pkg/errors"
)

// readMyCnf returns the [mysqld] section of the first path that exists, or nil
// when none do.
func readMyCnf(paths ...string) (*ini.Section, error) {
for _, path := range paths {
f, err := os.Open(path)
if errors.Is(err, os.ErrNotExist) {
continue
}
if err != nil {
return nil, errors.Wrapf(err, "open %s", path)
}
defer f.Close() //nolint

section, err := config.ParseSection(f, "mysqld")
if err != nil {
return nil, errors.Wrapf(err, "failed to parse %s", path)
}
return section, nil
}
return nil, nil
}

// these are options that mysql-shell overwrites
// without taking my.cnf into consideration
type createClusterOpts struct {
Expand Down
82 changes: 82 additions & 0 deletions cmd/bootstrap/gr/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gr
import (
"bytes"
"io"
"os"
"testing"

"github.com/go-ini/ini"
Expand Down Expand Up @@ -214,3 +215,84 @@ func TestGetConfigureInstanceOpts(t *testing.T) {
})
}
}

func TestReadMyCnf(t *testing.T) {
const (
userConf = "[mysqld]\nreplica_parallel_workers=9\n"
autoConf = "\nreplica_parallel_workers=5\n"
)

tests := map[string]struct {
setup func(t *testing.T) []string

wantWorkers string
wantNil bool
wantErrMsg string
}{
"neither file exists": {
setup: func(t *testing.T) []string { return []string{"/nonexistent/my.cnf", "/nonexistent/auto.cnf"} },
wantNil: true,
},
"only the auto-config exists": {
setup: func(t *testing.T) []string {
dir := t.TempDir()
return []string{dir + "/my.cnf", writeCnf(t, dir+"/auto.cnf", autoConf)}
},
wantWorkers: "5",
},
"only the user configuration exists": {
setup: func(t *testing.T) []string {
dir := t.TempDir()
return []string{writeCnf(t, dir+"/my.cnf", userConf), dir + "/auto.cnf"}
},
wantWorkers: "9",
},
"the user configuration wins over the auto-config": {
setup: func(t *testing.T) []string {
dir := t.TempDir()
return []string{writeCnf(t, dir+"/my.cnf", userConf), writeCnf(t, dir+"/auto.cnf", autoConf)}
},
wantWorkers: "9",
},
"an unreadable file is an error rather than a silent fallback": {
setup: func(t *testing.T) []string {
dir := t.TempDir()
notADir := writeCnf(t, dir+"/my.cnf", userConf)
return []string{notADir + "/nested.cnf", writeCnf(t, dir+"/auto.cnf", autoConf)}
},
wantErrMsg: "open",
},
"a malformed file is an error": {
setup: func(t *testing.T) []string {
dir := t.TempDir()
return []string{writeCnf(t, dir+"/my.cnf", "[mysqld\nbroken")}
},
wantErrMsg: "failed to parse",
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
got, err := readMyCnf(tc.setup(t)...)
if tc.wantErrMsg != "" {
require.ErrorContains(t, err, tc.wantErrMsg)
return
}
require.NoError(t, err)
if tc.wantNil {
assert.Nil(t, got)
return
}
require.NotNil(t, got)
value, err := config.GetKeyValue(got, "replica_parallel_workers")
require.NoError(t, err)
assert.Equal(t, tc.wantWorkers, value)
})
}
}

func writeCnf(t *testing.T, path, content string) string {
t.Helper()
require.NoError(t, os.WriteFile(path, []byte(content), 0o600))
return path
}
15 changes: 4 additions & 11 deletions cmd/bootstrap/gr/group_replication.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,8 @@ import (
"slices"
"strings"

"github.com/go-ini/ini"
_ "github.com/go-sql-driver/mysql"
v "github.com/hashicorp/go-version"
"github.com/percona/percona-server-mysql-operator/pkg/config"
"github.com/percona/percona-server-mysql-operator/pkg/mysql"
"github.com/pkg/errors"
"github.com/sjmudd/stopwatch"
"k8s.io/apimachinery/pkg/util/sets"
Expand All @@ -30,6 +27,7 @@ import (
"github.com/percona/percona-server-mysql-operator/cmd/bootstrap/utils"
database "github.com/percona/percona-server-mysql-operator/cmd/internal/db"
"github.com/percona/percona-server-mysql-operator/pkg/innodbcluster"
"github.com/percona/percona-server-mysql-operator/pkg/mysql"
"github.com/percona/percona-server-mysql-operator/pkg/util"
)

Expand Down Expand Up @@ -448,14 +446,9 @@ func Bootstrap(ctx context.Context) error {
log.Printf("WARNING: failed to clear group_replication_group_seeds: %v", err)
}

var myCnf *ini.Section
customMyCnf, err := os.Open(mysql.CustomMyCnfPath)
if err == nil {
defer customMyCnf.Close() //nolint
myCnf, err = config.ParseSection(customMyCnf, "mysqld")
if err != nil {
return errors.Wrapf(err, "failed to parse %s", mysql.CustomMyCnfPath)
}
myCnf, err := readMyCnf(mysql.CustomMyCnfPath, mysql.AutoConfigCnfPath)
if err != nil {
return err
}

configureOpts, err := getConfigureInstanceOpts(myCnf)
Expand Down
6 changes: 5 additions & 1 deletion cmd/example-gen/pkg/defaults/manual.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,16 @@ func ManualCluster(cr *apiv1.PerconaServerMySQL) {
}

func mysqlDefaults(spec *apiv1.MySQLSpec) {
podSpecDefaults(&spec.PodSpec, ImageMySQL, resources("2Gi", "", "4Gi", ""), configurationMySQL, 600, envList("BOOTSTRAP_READ_TIMEOUT", "600", "ASYNC_SOURCE_RETRY_COUNT", "3", "ASYNC_SOURCE_CONNECT_RETRY", "60"), envFromList("mysql-env-secret"))
podSpecDefaults(&spec.PodSpec, ImageMySQL, resources("2Gi", "1", "4Gi", "2"), configurationMySQL, 600, envList("BOOTSTRAP_READ_TIMEOUT", "600", "ASYNC_SOURCE_RETRY_COUNT", "3", "ASYNC_SOURCE_CONNECT_RETRY", "60"), envFromList("mysql-env-secret"))

spec.AutoRecovery = true
spec.VolumeSpec = nil
spec.ExposePrimary.Enabled = true

spec.AutoConfig.Enabled = new(true)
spec.AutoConfig.LoadType = apiv1.AutoConfigLoadTypeSomeWrites
spec.AutoConfig.Version = "8.4"

spec.Bootstrap.Mode = new(apiv1.BootstrapModeAuto)

c := func(name string) corev1.Container {
Expand Down
2 changes: 1 addition & 1 deletion cmd/example-gen/pkg/defaults/preset.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func FromPresets(cr any) error {
},
Resources: corev1.VolumeResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceStorage: resource.MustParse("2Gi"),
corev1.ResourceStorage: resource.MustParse("10Gi"),
},
},
},
Expand Down
Loading