介绍

假设一个Nginx的QPS(服务器一秒内处理的请求数)上限为500,如果外部访问的QPS达到了600,为了保证服务质量,必须扩容一个Nginx来分摊请求。

在Kubernetes环境中,如果外部请求超过了单个Pod的处理极限,我们则可以增加Pod数量来达到横向扩容的目的。

假设我们的服务是无状态服务,我们来利用kubebuilder来开发一个operator,来模拟我们已上所述的场景。

项目初始化

在开发 Operator 之前我们需要先提前想好我们的 CRD 资源对象,比如我们想要通过下面的 CR 资源来创建我们的Operator :

apiVersion: elasticweb.example.com/v1
kind: ElasticWeb
metadata:
name: elasticweb-sample
namespace: dev
spec:
image: nginx:1.17.1 # 镜像
port: 30003 # 外部访问的端口
singlePodsQPS: 800 # 单个 Pod 的 QPS
totalQPS: 2400 # 总 QPS

首先初始化项目,这里使用kubebuilder来构建我们的脚手架:

$ mkdir app-operator && cd app-operator
$ go mod init app-operator
$ kubebuilder init --domain example.com
kubebuilder init --domain example.com
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
...

脚手架创建完成后,然后定义资源API:

$ kubebuilder create api --group elasticweb --version v1 --kind El
asticWeb
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
...

这样我们的项目初始化就完成了,整体的代码结构如下:

$ tree -L 2
.
├── Dockerfile
├── Makefile
├── PROJECT
├── api
│   └── v1
├── bin
│   └── controller-gen
├── config
│   ├── crd
│   ├── default
│   ├── manager
│   ├── prometheus
│   ├── rbac
│   └── samples
├── controllers
│   ├── elasticweb_controller.go
│   └── suite_test.go
├── go.mod
├── go.sum
├── hack
│   └── boilerplate.go.txt
└── main.go 12 directories, 10 files

然后根据我们上面设计的 ElasticWeb 这个对象来编辑 Operator 的结构体即可,修改文件 api/v1/elasticweb_types.go 中的 ElasticWebSpec 结构体以及ElasticWebStatus结构体,ElasticWebStatus结构体主要用来记录当前集群实际支持的总QPS:

// api/v1/elasticweb_types.go

type ElasticWebSpec struct {
Image string `json:"image"`
Port *int32 `json:"port"`
// 单个pod的QPS上限
SinglePodsQPS *int32 `json:"singlePodsQPS"`
// 当前整个业务的QPS
TotalQPS *int32 `json:"totalQPS,omitempty"`
} type ElasticWebStatus struct {
// 当前 Kubernetes 集群实际支持的总QPS
RealQPS *int32 `json:"realQPS"`
}

同样,为了打印的日志方便我们阅读,我们给ElasticWeb添加一个String方法:

// api/v1/elasticweb_types.go

func (e *ElasticWeb) String() string {
var realQPS string
if nil == e.Status.RealQPS {
realQPS = ""
} else {
realQPS = strconv.Itoa(int(*e.Status.RealQPS))
} return fmt.Sprintf("Image [%s], Port [%d], SinglePodQPS [%d], TotalQPS [%d], RealQPS [%s]",
e.Spec.Image,
*e.Spec.Port,
*e.Spec.SinglePodsQPS,
*e.Spec.TotalQPS,
realQPS)
}

要注意每次修改完成需要执行make命令重新生成代码:

$ make
make
/Users/Christian/Documents/code/negan/app-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
api/v1/elasticweb_types.go
go vet ./...
go build -o bin/manager main.go

接下来我们就可以去控制器的 Reconcile 函数中来实现我们自己的业务逻辑了。

业务逻辑

首先在目录 controllers 下面创建一个 resource.go文件,用来根据我们的ElasticWeb对象生成对应的deploymentservice以及更新状态。

// controllers/resource.go

package controllers

