diff --git a/docs/development.md b/docs/development.md index e1c37ad04..4a04d1bc0 100644 --- a/docs/development.md +++ b/docs/development.md @@ -20,6 +20,10 @@ in the format of `$USER-TIMESTAMP`. This will need to be remembered as this is needed for a latter step or can be exported as the `$MYSQL_AGENT_VERSION` envrionment variable. +```bash +$ export MYSQL_AGENT_VERSION=$(cat dist/version.txt) +``` + ## Create a namespace Create the namespace that the operator will reside in. By default this is @@ -38,11 +42,13 @@ ServiceAccounts, ClusterRoles, and ClusterRoleBindings for the operator to function. ```bash -$ kubectl -n $USER apply \ - -f contrib/manifests/custom-resource-definitions.yaml \ - -f contrib/manifests/rbac.yaml -$ sed -e "s//${USER}/g" \ - contrib/manifests/role-binding-template.yaml | kubectl -n $USER apply -f - +$ kubectl -n $USER apply -f contrib/manifests/custom-resource-definitions.yaml +``` +```bash +$ sed -e "s//${USER}/g" contrib/manifests/rbac.yaml | kubectl -n $USER apply -f - +``` +```bash +$ sed -e "s//${USER}/g" contrib/manifests/role-binding-template.yaml | kubectl -n $USER apply -f - ``` ### Run the MySQL Operator @@ -54,9 +60,6 @@ development purposes. $ make run-dev ``` -If you did not set an envrionment variable previously, prefix this command with -`MYSQL_AGENT_VERSION=` followed by the $USER-TIMESTAMP fortmatted version. - ## Creating an InnoDB cluster For the purpose of this document, we will create a cluster with 3 members with diff --git a/docs/enterprise-edition-example.md b/docs/enterprise-edition-example.md new file mode 100644 index 000000000..514d24807 --- /dev/null +++ b/docs/enterprise-edition-example.md @@ -0,0 +1,53 @@ +# Enterprise edition tutorial +This tutorial will explain how to create a MySQL cluster that runs the enterprise version of MySQL. + +## Prerequisites + +- The MySQL operator repository checked out locally. +- Access to a Docker registry that contains the enterprise version of MySQL. + +## 01 - Create the Operator +You will need to create the following: + +1. Custom resources +2. RBAC configuration * +3. The Operator +4. The Agent ServiceAccount & RoleBinding + +The creation of these resources can be achieved by following the [introductory tutorial][1]; return here before creating a MySQL cluster. + +## 02 - Create a secret with registry credentials +To be able to pull the MySQL Enterprise Edition from Docker it is necessary to provide credentials, these credentials must be supplied in the form of a Kubernetes secret. + +- Remember the name of the secret *myregistrykey* as this will need to be used in step 03 when creating the cluster. +- If you are pulling the MySQL Enterprise image from a different registry than the one in the example then the secret must contain the relevant credentials for that registry. + +>For alternative ways to create Kubernetes secrets see their documentation on [creating secrets from Docker configs](https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod) or [creating secrets manually](https://kubernetes.io/docs/concepts/containers/images/#creating-a-secret-with-a-docker-config). + +Enter your credentials into the following command and execute it to create a Kubernetes secret that will enable pulling images from the Docker store. +``` +kubectl create secret docker-registry myregistrykey \ +--docker-server=https://index.docker.io/v1/ \ +--docker-username= \ +--docker-password= \ +--docker-email= +``` +## 03 - Create your MySQL Cluster +Finally, create your MySQL Cluster with the required specifications entered under `spec:` + +- The `repository:` field should be the path to a Docker registry containing the enterprise edition of MySQL. If this is omitted, the default is taken from the MySQL operator field `defaultMysqlServer:` which you can also specify. +- The `imagePullSecrets`: field allows you to specify a list of Kubernetes secret names. These secret(s) should contain your credentials for the Docker registry. +- The version to be used should be specified, without this, a default version is used which is **not** guaranteed to match an available image of MySQL Enterprise. +- The namespace of the cluster must match the namespace of the RBAC permissions created in step 01. +``` +kubectl apply -f examples/cluster/cluster-enterprise-version.yaml +``` +### Check that it is running +You can now run the following command to access the SQL prompt in your MySQL Cluster, just replace `` with the namespace you created your cluster in. +``` +sh hack/mysql.sh /mysql-0 +``` + +>*If you run into issues when creating RBAC roles see [Access controls](https://docs.cloud.oracle.com/iaas/Content/ContEng/Concepts/contengabouta]ccesscontrol.htm?) for more information. + +[1]: docs/tutorial.md diff --git a/examples/cluster/cluster-enterprise-version.yaml b/examples/cluster/cluster-enterprise-version.yaml new file mode 100644 index 000000000..0db356241 --- /dev/null +++ b/examples/cluster/cluster-enterprise-version.yaml @@ -0,0 +1,9 @@ +apiVersion: mysql.oracle.com/v1alpha1 +kind: Cluster +metadata: + name: mysql-enterprise +spec: + version: "8.0.11" + repository: store/oracle/mysql-enterprise-server + imagePullSecrets: + - name: myregistrykey diff --git a/pkg/apis/mysql/v1alpha1/cluster_test.go b/pkg/apis/mysql/v1alpha1/cluster_test.go index eb01c7703..80e74ebf7 100644 --- a/pkg/apis/mysql/v1alpha1/cluster_test.go +++ b/pkg/apis/mysql/v1alpha1/cluster_test.go @@ -85,8 +85,8 @@ func TestDefaultVersion(t *testing.T) { cluster := &Cluster{} cluster.EnsureDefaults() - if cluster.Spec.Version != defaultVersion { - t.Errorf("Expected default version to be %s but got %s", defaultVersion, cluster.Spec.Version) + if cluster.Spec.Version != DefaultVersion { + t.Errorf("Expected default version to be %s but got %s", DefaultVersion, cluster.Spec.Version) } } diff --git a/pkg/apis/mysql/v1alpha1/helpers.go b/pkg/apis/mysql/v1alpha1/helpers.go index dd9398b9d..df25b1465 100644 --- a/pkg/apis/mysql/v1alpha1/helpers.go +++ b/pkg/apis/mysql/v1alpha1/helpers.go @@ -20,13 +20,15 @@ import ( ) const ( - // The default MySQL version to use if not specified explicitly by user - defaultVersion = "8.0.12" + // DefaultVersion is the MySQL version to use if not specified explicitly by user + DefaultVersion = "8.0.12" defaultMembers = 3 defaultBaseServerID = 1000 // maxBaseServerID is the maximum safe value for BaseServerID calculated // as max MySQL server_id value - max Replication Group size. maxBaseServerID uint32 = 4294967295 - 9 + // MysqlServer is the image to use if no image is specified explicitly by the user. + MysqlServer = "mysql/mysql-server" ) const ( @@ -50,10 +52,10 @@ func getOperatorVersionLabel(labelMap map[string]string) string { return labelMap[constants.MySQLOperatorVersionLabel] } -// EnsureDefaults will ensure that if a user omits and fields in the +// EnsureDefaults will ensure that if a user omits any fields in the // spec that are required, we set some sensible defaults. -// For example a user can choose to omit the version -// and number of members. +// For example a user can choose to omit the version and number of +// members. func (c *Cluster) EnsureDefaults() *Cluster { if c.Spec.Members == 0 { c.Spec.Members = defaultMembers @@ -64,7 +66,7 @@ func (c *Cluster) EnsureDefaults() *Cluster { } if c.Spec.Version == "" { - c.Spec.Version = defaultVersion + c.Spec.Version = DefaultVersion } return c diff --git a/pkg/apis/mysql/v1alpha1/types.go b/pkg/apis/mysql/v1alpha1/types.go index 7d028c961..dcc8735cf 100644 --- a/pkg/apis/mysql/v1alpha1/types.go +++ b/pkg/apis/mysql/v1alpha1/types.go @@ -27,6 +27,11 @@ const MinimumMySQLVersion = "8.0.11" type ClusterSpec struct { // Version defines the MySQL Docker image version. Version string `json:"version"` + // Repository defines the image repository from which to pull the MySQL server image. + Repository string `json:"repository"` + // ImagePullSecret defines the name of the secret that contains the + // required credentials for pulling from the specified Repository. + ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecret"` // Members defines the number of MySQL instances in a cluster Members int32 `json:"members,omitempty"` // BaseServerID defines the base number used to create unique server_id diff --git a/pkg/controllers/cluster/controller.go b/pkg/controllers/cluster/controller.go index 9db425ffb..7d4eb874c 100644 --- a/pkg/controllers/cluster/controller.go +++ b/pkg/controllers/cluster/controller.go @@ -324,6 +324,10 @@ func (m *MySQLController) syncHandler(key string) error { return errors.Wrap(err, "validating Cluster") } + if cluster.Spec.Repository == "" { + cluster.Spec.Repository = m.opConfig.Images.DefaultMySQLServerImage + } + operatorVersion := buildversion.GetBuildVersion() // Ensure that the required labels are set on the cluster. sel := combineSelectors(SelectorForCluster(cluster), SelectorForClusterOperatorVersion(operatorVersion)) diff --git a/pkg/options/operator/options.go b/pkg/options/operator/options.go index c337dfb8e..504d14f3a 100644 --- a/pkg/options/operator/options.go +++ b/pkg/options/operator/options.go @@ -15,12 +15,14 @@ package operator import ( + "fmt" "io/ioutil" "os" "path/filepath" "time" "github.com/golang/glog" + "github.com/oracle/mysql-operator/pkg/apis/mysql/v1alpha1" "github.com/pkg/errors" "github.com/spf13/pflag" "gopkg.in/yaml.v2" @@ -29,15 +31,15 @@ import ( ) const ( - mysqlServer = "mysql/mysql-server" - mysqlAgent = "iad.ocir.io/oracle/mysql-agent" + mysqlAgent = "iad.ocir.io/oracle/mysql-agent" ) // Images is the configuration of required MySQLOperator images. Remember to configure the appropriate -// credentials for the target repositories. +// credentials for the target repositories. The DefaultMySQLServerImage can be overridden on a per-cluster +// basis by setting the Repository field. type Images struct { - MySQLServerImage string `yaml:"mysqlServer"` - MySQLAgentImage string `yaml:"mysqlAgent"` + MySQLAgentImage string `yaml:"mysqlAgent"` + DefaultMySQLServerImage string `yaml:"defaultMysqlServer"` } // MySQLOperatorOpts holds the options for the MySQLOperator. @@ -65,7 +67,7 @@ type MySQLOperatorOpts struct { MinResyncPeriod metav1.Duration `yaml:"minResyncPeriod"` } -// MySQLOperatorOpts will create a new MySQLOperatorOpts. If a valid +// NewMySQLOperatorOpts will create a new MySQLOperatorOpts. If a valid // config file is specified and exists, it will be used to initialise the // server. Otherwise, a default server will be created. // @@ -106,12 +108,12 @@ func (s *MySQLOperatorOpts) EnsureDefaults() { if &s.Images == nil { s.Images = Images{} } - if s.Images.MySQLServerImage == "" { - s.Images.MySQLServerImage = mysqlServer - } if s.Images.MySQLAgentImage == "" { s.Images.MySQLAgentImage = mysqlAgent } + if s.Images.DefaultMySQLServerImage == "" { + s.Images.DefaultMySQLServerImage = v1alpha1.MysqlServer + } if s.MinResyncPeriod.Duration <= 0 { s.MinResyncPeriod = metav1.Duration{Duration: 12 * time.Hour} } @@ -122,7 +124,7 @@ func (s *MySQLOperatorOpts) AddFlags(fs *pflag.FlagSet) *pflag.FlagSet { fs.StringVar(&s.KubeConfig, "kubeconfig", s.KubeConfig, "Path to Kubeconfig file with authorization and master location information.") fs.StringVar(&s.Master, "master", s.Master, "The address of the Kubernetes API server (overrides any value in kubeconfig).") fs.StringVar(&s.Namespace, "namespace", metav1.NamespaceAll, "The namespace for which the MySQL operator manages MySQL clusters. Defaults to all.") - fs.StringVar(&s.Images.MySQLServerImage, "mysql-server-image", s.Images.MySQLServerImage, "The name of the target 'mysql-server' image. Defaults to: mysql/mysql-server.") + fs.StringVar(&s.Images.DefaultMySQLServerImage, "mysql-server-image", s.Images.DefaultMySQLServerImage, fmt.Sprintf("The default image repository to pull the MySQL Server image from (can be overridden on a per-cluster basis). Defaults to: %q.", v1alpha1.MysqlServer)) fs.StringVar(&s.Images.MySQLAgentImage, "mysql-agent-image", s.Images.MySQLAgentImage, "The name of the target 'mysql-agent' image. Defaults to: iad.ocir.io/oracle/mysql-agent.") fs.DurationVar(&s.MinResyncPeriod.Duration, "min-resync-period", s.MinResyncPeriod.Duration, "The resync period in reflectors will be random between MinResyncPeriod and 2*MinResyncPeriod.") return fs diff --git a/pkg/options/operator/options_test.go b/pkg/options/operator/options_test.go index 95a4b1132..b3eb1f643 100644 --- a/pkg/options/operator/options_test.go +++ b/pkg/options/operator/options_test.go @@ -36,9 +36,6 @@ func assertRequiredDefaults(t *testing.T, s MySQLOperatorOpts) { if &s.Images == nil { t.Error("MySQLOperatorServer.Images: was nil, expected a valid configuration.") } - if s.Images.MySQLServerImage != mysqlServer { - t.Errorf("MySQLOperatorServer.Images.MySQLServerImage: was '%s', expected '%s'.", s.Images.MySQLServerImage, mysqlServer) - } if s.Images.MySQLAgentImage != mysqlAgent { t.Errorf("MySQLOperatorServer.Images.MySQLAgentImage: was '%s', expected '%s'.", s.Images.MySQLAgentImage, mysqlAgent) } @@ -66,8 +63,8 @@ func mockMySQLOperatorOpts() MySQLOperatorOpts { Master: "some-master", Hostname: "some-hostname", Images: Images{ - MySQLServerImage: "some-mysql-img", - MySQLAgentImage: "some-agent-img", + MySQLAgentImage: "some-agent-img", + DefaultMySQLServerImage: "mysql/mysql-server", }, MinResyncPeriod: v1.Duration{Duration: 42}, } diff --git a/pkg/resources/statefulsets/statefulset.go b/pkg/resources/statefulsets/statefulset.go index f97436adc..ebfea7a10 100644 --- a/pkg/resources/statefulsets/statefulset.go +++ b/pkg/resources/statefulsets/statefulset.go @@ -345,7 +345,7 @@ func NewForCluster(cluster *v1alpha1.Cluster, images operatoropts.Images, servic } containers := []v1.Container{ - mysqlServerContainer(cluster, images.MySQLServerImage, rootPassword, members, baseServerID), + mysqlServerContainer(cluster, cluster.Spec.Repository, rootPassword, members, baseServerID), mysqlAgentContainer(cluster, images.MySQLAgentImage, rootPassword, members)} podLabels := map[string]string{ @@ -399,6 +399,9 @@ func NewForCluster(cluster *v1alpha1.Cluster, images operatoropts.Images, servic }, } + if cluster.Spec.ImagePullSecrets != nil { + ss.Spec.Template.Spec.ImagePullSecrets = append(ss.Spec.Template.Spec.ImagePullSecrets, cluster.Spec.ImagePullSecrets...) + } if cluster.Spec.VolumeClaimTemplate != nil { ss.Spec.VolumeClaimTemplates = append(ss.Spec.VolumeClaimTemplates, *cluster.Spec.VolumeClaimTemplate) } diff --git a/pkg/resources/statefulsets/statefulset_test.go b/pkg/resources/statefulsets/statefulset_test.go index 650d14d0c..7d8544c05 100644 --- a/pkg/resources/statefulsets/statefulset_test.go +++ b/pkg/resources/statefulsets/statefulset_test.go @@ -286,4 +286,40 @@ func TestClusterWithOnlyMysqlServerResourceRequirements(t *testing.T) { assert.Equal(t, mysqlServerResourceRequirements, statefulSet.Spec.Template.Spec.Containers[0].Resources, "MySQL-Server container resource requirements do not match expected.") assert.Nil(t, statefulSet.Spec.Template.Spec.Containers[1].Resources.Limits, "MySQL-Agent container has resource limits set which were not initially defined in the spec") assert.Nil(t, statefulSet.Spec.Template.Spec.Containers[1].Resources.Requests, "MySQL-Agent container has resource requests set which were not initially defined in the spec") + +} + +func TestClusterEnterpriseImage(t *testing.T) { + cluster := &v1alpha1.Cluster{ + Spec: v1alpha1.ClusterSpec{ + Repository: "some/image/path", + ImagePullSecrets: []corev1.LocalObjectReference{{ + Name: "someSecretName", + }}, + }, + } + cluster.EnsureDefaults() + + statefulSet := NewForCluster(cluster, mockOperatorConfig().Images, "mycluster") + + pullSecrets := statefulSet.Spec.Template.Spec.ImagePullSecrets + ps := pullSecrets[len(pullSecrets)-1] + si := statefulSet.Spec.Template.Spec.Containers[0].Image + + assert.Equal(t, "someSecretName", ps.Name) + assert.Equal(t, "some/image/path:"+v1alpha1.DefaultVersion, si) +} + +func TestClusterDefaultOverride(t *testing.T) { + cluster := &v1alpha1.Cluster{} + cluster.EnsureDefaults() + cluster.Spec.Repository = "OverrideDefaultImage" + + operatorConf := mockOperatorConfig() + operatorConf.Images.DefaultMySQLServerImage = "newDefaultImage" + statefulSet := NewForCluster(cluster, operatorConf.Images, "mycluster") + + si := statefulSet.Spec.Template.Spec.Containers[0].Image + + assert.Equal(t, "OverrideDefaultImage:"+v1alpha1.DefaultVersion, si) }