Kubernetes编程/Operator专题精讲——Operator云原生应用开发——Operator介绍

Operator介绍

一、什么是 Operator?

   在 Kubernetes中我们经常使用 Deployment、DaemonSet、Service、ConfigMap 等资源,这些资源都是 Kubernetes 的内置资源,它们的创建、更新、删除等均由 Controller Manager 负责管理,触发相应的动作来满足期望状态(Spec),这种声明方式简化了用户的操作,用户在使用时只需要关心应用程序的最终状态即可。

   随着大数据、人工智能等领域出现了一些场景更复杂的分布式应用系统,原生 Kubernetes 内置资源在这些场景下就显得有些力不从心。

1)不同应用平台需要管理的目标各有差异,如何在 Kubernetes 中兼容定义管理的目标?

2)如何管理和备份系统的应用数据,协调各应用之间不同生命周期的状态?

3)能否用同样的 kubectl 命令来管理自己定义的复杂分布式应用?

4)……

  在这些场景下,Kubernetes 自身基础模型已经无法支撑不同业务领域下的自动化场景。为了满足这些需求,Google 提出了 Third Party Resources(TPR,TPR后来被 Red Hat 和 Google 建议废除 TPR,后改名为 CRD)概念,允许开发者根据业务需求的变化自定义资源和控制器,用于编写面向领域知识的业务逻辑控制,这就是 Operator 的核心概念

  Operator 是一种封装、部署和管理 Kubernetes 应用的方法,用户可以使用 Kubernetes API 和 kubectl 工具在 Kubernetes 上部署并管理 Kubernetes 应用。Operator 基于基本 Kubernetes 资源和控制器概念构建,但又涵盖了特定领域或应用的知识,用于实现其所管理软件的整个生命周期的自动化,它是一种特定于应用的控制器,可扩展 Kubernetes API 的功能,是 Kubernetes 用户创建、配置和管理复杂应用的实例。

  在 Kubernetes 内置资源中,Controller Manager 实施控制循环,反复比较集群中的期望状态和实际状态,如果集群的实际状态和期望状态不一致,则采取措施使二者一致。Operator 使用自定义资源(CR)管理应用,CR 引入新的对象类型后,Operator 监视 CR 类型,并采取特定于应用的操作,确保 CR 对象当前实际状态和期望状态一致,用户可通过在 Operator 中编写自定义规则来扩展新功能和更新现有功能,可以像操作 Kubernetes 原生组件一样,通过声明式的定义一组业务应用的期望状态,监控操作自定义资源,该特定使 Operator 几乎可以在 Kubernetes 中执行任何操作,包括扩展复杂的应用、版本升级、管理有状态的服务等。

  这种设使应用维护人员只需要关注配置自身应用的期望运行状态,而无须投入大量的精力在部署应用或业务运行过程中频繁操作可能出错的运维命令。

二、Operator 组成

  简单来说,Operator = Controller + CRD,Operator 是由 Kubernetes 自定义资源(CRD,Custom Resource Definition)和控制器(Controller)构成的云原生扩展服务。其中 CRD 定义每个 Operator 需要创建和管理的自定义资源对象,底层实际就是通过 APIServer 接口在 etcd 中注册一钟新的资源类型,注册完成后就可以创建该资源类型的对象了。但仅注册资源和创建资源对象是没有任何实际意义的,CRD 最重要的是需要配合对应的 Controller 来实现自定义资源的功能,达到自定义资源期望的状态,比如内置的 Deployment Controller 用来控制 Deployment 资源的功能,根据配置生成特定数量的 Pod 监控其状态,并根据事件做出相应的动作。

三、Operator如何使用

  用户想为自己的自定义资源构建一个 Kubernetes Operator,有很多工具可供选择,比如 Operator SDK、Kubebuilder,甚至可以使用 Operator SDK(Helm、Ansible、Go)这些工具创建 Kubernetes Operator 用来监控自定义资源,并且根据资源的变化调整资源状态:

  Operator 作为自定义扩展资源以 Deployment 的方式部署到 k8s 中,通过 ListWatch 方式监听对应资源的变化,当用户修改自定义资源中的任何内容时,Operator 会监控资源的更改,并根据更改内容执行特定的操作,这些操作通常会对 Kubernetes API 中某些资源进行调用。