import (
v1 "app-operator/api/v1"
"context"
"fmt"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
) var (
ElasticWebCommonLabelKey = "app"
) const (
// APP_NAME deployment 中 App 标签名
APP_NAME = "elastic-app"
// CONTAINER_PORT 容器的端口号
CONTAINER_PORT = 8080
// CPU_REQUEST 单个POD的CPU资源申请
CPU_REQUEST = "100m"
// CPU_LIMIT 单个POD的CPU资源上限
CPU_LIMIT = "100m"
// MEM_REQUEST 单个POD的内存资源申请
MEM_REQUEST = "512Mi"
// MEM_LIMIT 单个POD的内存资源上限
MEM_LIMIT = "512Mi"
) // 根据总QPS以及单个POD的QPS,计算需要多少个Pod
func getExpectReplicas(elasticWeb *v1.ElasticWeb) int32 {
// 单个pod的QPS
singlePodQPS := *elasticWeb.Spec.SinglePodsQPS
// 期望的总QPS
totalQPS := *elasticWeb.Spec.TotalQPS
// 需要创建的副本数
replicas := totalQPS / singlePodQPS if totalQPS%singlePodQPS != 0 {
replicas += 1
}
return replicas
} // CreateServiceIfNotExists 创建service
func CreateServiceIfNotExists(ctx context.Context, r *ElasticWebReconciler, elasticWeb *v1.ElasticWeb, req ctrl.Request) error {
logger := log.FromContext(ctx)
logger.WithValues("func", "createService")
svc := &corev1.Service{} svc.Name = elasticWeb.Name
svc.Namespace = elasticWeb.Namespace svc.Spec = corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: "http",
Port: CONTAINER_PORT,
NodePort: *elasticWeb.Spec.Port,
},
},
Type: corev1.ServiceTypeNodePort,
Selector: map[string]string{
ElasticWebCommonLabelKey: APP_NAME,
},
} // 设置关联关系
logger.Info("set reference")
if err := controllerutil.SetControllerReference(elasticWeb, svc, r.Scheme); err != nil {
logger.Error(err, "SetControllerReference error")
return err
} logger.Info("start create service")
if err := r.Create(ctx, svc); err != nil {
logger.Error(err, "create service error")
return err
} return nil
} // CreateDeployment 创建deployment
func CreateDeployment(ctx context.Context, r *ElasticWebReconciler, elasticWeb *v1.ElasticWeb) error {
logger := log.FromContext(ctx)
logger.WithValues("func", "createDeploy") // 计算期待pod的数量
expectReplicas := getExpectReplicas(elasticWeb)
logger.Info(fmt.Sprintf("expectReplicas [%d]", expectReplicas)) deploy := &appsv1.Deployment{} deploy.Labels = map[string]string{
ElasticWebCommonLabelKey: APP_NAME,
} deploy.Name = elasticWeb.Name
deploy.Namespace = elasticWeb.Namespace deploy.Spec = appsv1.DeploymentSpec{
Replicas: pointer.Int32Ptr(expectReplicas),
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
ElasticWebCommonLabelKey: APP_NAME,
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
ElasticWebCommonLabelKey: APP_NAME,
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: APP_NAME,
Image: elasticWeb.Spec.Image,
Ports: []corev1.ContainerPort{
{
Name: "http",
ContainerPort: CONTAINER_PORT,
Protocol: corev1.ProtocolSCTP,
},
},
Resources: corev1.ResourceRequirements{
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse(CPU_LIMIT),
corev1.ResourceMemory: resource.MustParse(MEM_LIMIT),
},
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse(CPU_REQUEST),
corev1.ResourceMemory: resource.MustParse(MEM_REQUEST),
},
},
},
},
},
},
} // 建立关联,删除web后会将deploy一起删除
logger.Info("set reference")
if err := controllerutil.SetControllerReference(elasticWeb, deploy, r.Scheme); err != nil {
logger.Error(err, "SetControllerReference error")
return err
} // 创建Deployment
logger.Info("start create deploy")
if err := r.Create(ctx, deploy); err != nil {
logger.Error(err, "create deploy error")
return err
} logger.Info("create deploy success")
return nil
} func UpdateStatus(ctx context.Context, r *ElasticWebReconciler, elasticWeb *v1.ElasticWeb) error {
logger := log.FromContext(ctx)
logger.WithValues("func", "updateStatus") // 单个pod的QPS
singlePodQPS := *elasticWeb.Spec.SinglePodsQPS // pod 总数
replicas := getExpectReplicas(elasticWeb) // 当pod创建完成后,当前系统的QPS为: 单个pod的QPS * pod总数
// 如果没有初始化,则需要先初始化
if nil == elasticWeb.Status.RealQPS {
elasticWeb.Status.RealQPS = new(int32)
} *elasticWeb.Status.RealQPS = singlePodQPS * replicas
logger.Info(fmt.Sprintf("singlePodQPS [%d],replicas [%d],realQPS[%d]", singlePodQPS, replicas, *elasticWeb.Status.RealQPS)) if err := r.Update(ctx, elasticWeb); err != nil {
logger.Error(err, "update instance error")
return err
}
return nil
}

