├── migrations ├── 1_init.down.sql └── 1_init.up.sql ├── docs └── README.md ├── image ├── 无标题-2023-08-10-2343.png └── 无标题-2023-08-11-2343.png ├── pkg ├── store │ ├── stores.go │ ├── model │ │ ├── delete.go │ │ └── resources.go │ └── mysql │ │ └── mysql_store.go ├── apis │ ├── multicluster │ │ └── v1alpha1 │ │ │ ├── doc.go │ │ │ ├── types.go │ │ │ ├── register.go │ │ │ └── zz_generated.deepcopy.go │ └── multiclusterresource │ │ └── v1alpha1 │ │ ├── doc.go │ │ ├── register.go │ │ ├── types.go │ │ └── zz_generated.deepcopy.go ├── config │ ├── config_test.go │ ├── init_ctl_config.go │ ├── config.go │ ├── init_db.go │ └── k8s_config.go ├── util │ ├── md5.go │ ├── util_test.go │ ├── panic_handler.go │ └── util.go ├── leaselock │ └── lock.go ├── server │ ├── middleware │ │ └── logger.go │ ├── service │ │ ├── join.go │ │ ├── list_wrap.go │ │ └── list.go │ ├── server.go │ └── handler.go ├── client │ ├── clientset │ │ └── versioned │ │ │ ├── doc.go │ │ │ ├── fake │ │ │ ├── doc.go │ │ │ ├── register.go │ │ │ └── clientset_generated.go │ │ │ ├── scheme │ │ │ ├── doc.go │ │ │ └── register.go │ │ │ ├── typed │ │ │ └── multicluster │ │ │ │ └── v1alpha1 │ │ │ │ ├── fake │ │ │ │ ├── doc.go │ │ │ │ ├── fake_multicluster_client.go │ │ │ │ └── fake_multicluster.go │ │ │ │ ├── generated_expansion.go │ │ │ │ ├── doc.go │ │ │ │ ├── multicluster_client.go │ │ │ │ └── multicluster.go │ │ │ └── clientset.go │ ├── listers │ │ └── multicluster │ │ │ └── v1alpha1 │ │ │ ├── expansion_generated.go │ │ │ └── multicluster.go │ └── informers │ │ └── externalversions │ │ ├── internalinterfaces │ │ └── factory_interfaces.go │ │ ├── multicluster │ │ ├── v1alpha1 │ │ │ ├── interface.go │ │ │ └── multicluster.go │ │ └── interface.go │ │ ├── generic.go │ │ └── factory.go ├── kubectl_client │ └── convert │ │ └── convert.go ├── multi_cluster_controller │ ├── cluster_controller.go │ ├── resource_controller.go │ └── init_resource.go ├── caches │ ├── workqueue │ │ └── workqueue.go │ └── handler.go └── options │ ├── server │ └── server_options.go │ └── mysql │ └── mysql_options.go ├── cmd ├── server │ ├── main.go │ └── app │ │ ├── options │ │ └── options.go │ │ └── app.go └── ctl_plugin │ ├── common │ ├── request_test.go │ ├── config_test.go │ ├── config.go │ └── request.go │ ├── resource │ ├── join │ │ ├── join.go │ │ └── cmd.go │ ├── describe │ │ ├── pod.go │ │ ├── configmap.go │ │ ├── deployment.go │ │ ├── resource.go │ │ └── cmd.go │ └── list │ │ ├── cluster.go │ │ ├── resource.go │ │ ├── cmd.go │ │ ├── configmap.go │ │ ├── deployment.go │ │ └── pod.go │ └── main.go ├── helm ├── .helmignore ├── templates │ ├── service.yaml │ ├── rbac.yaml │ └── deployment.yaml ├── Chart.yaml ├── README.md └── values.yaml ├── .gitignore ├── config.yaml ├── Dockerfile ├── yaml ├── multi_cluster_resource_configmap_example.yaml ├── multi_cluster_resource_pod_example.yaml └── multi_cluster_resource_deployment_example.yaml ├── generate-config.sh ├── Makefile ├── mysql ├── resources.sql └── README.md ├── deploy ├── rbac.yaml └── deploy.yaml └── go.mod /migrations/1_init.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `resources`; -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ### 示例文档 2 | - 跨集群互通方案:[范例](./multi-cluster-network-connectivity.md) 3 | - kind 部署项目:TODO -------------------------------------------------------------------------------- /image/无标题-2023-08-10-2343.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kubernetes-Learning-Playground/multi-clusters/HEAD/image/无标题-2023-08-10-2343.png -------------------------------------------------------------------------------- /image/无标题-2023-08-11-2343.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kubernetes-Learning-Playground/multi-clusters/HEAD/image/无标题-2023-08-11-2343.png -------------------------------------------------------------------------------- /pkg/store/stores.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | // Factory 存储接口,目前使用 gorm.DB 实例实现 8 | type Factory interface { 9 | Close() error 10 | GetDB() *gorm.DB 11 | } 12 | -------------------------------------------------------------------------------- /pkg/apis/multicluster/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | // Package v1alpha1 contains API Schema definitions for the ecs v1alpha1 API group 2 | // +k8s:deepcopy-gen=package,register 3 | // +groupName=mulitcluster.practice.com 4 | package v1alpha1 5 | -------------------------------------------------------------------------------- /pkg/apis/multiclusterresource/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | // Package v1alpha1 contains API Schema definitions for the ecs v1alpha1 API group 2 | // +k8s:deepcopy-gen=package,register 3 | // +groupName=mulitcluster.practice.com 4 | package v1alpha1 5 | -------------------------------------------------------------------------------- /pkg/store/model/delete.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "gorm.io/gorm" 4 | 5 | func DeleteResourcesByClusterName(db *gorm.DB, clusterName string) error { 6 | rr := Resources{Cluster: clusterName} 7 | return db.Where("cluster=?", clusterName).Delete(rr).Error 8 | } 9 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/myoperator/multiclusteroperator/cmd/server/app" 5 | "os" 6 | ) 7 | 8 | func main() { 9 | cmd := app.NewServerCommand() 10 | if err := cmd.Execute(); err != nil { 11 | os.Exit(1) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pkg/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "k8s.io/klog/v2" 6 | "testing" 7 | ) 8 | 9 | func TestLoadConfig(test *testing.T) { 10 | // 1. 项目配置 11 | sysConfig, err := BuildConfig("../../config.yaml") 12 | if err != nil { 13 | klog.Error("load config error: ", err) 14 | return 15 | } 16 | fmt.Println(sysConfig) 17 | fmt.Println(sysConfig.Clusters[0]) 18 | } 19 | -------------------------------------------------------------------------------- /helm/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | resources 2 | .DS_Store 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | bin 10 | testbin/* 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Kubernetes Generated files - skip generated files, except for vendored files 19 | 20 | !vendor/**/zz_generated.* 21 | 22 | # editor and IDE paraphernalia 23 | .idea 24 | *.swp 25 | *.swo 26 | *~ 27 | -------------------------------------------------------------------------------- /pkg/util/md5.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // HashObject 序列化内容进行 md5 10 | func HashObject(data []byte) string { 11 | has := md5.Sum(data) 12 | return fmt.Sprintf("%x", has) 13 | } 14 | 15 | func Md5slice(clusters []string) string { 16 | if len(clusters) == 0 { 17 | return "" 18 | } 19 | str := strings.Join(clusters, "") 20 | data := []byte(str) 21 | has := md5.Sum(data) 22 | md5str := fmt.Sprintf("%x", has) 23 | return md5str 24 | } 25 | -------------------------------------------------------------------------------- /helm/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ .Values.base.serviceName }} 5 | namespace: {{ .Values.base.namespace }} 6 | labels: 7 | app: {{ .Values.base.name }} 8 | spec: 9 | type: {{ .Values.service.type }} 10 | {{- if .Values.service.ports }} 11 | ports: 12 | {{- range .Values.service.ports }} 13 | - port: {{ .port }} 14 | targetPort: {{ .port }} 15 | nodePort: {{ .nodePort }} 16 | protocol: TCP 17 | name: {{ .name }} 18 | {{- end }} 19 | {{- end }} 20 | selector: 21 | app: {{ .Values.base.name }} 22 | -------------------------------------------------------------------------------- /cmd/ctl_plugin/common/request_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | v1 "k8s.io/api/core/v1" 6 | 7 | "k8s.io/apimachinery/pkg/util/json" 8 | "log" 9 | "testing" 10 | ) 11 | 12 | func TestRequest(t *testing.T) { 13 | 14 | m := map[string]string{} 15 | m["gvr"] = "v1.configmaps" 16 | m["cluster"] = "cluster1" 17 | rr := make([]*v1.ConfigMap, 0) 18 | r, err := HttpClient.DoGet("http://localhost:8888/v1/list", m) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | err = json.Unmarshal(r, &rr) 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | fmt.Println(rr) 28 | } 29 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | clusters: # 集群列表 2 | - metadata: 3 | clusterName: tencent1 # 自定义集群名 4 | insecure: true # 是否开启跳过 tls 证书认证 5 | configPath: /app/file/config-tencent1 # kube config 配置文件地址 6 | - metadata: 7 | clusterName: tencent2 # 自定义集群名 8 | insecure: true # 是否开启跳过 tls 证书认证 9 | configPath: /app/file/config-tencent2 # kube config 配置文件地址 10 | - metadata: 11 | clusterName: tencent4 # 自定义集群名 12 | insecure: true # 是否开启跳过 tls 证书认证 13 | configPath: /app/file/config-tencent4 # kube config 配置文件地址 14 | isMaster: true # 标示主集群 -------------------------------------------------------------------------------- /pkg/leaselock/lock.go: -------------------------------------------------------------------------------- 1 | package leaselock 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | clientset "k8s.io/client-go/kubernetes" 6 | "k8s.io/client-go/tools/leaderelection/resourcelock" 7 | ) 8 | 9 | // GetNewLock 创建集群锁资源 10 | func GetNewLock(lockname, podname, namespace string, client *clientset.Clientset) *resourcelock.LeaseLock { 11 | return &resourcelock.LeaseLock{ 12 | LeaseMeta: metav1.ObjectMeta{ 13 | Name: lockname, 14 | Namespace: namespace, 15 | }, 16 | Client: client.CoordinationV1(), 17 | LockConfig: resourcelock.ResourceLockConfig{ 18 | Identity: podname, // 锁的唯一标示,使用pod name 19 | }, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/server/middleware/logger.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "k8s.io/klog/v2" 6 | "time" 7 | ) 8 | 9 | // LogMiddleware 日志中间件 10 | func LogMiddleware() gin.HandlerFunc { 11 | return func(ctx *gin.Context) { 12 | startTime := time.Now() 13 | ctx.Next() 14 | endTime := time.Now() 15 | // 响应时间 16 | execTime := endTime.Sub(startTime) 17 | requestMethod := ctx.Request.Method 18 | requestURI := ctx.Request.RequestURI 19 | statusCode := ctx.Writer.Status() 20 | requestIP := ctx.ClientIP() 21 | // 日志格式 22 | klog.Infof("| status=%2d | duration=%v | ip=%s | method=%s | url=%s |", 23 | statusCode, 24 | execTime, 25 | requestIP, 26 | requestMethod, 27 | requestURI, 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/store/mysql/mysql_store.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "github.com/myoperator/multiclusteroperator/pkg/store" 5 | "github.com/pkg/errors" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type mysqlStoreFactory struct { 10 | db *gorm.DB 11 | } 12 | 13 | var _ store.Factory = (*mysqlStoreFactory)(nil) 14 | 15 | func (ds *mysqlStoreFactory) Close() error { 16 | db, err := ds.db.DB() 17 | if err != nil { 18 | return errors.Wrap(err, "get gorm db instance failed") 19 | } 20 | return db.Close() 21 | } 22 | 23 | func (ds *mysqlStoreFactory) GetDB() *gorm.DB { 24 | return ds.db 25 | } 26 | 27 | // NewStoreFactory 创建实例 28 | func NewStoreFactory(db *gorm.DB) (store.Factory, error) { 29 | st := &mysqlStoreFactory{ 30 | db: db, 31 | } 32 | return st, nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated clientset. 20 | package versioned 21 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated fake clientset. 20 | package fake 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # apiserver的Dockerfile文件 2 | FROM golang:1.20.7-alpine3.17 as builder 3 | 4 | WORKDIR /app 5 | 6 | # copy modules manifests 7 | COPY go.mod go.mod 8 | COPY go.sum go.sum 9 | 10 | ENV GOPROXY=https://goproxy.cn,direct 11 | ENV GO111MODULE=on 12 | 13 | # cache modules 14 | RUN go mod download 15 | 16 | # copy source code 17 | COPY pkg/ pkg/ 18 | COPY cmd/ cmd/ 19 | COPY resources/ resources/ 20 | COPY migrations/ migrations/ 21 | COPY config.yaml config.yaml 22 | 23 | # build 24 | RUN CGO_ENABLED=0 go build \ 25 | -a -o multi-cluster-operator cmd/server/main.go 26 | 27 | FROM alpine:3.13 28 | WORKDIR /app 29 | 30 | USER root 31 | COPY --from=builder --chown=root:root /app/multi-cluster-operator . 32 | COPY --from=builder --chown=root:root /app/migrations . 33 | ENTRYPOINT ["./multi-cluster-operator"] -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/scheme/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package contains the scheme of the automatically generated clientset. 20 | package scheme 21 | -------------------------------------------------------------------------------- /pkg/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | . "github.com/smartystreets/goconvey/convey" 5 | "k8s.io/apimachinery/pkg/runtime/schema" 6 | "testing" 7 | ) 8 | 9 | func TestParseIntoGvr(t *testing.T) { 10 | Convey("Parse to GVR test", t, func() { 11 | resTest := schema.GroupVersionResource{ 12 | Group: "api.practice.com", 13 | Version: "v1alpha1", 14 | Resource: "tests", 15 | } 16 | res, _ := ParseIntoGvr("api.practice.com/v1alpha1/tests", "/") 17 | So(res, ShouldEqual, resTest) 18 | 19 | pods := schema.GroupVersionResource{ 20 | Group: "", 21 | Version: "v1", 22 | Resource: "pods", 23 | } 24 | res, _ = ParseIntoGvr("core/v1/pods", "/") 25 | So(res, ShouldEqual, pods) 26 | 27 | _, err := ParseIntoGvr("pods", "/") 28 | So(err, ShouldNotBeNil) 29 | 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/multicluster/v1alpha1/fake/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // Package fake has the automatically generated clients. 20 | package fake 21 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/multicluster/v1alpha1/generated_expansion.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | type MultiClusterExpansion interface{} 22 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/multicluster/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | // This package has the automatically generated typed clients. 20 | package v1alpha1 21 | -------------------------------------------------------------------------------- /pkg/server/service/join.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/myoperator/multiclusteroperator/pkg/config" 5 | "github.com/myoperator/multiclusteroperator/pkg/multi_cluster_controller" 6 | "k8s.io/client-go/rest" 7 | ) 8 | 9 | type JoinService struct { 10 | Mch *multi_cluster_controller.MultiClusterHandler 11 | } 12 | 13 | func (join *JoinService) Join(clusterName string, insecure bool, restConfig *rest.Config) error { 14 | 15 | cluster := &config.Cluster{ 16 | MetaData: config.MetaData{ 17 | ClusterName: clusterName, 18 | IsMaster: false, 19 | Insecure: insecure, 20 | RestConfig: restConfig, 21 | }, 22 | } 23 | 24 | return multi_cluster_controller.AddMultiClusterHandler(cluster) 25 | } 26 | 27 | func (join *JoinService) UnJoin(clusterName string) error { 28 | return multi_cluster_controller.DeleteMultiClusterHandlerByClusterName(clusterName) 29 | } 30 | -------------------------------------------------------------------------------- /cmd/ctl_plugin/common/config_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | func TestLoadConfigFile(t *testing.T) { 11 | // 获取用户的 Home 目录 12 | homeDir, err := os.UserHomeDir() 13 | if err != nil { 14 | fmt.Printf("Failed to get user's home directory: %v\n", err) 15 | return 16 | } 17 | 18 | // 创建目录 19 | dirPath := filepath.Join(homeDir, ".multi-cluster-operator") 20 | err = os.MkdirAll(dirPath, 0777) 21 | if err != nil { 22 | fmt.Printf("Failed to create directory: %v\n", err) 23 | return 24 | } 25 | 26 | // 创建配置文件 27 | configFilePath := filepath.Join(dirPath, "config") 28 | configContent := "server: 8888\n" 29 | err = os.WriteFile(configFilePath, []byte(configContent), 0777) 30 | if err != nil { 31 | fmt.Printf("Failed to create config file: %v\n", err) 32 | return 33 | } 34 | 35 | fmt.Println("Config file created successfully.") 36 | } 37 | -------------------------------------------------------------------------------- /pkg/client/listers/multicluster/v1alpha1/expansion_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by lister-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | // MultiClusterListerExpansion allows custom methods to be added to 22 | // MultiClusterLister. 23 | type MultiClusterListerExpansion interface{} 24 | 25 | // MultiClusterNamespaceListerExpansion allows custom methods to be added to 26 | // MultiClusterNamespaceLister. 27 | type MultiClusterNamespaceListerExpansion interface{} 28 | -------------------------------------------------------------------------------- /cmd/ctl_plugin/resource/join/join.go: -------------------------------------------------------------------------------- 1 | package join 2 | 3 | import ( 4 | "fmt" 5 | "github.com/myoperator/multiclusteroperator/cmd/ctl_plugin/common" 6 | "net/http" 7 | ) 8 | 9 | // joinClusterByFile 根据传入文件调用 join 接口 10 | func joinClusterByFile(filePath, clusterName string) error { 11 | m := map[string]string{} 12 | 13 | if clusterName != "" { 14 | m["cluster"] = clusterName 15 | } 16 | 17 | url := fmt.Sprintf("http://%v:%v/v1/join", common.ServerIp, common.ServerPort) 18 | b, err := common.HttpClient.DoUploadFile(url, m, nil, filePath) 19 | if err != nil { 20 | return err 21 | } 22 | fmt.Println(string(b)) 23 | return nil 24 | } 25 | 26 | // unJoinClusterByName 根据传入集群名调用 unjoin 接口 27 | func unJoinClusterByName(clusterName string) error { 28 | m := map[string]string{} 29 | 30 | if clusterName != "" { 31 | m["cluster"] = clusterName 32 | } 33 | 34 | url := fmt.Sprintf("http://%v:%v/v1/unjoin", common.ServerIp, common.ServerPort) 35 | b, err := common.HttpClient.DoRequest(http.MethodDelete, url, m, nil, nil) 36 | if err != nil { 37 | return err 38 | } 39 | fmt.Println(string(b)) 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /yaml/multi_cluster_resource_configmap_example.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mulitcluster.practice.com/v1alpha1 2 | kind: MultiClusterResource 3 | metadata: 4 | name: myconfigmap.configmap 5 | namespace: default 6 | spec: 7 | # 模版:填写k8s 原生资源 8 | template: 9 | kind: ConfigMap 10 | apiVersion: v1 11 | metadata: 12 | name: multiclusterresource-configmap 13 | namespace: default 14 | data: 15 | example.property.1: hello 16 | example.property.2: world 17 | example.property.file: |- 18 | property.1=value-1 19 | property.2=value-2 20 | property.3=value-3 21 | # 可自行填写多个集群 22 | placement: 23 | clusters: 24 | - name: tencent1 25 | - name: tencent2 26 | - name: tencent4 27 | customize: 28 | clusters: 29 | - name: tencent1 30 | action: 31 | # 替换 key value 32 | - path: "/data/example.property.1" 33 | value: 34 | - "patch-configmaps-test" 35 | op: "replace" 36 | # 新增 key value 37 | - path: "/data/cluster1test" 38 | value: 39 | - "patch-configmaps-test" 40 | op: "add" 41 | -------------------------------------------------------------------------------- /pkg/apis/multicluster/v1alpha1/types.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // +genclient 8 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 9 | // MultiCluster 10 | type MultiCluster struct { 11 | metav1.TypeMeta `json:",inline"` 12 | metav1.ObjectMeta `json:"metadata,omitempty"` 13 | Spec ClusterSpec `json:"spec,omitempty"` 14 | Status ClusterStatus `json:"status,omitempty"` 15 | } 16 | 17 | type ClusterSpec struct { 18 | // Name 集群名 19 | Name string `json:"name"` 20 | // Host 集群地址 21 | Host string `json:"host"` 22 | // Version 集群版本 23 | Version string `json:"version"` 24 | // Platform 平台版本 25 | Platform string `json:"platform"` 26 | // IsMaster 是否为 master 主集群 27 | IsMaster string `json:"isMaster"` 28 | } 29 | 30 | type ClusterStatus struct { 31 | // Status 集群状态 32 | Status string `json:"status"` 33 | } 34 | 35 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 36 | // MultiClusterList 37 | type MultiClusterList struct { 38 | metav1.TypeMeta `json:",inline"` 39 | // +optional 40 | metav1.ListMeta `json:"metadata,omitempty"` 41 | 42 | Items []MultiCluster `json:"items"` 43 | } 44 | -------------------------------------------------------------------------------- /helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: helm 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /cmd/ctl_plugin/resource/describe/pod.go: -------------------------------------------------------------------------------- 1 | package describe 2 | 3 | import ( 4 | "fmt" 5 | yy "github.com/ghodss/yaml" 6 | "github.com/goccy/go-json" 7 | "github.com/myoperator/multiclusteroperator/cmd/ctl_plugin/common" 8 | v1 "k8s.io/api/core/v1" 9 | "log" 10 | "net/http" 11 | ) 12 | 13 | func Pods(cluster, name, namespace string) error { 14 | 15 | m := map[string]string{} 16 | m["limit"] = "0" 17 | m["gvr"] = "v1/pods" 18 | if cluster != "" { 19 | m["cluster"] = cluster 20 | } 21 | 22 | if name != "" { 23 | m["name"] = name 24 | } 25 | 26 | if namespace != "" { 27 | m["namespace"] = namespace 28 | } 29 | 30 | rr := make([]*v1.Pod, 0) 31 | url := fmt.Sprintf("http://%v:%v/v1/list", common.ServerIp, common.ServerPort) 32 | r, err := common.HttpClient.DoRequest(http.MethodGet, url, m, nil, nil) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | err = json.Unmarshal(r, &rr) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | for _, pod := range rr { 43 | resByte, err := json.Marshal(pod) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | resByte, _ = yy.JSONToYAML(resByte) 48 | fmt.Printf(string(resByte)) 49 | fmt.Println("---------------------------------") 50 | } 51 | 52 | return nil 53 | 54 | } 55 | -------------------------------------------------------------------------------- /cmd/ctl_plugin/resource/describe/configmap.go: -------------------------------------------------------------------------------- 1 | package describe 2 | 3 | import ( 4 | "fmt" 5 | yy "github.com/ghodss/yaml" 6 | "github.com/goccy/go-json" 7 | "github.com/myoperator/multiclusteroperator/cmd/ctl_plugin/common" 8 | v1 "k8s.io/api/core/v1" 9 | "log" 10 | "net/http" 11 | ) 12 | 13 | func Configmaps(cluster, name, namespace string) error { 14 | 15 | m := map[string]string{} 16 | m["limit"] = "0" 17 | m["gvr"] = "v1./configmaps" 18 | if cluster != "" { 19 | m["cluster"] = cluster 20 | } 21 | 22 | if name != "" { 23 | m["name"] = name 24 | } 25 | 26 | if namespace != "" { 27 | m["namespace"] = namespace 28 | } 29 | 30 | rr := make([]*v1.ConfigMap, 0) 31 | url := fmt.Sprintf("http://%v:%v/v1/list", common.ServerIp, common.ServerPort) 32 | r, err := common.HttpClient.DoRequest(http.MethodGet, url, m, nil, nil) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | err = json.Unmarshal(r, &rr) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | for _, cm := range rr { 43 | 44 | resByte, err := json.Marshal(cm) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | resByte, _ = yy.JSONToYAML(resByte) 49 | fmt.Printf(string(resByte)) 50 | fmt.Println("---------------------------------") 51 | } 52 | 53 | return nil 54 | 55 | } 56 | -------------------------------------------------------------------------------- /cmd/ctl_plugin/resource/describe/deployment.go: -------------------------------------------------------------------------------- 1 | package describe 2 | 3 | import ( 4 | "fmt" 5 | yy "github.com/ghodss/yaml" 6 | "github.com/goccy/go-json" 7 | "github.com/myoperator/multiclusteroperator/cmd/ctl_plugin/common" 8 | appsv1 "k8s.io/api/apps/v1" 9 | "log" 10 | "net/http" 11 | ) 12 | 13 | func Deployments(cluster, name, namespace string) error { 14 | 15 | m := map[string]string{} 16 | m["limit"] = "0" 17 | m["gvr"] = "apps/v1/deployments" 18 | if cluster != "" { 19 | m["cluster"] = cluster 20 | } 21 | 22 | if name != "" { 23 | m["name"] = name 24 | } 25 | 26 | if namespace != "" { 27 | m["namespace"] = namespace 28 | } 29 | 30 | rr := make([]*appsv1.Deployment, 0) 31 | url := fmt.Sprintf("http://%v:%v/v1/list", common.ServerIp, common.ServerPort) 32 | r, err := common.HttpClient.DoRequest(http.MethodGet, url, m, nil, nil) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | err = json.Unmarshal(r, &rr) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | for _, deployment := range rr { 43 | resByte, err := json.Marshal(deployment) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | resByte, _ = yy.JSONToYAML(resByte) 48 | fmt.Printf(string(resByte)) 49 | fmt.Println("---------------------------------") 50 | } 51 | 52 | return nil 53 | 54 | } 55 | -------------------------------------------------------------------------------- /generate-config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 用于生成 ctl 命令使用的配置文件 3 | 4 | 5 | # 默认配置参数 6 | # operator ip 7 | serverIP="localhost" 8 | # operator port 9 | serverPort="31888" 10 | # 主集群 kube config 路径。 11 | # 注:如果 .multi-cluster-operator 是远程调用, 12 | # 此变量不适用,亦即下列命令不生效: 13 | # kubectl-multicluster apply -f yaml/multi_cluster_resource_configmap_example.yaml 14 | # kubectl-multicluster delete -f yaml/multi_cluster_resource_configmap_example.yaml 15 | masterClusterKubeConfigPath="/root/.kube/config" 16 | 17 | # 解析选项标记 18 | while [[ $# -gt 0 ]]; do 19 | key="$1" 20 | 21 | case $key in 22 | --port) 23 | serverPort="$2" 24 | shift 25 | shift 26 | ;; 27 | --ip) 28 | serverIP="$2" 29 | shift 30 | shift 31 | ;; 32 | --config) 33 | masterClusterKubeConfigPath="$2" 34 | shift 35 | shift 36 | ;; 37 | *) 38 | echo "错误:未知选项标记 $key" 39 | exit 1 40 | ;; 41 | esac 42 | done 43 | 44 | # 生成用户主目录下的配置文件目录 45 | configDir="$HOME/.multi-cluster-operator" 46 | if [ ! -d "$configDir" ]; then 47 | mkdir -p "$configDir" 48 | fi 49 | 50 | # 生成配置文件 51 | configFile="$configDir/config" 52 | cat < "$configFile" 53 | serverIP: $serverIP 54 | serverPort: $serverPort 55 | masterClusterKubeConfigPath: $masterClusterKubeConfigPath 56 | EOF 57 | 58 | echo "配置文件已生成:$configFile" -------------------------------------------------------------------------------- /cmd/ctl_plugin/resource/describe/resource.go: -------------------------------------------------------------------------------- 1 | package describe 2 | 3 | import ( 4 | "fmt" 5 | yy "github.com/ghodss/yaml" 6 | "github.com/goccy/go-json" 7 | "github.com/myoperator/multiclusteroperator/cmd/ctl_plugin/common" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | "log" 10 | "net/http" 11 | ) 12 | 13 | // FIXME: describe 命令可以直接解析 gvk 就好,不用特别分资源 14 | 15 | func Resource(cluster, name, namespace, gvr string) error { 16 | 17 | m := map[string]string{} 18 | m["limit"] = "0" 19 | m["gvr"] = gvr 20 | if cluster != "" { 21 | m["cluster"] = cluster 22 | } 23 | 24 | if name != "" { 25 | m["name"] = name 26 | } 27 | 28 | if namespace != "" { 29 | m["namespace"] = namespace 30 | } 31 | 32 | rr := make([]*unstructured.Unstructured, 0) 33 | url := fmt.Sprintf("http://%v:%v/v1/list", common.ServerIp, common.ServerPort) 34 | r, err := common.HttpClient.DoRequest(http.MethodGet, url, m, nil, nil) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | err = json.Unmarshal(r, &rr) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | 44 | for _, re := range rr { 45 | resByte, err := json.Marshal(re) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | resByte, _ = yy.JSONToYAML(resByte) 50 | fmt.Printf(string(resByte)) 51 | fmt.Println("---------------------------------") 52 | } 53 | 54 | return nil 55 | 56 | } 57 | -------------------------------------------------------------------------------- /pkg/util/panic_handler.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "k8s.io/klog/v2" 5 | "net/http" 6 | "runtime" 7 | ) 8 | 9 | func HandleCrash(additionalHandlers ...func(interface{})) { 10 | if r := recover(); r != nil { 11 | for _, fn := range PanicHandlers { 12 | fn(r) 13 | } 14 | for _, fn := range additionalHandlers { 15 | fn(r) 16 | } 17 | } 18 | } 19 | 20 | var PanicHandlers = []func(interface{}){logPanic} 21 | 22 | // logPanic logs the caller tree when a panic occurs (except in the special case of http.ErrAbortHandler). 23 | func logPanic(r interface{}) { 24 | if r == http.ErrAbortHandler { 25 | // honor the http.ErrAbortHandler sentinel panic value: 26 | // ErrAbortHandler is a sentinel panic value to abort a handler. 27 | // While any panic from ServeHTTP aborts the response to the client, 28 | // panicking with ErrAbortHandler also suppresses logging of a stack trace to the server's error log. 29 | return 30 | } 31 | 32 | // Same as stdlib http server code. Manually allocate stack trace buffer size 33 | // to prevent excessively large logs 34 | const size = 64 << 10 35 | stacktrace := make([]byte, size) 36 | stacktrace = stacktrace[:runtime.Stack(stacktrace, false)] 37 | if _, ok := r.(string); ok { 38 | klog.Errorf("Observed a panic: %s\n%s", r, stacktrace) 39 | } else { 40 | klog.Errorf("Observed a panic: %#v (%v)\n%s", r, r, stacktrace) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/config/init_ctl_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // CreateCtlFile 创建命令行工具需要的配置文件 4 | // 默认在 "~/.multi-cluster-operator/config" 中配置 5 | // FIXME: 容器里创建没用,考虑废弃。。。。 6 | //func CreateCtlFile(opt *options.ServerOptions, masterClusterKubeConfigPath string) { 7 | // // 获取用户的 Home 目录 8 | // homeDir, err := os.UserHomeDir() 9 | // if err != nil { 10 | // klog.Errorf("Failed to get user's home directory: %v\n", err) 11 | // return 12 | // } 13 | // 14 | // // 创建目录 15 | // dirPath := filepath.Join(homeDir, ".multi-cluster-operator") 16 | // err = os.MkdirAll(dirPath, 0777) 17 | // if err != nil { 18 | // klog.Errorf("Failed to create directory: %v\n", err) 19 | // return 20 | // } 21 | // 22 | // // 创建配置文件 23 | // configFilePath := filepath.Join(dirPath, "config") 24 | // configContent := fmt.Sprintf("serverIP: %v\nserverPort: %v\nmasterClusterKubeConfigPath: %v", "localhost", opt.CtlPort, masterClusterKubeConfigPath) 25 | // 26 | // // 创建或覆盖文件 27 | // file, err := os.OpenFile(configFilePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777) 28 | // if err != nil { 29 | // klog.Fatalf("Error creating or truncating file: %s\n", err) 30 | // } 31 | // defer file.Close() 32 | // 33 | // _, err = io.WriteString(file, configContent) 34 | // if err != nil { 35 | // klog.Fatalf("Failed to create config file: %v\n", err) 36 | // return 37 | // } 38 | // 39 | // klog.Infof("multi-cluster-ctl config file created successfully.") 40 | //} 41 | -------------------------------------------------------------------------------- /cmd/ctl_plugin/resource/join/cmd.go: -------------------------------------------------------------------------------- 1 | package join 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var ( 9 | JoinCmd *cobra.Command 10 | UnJoinCmd *cobra.Command 11 | ) 12 | 13 | func init() { 14 | 15 | JoinCmd = &cobra.Command{ 16 | Use: "join [flags]", 17 | Short: "join --file=xxx, input kubeconfig file path", 18 | Example: "join --file xxxxx", 19 | RunE: func(c *cobra.Command, args []string) error { 20 | if len(args) == 0 { 21 | return fmt.Errorf("Please specify a resource pods, deployments, configmaps\n") 22 | } 23 | clusterName := args[0] 24 | 25 | file, err := c.Flags().GetString("file") 26 | if err != nil { 27 | return err 28 | } 29 | 30 | err = joinClusterByFile(file, clusterName) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | return nil 36 | }, 37 | } 38 | 39 | UnJoinCmd = &cobra.Command{ 40 | Use: "unjoin [flags]", 41 | Short: "unjoin , input clusterName", 42 | Example: "unjoin ", 43 | RunE: func(c *cobra.Command, args []string) error { 44 | if len(args) == 0 { 45 | return fmt.Errorf("Please specify a resource pods, deployments, configmaps\n") 46 | } 47 | clusterName := args[0] 48 | err := unJoinClusterByName(clusterName) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | return nil 54 | }, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /pkg/kubectl_client/convert/convert.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "fmt" 5 | "k8s.io/apimachinery/pkg/api/meta" 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | ) 9 | 10 | // ObjectToUnstructuredList 转换 runtime.Object -> []unstructured.Unstructured 11 | func ObjectToUnstructuredList(obj runtime.Object) ([]unstructured.Unstructured, error) { 12 | list := make([]unstructured.Unstructured, 0, 0) 13 | if meta.IsListType(obj) { 14 | if _, ok := obj.(*unstructured.UnstructuredList); !ok { 15 | return nil, fmt.Errorf("unable to convert runtime object to list") 16 | } 17 | 18 | for _, u := range obj.(*unstructured.UnstructuredList).Items { 19 | list = append(list, u) 20 | } 21 | return list, nil 22 | } 23 | 24 | unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | unstructuredObj := unstructured.Unstructured{Object: unstructuredMap} 30 | list = append(list, unstructuredObj) 31 | 32 | return list, nil 33 | } 34 | 35 | // ObjectToUnstructured 转换 runtime.Object -> unstructured.Unstructured 36 | func ObjectToUnstructured(obj runtime.Object) (unstructured.Unstructured, error) { 37 | unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) 38 | if err != nil { 39 | return unstructured.Unstructured{}, err 40 | } 41 | return unstructured.Unstructured{Object: unstructuredMap}, nil 42 | } 43 | -------------------------------------------------------------------------------- /migrations/1_init.up.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `resources`; 2 | CREATE TABLE `resources` ( 3 | `id` bigint(11) UNSIGNED NOT NULL AUTO_INCREMENT, 4 | `namespace` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL, 5 | `name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 6 | `cluster` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 7 | `group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 8 | `version` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 9 | `resource` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 10 | `kind` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 11 | `resource_version` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL, 12 | `owner` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT 'owner uid', 13 | `uid` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 14 | `hash` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL, 15 | `object` json NULL, 16 | `create_at` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | `delete_at` timestamp(0) NULL DEFAULT NULL, 18 | `update_at` timestamp(0) NULL DEFAULT NULL, 19 | PRIMARY KEY (`id`) USING BTREE, 20 | UNIQUE INDEX `uid`(`uid`) USING BTREE 21 | ) ENGINE = MyISAM AUTO_INCREMENT = 0 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic; 22 | 23 | 24 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/multicluster/v1alpha1/fake/fake_multicluster_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | v1alpha1 "github.com/myoperator/multiclusteroperator/pkg/client/clientset/versioned/typed/multicluster/v1alpha1" 23 | rest "k8s.io/client-go/rest" 24 | testing "k8s.io/client-go/testing" 25 | ) 26 | 27 | type FakeMulitclusterV1alpha1 struct { 28 | *testing.Fake 29 | } 30 | 31 | func (c *FakeMulitclusterV1alpha1) MultiClusters(namespace string) v1alpha1.MultiClusterInterface { 32 | return &FakeMultiClusters{c, namespace} 33 | } 34 | 35 | // RESTClient returns a RESTClient that is used to communicate 36 | // with API server by this client implementation. 37 | func (c *FakeMulitclusterV1alpha1) RESTClient() rest.Interface { 38 | var ret *rest.RESTClient 39 | return ret 40 | } 41 | -------------------------------------------------------------------------------- /pkg/apis/multicluster/v1alpha1/register.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | ) 8 | 9 | const ( 10 | MultiClusterGroup = "mulitcluster.practice.com" 11 | MultiClusterVersion = "v1alpha1" 12 | MultiClusterKind = "MultiCluster" 13 | MultiClusterApiVersion = "mulitcluster.practice.com/v1alpha1" 14 | ) 15 | 16 | // SchemeGroupVersion is group version used to register these objects 17 | var SchemeGroupVersion = schema.GroupVersion{Group: MultiClusterGroup, Version: MultiClusterVersion} 18 | 19 | // Kind takes an unqualified kind and return back a Group qualified GroupKind 20 | func Kind(kind string) schema.GroupKind { 21 | return SchemeGroupVersion.WithKind(kind).GroupKind() 22 | } 23 | 24 | // GetResource takes an unqualified multiclusterresource and returns a Group qualified GroupResource 25 | func GetResource(resource string) schema.GroupResource { 26 | return SchemeGroupVersion.WithResource(resource).GroupResource() 27 | } 28 | 29 | var ( 30 | SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 31 | AddToScheme = SchemeBuilder.AddToScheme 32 | ) 33 | 34 | // Adds the list of known types to Scheme. 35 | func addKnownTypes(scheme *runtime.Scheme) error { 36 | scheme.AddKnownTypes(SchemeGroupVersion, 37 | &MultiCluster{}, 38 | &MultiClusterList{}, 39 | ) 40 | metav1.AddToGroupVersion(scheme, SchemeGroupVersion) 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /cmd/ctl_plugin/common/config.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pkg/errors" 6 | "gopkg.in/yaml.v2" 7 | "io" 8 | "k8s.io/klog/v2" 9 | "log" 10 | "os" 11 | ) 12 | 13 | // CtlConfig 命令行配置文件 14 | type CtlConfig struct { 15 | // Server 端口 16 | ServerIP string `yaml:"serverIP"` 17 | ServerPort string `yaml:"serverPort"` 18 | MasterClusterKubeConfigPath string `yaml:"masterClusterKubeConfigPath"` 19 | Token string `yaml:"token"` 20 | } 21 | 22 | // LoadConfigFile 读取配置文件,模仿kubectl,默认在~/.multi-cluster-operator/config 23 | func LoadConfigFile() *CtlConfig { 24 | home, err := os.UserHomeDir() 25 | if err != nil { 26 | log.Fatalln(err) 27 | } 28 | configFile := fmt.Sprintf("%s/.multi-cluster-operator/config", home) 29 | if _, err := os.Stat(configFile); errors.Is(err, os.ErrNotExist) { 30 | klog.Fatal("config file not found") 31 | } 32 | 33 | // 接配置文件 34 | cfg := &CtlConfig{} 35 | err = yaml.Unmarshal(MustLoadFile(configFile), cfg) 36 | if err != nil { 37 | log.Fatalln(err) 38 | } 39 | return cfg 40 | } 41 | 42 | // MustLoadFile 如果读不到file,就panic 43 | func MustLoadFile(path string) []byte { 44 | b, err := LoadFile(path) 45 | if err != nil { 46 | panic(err) 47 | } 48 | return b 49 | } 50 | 51 | // LoadFile 加载指定目录的文件, 全部取出内容 52 | func LoadFile(path string) ([]byte, error) { 53 | f, err := os.Open(path) 54 | if err != nil { 55 | return nil, err 56 | } 57 | b, err := io.ReadAll(f) 58 | if err != nil { 59 | return nil, err 60 | } 61 | return b, err 62 | } 63 | -------------------------------------------------------------------------------- /pkg/apis/multiclusterresource/v1alpha1/register.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | ) 8 | 9 | const ( 10 | MultiClusterResourceGroup = "mulitcluster.practice.com" 11 | MultiClusterResourceVersion = "v1alpha1" 12 | MultiClusterResourceKind = "MultiClusterResource" 13 | MultiClusterResourceApiVersion = "mulitcluster.practice.com/v1alpha1" 14 | ) 15 | 16 | // SchemeGroupVersion is group version used to register these objects 17 | var SchemeGroupVersion = schema.GroupVersion{Group: MultiClusterResourceGroup, Version: MultiClusterResourceVersion} 18 | 19 | // Kind takes an unqualified kind and return back a Group qualified GroupKind 20 | func Kind(kind string) schema.GroupKind { 21 | return SchemeGroupVersion.WithKind(kind).GroupKind() 22 | } 23 | 24 | // GetResource takes an unqualified multiclusterresource and returns a Group qualified GroupResource 25 | func GetResource(resource string) schema.GroupResource { 26 | return SchemeGroupVersion.WithResource(resource).GroupResource() 27 | } 28 | 29 | var ( 30 | SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) 31 | AddToScheme = SchemeBuilder.AddToScheme 32 | ) 33 | 34 | // Adds the list of known types to Scheme. 35 | func addKnownTypes(scheme *runtime.Scheme) error { 36 | scheme.AddKnownTypes(SchemeGroupVersion, 37 | &MultiClusterResource{}, 38 | &MultiClusterResourceList{}, 39 | ) 40 | metav1.AddToGroupVersion(scheme, SchemeGroupVersion) 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /cmd/ctl_plugin/resource/list/cluster.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "fmt" 5 | "github.com/goccy/go-json" 6 | "github.com/myoperator/multiclusteroperator/cmd/ctl_plugin/common" 7 | "github.com/myoperator/multiclusteroperator/pkg/apis/multicluster/v1alpha1" 8 | "github.com/olekukonko/tablewriter" 9 | "log" 10 | "net/http" 11 | "os" 12 | ) 13 | 14 | type WrapCluster struct { 15 | Object *v1alpha1.MultiCluster `json:"Object"` 16 | ClusterName string `json:"clusterName"` 17 | } 18 | 19 | func Clusters(name string) error { 20 | m := map[string]string{} 21 | 22 | if name != "" { 23 | m["name"] = name 24 | } 25 | 26 | m["limit"] = "0" 27 | m["gvr"] = "mulitcluster.practice.com/v1alpha1/multiclusters" 28 | 29 | m["namespace"] = "default" 30 | 31 | rr := make([]*WrapCluster, 0) 32 | url := fmt.Sprintf("http://%v:%v/v1/list_with_cluster", common.ServerIp, common.ServerPort) 33 | r, err := common.HttpClient.DoRequest(http.MethodGet, url, m, nil, nil) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | err = json.Unmarshal(r, &rr) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | // 表格化呈现 44 | table := tablewriter.NewWriter(os.Stdout) 45 | content := []string{"Name", "VERSION", "HOST", "PLATFORM", "ISMASTER", "STATUS"} 46 | 47 | table.SetHeader(content) 48 | 49 | for _, cm := range rr { 50 | 51 | podRow := []string{cm.Object.Name, cm.Object.Spec.Version, cm.Object.Spec.Host, cm.Object.Spec.Platform, cm.Object.Spec.IsMaster, cm.Object.Status.Status} 52 | 53 | table.Append(podRow) 54 | } 55 | // 去掉表格线 56 | table = TableSet(table) 57 | 58 | table.Render() 59 | 60 | return nil 61 | 62 | } 63 | -------------------------------------------------------------------------------- /cmd/ctl_plugin/resource/list/resource.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "fmt" 5 | "github.com/goccy/go-json" 6 | "github.com/myoperator/multiclusteroperator/cmd/ctl_plugin/common" 7 | "github.com/olekukonko/tablewriter" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | "log" 10 | "net/http" 11 | "os" 12 | ) 13 | 14 | type WrapResource struct { 15 | Object *unstructured.Unstructured `json:"Object"` 16 | ClusterName string `json:"clusterName"` 17 | } 18 | 19 | func Resources(cluster, name, namespace, gvr string) error { 20 | 21 | m := map[string]string{} 22 | m["limit"] = "0" 23 | m["gvr"] = gvr 24 | if cluster != "" { 25 | m["cluster"] = cluster 26 | } 27 | 28 | if name != "" { 29 | m["name"] = name 30 | } 31 | 32 | if namespace != "" { 33 | m["namespace"] = namespace 34 | } 35 | 36 | rr := make([]*WrapResource, 0) 37 | url := fmt.Sprintf("http://%v:%v/v1/list_with_cluster", common.ServerIp, common.ServerPort) 38 | r, err := common.HttpClient.DoRequest(http.MethodGet, url, m, nil, nil) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | err = json.Unmarshal(r, &rr) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | // 表格化呈现 49 | table := tablewriter.NewWriter(os.Stdout) 50 | content := []string{"Cluster", "Name", "Namespace"} 51 | 52 | table.SetHeader(content) 53 | 54 | for _, pod := range rr { 55 | pod.Object.GetResourceVersion() 56 | podRow := []string{pod.ClusterName, pod.Object.GetName(), pod.Object.GetNamespace()} 57 | 58 | table.Append(podRow) 59 | } 60 | // 去掉表格线 61 | table = TableSet(table) 62 | 63 | table.Render() 64 | 65 | return nil 66 | 67 | } 68 | -------------------------------------------------------------------------------- /cmd/server/app/options/options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "flag" 5 | "github.com/myoperator/multiclusteroperator/pkg/options/mysql" 6 | "github.com/myoperator/multiclusteroperator/pkg/options/server" 7 | "github.com/pkg/errors" 8 | cliflag "k8s.io/component-base/cli/flag" 9 | "k8s.io/component-base/logs" 10 | ) 11 | 12 | type Options struct { 13 | Server *server.ServerOptions 14 | MySQL *mysql.MySQLOptions 15 | Logs *logs.Options 16 | } 17 | 18 | func NewOptions() *Options { 19 | return &Options{ 20 | Server: server.NewServerOptions(), 21 | MySQL: mysql.NewMySQLOptions(), 22 | Logs: logs.NewOptions(), 23 | } 24 | } 25 | 26 | func (o *Options) Flags() cliflag.NamedFlagSets { 27 | fss := cliflag.NamedFlagSets{} 28 | fss.FlagSet("generic").AddGoFlagSet(flag.CommandLine) 29 | 30 | logs.AddGoFlags(flag.CommandLine) 31 | 32 | // 入参解析 33 | o.Server.AddFlags(fss.FlagSet("server")) 34 | o.MySQL.AddFlags(fss.FlagSet("mysql")) 35 | return fss 36 | } 37 | 38 | // Complete 完成入参配置赋值 39 | func (o *Options) Complete() error { 40 | // TODO: 需要实现配置项赋值 41 | if err := o.Server.Complete(); err != nil { 42 | return err 43 | } 44 | if err := o.MySQL.Complete(); err != nil { 45 | return err 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func (o *Options) Validate() error { 52 | var errs []error 53 | 54 | errs = append(errs, o.Server.Validate()...) 55 | errs = append(errs, o.MySQL.Validate()...) 56 | 57 | if len(errs) == 0 { 58 | return nil 59 | } 60 | 61 | wrapped := errors.New("options validate error") 62 | for _, err := range errs { 63 | wrapped = errors.WithMessage(wrapped, err.Error()) 64 | } 65 | return wrapped 66 | } 67 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/internalinterfaces/factory_interfaces.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package internalinterfaces 20 | 21 | import ( 22 | time "time" 23 | 24 | versioned "github.com/myoperator/multiclusteroperator/pkg/client/clientset/versioned" 25 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | runtime "k8s.io/apimachinery/pkg/runtime" 27 | cache "k8s.io/client-go/tools/cache" 28 | ) 29 | 30 | // NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. 31 | type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer 32 | 33 | // SharedInformerFactory a small interface to allow for adding an informer without an import cycle 34 | type SharedInformerFactory interface { 35 | Start(stopCh <-chan struct{}) 36 | InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer 37 | } 38 | 39 | // TweakListOptionsFunc is a function that transforms a v1.ListOptions. 40 | type TweakListOptionsFunc func(*v1.ListOptions) 41 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/go-yaml/yaml" 5 | "github.com/pkg/errors" 6 | "k8s.io/client-go/rest" 7 | "k8s.io/klog/v2" 8 | "os" 9 | ) 10 | 11 | type Config struct { 12 | Clusters []Cluster `json:"clusters" yaml:"clusters"` 13 | } 14 | 15 | func NewConfig() *Config { 16 | return &Config{} 17 | } 18 | 19 | func loadConfigFile(path string) ([]byte, error) { 20 | b, err := os.ReadFile(path) 21 | if err != nil { 22 | klog.Error("read file error: ", err) 23 | return nil, err 24 | } 25 | return b, nil 26 | } 27 | 28 | func BuildConfig(path string) (*Config, error) { 29 | config := NewConfig() 30 | if b, err := loadConfigFile(path); b != nil { 31 | err := yaml.Unmarshal(b, config) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return config, err 36 | } else { 37 | return nil, errors.Wrap(err, "load config file error") 38 | } 39 | } 40 | 41 | // MetaData 集群对象所需的信息 42 | type MetaData struct { 43 | // ConfigPath kube config文件 44 | ConfigPath string `json:"configPath" yaml:"configPath"` 45 | // Insecure 是否跳过证书认证 46 | Insecure bool `json:"insecure" yaml:"insecure"` 47 | // ClusterName 集群名 48 | ClusterName string `json:"clusterName" yaml:"clusterName"` 49 | // IsMaster 是否为主集群 50 | IsMaster bool `json:"isMaster" yaml:"isMaster"` 51 | // Resources 监听的资源对象(用于多集群查询) 52 | Resources []Resource `json:"resources" yaml:"resources"` 53 | // RestConfig k8s 集群中 restConfig 对象, 54 | // 只在动态加入 join 时才赋值 55 | RestConfig *rest.Config 56 | } 57 | 58 | type Resource struct { 59 | // GVR 标示, ex: v1/pods apps/v1/deployments 60 | RType string `json:"rType" yaml:"rType"` 61 | } 62 | 63 | // Cluster 集群对象 64 | type Cluster struct { 65 | MetaData MetaData `json:"metadata" yaml:"metadata"` 66 | } 67 | -------------------------------------------------------------------------------- /cmd/ctl_plugin/resource/list/cmd.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var ( 9 | ListCmd *cobra.Command 10 | ) 11 | 12 | func init() { 13 | 14 | ListCmd = &cobra.Command{ 15 | Use: "list resource [flags]", 16 | Short: "list --name=xxx --clusterName=xxx --namespace=xxx" + 17 | " , input resource GVR, ex: core/v1/pods apps/v1/deployments, batch/v1/jobs", 18 | Example: "list core/v1/pods", 19 | RunE: func(c *cobra.Command, args []string) error { 20 | if len(args) == 0 { 21 | return fmt.Errorf("Please specify a resource pods, deployments, configmaps\n") 22 | } 23 | resource := args[0] 24 | 25 | cluster, err := c.Flags().GetString("clusterName") 26 | if err != nil { 27 | return err 28 | } 29 | 30 | ns, err := c.Flags().GetString("namespace") 31 | if err != nil { 32 | return err 33 | } 34 | 35 | name, err := c.Flags().GetString("name") 36 | if err != nil { 37 | return err 38 | } 39 | 40 | switch resource { 41 | case "pods": 42 | err = Pods(cluster, name, ns) 43 | case "v1.pods": 44 | err = Pods(cluster, name, ns) 45 | case "core.v1.pods": 46 | err = Pods(cluster, name, ns) 47 | case "apps.v1.deployments": 48 | err = Deployments(cluster, name, ns) 49 | case "deployments": 50 | err = Deployments(cluster, name, ns) 51 | case "core.v1.configmaps": 52 | err = Configmaps(cluster, name, ns) 53 | case "v1.configmaps": 54 | err = Configmaps(cluster, name, ns) 55 | case "configmaps": 56 | err = Configmaps(cluster, name, ns) 57 | case "clusters": 58 | err = Clusters(name) 59 | default: 60 | err = Resources(cluster, name, ns, resource) 61 | } 62 | return err 63 | }, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pkg/server/service/list_wrap.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/myoperator/multiclusteroperator/pkg/store/model" 5 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 6 | "k8s.io/apimachinery/pkg/runtime" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "k8s.io/klog/v2" 9 | ) 10 | 11 | type WrapWithCluster struct { 12 | runtime.Object 13 | ClusterName string `json:"clusterName"` 14 | } 15 | 16 | // ListWrapWithCluster 从数据库获取查询结果 17 | func (list *ListService) ListWrapWithCluster(name, namespace, cluster string, labels map[string]string, gvr schema.GroupVersionResource, 18 | limit int) ([]WrapWithCluster, error) { 19 | ret := make([]model.Resources, 0) 20 | 21 | // gvr 一定会传入 22 | db := list.DB.Model(&model.Resources{}). 23 | Where("`group`=?", gvr.Group). 24 | Where("version=?", gvr.Version). 25 | Where("resource=?", gvr.Resource) 26 | //Where("object->'$.metadata.labels.app'=?", "test") 27 | 28 | // 其他查询字段自由传入 29 | 30 | if cluster != "" { 31 | db = db.Where("cluster=?", cluster) 32 | } 33 | 34 | if name != "" { 35 | db = db.Where("name=?", name) 36 | } 37 | 38 | if namespace != "" { 39 | db = db.Where("namespace=?", namespace) 40 | } 41 | 42 | if limit != 0 { 43 | db = db.Limit(limit) 44 | } 45 | 46 | err := db.Order("create_at desc").Find(&ret).Error 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | objList := make([]WrapWithCluster, len(ret)) 52 | for i, res := range ret { 53 | obj := &unstructured.Unstructured{} 54 | if err = obj.UnmarshalJSON([]byte(res.Object)); err != nil { 55 | klog.Errorf("unmarshal json from db error: %s\n", err) 56 | } else { 57 | objList[i].Object = obj 58 | objList[i].ClusterName = res.Cluster 59 | } 60 | } 61 | 62 | return objList, err 63 | } 64 | -------------------------------------------------------------------------------- /cmd/ctl_plugin/resource/describe/cmd.go: -------------------------------------------------------------------------------- 1 | package describe 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var ( 9 | DescribeCmd *cobra.Command 10 | ) 11 | 12 | func init() { 13 | 14 | DescribeCmd = &cobra.Command{ 15 | Use: "describe resource [flags]", 16 | Short: "describe --name=xxx --clusterName=xxx --namespace=xxx" + 17 | " , input resource GVR, ex: core/v1/pods apps/v1/deployments, batch/v1/jobs", 18 | Example: "describe core/v1/pods", 19 | RunE: func(c *cobra.Command, args []string) error { 20 | if len(args) == 0 { 21 | return fmt.Errorf("Please specify a resource pods, deployments, configmaps\n") 22 | } 23 | resource := args[0] 24 | 25 | cluster, err := c.Flags().GetString("clusterName") 26 | if err != nil { 27 | return err 28 | } 29 | 30 | ns, err := c.Flags().GetString("namespace") 31 | if err != nil { 32 | return err 33 | } 34 | 35 | name, err := c.Flags().GetString("name") 36 | if err != nil { 37 | return err 38 | } 39 | 40 | switch resource { 41 | case "pods": 42 | err = Pods(cluster, name, ns) 43 | case "v1.pods": 44 | err = Pods(cluster, name, ns) 45 | case "core.v1.pods": 46 | err = Pods(cluster, name, ns) 47 | case "apps.v1.deployments": 48 | err = Deployments(cluster, name, ns) 49 | case "deployments": 50 | err = Deployments(cluster, name, ns) 51 | case "core.v1.configmaps": 52 | err = Configmaps(cluster, name, ns) 53 | case "v1.configmaps": 54 | err = Configmaps(cluster, name, ns) 55 | case "configmaps": 56 | err = Configmaps(cluster, name, ns) 57 | default: 58 | //return fmt.Errorf("Unsupport resource: %s\n", resource) 59 | err = Resource(cluster, name, ns, resource) 60 | } 61 | return err 62 | }, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /pkg/multi_cluster_controller/cluster_controller.go: -------------------------------------------------------------------------------- 1 | package multi_cluster_controller 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/myoperator/multiclusteroperator/pkg/apis/multicluster/v1alpha1" 7 | corev1 "k8s.io/api/core/v1" 8 | "k8s.io/apimachinery/pkg/api/errors" 9 | "k8s.io/client-go/tools/record" 10 | "k8s.io/klog/v2" 11 | "sigs.k8s.io/controller-runtime/pkg/client" 12 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 13 | ) 14 | 15 | type ClusterHandler struct { 16 | // operator 控制器 client 17 | client.Client 18 | // 事件发送器 19 | EventRecorder record.EventRecorder 20 | } 21 | 22 | func NewClusterHandler(client client.Client, eventRecorder record.EventRecorder) *ClusterHandler { 23 | return &ClusterHandler{Client: client, EventRecorder: eventRecorder} 24 | } 25 | 26 | func (ch *ClusterHandler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { 27 | // 获取 MultiCluster 资源对象 28 | rr := &v1alpha1.MultiCluster{} 29 | err := ch.Get(ctx, req.NamespacedName, rr) 30 | if err != nil { 31 | if errors.IsNotFound(err) { 32 | return reconcile.Result{}, nil 33 | } 34 | return reconcile.Result{}, err 35 | } 36 | 37 | if rr.Status.Status == "Healthy" { 38 | return reconcile.Result{}, nil 39 | } 40 | 41 | if !rr.DeletionTimestamp.IsZero() { 42 | klog.Infof("successful delete cluster %v\n", rr.Name) 43 | return reconcile.Result{}, nil 44 | } 45 | 46 | // 修改资源状态为 Healthy 47 | rr.Status.Status = "Healthy" 48 | 49 | err = ch.Client.Status().Update(ctx, rr) 50 | if err != nil { 51 | return reconcile.Result{}, err 52 | } 53 | 54 | ch.EventRecorder.Eventf(rr, corev1.EventTypeNormal, "Create", fmt.Sprintf("create %s cluster success", rr.Spec.Name)) 55 | 56 | klog.Infof("successful reconcile...") 57 | return reconcile.Result{}, nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/multicluster/v1alpha1/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | internalinterfaces "github.com/myoperator/multiclusteroperator/pkg/client/informers/externalversions/internalinterfaces" 23 | ) 24 | 25 | // Interface provides access to all the informers in this group version. 26 | type Interface interface { 27 | // MultiClusters returns a MultiClusterInformer. 28 | MultiClusters() MultiClusterInformer 29 | } 30 | 31 | type version struct { 32 | factory internalinterfaces.SharedInformerFactory 33 | namespace string 34 | tweakListOptions internalinterfaces.TweakListOptionsFunc 35 | } 36 | 37 | // New returns a new Interface. 38 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 39 | return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 40 | } 41 | 42 | // MultiClusters returns a MultiClusterInformer. 43 | func (v *version) MultiClusters() MultiClusterInformer { 44 | return &multiClusterInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} 45 | } 46 | -------------------------------------------------------------------------------- /pkg/server/service/list.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/myoperator/multiclusteroperator/pkg/store/model" 5 | "gorm.io/gorm" 6 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | "k8s.io/klog/v2" 10 | ) 11 | 12 | type ListService struct { 13 | DB *gorm.DB 14 | } 15 | 16 | // List 从数据库获取查询结果 17 | func (list *ListService) List(name, namespace, cluster string, labels map[string]string, gvr schema.GroupVersionResource, 18 | limit int) ([]runtime.Object, error) { 19 | ret := make([]model.Resources, 0) 20 | 21 | // gvr 一定会传入 22 | db := list.DB.Model(&model.Resources{}). 23 | Where("`group`=?", gvr.Group). 24 | Where("version=?", gvr.Version). 25 | Where("resource=?", gvr.Resource) 26 | //Where("object->'$.metadata.labels.app'=?", "test") 27 | 28 | // 其他查询字段自由传入 29 | 30 | if cluster != "" { 31 | db = db.Where("cluster=?", cluster) 32 | } 33 | 34 | if name != "" { 35 | db = db.Where("name=?", name) 36 | } 37 | 38 | if namespace != "" { 39 | db = db.Where("namespace=?", namespace) 40 | } 41 | 42 | // FIXME: labels支持有问题 43 | //if labels != nil { 44 | // for k, v := range labels { 45 | // db = db.Where(fmt.Sprintf("object->'$.metadata.labels.%s'=?", k), v) 46 | // } 47 | //} 48 | 49 | if limit != 0 { 50 | db = db.Limit(limit) 51 | } 52 | 53 | err := db.Order("create_at desc").Find(&ret).Error 54 | if err != nil { 55 | return nil, err 56 | } 57 | // 列出 runtime.Object 58 | objList := make([]runtime.Object, len(ret)) 59 | for i, res := range ret { 60 | obj := &unstructured.Unstructured{} 61 | if err = obj.UnmarshalJSON([]byte(res.Object)); err != nil { 62 | klog.Errorf("unmarshal json from db error: %s\n", err) 63 | } else { 64 | objList[i] = obj 65 | } 66 | } 67 | 68 | return objList, err 69 | } 70 | -------------------------------------------------------------------------------- /yaml/multi_cluster_resource_pod_example.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mulitcluster.practice.com/v1alpha1 2 | kind: MultiClusterResource 3 | metadata: 4 | name: mypod.pod 5 | namespace: default 6 | spec: 7 | template: 8 | apiVersion: v1 9 | kind: Pod 10 | metadata: 11 | name: multicluster-pod 12 | namespace: default 13 | spec: 14 | containers: 15 | - image: busybox 16 | command: 17 | - sleep 18 | - "3600" 19 | imagePullPolicy: IfNotPresent 20 | name: busybox 21 | restartPolicy: Always 22 | placement: 23 | clusters: 24 | - name: tencent1 25 | - name: tencent2 26 | - name: tencent4 27 | # 可以不填写 28 | customize: 29 | clusters: 30 | - name: tencent1 31 | action: 32 | - path: "/spec/containers/0/image" 33 | op: "replace" 34 | value: 35 | - "nginx:1.19.0-alpine" 36 | - name: tencent2 37 | action: 38 | - path: "/metadata/annotations/example" 39 | op: "add" 40 | value: 41 | - "example" 42 | - path: "/spec/containers/0/image" 43 | op: "replace" 44 | value: 45 | - "nginx:1.19.0-alpine" 46 | - name: tencent4 47 | action: 48 | - path: "/spec/containers/0/image" 49 | op: "replace" 50 | value: 51 | - "nginx:1.17.0-alpine" 52 | 53 | # pod patch 操作默认不能新增容器,所以如下操作不行 54 | # - path: "/spec/containers/-" 55 | # value: 56 | # - "name=busybox11" 57 | # - "image=nginx:1.19.0-alpine" 58 | # op: "add" 59 | 60 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/multicluster/interface.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package multicluster 20 | 21 | import ( 22 | internalinterfaces "github.com/myoperator/multiclusteroperator/pkg/client/informers/externalversions/internalinterfaces" 23 | v1alpha1 "github.com/myoperator/multiclusteroperator/pkg/client/informers/externalversions/multicluster/v1alpha1" 24 | ) 25 | 26 | // Interface provides access to each of this group's versions. 27 | type Interface interface { 28 | // V1alpha1 provides access to shared informers for resources in V1alpha1. 29 | V1alpha1() v1alpha1.Interface 30 | } 31 | 32 | type group struct { 33 | factory internalinterfaces.SharedInformerFactory 34 | namespace string 35 | tweakListOptions internalinterfaces.TweakListOptionsFunc 36 | } 37 | 38 | // New returns a new Interface. 39 | func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { 40 | return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} 41 | } 42 | 43 | // V1alpha1 returns a new v1alpha1.Interface. 44 | func (g *group) V1alpha1() v1alpha1.Interface { 45 | return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/config/init_db.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | _ "github.com/go-sql-driver/mysql" 7 | "github.com/golang-migrate/migrate/v4" 8 | "github.com/golang-migrate/migrate/v4/database/mysql" 9 | _ "github.com/golang-migrate/migrate/v4/source/file" 10 | "github.com/myoperator/multiclusteroperator/pkg/util" 11 | mysql1 "gorm.io/driver/mysql" 12 | "gorm.io/gorm" 13 | "k8s.io/klog/v2" 14 | "log" 15 | "time" 16 | ) 17 | 18 | type DbConfig struct { 19 | User string 20 | Password string 21 | Endpoint string 22 | Database string 23 | } 24 | 25 | func (dbc *DbConfig) InitDBOrDie() *gorm.DB { 26 | 27 | dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", 28 | dbc.User, dbc.Password, dbc.Endpoint, dbc.Database) 29 | gormdb, err := gorm.Open(mysql1.Open(dsn), &gorm.Config{}) 30 | if err != nil { 31 | klog.Fatalln(err) 32 | } 33 | db, err := gormdb.DB() 34 | if err != nil { 35 | klog.Fatalln(err) 36 | } 37 | db.SetConnMaxLifetime(time.Minute * 10) 38 | db.SetMaxIdleConns(10) 39 | db.SetMaxOpenConns(20) 40 | 41 | // 执行 migrate 42 | db1, _ := sql.Open("mysql", dsn) 43 | // 关闭数据库连接 44 | defer db1.Close() 45 | driver, err := mysql.WithInstance(db1, &mysql.Config{}) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | 50 | m, err := migrate.NewWithDatabaseInstance( 51 | fmt.Sprintf("file://%s/migrations", util.GetWd()), 52 | "mysql", 53 | driver, 54 | ) 55 | if err != nil { 56 | klog.Fatal(err) 57 | } 58 | 59 | // 执行数据库迁移 60 | err = m.Up() 61 | if err != nil && err != migrate.ErrNoChange { 62 | klog.Fatalf("Failed to apply migrations: %v", err) 63 | } 64 | 65 | // 获取当前数据库迁移版本 66 | version, _, err := m.Version() 67 | if err != nil && err != migrate.ErrNilVersion { 68 | klog.Fatalf("Failed to get migration version: %v", err) 69 | } 70 | 71 | klog.Infof("Applied migrations up to version: %v\n", version) 72 | 73 | return gormdb 74 | } 75 | -------------------------------------------------------------------------------- /cmd/ctl_plugin/resource/list/configmap.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "fmt" 5 | "github.com/goccy/go-json" 6 | "github.com/myoperator/multiclusteroperator/cmd/ctl_plugin/common" 7 | "github.com/olekukonko/tablewriter" 8 | v1 "k8s.io/api/core/v1" 9 | "log" 10 | "net/http" 11 | "os" 12 | "strconv" 13 | ) 14 | 15 | type WrapConfigMap struct { 16 | Object *v1.ConfigMap `json:"Object"` 17 | ClusterName string `json:"clusterName"` 18 | } 19 | 20 | func Configmaps(cluster, name, namespace string) error { 21 | 22 | m := map[string]string{} 23 | m["limit"] = "0" 24 | m["gvr"] = "v1/configmaps" 25 | if cluster != "" { 26 | m["cluster"] = cluster 27 | } 28 | 29 | if name != "" { 30 | m["name"] = name 31 | } 32 | 33 | if namespace != "" { 34 | m["namespace"] = namespace 35 | } 36 | 37 | rr := make([]*WrapConfigMap, 0) 38 | url := fmt.Sprintf("http://%v:%v/v1/list_with_cluster", common.ServerIp, common.ServerPort) 39 | r, err := common.HttpClient.DoRequest(http.MethodGet, url, m, nil, nil) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | 44 | err = json.Unmarshal(r, &rr) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | // 表格化呈现 50 | table := tablewriter.NewWriter(os.Stdout) 51 | content := []string{"Cluster", "Name", "Namespace", "DATA"} 52 | 53 | //if common.ShowLabels { 54 | // content = append(content, "标签") 55 | //} 56 | //if common.ShowAnnotations { 57 | // content = append(content, "Annotations") 58 | //} 59 | 60 | table.SetHeader(content) 61 | 62 | for _, cm := range rr { 63 | 64 | podRow := []string{cm.ClusterName, cm.Object.Name, cm.Object.Namespace, strconv.Itoa(len(cm.Object.Data))} 65 | //podRow := []string{pod.Name, pod.Namespace, pod.Status.PodIP, string(pod.Status.Phase)} 66 | 67 | //if common.ShowLabels { 68 | // podRow = append(podRow, common.LabelsMapToString(pod.Labels)) 69 | //} 70 | //if common.ShowAnnotations { 71 | // podRow = append(podRow, common.AnnotationsMapToString(pod.Annotations)) 72 | //} 73 | 74 | table.Append(podRow) 75 | } 76 | // 去掉表格线 77 | table = TableSet(table) 78 | 79 | table.Render() 80 | 81 | return nil 82 | 83 | } 84 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | mulitclusterv1alpha1 "github.com/myoperator/multiclusteroperator/pkg/apis/multicluster/v1alpha1" 23 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | schema "k8s.io/apimachinery/pkg/runtime/schema" 26 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 27 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 28 | ) 29 | 30 | var scheme = runtime.NewScheme() 31 | var codecs = serializer.NewCodecFactory(scheme) 32 | 33 | var localSchemeBuilder = runtime.SchemeBuilder{ 34 | mulitclusterv1alpha1.AddToScheme, 35 | } 36 | 37 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 38 | // of clientsets, like in: 39 | // 40 | // import ( 41 | // "k8s.io/client-go/kubernetes" 42 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 43 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 44 | // ) 45 | // 46 | // kclientset, _ := kubernetes.NewForConfig(c) 47 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 48 | // 49 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 50 | // correctly. 51 | var AddToScheme = localSchemeBuilder.AddToScheme 52 | 53 | func init() { 54 | v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) 55 | utilruntime.Must(AddToScheme(scheme)) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/caches/workqueue/workqueue.go: -------------------------------------------------------------------------------- 1 | package workqueue 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "k8s.io/apimachinery/pkg/runtime" 6 | "k8s.io/client-go/util/workqueue" 7 | ) 8 | 9 | type EventType string 10 | 11 | var ( 12 | AddEvent EventType = "add" 13 | UpdateEvent EventType = "update" 14 | DeleteEvent EventType = "delete" 15 | ) 16 | 17 | type QueueResource struct { 18 | Object runtime.Object 19 | EventType EventType 20 | } 21 | 22 | // Queue 接口对象 23 | type Queue interface { 24 | // Push 将监听到的资源放入queue中 25 | Push(resources *QueueResource) 26 | // Pop 拿出队列 27 | Pop() (*QueueResource, error) 28 | // ReQueue 重新放入队列,次数可配置 29 | ReQueue(*QueueResource) error 30 | // Finish 完成入列操作 31 | Finish(*QueueResource) 32 | // Close 关闭所有informer 33 | Close() 34 | // SetReMaxReQueueTime 设置最大重新入列次数 35 | SetReMaxReQueueTime(int) 36 | } 37 | 38 | // wq 使用限速队列实现 Queue 接口 39 | type wq struct { 40 | workqueue.RateLimitingInterface 41 | MaxReQueueTime int 42 | } 43 | 44 | var _ Queue = &wq{} 45 | 46 | func NewWorkQueue(maxReQueueTime int) *wq { 47 | return &wq{ 48 | workqueue.NewRateLimitingQueue(workqueue.DefaultItemBasedRateLimiter()), 49 | maxReQueueTime, 50 | } 51 | } 52 | 53 | // Push 放入队列 54 | func (c *wq) Push(obj *QueueResource) { 55 | c.AddRateLimited(obj) 56 | } 57 | 58 | // Pop 取出队列 59 | func (c *wq) Pop() (*QueueResource, error) { 60 | obj, quit := c.Get() 61 | 62 | if quit { 63 | return &QueueResource{}, errors.New("Controller has been stopped.") 64 | } 65 | return obj.(*QueueResource), nil 66 | } 67 | 68 | // Finish 结束要干两件事,忘记+done 69 | func (c *wq) Finish(obj *QueueResource) { 70 | c.Forget(obj) 71 | c.Done(obj) 72 | } 73 | 74 | // ReQueue 重新放入 75 | func (c *wq) ReQueue(obj *QueueResource) error { 76 | if c.NumRequeues(obj) < c.MaxReQueueTime { 77 | // 这里会重新放入对列 78 | c.AddRateLimited(obj) 79 | return nil 80 | } 81 | // 如果次数大于最大重试次数,直接丢弃 82 | c.Forget(obj) 83 | c.Done(obj) 84 | return errors.New("This object has been requeued for many times, but still fails.") 85 | } 86 | 87 | func (c *wq) Close() { 88 | c.ShutDown() 89 | } 90 | 91 | func (c *wq) SetReMaxReQueueTime(maxReQueueTime int) { 92 | c.MaxReQueueTime = maxReQueueTime 93 | } 94 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/scheme/register.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package scheme 20 | 21 | import ( 22 | mulitclusterv1alpha1 "github.com/myoperator/multiclusteroperator/pkg/apis/multicluster/v1alpha1" 23 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | runtime "k8s.io/apimachinery/pkg/runtime" 25 | schema "k8s.io/apimachinery/pkg/runtime/schema" 26 | serializer "k8s.io/apimachinery/pkg/runtime/serializer" 27 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 28 | ) 29 | 30 | var Scheme = runtime.NewScheme() 31 | var Codecs = serializer.NewCodecFactory(Scheme) 32 | var ParameterCodec = runtime.NewParameterCodec(Scheme) 33 | var localSchemeBuilder = runtime.SchemeBuilder{ 34 | mulitclusterv1alpha1.AddToScheme, 35 | } 36 | 37 | // AddToScheme adds all types of this clientset into the given scheme. This allows composition 38 | // of clientsets, like in: 39 | // 40 | // import ( 41 | // "k8s.io/client-go/kubernetes" 42 | // clientsetscheme "k8s.io/client-go/kubernetes/scheme" 43 | // aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" 44 | // ) 45 | // 46 | // kclientset, _ := kubernetes.NewForConfig(c) 47 | // _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) 48 | // 49 | // After this, RawExtensions in Kubernetes types will serialize kube-aggregator types 50 | // correctly. 51 | var AddToScheme = localSchemeBuilder.AddToScheme 52 | 53 | func init() { 54 | v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) 55 | utilruntime.Must(AddToScheme(Scheme)) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/generic.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package externalversions 20 | 21 | import ( 22 | "fmt" 23 | 24 | v1alpha1 "github.com/myoperator/multiclusteroperator/pkg/apis/multicluster/v1alpha1" 25 | schema "k8s.io/apimachinery/pkg/runtime/schema" 26 | cache "k8s.io/client-go/tools/cache" 27 | ) 28 | 29 | // GenericInformer is type of SharedIndexInformer which will locate and delegate to other 30 | // sharedInformers based on type 31 | type GenericInformer interface { 32 | Informer() cache.SharedIndexInformer 33 | Lister() cache.GenericLister 34 | } 35 | 36 | type genericInformer struct { 37 | informer cache.SharedIndexInformer 38 | resource schema.GroupResource 39 | } 40 | 41 | // Informer returns the SharedIndexInformer. 42 | func (f *genericInformer) Informer() cache.SharedIndexInformer { 43 | return f.informer 44 | } 45 | 46 | // Lister returns the GenericLister. 47 | func (f *genericInformer) Lister() cache.GenericLister { 48 | return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) 49 | } 50 | 51 | // ForResource gives generic access to a shared informer of the matching type 52 | // TODO extend this to unknown resources with a client pool 53 | func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { 54 | switch resource { 55 | // Group=mulitcluster.practice.com, Version=v1alpha1 56 | case v1alpha1.SchemeGroupVersion.WithResource("multiclusters"): 57 | return &genericInformer{resource: resource.GroupResource(), informer: f.Mulitcluster().V1alpha1().MultiClusters().Informer()}, nil 58 | 59 | } 60 | 61 | return nil, fmt.Errorf("no informer found for %v", resource) 62 | } 63 | -------------------------------------------------------------------------------- /cmd/ctl_plugin/resource/list/deployment.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "fmt" 5 | "github.com/goccy/go-json" 6 | "github.com/myoperator/multiclusteroperator/cmd/ctl_plugin/common" 7 | "github.com/olekukonko/tablewriter" 8 | appsv1 "k8s.io/api/apps/v1" 9 | "log" 10 | "net/http" 11 | "os" 12 | "strconv" 13 | ) 14 | 15 | type WrapDeployment struct { 16 | Object *appsv1.Deployment `json:"Object"` 17 | ClusterName string `json:"clusterName"` 18 | } 19 | 20 | func Deployments(cluster, name, namespace string) error { 21 | 22 | m := map[string]string{} 23 | m["limit"] = "0" 24 | m["gvr"] = "apps.v1.deployments" 25 | if cluster != "" { 26 | m["cluster"] = cluster 27 | } 28 | 29 | if name != "" { 30 | m["name"] = name 31 | } 32 | 33 | if namespace != "" { 34 | m["namespace"] = namespace 35 | } 36 | 37 | rr := make([]*WrapDeployment, 0) 38 | url := fmt.Sprintf("http://%v:%v/v1/list_with_cluster", common.ServerIp, common.ServerPort) 39 | r, err := common.HttpClient.DoRequest(http.MethodGet, url, m, nil, nil) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | 44 | err = json.Unmarshal(r, &rr) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | // 表格化呈现 50 | table := tablewriter.NewWriter(os.Stdout) 51 | content := []string{"Cluster", "Name", "Namespace", "TOTAL", "Available", "Ready"} 52 | 53 | //if common.ShowLabels { 54 | // content = append(content, "标签") 55 | //} 56 | //if common.ShowAnnotations { 57 | // content = append(content, "Annotations") 58 | //} 59 | 60 | table.SetHeader(content) 61 | 62 | for _, deployment := range rr { 63 | deploymentRow := []string{deployment.ClusterName, deployment.Object.Name, deployment.Object.Namespace, strconv.Itoa(int(deployment.Object.Status.Replicas)), strconv.Itoa(int(deployment.Object.Status.AvailableReplicas)), strconv.Itoa(int(deployment.Object.Status.ReadyReplicas))} 64 | //podRow := []string{pod.Name, pod.Namespace, pod.Status.PodIP, string(pod.Status.Phase)} 65 | 66 | //if common.ShowLabels { 67 | // podRow = append(podRow, common.LabelsMapToString(pod.Labels)) 68 | //} 69 | //if common.ShowAnnotations { 70 | // podRow = append(podRow, common.AnnotationsMapToString(pod.Annotations)) 71 | //} 72 | 73 | table.Append(deploymentRow) 74 | } 75 | // 去掉表格线 76 | table = TableSet(table) 77 | 78 | table.Render() 79 | 80 | return nil 81 | 82 | } 83 | -------------------------------------------------------------------------------- /pkg/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "k8s.io/apimachinery/pkg/api/meta" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // ParseIntoGvr 解析并指定资源对象GVR,http server 接口使用 "/" 作为分割符 12 | // ex: "apps/v1/deployments" "core/v1/pods" "batch/v1/jobs" 13 | func ParseIntoGvr(gvr, splitString string) (schema.GroupVersionResource, error) { 14 | var group, version, resource string 15 | gvList := strings.Split(gvr, splitString) 16 | 17 | // 防止越界 18 | if len(gvList) < 2 { 19 | return schema.GroupVersionResource{}, fmt.Errorf("gvr input error, please input like format apps/v1/deployments or core/v1/multiclusterresource") 20 | } 21 | 22 | if len(gvList) == 2 { 23 | group = "" 24 | version = gvList[0] 25 | resource = gvList[1] 26 | } else { 27 | if gvList[0] == "core" { 28 | gvList[0] = "" 29 | } 30 | group, version, resource = gvList[0], gvList[1], gvList[2] 31 | } 32 | 33 | return schema.GroupVersionResource{ 34 | Group: group, Version: version, Resource: resource, 35 | }, nil 36 | } 37 | 38 | // IsNameSpaceScope 是否 namespace 资源 39 | func IsNameSpaceScope(restMapper meta.RESTMapper, gvr schema.GroupVersionResource) bool { 40 | gvk, err := restMapper.KindFor(gvr) 41 | if err != nil { 42 | panic(err) 43 | } 44 | mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvr.Version) 45 | if err != nil { 46 | panic(err) 47 | } 48 | return string(mapping.Scope.Name()) == "namespace" 49 | } 50 | 51 | // contains 是否包含 52 | func contains(slice []string, item string) bool { 53 | for _, s := range slice { 54 | if s == item { 55 | return true 56 | } 57 | } 58 | return false 59 | } 60 | 61 | func GetDiffString(old []string, new []string) []string { 62 | // 待删除list 63 | forDelete := make([]string, 0) 64 | for _, item := range old { 65 | // 如果不包含,代表需要删除 66 | if !contains(new, item) { 67 | forDelete = append(forDelete, item) 68 | } 69 | } 70 | return forDelete 71 | } 72 | 73 | // RemoveItem 74 | // ex: [a,b,c,d,e,f,g] ===> [a,b,d,e,f,g] 75 | func RemoveItem(list []string, item string) []string { 76 | for i := 0; i < len(list); i++ { 77 | if list[i] == item { 78 | list = append(list[:i], list[i+1:]...) 79 | i-- 80 | } 81 | } 82 | return list 83 | } 84 | 85 | // GetWd 获取工作目录 86 | func GetWd() string { 87 | wd := os.Getenv("WORK_DIR") 88 | if wd == "" { 89 | wd, _ = os.Getwd() 90 | } 91 | return wd 92 | } 93 | -------------------------------------------------------------------------------- /pkg/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "github.com/myoperator/multiclusteroperator/pkg/multi_cluster_controller" 8 | "github.com/myoperator/multiclusteroperator/pkg/server/middleware" 9 | "github.com/myoperator/multiclusteroperator/pkg/server/service" 10 | "github.com/myoperator/multiclusteroperator/pkg/store" 11 | "gorm.io/gorm" 12 | "k8s.io/klog/v2" 13 | "net/http" 14 | "net/http/pprof" 15 | 16 | "github.com/gin-gonic/gin" 17 | ) 18 | 19 | var ( 20 | RR *ResourceController 21 | ) 22 | 23 | type Server struct { 24 | factory store.Factory 25 | httpSrv *http.Server 26 | } 27 | 28 | func NewServer(addr int, tls *tls.Config) *Server { 29 | s := &http.Server{ 30 | Addr: fmt.Sprintf(":%v", addr), 31 | } 32 | klog.Infof("http server port: %v\n", addr) 33 | if tls != nil { 34 | s.TLSConfig = tls 35 | } 36 | 37 | return &Server{ 38 | httpSrv: s, 39 | } 40 | } 41 | 42 | func (s *Server) InjectStoreFactory(factory store.Factory) { 43 | s.factory = factory 44 | } 45 | 46 | func (s *Server) Start(db *gorm.DB) error { 47 | // route 48 | s.httpSrv.Handler = s.router(db) 49 | 50 | if s.httpSrv.TLSConfig == nil { 51 | return s.httpSrv.ListenAndServe() 52 | } 53 | return s.httpSrv.ListenAndServeTLS("", "") 54 | } 55 | 56 | func (s *Server) Stop() { 57 | if s.httpSrv != nil { 58 | s.httpSrv.Shutdown(context.TODO()) 59 | } 60 | } 61 | 62 | func (s *Server) router(db *gorm.DB) http.Handler { 63 | r := gin.New() 64 | r.Use(middleware.LogMiddleware(), gin.Recovery()) 65 | 66 | r.NoRoute(func(c *gin.Context) { 67 | c.JSON(http.StatusNotFound, gin.H{"message": "Not Found handler"}) 68 | }) 69 | 70 | RR = &ResourceController{ 71 | ListService: &service.ListService{ 72 | DB: db, 73 | }, 74 | JoinService: &service.JoinService{ 75 | Mch: multi_cluster_controller.GlobalMultiClusterHandler, 76 | }, 77 | } 78 | 79 | // pprof 80 | { 81 | r.GET("/debug/pprof/", gin.WrapF(pprof.Index)) 82 | r.GET("/debug/pprof/cmdline", gin.WrapF(pprof.Cmdline)) 83 | r.GET("/debug/pprof/profile", gin.WrapF(pprof.Profile)) 84 | r.GET("/debug/pprof/symbol", gin.WrapF(pprof.Symbol)) 85 | r.GET("/debug/pprof/trace", gin.WrapF(pprof.Trace)) 86 | } 87 | 88 | v1 := r.Group("/v1") 89 | { 90 | v1.GET("/list", RR.List) 91 | v1.GET("/list_with_cluster", RR.ListWrapWithCluster) 92 | v1.POST("/join", RR.Join) 93 | v1.DELETE("/unjoin", RR.UnJoin) 94 | } 95 | 96 | return r 97 | } 98 | -------------------------------------------------------------------------------- /helm/README.md: -------------------------------------------------------------------------------- 1 | ## helm 部署 2 | 3 | ### 修改配置文件 4 | 用户需要**自行修改** [配置文件](./values.yaml)。 5 | - base: 基础配置,镜像需要自行构建 (docker build...) 6 | - rbac: 用于创建 rbac 使用 7 | - db: 数据库配置 8 | - service: service 配置 9 | - MultiClusterConfiguration: 多集群中 每个集群的 kubeconfig 文件需要挂载到 pod 中 10 | 11 | 注:如果部署有任何问题,欢迎提 issue 或 直接联系 12 | ```yaml 13 | # 基础配置 14 | base: 15 | # 副本数 16 | replicaCount: 1 17 | # 应用名或 namespace 18 | name: multi-cluster-operator 19 | namespace: default 20 | serviceName: multi-cluster-operator-svc 21 | # 镜像名 22 | image: multi-cluster-operator:v1 23 | # 多集群配置文件目录 (注意:在容器中的位置) 24 | config: /app/file/config.yaml 25 | # 是否指定调度到某个节点,如果不需要则不填 26 | nodeName: vm-0-16-centos 27 | 28 | 29 | # 用于创建 rbac 使用 30 | rbac: 31 | serviceaccountname: multi-cluster-operator-sa 32 | namespace: default 33 | clusterrole: multi-cluster-operator-clusterrole 34 | clusterrolebinding: multi-cluster-operator-ClusterRoleBinding 35 | 36 | # db 配置 37 | db: 38 | dbuser: root 39 | dbpassword: 123456 40 | # 注意:必须容器网络可达 41 | dbendpoint: 10.0.0.16:30110 42 | dbdatabase: resources 43 | debugmode: false 44 | 45 | # service 配置 46 | service: 47 | type: NodePort 48 | ports: 49 | - port: 8888 # 容器端口 50 | nodePort: 31888 # 对外暴露的端口 51 | name: server 52 | - port: 29999 # 健康检查端口 53 | nodePort: 31889 # 对外暴露的端口 54 | name: health 55 | 56 | # 多集群中 每个集群的 kubeconfig 文件需要挂载到 pod 中 57 | MultiClusterConfiguration: 58 | volumeMounts: 59 | # 挂载不同集群的 kubeconfig,请自行修改 60 | - name: tencent1 61 | mountPath: /app/file/config-tencent1 62 | - name: tencent2 63 | mountPath: /app/file/config-tencent2 64 | - name: tencent4 65 | mountPath: /app/file/config-tencent4 66 | # 配置文件 config.yaml 67 | - name: config 68 | mountPath: /app/file/config.yaml 69 | - name: migrate 70 | mountPath: /app/migrations 71 | # 节点上的位置 72 | volumes: 73 | - name: tencent1 74 | hostPath: 75 | path: /root/multi_resource_operator/resources/config-tencent1 76 | - name: tencent2 77 | hostPath: 78 | path: /root/multi_resource_operator/resources/config-tencent2 79 | - name: tencent4 80 | hostPath: 81 | path: /root/multi_resource_operator/resources/config-tencent4 82 | - name: config 83 | hostPath: 84 | path: /root/multi_resource_operator/config.yaml 85 | - name: migrate 86 | hostPath: 87 | path: /root/multi_resource_operator/migrations 88 | ``` 89 | -------------------------------------------------------------------------------- /cmd/ctl_plugin/resource/list/pod.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "fmt" 5 | "github.com/goccy/go-json" 6 | "github.com/myoperator/multiclusteroperator/cmd/ctl_plugin/common" 7 | "github.com/olekukonko/tablewriter" 8 | v1 "k8s.io/api/core/v1" 9 | "log" 10 | "net/http" 11 | "os" 12 | ) 13 | 14 | type WrapPod struct { 15 | Object *v1.Pod `json:"Object"` 16 | ClusterName string `json:"clusterName"` 17 | } 18 | 19 | func Pods(cluster, name, namespace string) error { 20 | 21 | m := map[string]string{} 22 | m["limit"] = "0" 23 | m["gvr"] = "v1/pods" 24 | if cluster != "" { 25 | m["cluster"] = cluster 26 | } 27 | 28 | if name != "" { 29 | m["name"] = name 30 | } 31 | 32 | if namespace != "" { 33 | m["namespace"] = namespace 34 | } 35 | 36 | rr := make([]*WrapPod, 0) 37 | url := fmt.Sprintf("http://%v:%v/v1/list_with_cluster", common.ServerIp, common.ServerPort) 38 | r, err := common.HttpClient.DoRequest(http.MethodGet, url, m, nil, nil) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | err = json.Unmarshal(r, &rr) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | // 表格化呈现 49 | table := tablewriter.NewWriter(os.Stdout) 50 | content := []string{"集群名称", "Name", "Namespace", "NODE", "POD IP", "状态", "容器名", "容器镜像"} 51 | 52 | //if common.ShowLabels { 53 | // content = append(content, "标签") 54 | //} 55 | //if common.ShowAnnotations { 56 | // content = append(content, "Annotations") 57 | //} 58 | 59 | table.SetHeader(content) 60 | 61 | for _, pod := range rr { 62 | podRow := []string{pod.ClusterName, pod.Object.Name, pod.Object.Namespace, pod.Object.Spec.NodeName, pod.Object.Status.PodIP, string(pod.Object.Status.Phase), pod.Object.Spec.Containers[0].Name, pod.Object.Spec.Containers[0].Image} 63 | 64 | //if common.ShowLabels { 65 | // podRow = append(podRow, common.LabelsMapToString(pod.Labels)) 66 | //} 67 | //if common.ShowAnnotations { 68 | // podRow = append(podRow, common.AnnotationsMapToString(pod.Annotations)) 69 | //} 70 | 71 | table.Append(podRow) 72 | } 73 | // 去掉表格线 74 | table = TableSet(table) 75 | 76 | table.Render() 77 | 78 | return nil 79 | 80 | } 81 | 82 | func TableSet(table *tablewriter.Table) *tablewriter.Table { 83 | // 去掉表格线 84 | table.SetAutoWrapText(false) 85 | table.SetAutoFormatHeaders(true) 86 | table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 87 | table.SetAlignment(tablewriter.ALIGN_LEFT) 88 | table.SetCenterSeparator("") 89 | table.SetColumnSeparator("") 90 | table.SetRowSeparator("") 91 | table.SetHeaderLine(false) 92 | table.SetBorder(false) 93 | table.SetTablePadding("\t") // pad with tabs 94 | table.SetNoWhiteSpace(true) 95 | 96 | return table 97 | } 98 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Image URL to use all building/pushing image targets 3 | IMG ?= multi-cluster-operator:v1 4 | 5 | .PHONY: all 6 | all: build 7 | 8 | ##@ General 9 | 10 | # The help target prints out all targets with their descriptions organized 11 | # beneath their categories. The categories are represented by '##@' and the 12 | # target descriptions by '##'. The awk commands is responsible for reading the 13 | # entire set of makefiles included in this invocation, looking for lines of the 14 | # file as xyz: ## something, and then pretty-format the target and help. Then, 15 | # if there's a line with ##@ something, that gets pretty-printed as a category. 16 | # More info on the usage of ANSI control characters for terminal formatting: 17 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 18 | # More info on the awk command: 19 | # http://linuxcommand.org/lc3_adv_awk.php 20 | 21 | .PHONY: help 22 | help: ## Display this help. 23 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 24 | 25 | ##@ Development 26 | 27 | .PHONY: fmt 28 | fmt: ## Run go fmt against code. 29 | go fmt ./... 30 | 31 | .PHONY: test 32 | test: ## Run tests. 33 | go test ./... -coverprofile cover.out 34 | 35 | ##@ Build 36 | 37 | .PHONY: build 38 | build: ## Build manager binary. 39 | go build -a -o multi-cluster-operator cmd/main.go 40 | 41 | .PHONY: run 42 | run: ## Run a controller from your host. 43 | go run ./cmd/main.go --config=./resources/config_home_test.yaml 44 | 45 | .PHONY: docker-build 46 | docker-build: ## Build docker image with the manager. 47 | docker build -t ${IMG} . 48 | 49 | .PHONY: docker-push 50 | docker-push: ## Push docker image with the manager. 51 | docker push ${IMG} 52 | 53 | ##@ Deployment 54 | 55 | .PHONY: deploy 56 | deploy: ## Deploy controller to the K8s cluster specified in ~/.kube/config. 57 | kubectl apply -f deploy/. 58 | 59 | .PHONY: undeploy 60 | undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. 61 | kubectl delete -f deploy/. 62 | 63 | 64 | 65 | 66 | # go-get-tool will 'go get' any package $2 and install it to $1. 67 | PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) 68 | define go-get-tool 69 | @[ -f $(1) ] || { \ 70 | set -e ;\ 71 | TMP_DIR=$$(mktemp -d) ;\ 72 | cd $$TMP_DIR ;\ 73 | go mod init tmp ;\ 74 | echo "Downloading $(2)" ;\ 75 | GOBIN=$(PROJECT_DIR)/bin go get $(2) ;\ 76 | rm -rf $$TMP_DIR ;\ 77 | } 78 | endef 79 | -------------------------------------------------------------------------------- /yaml/multi_cluster_resource_deployment_example.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: mulitcluster.practice.com/v1alpha1 2 | kind: MultiClusterResource 3 | metadata: 4 | name: mydeployment.deployment 5 | namespace: default 6 | spec: 7 | # 模版:填写k8s 原生资源 8 | template: 9 | apiVersion: apps/v1 10 | kind: Deployment 11 | metadata: 12 | name: multiclusterresource-deployment 13 | namespace: default 14 | labels: 15 | app: example 16 | spec: 17 | replicas: 3 18 | selector: 19 | matchLabels: 20 | app: example 21 | template: 22 | metadata: 23 | labels: 24 | app: example 25 | spec: 26 | containers: 27 | - name: example-container 28 | image: nginx:latest 29 | ports: 30 | - containerPort: 80 31 | # 可自行填写多个集群 32 | placement: 33 | clusters: 34 | - name: tencent1 35 | - name: tencent2 36 | - name: tencent4 37 | # 多集群间差异化配置 38 | customize: 39 | clusters: 40 | - name: tencent1 41 | action: 42 | # 删除label 43 | - path: "/metadata/labels/app" 44 | value: 45 | - "example" 46 | op: "remove" 47 | # 替换镜像 48 | - path: "/spec/template/spec/containers/0/image" 49 | value: 50 | - "nginx:1.19.0-alpine" 51 | op: "replace" 52 | # 修改副本数 53 | - path: "/spec/replicas" 54 | op: "replace" 55 | value: 56 | - 2 57 | - name: tencent2 58 | action: 59 | # 新增 annotations 60 | - path: "/metadata/annotations" 61 | op: "add" 62 | value: 63 | - "foo=bar" 64 | # 修改副本数 65 | - path: "/spec/replicas" 66 | op: "replace" 67 | value: 68 | - 1 69 | - name: tencent4 70 | action: 71 | # 新增 annotations 72 | - path: "/metadata/annotations" 73 | op: "add" 74 | value: 75 | - "app=bar" 76 | # 替换镜像 77 | - path: "/spec/template/spec/containers/0/image" 78 | op: "replace" 79 | value: 80 | - "nginx:1.19.0-alpine" 81 | 82 | # 不支持新增 sidecar 容器,重入会有报错问题,reco 83 | # - path: "/spec/template/spec/containers/1" 84 | # value: 85 | # - "name=redis" 86 | # - "image=redis:5-alpine" 87 | # op: "add" 88 | 89 | -------------------------------------------------------------------------------- /mysql/resources.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : mysql57 5 | Source Server Type : MySQL 6 | Source Server Version : 50739 7 | Source Host : localhost:3307 8 | Source Schema : k8s 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 50739 12 | File Encoding : 65001 13 | 14 | Date: 27/09/2022 17:56:03 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for resources 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `resources`; 24 | CREATE TABLE `resources` ( 25 | `id` bigint(11) UNSIGNED NOT NULL AUTO_INCREMENT, 26 | `namespace` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL, 27 | `name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 28 | `cluster` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 29 | `group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 30 | `version` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 31 | `resource` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 32 | `kind` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 33 | `resource_version` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL, 34 | `owner` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT 'owner uid', 35 | `uid` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 36 | `hash` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL, 37 | `object` json NULL, 38 | `create_at` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, 39 | `delete_at` timestamp(0) NULL DEFAULT NULL, 40 | `update_at` timestamp(0) NULL DEFAULT NULL, 41 | PRIMARY KEY (`id`) USING BTREE, 42 | UNIQUE INDEX `uid`(`uid`) USING BTREE 43 | ) ENGINE = MyISAM AUTO_INCREMENT = 0 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic; 44 | 45 | 46 | DROP TABLE IF EXISTS `clusters`; 47 | CREATE TABLE `clusters` ( 48 | `id` bigint(11) UNSIGNED NOT NULL AUTO_INCREMENT, 49 | `name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 50 | `isMaster` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 51 | `status` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 52 | `create_at` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, 53 | PRIMARY KEY (`id`) USING BTREE, 54 | UNIQUE INDEX `name`(`name`) USING BTREE 55 | ) ENGINE = MyISAM AUTO_INCREMENT = 0 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic; 56 | 57 | 58 | -------------------------------------------------------------------------------- /pkg/apis/multiclusterresource/v1alpha1/types.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "sigs.k8s.io/yaml" 6 | ) 7 | 8 | // +genclient 9 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 10 | // MultiClusterResource 11 | type MultiClusterResource struct { 12 | metav1.TypeMeta `json:",inline"` 13 | metav1.ObjectMeta `json:"metadata,omitempty"` 14 | Spec ResourceSpec `json:"spec,omitempty"` 15 | Status StatusTemplate `json:"status,omitempty"` 16 | } 17 | 18 | type ResourceSpec struct { 19 | // 资源模版 20 | Template DataTemplate `json:"template,omitempty"` 21 | // 集群 22 | /* 23 | placement 24 | clusters: 25 | - name: cluster1 26 | - name: cluster2 27 | */ 28 | Placement DataTemplate `json:"placement,omitempty"` 29 | // 差异化适配 30 | /* 31 | customize: 32 | clusters: 33 | - name: cluster1 34 | action: 35 | - path: "/spec/containers/0/image" 36 | op: "replace" 37 | value: 38 | - "nginx:1.19.0-alpine" 39 | */ 40 | Customize Customize `json:"customize,omitempty"` 41 | } 42 | 43 | type Customize struct { 44 | Clusters []Cluster `json:"clusters,omitempty"` 45 | } 46 | 47 | type Cluster struct { 48 | Name string `json:"name,omitempty"` 49 | Action []Action `json:"action,omitempty"` 50 | } 51 | 52 | type Action struct { 53 | Type string `json:"type,omitempty"` 54 | Path string `json:"path,omitempty"` 55 | Value []interface{} `json:"value,omitempty"` 56 | Op string `json:"op,omitempty"` 57 | } 58 | 59 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 60 | // MultiClusterResourceList 61 | type MultiClusterResourceList struct { 62 | metav1.TypeMeta `json:",inline"` 63 | // +optional 64 | metav1.ListMeta `json:"metadata,omitempty"` 65 | 66 | Items []MultiClusterResource `json:"items"` 67 | } 68 | 69 | // TODO: 需要用到,存储在每个集群中的状态 70 | type StatusTemplate map[string]interface{} 71 | 72 | func (in *StatusTemplate) DeepCopyInto(out *StatusTemplate) { 73 | if in == nil { 74 | return 75 | } 76 | b, err := yaml.Marshal(in) 77 | if err != nil { 78 | return 79 | } 80 | 81 | err = yaml.Unmarshal(b, &out) 82 | if err != nil { 83 | return 84 | } 85 | } 86 | 87 | type DataTemplate map[string]interface{} 88 | 89 | func (in *DataTemplate) DeepCopyInto(out *DataTemplate) { 90 | if in == nil { 91 | return 92 | } 93 | b, err := yaml.Marshal(in) 94 | if err != nil { 95 | return 96 | } 97 | 98 | err = yaml.Unmarshal(b, &out) 99 | if err != nil { 100 | return 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /deploy/rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: multi-cluster-operator-sa 5 | namespace: default 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRole 9 | metadata: 10 | name: multi-cluster-operator-clusterrole 11 | rules: 12 | - apiGroups: 13 | - "" 14 | resources: 15 | - namespaces 16 | verbs: 17 | - get 18 | - apiGroups: 19 | - "" 20 | resources: 21 | - configmaps 22 | - pods 23 | - secrets 24 | - endpoints 25 | verbs: 26 | - get 27 | - list 28 | - watch 29 | - apiGroups: 30 | - "" 31 | resources: 32 | - services 33 | verbs: 34 | - get 35 | - list 36 | - watch 37 | - apiGroups: 38 | - networking.k8s.io 39 | resources: 40 | - ingresses 41 | verbs: 42 | - get 43 | - list 44 | - watch 45 | - apiGroups: 46 | - networking.k8s.io 47 | resources: 48 | - ingresses/status 49 | verbs: 50 | - update 51 | - apiGroups: 52 | - networking.k8s.io 53 | resources: 54 | - ingressclasses 55 | verbs: 56 | - get 57 | - list 58 | - watch 59 | - apiGroups: 60 | - "" 61 | resources: 62 | - configmaps 63 | verbs: 64 | - create 65 | - apiGroups: 66 | - "" 67 | resources: 68 | - events 69 | verbs: 70 | - create 71 | - patch 72 | - apiGroups: 73 | - mulitcluster.practice.com 74 | resources: 75 | - multiclusterresources 76 | verbs: 77 | - create 78 | - delete 79 | - get 80 | - list 81 | - patch 82 | - update 83 | - watch 84 | - apiGroups: 85 | - mulitcluster.practice.com 86 | resources: 87 | - multiclusterresources/finalizers 88 | verbs: 89 | - update 90 | - apiGroups: 91 | - mulitcluster.practice.com 92 | resources: 93 | - multiclusterresources/status 94 | verbs: 95 | - get 96 | - patch 97 | - update 98 | - apiGroups: 99 | - coordination.k8s.io 100 | resources: 101 | - leases 102 | verbs: 103 | - '*' 104 | --- 105 | apiVersion: rbac.authorization.k8s.io/v1 106 | kind: ClusterRoleBinding 107 | metadata: 108 | name: multi-cluster-operator-ClusterRoleBinding 109 | roleRef: 110 | apiGroup: rbac.authorization.k8s.io 111 | kind: ClusterRole 112 | name: multi-cluster-operator-clusterrole 113 | subjects: 114 | - kind: ServiceAccount 115 | name: multi-cluster-operator-sa 116 | namespace: default -------------------------------------------------------------------------------- /helm/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ .Values.rbac.serviceaccountname }} 5 | namespace: {{ .Values.rbac.namespace }} 6 | --- 7 | apiVersion: rbac.authorization.k8s.io/v1 8 | kind: ClusterRole 9 | metadata: 10 | name: {{ .Values.rbac.clusterrole }} 11 | rules: 12 | - apiGroups: 13 | - "" 14 | resources: 15 | - namespaces 16 | verbs: 17 | - get 18 | - apiGroups: 19 | - "" 20 | resources: 21 | - configmaps 22 | - pods 23 | - secrets 24 | - endpoints 25 | verbs: 26 | - get 27 | - list 28 | - watch 29 | - apiGroups: 30 | - "" 31 | resources: 32 | - services 33 | verbs: 34 | - get 35 | - list 36 | - watch 37 | - apiGroups: 38 | - networking.k8s.io 39 | resources: 40 | - ingresses 41 | verbs: 42 | - get 43 | - list 44 | - watch 45 | - apiGroups: 46 | - networking.k8s.io 47 | resources: 48 | - ingresses/status 49 | verbs: 50 | - update 51 | - apiGroups: 52 | - networking.k8s.io 53 | resources: 54 | - ingressclasses 55 | verbs: 56 | - get 57 | - list 58 | - watch 59 | - apiGroups: 60 | - "" 61 | resources: 62 | - configmaps 63 | verbs: 64 | - create 65 | - apiGroups: 66 | - "" 67 | resources: 68 | - events 69 | verbs: 70 | - create 71 | - patch 72 | - apiGroups: 73 | - mulitcluster.practice.com 74 | resources: 75 | - multiclusterresources 76 | verbs: 77 | - create 78 | - delete 79 | - get 80 | - list 81 | - patch 82 | - update 83 | - watch 84 | - apiGroups: 85 | - mulitcluster.practice.com 86 | resources: 87 | - multiclusterresources/finalizers 88 | verbs: 89 | - update 90 | - apiGroups: 91 | - mulitcluster.practice.com 92 | resources: 93 | - multiclusterresources/status 94 | verbs: 95 | - get 96 | - patch 97 | - update 98 | - apiGroups: 99 | - coordination.k8s.io 100 | resources: 101 | - leases 102 | verbs: 103 | - '*' 104 | --- 105 | apiVersion: rbac.authorization.k8s.io/v1 106 | kind: ClusterRoleBinding 107 | metadata: 108 | name: {{ .Values.rbac.clusterrolebinding }} 109 | roleRef: 110 | apiGroup: rbac.authorization.k8s.io 111 | kind: ClusterRole 112 | name: {{ .Values.rbac.clusterrole }} 113 | subjects: 114 | - kind: ServiceAccount 115 | name: {{ .Values.rbac.serviceaccountname }} 116 | namespace: {{ .Values.rbac.namespace }} -------------------------------------------------------------------------------- /helm/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ .Values.base.name }} 5 | namespace: {{ .Values.base.namespace }} 6 | labels: 7 | app: {{ .Values.base.name }} 8 | spec: 9 | replicas: {{ .Values.base.replicaCount }} 10 | selector: 11 | matchLabels: 12 | app: {{ .Values.base.name }} 13 | template: 14 | metadata: 15 | labels: 16 | app: {{ .Values.base.name }} 17 | spec: 18 | serviceAccountName: {{ .Values.rbac.serviceaccountname }} 19 | {{- if .Values.nodeName }} 20 | nodeName: {{ .Values.nodeName }} 21 | {{- end }} 22 | containers: 23 | - name: {{ .Values.base.name }} 24 | image: {{ .Values.base.image }} 25 | imagePullPolicy: IfNotPresent 26 | env: 27 | - name: "Release" 28 | value: "1" 29 | - name: POD_NAME 30 | valueFrom: 31 | fieldRef: 32 | apiVersion: v1 33 | fieldPath: metadata.name 34 | args: 35 | - --db-user={{ .Values.db.dbuser | default "root" }} # db 用户 36 | - --db-password={{ .Values.db.dbpassword | default "123456" }} # db 用户密码 37 | - --db-endpoint={{ .Values.db.dbendpoint | default "10.0.0.16:30110" }} # db 地址 38 | - --db-database={{ .Values.db.dbdatabase | default "resources" }} # db 数据库 39 | - --debug-mode={{ .Values.base.debugMode | default "true" }} # 模式 40 | - --config={{ .Values.base.config | default "/app/file/config.yaml" }} # 配置文件路径 41 | - --lease-name={{ .Values.base.leaseName | default "multi-cluster-operator-lease" }} # 租约名 42 | - --lease-namespace={{ .Values.base.namespace | default "default" }} # 命名空间 43 | - --lease-mode={{ .Values.base.leaseMode | default true }} # 是否启用租约模式 44 | - --ctl-port=31888 # 命令行读取的端口 45 | resources: 46 | {{- toYaml .Values.resources | nindent 12 }} 47 | volumeMounts: 48 | {{- range .Values.MultiClusterConfiguration.volumeMounts }} 49 | - name: {{ .name }} 50 | mountPath: {{ .mountPath }} 51 | {{- end }} 52 | 53 | {{- with .Values.nodeSelector }} 54 | nodeSelector: 55 | {{- toYaml . | nindent 8 }} 56 | {{- end }} 57 | {{- with .Values.affinity }} 58 | affinity: 59 | {{- toYaml . | nindent 8 }} 60 | {{- end }} 61 | {{- with .Values.tolerations }} 62 | tolerations: 63 | {{- toYaml . | nindent 8 }} 64 | {{- end }} 65 | volumes: 66 | {{- range .Values.MultiClusterConfiguration.volumes }} 67 | - name: {{ .name }} 68 | hostPath: 69 | path: {{ .hostPath.path }} 70 | {{- end }} 71 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/multicluster/v1alpha1/multicluster_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | v1alpha1 "github.com/myoperator/multiclusteroperator/pkg/apis/multicluster/v1alpha1" 23 | "github.com/myoperator/multiclusteroperator/pkg/client/clientset/versioned/scheme" 24 | rest "k8s.io/client-go/rest" 25 | ) 26 | 27 | type MulitclusterV1alpha1Interface interface { 28 | RESTClient() rest.Interface 29 | MultiClustersGetter 30 | } 31 | 32 | // MulitclusterV1alpha1Client is used to interact with features provided by the mulitcluster.practice.com group. 33 | type MulitclusterV1alpha1Client struct { 34 | restClient rest.Interface 35 | } 36 | 37 | func (c *MulitclusterV1alpha1Client) MultiClusters(namespace string) MultiClusterInterface { 38 | return newMultiClusters(c, namespace) 39 | } 40 | 41 | // NewForConfig creates a new MulitclusterV1alpha1Client for the given config. 42 | func NewForConfig(c *rest.Config) (*MulitclusterV1alpha1Client, error) { 43 | config := *c 44 | if err := setConfigDefaults(&config); err != nil { 45 | return nil, err 46 | } 47 | client, err := rest.RESTClientFor(&config) 48 | if err != nil { 49 | return nil, err 50 | } 51 | return &MulitclusterV1alpha1Client{client}, nil 52 | } 53 | 54 | // NewForConfigOrDie creates a new MulitclusterV1alpha1Client for the given config and 55 | // panics if there is an error in the config. 56 | func NewForConfigOrDie(c *rest.Config) *MulitclusterV1alpha1Client { 57 | client, err := NewForConfig(c) 58 | if err != nil { 59 | panic(err) 60 | } 61 | return client 62 | } 63 | 64 | // New creates a new MulitclusterV1alpha1Client for the given RESTClient. 65 | func New(c rest.Interface) *MulitclusterV1alpha1Client { 66 | return &MulitclusterV1alpha1Client{c} 67 | } 68 | 69 | func setConfigDefaults(config *rest.Config) error { 70 | gv := v1alpha1.SchemeGroupVersion 71 | config.GroupVersion = &gv 72 | config.APIPath = "/apis" 73 | config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() 74 | 75 | if config.UserAgent == "" { 76 | config.UserAgent = rest.DefaultKubernetesUserAgent() 77 | } 78 | 79 | return nil 80 | } 81 | 82 | // RESTClient returns a RESTClient that is used to communicate 83 | // with API server by this client implementation. 84 | func (c *MulitclusterV1alpha1Client) RESTClient() rest.Interface { 85 | if c == nil { 86 | return nil 87 | } 88 | return c.restClient 89 | } 90 | -------------------------------------------------------------------------------- /pkg/config/k8s_config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/api/meta" 5 | "k8s.io/client-go/dynamic" 6 | "k8s.io/client-go/dynamic/dynamicinformer" 7 | "k8s.io/client-go/kubernetes" 8 | "k8s.io/client-go/rest" 9 | "k8s.io/client-go/restmapper" 10 | "k8s.io/client-go/tools/clientcmd" 11 | "k8s.io/klog/v2" 12 | ) 13 | 14 | type K8sConfig struct { 15 | kubeconfigPath string 16 | insecure bool 17 | restconfig *rest.Config 18 | isPatch bool 19 | } 20 | 21 | // NewK8sConfig 创建 K8sConfig 对象, 22 | // 其中: restconfig 对象在 init 时是 nil, 只有在动态时加入才会传入 23 | // isPatch 字段是用于区分是否为初始化加入还是动态加入 24 | func NewK8sConfig(path string, insecure bool, restconfig *rest.Config, isPatch bool) *K8sConfig { 25 | if isPatch { 26 | if restconfig != nil { 27 | restconfig.Insecure = insecure 28 | } 29 | } 30 | 31 | return &K8sConfig{ 32 | kubeconfigPath: path, 33 | insecure: insecure, 34 | restconfig: restconfig, 35 | isPatch: isPatch, 36 | } 37 | } 38 | 39 | func (kc *K8sConfig) k8sRestConfigDefaultOrDie(insecure bool) *rest.Config { 40 | 41 | config, err := clientcmd.BuildConfigFromFlags("", kc.kubeconfigPath) 42 | if err != nil { 43 | klog.Fatal(err) 44 | } 45 | config.Insecure = insecure 46 | return config 47 | } 48 | 49 | // initDynamicClientOrDie 初始化 DynamicClient 50 | func (kc *K8sConfig) initDynamicClientOrDie() dynamic.Interface { 51 | client, err := dynamic.NewForConfig(kc.k8sRestConfigDefaultOrDie(kc.insecure)) 52 | if err != nil { 53 | klog.Fatal(err) 54 | } 55 | return client 56 | } 57 | 58 | // initClient 初始化 clientSet 59 | func (kc *K8sConfig) initClientOrDie() *kubernetes.Clientset { 60 | var err error 61 | var c *kubernetes.Clientset 62 | if kc.isPatch { 63 | c, err = kubernetes.NewForConfig(kc.restconfig) 64 | } else { 65 | c, err = kubernetes.NewForConfig(kc.k8sRestConfigDefaultOrDie(kc.insecure)) 66 | } 67 | 68 | if err != nil { 69 | klog.Fatal(err) 70 | } 71 | return c 72 | } 73 | 74 | // NewRestMapperOrDie 获取 api group multiclusterresource 75 | func (kc *K8sConfig) NewRestMapperOrDie() *meta.RESTMapper { 76 | gr, err := restmapper.GetAPIGroupResources(kc.initClientOrDie().Discovery()) 77 | if err != nil { 78 | klog.Fatal(err) 79 | } 80 | mapper := restmapper.NewDiscoveryRESTMapper(gr) 81 | return &mapper 82 | } 83 | 84 | // InitWatchFactoryAndRestConfig 初始化 dynamic client informerFactory, restConfig 85 | func (kc *K8sConfig) InitWatchFactoryAndRestConfig() (dynamicinformer.DynamicSharedInformerFactory, dynamic.Interface, *rest.Config) { 86 | dynClient := kc.initDynamicClientOrDie() 87 | return dynamicinformer.NewDynamicSharedInformerFactory(dynClient, 0), dynClient, kc.k8sRestConfigDefaultOrDie(kc.insecure) 88 | } 89 | 90 | func (kc *K8sConfig) PatchWatchFactoryAndRestConfig() (dynamicinformer.DynamicSharedInformerFactory, dynamic.Interface, *rest.Config) { 91 | 92 | dynClient, err := dynamic.NewForConfig(kc.restconfig) 93 | if err != nil { 94 | klog.Fatal(err) 95 | } 96 | return dynamicinformer.NewDynamicSharedInformerFactory(dynClient, 0), dynClient, kc.restconfig 97 | } 98 | -------------------------------------------------------------------------------- /cmd/ctl_plugin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/myoperator/multiclusteroperator/cmd/ctl_plugin/common" 5 | "github.com/myoperator/multiclusteroperator/cmd/ctl_plugin/resource/describe" 6 | "github.com/myoperator/multiclusteroperator/cmd/ctl_plugin/resource/join" 7 | "github.com/myoperator/multiclusteroperator/cmd/ctl_plugin/resource/list" 8 | "github.com/spf13/cobra" 9 | "k8s.io/cli-runtime/pkg/genericclioptions" 10 | "k8s.io/klog/v2" 11 | "k8s.io/kubectl/pkg/cmd/apply" 12 | "k8s.io/kubectl/pkg/cmd/create" 13 | "k8s.io/kubectl/pkg/cmd/delete" 14 | "k8s.io/kubectl/pkg/cmd/edit" 15 | cmdutil "k8s.io/kubectl/pkg/cmd/util" 16 | "os" 17 | "strconv" 18 | ) 19 | 20 | type CmdMetaData struct { 21 | Use string 22 | Short string 23 | Example string 24 | } 25 | 26 | var ( 27 | cmdMetaData *CmdMetaData 28 | ) 29 | 30 | func init() { 31 | cmdMetaData = &CmdMetaData{ 32 | Use: "kubectl-multicluster [flags]", 33 | Short: "kubectl-multicluster, Multi-cluster command line tool", 34 | Example: "kubectl-multicluster list core/v1/pods, or kubectl-multicluster describe core/v1/pods", 35 | } 36 | } 37 | 38 | func main() { 39 | 40 | // 主命令 41 | mainCmd := &cobra.Command{ 42 | Use: cmdMetaData.Use, 43 | Short: cmdMetaData.Short, 44 | Example: cmdMetaData.Example, 45 | SilenceUsage: true, 46 | } 47 | 48 | // 从配置文件获取端口信息 49 | r := common.LoadConfigFile() 50 | common.ServerPort, _ = strconv.Atoi(r.ServerPort) 51 | common.ServerIp = r.ServerIP 52 | common.KubeConfigPath = r.MasterClusterKubeConfigPath 53 | // 注册 list describe 命令 54 | MergeFlags(list.ListCmd, describe.DescribeCmd, join.JoinCmd, join.UnJoinCmd) 55 | // 只需要加入 --clusterName=xxx, --name=xxx, 其他适配 kubectl 56 | list.ListCmd.Flags().StringVar(&common.Cluster, "clusterName", "", "--clusterName=xxx") 57 | list.ListCmd.Flags().StringVar(&common.Name, "name", "", "--name=xxx") 58 | 59 | describe.DescribeCmd.Flags().StringVar(&common.Cluster, "clusterName", "", "--clusterName=xxx") 60 | describe.DescribeCmd.Flags().StringVar(&common.Name, "name", "", "--name=xxx") 61 | // join 命令需要 --file=xxx 上传文件 62 | join.JoinCmd.Flags().StringVar(&common.File, "file", "", "--file=xxx") 63 | 64 | // kubeconfig 配置 65 | kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() 66 | // 获取clientSet 67 | kubeConfigFlags.KubeConfig = &common.KubeConfigPath 68 | matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags) 69 | f := cmdutil.NewFactory(matchVersionKubeConfigFlags) 70 | // 输出地点 71 | ioStreams := genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} 72 | 73 | // 主 Cmd 需要加入子 Cmd 74 | mainCmd.AddCommand(list.ListCmd, 75 | describe.DescribeCmd, 76 | join.JoinCmd, 77 | join.UnJoinCmd, 78 | apply.NewCmdApply("kubectl", f, ioStreams), 79 | delete.NewCmdDelete(f, ioStreams), 80 | create.NewCmdCreate(f, ioStreams), 81 | edit.NewCmdEdit(f, ioStreams), 82 | ) 83 | 84 | err := mainCmd.Execute() // 主命令执行 85 | 86 | if err != nil { 87 | klog.Fatalln(err) 88 | } 89 | } 90 | 91 | var cfgFlags *genericclioptions.ConfigFlags 92 | 93 | func MergeFlags(cmds ...*cobra.Command) { 94 | cfgFlags = genericclioptions.NewConfigFlags(true) 95 | for _, cmd := range cmds { 96 | cfgFlags.AddFlags(cmd.Flags()) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/fake/clientset_generated.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | clientset "github.com/myoperator/multiclusteroperator/pkg/client/clientset/versioned" 23 | mulitclusterv1alpha1 "github.com/myoperator/multiclusteroperator/pkg/client/clientset/versioned/typed/multicluster/v1alpha1" 24 | fakemulitclusterv1alpha1 "github.com/myoperator/multiclusteroperator/pkg/client/clientset/versioned/typed/multicluster/v1alpha1/fake" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | "k8s.io/apimachinery/pkg/watch" 27 | "k8s.io/client-go/discovery" 28 | fakediscovery "k8s.io/client-go/discovery/fake" 29 | "k8s.io/client-go/testing" 30 | ) 31 | 32 | // NewSimpleClientset returns a clientset that will respond with the provided objects. 33 | // It's backed by a very simple object tracker that processes creates, updates and deletions as-is, 34 | // without applying any validations and/or defaults. It shouldn't be considered a replacement 35 | // for a real clientset and is mostly useful in simple unit tests. 36 | func NewSimpleClientset(objects ...runtime.Object) *Clientset { 37 | o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) 38 | for _, obj := range objects { 39 | if err := o.Add(obj); err != nil { 40 | panic(err) 41 | } 42 | } 43 | 44 | cs := &Clientset{tracker: o} 45 | cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} 46 | cs.AddReactor("*", "*", testing.ObjectReaction(o)) 47 | cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { 48 | gvr := action.GetResource() 49 | ns := action.GetNamespace() 50 | watch, err := o.Watch(gvr, ns) 51 | if err != nil { 52 | return false, nil, err 53 | } 54 | return true, watch, nil 55 | }) 56 | 57 | return cs 58 | } 59 | 60 | // Clientset implements clientset.Interface. Meant to be embedded into a 61 | // struct to get a default implementation. This makes faking out just the method 62 | // you want to test easier. 63 | type Clientset struct { 64 | testing.Fake 65 | discovery *fakediscovery.FakeDiscovery 66 | tracker testing.ObjectTracker 67 | } 68 | 69 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 70 | return c.discovery 71 | } 72 | 73 | func (c *Clientset) Tracker() testing.ObjectTracker { 74 | return c.tracker 75 | } 76 | 77 | var ( 78 | _ clientset.Interface = &Clientset{} 79 | _ testing.FakeClient = &Clientset{} 80 | ) 81 | 82 | // MulitclusterV1alpha1 retrieves the MulitclusterV1alpha1Client 83 | func (c *Clientset) MulitclusterV1alpha1() mulitclusterv1alpha1.MulitclusterV1alpha1Interface { 84 | return &fakemulitclusterv1alpha1.FakeMulitclusterV1alpha1{Fake: &c.Fake} 85 | } 86 | -------------------------------------------------------------------------------- /deploy/deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: multi-cluster-operator 5 | namespace: default 6 | spec: 7 | replicas: 2 8 | selector: 9 | matchLabels: 10 | app: multi-cluster-operator 11 | template: 12 | metadata: 13 | labels: 14 | app: multi-cluster-operator 15 | spec: 16 | serviceAccountName: multi-cluster-operator-sa 17 | nodeName: vm-0-16-centos # 只调度到这个节点上,因为测试集群只有此node,请自行修改 18 | containers: 19 | - name: multi-cluster-operator 20 | image: multi-cluster-operator:v1 21 | imagePullPolicy: IfNotPresent 22 | # args 启动参数请自行修改 23 | args: 24 | - --db-user=root # db 用户 25 | - --db-password=123456 # db 用户密码 26 | - --db-endpoint=10.0.0.16:30110 # db 地址 27 | - --db-database=resources # db 数据库 28 | - --debug-mode=true # 模式 29 | - --config=/app/file/config.yaml # 配置文件路径 30 | - --ctl-port=31888 # 命令行读取的端口 31 | - --lease-name=multi-cluster-operator-lease # 分布式锁名 32 | - --lease-namespace=default # 分布式锁 namespace 33 | - --lease-mode=true # 是否启动选主模式 34 | env: 35 | - name: "Release" 36 | value: "1" 37 | - name: POD_NAME 38 | valueFrom: 39 | fieldRef: 40 | apiVersion: v1 41 | fieldPath: metadata.name 42 | volumeMounts: 43 | # 挂载不同集群的 kubeconfig,请自行修改 44 | - name: tencent1 45 | mountPath: /app/file/config-tencent1 46 | - name: tencent2 47 | mountPath: /app/file/config-tencent2 48 | - name: tencent4 49 | mountPath: /app/file/config-tencent4 50 | # 配置文件 config.yaml 51 | - name: config 52 | mountPath: /app/file/config.yaml 53 | - name: migrate 54 | mountPath: /app/migrations 55 | # 需要挂载用户自己监听的 kubeconfig 文件 56 | # 请自行修改 57 | volumes: 58 | - name: tencent1 59 | hostPath: 60 | path: /root/multi_resource_operator/resources/config-tencent1 61 | - name: tencent2 62 | hostPath: 63 | path: /root/multi_resource_operator/resources/config-tencent2 64 | - name: tencent4 65 | hostPath: 66 | path: /root/multi_resource_operator/resources/config-tencent4 67 | - name: config 68 | hostPath: 69 | path: /root/multi_resource_operator/config.yaml 70 | - name: migrate 71 | hostPath: 72 | path: /root/multi_resource_operator/migrations 73 | --- 74 | apiVersion: v1 75 | kind: Service 76 | metadata: 77 | name: multi-cluster-operator-svc 78 | namespace: default 79 | spec: 80 | type: NodePort 81 | ports: 82 | - port: 8888 # 容器端口 83 | nodePort: 31888 # 对外暴露的端口 84 | name: server 85 | - port: 29999 # 健康检查端口 86 | nodePort: 31889 # 对外暴露的端口 87 | name: health 88 | selector: 89 | app: multi-cluster-operator -------------------------------------------------------------------------------- /pkg/options/server/server_options.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "github.com/myoperator/multiclusteroperator/pkg/server" 7 | "github.com/pkg/errors" 8 | "github.com/spf13/pflag" 9 | "os" 10 | "path/filepath" 11 | ) 12 | 13 | type ServerOptions struct { 14 | Port int 15 | CertDir string 16 | ConfigPath string 17 | HealthPort int 18 | CtlPort int 19 | DebugMode bool 20 | 21 | LeaseLockName string // 锁名 22 | LeaseLockNamespace string // 获取锁的namespace 23 | LeaseLockMode bool // 是否为选主模式 24 | } 25 | 26 | func NewServerOptions() *ServerOptions { 27 | return &ServerOptions{} 28 | } 29 | 30 | func (o *ServerOptions) AddFlags(fs *pflag.FlagSet) { 31 | fs.IntVar(&o.Port, "address", 8888, 32 | "Address of http server to bind on. Default to 8888.") 33 | fs.IntVar(&o.HealthPort, "HealthPort", 29999, 34 | "Address of health http server to bind on. Default to 29999.") 35 | fs.IntVar(&o.CtlPort, "ctl-port", 8888, 36 | "Port used for ctl configuration server.") 37 | 38 | fs.StringVar(&o.ConfigPath, "config", "resources/config_home_test.yaml", 39 | "kubeconfig path for k8s cluster") 40 | fs.StringVar(&o.CertDir, "cert-dir", "", 41 | "The directory of cert to use, use tls.crt and tls.key as certificates. Default to disable https.") 42 | 43 | fs.BoolVar(&o.DebugMode, "debug-mode", false, "Debug Mode") 44 | 45 | fs.BoolVar(&o.LeaseLockMode, "lease-mode", false, "Whether to use election mode") 46 | fs.StringVar(&o.LeaseLockName, "lease-name", "lease-default-name", "election lease leaselock name") 47 | fs.StringVar(&o.LeaseLockNamespace, "lease-namespace", "default", "election lease leaselock namespace") 48 | 49 | } 50 | 51 | // Complete TODO: 实现赋值逻辑 52 | func (o *ServerOptions) Complete() error { 53 | return nil 54 | } 55 | 56 | // Validate 验证逻辑 57 | func (o *ServerOptions) Validate() []error { 58 | var errs []error 59 | 60 | // 验证 Address 字段是否在有效范围内 61 | if o.Port <= 0 || o.Port > 65535 { 62 | errs = append(errs, fmt.Errorf("Invalid server address. Must be within the range of 1-65535")) 63 | } 64 | 65 | // 验证 HealthPort 字段是否在有效范围内 66 | if o.HealthPort <= 0 || o.HealthPort > 65535 { 67 | errs = append(errs, fmt.Errorf("Invalid health port. Must be within the range of 1-65535")) 68 | } 69 | 70 | // 验证 CtlPort 字段是否在有效范围内 71 | if o.CtlPort <= 0 || o.CtlPort > 65535 { 72 | errs = append(errs, fmt.Errorf("Invalid ctl port. Must be within the range of 1-65535")) 73 | } 74 | 75 | // 验证 ConfigPath 字段是否为空 76 | if o.ConfigPath == "" { 77 | errs = append(errs, fmt.Errorf("Config path is required")) 78 | } 79 | 80 | // 验证 CertDir 字段是否符合要求 81 | if o.CertDir != "" { 82 | // 在这里可以添加更多的证书目录验证逻辑 83 | // 例如,验证证书文件是否存在、证书文件格式是否正确等 84 | if _, err := os.Stat(o.CertDir); os.IsNotExist(err) { 85 | errs = append(errs, errors.New("Cert directory does not exist")) 86 | } 87 | } 88 | 89 | return errs 90 | } 91 | 92 | func (o *ServerOptions) NewServer() (*server.Server, error) { 93 | var tlsConfig *tls.Config = nil 94 | if o.CertDir != "" { 95 | cert, err := tls.LoadX509KeyPair(filepath.Join(o.CertDir, "tls.crt"), filepath.Join(o.CertDir, "tls.key")) 96 | if err != nil { 97 | return nil, errors.WithMessage(err, "unable to load tls certificate") 98 | } 99 | tlsConfig = &tls.Config{ 100 | Certificates: []tls.Certificate{cert}, 101 | } 102 | } 103 | return server.NewServer(o.Port, tlsConfig), nil 104 | } 105 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/clientset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package versioned 20 | 21 | import ( 22 | "fmt" 23 | 24 | mulitclusterv1alpha1 "github.com/myoperator/multiclusteroperator/pkg/client/clientset/versioned/typed/multicluster/v1alpha1" 25 | discovery "k8s.io/client-go/discovery" 26 | rest "k8s.io/client-go/rest" 27 | flowcontrol "k8s.io/client-go/util/flowcontrol" 28 | ) 29 | 30 | type Interface interface { 31 | Discovery() discovery.DiscoveryInterface 32 | MulitclusterV1alpha1() mulitclusterv1alpha1.MulitclusterV1alpha1Interface 33 | } 34 | 35 | // Clientset contains the clients for groups. Each group has exactly one 36 | // version included in a Clientset. 37 | type Clientset struct { 38 | *discovery.DiscoveryClient 39 | mulitclusterV1alpha1 *mulitclusterv1alpha1.MulitclusterV1alpha1Client 40 | } 41 | 42 | // MulitclusterV1alpha1 retrieves the MulitclusterV1alpha1Client 43 | func (c *Clientset) MulitclusterV1alpha1() mulitclusterv1alpha1.MulitclusterV1alpha1Interface { 44 | return c.mulitclusterV1alpha1 45 | } 46 | 47 | // Discovery retrieves the DiscoveryClient 48 | func (c *Clientset) Discovery() discovery.DiscoveryInterface { 49 | if c == nil { 50 | return nil 51 | } 52 | return c.DiscoveryClient 53 | } 54 | 55 | // NewForConfig creates a new Clientset for the given config. 56 | // If config's RateLimiter is not set and QPS and Burst are acceptable, 57 | // NewForConfig will generate a rate-limiter in configShallowCopy. 58 | func NewForConfig(c *rest.Config) (*Clientset, error) { 59 | configShallowCopy := *c 60 | if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { 61 | if configShallowCopy.Burst <= 0 { 62 | return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") 63 | } 64 | configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) 65 | } 66 | var cs Clientset 67 | var err error 68 | cs.mulitclusterV1alpha1, err = mulitclusterv1alpha1.NewForConfig(&configShallowCopy) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy) 74 | if err != nil { 75 | return nil, err 76 | } 77 | return &cs, nil 78 | } 79 | 80 | // NewForConfigOrDie creates a new Clientset for the given config and 81 | // panics if there is an error in the config. 82 | func NewForConfigOrDie(c *rest.Config) *Clientset { 83 | var cs Clientset 84 | cs.mulitclusterV1alpha1 = mulitclusterv1alpha1.NewForConfigOrDie(c) 85 | 86 | cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c) 87 | return &cs 88 | } 89 | 90 | // New creates a new Clientset for the given RESTClient. 91 | func New(c rest.Interface) *Clientset { 92 | var cs Clientset 93 | cs.mulitclusterV1alpha1 = mulitclusterv1alpha1.New(c) 94 | 95 | cs.DiscoveryClient = discovery.NewDiscoveryClient(c) 96 | return &cs 97 | } 98 | -------------------------------------------------------------------------------- /pkg/caches/handler.go: -------------------------------------------------------------------------------- 1 | package caches 2 | 3 | import ( 4 | "context" 5 | "github.com/myoperator/multiclusteroperator/pkg/caches/workqueue" 6 | "github.com/myoperator/multiclusteroperator/pkg/store/model" 7 | "github.com/myoperator/multiclusteroperator/pkg/util" 8 | "gorm.io/gorm" 9 | "k8s.io/apimachinery/pkg/api/meta" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "k8s.io/client-go/tools/cache" 12 | "k8s.io/klog/v2" 13 | ) 14 | 15 | type ResourceHandler struct { 16 | DB *gorm.DB 17 | // RestMapper 资源对象 18 | RestMapper meta.RESTMapper 19 | // Queue 工作队列接口 20 | workqueue.Queue 21 | // clusterName 集群名 22 | clusterName string 23 | } 24 | 25 | // Start 启动工作队列 26 | func (r *ResourceHandler) Start(ctx context.Context) { 27 | klog.Info("worker queue start...") 28 | go func() { 29 | defer util.HandleCrash() 30 | for { 31 | select { 32 | case <-ctx.Done(): 33 | klog.Info("exit work queue...") 34 | r.Close() 35 | return 36 | default: 37 | } 38 | 39 | // 不断由队列中获取元素处理 40 | obj, err := r.Pop() 41 | if err != nil { 42 | klog.Errorf("work queue pop error: %s\n", err) 43 | continue 44 | } 45 | 46 | // 如果自己的业务逻辑发生问题,可以重新放回队列。 47 | if err = r.handleObject(obj); err != nil { 48 | klog.Errorf("handle obj from work queue error: %s\n", err) 49 | // 重新入列 50 | _ = r.ReQueue(obj) 51 | } else { 52 | // 完成就结束 53 | r.Finish(obj) 54 | } 55 | } 56 | }() 57 | } 58 | 59 | // handleObject 处理 work queue 传入对象 60 | func (r *ResourceHandler) handleObject(obj *workqueue.QueueResource) error { 61 | //klog.Infof("[%s] handler [%s] object from work queue\n", r.clusterName, obj.EventType) 62 | res, err := model.NewResource(obj.Object, r.RestMapper, r.clusterName) 63 | if err != nil { 64 | klog.Errorf("new resource [%s] object error: %s\n", obj.EventType, err) 65 | return err 66 | } 67 | 68 | // 区分传入的事件,并进行相应处理 69 | switch obj.EventType { 70 | case workqueue.AddEvent: 71 | err = res.Add(r.DB) 72 | if err != nil { 73 | klog.Errorf("[%s] [%s] object error: %s\n", r.clusterName, obj.EventType, err) 74 | return err 75 | } 76 | case workqueue.UpdateEvent: 77 | err = res.Update(r.DB) 78 | if err != nil { 79 | klog.Errorf("[%s] [%s] object error: %s\n", r.clusterName, obj.EventType, err) 80 | return err 81 | } 82 | case workqueue.DeleteEvent: 83 | err = res.Delete(r.DB) 84 | if err != nil { 85 | klog.Errorf("[%s] [%s] object error: %s\n", r.clusterName, obj.EventType, err) 86 | return err 87 | } 88 | } 89 | return nil 90 | } 91 | 92 | func NewResourceHandler(DB *gorm.DB, restMapper meta.RESTMapper, clusterName string) *ResourceHandler { 93 | return &ResourceHandler{ 94 | DB: DB, 95 | RestMapper: restMapper, 96 | Queue: workqueue.NewWorkQueue(5), 97 | clusterName: clusterName, 98 | } 99 | } 100 | 101 | func (r *ResourceHandler) OnAdd(obj interface{}, isInInitialList bool) { 102 | if o, ok := obj.(runtime.Object); ok { 103 | rr := &workqueue.QueueResource{Object: o, EventType: workqueue.AddEvent} 104 | r.Push(rr) 105 | } 106 | } 107 | 108 | func (r *ResourceHandler) OnUpdate(oldObj, newObj interface{}) { 109 | if o, ok := newObj.(runtime.Object); ok { 110 | rr := &workqueue.QueueResource{Object: o, EventType: workqueue.UpdateEvent} 111 | r.Push(rr) 112 | } 113 | } 114 | 115 | func (r *ResourceHandler) OnDelete(obj interface{}) { 116 | if o, ok := obj.(runtime.Object); ok { 117 | rr := &workqueue.QueueResource{Object: o, EventType: workqueue.DeleteEvent} 118 | r.Push(rr) 119 | } 120 | } 121 | 122 | var _ cache.ResourceEventHandler = &ResourceHandler{} 123 | -------------------------------------------------------------------------------- /pkg/store/model/resources.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/myoperator/multiclusteroperator/pkg/util" 5 | "gorm.io/gorm" 6 | "gorm.io/gorm/clause" 7 | "k8s.io/apimachinery/pkg/api/meta" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/klog/v2" 11 | "sigs.k8s.io/yaml" 12 | "time" 13 | ) 14 | 15 | // Resources 放入表中的模型 16 | type Resources struct { 17 | // 区分集群 18 | Cluster string 19 | // obj Unstructured 结构体,不用于入库 20 | obj *unstructured.Unstructured `gorm:"-"` 21 | // objbytes []byte 对象,不用于入库,需要获取 yaml or json 格式使用 22 | objbytes []byte `gorm:"-"` 23 | // 主键id 24 | Id int `gorm:"column:id;primaryKey;autoIncrement"` 25 | Name string `gorm:"column:name"` 26 | NameSpace string `gorm:"column:namespace"` 27 | ResourceVersion string `gorm:"column:resource_version"` 28 | // Hash 值 29 | Hash string `gorm:"column:hash"` 30 | Uid string `gorm:"column:uid"` 31 | // GVR GVK 有关 32 | Group string `gorm:"column:group"` 33 | Version string `gorm:"column:version"` 34 | Resource string `gorm:"column:resource"` 35 | Kind string `gorm:"column:kind"` 36 | // owner 37 | Owner string `gorm:"column:owner"` 38 | Object string `gorm:"column:object"` 39 | // 时间相关,UpdateAt DeleteAt 插入时 不需要赋值 40 | CreateAt time.Time `gorm:"column:create_at"` 41 | UpdateAt time.Time `gorm:"column:update_at"` 42 | DeleteAt time.Time `gorm:"column:delete_at"` 43 | } 44 | 45 | func NewResource(obj runtime.Object, restmapper meta.RESTMapper, clusterName string) (*Resources, error) { 46 | o := &unstructured.Unstructured{} 47 | b, err := yaml.Marshal(obj) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | err = yaml.Unmarshal(b, o) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | gvk := o.GroupVersionKind() 58 | mapping, err := restmapper.RESTMapping(gvk.GroupKind()) 59 | if err != nil { 60 | return nil, err 61 | } 62 | // 赋值 63 | retObj := &Resources{obj: o, objbytes: b} 64 | retObj.Cluster = clusterName 65 | // name namespace 66 | retObj.Name = o.GetName() 67 | retObj.NameSpace = o.GetNamespace() 68 | // gvr gvk 69 | retObj.Group = gvk.Group 70 | retObj.Version = gvk.Version 71 | retObj.Kind = gvk.Kind 72 | retObj.Resource = mapping.Resource.Resource 73 | // 版本号 74 | retObj.ResourceVersion = o.GetResourceVersion() 75 | retObj.CreateAt = o.GetCreationTimestamp().Time 76 | 77 | retObj.Uid = string(o.GetUID()) 78 | 79 | return retObj, nil 80 | } 81 | 82 | // prepare 事前准备 83 | func (r *Resources) prepare() { 84 | // 如果有 OwnerReferences,则设置 85 | if len(r.obj.GetOwnerReferences()) > 0 { 86 | r.Owner = string(r.obj.GetOwnerReferences()[0].UID) 87 | } 88 | // 获取md5值 89 | r.Hash = util.HashObject(r.objbytes) 90 | // 数据库为 Json 类型 91 | //r.Object = string(r.objbytes) 92 | objJson, err := yaml.YAMLToJSON(r.objbytes) 93 | if err != nil { 94 | klog.Fatalln(err) 95 | } 96 | r.Object = string(objJson) 97 | } 98 | 99 | func (r *Resources) Add(db *gorm.DB) error { 100 | r.prepare() 101 | // 处理冲突方法:当 uid 发现存在时,只更新 resource_version update_at 字段 102 | return db.Clauses(clause.OnConflict{ 103 | Columns: []clause.Column{{Name: "uid"}}, 104 | DoUpdates: clause.Assignments( 105 | map[string]interface{}{ 106 | "resource_version": r.ResourceVersion, 107 | "update_at": time.Now(), 108 | }), 109 | }).Create(r).Error 110 | } 111 | 112 | func (r *Resources) Update(db *gorm.DB) error { 113 | r.prepare() 114 | r.UpdateAt = time.Now() 115 | // 当 hash 值不与库中的相等时,才进行更新 116 | return db.Where("uid=?", r.Uid).Where("hash!=?", r.Hash).Updates(r).Error 117 | } 118 | 119 | func (r *Resources) Delete(db *gorm.DB) error { 120 | return db.Where("uid=?", r.Uid).Delete(r).Error 121 | } 122 | 123 | func (*Resources) TableName() string { 124 | return "resources" 125 | } 126 | -------------------------------------------------------------------------------- /helm/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for helm. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | # 基础配置 6 | base: 7 | # 副本数 8 | replicaCount: 2 9 | # 应用名或 namespace 10 | name: multi-cluster-operator 11 | namespace: default 12 | serviceName: multi-cluster-operator-svc 13 | # 镜像名 14 | image: multi-cluster-operator:v1 15 | # 多集群配置文件目录 (注意:在容器中的位置) 16 | config: /app/file/config.yaml 17 | # 是否指定调度到某个节点,如果不需要则不填 18 | nodeName: vm-0-16-centos 19 | # 是否启动多副本选主机制 20 | leaseMode: "true" 21 | # 租约名 22 | leaseName: multi-cluster-operator-lease 23 | debugMode: "false" 24 | 25 | 26 | # 用于创建 rbac 使用 27 | rbac: 28 | serviceaccountname: multi-cluster-operator-sa 29 | namespace: default 30 | clusterrole: multi-cluster-operator-clusterrole 31 | clusterrolebinding: multi-cluster-operator-ClusterRoleBinding 32 | 33 | # db 配置 34 | db: 35 | dbuser: root 36 | dbpassword: 123456 37 | # 注意:必须容器网络可达 38 | dbendpoint: 10.0.0.16:30110 39 | dbdatabase: resources 40 | 41 | # service 配置 42 | service: 43 | type: NodePort 44 | ports: 45 | - port: 8888 # 容器端口 46 | nodePort: 31888 # 对外暴露的端口 47 | name: server 48 | - port: 29999 # 健康检查端口 49 | nodePort: 31889 # 对外暴露的端口 50 | name: health 51 | 52 | # 多集群中 每个集群的 kubeconfig 文件需要挂载到 pod 中 53 | MultiClusterConfiguration: 54 | volumeMounts: 55 | # 挂载不同集群的 kubeconfig,请自行修改 56 | - name: tencent1 57 | mountPath: /app/file/config-tencent1 58 | - name: tencent2 59 | mountPath: /app/file/config-tencent2 60 | - name: tencent4 61 | mountPath: /app/file/config-tencent4 62 | # 配置文件 config.yaml 63 | - name: config 64 | mountPath: /app/file/config.yaml 65 | - name: migrate 66 | mountPath: /app/migrations 67 | # 节点上的位置 68 | volumes: 69 | - name: tencent1 70 | # 目录请自行替换 71 | hostPath: 72 | path: /root/multi_resource_operator/resources/config-tencent1 73 | - name: tencent2 74 | hostPath: 75 | path: /root/multi_resource_operator/resources/config-tencent2 76 | - name: tencent4 77 | hostPath: 78 | path: /root/multi_resource_operator/resources/config-tencent4 79 | - name: config 80 | hostPath: 81 | path: /root/multi_resource_operator/config.yaml 82 | - name: migrate 83 | hostPath: 84 | path: /root/multi_resource_operator/migrations 85 | 86 | 87 | 88 | imagePullSecrets: [] 89 | nameOverride: "" 90 | 91 | serviceAccount: 92 | # Specifies whether a service account should be created 93 | create: true 94 | # Annotations to add to the service account 95 | annotations: {} 96 | # The name of the service account to use. 97 | # If not set and create is true, a name is generated using the fullname template 98 | name: "" 99 | 100 | podAnnotations: {} 101 | 102 | podSecurityContext: {} 103 | # fsGroup: 2000 104 | 105 | securityContext: {} 106 | # capabilities: 107 | # drop: 108 | # - ALL 109 | # readOnlyRootFilesystem: true 110 | # runAsNonRoot: true 111 | # runAsUser: 1000 112 | 113 | 114 | resources: {} 115 | # We usually recommend not to specify default resources and to leave this as a conscious 116 | # choice for the user. This also increases chances charts run on environments with little 117 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 118 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 119 | # limits: 120 | # cpu: 100m 121 | # memory: 128Mi 122 | # requests: 123 | # cpu: 100m 124 | # memory: 128Mi 125 | 126 | autoscaling: 127 | enabled: false 128 | minReplicas: 1 129 | maxReplicas: 100 130 | targetCPUUtilizationPercentage: 80 131 | # targetMemoryUtilizationPercentage: 80 132 | 133 | nodeSelector: {} 134 | 135 | tolerations: [] 136 | 137 | affinity: {} 138 | -------------------------------------------------------------------------------- /pkg/multi_cluster_controller/resource_controller.go: -------------------------------------------------------------------------------- 1 | package multi_cluster_controller 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/myoperator/multiclusteroperator/pkg/apis/multiclusterresource/v1alpha1" 7 | corev1 "k8s.io/api/core/v1" 8 | "k8s.io/apimachinery/pkg/api/errors" 9 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 10 | "time" 11 | ) 12 | 13 | func (mc *MultiClusterHandler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { 14 | // 获取 Resource 15 | rr := &v1alpha1.MultiClusterResource{} 16 | err := mc.Get(ctx, req.NamespacedName, rr) 17 | mc.Logger.Info("get multi-cluster-resource: ", rr.GetName()+"/"+rr.GetNamespace()) 18 | if err != nil { 19 | if errors.IsNotFound(err) { 20 | mc.Logger.Info("not found multi-cluster-resource: ", rr.GetName()+"/"+rr.GetNamespace()) 21 | return reconcile.Result{}, nil 22 | } 23 | mc.Logger.Error(err, "get multi-cluster-resource: ", rr.GetName()+"/"+rr.GetNamespace(), " failed") 24 | return reconcile.Result{}, err 25 | } 26 | 27 | // 删除状态,会等到 Finalizer 字段清空后才会真正删除 28 | // 1、删除所有集群资源 29 | // 2、清空 Finalizer,更新状态 30 | if !rr.DeletionTimestamp.IsZero() { 31 | err = mc.resourceDelete(ctx, rr) 32 | if err != nil { 33 | mc.Logger.Error(err, "delete multi-cluster-resource: ", rr.GetName()+"/"+rr.GetNamespace(), " failed") 34 | mc.EventRecorder.Event(rr, corev1.EventTypeWarning, "Delete", fmt.Sprintf("delete %s fail", rr.Name)) 35 | return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 60}, err 36 | } 37 | 38 | err = mc.Client.Update(ctx, rr) 39 | if err != nil { 40 | mc.Logger.Error(err, "update multi-cluster-resource: ", rr.GetName()+"/"+rr.GetNamespace(), " failed") 41 | mc.EventRecorder.Event(rr, corev1.EventTypeWarning, "UpdateFailed", fmt.Sprintf("update %s fail", rr.Name)) 42 | return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 60}, err 43 | } 44 | 45 | return reconcile.Result{}, nil 46 | } 47 | 48 | // 设置 crd 对象的 Finalizer 字段,并判断是否改变 49 | forDelete, finalizer, isChange := mc.setResourceFinalizer(rr) 50 | 51 | // TODO: 操作后,需要更新 status 字段 52 | 53 | // 如果 Finalizer 字段改变, 54 | // 代表可能是需要进行特定集群的删除资源操作 55 | if isChange { 56 | err = mc.resourceDeleteBySlice(ctx, rr, forDelete) 57 | if err != nil { 58 | mc.Logger.Error(err, "delete slice multi-cluster-resource: ", rr.GetName()+"/"+rr.GetNamespace(), " failed") 59 | mc.EventRecorder.Event(rr, corev1.EventTypeWarning, "DeleteFailed", fmt.Sprintf("resourceDeleteBySlice %s fail", rr.Name)) 60 | return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 60}, err 61 | } 62 | // 删除后覆盖 63 | rr.Finalizers = finalizer 64 | err = mc.Client.Update(ctx, rr) 65 | if err != nil { 66 | mc.Logger.Error(err, "update slice multi-cluster-resource: ", rr.GetName()+"/"+rr.GetNamespace(), " failed") 67 | mc.EventRecorder.Event(rr, corev1.EventTypeWarning, "UpdateFailed", fmt.Sprintf("update %s fail", rr.Name)) 68 | return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 60}, err 69 | } 70 | } 71 | 72 | // apply 操作 73 | err = mc.resourceApply(ctx, rr) 74 | if err != nil { 75 | mc.Logger.Error(err, "apply slice multi-cluster-resource: ", rr.GetName()+"/"+rr.GetNamespace(), " failed") 76 | mc.EventRecorder.Event(rr, corev1.EventTypeWarning, "ApplyFailed", fmt.Sprintf("resourceApply %s fail", rr.Name)) 77 | return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 60}, err 78 | } 79 | 80 | // patch 操作 81 | err = mc.resourcePatch(ctx, rr) 82 | if err != nil { 83 | mc.Logger.Error(err, "patch slice multi-cluster-resource: ", rr.GetName()+"/"+rr.GetNamespace(), " failed") 84 | mc.EventRecorder.Event(rr, corev1.EventTypeWarning, "PatchFailed", fmt.Sprintf("resourcePatch %s fail", rr.Name)) 85 | return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 60}, err 86 | } 87 | mc.Logger.Info("reconcile multi-cluster-resource: ", rr.GetName()+"/"+rr.GetNamespace(), " success") 88 | 89 | return reconcile.Result{}, nil 90 | } 91 | -------------------------------------------------------------------------------- /pkg/apis/multicluster/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by deepcopy-gen. DO NOT EDIT. 21 | 22 | package v1alpha1 23 | 24 | import ( 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { 30 | *out = *in 31 | return 32 | } 33 | 34 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSpec. 35 | func (in *ClusterSpec) DeepCopy() *ClusterSpec { 36 | if in == nil { 37 | return nil 38 | } 39 | out := new(ClusterSpec) 40 | in.DeepCopyInto(out) 41 | return out 42 | } 43 | 44 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 45 | func (in *ClusterStatus) DeepCopyInto(out *ClusterStatus) { 46 | *out = *in 47 | return 48 | } 49 | 50 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterStatus. 51 | func (in *ClusterStatus) DeepCopy() *ClusterStatus { 52 | if in == nil { 53 | return nil 54 | } 55 | out := new(ClusterStatus) 56 | in.DeepCopyInto(out) 57 | return out 58 | } 59 | 60 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 61 | func (in *MultiCluster) DeepCopyInto(out *MultiCluster) { 62 | *out = *in 63 | out.TypeMeta = in.TypeMeta 64 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 65 | out.Spec = in.Spec 66 | out.Status = in.Status 67 | return 68 | } 69 | 70 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MultiCluster. 71 | func (in *MultiCluster) DeepCopy() *MultiCluster { 72 | if in == nil { 73 | return nil 74 | } 75 | out := new(MultiCluster) 76 | in.DeepCopyInto(out) 77 | return out 78 | } 79 | 80 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 81 | func (in *MultiCluster) DeepCopyObject() runtime.Object { 82 | if c := in.DeepCopy(); c != nil { 83 | return c 84 | } 85 | return nil 86 | } 87 | 88 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 89 | func (in *MultiClusterList) DeepCopyInto(out *MultiClusterList) { 90 | *out = *in 91 | out.TypeMeta = in.TypeMeta 92 | in.ListMeta.DeepCopyInto(&out.ListMeta) 93 | if in.Items != nil { 94 | in, out := &in.Items, &out.Items 95 | *out = make([]MultiCluster, len(*in)) 96 | for i := range *in { 97 | (*in)[i].DeepCopyInto(&(*out)[i]) 98 | } 99 | } 100 | return 101 | } 102 | 103 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MultiClusterList. 104 | func (in *MultiClusterList) DeepCopy() *MultiClusterList { 105 | if in == nil { 106 | return nil 107 | } 108 | out := new(MultiClusterList) 109 | in.DeepCopyInto(out) 110 | return out 111 | } 112 | 113 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 114 | func (in *MultiClusterList) DeepCopyObject() runtime.Object { 115 | if c := in.DeepCopy(); c != nil { 116 | return c 117 | } 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /mysql/README.md: -------------------------------------------------------------------------------- 1 | ## mysql 库与表创建 2 | 3 | 本项目需要依赖 mysql 来存储。以下提供创建 mysql 库与表的介绍与参考。 4 | 5 | 1. 必须先准备一个 mysql 类型的服务,并且集群网络可达,并在 helm value.yaml 中设置 6 | 7 | 8 | ```bash 9 | [root@VM-0-16-centos yaml]# kubectl exec -it mydbconfig-controller-f484d984-wmr7q -- sh 10 | Defaulted container "mysqltest" out of: mysqltest, mydbconfig 11 | # mysql -uroot -p123456 12 | Welcome to the MariaDB monitor. Commands end with ; or \g. 13 | Your MariaDB connection id is 121 14 | Server version: 10.5.22-MariaDB-1:10.5.22+maria~ubu2004 mariadb.org binary distribution 15 | 16 | Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others. 17 | 18 | Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. 19 | MariaDB [(none)]> drop database resources; 20 | Query OK, 3 rows affected (0.006 sec) 21 | 22 | MariaDB [(none)]> show databases; 23 | +--------------------+ 24 | | Database | 25 | +--------------------+ 26 | | information_schema | 27 | | mysql | 28 | | performance_schema | 29 | +--------------------+ 30 | 3 rows in set (0.000 sec) 31 | 32 | MariaDB [(none)]> use resources; 33 | Database changed 34 | MariaDB [resources]> 35 | MariaDB [resources]> DROP TABLE IF EXISTS `resources`; 36 | Query OK, 0 rows affected, 1 warning (0.000 sec) 37 | 38 | MariaDB [resources]> CREATE TABLE `resources` ( 39 | -> `id` bigint(11) UNSIGNED NOT NULL AUTO_INCREMENT, 40 | -> `namespace` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL, 41 | -> `name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 42 | -> `cluster` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 43 | -> `group` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 44 | -> `version` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 45 | -> `resource` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 46 | -> `kind` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 47 | -> `resource_version` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL, 48 | -> `owner` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT 'owner uid', 49 | -> `uid` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 50 | -> `hash` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL, 51 | -> `object` json NULL, 52 | -> `create_at` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, 53 | -> `delete_at` timestamp(0) NULL DEFAULT NULL, 54 | -> `update_at` timestamp(0) NULL DEFAULT NULL, 55 | -> PRIMARY KEY (`id`) USING BTREE, 56 | -> UNIQUE INDEX `uid`(`uid`) USING BTREE 57 | -> ) ENGINE = MyISAM AUTO_INCREMENT = 0 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic; 58 | Query OK, 0 rows affected (0.004 sec) 59 | 60 | MariaDB [resources]> DROP TABLE IF EXISTS `clusters`; 61 | Query OK, 0 rows affected, 1 warning (0.000 sec) 62 | 63 | MariaDB [resources]> CREATE TABLE `clusters` ( 64 | -> `id` bigint(11) UNSIGNED NOT NULL AUTO_INCREMENT, 65 | -> `name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 66 | -> `isMaster` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 67 | -> `status` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, 68 | -> `create_at` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, 69 | -> PRIMARY KEY (`id`) USING BTREE, 70 | -> UNIQUE INDEX `name`(`name`) USING BTREE 71 | -> ) ENGINE = MyISAM AUTO_INCREMENT = 0 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic; 72 | Query OK, 0 rows affected (0.004 sec) 73 | 74 | MariaDB [resources]> 75 | ``` 76 | 77 | 78 | ```bash 79 | [root@VM-0-16-centos helm]# kubectl logs multi-cluster-operator-6c6fc9dcc9-xs79f 80 | F0129 10:02:52.587503 1 mysql_options.go:144] Failed to apply migrations: Dirty database version 1. Fix and force version. 81 | [root@VM-0-16-centos helm]# kubectl get pods 82 | 83 | 使用 84 | 85 | MariaDB [resources]> UPDATE schema_migrations SET dirty = 0; 86 | Query OK, 1 row affected (0.001 sec) 87 | Rows matched: 1 Changed: 1 Warnings: 0 88 | 89 | ``` -------------------------------------------------------------------------------- /cmd/ctl_plugin/common/request.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "k8s.io/klog/v2" 8 | "mime/multipart" 9 | "net/http" 10 | "os" 11 | ) 12 | 13 | var ( 14 | HttpClient *Http 15 | ServerPort int 16 | ServerIp string 17 | KubeConfigPath string 18 | ) 19 | 20 | func init() { 21 | HttpClient = &Http{ 22 | Client: http.DefaultClient, 23 | } 24 | Cluster = "" 25 | Name = "" 26 | } 27 | 28 | var ( 29 | Cluster string 30 | Name string 31 | File string 32 | ) 33 | 34 | type Http struct { 35 | Client *http.Client 36 | } 37 | 38 | // createFormFile 39 | // 将文件内容写入一个 multipart.Writer 中,并返回一个允许读取写入的文件内容的 io.Reader 对象。 40 | // 发送 HTTP 请求时,我们需要将文件内容作为请求体的一部分进行发送。 41 | func createFormFile(fieldName string, file *os.File) (io.Reader, *multipart.Writer) { 42 | bodyReader, bodyWriter := io.Pipe() 43 | writer := multipart.NewWriter(bodyWriter) 44 | 45 | go func() { 46 | defer bodyWriter.Close() 47 | 48 | part, err := writer.CreateFormFile(fieldName, file.Name()) 49 | if err != nil { 50 | bodyWriter.CloseWithError(err) 51 | return 52 | } 53 | 54 | _, err = io.Copy(part, file) 55 | if err != nil { 56 | bodyWriter.CloseWithError(err) 57 | return 58 | } 59 | 60 | writer.Close() 61 | }() 62 | 63 | return bodyReader, writer 64 | } 65 | 66 | func (c *Http) DoUploadFile(url string, queryParams map[string]string, headerParams map[string]string, filePath string) ([]byte, error) { 67 | klog.V(4).Info("post url: ", url, " queryParams: ", queryParams, "header: ", headerParams) 68 | 69 | // 打开文件 70 | file, err := os.Open(filePath) 71 | if err != nil { 72 | return nil, err 73 | } 74 | defer file.Close() 75 | 76 | body, writer := createFormFile("kubeconfig", file) 77 | 78 | // 创建 HTTP POST 请求 79 | req, err := http.NewRequest(http.MethodPost, url, body) 80 | if err != nil { 81 | klog.Errorf("failed to create POST request: %v\n", err) 82 | return nil, err 83 | } 84 | params := req.URL.Query() 85 | for k, v := range queryParams { 86 | params.Add(k, v) 87 | } 88 | contentType := writer.FormDataContentType() 89 | req.Header.Set("Content-Type", contentType) 90 | req.URL.RawQuery = params.Encode() 91 | 92 | // 遍历头部信息,并添加到请求头中 93 | for key, value := range headerParams { 94 | req.Header.Set(key, value) 95 | } 96 | 97 | resp, err := c.Client.Do(req) 98 | if err != nil { 99 | klog.Errorf("failed to send POST request: %v\n", err) 100 | return nil, err 101 | } 102 | defer resp.Body.Close() 103 | 104 | if resp.StatusCode != http.StatusOK { 105 | return nil, fmt.Errorf("HTTP response error: %s\n, error: %v\n", resp.Status, err) 106 | } 107 | 108 | responseBody, err := io.ReadAll(resp.Body) 109 | if err != nil { 110 | klog.Errorf("failed to read HTTP response body: %v\n", err) 111 | return nil, err 112 | } 113 | 114 | return responseBody, nil 115 | } 116 | 117 | func (c *Http) DoRequest(method string, url string, queryParams map[string]string, headerParams map[string]string, body []byte) ([]byte, error) { 118 | klog.V(4).Info(method, " url: ", url, " queryParams: ", queryParams) 119 | req, err := http.NewRequest(method, url, bytes.NewBuffer(body)) 120 | if err != nil { 121 | klog.Errorf("failed to create %s request: %v\n", method, err) 122 | return nil, err 123 | } 124 | params := req.URL.Query() 125 | for k, v := range queryParams { 126 | params.Add(k, v) 127 | } 128 | 129 | // 遍历头部信息,并添加到请求头中 130 | for key, value := range headerParams { 131 | req.Header.Set(key, value) 132 | } 133 | 134 | req.URL.RawQuery = params.Encode() 135 | 136 | resp, err := c.Client.Do(req) 137 | if err != nil { 138 | klog.Errorf("failed to send %s request: %v\n", method, err) 139 | return nil, err 140 | } 141 | defer resp.Body.Close() 142 | 143 | if resp.StatusCode != http.StatusOK { 144 | return nil, fmt.Errorf("HTTP response error: %s\n", resp.Status) 145 | } 146 | 147 | responseBody, err := io.ReadAll(resp.Body) 148 | if err != nil { 149 | klog.Errorf("failed to read HTTP response body: %v\n", err) 150 | return nil, err 151 | } 152 | 153 | return responseBody, nil 154 | } 155 | -------------------------------------------------------------------------------- /pkg/client/listers/multicluster/v1alpha1/multicluster.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by lister-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | v1alpha1 "github.com/myoperator/multiclusteroperator/pkg/apis/multicluster/v1alpha1" 23 | "k8s.io/apimachinery/pkg/api/errors" 24 | "k8s.io/apimachinery/pkg/labels" 25 | "k8s.io/client-go/tools/cache" 26 | ) 27 | 28 | // MultiClusterLister helps list MultiClusters. 29 | // All objects returned here must be treated as read-only. 30 | type MultiClusterLister interface { 31 | // List lists all MultiClusters in the indexer. 32 | // Objects returned here must be treated as read-only. 33 | List(selector labels.Selector) (ret []*v1alpha1.MultiCluster, err error) 34 | // MultiClusters returns an object that can list and get MultiClusters. 35 | MultiClusters(namespace string) MultiClusterNamespaceLister 36 | MultiClusterListerExpansion 37 | } 38 | 39 | // multiClusterLister implements the MultiClusterLister interface. 40 | type multiClusterLister struct { 41 | indexer cache.Indexer 42 | } 43 | 44 | // NewMultiClusterLister returns a new MultiClusterLister. 45 | func NewMultiClusterLister(indexer cache.Indexer) MultiClusterLister { 46 | return &multiClusterLister{indexer: indexer} 47 | } 48 | 49 | // List lists all MultiClusters in the indexer. 50 | func (s *multiClusterLister) List(selector labels.Selector) (ret []*v1alpha1.MultiCluster, err error) { 51 | err = cache.ListAll(s.indexer, selector, func(m interface{}) { 52 | ret = append(ret, m.(*v1alpha1.MultiCluster)) 53 | }) 54 | return ret, err 55 | } 56 | 57 | // MultiClusters returns an object that can list and get MultiClusters. 58 | func (s *multiClusterLister) MultiClusters(namespace string) MultiClusterNamespaceLister { 59 | return multiClusterNamespaceLister{indexer: s.indexer, namespace: namespace} 60 | } 61 | 62 | // MultiClusterNamespaceLister helps list and get MultiClusters. 63 | // All objects returned here must be treated as read-only. 64 | type MultiClusterNamespaceLister interface { 65 | // List lists all MultiClusters in the indexer for a given namespace. 66 | // Objects returned here must be treated as read-only. 67 | List(selector labels.Selector) (ret []*v1alpha1.MultiCluster, err error) 68 | // Get retrieves the MultiCluster from the indexer for a given namespace and name. 69 | // Objects returned here must be treated as read-only. 70 | Get(name string) (*v1alpha1.MultiCluster, error) 71 | MultiClusterNamespaceListerExpansion 72 | } 73 | 74 | // multiClusterNamespaceLister implements the MultiClusterNamespaceLister 75 | // interface. 76 | type multiClusterNamespaceLister struct { 77 | indexer cache.Indexer 78 | namespace string 79 | } 80 | 81 | // List lists all MultiClusters in the indexer for a given namespace. 82 | func (s multiClusterNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.MultiCluster, err error) { 83 | err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { 84 | ret = append(ret, m.(*v1alpha1.MultiCluster)) 85 | }) 86 | return ret, err 87 | } 88 | 89 | // Get retrieves the MultiCluster from the indexer for a given namespace and name. 90 | func (s multiClusterNamespaceLister) Get(name string) (*v1alpha1.MultiCluster, error) { 91 | obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) 92 | if err != nil { 93 | return nil, err 94 | } 95 | if !exists { 96 | return nil, errors.NewNotFound(v1alpha1.Resource("multicluster"), name) 97 | } 98 | return obj.(*v1alpha1.MultiCluster), nil 99 | } 100 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/multicluster/v1alpha1/multicluster.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | "context" 23 | time "time" 24 | 25 | multiclusterv1alpha1 "github.com/myoperator/multiclusteroperator/pkg/apis/multicluster/v1alpha1" 26 | versioned "github.com/myoperator/multiclusteroperator/pkg/client/clientset/versioned" 27 | internalinterfaces "github.com/myoperator/multiclusteroperator/pkg/client/informers/externalversions/internalinterfaces" 28 | v1alpha1 "github.com/myoperator/multiclusteroperator/pkg/client/listers/multicluster/v1alpha1" 29 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | runtime "k8s.io/apimachinery/pkg/runtime" 31 | watch "k8s.io/apimachinery/pkg/watch" 32 | cache "k8s.io/client-go/tools/cache" 33 | ) 34 | 35 | // MultiClusterInformer provides access to a shared informer and lister for 36 | // MultiClusters. 37 | type MultiClusterInformer interface { 38 | Informer() cache.SharedIndexInformer 39 | Lister() v1alpha1.MultiClusterLister 40 | } 41 | 42 | type multiClusterInformer struct { 43 | factory internalinterfaces.SharedInformerFactory 44 | tweakListOptions internalinterfaces.TweakListOptionsFunc 45 | namespace string 46 | } 47 | 48 | // NewMultiClusterInformer constructs a new informer for MultiCluster type. 49 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 50 | // one. This reduces memory footprint and number of connections to the server. 51 | func NewMultiClusterInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { 52 | return NewFilteredMultiClusterInformer(client, namespace, resyncPeriod, indexers, nil) 53 | } 54 | 55 | // NewFilteredMultiClusterInformer constructs a new informer for MultiCluster type. 56 | // Always prefer using an informer factory to get a shared informer instead of getting an independent 57 | // one. This reduces memory footprint and number of connections to the server. 58 | func NewFilteredMultiClusterInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { 59 | return cache.NewSharedIndexInformer( 60 | &cache.ListWatch{ 61 | ListFunc: func(options v1.ListOptions) (runtime.Object, error) { 62 | if tweakListOptions != nil { 63 | tweakListOptions(&options) 64 | } 65 | return client.MulitclusterV1alpha1().MultiClusters(namespace).List(context.TODO(), options) 66 | }, 67 | WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { 68 | if tweakListOptions != nil { 69 | tweakListOptions(&options) 70 | } 71 | return client.MulitclusterV1alpha1().MultiClusters(namespace).Watch(context.TODO(), options) 72 | }, 73 | }, 74 | &multiclusterv1alpha1.MultiCluster{}, 75 | resyncPeriod, 76 | indexers, 77 | ) 78 | } 79 | 80 | func (f *multiClusterInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { 81 | return NewFilteredMultiClusterInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) 82 | } 83 | 84 | func (f *multiClusterInformer) Informer() cache.SharedIndexInformer { 85 | return f.factory.InformerFor(&multiclusterv1alpha1.MultiCluster{}, f.defaultInformer) 86 | } 87 | 88 | func (f *multiClusterInformer) Lister() v1alpha1.MultiClusterLister { 89 | return v1alpha1.NewMultiClusterLister(f.Informer().GetIndexer()) 90 | } 91 | -------------------------------------------------------------------------------- /cmd/server/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "github.com/myoperator/multiclusteroperator/cmd/server/app/options" 6 | "github.com/myoperator/multiclusteroperator/pkg/leaselock" 7 | "github.com/myoperator/multiclusteroperator/pkg/multi_cluster_controller" 8 | "github.com/myoperator/multiclusteroperator/pkg/util" 9 | "github.com/spf13/cobra" 10 | clientset "k8s.io/client-go/kubernetes" 11 | "k8s.io/client-go/rest" 12 | "k8s.io/client-go/tools/leaderelection" 13 | cliflag "k8s.io/component-base/cli/flag" 14 | "k8s.io/component-base/term" 15 | "k8s.io/klog/v2" 16 | "os" 17 | "time" 18 | ) 19 | 20 | func NewServerCommand() *cobra.Command { 21 | opts := options.NewOptions() 22 | 23 | cmd := &cobra.Command{ 24 | Use: "multi-clusters-server", 25 | RunE: func(cmd *cobra.Command, args []string) error { 26 | cliflag.PrintFlags(cmd.Flags()) 27 | 28 | if err := opts.Complete(); err != nil { 29 | klog.Errorf("unable to complete options, %+v", err) 30 | return err 31 | } 32 | 33 | if err := opts.Validate(); err != nil { 34 | klog.Errorf("unable to validate options, %+v", err) 35 | return err 36 | } 37 | 38 | if err := leaderElectionRun(opts); err != nil { 39 | klog.Errorf("unable to run server, %+v", err) 40 | return err 41 | } 42 | 43 | return nil 44 | }, 45 | } 46 | 47 | fs := cmd.Flags() 48 | namedFlagSets := opts.Flags() 49 | for _, f := range namedFlagSets.FlagSets { 50 | fs.AddFlagSet(f) 51 | } 52 | 53 | cols, _, _ := term.TerminalSize(cmd.OutOrStdout()) 54 | cliflag.SetUsageAndHelpFunc(cmd, namedFlagSets, cols) 55 | 56 | return cmd 57 | } 58 | 59 | var podName = os.Getenv("POD_NAME") 60 | 61 | // leaderElectionRun 是否启动选举机制 62 | func leaderElectionRun(opts *options.Options) error { 63 | 64 | switch opts.Server.LeaseLockMode { 65 | case true: 66 | klog.Info("lead election mode run...") 67 | var config *rest.Config 68 | if opts.Server.LeaseLockMode { 69 | config, _ = rest.InClusterConfig() 70 | } 71 | 72 | client := clientset.NewForConfigOrDie(config) 73 | 74 | lock := leaselock.GetNewLock(opts.Server.LeaseLockName, podName, opts.Server.LeaseLockNamespace, client) 75 | // 选主模式 76 | leaderelection.RunOrDie(context.TODO(), leaderelection.LeaderElectionConfig{ 77 | Lock: lock, 78 | ReleaseOnCancel: true, 79 | LeaseDuration: 15 * time.Second, // 租约时长,follower用来判断集群锁是否过期 80 | RenewDeadline: 10 * time.Second, // leader更新锁的时长 81 | RetryPeriod: 2 * time.Second, // 重试获取锁的间隔 82 | // 当发生不同选主事件时的回调方法 83 | Callbacks: leaderelection.LeaderCallbacks{ 84 | // 成为leader时,需要执行的回调 85 | OnStartedLeading: func(c context.Context) { 86 | // 执行server逻辑 87 | klog.Info("leader election server running...") 88 | err := run(opts) 89 | if err != nil { 90 | return 91 | } 92 | }, 93 | // 不是leader时,需要执行的回调 94 | OnStoppedLeading: func() { 95 | klog.Info("no longer a leader...") 96 | klog.Info("clean up server...") 97 | }, 98 | // 当产生新leader时,执行的回调 99 | OnNewLeader: func(currentId string) { 100 | if currentId == podName { 101 | klog.Info("still the leader!") 102 | return 103 | } 104 | klog.Infof("new leader is %v", currentId) 105 | }, 106 | }, 107 | }) 108 | 109 | case false: 110 | klog.Info("normal run...") 111 | err := run(opts) 112 | if err != nil { 113 | return err 114 | } 115 | } 116 | return nil 117 | } 118 | 119 | // run 启动 http server + operator manager 120 | func run(opts *options.Options) error { 121 | // 1. 初始化 db 实例 122 | mysqlClient, err := opts.MySQL.NewClient() 123 | 124 | if err != nil { 125 | return err 126 | } 127 | 128 | // 2. 实例化 server 129 | server, err := opts.Server.NewServer() 130 | if err != nil { 131 | return err 132 | } 133 | 134 | // 3. 注入 db factory 135 | server.InjectStoreFactory(mysqlClient) 136 | 137 | ctx, cancel := context.WithCancel(context.Background()) 138 | defer cancel() 139 | 140 | mch, err := multi_cluster_controller.NewMultiClusterHandlerFromConfig(opts.Server.ConfigPath, mysqlClient.GetDB()) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | // 4. 启动多集群 handler 146 | mch.StartWorkQueueHandler(ctx) 147 | 148 | // 5. 启动 operator 管理器 149 | go func() { 150 | defer util.HandleCrash() 151 | klog.Info("operator manager start...") 152 | if err = mch.StartOperatorManager(); err != nil { 153 | klog.Fatal(err) 154 | } 155 | }() 156 | 157 | // 6. 启动 http server 158 | return server.Start(mysqlClient.GetDB()) 159 | } 160 | -------------------------------------------------------------------------------- /pkg/server/handler.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/myoperator/multiclusteroperator/pkg/server/service" 7 | "github.com/myoperator/multiclusteroperator/pkg/util" 8 | "io" 9 | "k8s.io/client-go/tools/clientcmd" 10 | "k8s.io/klog/v2" 11 | "net/http" 12 | "os" 13 | "strconv" 14 | ) 15 | 16 | type ResourceController struct { 17 | ListService *service.ListService 18 | JoinService *service.JoinService 19 | } 20 | 21 | // List 查询接口 22 | func (r *ResourceController) List(c *gin.Context) { 23 | // 接口传入参数 24 | gvrParam := c.Query("gvr") 25 | name := c.DefaultQuery("name", "") 26 | ns := c.DefaultQuery("namespace", "") 27 | limit := c.DefaultQuery("limit", "10") 28 | cluster := c.DefaultQuery("cluster", "") 29 | var labels map[string]string // 默认是nil 30 | if labelsQuery, ok := c.GetQueryMap("labels"); ok { 31 | labels = labelsQuery 32 | } 33 | 34 | // 解析 gvr 35 | gvr, _ := util.ParseIntoGvr(gvrParam, "/") 36 | if gvr.Empty() { 37 | c.JSON(http.StatusBadRequest, gin.H{"error": "not found gvr"}) 38 | return 39 | } 40 | ll, _ := strconv.Atoi(limit) 41 | // 获取列表 42 | list, err := r.ListService.List(name, ns, cluster, labels, gvr, ll) 43 | if err != nil { 44 | c.JSON(http.StatusBadRequest, gin.H{"list error": err}) 45 | return 46 | } 47 | c.JSON(http.StatusOK, list) 48 | return 49 | } 50 | 51 | // ListWrapWithCluster 包裹 clusterName 返回,目前默认给命令行工具使用 52 | func (r *ResourceController) ListWrapWithCluster(c *gin.Context) { 53 | // 接口传入参数 54 | gvrParam := c.Query("gvr") 55 | name := c.DefaultQuery("name", "") 56 | ns := c.DefaultQuery("namespace", "") 57 | limit := c.DefaultQuery("limit", "10") 58 | cluster := c.DefaultQuery("cluster", "") 59 | var labels map[string]string // 默认是nil 60 | if labelsQuery, ok := c.GetQueryMap("labels"); ok { 61 | labels = labelsQuery 62 | } 63 | 64 | // 解析 gvr 65 | gvr, _ := util.ParseIntoGvr(gvrParam, "/") 66 | if gvr.Empty() { 67 | c.JSON(http.StatusBadRequest, gin.H{"error": "not found gvr"}) 68 | return 69 | } 70 | ll, _ := strconv.Atoi(limit) 71 | // 获取列表 72 | list, err := r.ListService.ListWrapWithCluster(name, ns, cluster, labels, gvr, ll) 73 | if err != nil { 74 | c.JSON(http.StatusBadRequest, gin.H{"list wrap with cluster error": err}) 75 | return 76 | } 77 | c.JSON(http.StatusOK, list) 78 | 79 | } 80 | 81 | func (r *ResourceController) Join(c *gin.Context) { 82 | 83 | cluster := c.DefaultQuery("cluster", "") 84 | insecure := c.DefaultQuery("insecure", "true") 85 | 86 | // 检查请求方法是否为 POST 87 | if c.Request.Method != http.MethodPost { 88 | c.JSON(http.StatusMethodNotAllowed, gin.H{"error": "only support POST request"}) 89 | return 90 | } 91 | 92 | // 解析上传的文件 93 | file, _, err := c.Request.FormFile("kubeconfig") 94 | if err != nil { 95 | klog.Errorf("error: %v", err) 96 | c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("parse file from request error: %s", err)}) 97 | return 98 | } 99 | defer file.Close() 100 | 101 | // 创建临时文件用于保存上传的文件内容 102 | tempFile, err := os.CreateTemp("", "kubeconfig-") 103 | if err != nil { 104 | klog.Errorf("error: ", err) 105 | c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("create temp file error: %s", err)}) 106 | return 107 | } 108 | defer os.Remove(tempFile.Name()) 109 | defer tempFile.Close() 110 | 111 | // 将上传的文件内容写入临时文件 112 | _, err = io.Copy(tempFile, file) 113 | if err != nil { 114 | c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("io copy error: %s", err)}) 115 | return 116 | } 117 | 118 | // 使用 BuildConfigFromFlags 从上传的 kubeconfig 文件中获取配置 119 | config, err := clientcmd.BuildConfigFromFlags("", tempFile.Name()) 120 | if err != nil { 121 | c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("get k8s config error: %s", err)}) 122 | return 123 | } 124 | s, err := strconv.ParseBool(insecure) 125 | if err != nil { 126 | c.JSON(http.StatusBadRequest, gin.H{"error": err}) 127 | return 128 | } 129 | // 获取列表 130 | err = r.JoinService.Join(cluster, s, config) 131 | if err != nil { 132 | c.JSON(http.StatusBadRequest, gin.H{"join cluster error": err}) 133 | return 134 | } 135 | c.JSON(http.StatusOK, gin.H{"res": "join cluster successful"}) 136 | return 137 | } 138 | 139 | func (r *ResourceController) UnJoin(c *gin.Context) { 140 | cluster := c.DefaultQuery("cluster", "") 141 | 142 | // 检查请求方法是否为 DELETE 143 | if c.Request.Method != http.MethodDelete { 144 | c.JSON(http.StatusMethodNotAllowed, gin.H{"error": "only support DELETE request"}) 145 | return 146 | } 147 | 148 | // 获取列表 149 | err := r.JoinService.UnJoin(cluster) 150 | if err != nil { 151 | c.JSON(http.StatusBadRequest, gin.H{"unjoin cluster error": err}) 152 | return 153 | } 154 | c.JSON(http.StatusOK, gin.H{"res": "unjoin cluster successful"}) 155 | return 156 | } 157 | -------------------------------------------------------------------------------- /pkg/options/mysql/mysql_options.go: -------------------------------------------------------------------------------- 1 | package mysql 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "github.com/golang-migrate/migrate/v4" 7 | migratemysql "github.com/golang-migrate/migrate/v4/database/mysql" 8 | "github.com/myoperator/multiclusteroperator/pkg/store" 9 | mysqlstore "github.com/myoperator/multiclusteroperator/pkg/store/mysql" 10 | "github.com/myoperator/multiclusteroperator/pkg/util" 11 | "github.com/spf13/pflag" 12 | "gorm.io/driver/mysql" 13 | "gorm.io/gorm" 14 | "k8s.io/klog/v2" 15 | "log" 16 | "time" 17 | ) 18 | 19 | type MySQLOptions struct { 20 | Host string 21 | Username string 22 | Password string 23 | Database string 24 | MaxIdleConnections int 25 | MaxOpenConnections int 26 | MaxConnectionLifeTime time.Duration 27 | } 28 | 29 | func NewMySQLOptions() *MySQLOptions { 30 | return &MySQLOptions{} 31 | } 32 | 33 | func (o *MySQLOptions) AddFlags(fs *pflag.FlagSet) { 34 | fs.StringVar(&o.Host, "db-endpoint", "127.0.0.1:3306", 35 | "MySQL service host address and port. Default to 127.0.0.1:3306.") 36 | fs.StringVar(&o.Username, "db-user", "root", 37 | "Username for access to mysql service. Default to root.") 38 | fs.StringVar(&o.Password, "db-password", "1234567", 39 | "Password for access to mysql. Default to 1234567.") 40 | fs.StringVar(&o.Database, "db-database", "resources", 41 | "Database name for the server to use. Default to resources.") 42 | 43 | fs.IntVar(&o.MaxIdleConnections, "mysql-max-idle-connections", 100, 44 | "Maximum idle connections allowed to connect to mysql. Default to 100.") 45 | fs.IntVar(&o.MaxOpenConnections, "mysql-max-open-connections", 100, 46 | "Maximum open connections allowed to connect to mysql. Default to 100.") 47 | fs.DurationVar(&o.MaxConnectionLifeTime, "mysql-max-connection-life-time", time.Duration(10)*time.Second, 48 | "Maximum connection life time allowed to connect to mysql. Default to 10s.") 49 | } 50 | 51 | // Complete TODO: 实现赋值逻辑 52 | func (o *MySQLOptions) Complete() error { 53 | return nil 54 | } 55 | 56 | // Validate 验证逻辑 57 | func (o *MySQLOptions) Validate() []error { 58 | var errs []error 59 | // 验证 Host 字段是否为空 60 | if o.Host == "" { 61 | errs = append(errs, fmt.Errorf("MySQL host address is required")) 62 | } 63 | 64 | // 验证 Username 字段是否为空 65 | if o.Username == "" { 66 | errs = append(errs, fmt.Errorf("MySQL username is required")) 67 | } 68 | 69 | // 验证 Password 字段是否为空 70 | if o.Password == "" { 71 | errs = append(errs, fmt.Errorf("MySQL password is required")) 72 | } 73 | 74 | // 验证 Database 字段是否为空 75 | if o.Database == "" { 76 | errs = append(errs, fmt.Errorf("MySQL database name is required")) 77 | } 78 | 79 | // 验证 MaxIdleConnections 字段是否为正数 80 | if o.MaxIdleConnections <= 0 { 81 | errs = append(errs, fmt.Errorf("MySQL max idle connections must be a positive number")) 82 | } 83 | 84 | // 验证 MaxOpenConnections 字段是否为正数 85 | if o.MaxOpenConnections <= 0 { 86 | errs = append(errs, fmt.Errorf("MySQL max open connections must be a positive number")) 87 | } 88 | 89 | // 验证 MaxConnectionLifeTime 字段是否为正数 90 | if o.MaxConnectionLifeTime <= 0 { 91 | errs = append(errs, fmt.Errorf("MySQL max connection life time must be a positive duration")) 92 | } 93 | return errs 94 | } 95 | 96 | var GlobalDB *gorm.DB 97 | 98 | // NewClient 创建 mysql 客户端并进行 migrate 99 | func (o *MySQLOptions) NewClient() (store.Factory, error) { 100 | dsn := fmt.Sprintf(`%s:%s@tcp(%s)/%s?charset=utf8&parseTime=%t&loc=%s`, 101 | o.Username, 102 | o.Password, 103 | o.Host, 104 | o.Database, 105 | true, 106 | "Local", 107 | ) 108 | db, err := gorm.Open(mysql.Open(dsn)) 109 | if err != nil { 110 | return nil, err 111 | } 112 | // 113 | GlobalDB = db 114 | 115 | sqlDB, err := db.DB() 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | // SetMaxOpenConns sets the maximum number of open connections to the database. 121 | sqlDB.SetMaxOpenConns(o.MaxOpenConnections) 122 | // SetConnMaxLifetime sets the maximum amount of time a connection may be reused. 123 | sqlDB.SetConnMaxLifetime(o.MaxConnectionLifeTime) 124 | // SetMaxIdleConns sets the maximum number of connections in the idle connection pool. 125 | sqlDB.SetMaxIdleConns(o.MaxIdleConnections) 126 | 127 | // 执行 migrate 128 | db1, _ := sql.Open("mysql", dsn) 129 | // 关闭数据库连接 130 | defer db1.Close() 131 | driver, err := migratemysql.WithInstance(db1, &migratemysql.Config{}) 132 | if err != nil { 133 | log.Fatal(err) 134 | } 135 | 136 | m, err := migrate.NewWithDatabaseInstance( 137 | fmt.Sprintf("file://%s/migrations", util.GetWd()), 138 | "mysql", 139 | driver, 140 | ) 141 | if err != nil { 142 | klog.Fatal(err) 143 | } 144 | 145 | err = m.Force(1) 146 | if err != nil { 147 | // 处理错误 148 | klog.Fatalf("Error forcing database version:", err) 149 | } 150 | 151 | // 执行数据库迁移 152 | err = m.Up() 153 | if err != nil && err != migrate.ErrNoChange { 154 | klog.Fatalf("Failed to apply migrations: %v", err) 155 | } 156 | 157 | // 获取当前数据库迁移版本 158 | version, _, err := m.Version() 159 | if err != nil && err != migrate.ErrNilVersion { 160 | klog.Fatalf("Failed to get migration version: %v", err) 161 | } 162 | 163 | klog.Infof("Applied migrations up to version: %v\n", version) 164 | 165 | return mysqlstore.NewStoreFactory(db) 166 | } 167 | -------------------------------------------------------------------------------- /pkg/multi_cluster_controller/init_resource.go: -------------------------------------------------------------------------------- 1 | package multi_cluster_controller 2 | 3 | import ( 4 | "context" 5 | v1alpha12 "github.com/myoperator/multiclusteroperator/pkg/apis/multicluster/v1alpha1" 6 | "github.com/myoperator/multiclusteroperator/pkg/apis/multiclusterresource/v1alpha1" 7 | "k8s.io/apimachinery/pkg/runtime/schema" 8 | "k8s.io/apimachinery/pkg/util/yaml" 9 | "k8s.io/client-go/discovery" 10 | "k8s.io/client-go/kubernetes" 11 | "k8s.io/client-go/rest" 12 | "k8s.io/klog/v2" 13 | ) 14 | 15 | const ResourceCRD = ` 16 | apiVersion: apiextensions.k8s.io/v1 17 | kind: CustomResourceDefinition 18 | metadata: 19 | # 名字必需与下面的 spec group字段匹配,并且格式为 '<名称的复数形式>.<组名>' 20 | name: multiclusterresources.mulitcluster.practice.com 21 | labels: 22 | version: "0.1" 23 | spec: 24 | group: mulitcluster.practice.com 25 | versions: 26 | - name: v1alpha1 27 | # 是否有效 28 | served: true 29 | #是否是存储版本 30 | storage: true 31 | schema: 32 | openAPIV3Schema: 33 | type: object 34 | #没有任何内容会被修剪,哪怕不被识别 35 | x-kubernetes-preserve-unknown-fields: true 36 | subresources: 37 | status: {} 38 | names: 39 | # 复数名 40 | plural: multiclusterresources 41 | # 单数名 42 | singular: multiclusterresource 43 | kind: MultiClusterResource 44 | listKind: MultiClusterResourceList 45 | # kind的简称 46 | shortNames: 47 | - rr 48 | scope: Namespaced 49 | ` 50 | 51 | const ClusterCRD = ` 52 | apiVersion: apiextensions.k8s.io/v1 53 | kind: CustomResourceDefinition 54 | metadata: 55 | # 名字必需与下面的 spec group字段匹配,并且格式为 '<名称的复数形式>.<组名>' 56 | name: multiclusters.mulitcluster.practice.com 57 | labels: 58 | version: "0.1" 59 | spec: 60 | group: mulitcluster.practice.com 61 | versions: 62 | - name: v1alpha1 63 | # 是否有效 64 | served: true 65 | #是否是存储版本 66 | storage: true 67 | additionalPrinterColumns: 68 | - name: Version 69 | type: string 70 | jsonPath: .spec.version 71 | - name: Host 72 | type: string 73 | jsonPath: .spec.host 74 | - name: Platform 75 | type: string 76 | jsonPath: .spec.platform 77 | - name: IsMaster 78 | type: string 79 | jsonPath: .spec.isMaster 80 | - name: Status 81 | type: string 82 | jsonPath: .status.status 83 | - name: Age 84 | type: date 85 | jsonPath: .metadata.creationTimestamp 86 | schema: 87 | openAPIV3Schema: 88 | type: object 89 | #没有任何内容会被修剪,哪怕不被识别 90 | x-kubernetes-preserve-unknown-fields: true 91 | subresources: 92 | status: {} 93 | names: 94 | # 复数名 95 | plural: multiclusters 96 | # 单数名 97 | singular: multicluster 98 | kind: MultiCluster 99 | listKind: MultiClusterList 100 | # kind的简称 101 | shortNames: 102 | - cl 103 | scope: Namespaced 104 | ` 105 | 106 | var ( 107 | DefaultClientSet kubernetes.Interface 108 | DefaultRestConfig *rest.Config 109 | ) 110 | 111 | // CRDsInstalled checks if the CRDs are installed or not 112 | func checkCRDsInstalled(discovery discovery.DiscoveryInterface) bool { 113 | gvs := []schema.GroupVersionKind{ 114 | v1alpha1.SchemeGroupVersion.WithKind(v1alpha1.MultiClusterResourceKind), 115 | v1alpha1.SchemeGroupVersion.WithKind(v1alpha12.MultiClusterKind), 116 | } 117 | 118 | for _, gv := range gvs { 119 | if !isCRDInstalled(discovery, gv) { 120 | return false 121 | } 122 | } 123 | 124 | return true 125 | } 126 | 127 | func isCRDInstalled(discovery discovery.DiscoveryInterface, gvk schema.GroupVersionKind) bool { 128 | crdList, err := discovery.ServerResourcesForGroupVersion(gvk.GroupVersion().String()) 129 | if err != nil { 130 | klog.ErrorS(err, "resource not found", "resource", gvk) 131 | return false 132 | } 133 | 134 | for _, crd := range crdList.APIResources { 135 | if crd.Kind == gvk.Kind { 136 | klog.InfoS("resource CRD not found", "resource", crd.Kind) 137 | return true 138 | } 139 | } 140 | return false 141 | } 142 | 143 | func getMasterClusterClient() kubernetes.Interface { 144 | client, err := kubernetes.NewForConfig(DefaultRestConfig) 145 | if err != nil { 146 | klog.Fatal(err) 147 | } 148 | return client 149 | } 150 | 151 | // applyCrdToMasterCluster 在主集群中 apply CRD 152 | // 目前将两个 CRD 全都内置到主集群中 153 | func (mc *MultiClusterHandler) applyCrdToMasterClusterOrDie() { 154 | if mc.MasterCluster == "" { 155 | klog.Fatal("masterCluster is empty") 156 | } 157 | 158 | DefaultRestConfig = mc.RestConfigMap[mc.MasterCluster] 159 | DefaultClientSet = getMasterClusterClient() 160 | 161 | // check crd resource 162 | if checkCRDsInstalled(DefaultClientSet.Discovery()) { 163 | return 164 | } 165 | 166 | // apply 第一个 167 | jsonBytes, err := yaml.ToJSON([]byte(ResourceCRD)) 168 | if err != nil { 169 | klog.Fatal(err) 170 | } 171 | 172 | err = mc.KubectlClientMap[mc.MasterCluster].Apply(context.Background(), jsonBytes) 173 | if err != nil { 174 | klog.Fatal(err) 175 | } 176 | 177 | // apply 第二个 178 | cjsonBytes, err := yaml.ToJSON([]byte(ClusterCRD)) 179 | if err != nil { 180 | klog.Fatal(err) 181 | } 182 | 183 | err = mc.KubectlClientMap[mc.MasterCluster].Apply(context.Background(), cjsonBytes) 184 | if err != nil { 185 | klog.Fatal(err) 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /pkg/apis/multiclusterresource/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | //go:build !ignore_autogenerated 2 | // +build !ignore_autogenerated 3 | 4 | /* 5 | Copyright The Kubernetes Authors. 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | */ 19 | 20 | // Code generated by deepcopy-gen. DO NOT EDIT. 21 | 22 | package v1alpha1 23 | 24 | import ( 25 | runtime "k8s.io/apimachinery/pkg/runtime" 26 | ) 27 | 28 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 29 | func (in *Action) DeepCopyInto(out *Action) { 30 | *out = *in 31 | return 32 | } 33 | 34 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Action. 35 | func (in *Action) DeepCopy() *Action { 36 | if in == nil { 37 | return nil 38 | } 39 | out := new(Action) 40 | in.DeepCopyInto(out) 41 | return out 42 | } 43 | 44 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 45 | func (in *Cluster) DeepCopyInto(out *Cluster) { 46 | *out = *in 47 | out.Action = in.Action 48 | return 49 | } 50 | 51 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Cluster. 52 | func (in *Cluster) DeepCopy() *Cluster { 53 | if in == nil { 54 | return nil 55 | } 56 | out := new(Cluster) 57 | in.DeepCopyInto(out) 58 | return out 59 | } 60 | 61 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 62 | func (in *Customize) DeepCopyInto(out *Customize) { 63 | *out = *in 64 | if in.Clusters != nil { 65 | in, out := &in.Clusters, &out.Clusters 66 | *out = make([]Cluster, len(*in)) 67 | copy(*out, *in) 68 | } 69 | return 70 | } 71 | 72 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Customize. 73 | func (in *Customize) DeepCopy() *Customize { 74 | if in == nil { 75 | return nil 76 | } 77 | out := new(Customize) 78 | in.DeepCopyInto(out) 79 | return out 80 | } 81 | 82 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataTemplate. 83 | func (in DataTemplate) DeepCopy() DataTemplate { 84 | if in == nil { 85 | return nil 86 | } 87 | out := new(DataTemplate) 88 | in.DeepCopyInto(out) 89 | return *out 90 | } 91 | 92 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 93 | func (in *MultiClusterResource) DeepCopyInto(out *MultiClusterResource) { 94 | *out = *in 95 | out.TypeMeta = in.TypeMeta 96 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 97 | in.Spec.DeepCopyInto(&out.Spec) 98 | out.Status = in.Status.DeepCopy() 99 | return 100 | } 101 | 102 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MultiClusterResource. 103 | func (in *MultiClusterResource) DeepCopy() *MultiClusterResource { 104 | if in == nil { 105 | return nil 106 | } 107 | out := new(MultiClusterResource) 108 | in.DeepCopyInto(out) 109 | return out 110 | } 111 | 112 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 113 | func (in *MultiClusterResource) DeepCopyObject() runtime.Object { 114 | if c := in.DeepCopy(); c != nil { 115 | return c 116 | } 117 | return nil 118 | } 119 | 120 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 121 | func (in *MultiClusterResourceList) DeepCopyInto(out *MultiClusterResourceList) { 122 | *out = *in 123 | out.TypeMeta = in.TypeMeta 124 | in.ListMeta.DeepCopyInto(&out.ListMeta) 125 | if in.Items != nil { 126 | in, out := &in.Items, &out.Items 127 | *out = make([]MultiClusterResource, len(*in)) 128 | for i := range *in { 129 | (*in)[i].DeepCopyInto(&(*out)[i]) 130 | } 131 | } 132 | return 133 | } 134 | 135 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MultiClusterResourceList. 136 | func (in *MultiClusterResourceList) DeepCopy() *MultiClusterResourceList { 137 | if in == nil { 138 | return nil 139 | } 140 | out := new(MultiClusterResourceList) 141 | in.DeepCopyInto(out) 142 | return out 143 | } 144 | 145 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 146 | func (in *MultiClusterResourceList) DeepCopyObject() runtime.Object { 147 | if c := in.DeepCopy(); c != nil { 148 | return c 149 | } 150 | return nil 151 | } 152 | 153 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 154 | func (in *ResourceSpec) DeepCopyInto(out *ResourceSpec) { 155 | *out = *in 156 | out.Template = in.Template.DeepCopy() 157 | out.Placement = in.Placement.DeepCopy() 158 | in.Customize.DeepCopyInto(&out.Customize) 159 | return 160 | } 161 | 162 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceSpec. 163 | func (in *ResourceSpec) DeepCopy() *ResourceSpec { 164 | if in == nil { 165 | return nil 166 | } 167 | out := new(ResourceSpec) 168 | in.DeepCopyInto(out) 169 | return out 170 | } 171 | 172 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StatusTemplate. 173 | func (in StatusTemplate) DeepCopy() StatusTemplate { 174 | if in == nil { 175 | return nil 176 | } 177 | out := new(StatusTemplate) 178 | in.DeepCopyInto(out) 179 | return *out 180 | } 181 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/multicluster/v1alpha1/fake/fake_multicluster.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package fake 20 | 21 | import ( 22 | "context" 23 | 24 | v1alpha1 "github.com/myoperator/multiclusteroperator/pkg/apis/multicluster/v1alpha1" 25 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | labels "k8s.io/apimachinery/pkg/labels" 27 | schema "k8s.io/apimachinery/pkg/runtime/schema" 28 | types "k8s.io/apimachinery/pkg/types" 29 | watch "k8s.io/apimachinery/pkg/watch" 30 | testing "k8s.io/client-go/testing" 31 | ) 32 | 33 | // FakeMultiClusters implements MultiClusterInterface 34 | type FakeMultiClusters struct { 35 | Fake *FakeMulitclusterV1alpha1 36 | ns string 37 | } 38 | 39 | var multiclustersResource = schema.GroupVersionResource{Group: "mulitcluster.practice.com", Version: "v1alpha1", Resource: "multiclusters"} 40 | 41 | var multiclustersKind = schema.GroupVersionKind{Group: "mulitcluster.practice.com", Version: "v1alpha1", Kind: "MultiCluster"} 42 | 43 | // Get takes name of the multiCluster, and returns the corresponding multiCluster object, and an error if there is any. 44 | func (c *FakeMultiClusters) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.MultiCluster, err error) { 45 | obj, err := c.Fake. 46 | Invokes(testing.NewGetAction(multiclustersResource, c.ns, name), &v1alpha1.MultiCluster{}) 47 | 48 | if obj == nil { 49 | return nil, err 50 | } 51 | return obj.(*v1alpha1.MultiCluster), err 52 | } 53 | 54 | // List takes label and field selectors, and returns the list of MultiClusters that match those selectors. 55 | func (c *FakeMultiClusters) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.MultiClusterList, err error) { 56 | obj, err := c.Fake. 57 | Invokes(testing.NewListAction(multiclustersResource, multiclustersKind, c.ns, opts), &v1alpha1.MultiClusterList{}) 58 | 59 | if obj == nil { 60 | return nil, err 61 | } 62 | 63 | label, _, _ := testing.ExtractFromListOptions(opts) 64 | if label == nil { 65 | label = labels.Everything() 66 | } 67 | list := &v1alpha1.MultiClusterList{ListMeta: obj.(*v1alpha1.MultiClusterList).ListMeta} 68 | for _, item := range obj.(*v1alpha1.MultiClusterList).Items { 69 | if label.Matches(labels.Set(item.Labels)) { 70 | list.Items = append(list.Items, item) 71 | } 72 | } 73 | return list, err 74 | } 75 | 76 | // Watch returns a watch.Interface that watches the requested multiClusters. 77 | func (c *FakeMultiClusters) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { 78 | return c.Fake. 79 | InvokesWatch(testing.NewWatchAction(multiclustersResource, c.ns, opts)) 80 | 81 | } 82 | 83 | // Create takes the representation of a multiCluster and creates it. Returns the server's representation of the multiCluster, and an error, if there is any. 84 | func (c *FakeMultiClusters) Create(ctx context.Context, multiCluster *v1alpha1.MultiCluster, opts v1.CreateOptions) (result *v1alpha1.MultiCluster, err error) { 85 | obj, err := c.Fake. 86 | Invokes(testing.NewCreateAction(multiclustersResource, c.ns, multiCluster), &v1alpha1.MultiCluster{}) 87 | 88 | if obj == nil { 89 | return nil, err 90 | } 91 | return obj.(*v1alpha1.MultiCluster), err 92 | } 93 | 94 | // Update takes the representation of a multiCluster and updates it. Returns the server's representation of the multiCluster, and an error, if there is any. 95 | func (c *FakeMultiClusters) Update(ctx context.Context, multiCluster *v1alpha1.MultiCluster, opts v1.UpdateOptions) (result *v1alpha1.MultiCluster, err error) { 96 | obj, err := c.Fake. 97 | Invokes(testing.NewUpdateAction(multiclustersResource, c.ns, multiCluster), &v1alpha1.MultiCluster{}) 98 | 99 | if obj == nil { 100 | return nil, err 101 | } 102 | return obj.(*v1alpha1.MultiCluster), err 103 | } 104 | 105 | // UpdateStatus was generated because the type contains a Status member. 106 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 107 | func (c *FakeMultiClusters) UpdateStatus(ctx context.Context, multiCluster *v1alpha1.MultiCluster, opts v1.UpdateOptions) (*v1alpha1.MultiCluster, error) { 108 | obj, err := c.Fake. 109 | Invokes(testing.NewUpdateSubresourceAction(multiclustersResource, "status", c.ns, multiCluster), &v1alpha1.MultiCluster{}) 110 | 111 | if obj == nil { 112 | return nil, err 113 | } 114 | return obj.(*v1alpha1.MultiCluster), err 115 | } 116 | 117 | // Delete takes name of the multiCluster and deletes it. Returns an error if one occurs. 118 | func (c *FakeMultiClusters) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { 119 | _, err := c.Fake. 120 | Invokes(testing.NewDeleteAction(multiclustersResource, c.ns, name), &v1alpha1.MultiCluster{}) 121 | 122 | return err 123 | } 124 | 125 | // DeleteCollection deletes a collection of objects. 126 | func (c *FakeMultiClusters) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { 127 | action := testing.NewDeleteCollectionAction(multiclustersResource, c.ns, listOpts) 128 | 129 | _, err := c.Fake.Invokes(action, &v1alpha1.MultiClusterList{}) 130 | return err 131 | } 132 | 133 | // Patch applies the patch and returns the patched multiCluster. 134 | func (c *FakeMultiClusters) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.MultiCluster, err error) { 135 | obj, err := c.Fake. 136 | Invokes(testing.NewPatchSubresourceAction(multiclustersResource, c.ns, name, pt, data, subresources...), &v1alpha1.MultiCluster{}) 137 | 138 | if obj == nil { 139 | return nil, err 140 | } 141 | return obj.(*v1alpha1.MultiCluster), err 142 | } 143 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/myoperator/multiclusteroperator 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/ghodss/yaml v1.0.0 7 | github.com/gin-gonic/gin v1.9.1 8 | github.com/go-logr/logr v1.2.4 9 | github.com/go-sql-driver/mysql v1.7.0 10 | github.com/go-yaml/yaml v2.1.0+incompatible 11 | github.com/goccy/go-json v0.10.2 12 | github.com/golang-migrate/migrate/v4 v4.16.2 13 | github.com/olekukonko/tablewriter v0.0.5 14 | github.com/pkg/errors v0.9.1 15 | github.com/smartystreets/goconvey v1.8.1 16 | github.com/spf13/cobra v1.7.0 17 | github.com/spf13/pflag v1.0.5 18 | gopkg.in/yaml.v2 v2.4.0 19 | gorm.io/driver/mysql v1.5.1 20 | gorm.io/gorm v1.25.4 21 | k8s.io/api v0.28.2 22 | k8s.io/apimachinery v0.28.2 23 | k8s.io/cli-runtime v0.28.2 24 | k8s.io/client-go v0.28.2 25 | k8s.io/component-base v0.28.2 26 | k8s.io/klog/v2 v2.100.1 27 | k8s.io/kubectl v0.28.2 28 | sigs.k8s.io/controller-runtime v0.16.2 29 | sigs.k8s.io/yaml v1.3.0 30 | ) 31 | 32 | require ( 33 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 34 | github.com/MakeNowJust/heredoc v1.0.0 // indirect 35 | github.com/beorn7/perks v1.0.1 // indirect 36 | github.com/blang/semver/v4 v4.0.0 // indirect 37 | github.com/bytedance/sonic v1.9.1 // indirect 38 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 39 | github.com/chai2010/gettext-go v1.0.2 // indirect 40 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 41 | github.com/davecgh/go-spew v1.1.1 // indirect 42 | github.com/emicklei/go-restful/v3 v3.9.0 // indirect 43 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 44 | github.com/evanphx/json-patch/v5 v5.6.0 // indirect 45 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect 46 | github.com/fatih/camelcase v1.0.0 // indirect 47 | github.com/fsnotify/fsnotify v1.6.0 // indirect 48 | github.com/fvbommel/sortorder v1.1.0 // indirect 49 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 50 | github.com/gin-contrib/sse v0.1.0 // indirect 51 | github.com/go-errors/errors v1.4.2 // indirect 52 | github.com/go-logr/zapr v1.2.4 // indirect 53 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 54 | github.com/go-openapi/jsonreference v0.20.2 // indirect 55 | github.com/go-openapi/swag v0.22.3 // indirect 56 | github.com/go-playground/locales v0.14.1 // indirect 57 | github.com/go-playground/universal-translator v0.18.1 // indirect 58 | github.com/go-playground/validator/v10 v10.14.0 // indirect 59 | github.com/gogo/protobuf v1.3.2 // indirect 60 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 61 | github.com/golang/protobuf v1.5.3 // indirect 62 | github.com/google/btree v1.0.1 // indirect 63 | github.com/google/gnostic-models v0.6.8 // indirect 64 | github.com/google/go-cmp v0.5.9 // indirect 65 | github.com/google/gofuzz v1.2.0 // indirect 66 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 67 | github.com/google/uuid v1.3.0 // indirect 68 | github.com/gopherjs/gopherjs v1.17.2 // indirect 69 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect 70 | github.com/hashicorp/errwrap v1.1.0 // indirect 71 | github.com/hashicorp/go-multierror v1.1.1 // indirect 72 | github.com/imdario/mergo v0.3.6 // indirect 73 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 74 | github.com/jinzhu/inflection v1.0.0 // indirect 75 | github.com/jinzhu/now v1.1.5 // indirect 76 | github.com/jonboulle/clockwork v0.2.2 // indirect 77 | github.com/josharian/intern v1.0.0 // indirect 78 | github.com/json-iterator/go v1.1.12 // indirect 79 | github.com/jtolds/gls v4.20.0+incompatible // indirect 80 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 81 | github.com/leodido/go-urn v1.2.4 // indirect 82 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 83 | github.com/mailru/easyjson v0.7.7 // indirect 84 | github.com/mattn/go-isatty v0.0.19 // indirect 85 | github.com/mattn/go-runewidth v0.0.9 // indirect 86 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 87 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 88 | github.com/moby/spdystream v0.2.0 // indirect 89 | github.com/moby/term v0.5.0 // indirect 90 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 91 | github.com/modern-go/reflect2 v1.0.2 // indirect 92 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 93 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 94 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 95 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 96 | github.com/prometheus/client_golang v1.16.0 // indirect 97 | github.com/prometheus/client_model v0.4.0 // indirect 98 | github.com/prometheus/common v0.44.0 // indirect 99 | github.com/prometheus/procfs v0.10.1 // indirect 100 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 101 | github.com/smarty/assertions v1.15.0 // indirect 102 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 103 | github.com/ugorji/go/codec v1.2.11 // indirect 104 | github.com/xlab/treeprint v1.2.0 // indirect 105 | go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect 106 | go.uber.org/atomic v1.10.0 // indirect 107 | go.uber.org/multierr v1.11.0 // indirect 108 | go.uber.org/zap v1.25.0 // indirect 109 | golang.org/x/arch v0.3.0 // indirect 110 | golang.org/x/crypto v0.11.0 // indirect 111 | golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect 112 | golang.org/x/net v0.13.0 // indirect 113 | golang.org/x/oauth2 v0.8.0 // indirect 114 | golang.org/x/sync v0.2.0 // indirect 115 | golang.org/x/sys v0.11.0 // indirect 116 | golang.org/x/term v0.10.0 // indirect 117 | golang.org/x/text v0.11.0 // indirect 118 | golang.org/x/time v0.3.0 // indirect 119 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 120 | google.golang.org/appengine v1.6.7 // indirect 121 | google.golang.org/protobuf v1.30.0 // indirect 122 | gopkg.in/inf.v0 v0.9.1 // indirect 123 | gopkg.in/yaml.v3 v3.0.1 // indirect 124 | k8s.io/apiextensions-apiserver v0.28.0 // indirect 125 | k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect 126 | k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect 127 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 128 | sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect 129 | sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect 130 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 131 | ) 132 | -------------------------------------------------------------------------------- /pkg/client/informers/externalversions/factory.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by informer-gen. DO NOT EDIT. 18 | 19 | package externalversions 20 | 21 | import ( 22 | reflect "reflect" 23 | sync "sync" 24 | time "time" 25 | 26 | versioned "github.com/myoperator/multiclusteroperator/pkg/client/clientset/versioned" 27 | internalinterfaces "github.com/myoperator/multiclusteroperator/pkg/client/informers/externalversions/internalinterfaces" 28 | multicluster "github.com/myoperator/multiclusteroperator/pkg/client/informers/externalversions/multicluster" 29 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | runtime "k8s.io/apimachinery/pkg/runtime" 31 | schema "k8s.io/apimachinery/pkg/runtime/schema" 32 | cache "k8s.io/client-go/tools/cache" 33 | ) 34 | 35 | // SharedInformerOption defines the functional option type for SharedInformerFactory. 36 | type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory 37 | 38 | type sharedInformerFactory struct { 39 | client versioned.Interface 40 | namespace string 41 | tweakListOptions internalinterfaces.TweakListOptionsFunc 42 | lock sync.Mutex 43 | defaultResync time.Duration 44 | customResync map[reflect.Type]time.Duration 45 | 46 | informers map[reflect.Type]cache.SharedIndexInformer 47 | // startedInformers is used for tracking which informers have been started. 48 | // This allows Start() to be called multiple times safely. 49 | startedInformers map[reflect.Type]bool 50 | } 51 | 52 | // WithCustomResyncConfig sets a custom resync period for the specified informer types. 53 | func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { 54 | return func(factory *sharedInformerFactory) *sharedInformerFactory { 55 | for k, v := range resyncConfig { 56 | factory.customResync[reflect.TypeOf(k)] = v 57 | } 58 | return factory 59 | } 60 | } 61 | 62 | // WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. 63 | func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { 64 | return func(factory *sharedInformerFactory) *sharedInformerFactory { 65 | factory.tweakListOptions = tweakListOptions 66 | return factory 67 | } 68 | } 69 | 70 | // WithNamespace limits the SharedInformerFactory to the specified namespace. 71 | func WithNamespace(namespace string) SharedInformerOption { 72 | return func(factory *sharedInformerFactory) *sharedInformerFactory { 73 | factory.namespace = namespace 74 | return factory 75 | } 76 | } 77 | 78 | // NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. 79 | func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { 80 | return NewSharedInformerFactoryWithOptions(client, defaultResync) 81 | } 82 | 83 | // NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. 84 | // Listers obtained via this SharedInformerFactory will be subject to the same filters 85 | // as specified here. 86 | // Deprecated: Please use NewSharedInformerFactoryWithOptions instead 87 | func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { 88 | return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) 89 | } 90 | 91 | // NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. 92 | func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { 93 | factory := &sharedInformerFactory{ 94 | client: client, 95 | namespace: v1.NamespaceAll, 96 | defaultResync: defaultResync, 97 | informers: make(map[reflect.Type]cache.SharedIndexInformer), 98 | startedInformers: make(map[reflect.Type]bool), 99 | customResync: make(map[reflect.Type]time.Duration), 100 | } 101 | 102 | // Apply all options 103 | for _, opt := range options { 104 | factory = opt(factory) 105 | } 106 | 107 | return factory 108 | } 109 | 110 | // Start initializes all requested informers. 111 | func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { 112 | f.lock.Lock() 113 | defer f.lock.Unlock() 114 | 115 | for informerType, informer := range f.informers { 116 | if !f.startedInformers[informerType] { 117 | go informer.Run(stopCh) 118 | f.startedInformers[informerType] = true 119 | } 120 | } 121 | } 122 | 123 | // WaitForCacheSync waits for all started informers' cache were synced. 124 | func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { 125 | informers := func() map[reflect.Type]cache.SharedIndexInformer { 126 | f.lock.Lock() 127 | defer f.lock.Unlock() 128 | 129 | informers := map[reflect.Type]cache.SharedIndexInformer{} 130 | for informerType, informer := range f.informers { 131 | if f.startedInformers[informerType] { 132 | informers[informerType] = informer 133 | } 134 | } 135 | return informers 136 | }() 137 | 138 | res := map[reflect.Type]bool{} 139 | for informType, informer := range informers { 140 | res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) 141 | } 142 | return res 143 | } 144 | 145 | // InternalInformerFor returns the SharedIndexInformer for obj using an internal 146 | // client. 147 | func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { 148 | f.lock.Lock() 149 | defer f.lock.Unlock() 150 | 151 | informerType := reflect.TypeOf(obj) 152 | informer, exists := f.informers[informerType] 153 | if exists { 154 | return informer 155 | } 156 | 157 | resyncPeriod, exists := f.customResync[informerType] 158 | if !exists { 159 | resyncPeriod = f.defaultResync 160 | } 161 | 162 | informer = newFunc(f.client, resyncPeriod) 163 | f.informers[informerType] = informer 164 | 165 | return informer 166 | } 167 | 168 | // SharedInformerFactory provides shared informers for resources in all known 169 | // API group versions. 170 | type SharedInformerFactory interface { 171 | internalinterfaces.SharedInformerFactory 172 | ForResource(resource schema.GroupVersionResource) (GenericInformer, error) 173 | WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool 174 | 175 | Mulitcluster() multicluster.Interface 176 | } 177 | 178 | func (f *sharedInformerFactory) Mulitcluster() multicluster.Interface { 179 | return multicluster.New(f, f.namespace, f.tweakListOptions) 180 | } 181 | -------------------------------------------------------------------------------- /pkg/client/clientset/versioned/typed/multicluster/v1alpha1/multicluster.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Code generated by client-gen. DO NOT EDIT. 18 | 19 | package v1alpha1 20 | 21 | import ( 22 | "context" 23 | "time" 24 | 25 | v1alpha1 "github.com/myoperator/multiclusteroperator/pkg/apis/multicluster/v1alpha1" 26 | scheme "github.com/myoperator/multiclusteroperator/pkg/client/clientset/versioned/scheme" 27 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | types "k8s.io/apimachinery/pkg/types" 29 | watch "k8s.io/apimachinery/pkg/watch" 30 | rest "k8s.io/client-go/rest" 31 | ) 32 | 33 | // MultiClustersGetter has a method to return a MultiClusterInterface. 34 | // A group's client should implement this interface. 35 | type MultiClustersGetter interface { 36 | MultiClusters(namespace string) MultiClusterInterface 37 | } 38 | 39 | // MultiClusterInterface has methods to work with MultiCluster resources. 40 | type MultiClusterInterface interface { 41 | Create(ctx context.Context, multiCluster *v1alpha1.MultiCluster, opts v1.CreateOptions) (*v1alpha1.MultiCluster, error) 42 | Update(ctx context.Context, multiCluster *v1alpha1.MultiCluster, opts v1.UpdateOptions) (*v1alpha1.MultiCluster, error) 43 | UpdateStatus(ctx context.Context, multiCluster *v1alpha1.MultiCluster, opts v1.UpdateOptions) (*v1alpha1.MultiCluster, error) 44 | Delete(ctx context.Context, name string, opts v1.DeleteOptions) error 45 | DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error 46 | Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.MultiCluster, error) 47 | List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.MultiClusterList, error) 48 | Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) 49 | Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.MultiCluster, err error) 50 | MultiClusterExpansion 51 | } 52 | 53 | // multiClusters implements MultiClusterInterface 54 | type multiClusters struct { 55 | client rest.Interface 56 | ns string 57 | } 58 | 59 | // newMultiClusters returns a MultiClusters 60 | func newMultiClusters(c *MulitclusterV1alpha1Client, namespace string) *multiClusters { 61 | return &multiClusters{ 62 | client: c.RESTClient(), 63 | ns: namespace, 64 | } 65 | } 66 | 67 | // Get takes name of the multiCluster, and returns the corresponding multiCluster object, and an error if there is any. 68 | func (c *multiClusters) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.MultiCluster, err error) { 69 | result = &v1alpha1.MultiCluster{} 70 | err = c.client.Get(). 71 | Namespace(c.ns). 72 | Resource("multiclusters"). 73 | Name(name). 74 | VersionedParams(&options, scheme.ParameterCodec). 75 | Do(ctx). 76 | Into(result) 77 | return 78 | } 79 | 80 | // List takes label and field selectors, and returns the list of MultiClusters that match those selectors. 81 | func (c *multiClusters) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.MultiClusterList, err error) { 82 | var timeout time.Duration 83 | if opts.TimeoutSeconds != nil { 84 | timeout = time.Duration(*opts.TimeoutSeconds) * time.Second 85 | } 86 | result = &v1alpha1.MultiClusterList{} 87 | err = c.client.Get(). 88 | Namespace(c.ns). 89 | Resource("multiclusters"). 90 | VersionedParams(&opts, scheme.ParameterCodec). 91 | Timeout(timeout). 92 | Do(ctx). 93 | Into(result) 94 | return 95 | } 96 | 97 | // Watch returns a watch.Interface that watches the requested multiClusters. 98 | func (c *multiClusters) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { 99 | var timeout time.Duration 100 | if opts.TimeoutSeconds != nil { 101 | timeout = time.Duration(*opts.TimeoutSeconds) * time.Second 102 | } 103 | opts.Watch = true 104 | return c.client.Get(). 105 | Namespace(c.ns). 106 | Resource("multiclusters"). 107 | VersionedParams(&opts, scheme.ParameterCodec). 108 | Timeout(timeout). 109 | Watch(ctx) 110 | } 111 | 112 | // Create takes the representation of a multiCluster and creates it. Returns the server's representation of the multiCluster, and an error, if there is any. 113 | func (c *multiClusters) Create(ctx context.Context, multiCluster *v1alpha1.MultiCluster, opts v1.CreateOptions) (result *v1alpha1.MultiCluster, err error) { 114 | result = &v1alpha1.MultiCluster{} 115 | err = c.client.Post(). 116 | Namespace(c.ns). 117 | Resource("multiclusters"). 118 | VersionedParams(&opts, scheme.ParameterCodec). 119 | Body(multiCluster). 120 | Do(ctx). 121 | Into(result) 122 | return 123 | } 124 | 125 | // Update takes the representation of a multiCluster and updates it. Returns the server's representation of the multiCluster, and an error, if there is any. 126 | func (c *multiClusters) Update(ctx context.Context, multiCluster *v1alpha1.MultiCluster, opts v1.UpdateOptions) (result *v1alpha1.MultiCluster, err error) { 127 | result = &v1alpha1.MultiCluster{} 128 | err = c.client.Put(). 129 | Namespace(c.ns). 130 | Resource("multiclusters"). 131 | Name(multiCluster.Name). 132 | VersionedParams(&opts, scheme.ParameterCodec). 133 | Body(multiCluster). 134 | Do(ctx). 135 | Into(result) 136 | return 137 | } 138 | 139 | // UpdateStatus was generated because the type contains a Status member. 140 | // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). 141 | func (c *multiClusters) UpdateStatus(ctx context.Context, multiCluster *v1alpha1.MultiCluster, opts v1.UpdateOptions) (result *v1alpha1.MultiCluster, err error) { 142 | result = &v1alpha1.MultiCluster{} 143 | err = c.client.Put(). 144 | Namespace(c.ns). 145 | Resource("multiclusters"). 146 | Name(multiCluster.Name). 147 | SubResource("status"). 148 | VersionedParams(&opts, scheme.ParameterCodec). 149 | Body(multiCluster). 150 | Do(ctx). 151 | Into(result) 152 | return 153 | } 154 | 155 | // Delete takes name of the multiCluster and deletes it. Returns an error if one occurs. 156 | func (c *multiClusters) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { 157 | return c.client.Delete(). 158 | Namespace(c.ns). 159 | Resource("multiclusters"). 160 | Name(name). 161 | Body(&opts). 162 | Do(ctx). 163 | Error() 164 | } 165 | 166 | // DeleteCollection deletes a collection of objects. 167 | func (c *multiClusters) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { 168 | var timeout time.Duration 169 | if listOpts.TimeoutSeconds != nil { 170 | timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second 171 | } 172 | return c.client.Delete(). 173 | Namespace(c.ns). 174 | Resource("multiclusters"). 175 | VersionedParams(&listOpts, scheme.ParameterCodec). 176 | Timeout(timeout). 177 | Body(&opts). 178 | Do(ctx). 179 | Error() 180 | } 181 | 182 | // Patch applies the patch and returns the patched multiCluster. 183 | func (c *multiClusters) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.MultiCluster, err error) { 184 | result = &v1alpha1.MultiCluster{} 185 | err = c.client.Patch(pt). 186 | Namespace(c.ns). 187 | Resource("multiclusters"). 188 | Name(name). 189 | SubResource(subresources...). 190 | VersionedParams(&opts, scheme.ParameterCodec). 191 | Body(data). 192 | Do(ctx). 193 | Into(result) 194 | return 195 | } 196 | --------------------------------------------------------------------------------