diff --git a/controllers/cluster/README.md b/controllers/cluster/README.md index aab52ccbd..f6cbb0d2a 100644 --- a/controllers/cluster/README.md +++ b/controllers/cluster/README.md @@ -76,6 +76,11 @@ make manifests More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) +## Cluster operator specify the sealos version + +Using the annotations: "sealos.io/sealos/version" +Example: "sealos.io/sealos/version=4.1.3" + ## License Copyright 2022. diff --git a/controllers/cluster/api/v1/cluster_types.go b/controllers/cluster/api/v1/cluster_types.go index f1a557809..55cb374a2 100644 --- a/controllers/cluster/api/v1/cluster_types.go +++ b/controllers/cluster/api/v1/cluster_types.go @@ -21,24 +21,6 @@ import ( 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. - -// ClusterSpec defines the desired state of Cluster -type ClusterSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - Infra string `json:"infra,omitempty"` - Images []string `json:"images,omitempty"` - SSH v1beta1.SSH `json:"ssh,omitempty"` -} - -// ClusterStatus defines the observed state of Cluster -type ClusterStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file - Status string `json:"status,omitempty"` -} type Status int const ( @@ -64,6 +46,40 @@ func (s Status) String() string { } } +// ClusterSpec defines the desired state of InfraMetadata +type ClusterSpec struct { + // desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + // Foo is an example field of Cluster. Edit Cluster_types.go to remove/update + Infra string `json:"infra,omitempty"` + Image v1beta1.ImageList `json:"image,omitempty"` + SSH v1beta1.SSH `json:"ssh,omitempty"` + Hosts []v1beta1.Host `json:"hosts,omitempty"` + // Why env not using map[string]string + // Because some argument is list, like: CertSANS=127.0.0.1 CertSANS=localhost, if ENV is map, will merge those two values + // but user want to InfraMetadata a list, using array we can convert it to {CertSANS:[127.0.0.1, localhost]} + Env []string `json:"env,omitempty"` + // Entrypoint array. Not executed within a shell. + // The docker image's ENTRYPOINT is used if this is not provided. + // Variable references $(VAR_NAME) are expanded using the container's environment. If a variable + // cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced + // to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will + // produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless + // of whether the variable exists or not. Cannot be updated. + // More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell + // +optional + Command []string `json:"command,omitempty"` +} + +// ClusterStatus defines the observed state of Cluster +type ClusterStatus struct { + Phase v1beta1.ClusterPhase `json:"phase,omitempty"` + Mounts []v1beta1.MountImage `json:"mounts,omitempty"` + Conditions []v1beta1.ClusterCondition `json:"conditions,omitempty" ` + // Important: Run "make" to regenerate code after modifying this file + Status string `json:"status,omitempty"` +} + //+kubebuilder:object:root=true //+kubebuilder:subresource:status diff --git a/controllers/cluster/api/v1/zz_generated.deepcopy.go b/controllers/cluster/api/v1/zz_generated.deepcopy.go index 73146617b..3153675fe 100644 --- a/controllers/cluster/api/v1/zz_generated.deepcopy.go +++ b/controllers/cluster/api/v1/zz_generated.deepcopy.go @@ -22,6 +22,7 @@ limitations under the License. package v1 import ( + "github.com/labring/sealos/pkg/types/v1beta1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -30,8 +31,8 @@ func (in *Cluster) DeepCopyInto(out *Cluster) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Cluster. @@ -87,6 +88,29 @@ func (in *ClusterList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { *out = *in + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = make(v1beta1.ImageList, len(*in)) + copy(*out, *in) + } + out.SSH = in.SSH + if in.Hosts != nil { + in, out := &in.Hosts, &out.Hosts + *out = make([]v1beta1.Host, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Command != nil { + in, out := &in.Command, &out.Command + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSpec. @@ -102,6 +126,20 @@ func (in *ClusterSpec) DeepCopy() *ClusterSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterStatus) DeepCopyInto(out *ClusterStatus) { *out = *in + if in.Mounts != nil { + in, out := &in.Mounts, &out.Mounts + *out = make([]v1beta1.MountImage, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1beta1.ClusterCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterStatus. diff --git a/controllers/cluster/applier/applier.go b/controllers/cluster/applier/applier.go deleted file mode 100644 index 06816b9a0..000000000 --- a/controllers/cluster/applier/applier.go +++ /dev/null @@ -1,210 +0,0 @@ -package applier - -import ( - "fmt" - "log" - "strconv" - - cv1 "github.com/labring/sealos/controllers/cluster/api/v1" - v1 "github.com/labring/sealos/controllers/infra/api/v1" - "github.com/labring/sealos/controllers/infra/common" - "github.com/labring/sealos/pkg/ssh" - "github.com/labring/sealos/pkg/types/v1beta1" -) - -type Applier struct{} - -func NewApplier() Reconcile { - return &Applier{} -} - -type Reconcile interface { - ReconcileCluster(infra *v1.Infra, hosts []v1.Hosts, cluster *cv1.Cluster) error -} - -type IP struct { - PrivateIP string - PublicIP string -} - -func (a *Applier) ReconcileCluster(infra *v1.Infra, hosts []v1.Hosts, cluster *cv1.Cluster) error { - //todo 在job中去做,能够实现fmt打印导出到前端 - master := make([]IP, 0) - node := make([]IP, 0) - images := getImages(cluster) - - for i := range infra.Spec.Hosts { - if i >= len(hosts) || infra.Spec.Hosts[i].Count != hosts[i].Count { - return fmt.Errorf("there is no expected number of instances") - } - if getRoleMaster(hosts[i].Roles) { - for _, j := range hosts[i].Metadata { - master = append(master, IP{getPrivateIP(j.IP), getPublicIP(j.IP)}) - } - } else { - for _, j := range hosts[i].Metadata { - node = append(node, IP{getPrivateIP(j.IP), getPublicIP(j.IP)}) - } - } - } - if len(master) == 0 { - return fmt.Errorf("zero master") - } - - ssh, err := ssh.NewSSHByCluster(&v1beta1.Cluster{Spec: v1beta1.ClusterSpec{SSH: cluster.Spec.SSH}}, true) - if err != nil { - return fmt.Errorf("ssh create fail %v", err) - } - passwd := cluster.Spec.SSH.Passwd - if len(node) == 0 { - if len(master) == 1 { - // 1 master and zero node -> single - if err = makeClusterSingle(ssh, master[0].PublicIP, images); err != nil { - return fmt.Errorf("make cluster single error :%v", err) - } - return nil - } - return fmt.Errorf("zero node but more than 1 master") - } - - // >= 1 master and >= 1 node - sealos := master[0].PublicIP - for i := 0; i < len(master); i++ { - ip := master[i].PublicIP - if err = makeClusterMaster(ssh, ip, passwd, "master"+strconv.Itoa(i)); err != nil { - return err - } - } - - for i := 0; i < len(node); i++ { - ip := node[i].PublicIP - if err = makeClusterNode(ssh, ip, passwd, "node"+strconv.Itoa(i)); err != nil { - return err - } - } - - ipMasters, ipNodes := getIPs(master), getIPs(node) - - //sealos run on master - cmdSealosRun := fmt.Sprintf("sudo sealos run %s --masters %s --nodes %s --passwd %s", images, ipMasters, ipNodes, passwd) - log.Println(cmdSealosRun) - if err = ssh.CmdAsync(sealos, cmdSealosRun); err != nil { - return fmt.Errorf("execute ssh commend %s,fail:%v", cmdSealosRun, err) - } - return nil -} - -func makeClusterSingle(ssh ssh.Interface, ip, images string) error { - if err := downLoadSealos(ssh, ip); err != nil { - return err - } - //sealos run on single - cmdSealosRun := fmt.Sprintf("sudo sealos run %s --single", images) - if err := ssh.CmdAsync(ip, cmdSealosRun); err != nil { - return fmt.Errorf("execute commend: %s,error: %v", cmdSealosRun, err) - } - return nil -} - -// makeClusterNode configure ssh and root password -func makeClusterNode(ssh ssh.Interface, ip, passwd, role string) error { - if err := setRootPasswd(ssh, ip, passwd); err != nil { - return err - } - if err := setHostNameRole(ssh, ip, role); err != nil { - return err - } - if err := modifySSHConfig(ssh, ip); err != nil { - return err - } - return nil -} - -func makeClusterMaster(ssh ssh.Interface, ip, passwd, role string) error { - if err := makeClusterNode(ssh, ip, passwd, role); err != nil { - return err - } - if err := downLoadSealos(ssh, ip); err != nil { - return err - } - return nil -} - -func downLoadSealos(ssh ssh.Interface, ip string) error { - cmd := "sudo wget -c https://github.com/labring/sealos/releases/download/v4.1.3/sealos_4.1.3_linux_amd64.tar.gz && sudo tar zxvf sealos_4.1.3_linux_amd64.tar.gz sealos && sudo rm -rf sealos_4.1.3_linux_amd64.tar.gz && sudo chmod +x sealos && sudo mv sealos /usr/bin" - if err := ssh.CmdAsync(ip, cmd); err != nil { - return fmt.Errorf("execute commend: %s,error: %v", cmd, err) - } - return nil -} - -func modifySSHConfig(ssh ssh.Interface, ip string) error { - cmd := "sudo sed -i '63c PasswordAuthentication yes' /etc/ssh/sshd_config && sudo sed -i 's/# StrictHostKeyChecking ask/StrictHostKeyChecking no/' /etc/ssh/ssh_config && sudo systemctl restart sshd" - if err := ssh.CmdAsync(ip, cmd); err != nil { - return fmt.Errorf("execute commend: %s,error: %v", cmd, err) - } - return nil -} - -func setRootPasswd(ssh ssh.Interface, ip string, passwd string) error { - cmd := fmt.Sprintf("sudo echo -e %s\"\\n\"%s | sudo passwd root", passwd, passwd) - if err := ssh.CmdAsync(ip, cmd); err != nil { - return fmt.Errorf("execute commend: %s,error: %v", cmd, err) - } - return nil -} - -func setHostNameRole(ssh ssh.Interface, ip string, role string) error { - cmd := fmt.Sprintf("sudo hostnamectl set-hostname %s", role) - if err := ssh.CmdAsync(ip, cmd); err != nil { - return fmt.Errorf("execute commend: %s,error: %v", cmd, err) - } - return nil -} - -func getImages(cluster *cv1.Cluster) string { - images := "" - for _, image := range cluster.Spec.Images { - images += image + " " - } - return images -} - -func getIPs(ips []IP) string { - ip := "" - for i := 0; i < len(ips); i++ { - if i == len(ips)-1 { - ip = ip + ips[i].PrivateIP - break - } - ip = ip + ips[i].PrivateIP + "," - } - return ip -} - -func getRoleMaster(Roles []string) bool { - for _, j := range Roles { - if j == "master" { - return true - } - } - return false -} - -func getPrivateIP(ips []v1.IPAddress) string { - for _, ip := range ips { - if ip.IPType == common.IPTypePrivate { - return ip.IPValue - } - } - return "" -} - -func getPublicIP(ips []v1.IPAddress) string { - for _, ip := range ips { - if ip.IPType == common.IPTypePublic { - return ip.IPValue - } - } - return "" -} diff --git a/controllers/cluster/applier/applier_test.go b/controllers/cluster/applier/applier_test.go deleted file mode 100644 index 40490fce8..000000000 --- a/controllers/cluster/applier/applier_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package applier - -import ( - "testing" - - cv1 "github.com/labring/sealos/controllers/cluster/api/v1" - v1 "github.com/labring/sealos/controllers/infra/api/v1" - "github.com/labring/sealos/pkg/types/v1beta1" -) - -func TestApplier_ReconcileCluster(t *testing.T) { - type args struct { - infra *v1.Infra - hosts []v1.Hosts - cluster *cv1.Cluster - } - hosts := []v1.Hosts{ - { - Roles: []string{"master"}, - Metadata: []v1.Metadata{}, - }, - } - cluster := &cv1.Cluster{ - Spec: cv1.ClusterSpec{ - Images: []string{"labring/kubernetes:v1.24.0", "labring/calico:v3.22.1"}, - }, - } - tests := []struct { - name string - args args - wantErr bool - }{ - { - "test for apply cluster", - args{ - infra: &v1.Infra{Spec: v1.InfraSpec{SSH: v1beta1.SSH{ - User: "ec2-user", - Passwd: "123456", - Pk: "/root/hurz_key.pem", - Port: uint16(22), - }}}, - hosts: hosts, - cluster: cluster, - }, - false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - a := &Applier{} - if err := a.ReconcileCluster(tt.args.infra, tt.args.hosts, tt.args.cluster); (err != nil) != tt.wantErr { - t.Errorf("ReconcileCluster() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} diff --git a/controllers/cluster/config/crd/bases/cluster.sealos.io_clusters.yaml b/controllers/cluster/config/crd/bases/cluster.sealos.io_clusters.yaml index d67fa8c48..55f86da16 100644 --- a/controllers/cluster/config/crd/bases/cluster.sealos.io_clusters.yaml +++ b/controllers/cluster/config/crd/bases/cluster.sealos.io_clusters.yaml @@ -33,15 +33,55 @@ spec: metadata: type: object spec: - description: ClusterSpec defines the desired state of Cluster + description: ClusterSpec defines the desired state of InfraMetadata properties: - images: + command: + description: 'Entrypoint array. Not executed within a shell. The docker + image''s ENTRYPOINT is used if this is not provided. Variable references + $(VAR_NAME) are expanded using the container''s environment. If + a variable cannot be resolved, the reference in the input string + will be unchanged. Double $$ are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce + the string literal "$(VAR_NAME)". Escaped references will never + be expanded, regardless of whether the variable exists or not. Cannot + be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + env: + description: 'Why env not using map[string]string Because some argument + is list, like: CertSANS=127.0.0.1 CertSANS=localhost, if ENV is + map, will merge those two values but user want to InfraMetadata + a list, using array we can convert it to {CertSANS:[127.0.0.1, localhost]}' + items: + type: string + type: array + hosts: + items: + properties: + env: + description: overwrite env + items: + type: string + type: array + ips: + items: + type: string + type: array + roles: + items: + type: string + type: array + type: object + type: array + image: items: type: string type: array infra: - description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - Important: Run "make" to regenerate code after modifying this file' + description: 'desired state of cluster Important: Run "make" to regenerate + code after modifying this file Foo is an example field of Cluster. + Edit Cluster_types.go to remove/update' type: string ssh: properties: @@ -49,12 +89,12 @@ spec: type: string pk: type: string + pkData: + type: string + pkName: + type: string pkPasswd: type: string - pkdata: - type: string - pkname: - type: string port: type: integer user: @@ -64,9 +104,65 @@ spec: status: description: ClusterStatus defines the observed state of Cluster properties: + conditions: + items: + description: ClusterCondition describes the state of a cluster at + a certain point. + properties: + lastHeartbeatTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + required: + - status + - type + type: object + type: array + mounts: + items: + properties: + cmd: + items: + type: string + type: array + entrypoint: + items: + type: string + type: array + env: + additionalProperties: + type: string + type: object + imageName: + type: string + labels: + additionalProperties: + type: string + type: object + mountPoint: + type: string + name: + type: string + type: + type: string + required: + - imageName + - mountPoint + - name + - type + type: object + type: array + phase: + type: string status: - description: 'INSERT ADDITIONAL STATUS FIELD - define observed state - of cluster Important: Run "make" to regenerate code after modifying + description: 'Important: Run "make" to regenerate code after modifying this file' type: string type: object diff --git a/controllers/cluster/config/samples/cluster.yaml b/controllers/cluster/config/samples/cluster.yaml index 1edff36a0..38bf07997 100644 --- a/controllers/cluster/config/samples/cluster.yaml +++ b/controllers/cluster/config/samples/cluster.yaml @@ -1,36 +1,30 @@ -apiVersion: infra.sealos.io/v1 -kind: Infra -metadata: - name: infra-apply-test -spec: - ssh: - user: ec2-user - passwd: "123456" - pk: /root/hurz_key.pem - pkname: hurz_key - port: 22 - hosts: - - roles: [master] # required - count: 1 # Required - flavor: "t2.large" - image: "ami-05248307900d52e3a" - - roles: [ node ] # required - count: 1 # Required - flavor: "t2.medium" - image: "ami-05248307900d52e3a" ---- +#apiVersion: infra.sealos.io/v1 +#kind: Infra +#metadata: +# name: infra-apply-test +#spec: +# ssh: +# user: ec2-user +# passwd: "123456" +# pk: /root/hurz_key.pem +# pkname: hurz_key +# port: 22 +# hosts: +# - roles: [master] # required +# count: 1 # Required +# flavor: "t2.large" +# image: "ami-05248307900d52e3a" +# - roles: [ node ] # required +# count: 1 # Required +# flavor: "t2.medium" +# image: "ami-05248307900d52e3a" +#--- apiVersion: cluster.sealos.io/v1 kind: Cluster metadata: name: my-cluster spec: - ssh: - user: ec2-user - passwd: "123456" - pk: /root/hurz_key.pem - pkname: hurz_key - port: 22 infra: infra-apply-test - images: - - labring/kubernetes:v1.24.0 - - labring/calico:v3.22.1 \ No newline at end of file + image: + - labring/kubernetes:v1.24.0 + - labring/calico:v3.22.1 diff --git a/controllers/cluster/controllers/cluster_controller.go b/controllers/cluster/controllers/cluster_controller.go index 716c61333..6120c5465 100644 --- a/controllers/cluster/controllers/cluster_controller.go +++ b/controllers/cluster/controllers/cluster_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2022. +Copyright 2022 labring/sealos. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,26 +19,45 @@ package controllers import ( "context" "fmt" + "time" - "github.com/labring/sealos/controllers/cluster/applier" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + infracommon "github.com/labring/sealos/controllers/infra/common" + + "github.com/labring/sealos/pkg/ssh" + + "sigs.k8s.io/yaml" + + "github.com/labring/sealos/pkg/types/v1beta1" + + "k8s.io/kubernetes/pkg/apis/core" + + "k8s.io/client-go/tools/record" + + v1 "github.com/labring/sealos/controllers/cluster/api/v1" infrav1 "github.com/labring/sealos/controllers/infra/api/v1" "github.com/labring/sealos/controllers/infra/drivers" - "github.com/labring/sealos/pkg/utils/logger" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" +) - v1 "github.com/labring/sealos/controllers/cluster/api/v1" +const ( + defaultUser = "root" + defaultSealosVersion = "4.1.4-rc1" +) +const ( + applyClusterfileCmd = "sealos apply -f /root/Clusterfile" + downloadSealosCmd = `sealos version || wget https://github.com/labring/sealos/releases/download/v%s/sealos_%s_linux_amd64.tar.gz && tar -zxvf sealos_%s_linux_amd64.tar.gz sealos && chmod +x sealos && mv sealos /usr/bin` ) // ClusterReconciler reconciles a Cluster object type ClusterReconciler struct { client.Client - driver drivers.Driver - applier applier.Reconcile - Scheme *runtime.Scheme + driver drivers.Driver + Scheme *runtime.Scheme + recorder record.EventRecorder } //+kubebuilder:rbac:groups=cluster.sealos.io,resources=clusters,verbs=get;list;watch;create;update;patch;delete @@ -56,41 +75,131 @@ type ClusterReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) cluster := &v1.Cluster{} - if err := r.Get(ctx, req.NamespacedName, cluster); err != nil { - logger.Debug("ignore not found cluster error: %v", err) + r.recorder.Event(cluster, core.EventTypeWarning, "GetCluster", err.Error()) return ctrl.Result{}, client.IgnoreNotFound(err) } + infra := &infrav1.Infra{} infra.Name = cluster.Spec.Infra infra.Namespace = cluster.Namespace - key := client.ObjectKey{Namespace: infra.Namespace, Name: infra.Name} if err := r.Get(ctx, key, infra); err != nil { - logger.Debug("ignore not found cluster error: %v", err) + r.recorder.Event(cluster, core.EventTypeWarning, "GetInfra", err.Error()) return ctrl.Result{}, client.IgnoreNotFound(err) } - hosts, err := r.driver.GetInstances(infra) + clusterfile, err := generateClusterfile(infra, cluster) if err != nil { - logger.Error("get instances error: %v", err) - return ctrl.Result{}, err + r.recorder.Event(cluster, core.EventTypeWarning, "GenerateClusterfile", err.Error()) + return ctrl.Result{Requeue: true, RequeueAfter: time.Second * 60}, err + } + fmt.Println(clusterfile) + if err := applyClusterfile(infra, clusterfile, getSealosVersion(cluster)); err != nil { + r.recorder.Event(cluster, core.EventTypeWarning, "ApplyClusterfile", err.Error()) + return ctrl.Result{Requeue: true, RequeueAfter: time.Second * 60}, err } - if cluster.Spec.SSH.PkData == "" { - cluster.Spec.SSH.PkData = infra.Spec.SSH.PkData - } - err = r.applier.ReconcileCluster(infra, hosts, cluster) - if err != nil { - logger.Error("reconcile cluster error: %v", err) + // update cluster status + cluster.Status.Status = infrav1.Running.String() + if err := r.Status().Update(ctx, cluster); err != nil { + r.recorder.Event(cluster, core.EventTypeWarning, "UpdateClusterStatus", err.Error()) return ctrl.Result{}, err } return ctrl.Result{}, nil } +// Get private ip from metadata +func getPrivateIP(meta infrav1.Metadata) string { + for _, ip := range meta.IP { + if ip.IPType == infracommon.IPTypePrivate { + return ip.IPValue + } + } + return "" +} + +// Generate Clusterfile by infra and cluster +func generateClusterfile(infra *infrav1.Infra, cluster *v1.Cluster) (string, error) { + new := cluster.DeepCopy() + new.CreationTimestamp = metav1.Time{} + new.Spec.SSH = infra.Spec.SSH + new.Spec.SSH.User = defaultUser + + for _, host := range infra.Spec.Hosts { + for _, meta := range host.Metadata { + privateIP := getPrivateIP(meta) + if privateIP == "" { + continue + } + + new.Spec.Hosts = append(new.Spec.Hosts, v1beta1.Host{ + IPS: []string{privateIP}, + Roles: host.Roles, + }) + } + } + + // convert cluster to yaml + clusterfile, err := yaml.Marshal(new) + if err != nil { + return "", fmt.Errorf("marshal cluster [%s] to yaml failed: %v", new.Name, err) + } + + return string(clusterfile), nil +} + +// Get master0 public ip from infra +func getMaster0PublicIP(infra *infrav1.Infra) string { + for _, host := range infra.Spec.Hosts { + for _, meta := range host.Metadata { + for _, ip := range meta.IP { + if ip.IPType == infracommon.IPTypePublic && host.Roles[0] == v1beta1.MASTER { + return ip.IPValue + } + } + } + } + return "" +} + +// get sealos version from cluster +func getSealosVersion(cluster *v1.Cluster) string { + if v, ok := cluster.Annotations["sealos.io/sealos/version"]; ok && v != "" { + return v + } + return defaultSealosVersion +} + +// Apply clusterfile on infra +func applyClusterfile(infra *infrav1.Infra, clusterfile, sealosVersion string) error { + s := &v1beta1.SSH{ + User: infra.Spec.SSH.User, + PkData: infra.Spec.SSH.PkData, + } + c := ssh.NewSSHClient(s, true) + EIP := getMaster0PublicIP(infra) + if EIP == "" { + return fmt.Errorf("get master0 public ip failed") + } + + if err := ssh.WaitSSHReady(c, 5, EIP); err != nil { + return fmt.Errorf("wait ssh ready failed: %v", err) + } + + createClusterfile := fmt.Sprintf("echo '%s' > /root/Clusterfile", clusterfile) + downloadSealos := fmt.Sprintf(downloadSealosCmd, sealosVersion, sealosVersion, sealosVersion) + + cmds := []string{createClusterfile, downloadSealos, applyClusterfileCmd} + if err := c.CmdAsync(EIP, cmds...); err != nil { + return fmt.Errorf("write clusterfile to remote failed: %v", err) + } + + return nil +} + // SetupWithManager sets up the controller with the Manager. func (r *ClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { driver, err := drivers.NewDriver() @@ -98,7 +207,7 @@ func (r *ClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { return fmt.Errorf("cluster controller new driver failed: %v", err) } r.driver = driver - r.applier = applier.NewApplier() + r.recorder = mgr.GetEventRecorderFor("sealos-cluster-controller") return ctrl.NewControllerManagedBy(mgr). For(&v1.Cluster{}). diff --git a/controllers/cluster/controllers/cluster_controller_test.go b/controllers/cluster/controllers/cluster_controller_test.go index 24716bf9b..4b85735fc 100644 --- a/controllers/cluster/controllers/cluster_controller_test.go +++ b/controllers/cluster/controllers/cluster_controller_test.go @@ -3,37 +3,38 @@ package controllers import ( "testing" - "github.com/labring/sealos/controllers/cluster/applier" - "github.com/labring/sealos/controllers/infra/drivers" - ctrl "sigs.k8s.io/controller-runtime" + infrav1 "github.com/labring/sealos/controllers/infra/api/v1" + + v1 "github.com/labring/sealos/controllers/cluster/api/v1" ) -func TestClusterReconciler_SetupWithManager(t *testing.T) { +func Test_generateClusterfile(t *testing.T) { type args struct { - mgr ctrl.Manager + infra *infrav1.Infra + cluster *v1.Cluster } - var tests = []struct { + tests := []struct { name string args args + want string wantErr bool }{ { - "test for setup", - args{ - mgr: nil, - }, + "test for generate clusterfile", + args{}, + "", false, }, } for _, tt := range tests { - driver, _ := drivers.NewDriver() t.Run(tt.name, func(t *testing.T) { - r := &ClusterReconciler{ - driver: driver, - applier: applier.NewApplier(), + got, err := generateClusterfile(tt.args.infra, tt.args.cluster) + if (err != nil) != tt.wantErr { + t.Errorf("generateClusterfile() error = %v, wantErr %v", err, tt.wantErr) + return } - if err := r.SetupWithManager(tt.args.mgr); (err != nil) != tt.wantErr { - t.Errorf("SetupWithManager() error = %v, wantErr %v", err, tt.wantErr) + if got != tt.want { + t.Errorf("generateClusterfile() got = %v, want %v", got, tt.want) } }) } diff --git a/controllers/infra/controllers/infra_controller.go b/controllers/infra/controllers/infra_controller.go index 3af56071d..b36db72a9 100644 --- a/controllers/infra/controllers/infra_controller.go +++ b/controllers/infra/controllers/infra_controller.go @@ -99,7 +99,7 @@ func (r *InfraReconciler) SetupWithManager(mgr ctrl.Manager) error { } r.driver = driver r.applier = &drivers.Applier{} - r.recorder = mgr.GetEventRecorderFor("salos-infra-controller") + r.recorder = mgr.GetEventRecorderFor("sealos-infra-controller") return ctrl.NewControllerManagedBy(mgr). For(&infrav1.Infra{}). diff --git a/controllers/infra/drivers/aws/create_instance.go b/controllers/infra/drivers/aws/create_instance.go index 6421f6ae2..2b0a77d13 100644 --- a/controllers/infra/drivers/aws/create_instance.go +++ b/controllers/infra/drivers/aws/create_instance.go @@ -210,6 +210,7 @@ func (d Driver) CreateKeyPair(infra *v1.Infra) error { if infra.Spec.SSH.PkName != "" { return nil } + mutex.Lock() defer mutex.Unlock() client := d.Client