上面的代码虽然很多,但逻辑很简单,就是根据我们的 ElasticWeb 去构造 deployservice资源对象,构造完成后,当我们创建 ElasticWeb 的时候就可以在控制器的 Reconcile 函数中去进行逻辑处理了。

同时,我们需要在Reconcile函数注释中添加 deployservice的RBAC声明。

// controllers/elasticweb_controller.go

//+kubebuilder:rbac:groups=elasticweb.example.com,resources=elasticwebs,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=elasticweb.example.com,resources=elasticwebs/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=elasticweb.example.com,resources=elasticwebs/finalizers,verbs=update
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete func (r *ElasticWebReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx) instance := &elasticwebv1.ElasticWeb{} if err := r.Get(ctx, req.NamespacedName, instance); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
} logger.Info(fmt.Sprintf("instance:%s", instance.String())) // 获取deployment
deploy := &appsv1.Deployment{}
if err := r.Get(ctx, req.NamespacedName, deploy); err != nil {
if errors.IsNotFound(err) {
// 如果没有查找到,则需要创建
logger.Info("deploy not exists")
// 判断qps的需求,如果qps没有需求,则啥都不做
if *instance.Spec.TotalQPS < 1 {
logger.Info("not need deployment")
return ctrl.Result{}, nil
} // 创建service
if err = CreateServiceIfNotExists(ctx, r, instance, req); err != nil {
return ctrl.Result{}, err
} // 创建Deploy
if err := CreateDeployment(ctx, r, instance); err != nil {
return ctrl.Result{}, err
} // 更新状态
if err := UpdateStatus(ctx, r, instance); err != nil {
return ctrl.Result{}, err
} return ctrl.Result{}, nil
}
logger.Error(err, "failed to get deploy")
return ctrl.Result{}, err
} // 根据单个Pod的QPS计算期望pod的副本
expectReplicas := getExpectReplicas(instance) // 获取当前deployment实际的pod副本
realReplicas := deploy.Spec.Replicas if expectReplicas == *realReplicas {
logger.Info("not need to reconcile")
return ctrl.Result{}, nil
} // 重新赋值
deploy.Spec.Replicas = &expectReplicas
// 更新 deploy
if err := r.Update(ctx, deploy); err != nil {
logger.Error(err, "update deploy replicas error")
return ctrl.Result{}, err
} // 更新状态
if err := UpdateStatus(ctx, r, instance); err != nil {
logger.Error(err, "update status error")
return ctrl.Result{}, err
} return ctrl.Result{}, nil
}

调试

接下来我们首先安装我们的 CRD 对象,让我们的 Kubernetes 系统识别我们的 ElasitcWeb 对象:

$ make install
/Users/Christian/Documents/code/negan/app-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/Users/Christian/Documents/code/negan/app-operator/bin/kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/elasticwebs.elasticweb.example.com configured

接着运行控制器:

