diff --git a/Dockerfile b/Dockerfile index 5f63041f..4219942e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,6 +15,7 @@ COPY cmd/manager/main.go cmd/manager/main.go COPY api/ api/ COPY cluster/ cluster/ COPY controllers/ controllers/ +COPY backup/ backup/ COPY internal/ internal/ COPY utils/ utils/ diff --git a/Dockerfile.sidecar b/Dockerfile.sidecar index a29933a4..45d97a9a 100644 --- a/Dockerfile.sidecar +++ b/Dockerfile.sidecar @@ -8,6 +8,7 @@ WORKDIR /workspace # Copy the Go Modules manifests COPY go.mod go.mod COPY go.sum go.sum + # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer RUN go env -w GOPROXY=https://goproxy.cn,direct; \ @@ -24,12 +25,32 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o bin/sidecar cmd/sidecar ############################################################################### # Docker image for Sidecar ############################################################################### -FROM alpine:3.13 +FROM ubuntu:focal + +RUN set -ex; \ + groupadd --gid 1001 --system mysql; \ + useradd \ + --uid 1001 \ + --system \ + --home-dir /var/lib/mysql \ + --no-create-home \ + --gid mysql \ + mysql; -# Create a group and user -RUN addgroup -g 1001 mysql && adduser -u 1001 -g 1001 -S mysql +ENV PS_VERSION 5.7.33-36-1 +ENV OS_VER focal +ENV FULL_PERCONA_VERSION "$PS_VERSION.$OS_VER" + +ARG XTRABACKUP_PKG=percona-xtrabackup-24 +RUN set -ex; \ + apt-get update; \ + apt-get install -y --no-install-recommends gnupg2 wget lsb-release curl; \ + wget -P /tmp --no-check-certificate https://repo.percona.com/apt/percona-release_latest.$(lsb_release -sc)_all.deb; \ + dpkg -i /tmp/percona-release_latest.$(lsb_release -sc)_all.deb; \ + apt-get update; \ + apt-get install -y --no-install-recommends ${XTRABACKUP_PKG}; \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* WORKDIR / COPY --from=builder /workspace/bin/sidecar /usr/local/bin/sidecar - ENTRYPOINT ["sidecar"] diff --git a/PROJECT b/PROJECT index 06099a46..3fb29742 100644 --- a/PROJECT +++ b/PROJECT @@ -18,4 +18,13 @@ resources: group: mysql kind: Status version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: radondb.com + group: mysql + kind: Backup + path: github.com/radondb/radondb-mysql-kubernetes/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/v1alpha1/backup_types.go b/api/v1alpha1/backup_types.go new file mode 100644 index 00000000..6a0af860 --- /dev/null +++ b/api/v1alpha1/backup_types.go @@ -0,0 +1,108 @@ +/* +Copyright 2021 RadonDB. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// BackupSpec defines the desired state of Backup +type BackupSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // To specify the image that will be used for sidecar container. + // +optional + // +kubebuilder:default:="acekingke/sidecar:0.1" + Image string `json:"image"` + + // HostName represents the host for which to take backup + HostName string `json:"hostname"` + + // Cluster represents the cluster name to backup + ClusterName string `json:"clustname"` + + // History Limit of job + // +optional + // +kubebuilder:default:=3 + HistoryLimit *int32 `json:"historyLimit,omitempty"` +} + +// BackupStatus defines the observed state of Backup +type BackupStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Completed represents the backup has finished + Completed bool `json:"completed,omitempty"` + + // Conditions represents the backup resource conditions list. + Conditions []BackupCondition `json:"conditions,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Backup is the Schema for the backups API +type Backup struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec BackupSpec `json:"spec,omitempty"` + Status BackupStatus `json:"status,omitempty"` +} + +// BackupCondition defines condition struct for backup resource +type BackupCondition struct { + // type of cluster condition, values in (\"Ready\") + Type BackupConditionType `json:"type"` + // Status of the condition, one of (\"True\", \"False\", \"Unknown\") + Status corev1.ConditionStatus `json:"status"` + // LastTransitionTime + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + // Reason + Reason string `json:"reason"` + // Message + Message string `json:"message"` +} + +// BackupConditionType defines condition types of a backup resources +type BackupConditionType string + +const ( + // BackupComplete means the backup has finished his execution + BackupComplete BackupConditionType = "Complete" + // BackupFailed means backup has failed + BackupFailed BackupConditionType = "Failed" +) + +//+kubebuilder:object:root=true + +// BackupList contains a list of Backup +type BackupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Backup `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Backup{}, &BackupList{}) +} diff --git a/api/v1alpha1/cluster_types.go b/api/v1alpha1/cluster_types.go index 2d78e2d5..30ea3bd8 100644 --- a/api/v1alpha1/cluster_types.go +++ b/api/v1alpha1/cluster_types.go @@ -72,6 +72,15 @@ type ClusterSpec struct { // +optional // +kubebuilder:default:={enabled: true, accessModes: {"ReadWriteOnce"}, size: "10Gi"} Persistence Persistence `json:"persistence,omitempty"` + + // Represents the name of the secret that contains credentials to connect to + // the storage provider to store backups. + // +optional + BackupSecretName string `json:"backupSecretName,omitempty"` + + // Represents the name of the cluster restore from backup path + // +optional + RestoreFrom string `json:"restoreFrom,omitempty"` } // MysqlOpts defines the options of MySQL container. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 57887b88..220c4523 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -25,6 +25,123 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Backup) DeepCopyInto(out *Backup) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Backup. +func (in *Backup) DeepCopy() *Backup { + if in == nil { + return nil + } + out := new(Backup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Backup) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupCondition) DeepCopyInto(out *BackupCondition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupCondition. +func (in *BackupCondition) DeepCopy() *BackupCondition { + if in == nil { + return nil + } + out := new(BackupCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupList) DeepCopyInto(out *BackupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Backup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupList. +func (in *BackupList) DeepCopy() *BackupList { + if in == nil { + return nil + } + out := new(BackupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BackupList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupSpec) DeepCopyInto(out *BackupSpec) { + *out = *in + if in.HistoryLimit != nil { + in, out := &in.HistoryLimit, &out.HistoryLimit + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupSpec. +func (in *BackupSpec) DeepCopy() *BackupSpec { + if in == nil { + return nil + } + out := new(BackupSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupStatus) DeepCopyInto(out *BackupStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]BackupCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupStatus. +func (in *BackupStatus) DeepCopy() *BackupStatus { + if in == nil { + return nil + } + out := new(BackupStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Cluster) DeepCopyInto(out *Cluster) { *out = *in diff --git a/backup/backup.go b/backup/backup.go new file mode 100644 index 00000000..a0667199 --- /dev/null +++ b/backup/backup.go @@ -0,0 +1,55 @@ +/* +Copyright 2021 RadonDB. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package backup + +import ( + "fmt" + + "github.com/radondb/radondb-mysql-kubernetes/utils" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + v1alhpa1 "github.com/radondb/radondb-mysql-kubernetes/api/v1alpha1" +) + +var log = logf.Log.WithName("backup") + +// Backup is a type wrapper over Backup that contains the Business logic +type Backup struct { + *v1alhpa1.Backup +} + +// New returns a wraper object over Backup +func New(backup *v1alhpa1.Backup) *Backup { + return &Backup{ + Backup: backup, + } +} + +// Unwrap returns the api backup object +func (b *Backup) Unwrap() *v1alhpa1.Backup { + return b.Backup +} + +// GetNameForJob returns the name of the job +func (b *Backup) GetNameForJob() string { + return fmt.Sprintf("%s-backup", b.Name) +} + +func (b *Backup) GetBackupURL(cluster_name string, hostname string) string { + return fmt.Sprintf("%s.%s-mysql.%s:%v", hostname, cluster_name, b.Namespace, utils.XBackupPort) + +} diff --git a/backup/status.go b/backup/status.go new file mode 100644 index 00000000..17fa3957 --- /dev/null +++ b/backup/status.go @@ -0,0 +1,88 @@ +/* +Copyright 2021 RadonDB. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package backup + +import ( + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + apiv1alpha1 "github.com/radondb/radondb-mysql-kubernetes/api/v1alpha1" +) + +// UpdateStatusCondition sets the condition to a status. +// for example Ready condition to True, or False +func (c *Backup) UpdateStatusCondition(condType apiv1alpha1.BackupConditionType, + status corev1.ConditionStatus, reason, msg string) { + newCondition := apiv1alpha1.BackupCondition{ + Type: condType, + Status: status, + Reason: reason, + Message: msg, + } + + t := time.Now() + + if len(c.Status.Conditions) == 0 { + log.V(4).Info(fmt.Sprintf("Setting lastTransitionTime for mysql backup "+ + "%q condition %q to %v", c.Name, condType, t)) + newCondition.LastTransitionTime = metav1.NewTime(t) + c.Status.Conditions = []apiv1alpha1.BackupCondition{newCondition} + } else { + if i, exist := c.condExists(condType); exist { + cond := c.Status.Conditions[i] + if cond.Status != newCondition.Status { + log.V(3).Info(fmt.Sprintf("Found status change for mysql backup "+ + "%q condition %q: %q -> %q; setting lastTransitionTime to %v", + c.Name, condType, cond.Status, status, t)) + newCondition.LastTransitionTime = metav1.NewTime(t) + } else { + newCondition.LastTransitionTime = cond.LastTransitionTime + } + log.V(4).Info(fmt.Sprintf("Setting lastTransitionTime for mysql backup "+ + "%q condition %q to %q", c.Name, condType, status)) + c.Status.Conditions[i] = newCondition + } else { + log.V(4).Info(fmt.Sprintf("Setting new condition for mysql backup %q, condition %q to %q", + c.Name, condType, status)) + newCondition.LastTransitionTime = metav1.NewTime(t) + c.Status.Conditions = append(c.Status.Conditions, newCondition) + } + } +} + +func (c *Backup) condExists(ty apiv1alpha1.BackupConditionType) (int, bool) { + for i, cond := range c.Status.Conditions { + if cond.Type == ty { + return i, true + } + } + + return 0, false +} + +// GetBackupCondition returns a pointer to the condition of the provided type +func (c *Backup) GetBackupCondition(condType apiv1alpha1.BackupConditionType) *apiv1alpha1.BackupCondition { + i, found := c.condExists(condType) + if found { + return &c.Status.Conditions[i] + } + + return nil +} diff --git a/backup/syncer/job.go b/backup/syncer/job.go new file mode 100644 index 00000000..40a8d85a --- /dev/null +++ b/backup/syncer/job.go @@ -0,0 +1,169 @@ +/* +Copyright 2021 RadonDB. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package syncer + +import ( + "fmt" + + "github.com/presslabs/controller-util/syncer" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + v1alpha1 "github.com/radondb/radondb-mysql-kubernetes/api/v1alpha1" + "github.com/radondb/radondb-mysql-kubernetes/backup" +) + +var log = logf.Log.WithName("backup.syncer.job") + +type jobSyncer struct { + job *batchv1.Job + backup *backup.Backup +} + +// NewJobSyncer returns a syncer for backup jobs +func NewJobSyncer(c client.Client, s *runtime.Scheme, backup *backup.Backup) syncer.Interface { + obj := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: backup.GetNameForJob(), + Namespace: backup.Namespace, + }, + } + + sync := &jobSyncer{ + job: obj, + backup: backup, + } + + return syncer.NewObjectSyncer("Job", backup.Unwrap(), obj, c, sync.SyncFn) +} + +func (s *jobSyncer) SyncFn() error { + if s.backup.Status.Completed { + log.V(1).Info("backup already completed", "backup", s.backup) + // skip doing anything + return syncer.ErrIgnore + } + + // check if job is already created an just update the status + if !s.job.ObjectMeta.CreationTimestamp.IsZero() { + s.updateStatus(s.job) + return nil + } + + s.job.Labels = map[string]string{ + "Host": s.backup.Spec.HostName, + } + + s.job.Spec.Template.Spec = s.ensurePodSpec(s.job.Spec.Template.Spec) + return nil +} + +func (s *jobSyncer) updateStatus(job *batchv1.Job) { + // check for completion condition + if cond := jobCondition(batchv1.JobComplete, job); cond != nil { + s.backup.UpdateStatusCondition(v1alpha1.BackupComplete, cond.Status, cond.Reason, cond.Message) + + if cond.Status == corev1.ConditionTrue { + s.backup.Status.Completed = true + } + } + + // check for failed condition + if cond := jobCondition(batchv1.JobFailed, job); cond != nil { + s.backup.UpdateStatusCondition(v1alpha1.BackupFailed, cond.Status, cond.Reason, cond.Message) + + if cond.Status == corev1.ConditionTrue { + s.backup.Status.Completed = true + } + } +} + +func jobCondition(condType batchv1.JobConditionType, job *batchv1.Job) *batchv1.JobCondition { + for _, c := range job.Status.Conditions { + if c.Type == condType { + return &c + } + } + + return nil +} + +func (s *jobSyncer) ensurePodSpec(in corev1.PodSpec) corev1.PodSpec { + if len(in.Containers) == 0 { + in.Containers = make([]corev1.Container, 1) + } + + in.RestartPolicy = corev1.RestartPolicyNever + sctName := fmt.Sprintf("%s-secret", s.backup.Spec.ClusterName) + in.Containers[0].Name = "backup" + in.Containers[0].Image = s.backup.Spec.Image + in.Containers[0].Args = []string{ + "request_a_backup", + s.backup.GetBackupURL(s.backup.Spec.ClusterName, s.backup.Spec.HostName), + } + var optTrue bool = true + in.Containers[0].Env = []corev1.EnvVar{ + { + Name: "NAMESPACE", + Value: s.backup.Namespace, + }, + { + Name: "SERVICE_NAME", + Value: fmt.Sprintf("%s-mysql", s.backup.Spec.ClusterName), + }, + { + Name: "HOST_NAME", + + Value: s.backup.Spec.HostName, + }, + { + Name: "REPLICAS", + Value: "1", + }, + //backup user for sidecar http server + { + Name: "BACKUP_USER", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: sctName, + }, + Key: "backup-user", + Optional: &optTrue, + }, + }, + }, + //backup user for sidecar http server + { + Name: "BACKUP_PASSWORD", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: sctName, + }, + Key: "backup-password", + Optional: &optTrue, + }, + }, + }, + } + return in +} diff --git a/charts/mysql-operator/crds/mysql.radondb.com_backups.yaml b/charts/mysql-operator/crds/mysql.radondb.com_backups.yaml new file mode 100644 index 00000000..7b758bdf --- /dev/null +++ b/charts/mysql-operator/crds/mysql.radondb.com_backups.yaml @@ -0,0 +1,102 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: backups.mysql.radondb.com +spec: + group: mysql.radondb.com + names: + kind: Backup + listKind: BackupList + plural: backups + singular: backup + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Backup is the Schema for the backups API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: BackupSpec defines the desired state of Backup + properties: + clustname: + description: Cluster represents the cluster name to backup + type: string + hostname: + description: HostName represents the host for which to take backup + type: string + image: + default: acekingke/sidecar:0.1 + description: To specify the image that will be used for sidecar container. + type: string + required: + - clustname + - hostname + type: object + status: + description: BackupStatus defines the observed state of Backup + properties: + completed: + description: Completed represents the backup has finished + type: boolean + conditions: + description: Conditions represents the backup resource conditions + list. + items: + description: BackupCondition defines condition struct for backup + resource + properties: + lastTransitionTime: + description: LastTransitionTime + format: date-time + type: string + message: + description: Message + type: string + reason: + description: Reason + type: string + status: + description: Status of the condition, one of (\"True\", \"False\", + \"Unknown\") + type: string + type: + description: type of cluster condition, values in (\"Ready\") + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/charts/mysql-operator/crds/mysql.radondb.com_clusters.yaml b/charts/mysql-operator/crds/mysql.radondb.com_clusters.yaml index 0d096ee7..8c8c4670 100644 --- a/charts/mysql-operator/crds/mysql.radondb.com_clusters.yaml +++ b/charts/mysql-operator/crds/mysql.radondb.com_clusters.yaml @@ -50,6 +50,10 @@ spec: spec: description: ClusterSpec defines the desired state of Cluster properties: + backupSecretName: + description: Represents the name of the secret that contains credentials + to connect to the storage provider to store backups. + type: string metricsOpts: default: enabled: false @@ -61,7 +65,7 @@ spec: requests: cpu: 10m memory: 32Mi - description: XenonOpts is the options of metrics container. + description: MetricsOpts is the options of metrics container. properties: enabled: default: false @@ -1235,6 +1239,10 @@ spec: - 5 format: int32 type: integer + restoreFrom: + description: Represents the name of the cluster restore from backup + path + type: string xenonOpts: default: admitDefeatHearbeatCount: 5 diff --git a/charts/mysql-operator/templates/cluster_rbac.yaml b/charts/mysql-operator/templates/cluster_rbac.yaml index f34aaddc..4d95d85a 100644 --- a/charts/mysql-operator/templates/cluster_rbac.yaml +++ b/charts/mysql-operator/templates/cluster_rbac.yaml @@ -111,6 +111,37 @@ rules: - patch - update - watch +- apiGroups: + - batch + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - mysql.radondb.com + resources: + - backups + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - mysql.radondb.com + resources: + - backups/status + verbs: + - get + - apiGroups: - rbac.authorization.k8s.io resources: diff --git a/charts/mysql-operator/values.yaml b/charts/mysql-operator/values.yaml index 2399b794..f3db13c5 100644 --- a/charts/mysql-operator/values.yaml +++ b/charts/mysql-operator/values.yaml @@ -15,7 +15,7 @@ fullnameOverride: "" manager: image: radondb/mysql-operator - tag: 0.1 + tag: 0.1.88 resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little diff --git a/cluster/container/backup.go b/cluster/container/backup.go new file mode 100644 index 00000000..b543f680 --- /dev/null +++ b/cluster/container/backup.go @@ -0,0 +1,142 @@ +/* +Copyright 2021 RadonDB. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package container + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/radondb/radondb-mysql-kubernetes/cluster" + "github.com/radondb/radondb-mysql-kubernetes/utils" +) + +type backupSidecar struct { + *cluster.Cluster + name string +} + +func (c *backupSidecar) getName() string { + return c.name +} + +func (c *backupSidecar) getImage() string { + return c.Spec.PodSpec.SidecarImage +} + +func (c *backupSidecar) getCommand() []string { + return []string{"sidecar", "http"} +} + +func (c *backupSidecar) getEnvVars() []corev1.EnvVar { + sctNameBakup := c.Spec.BackupSecretName + sctName := c.GetNameForResource(utils.Secret) + envs := []corev1.EnvVar{ + { + Name: "NAMESPACE", + Value: c.Namespace, + }, + { + Name: "SERVICE_NAME", + Value: c.GetNameForResource(utils.HeadlessSVC), + }, + { + Name: "REPLICAS", + Value: fmt.Sprintf("%d", *c.Spec.Replicas), + }, + //backup user password for sidecar http server + getEnvVarFromSecret(sctName, "BACKUP_USER", "backup-user", true), + getEnvVarFromSecret(sctName, "BACKUP_PASSWORD", "backup-password", true), + } + if len(sctNameBakup) != 0 { + envs = append(envs, + getEnvVarFromSecret(sctNameBakup, "S3_ENDPOINT", "s3-endpoint", false), + getEnvVarFromSecret(sctNameBakup, "S3_ACCESSKEY", "s3-access-key", true), + getEnvVarFromSecret(sctNameBakup, "S3_SECRETKEY", "s3-secret-key", true), + getEnvVarFromSecret(sctNameBakup, "S3_BUCKET", "s3-bucket", true), + ) + } + return envs +} + +func (c *backupSidecar) getLifecycle() *corev1.Lifecycle { + return nil +} + +func (c *backupSidecar) getResources() corev1.ResourceRequirements { + return c.Spec.PodSpec.Resources +} + +func (c *backupSidecar) getPorts() []corev1.ContainerPort { + return []corev1.ContainerPort{ + { + Name: utils.XBackupPortName, + ContainerPort: utils.XBackupPort, + }, + } +} + +func (c *backupSidecar) getLivenessProbe() *corev1.Probe { + return &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/health", + Port: intstr.FromInt(utils.XBackupPort), + }, + }, + InitialDelaySeconds: 15, + TimeoutSeconds: 5, + PeriodSeconds: 10, + SuccessThreshold: 1, + FailureThreshold: 3, + } +} + +func (c *backupSidecar) getReadinessProbe() *corev1.Probe { + return &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/health", + Port: intstr.FromInt(utils.XBackupPort), + }, + }, + InitialDelaySeconds: 5, + TimeoutSeconds: 1, + PeriodSeconds: 10, + SuccessThreshold: 1, + FailureThreshold: 3, + } +} + +func (c *backupSidecar) getVolumeMounts() []corev1.VolumeMount { + return []corev1.VolumeMount{ + { + Name: utils.ConfVolumeName, + MountPath: utils.ConfVolumeMountPath, + }, + { + Name: utils.DataVolumeName, + MountPath: utils.DataVolumeMountPath, + }, + { + Name: utils.LogsVolumeName, + MountPath: utils.LogsVolumeMountPath, + }, + } + +} diff --git a/cluster/container/container.go b/cluster/container/container.go index 3d14021f..0dbeb0fb 100644 --- a/cluster/container/container.go +++ b/cluster/container/container.go @@ -55,6 +55,8 @@ func EnsureContainer(name string, c *cluster.Cluster) corev1.Container { ctr = &slowLog{c, name} case utils.ContainerAuditLogName: ctr = &auditLog{c, name} + case utils.ContainerBackupName: + ctr = &backupSidecar{c, name} } return corev1.Container{ diff --git a/cluster/container/init_sidecar.go b/cluster/container/init_sidecar.go index 8514f340..bd5938f2 100644 --- a/cluster/container/init_sidecar.go +++ b/cluster/container/init_sidecar.go @@ -52,6 +52,7 @@ func (c *initSidecar) getCommand() []string { // getEnvVars get the container env. func (c *initSidecar) getEnvVars() []corev1.EnvVar { sctName := c.GetNameForResource(utils.Secret) + sctNamebackup := c.Spec.BackupSecretName envs := []corev1.EnvVar{ { Name: "POD_HOSTNAME", @@ -90,6 +91,11 @@ func (c *initSidecar) getEnvVars() []corev1.EnvVar { Name: "MYSQL_VERSION", Value: c.GetMySQLVersion(), }, + { + Name: "RESTORE_FROM", + Value: c.Spec.RestoreFrom, + }, + getEnvVarFromSecret(sctName, "MYSQL_ROOT_PASSWORD", "root-password", false), getEnvVarFromSecret(sctName, "MYSQL_DATABASE", "mysql-database", true), getEnvVarFromSecret(sctName, "MYSQL_USER", "mysql-user", true), @@ -100,6 +106,19 @@ func (c *initSidecar) getEnvVars() []corev1.EnvVar { getEnvVarFromSecret(sctName, "METRICS_PASSWORD", "metrics-password", true), getEnvVarFromSecret(sctName, "OPERATOR_USER", "operator-user", true), getEnvVarFromSecret(sctName, "OPERATOR_PASSWORD", "operator-password", true), + + //backup user password for sidecar http server + getEnvVarFromSecret(sctName, "BACKUP_USER", "backup-user", true), + getEnvVarFromSecret(sctName, "BACKUP_PASSWORD", "backup-password", true), + } + + if len(c.Spec.BackupSecretName) != 0 { + envs = append(envs, + getEnvVarFromSecret(sctNamebackup, "S3_ENDPOINT", "s3-endpoint", false), + getEnvVarFromSecret(sctNamebackup, "S3_ACCESSKEY", "s3-access-key", true), + getEnvVarFromSecret(sctNamebackup, "S3_SECRETKEY", "s3-secret-key", true), + getEnvVarFromSecret(sctNamebackup, "S3_BUCKET", "s3-bucket", true), + ) } if c.Spec.MysqlOpts.InitTokuDB { diff --git a/cluster/container/init_sidecar_test.go b/cluster/container/init_sidecar_test.go index 687fb9b6..8f5900ea 100644 --- a/cluster/container/init_sidecar_test.go +++ b/cluster/container/init_sidecar_test.go @@ -103,6 +103,10 @@ var ( Name: "MYSQL_VERSION", Value: "5.7.33", }, + { + Name: "RESTORE_FROM", + Value: "", + }, { Name: "MYSQL_ROOT_PASSWORD", ValueFrom: &corev1.EnvVarSource{ @@ -223,6 +227,31 @@ var ( }, }, }, + { + Name: "BACKUP_USER", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: sctName, + }, + Key: "backup-user", + Optional: &optTrue, + }, + }, + }, + { + Name: "BACKUP_PASSWORD", + + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: sctName, + }, + Key: "backup-password", + Optional: &optTrue, + }, + }, + }, } defaultInitsidecarVolumeMounts = []corev1.VolumeMount{ { diff --git a/cluster/syncer/metrics_service.go b/cluster/syncer/metrics_service.go index 19cfdb1c..9cf2501c 100644 --- a/cluster/syncer/metrics_service.go +++ b/cluster/syncer/metrics_service.go @@ -51,7 +51,7 @@ func NewMetricsSVCSyncer(cli client.Client, c *cluster.Cluster) syncer.Interface service.Spec.Ports[0].Name = utils.MetricsPortName service.Spec.Ports[0].Port = utils.MetricsPort service.Spec.Ports[0].TargetPort = intstr.FromInt(utils.MetricsPort) - + return nil }) } diff --git a/cluster/syncer/secret.go b/cluster/syncer/secret.go index fd2d7eec..ca214990 100644 --- a/cluster/syncer/secret.go +++ b/cluster/syncer/secret.go @@ -57,6 +57,12 @@ func NewSecretSyncer(cli client.Client, c *cluster.Cluster) syncer.Interface { return err } + //xtrabackup http server user and password + secret.Data["backup-user"] = []byte(utils.BackupUser) + if err := addRandomPassword(secret.Data, "backup-password"); err != nil { + return err + } + secret.Data["metrics-user"] = []byte(utils.MetricsUser) if err := addRandomPassword(secret.Data, "metrics-password"); err != nil { return err diff --git a/cluster/syncer/statefulset.go b/cluster/syncer/statefulset.go index aca1af48..237260ad 100644 --- a/cluster/syncer/statefulset.go +++ b/cluster/syncer/statefulset.go @@ -40,6 +40,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + apiv1alpha1 "github.com/radondb/radondb-mysql-kubernetes/api/v1alpha1" "github.com/radondb/radondb-mysql-kubernetes/cluster" "github.com/radondb/radondb-mysql-kubernetes/cluster/container" "github.com/radondb/radondb-mysql-kubernetes/internal" @@ -196,6 +197,12 @@ func (s *StatefulSetSyncer) updatePod(ctx context.Context) error { log.Info("can't start/continue 'update': waiting for all replicas are ready") return nil } + + if backuping, _ := s.backupIsRunning(ctx); backuping { + // return error, it will reconsile again + return fmt.Errorf("can't start/continue 'update': waiting for all backup completed") + } + // Get all pods. pods := corev1.PodList{} if err := s.cli.List(ctx, @@ -403,7 +410,8 @@ func (s *StatefulSetSyncer) ensurePodSpec() corev1.PodSpec { mysql := container.EnsureContainer(utils.ContainerMysqlName, s.Cluster) xenon := container.EnsureContainer(utils.ContainerXenonName, s.Cluster) - containers := []corev1.Container{mysql, xenon} + backup := container.EnsureContainer(utils.ContainerBackupName, s.Cluster) + containers := []corev1.Container{mysql, xenon, backup} if s.Spec.MetricsOpts.Enabled { containers = append(containers, container.EnsureContainer(utils.ContainerMetricsName, s.Cluster)) } @@ -567,3 +575,25 @@ func xenonHttpRequest(host, method, url string, rootPasswd []byte, body io.Reade return resp.Body, nil } + +//check the backup is exist and running +func (s *StatefulSetSyncer) backupIsRunning(ctx context.Context) (bool, error) { + backuplist := apiv1alpha1.BackupList{} + if err := s.cli.List(ctx, + &backuplist, + &client.ListOptions{ + Namespace: s.sfs.Namespace, + }, + ); err != nil { + return false, err + } + for _, bcp := range backuplist.Items { + if bcp.ClusterName != s.ClusterName { + continue + } + if bcp.Status.Completed != true { + return true, nil + } + } + return false, nil +} diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 49f1e242..ee5241c3 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -94,6 +94,14 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Status") os.Exit(1) } + if err = (&controllers.BackupReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("controller.Backup"), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Backup") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/cmd/sidecar/main.go b/cmd/sidecar/main.go index 1e91a867..66cc19de 100644 --- a/cmd/sidecar/main.go +++ b/cmd/sidecar/main.go @@ -17,6 +17,7 @@ limitations under the License. package main import ( + "fmt" "os" "github.com/spf13/cobra" @@ -49,12 +50,61 @@ var ( func main() { // setup logging logf.SetLogger(zap.New(zap.UseDevMode(true))) - cfg := sidecar.NewConfig() - + stop := make(chan struct{}) initCmd := sidecar.NewInitCommand(cfg) cmd.AddCommand(initCmd) + takeBackupCmd := &cobra.Command{ + Use: "backup", + Short: "Take a backup from node and push it to rclone path.", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("require one arguments. source host and destination bucket") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + err := sidecar.RunTakeBackupCommand(cfg, args[0]) + if err != nil { + log.Error(err, "take backup command failed") + os.Exit(1) + + } + }, + } + cmd.AddCommand(takeBackupCmd) + + httpCmd := &cobra.Command{ + Use: "http", + Short: "start http server", + Run: func(cmd *cobra.Command, args []string) { + if err := sidecar.RunHttpServer(cfg, stop); err != nil { + log.Error(err, "run command failed") + os.Exit(1) + } + }, + } + cmd.AddCommand(httpCmd) + + reqBackupCmd := &cobra.Command{ + Use: "request_a_backup", + Short: "start request a backup", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("require one arguments. ") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + if err := sidecar.RunRequestBackup(cfg, args[0]); err != nil { + log.Error(err, "run command failed") + os.Exit(1) + } + }, + } + cmd.AddCommand(reqBackupCmd) + if err := cmd.Execute(); err != nil { log.Error(err, "failed to execute command", "cmd", cmd) os.Exit(1) diff --git a/config/crd/bases/mysql.radondb.com_backups.yaml b/config/crd/bases/mysql.radondb.com_backups.yaml new file mode 100644 index 00000000..7ca282e5 --- /dev/null +++ b/config/crd/bases/mysql.radondb.com_backups.yaml @@ -0,0 +1,107 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: backups.mysql.radondb.com +spec: + group: mysql.radondb.com + names: + kind: Backup + listKind: BackupList + plural: backups + singular: backup + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Backup is the Schema for the backups API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: BackupSpec defines the desired state of Backup + properties: + clustname: + description: Cluster represents the cluster name to backup + type: string + historyLimit: + default: 3 + description: History Limit of job + format: int32 + type: integer + hostname: + description: HostName represents the host for which to take backup + type: string + image: + default: acekingke/sidecar:0.1 + description: To specify the image that will be used for sidecar container. + type: string + required: + - clustname + - hostname + type: object + status: + description: BackupStatus defines the observed state of Backup + properties: + completed: + description: Completed represents the backup has finished + type: boolean + conditions: + description: Conditions represents the backup resource conditions + list. + items: + description: BackupCondition defines condition struct for backup + resource + properties: + lastTransitionTime: + description: LastTransitionTime + format: date-time + type: string + message: + description: Message + type: string + reason: + description: Reason + type: string + status: + description: Status of the condition, one of (\"True\", \"False\", + \"Unknown\") + type: string + type: + description: type of cluster condition, values in (\"Ready\") + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/bases/mysql.radondb.com_clusters.yaml b/config/crd/bases/mysql.radondb.com_clusters.yaml index a95bd11b..8c8c4670 100644 --- a/config/crd/bases/mysql.radondb.com_clusters.yaml +++ b/config/crd/bases/mysql.radondb.com_clusters.yaml @@ -50,6 +50,10 @@ spec: spec: description: ClusterSpec defines the desired state of Cluster properties: + backupSecretName: + description: Represents the name of the secret that contains credentials + to connect to the storage provider to store backups. + type: string metricsOpts: default: enabled: false @@ -1235,6 +1239,10 @@ spec: - 5 format: int32 type: integer + restoreFrom: + description: Represents the name of the cluster restore from backup + path + type: string xenonOpts: default: admitDefeatHearbeatCount: 5 diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 3d347170..a704f9b0 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -3,6 +3,7 @@ # It should be run by config/default resources: - bases/mysql.radondb.com_clusters.yaml +- bases/mysql.radondb.com_backups.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: diff --git a/config/rbac/backup_editor_role.yaml b/config/rbac/backup_editor_role.yaml new file mode 100644 index 00000000..be99af82 --- /dev/null +++ b/config/rbac/backup_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit backups. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: backup-editor-role +rules: +- apiGroups: + - mysql.radondb.com + resources: + - backups + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - mysql.radondb.com + resources: + - backups/status + verbs: + - get diff --git a/config/rbac/backup_viewer_role.yaml b/config/rbac/backup_viewer_role.yaml new file mode 100644 index 00000000..33b5d7db --- /dev/null +++ b/config/rbac/backup_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view backups. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: backup-viewer-role +rules: +- apiGroups: + - mysql.radondb.com + resources: + - backups + verbs: + - get + - list + - watch +- apiGroups: + - mysql.radondb.com + resources: + - backups/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index da2fa859..dd05c4dc 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -18,6 +18,18 @@ rules: - patch - update - watch +- apiGroups: + - batch + resources: + - jobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - coordination.k8s.io resources: @@ -64,6 +76,26 @@ rules: - list - update - watch +- apiGroups: + - mysql.radondb.com + resources: + - backups + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - mysql.radondb.com + resources: + - backups/status + verbs: + - get + - patch + - update - apiGroups: - mysql.radondb.com resources: diff --git a/config/samples/backup_secret.yaml b/config/samples/backup_secret.yaml new file mode 100644 index 00000000..93fa9e2e --- /dev/null +++ b/config/samples/backup_secret.yaml @@ -0,0 +1,11 @@ +kind: Secret +apiVersion: v1 +metadata: + name: sample-backup-secret + namespace: default +data: + s3-endpoint: + s3-access-key: + s3-secret-key: + s3-bucket: +type: Opaque diff --git a/config/samples/mysql_v1alpha1_backup.yaml b/config/samples/mysql_v1alpha1_backup.yaml new file mode 100644 index 00000000..c200ad22 --- /dev/null +++ b/config/samples/mysql_v1alpha1_backup.yaml @@ -0,0 +1,9 @@ +apiVersion: mysql.radondb.com/v1alpha1 +kind: Backup +metadata: + name: backup-sample1 +spec: + # Add fields here + image: radondb/mysql-sidecar:0.1.88 + hostname: sample-mysql-0 + clustname: sample diff --git a/config/samples/mysql_v1alpha1_cluster.yaml b/config/samples/mysql_v1alpha1_cluster.yaml index c7aab73e..4ac72629 100644 --- a/config/samples/mysql_v1alpha1_cluster.yaml +++ b/config/samples/mysql_v1alpha1_cluster.yaml @@ -5,7 +5,14 @@ metadata: spec: replicas: 3 mysqlVersion: "5.7" - + + # the backupSecretName specify the secret file name which store S3 information, + # if you want S3 backup or restore, please create backup_secret.yaml, uncomment bellow and fill secret name: + # backupSecretName: + + # if you want create cluster from S3 , uncomment and fill the directory of S3 bucket bellow: + # restoreFrom: + mysqlOpts: rootPassword: "" rootHost: localhost @@ -55,7 +62,7 @@ spec: podSpec: imagePullPolicy: IfNotPresent - sidecarImage: radondb/mysql-sidecar:0.1 + sidecarImage: radondb/mysql-sidecar:0.1.88 busyboxImage: busybox:1.32 slowLogTail: false @@ -78,4 +85,4 @@ spec: accessModes: - ReadWriteOnce #storageClass: "" - size: 10Gi + size: 20Gi diff --git a/controllers/backup_controller.go b/controllers/backup_controller.go new file mode 100644 index 00000000..c6ff552e --- /dev/null +++ b/controllers/backup_controller.go @@ -0,0 +1,162 @@ +/* +Copyright 2021 RadonDB. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controllers + +import ( + "context" + "fmt" + "reflect" + "sort" + + "github.com/presslabs/controller-util/syncer" + batchv1 "k8s.io/api/batch/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + apiv1alpha1 "github.com/radondb/radondb-mysql-kubernetes/api/v1alpha1" + "github.com/radondb/radondb-mysql-kubernetes/backup" + backupSyncer "github.com/radondb/radondb-mysql-kubernetes/backup/syncer" +) + +// BackupReconciler reconciles a Backup object +type BackupReconciler struct { + client.Client + Scheme *runtime.Scheme + Recorder record.EventRecorder +} + +//+kubebuilder:rbac:groups=mysql.radondb.com,resources=backups,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=mysql.radondb.com,resources=backups/status,verbs=get;update;patch + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the Backup object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.7.2/pkg/reconcile +func (r *BackupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + // your logic here + // Fetch the Backup instance + log := log.Log.WithName("controllers").WithName("Backup") + backup := backup.New(&apiv1alpha1.Backup{}) + err := r.Get(context.TODO(), req.NamespacedName, backup.Unwrap()) + if err != nil { + if errors.IsNotFound(err) { + // Object not found, return. Created objects are automatically garbage collected. + // For additional cleanup logic use finalizers. + return reconcile.Result{}, nil + } + // Error reading the object - requeue the request. + return reconcile.Result{}, err + } + // Set defaults on backup + r.Scheme.Default(backup.Unwrap()) + + // save the backup for later check for diff + savedBackup := backup.Unwrap().DeepCopy() + + jobSyncer := backupSyncer.NewJobSyncer(r.Client, r.Scheme, backup) + if err := syncer.Sync(ctx, jobSyncer, r.Recorder); err != nil { + return reconcile.Result{}, err + } + + if err = r.updateBackup(savedBackup, backup); err != nil { + return reconcile.Result{}, err + } + + // Clear the backup, Just keep historyLimit len + backups := batchv1.JobList{} + if err := r.List(context.TODO(), &backups, &client.ListOptions{ + Namespace: req.Namespace}); err != nil { + return reconcile.Result{}, err + } + + var finishedBackups []*batchv1.Job + for _, job := range backups.Items { + if IsJobFinished(&job) { + finishedBackups = append(finishedBackups, &job) + } + + } + + sort.Slice(finishedBackups, func(i, j int) bool { + if finishedBackups[i].Status.StartTime == nil { + return finishedBackups[j].Status.StartTime != nil + } + return finishedBackups[i].Status.StartTime.Before(finishedBackups[j].Status.StartTime) + }) + + for i, job := range finishedBackups { + if int32(i) >= int32(len(finishedBackups))-*backup.Spec.HistoryLimit { + break + } + if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil { + log.Error(err, "unable to delete old completed job", "job", job) + } else { + log.V(0).Info("deleted old completed job", "job", job) + } + } + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *BackupReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&apiv1alpha1.Backup{}). + Owns(&batchv1.Job{}). + Complete(r) +} + +// Update backup Object and Status +func (r *BackupReconciler) updateBackup(savedBackup *apiv1alpha1.Backup, backup *backup.Backup) error { + log := log.Log.WithName("controllers").WithName("Backup") + if !reflect.DeepEqual(savedBackup, backup.Unwrap()) { + if err := r.Update(context.TODO(), backup.Unwrap()); err != nil { + return err + } + } + if !reflect.DeepEqual(savedBackup.Status, backup.Unwrap().Status) { + + log.Info("update backup object status") + if err := r.Status().Update(context.TODO(), backup.Unwrap()); err != nil { + log.Error(err, fmt.Sprintf("update status backup %s/%s", backup.Name, backup.Namespace), + "backupStatus", backup.Status) + return err + } + } + return nil +} + +// Check the job is finished. +func IsJobFinished(job *batchv1.Job) bool { + for _, c := range job.Status.Conditions { + if c.Type == batchv1.JobComplete || c.Type == batchv1.JobFailed { + return true + } + } + return false +} diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md new file mode 100644 index 00000000..11fbcd8c --- /dev/null +++ b/docs/DEPLOY.md @@ -0,0 +1,127 @@ +# mysql-operator + +## Quickstart for backup + +Install the operator named `test`: + +```shell +helm install test charts/mysql-operator +``` + +### configure backup + +add the secret file +```yaml +kind: Secret +apiVersion: v1 +metadata: + name: sample-backup-secret + namespace: default +data: + s3-endpoint: aHR0cDovL3MzLnNoMWEucWluZ3N0b3IuY29t + s3-access-key: SEdKWldXVllLSENISllFRERKSUc= + s3-secret-key: TU44TkNUdDJLdHlZREROTTc5cTNwdkxtNTlteE01blRaZlRQMWxoag== + s3-bucket: bGFsYS1teXNxbA== +type: Opaque + +``` +s3-xxxx value is encode by base64, you can get like that +```shell +echo -n "hello"|base64 +``` +then, create the secret in k8s. +``` +kubectl create -f config/samples/backup_secret.yaml +``` +Please add the backupSecretName in mysql_v1apha1_cluster.yaml, name as secret file: +```yaml +spec: + replicas: 3 + mysqlVersion: "5.7" + backupSecretName: sample-backup-secret + ... +``` +now create backup yaml file mysql_v1apha1_backup.yaml like this: + +```yaml +apiVersion: mysql.radondb.com/v1alpha1 +kind: Backup +metadata: + name: backup-sample1 +spec: + # Add fields here + hostname: sample-mysql-0 + clustname: sample + +``` +| name | function | +|------|--------| +|hostname|pod name in cluser| +|clustname|cluster name| + +### start cluster + +```shell +kubectl apply -f config/samples/mysql_v1alpha1_cluster.yaml +``` +### start backup +After run cluster success +```shell +kubectl apply -f config/samples/mysql_v1alpha1_backup.yaml +``` + +## Uninstall + +Uninstall the cluster named `sample`: + +```shell +kubectl delete clusters.mysql.radondb.com sample +``` + +Uninstall the operator name `test`: + +```shell +helm uninstall test +kubectl delete -f config/samples/mysql_v1alpha1_backup.yaml +``` + +Uninstall the crd: + +```shell +kubectl delete customresourcedefinitions.apiextensions.k8s.io clusters.mysql.radondb.com +``` + + +## restore cluster from backup copy +check your s3 bucket, get the directory where your backup to, such as `backup_2021720827`. +add it to RestoreFrom in yaml file +```yaml +... +spec: + replicas: 3 + mysqlVersion: "5.7" + backupSecretName: sample-backup-secret + restoreFrom: "backup_2021720827" +... +``` +Then you use: +```shell +kubectl apply -f config/samples/mysql_v1alpha1_cluster.yaml +``` +could restore a cluster from the `backup_2021720827 ` copy in the S3 bucket. + + + ## build your own image + such as : + ``` + docker build -f Dockerfile.sidecar -t acekingke/sidecar:0.1 . && docker push acekingke/sidecar:0.1 + docker build -t acekingke/controller:0.1 . && docker push acekingke/controller:0.1 + ``` + you can replace acekingke/sidecar:0.1 with your own tag + + ## deploy your own manager +```shell +make manifests +make install +make deploy IMG=acekingke/controller:0.1 KUSTOMIZE=~/radondb-mysql-kubernetes/bin/kustomize +``` diff --git a/sidecar/config.go b/sidecar/config.go index f51568d4..4889e680 100644 --- a/sidecar/config.go +++ b/sidecar/config.go @@ -18,7 +18,10 @@ package sidecar import ( "fmt" + "os" "strconv" + "strings" + "text/template" "github.com/blang/semver" "github.com/go-ini/ini" @@ -77,6 +80,43 @@ type Config struct { // Whether the MySQL data exists. existMySQLData bool + //for mysql backup + // backup user and password for http endpoint + ClusterName string + + //Backup user name to http Server + BackupUser string + + //Backup Password to htpp Server + BackupPassword string + + // XbstreamExtraArgs is a list of extra command line arguments to pass to xbstream. + XbstreamExtraArgs []string + + // XtrabackupExtraArgs is a list of extra command line arguments to pass to xtrabackup. + XtrabackupExtraArgs []string + + // XtrabackupPrepareExtraArgs is a list of extra command line arguments to pass to xtrabackup + // during --prepare. + XtrabackupPrepareExtraArgs []string + + // XtrabackupTargetDir is a backup destination directory for xtrabackup. + XtrabackupTargetDir string + + //S3 endpoint domain name + XCloudS3EndPoint string + + //S3 access key + XCloudS3AccessKey string + + //S3 secrete key + XCloudS3SecretKey string + + //S3 Bucket names + XCloudS3Bucket string + + // directory in S3 bucket for cluster restore from + XRestoreFrom string } // NewConfig returns a pointer to Config. @@ -139,8 +179,60 @@ func NewConfig() *Config { AdmitDefeatHearbeatCount: int32(admitDefeatHearbeatCount), ElectionTimeout: int32(electionTimeout), - existMySQLData: existMySQLData, + existMySQLData: existMySQLData, + ClusterName: getEnvValue("SERVICE_NAME"), + BackupUser: getEnvValue("BACKUP_USER"), + BackupPassword: getEnvValue("BACKUP_PASSWORD"), + XbstreamExtraArgs: strings.Fields(getEnvValue("XBSTREAM_EXTRA_ARGS")), + XtrabackupExtraArgs: strings.Fields(getEnvValue("XTRABACKUP_EXTRA_ARGS")), + XtrabackupPrepareExtraArgs: strings.Fields(getEnvValue("XTRABACKUP_PREPARE_EXTRA_ARGS")), + XtrabackupTargetDir: getEnvValue("XTRABACKUP_TARGET_DIR"), + + XCloudS3EndPoint: getEnvValue("S3_ENDPOINT"), + XCloudS3AccessKey: getEnvValue("S3_ACCESSKEY"), + XCloudS3SecretKey: getEnvValue("S3_SECRETKEY"), + XCloudS3Bucket: getEnvValue("S3_BUCKET"), + XRestoreFrom: getEnvValue("RESTORE_FROM"), + } +} + +//build Xtrabackup arguments +func (cfg *Config) XtrabackupArgs() []string { + // xtrabackup --backup --target-dir= + user := "root" + if len(cfg.ReplicationUser) != 0 { + user = cfg.ReplicationUser + } + + tmpdir := "/root/backup/" + if len(cfg.XtrabackupTargetDir) != 0 { + tmpdir = cfg.XtrabackupTargetDir + } + xtrabackupArgs := []string{ + "--backup", + "--stream=xbstream", + "--host=127.0.0.1", + fmt.Sprintf("--user=%s", user), + fmt.Sprintf("--target-dir=%s", tmpdir), + } + + return append(xtrabackupArgs, cfg.XtrabackupExtraArgs...) +} + +//Build xbcloud arguments +func (cfg *Config) XCloudArgs() []string { + xcloudArgs := []string{ + "put", + "--storage=S3", + fmt.Sprintf("--s3-endpoint=%s", cfg.XCloudS3EndPoint), + fmt.Sprintf("--s3-access-key=%s", cfg.XCloudS3AccessKey), + fmt.Sprintf("--s3-secret-key=%s", cfg.XCloudS3SecretKey), + fmt.Sprintf("--s3-bucket=%s", cfg.XCloudS3Bucket), + "--parallel=10", + utils.BuildBackupName(), + "--insecure", } + return xcloudArgs } // buildExtraConfig build a ini file for mysql. @@ -405,3 +497,65 @@ curl -X PATCH -H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/ser `, cfg.NameSpace) return utils.StringToBytes(str) } + +// build S3 restore shell script +func (cfg *Config) buildS3Restore(path string) error { + if len(cfg.XRestoreFrom) == 0 { + return fmt.Errorf("Do not have restore from") + } + if len(cfg.XCloudS3EndPoint) == 0 || + len(cfg.XCloudS3AccessKey) == 0 || + len(cfg.XCloudS3SecretKey) == 0 || + len(cfg.XCloudS3Bucket) == 0 { + return fmt.Errorf("Do not have S3 information") + } + f, err := os.Create(path) + if err != nil { + return fmt.Errorf("create restore.sh fail : %s", err) + } + defer func() { + f.Close() + }() + + restoresh := `#!/bin/sh +if [ ! -d {{.DataDir}} ] ; then + echo "is not exist the var lib mysql" + mkdir {{.DataDir}} + chown -R mysql.mysql {{.DataDir}} +fi +mkdir /root/backup +xbcloud get --storage=S3 \ +--s3-endpoint="{{.XCloudS3EndPoint}}" \ +--s3-access-key="{{.XCloudS3AccessKey}}" \ +--s3-secret-key="{{.XCloudS3SecretKey}}" \ +--s3-bucket="{{.XCloudS3Bucket}}" \ +--parallel=10 {{.XRestoreFrom}} \ +--insecure |xbstream -xv -C /root/backup +# prepare redolog +xtrabackup --defaults-file={{.MyCnfMountPath}} --use-memory=3072M --prepare --apply-log-only --target-dir=/root/backup +# prepare data +xtrabackup --defaults-file={{.MyCnfMountPath}} --use-memory=3072M --prepare --target-dir=/root/backup +chown -R mysql.mysql /root/backup +xtrabackup --defaults-file={{.MyCnfMountPath}} --datadir={{.DataDir}} --copy-back --target-dir=/root/backup +chown -R mysql.mysql {{.DataDir}} +rm -rf /root/backup +` + template_restore := template.New("restore.sh") + template_restore, err = template_restore.Parse(restoresh) + if err != nil { + return err + } + err2 := template_restore.Execute(f, struct { + Config + DataDir string + MyCnfMountPath string + }{ + *cfg, + utils.DataVolumeMountPath, + utils.ConfVolumeMountPath + "/my.cnf", + }) + if err2 != nil { + return err2 + } + return nil +} diff --git a/sidecar/init.go b/sidecar/init.go index c9c9c4dd..fcdb5c68 100644 --- a/sidecar/init.go +++ b/sidecar/init.go @@ -161,7 +161,35 @@ func runInitCommand(cfg *Config) error { if err = ioutil.WriteFile(xenonFilePath, cfg.buildXenonConf(), 0644); err != nil { return fmt.Errorf("failed to write xenon.json: %s", err) } + // run the restore + if len(cfg.XRestoreFrom) != 0 { + var restoreName string = "/restore.sh" + err_f := cfg.buildS3Restore(restoreName) + if err_f != nil { + return fmt.Errorf("build restore.sh fail : %s", err_f) + } + if err = os.Chmod(restoreName, os.FileMode(0755)); err != nil { + return fmt.Errorf("failed to chmod scripts: %s", err) + } + cmd := exec.Command("sh", "-c", restoreName) + cmd.Stderr = os.Stderr + if err = cmd.Run(); err != nil { + return fmt.Errorf("failed to disable the run restore: %s", err) + } + } log.Info("init command success") return nil } + +/*start the backup http server*/ +func RunHttpServer(cfg *Config, stop <-chan struct{}) error { + srv := newServer(cfg, stop) + return srv.ListenAndServe() +} + +// request a backup command +func RunRequestBackup(cfg *Config, host string) error { + _, err := requestABackup(cfg, host, serverBackupEndpoint) + return err +} diff --git a/sidecar/server.go b/sidecar/server.go new file mode 100644 index 00000000..8b3846fd --- /dev/null +++ b/sidecar/server.go @@ -0,0 +1,155 @@ +/* +Copyright 2021 RadonDB. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sidecar + +import ( + "context" + "fmt" + "net" + "net/http" + "strings" + "time" + + "github.com/radondb/radondb-mysql-kubernetes/utils" +) + +const ( + serverPort = utils.XBackupPort + serverProbeEndpoint = "/health" + serverBackupEndpoint = "/xbackup" + serverConnectTimeout = 5 * time.Second +) + +type server struct { + cfg *Config + http.Server +} + +func newServer(cfg *Config, stop <-chan struct{}) *server { + mux := http.NewServeMux() + srv := &server{ + cfg: cfg, + Server: http.Server{ + Addr: fmt.Sprintf(":%d", serverPort), + Handler: mux, + }, + } + + // Add handle functions + mux.HandleFunc(serverProbeEndpoint, srv.healthHandler) + mux.Handle(serverBackupEndpoint, maxClients(http.HandlerFunc(srv.backupHandler), 1)) + + // Shutdown gracefully the http server + go func() { + <-stop // wait for stop signal + if err := srv.Shutdown(context.Background()); err != nil { + log.Error(err, "failed to stop http server") + + } + }() + + return srv +} + +// nolint: unparam +func (s *server) healthHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + if _, err := w.Write([]byte("OK")); err != nil { + log.Error(err, "failed writing request") + } +} + +func (s *server) backupHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Connection", "keep-alive") + if !s.isAuthenticated(r) { + http.Error(w, "Not authenticated!", http.StatusForbidden) + return + } + err := RunTakeBackupCommand(s.cfg, "hello") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } else { + w.Write([]byte("OK")) + } +} + +func (s *server) isAuthenticated(r *http.Request) bool { + user, pass, ok := r.BasicAuth() + return ok && user == s.cfg.BackupUser && pass == s.cfg.BackupPassword +} + +// maxClients limit an http endpoint to allow just n max concurrent connections +func maxClients(h http.Handler, n int) http.Handler { + sema := make(chan struct{}, n) + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sema <- struct{}{} + defer func() { + <-sema + }() + h.ServeHTTP(w, r) + }) +} + +func prepareURL(svc string, endpoint string) string { + if !strings.Contains(svc, ":") { + svc = fmt.Sprintf("%s:%d", svc, serverPort) + } + return fmt.Sprintf("http://%s%s", svc, endpoint) +} + +func transportWithTimeout(connectTimeout time.Duration) http.RoundTripper { + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: connectTimeout, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } +} + +// requestABackup connects to specified host and endpoint and gets the backup +func requestABackup(cfg *Config, host string, endpoint string) (*http.Response, error) { + log.Info("initialize a backup", "host", host, "endpoint", endpoint) + + req, err := http.NewRequest("GET", prepareURL(host, endpoint), nil) + if err != nil { + return nil, fmt.Errorf("fail to create request: %s", err) + } + + // set authentication user and password + req.SetBasicAuth(cfg.BackupUser, cfg.BackupPassword) + + client := &http.Client{} + client.Transport = transportWithTimeout(serverConnectTimeout) + + resp, err := client.Do(req) + if err != nil || resp.StatusCode != 200 { + status := "unknown" + if resp != nil { + status = resp.Status + } + return nil, fmt.Errorf("fail to get backup: %s, code: %s", err, status) + } + + return resp, nil +} diff --git a/sidecar/takebackup.go b/sidecar/takebackup.go new file mode 100644 index 00000000..ff960dd4 --- /dev/null +++ b/sidecar/takebackup.go @@ -0,0 +1,66 @@ +/* +Copyright 2021 RadonDB. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package sidecar + +import ( + "os" + "os/exec" + "strings" +) + +// RunTakeBackupCommand starts a backup command +func RunTakeBackupCommand(cfg *Config, name string) error { + log.Info("backup mysql", "name", name) + // cfg->XtrabackupArgs() + xtrabackup := exec.Command(xtrabackupCommand, cfg.XtrabackupArgs()...) + + var err error + //if len(cfg.XCloudS3AccessKey) == 0 || len(cfg.XCloudS3Bucket) == 0 || len(cfg.X) + xcloud := exec.Command(xcloudCommand, cfg.XCloudArgs()...) + log.Info("xargs ", "xargs", strings.Join(cfg.XCloudArgs(), " ")) + if xcloud.Stdin, err = xtrabackup.StdoutPipe(); err != nil { + log.Error(err, "failed to pipline") + return err + } + xtrabackup.Stderr = os.Stderr + xcloud.Stderr = os.Stderr + + if err := xtrabackup.Start(); err != nil { + log.Error(err, "failed to start xtrabackup command") + return err + } + if err := xcloud.Start(); err != nil { + log.Error(err, "fail start xcloud ") + return err + } + + // pipe command fail one, whole things fail + errorChannel := make(chan error, 2) + go func() { + errorChannel <- xcloud.Wait() + }() + go func() { + errorChannel <- xtrabackup.Wait() + }() + + for i := 0; i < 2; i++ { + if err = <-errorChannel; err != nil { + return err + } + } + return nil +} diff --git a/sidecar/util.go b/sidecar/util.go index 1f669d21..4b5a8b40 100644 --- a/sidecar/util.go +++ b/sidecar/util.go @@ -57,6 +57,12 @@ var ( // initFilePath is the init files path for mysql. initFilePath = utils.InitFileVolumeMountPath + + // xtrabackupCommand is the backup tool file name + xtrabackupCommand = "xtrabackup" + + //xcloudCommand is the upload tool file name + xcloudCommand = "xbcloud" ) // copyFile the src file to dst. diff --git a/utils/common.go b/utils/common.go index 894a5ee7..bea0f9f9 100644 --- a/utils/common.go +++ b/utils/common.go @@ -22,6 +22,7 @@ import ( "sort" "strconv" "strings" + "time" ) // Min returns the smallest int64 that was passed in the arguments. @@ -90,3 +91,11 @@ func ExistUpdateFile() bool { err = f.Close() return true } + +// Build the backup directory name by time +func BuildBackupName() string { + cur_time := time.Now() + return fmt.Sprintf("backup_%v%v%v%v%v%v", cur_time.Year(), int(cur_time.Month()), + cur_time.Day(), cur_time.Hour(), cur_time.Minute(), cur_time.Second()) + +} diff --git a/utils/constants.go b/utils/constants.go index 4bca925d..18e72ca2 100644 --- a/utils/constants.go +++ b/utils/constants.go @@ -42,7 +42,10 @@ const ( ContainerMetricsName = "metrics" ContainerSlowLogName = "slowlog" ContainerAuditLogName = "auditlog" + ContainerBackupName = "backup" + XBackupPortName = "xtrabackup" + XBackupPort = 8082 // MySQL port. MysqlPortName = "mysql" MysqlPort = 3306 @@ -63,6 +66,9 @@ const ( // The MySQL user used for operator to connect to the mysql node for configuration. OperatorUser = "qc_operator" + //xtrabackup http server user + BackupUser = "sys_backup" + // volumes names. ConfVolumeName = "conf" ConfMapVolumeName = "config-map"