四、Operator应用案例

  前面介绍了基于 CR 和相应的自定义资源控制器,我们可以自定义扩展 Kubernetes 原生的模型元素,这样的自定义模型可以加入到原生 Kubernetes API 管理;同时 Operator 开发者可以像使原生 API 进行管理一样,通过声明式的方式定义一组业务应用的期望状态,并且根据业务应用的自身特点编写相应控制器逻辑,以此完成对应用运行时刻声明周期的管理,并持续维护与期望状态的一致性。

  我们介绍如何使用 Kubebuilder 工具,快速构建一个 Kubernetes Operator,通过创建 CRD 完成一个简单的 Web 应用部署,通过编写控制器相关业务逻辑,完成对 CRD 的自动化管理。

  Kubebuilder 是一个使用 Go 语言构建 Kubernetes API 控制器和 CRD 的脚手架工具,通过使用 Kubebuilder,用户可以遵循一套简单的编程框架,编写 Operator 应用案例,本质就是使用 CRD 构建 K8s API 的 SDK,主要是:

    • 提供脚手架工具初始化 CRDs 工程,自动生成 boilerplate 代码和配置;
    • 提供代码库封装底层的 K8s go-client;

  方便用户从零开始开发 CRDs,Controllers 和 Admission Webhooks 来扩展 K8s。

4.1)kubebuilder 依赖条件:

https://book.kubebuilder.io/quick-start.html#installation(点击 https://github.com/kubernetes-sigs/kubebuilder 下 Installation 会跳转到此网站)显示要求如下:

go version v1.17.9+
docker version 17.03+.
kubectl version v1.11.3+.
Access to a Kubernetes v1.11.3+ cluster.

4.2)安装 kubebuilder

2.1)安装GO,我这里原来用的Go旧的版本,现升级到了 1.18.3,就不展示升级步骤了: 

[root@localhost ~]# go version
go version go1.18.3 linux/amd64
[root@localhost ~]# go env
GO111MODULE="on"
GOARCH="amd64"
GOBIN=""
GOCACHE="/root/.cache/go-build"
GOENV="/root/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/root/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/root/go"
GOPRIVATE=""
GOPROXY="https://goproxy.cn"
GOROOT="/usr/local/go/current"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/current/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.18.3"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/dev/null"
GOWORK=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build781782133=/tmp/go-build -gno-record-gcc-switches"
[root@localhost ~]# 

 2.2)安装 kubebuilder

[root@localhost ~]# curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    68  100    68    0     0    254      0 --:--:-- --:--:-- --:--:--   255
100   110  100   110    0     0    360      0 --:--:-- --:--:-- --:--:--   360
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
  0     0    0     0    0     0      0      0 --:--:--  0:00:01 --:--:--     0
100 30.4M  100 30.4M    0     0  3369k      0  0:00:09  0:00:09 --:--:-- 3579k
[root@localhost ~]# chmod +x kubebuilder && mv kubebuilder /usr/local/bin/
[root@localhost ~]# kubebuilder version
Version: main.version{KubeBuilderVersion:"3.5.0", KubernetesVendor:"1.24.1", GitCommit:"26d12ab1134964dbbc3f68877ebe9cf6314e926a", BuildDate:"2022-06-24T12:17:52Z", GoOs:"linux", GoArch:"amd64"}

4.3)Welcome 案例介绍

  Welcome 案例主要实现使用 Operator 和 CRD 部署一套完整的应用环境,可以实现根据自定义类型创建资源,通过创建一个 Welcome 类型的资源,后台自动创建 Deployment 和 Service,通过 Web 页面访问 Service 呈现应用部署,通过自定义控制器方法进行控制管理,整体流程如下:

  之前我们已经创建过 CRDs,就接着改改,创建这次我们需要的 kind;welcome,我们需要创建 Welcome 自定义资源及对应的 Controllers,最终我们可以通过类似代码清单的 yaml 文件部署简单的 Web 应用。

[root@k8s-masternode-2-121 crds]# cat web.app.demo.welcome.domain.yaml 
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: welcomes.auth.zuoyang.tech
spec:
  group: auth.zuoyang.tech
  names:
    kind: welcome
    plural: welcomes
    singular: welcome
    shortNames:
    - w
  scope: Namespaced
  versions:
  - served: true
    storage: true
    name: v1alpha1
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              name: 
                type: string
              userID:
                type: integer
                minimum: 1
                maximum: 65535
              groups :
                type: array
                items:
                  type: string
              email:
                type: string
              password:
                type: string
                format: password
            #required: ["userID","groups"]
[root@k8s-masternode-2-121 crds]# 
[root@k8s-masternode-2-121 crds]# cat web.app.demo.welcome.domain_v1.yaml apiVersion: auth.zuoyang.tech/v1alpha1
kind: welcome
metadata:
  name: welcome-sample
  namespace: default