$ make install
/Users/Christian/Documents/code/negan/app-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/Users/Christian/Documents/code/negan/app-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
controllers/elasticweb_controller.go
go vet ./...
go run ./main.go
1.652941435373431e+09 INFO controller-runtime.metrics Metrics server is starting to listen {"addr": ":8080"}
1.6529414353737469e+09 INFO setup starting manager
1.6529414353739378e+09 INFO Starting server {"path": "/metrics", "kind": "metrics", "addr": "[::]:8080"}
1.652941435373951e+09 INFO Starting server {"kind": "health probe", "addr": "[::]:8081"}
1.6529414353741682e+09 INFO controller.elasticweb Starting EventSource {"reconciler group": "elasticweb.example.com", "reconciler kind": "ElasticWeb", "source": "kind source: *v1.ElasticWeb"}
1.652941435374196e+09 INFO controller.elasticweb Starting EventSource {"reconciler group": "elasticweb.example.com", "reconciler kind": "ElasticWeb", "source": "kind source: *v1.Deployment"}
1.652941435374202e+09 INFO controller.elasticweb Starting Controller {"reconciler group": "elasticweb.example.com", "reconciler kind": "ElasticWeb"}
1.65294143547575e+09 INFO controller.elasticweb Starting workers {"reconciler group": "elasticweb.example.com", "reconciler kind": "ElasticWeb", "worker count": 1}

控制器启动成功后我们就可以去创建我们的CR了,将示例 CR 资源清单修改成下面的 YAML:

apiVersion: elasticweb.example.com/v1
kind: ElasticWeb
metadata:
name: elasticweb-sample
spec:
image: nginx:1.17.1
port: 30003
singlePodsQPS: 800
totalQPS: 2400

另外开启一个终端创建上面的资源对象:

$ kubectl apply -f config/samples/elasticweb_v1_elasticweb.yaml
elasticweb.elasticweb.example.com/elasticweb-sample created

创建完成后我们可以查看对应的 ElasticWeb对象:

$ kubectl get ElasticWeb
NAME AGE
elasticweb-sample 40s

对应也会自动创建我们的 Deployment 和 Service 资源清单:

$ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/elasticweb-sample-6879bdfcf4-42jtc 1/1 Running 0 2m40s
pod/elasticweb-sample-6879bdfcf4-sdmbp 1/1 Running 0 2m40s
pod/elasticweb-sample-6879bdfcf4-w87tj 1/1 Running 0 2m40s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/elasticweb-sample NodePort 10.100.200.7 <none> 8080:30003/TCP 2m40s
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 14d NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/elasticweb-sample 3/3 3 3 2m40s NAME DESIRED CURRENT READY AGE
replicaset.apps/elasticweb-sample-6879bdfcf4 3 3 3 2m40s

优化

现在我们需要对Deploy进行Watch,Service是的创建包含在创建Deploy的逻辑里,所以Deploy出现变化,我们需要重新进行调谐。当然我们只需要Watch被ElasticWeb控制的这部分独享即可。在elasticweb_controller.go文件中更新SetupWithManager函数即可:

func (r *ElasticWebReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&elasticwebv1.ElasticWeb{}).
Owns(&appsv1.Deployment{}).
Complete(r)
}

而且我们发现在终端打印的日志中,worker count 为1,这时候我们同样可以更新SetupWithManager函数:

func (r *ElasticWebReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
WithOptions(controller.Options{MaxConcurrentReconciles: 5}).
For(&elasticwebv1.ElasticWeb{}).
Owns(&appsv1.Deployment{}).
Complete(r)
}

同样我们发现输出的日志是时间戳格式,不够直观。 在 main 函数中有个zap的Options,我们可以在这里面进行设置:

opts := zap.Options{
Development: true,
TimeEncoder: zapcore.ISO8601TimeEncoder,
}

自定义输出列

我们这里的 Elastic 实例,我们可以使用 kubectl 命令列出这个对象:

$ kubectl get ElasticWeb
NAME AGE
elasticweb-sample 40s

但是这个信息太过于简单,如果我们想要查看这个对象使用了什么镜像,部署了多少个副本,我们可能还需要通过 kubectl describe 命令去查看,这样就太过于麻烦了。这个时候我们就可以在 CRD 定义的结构体类型中使用 +kubebuilder:printcolumn 这个注释来告诉 kubebuilder 将我们所需的信息添加到 CRD 中,比如我们想要打印使用的镜像,在 +kubebuilder:object:root=true 注释下面添加一列新的注释,如下所示:

/+kubebuilder:object:root=true
// +kubebuilder:printcolumn:name="Image",type="string",JSONPath=".spec.image",description="The Docker Image of MyAPP"
//+kubebuilder:subresource:status // ElasticWeb is the Schema for the elasticwebs API
type ElasticWeb struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"` Spec ElasticWebSpec `json:"spec,omitempty"`
Status ElasticWebStatus `json:"status,omitempty"`
}

printcolumn 注释有几个不同的选项,在这里我们只使用了其中一部分:

  • name:这是我们新增的列的标题,由 kubectl 打印在标题中
  • type:要打印的值的数据类型,有效类型为 integer、number、string、boolean 和 date
  • JSONPath:这是要打印数据的路径,在我们的例子中,镜像 image 属于 spec 下面的属性,所以我们使用 .spec.image。需要注意的是 JSONPath 属性引用的是生成的 JSON CRD,而不是引用本地 Go 类。
  • description:描述列的可读字符串,目前暂未发现该属性的作用...

新增了注释后,我们需要运行 make install 命令重新生成 CRD 并安装,然后我们再次尝试列出 CRD。

$ kubectl get ElasticWeb
NAME IMAGE
elasticweb-sample nginx:1.17.1

可以看到现在列出来的数据有一列 IMAGE 的数据了,不过却没有了之前列出来的 AGE 这一列了。这是因为当我们添加自定义列的时候,就不会再显示其他默认的列了(NAME 除外),所以如果我们还想出现 AGE 这一列,我们还需要在 EtcdCluster 的结构体上面添加对应的注释信息,如下所示:

// +kubebuilder:object:root=true
// +kubebuilder:printcolumn:name="Image",type="string",JSONPath=".spec.image",description="The Docker Image of Etcd"
// +kubebuilder:printcolumn:name="Port",type="integer",JSONPath=".spec.port",description="container port"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:subresource:status // ElasticWeb is the Schema for the elasticwebs API
type ElasticWeb struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"` Spec ElasticWebSpec `json:"spec,omitempty"`
Status ElasticWebStatus `json:"status,omitempty"`
}

运行 make install 命令行,再次查看 CRD 数据:

$ kubectl get ElasticWeb
NAME IMAGE PORT AGE
elasticweb-sample nginx:1.17.1 30003 37m

如果我们还想获取当前应用的状态,同样也可以通过 +kubebuilder:printcolumn 来添加对应的信息,只是状态的数据是通过 .status 在 JSONPath 属性中去获取了。

如果你觉得这里添加了太多的信息,如果我们想隐藏某个字段并只在需要时显示该字段怎么办?

这个时候就需要使用 priority 这个属性了,如果没有配置这个属性,默认值为0,也就是默认情况下列出显示的数据是 priority=0 的列,如果将 priority 设置为大于1的数字,那么则只会当我们使用 -o wide 参数的时候才会显示,比如我们给 Port 这一列添加一个 priority=1 的属性:

// +kubebuilder:printcolumn:name="Port",type="string",priority=1,JSONPath=".spec.image",description="The Docker Image of Etcd"

同样重新运行make install命令后,再次查看CRD:

$ kubectl get ElasticWeb
NAME IMAGE AGE
elasticweb-sample nginx:1.17.1 41m $ kubectl get ElasticWeb -o wide
NAME IMAGE PORT AGE
elasticweb-sample nginx:1.17.1 30003 41m

了解更多详细信息请查看 CRD 文档上的 AdditionalPrinterColumns 字段

部署