spec:
  name: myfriends
[root@k8s-masternode-2-121 crds]# 

  "kubectl get welcome" 命令查看如下:

[root@k8s-masternode-2-121 crds]# kubectl get welcome -o yaml
apiVersion: v1
items:
- apiVersion: auth.zuoyang.tech/v1alpha1
  kind: welcome
  metadata:
    annotations:
      kubectl.kubernetes.io/last-applied-configuration: |
        {"apiVersion":"auth.zuoyang.tech/v1alpha1","kind":"welcome","metadata":{"annotations":{},"name":"welcome-sample","namespace":"default"},"spec":{"name":"myfriends"}}
    creationTimestamp: "2022-07-14T09:58:43Z"
    generation: 1
    name: welcome-sample
    namespace: default
    resourceVersion: "21193"
    uid: f462efbb-c520-45a2-93cc-011cde1ca200
  spec:
    name: myfriends
kind: List
metadata:
  resourceVersion: ""
[root@k8s-masternode-2-121 crds]# 

1)一个简单的 Go 语言 http 模块创建了一个 web 服务器,用户访问页面后会自动加载 NAME 及 PORT 环境变量并渲染 index.html 静态文件,代码逻辑如下:

package main

import (
  "fmt"
  "net/http"
  "os"
)

func main() {
  name := os.Getenv("NAME")
  hello := fmt.Sprintf("Hello %s", name)
  
  fs := http.FileServer(http.Dir("static"))
  http.Handle("/hello/", http.StripPrefix("/hello/", fs))

  f, err := os.OpenFile("./static/index.html", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
  if err != nil {
    panic(err)
  }
  defer f.Close()
  if _, err = f.WriteString(hello); err != nil {
    panic(err)
  }
 
  port := os.Getenv("PORT")
  if port == ""{
     port = "8080"
  }

  http.ListenAndServe("192.168.2.121:8088",nil)

}

  页面展示如下:

2)项目初始化

接下来,我们对项目进行初始化,初始化命令如下:

[root@iZj6cdj8nbj0zla2lnq2g9Z demo]# kubebuilder init --domain demo.welcome.domain
Writing kustomize manifests for you to edit...
Writing scaffold for you to edit...
Get controller runtime:
$ go get sigs.k8s.io/controller-runtime@v0.12.1
go: downloading sigs.k8s.io/controller-runtime v0.12.1
go: downloading k8s.io/apimachinery v0.24.0
go: downloading github.com/gogo/protobuf v1.3.2
go: downloading github.com/go-logr/logr v1.2.0
go: downloading k8s.io/client-go v0.24.0
go: downloading k8s.io/klog/v2 v2.60.1
go: downloading k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
go: downloading github.com/google/gofuzz v1.1.0
go: downloading github.com/prometheus/client_golang v1.12.1
go: downloading sigs.k8s.io/structured-merge-diff/v4 v4.2.1
go: downloading github.com/evanphx/json-patch v4.12.0+incompatible
go: downloading golang.org/x/time v0.0.0-20220210224613-90d013bbcef8
go: downloading gomodules.xyz/jsonpatch/v2 v2.2.0
go: downloading k8s.io/api v0.24.0
go: downloading k8s.io/apiextensions-apiserver v0.24.0
go: downloading golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd
go: downloading github.com/imdario/mergo v0.3.12
go: downloading github.com/spf13/pflag v1.0.5
go: downloading golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
go: downloading k8s.io/component-base v0.24.0
go: downloading gopkg.in/inf.v0 v0.9.1
go: downloading github.com/prometheus/client_model v0.2.0
go: downloading github.com/prometheus/common v0.32.1
go: downloading github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
go: downloading sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2
go: downloading github.com/json-iterator/go v1.1.12
go: downloading gopkg.in/yaml.v2 v2.4.0
go: downloading k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42
go: downloading github.com/pkg/errors v0.9.1
go: downloading github.com/golang/protobuf v1.5.2
go: downloading github.com/google/gnostic v0.5.7-v3refs
go: downloading github.com/davecgh/go-spew v1.1.1
go: downloading golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
go: downloading golang.org/x/sys v0.0.0-20220209214540-3681064d5158
go: downloading sigs.k8s.io/yaml v1.3.0
go: downloading github.com/beorn7/perks v1.0.1
go: downloading github.com/cespare/xxhash/v2 v2.1.2
go: downloading github.com/prometheus/procfs v0.7.3
go: downloading google.golang.org/protobuf v1.27.1
go: downloading github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369
go: downloading github.com/google/uuid v1.1.2
go: downloading github.com/fsnotify/fsnotify v1.5.1
go: downloading github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd
go: downloading github.com/modern-go/reflect2 v1.0.2
go: downloading gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
go: downloading golang.org/x/text v0.3.7
go: downloading github.com/google/go-cmp v0.5.5
go: downloading github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
go: downloading google.golang.org/appengine v1.6.7
go: downloading github.com/emicklei/go-restful v2.9.5+incompatible
go: downloading github.com/go-openapi/swag v0.19.14
go: downloading github.com/go-openapi/jsonreference v0.19.5
go: downloading github.com/mailru/easyjson v0.7.6
go: downloading github.com/josharian/intern v1.0.0
go: downloading github.com/PuerkitoBio/purell v1.1.1
go: downloading github.com/go-openapi/jsonpointer v0.19.5
go: downloading github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578
Update dependencies:
$ go mod tidy
go: downloading github.com/stretchr/testify v1.7.0
go: downloading github.com/onsi/ginkgo v1.16.5
go: downloading github.com/onsi/gomega v1.18.1
go: downloading github.com/go-logr/zapr v1.2.0
go: downloading go.uber.org/zap v1.19.1
go: downloading github.com/Azure/go-autorest/autorest v0.11.18
go: downloading github.com/Azure/go-autorest/autorest/adal v0.9.13
go: downloading github.com/Azure/go-autorest v14.2.0+incompatible
go: downloading github.com/pmezard/go-difflib v1.0.0
go: downloading go.uber.org/goleak v1.1.12
go: downloading go.uber.org/atomic v1.7.0
go: downloading go.uber.org/multierr v1.6.0
go: downloading github.com/benbjohnson/clock v1.1.0
go: downloading github.com/Azure/go-autorest/logger v0.2.1
go: downloading github.com/Azure/go-autorest/autorest/date v0.3.0
go: downloading github.com/Azure/go-autorest/tracing v0.6.0
go: downloading github.com/form3tech-oss/jwt-go v3.2.3+incompatible
go: downloading golang.org/x/crypto v0.0.0-20220214200702-86341886e292
go: downloading github.com/Azure/go-autorest/autorest/mocks v0.4.1
go: downloading cloud.google.com/go v0.81.0
go: downloading gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f
go: downloading github.com/nxadm/tail v1.4.8
go: downloading golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
go: downloading github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e
go: downloading gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7
go: downloading github.com/kr/text v0.2.0
Next: define a resource with:
$ kubebuilder create api
[root@iZj6cdj8nbj0zla2lnq2g9Z demo]# 
[root@iZj6cdj8nbj0zla2lnq2g9Z demo]# tree .
.
├── config
│   ├── default
│   │   ├── kustomization.yaml
│   │   ├── manager_auth_proxy_patch.yaml
│   │   └── manager_config_patch.yaml
│   ├── manager
│   │   ├── controller_manager_config.yaml
│   │   ├── kustomization.yaml
│   │   └── manager.yaml
│   ├── prometheus
│   │   ├── kustomization.yaml
│   │   └── monitor.yaml
│   └── rbac
│       ├── auth_proxy_client_clusterrole.yaml
│       ├── auth_proxy_role_binding.yaml
│       ├── auth_proxy_role.yaml
│       ├── auth_proxy_service.yaml
│       ├── kustomization.yaml
│       ├── leader_election_role_binding.yaml
│       ├── leader_election_role.yaml
│       ├── role_binding.yaml
│       └── service_account.yaml
├── Dockerfile
├── go.mod
├── go.sum
├── hack
│   └── boilerplate.go.txt
├── main.go
├── Makefile
├── PROJECT
└── README.md

6 directories, 25 files
[root@iZj6cdj8nbj0zla2lnq2g9Z demo]# 

 4.3.1、创建 API 资源(CRD+CR)

4.3.1.1、执行创建 API 命令,按提示确认资源信息

kubebuilder create api --group auth --version v1alpha1 --kind Welcome
# 提示 "Create Resource [y/n]:" 输入 y(生成 CRD 相关定义)
# 提示 "Create Controller [y/n]:" 输入 y(生成控制器骨架代码)

4.3.1.2、完善 API 资源结构(修改 api/v1alpha1/welcome_types.go)

package v1alpha1