现在我们已经完成了开发工作,并在本地完成了测试工作,这时候我们就需要把我们的operator部署到kubernetes环境中。

首先我们需要修改Dockerfile文件,需要添加上go mod的代理配置:

# Build the manager binary
FROM golang:1.17 as builder WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
ENV GOPROXY https://goproxy.cn RUN go mod download # Copy the go source
COPY main.go main.go
COPY api/ api/
COPY controllers/ controllers/ # Build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go # Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532 ENTRYPOINT ["/manager"]

接下来就是登陆docker了,我这边使用的docker hub,直接在命令行登陆即可。

$ docker login
Authenticating with existing credentials...
Login Succeeded Logging in with your password grants your terminal complete access to your account.
For better security, log in with a limited-privilege personal access token. Learn more at https://docs.docker.com/go/access-tokens/

登陆成功后,就可以构建镜像了。

注意如果你用的是Mac M1的电脑,那么需要对Makefile做一小点修改,具体可见issues

.PHONY: test
test: manifests generate fmt vet envtest ## Run tests.
#KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) --arch=amd64 use $(ENVTEST_K8S_VERSION) -p path)" go test ./... -coverprofile cover.out

接下来就是构建并将镜像推送到镜像仓库:

$ make docker-build docker-push IMG=<some-registry>/<project-name>:tag

$ make docker-build docker-push IMG=huiyichanmian/elasitcweb:v0.0.1

等待推送成功后,就可以根据IMG指定的镜像将控制器部署到集群中:

$ make deploy IMG=<some-registry>/<project-name>:tag

$ make deploy IMG=huiyichanmian/elasticweb:v0.0.1

同样,这里可能会遇到镜像gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0这个镜像拉不下来的情况,这里可以使用kubesphere/kube-rbac-proxy:v0.8.0进行替代。

可以直接修改config/default/manager_auth_proxy_patch.yaml或者使用docker tag进行改名。

部署完成后,系统会自动创建项目名- system的命名空间,我们的控制器所有东西都在这个namespace下。

最后如果要从集群中卸载operator也很简单:

$ make undeploy

参考:

https://xinchen.blog.csdn.net/article/details/113836090

从0到1使用kubebuiler开发operator的更多相关文章

  1. 在MAC上搭建cordova3.4.0的IOS和android开发环境

    Hello,大家好,今天给大家说说在mac上搭建cordova3.4.0的iOS和Android开发环境,首先下载cordova,地址:https://cordova.apache.org/#down ...

  2. 驱动开发学习笔记. 0.06 嵌入式linux视频开发之预备知识

    驱动开发读书笔记. 0.06  嵌入式linux视频开发之预备知识 由于毕业设计选择了嵌入式linux视频开发相关的项目,于是找了相关的资料,下面是一下预备知识 UVC : UVC,全称为:USB v ...

  3. GJM : Taurus.MVC 2.0 开源发布:WebAPI开发教程 [转载]

    Taurus.MVC 2.0 开源发布:WebAPI开发教程 转载自http://www.cnblogs.com/cyq1162/p/6069020.html 因是新手  粘贴时有一个版权问题 本文原 ...

  4. MVC通用控件库展示-MVC4.0+WebAPI+EasyUI+Knockout--SNF快速开发平台3.0

    在我们开发中怎么才能提高效率,就是要有大量的公共组件(控件)可以直接使用而不用自己再开发一遍,既然是公共控件那也得简单实用才行.下面就介绍一下SNF-MVC当中的控件库. 总体控件库展示: 1.通用用 ...

  5. NET Core 1.1 版本项目和2.0环境下的项目开发注意事项

    在NET Core 1.1开发下的项目最好不要随便把工具更新升级到2.0,这样最容易导致之前的.NETCore直接被升级不兼容早前版本 会引起项目无法启动在运行调试IIS express 时候直接一闪 ...

  6. 0、Spring 注解驱动开发

    0.Spring注解驱动开发 0.1 简介 <Spring注解驱动开发>是一套帮助我们深入了解Spring原理机制的教程: 现今SpringBoot.SpringCloud技术非常火热,作 ...

  7. NX11.0和VS2013 创建NXOpen 开发模版失败解决方案【转载】

    转载自PLM之家论坛 NX11.0和VS2013 创建NXOpen 开发模版失败解决方案 首先我觉得这个可能是西门子疏忽,基本上每个大版本没有补丁前都有类似问题,下面来说说怎么解决吧.注意这里版本,N ...

  8. 高性能PHP框架thinkphp5.0.0 Beta发布-为API开发而设计

    ThinkPHP V5.——为API开发而设计的高性能框架 ThinkPHP5..0版本是一个颠覆和重构版本,采用全新的架构思想,引入了很多的PHP新特性,优化了核心,减少了依赖,实现了真正的惰性加载 ...

  9. 0.[WP Developer体验Andriod开发]之从零安装配置Android Studio并编写第一个Android App

    0. 所需的安装文件 笔者做了几年WP,近来对Android有点兴趣,尝试一下Android开发,废话不多说,直接进入主题,先安装开发环境,笔者的系统环境为windows8.1&x64. 安装 ...

随机推荐

  1. 重定向(redirect)与转发(forward)的区别

    重定向(redirect)与转发(forward)的区别 1.重定向时地址栏会发生改变,转发时地址栏不会发生改变 当浏览器请求资源时,服务器直接访问目标地址的URL,将URL的响应内容读取,之后再将读 ...

  2. 10.Flink实时项目之订单维度表关联

    1. 维度查询 在上一篇中,我们已经把订单和订单明细表join完,本文将关联订单的其他维度数据,维度关联实际上就是在流中查询存储在 hbase 中的数据表.但是即使通过主键的方式查询,hbase 速度 ...

  3. 用纯CSS实现优雅的tab页

    说明 又是一个练手的小玩意儿,本身没什么技术含量,就是几个不常用的CSS3特性的结合而已. 要点 Label标签的for属性 单选框的:checked伪类 CSS的加号[+]选择器 效果图 原理 通常 ...

  4. 使用Dropbox搭建静态网站详细教程

    DropBox是一款非常好用的免费网络文件同步工具,是Dropbox公司运行的在线存储服务,通过云计算实现因特网上的文件同步,用户可以存储并共享文件和文件夹.今天小z和大家分享一下如何使用dropbo ...

  5. electron制作聊天界面(仿制qq)

    效果图: 样式使用scss和flex布局 这也是制作IM系统的最后一个界面了!在制作之前参考了qq和千牛 需要注意的点 qq将滚动条美化了 而且在无操作的情况下是不会显示的 滚动条美化 ::-webk ...

  6. react在移动端的自适应布局

    react+flexible适配布局 (1)npm i lib-flexible --save (2)npm i postcss-px2rem --save (3)在 node_modules/rea ...

  7. Blazor组件自做七 : 使用JS隔离制作定位/持续定位组件

    1. 运行截图 演示地址 2. 在文件夹wwwroot/lib,添加geolocation子文件夹,添加geolocation.js文件 本组件主要是调用浏览器两个API实现基于浏览器的定位功能,现代 ...

  8. [源码解析] TensorFlow 分布式环境(8) --- 通信机制

    [源码解析] TensorFlow 分布式环境(8) --- 通信机制 目录 [源码解析] TensorFlow 分布式环境(8) --- 通信机制 1. 机制 1.1 消息标识符 1.1.1 定义 ...

  9. 安装 UE 源码版

    # 安装 UE 源码版 ## 下载安装包 > - 先去 Github 找 UE 官方开源的引擎组(这个需要申请加入) > - 加入后找到开源的源码版项目下载 zip 到本地 > - ...

  10. linux中find与三剑客之grep用法

    find用法 find一般用来用来查找文件名称 根据文件的名称或者属性查找文件. 语法格式: find [查找范围] [参数] 参数: -name : 按照文件的名字查找文件 * :通配符 -inam ...