import (
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// WelcomeSpec 定义自定义资源的期望状态
type WelcomeSpec struct {
	Name   string `json:"name"`                    // 对应 Web 应用的 NAME 环境变量
	UserID int32  `json:"userID,omitempty"`        // 可选字段,用户 ID
	Groups []string `json:"groups,omitempty"`      // 可选字段,用户组列表
	Email  string `json:"email,omitempty"`         // 可选字段,邮箱
	Password string `json:"password,omitempty"`    // 可选字段,密码(format: password)
	Replicas *int32 `json:"replicas,omitempty"`    // 可选字段,Deployment 副本数(默认 1)
	Port     int32  `json:"port,omitempty"`        // 可选字段,Web 应用端口(默认 8088)
}

// WelcomeStatus 定义自定义资源的实际状态
type WelcomeStatus struct {
	ReadyReplicas int32  `json:"readyReplicas,omitempty"` // 就绪的副本数
	ServiceURL    string `json:"serviceURL,omitempty"`    // Service 访问地址
	Conditions    []metav1.Condition `json:"conditions,omitempty"` // 资源状态条件
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:printcolumn:name="Name",type=string,JSONPath=`.spec.name`
//+kubebuilder:printcolumn:name="ReadyReplicas",type=integer,JSONPath=`.status.readyReplicas`
//+kubebuilder:printcolumn:name="ServiceURL",type=string,JSONPath=`.status.serviceURL`
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"

// Welcome 是自定义资源的核心结构体
type Welcome struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   WelcomeSpec   `json:"spec,omitempty"`
	Status WelcomeStatus `json:"status,omitempty"`
}

//+kubebuilder:object:root=true

// WelcomeList 用于批量管理 Welcome 资源
type WelcomeList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitempty"`
	Items           []Welcome `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Welcome{}, &WelcomeList{})
}

4.3.1.3、生成 CRD 清单和客户端代码

# 生成 deepcopy 等基础代码
make generate
# 生成 CRD YAML 文件(输出到 config/crd/bases/)
make manifests

五、编写控制器核心逻辑(controllers/welcome_controller.go)

控制器是 Operator 的核心,负责监听 Welcome 资源变化,自动创建 / 更新 Deployment 和 Service,并同步状态到 Welcome 的 Status 字段。

5.1、替换控制器 Reconclie 函数逻辑

package controllers

import (
	"context"
	"fmt"
	"strconv"

	corev1 "k8s.io/api/core/v1"
	appsv1 "k8s.io/api/apps/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/log"

	authv1alpha1 "demo.welcome.domain/api/v1alpha1"
)

// WelcomeReconciler 管理 Welcome 资源
type WelcomeReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

//+kubebuilder:rbac:groups=auth.zuoyang.tech,resources=welcomes,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=auth.zuoyang.tech,resources=welcomes/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=auth.zuoyang.tech,resources=welcomes/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
//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch

// Reconcile 实现控制循环逻辑
func (r *WelcomeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := log.FromContext(ctx)

	// 1. 获取当前 Welcome 资源实例
	welcome := &authv1alpha1.Welcome{}
	if err := r.Get(ctx, req.NamespacedName, welcome); err != nil {
		log.Error(err, "Failed to fetch Welcome")
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	// 2. 处理 Deployment 资源(创建/更新)
	deployment := &appsv1.Deployment{}
	deployment.Name = welcome.Name
	deployment.Namespace = welcome.Namespace
	// 设置默认副本数(1)和端口(8088)
	replicas := int32(1)
	if welcome.Spec.Replicas != nil {
		replicas = *welcome.Spec.Replicas
	}
	port := int32(8088)
	if welcome.Spec.Port != 0 {
		port = welcome.Spec.Port
	}

	// 构建 Deployment 期望状态
	desiredDeployment := r.buildDeployment(welcome, replicas, port)
	// 关联 OwnerReference,实现资源联动删除
	if err := controllerutil.SetControllerReference(welcome, desiredDeployment, r.Scheme); err != nil {
		return ctrl.Result{}, err
	}

	// 检查 Deployment 是否存在,不存在则创建,存在则更新
	if err := r.Get(ctx, types.NamespacedName{Name: deployment.Name, Namespace: deployment.Namespace}, deployment); err != nil {
		if errors.IsNotFound(err) {
			log.Info("Creating a new Deployment", "Deployment.Namespace", desiredDeployment.Namespace, "Deployment.Name", desiredDeployment.Name)
			if err := r.Create(ctx, desiredDeployment); err != nil {
				return ctrl.Result{}, err
			}
			return ctrl.Result{Requeue: true}, nil // 创建后重新入队,等待 Deployment 就绪
		}
		log.Error(err, "Failed to fetch Deployment")
		return ctrl.Result{}, err
	}

	// 如果副本数不匹配,更新 Deployment
	if *deployment.Spec.Replicas != replicas {
		deployment.Spec.Replicas = &replicas
		if err := r.Update(ctx, deployment); err != nil {
			log.Error(err, "Failed to update Deployment")
			return ctrl.Result{}, err
		}
		return ctrl.Result{Requeue: true}, nil
	}

	// 3. 处理 Service 资源(创建/更新)
	service := &corev1.Service{}
	service.Name = welcome.Name
	service.Namespace = welcome.Namespace
	desiredService := r.buildService(welcome, port)
	if err := controllerutil.SetControllerReference(welcome, desiredService, r.Scheme); err != nil {
		return ctrl.Result{}, err
	}

	// 检查 Service 是否存在,不存在则创建
	if err := r.Get(ctx, types.NamespacedName{Name: service.Name, Namespace: service.Namespace}, service); err != nil {
		if errors.IsNotFound(err) {
			log.Info("Creating a new Service", "Service.Namespace", desiredService.Namespace, "Service.Name", desiredService.Name)
			if err := r.Create(ctx, desiredService); err != nil {
				return ctrl.Result{}, err
			}
			return ctrl.Result{Requeue: true}, nil
		}
		log.Error(err, "Failed to fetch Service")
		return ctrl.Result{}, err
	}

	// 4. 更新 Welcome 资源状态
	welcome.Status.ReadyReplicas = deployment.Status.ReadyReplicas
	// 构建 Service 访问地址(NodePort 类型,格式:节点IP:NodePort)
	serviceURL := fmt.Sprintf("http://<k8s-node-ip>:%d/hello", service.Spec.Ports[0].NodePort)
	welcome.Status.ServiceURL = serviceURL
	if err := r.Status().Update(ctx, welcome); err != nil {
		log.Error(err, "Failed to update Welcome status")
		return ctrl.Result{}, err
	}

	log.Info("Reconciliation completed successfully", "ReadyReplicas", welcome.Status.ReadyReplicas, "ServiceURL", welcome.Status.ServiceURL)
	return ctrl.Result{}, nil
}

// buildDeployment 构建 Deployment 资源配置
func (r *WelcomeReconciler) buildDeployment(welcome *authv1alpha1.Welcome, replicas, port int32) *appsv1.Deployment {
	return &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      welcome.Name,
			Namespace: welcome.Namespace,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: &replicas,
			Selector: &metav1.LabelSelector{
				MatchLabels: map[string]string{
					"app": welcome.Name, // 标签匹配 Web 应用 Pod
				},
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: map[string]string{
						"app": welcome.Name,
					},
				},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{
							Name:  "welcome-web",
							Image: "your-docker-registry/welcome-web:v1", // 后续构建的 Web 应用镜像
							Ports: []corev1.ContainerPort{
								{
									ContainerPort: port,
									Name:          "web-port",
								},
							},
							Env: []corev1.EnvVar{
								{
									Name:  "NAME",
									Value: welcome.Spec.Name, // 传递自定义资源的 name 到环境变量
								},
								{
									Name:  "PORT",
									Value: strconv.FormatInt(int64(port), 10),
								},
							},
						},
					},
				},
			},
		},
	}
}

// buildService 构建 Service 资源配置(NodePort 类型,便于外部访问)
func (r *WelcomeReconciler) buildService(welcome *authv1alpha1.Welcome, port int32) *corev1.Service {
	return &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:      welcome.Name,
			Namespace: welcome.Namespace,
		},
		Spec: corev1.ServiceSpec{
			Type: corev1.ServiceTypeNodePort,
			Selector: map[string]string{
				"app": welcome.Name, // 匹配 Deployment 管理的 Pod
			},
			Ports: []corev1.ServicePort{
				{
					Port:     port,
					TargetPort: corev1.IntOrString{IntVal: port},
					Name:     "web-port",
				},
			},
		},
	}
}

// SetupWithManager 将控制器注册到 Manager
func (r *WelcomeReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&authv1alpha1.Welcome{}).
		Owns(&appsv1.Deployment{}). // 监听关联的 Deployment 变化
		Owns(&corev1.Service{}).    // 监听关联的 Service 变化
		Complete(r)
}
posted @ 2022-07-14 11:55  左扬  阅读(2162)  评论(0)    收藏  举报