(四)Aggregated API Server 的实现
# 目的
在开发过程中通常需要自定义一些目的来控制或提供一些额外的功能,为了应对大量这样的需求,Kuberbetes 可以自定义一些 API Server,这里我们就实现一个简单的 Aggregated API Server
# 扩展目标
提示
TIP1: 在决定使用 Aggregated API Server 去实现功能的时候,请认真考虑 CRD 是否已经可以满足你的需求,这样会大大节省时间和精力,虽然它缺失一些灵活性,但是足以满足大多是的应用需求
TIP2: 真实项目优先使用 apiserver-builder (opens new window) 它会为你生成你想要的一切,并且是遵循社区编码规范和要求的
# Aggregated API Server 的制作步骤
- 设计编码 API Object
设计出支持 API Object,并编码实现他们
- 代码生成
利用代码生成器,为 API Object 生成配套代码
- 注册 API Object
编写把 API Object 注册进 Schema 的方法
- Registry - AO存储和 REST
编码实现对 API Object 实例的 CRUD。并最终处理 Restful 请求
- 添加 Admission
利用 Admission 机制对针对 API Object 的请求进行修正和校验
- 创建 Web Server
制作 Web Server 可以响应 HTTP Restful 请求
- 部署并测试
把 Aggregated API Server 部署到集群,并测试其功能
# 1. 设计编码 API Object
设计重点:
- 设计好AO: 提前构想出需要支持的 API Object 属性,文档化便于后续实现;注意有内部版本和外部版本,他们结构可以不同
- 创建工程: 工程最好创建在 GOPATH 下面的 src 目录中;内部工程目录参考 kubernetes 项目
- 内外部版本: 如果有多个外部版本,那么内部版本即有可能和外部版本有不同的结构;外部版本需要有字段 tag,用于 go 结构和 json 格式的相互转换

外部版本(对接使用者)
package v1alpha
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type JenkinsService struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,name=metadata"`
Spec JenkinsServiceSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`
Status JenkinsServiceStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}
type JenkinsServiceSpec struct {
InstanceAmount int `json:"instanceamount,omitempty" protobuf:"int32,1,opt,name=instanceamount"`
InstanceCpu int `json:"instancecpu,omitempty" protobuf:"int32,2,opt,name=instancecpu"`
}
type JenkinsServiceStatus struct {
ApprovalStatus string `json:"approvalstatus" protobuf:"bytes,1,name=approvalstatus"`
Instances []JenkinsServerInstance `json:"instances" protobuf:"bytes,2,rep,name=instances"`
}
type JenkinsServerInstance struct {
Cpu int `json:"cpu" protobuf:"int32,1,name=cpu"`
Running bool `json:"running" protobuf:"bool,2,name=running"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type JenkinsServiceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
Items []JenkinsService `json:"items" protobuf:"bytes,2,rep,name=items"`
}
# 2. 代码生成
提示
针对 API Object 的内外部版本,代码生成机制会生成:
- default、deepcopy 和版本间 conversion 的代码;
- clientset - 作为 “客户端” 访问我们的 aggregated api server 的 api;
- informer - 作为 controller 等监控 aggregated api server 中资源的 API;
- lister - 从 server 读取资源的基础 API;
- 同时也会生成支持 openapi 的 go 代码
关键步骤:
- 1. 加代码注解: k8s 的代码生成工具
code-gen根据代码上的注解工作,我们需要加上global、local注解来指导生成过程 - 2. 客制化的逻辑: 对于复杂的 API Object,工具生成的 conversion 方法可能不能满足要求,我们可以注入自己的逻辑;Defaulter 的生成也需要我们写
SetDefaults_<Type>方法 - 3. 编写脚本: 引入
code-gen包并通过go mod vendor复制该包到工程目录 vendor 目录下,注意所使用的版本;通过编写生成的脚本来避免使用大量命令行操作 - 4. 检查生成内容: 执行脚本,代码生成完毕后需要查看一下 IDE 的提示和错误
# DeepCopy 方法
- DeepCopy(): 给结构体添加该方法,使得从一个结构体的实例 copy 出一个新实例
- DeepCopyInto(): 给结构体添加该方法,把一个结构体的实例 copy 到另一个实例中
- DeepCopyObject(): 给结构体添加该方法,把一个结构体的实例 copy 到另一个新实例,该新实例的类型时
runtime.Object
// cicd-apiserver/pkg/apis/cicd/v1alpha/doc.go
// +k8s:deepcopy-gen=package
package v1alpha
// cicd-apiserver/pkg/apis/cicd/v1alpha/types.go
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type JenkinsService struct {
# Conversion 方法
- Convert_<内部版本AO>TO<外部版本AO>(): 给结构体添加该方法,使得从一个结构体的内部版本实例到一个外部版本实例
- Convert_<外部版本AO>TO<内部版本AO>(): 反向转换的方法
如何转换为特定转换生成代码
在运行代码生成脚本前,自己按照命名规则,在正确的包下写一个同名的转换方法,代码生成器会直接复用你生成的代码
// cicd-apiserver/pkg/apis/cicd/v1alpha/doc.go
// +k8s:conversion-gen=cicd-apiserver/pkg/apis/cicd
package v1alpha
# Defaulter 方法
- SetObjectDefaults_<AO>: 在这个方法内,AO 实例各个字段的默认值会逐一设置,但只考虑如下字段:他们的类型具有与之相对应的
SetDefaults_<Type>方法,这里的 Type 时该字段的类型
提示
SetDefaults_<Type> 时我们自己手动编写的
// cicd-apiserver/pkg/apis/cicd/v1alpha/doc.go
// +k8s:defaulter-gen=TypeMeta
package v1alpha
# Clientset 相关的包和方法
// 这个注解生成相应的 Clientset 代码
// +genclient
# Informer,Lister,OpenAPI
这些代码也会被 code-gen 生成
code-gen 脚本:
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
SCRIPT_ROOT=$(dirname ${BASH_SOURCE[0]})/..
CODEGEN_PKG=${CODEGEN_PKG:-$(cd ${SCRIPT_ROOT}; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)}
bash ${CODEGEN_PKG}/generate-internal-groups.sh all \
cicd-apiserver/pkg/generated cicd-apiserver/pkg/apis cicd-apiserver/pkg/apis \
"cicd:v1alpha" \
--go-header-file ${SCRIPT_ROOT}/hack/boilerplate.go.txt
# 3. 注册 API Object
提示
利用 Builder 设计模式,向 Scheme 中注册我们的 API Object
内部 register.go
package cicd
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const GroupName = "autobusi.group.cicd"
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}
func Kind(kind string) schema.GroupKind {
return SchemeGroupVersion.WithKind(kind).GroupKind()
}
// 需要的,按名字找到resource实例
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
// 下面的两个var是必须的,因为在生成的代码里直接用了这个包下的这两个变量
var (
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
AddToScheme = SchemeBuilder.AddToScheme
)
// 被SchemeBuilder调用,从而把自己知道的Object(Type)注册到scheme中
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(
SchemeGroupVersion,
&JenkinsService{},
&JenkinsServiceList{},
)
return nil
}
外部 register.go
package v1alpha
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
const GroupName = "autobusi.group.cicd"
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha"}
// 需要的,按名字找到resource实例
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
// var的定义和internal version的register中基本类似,
// 只是创建Builder时多了一个中间产物local scheme builder,local builder会在包括生成代码的init中去使用
var (
SchemeBuilder runtime.SchemeBuilder
localSchemeBuilder = &SchemeBuilder
AddToScheme = localSchemeBuilder.AddToScheme
)
func init() {
//这里去注册本version的类型,以及它们向internal version的转换函数
localSchemeBuilder.Register(addKnownTypes)
}
// 被SchemeBuilder调用,从而把自己知道的Object(Type)注册到scheme中
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(
SchemeGroupVersion,
&JenkinsService{},
&JenkinsServiceList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}
install/install.go
package install
import (
"cicd-apiserver/pkg/apis/cicd"
"cicd-apiserver/pkg/apis/cicd/v1alpha"
"k8s.io/apimachinery/pkg/runtime"
util "k8s.io/apimachinery/pkg/util/runtime"
)
func Install(scheme *runtime.Scheme) {
util.Must(cicd.AddToScheme(scheme))
util.Must(v1alpha.AddToScheme(scheme))
util.Must(scheme.SetVersionPriority(v1alpha.SchemeGroupVersion))
}
# 4. Registry - AO存储和 REST
创建 API Object 实例如何存储到 ETCD 中?为响应 Restful 请求,怎么从 ETCD 中存取 API Object?
关键步骤:
- **Strategy:**针对 Create、Update、Delete 等操作我们可以编写必要的代码区客制化 apiserver 的标准实现,系统在合适的时间去掉用他们
- Store: Kuberbetes 包为 Aggregated API Server 提供了绝大部分处理 ETCD 数据存取的工作,主要的载体就是 Store 结构体。我们需要借用 StandardStore 来制作自己的 Store

# 5. 添加 Admission
Admission 给我们提供了一个扩展点,对 RESTful 请求进行修正和校验
关键步骤:
- Admission Plugin: 首先我们要制作一个 Admission Plugin。这只要实现一个简单的 Admission.Interface 就可以了
- 注入 Informer:(option),但是在 Admission 过程中极有可能需要读取 API Object 信息,这时候就需要 Informer,我们这一步注入 Informer 到 Plugin 中供其使用
- Mutation 接口: Admission 的第一阶段是 mutation,我们可以对接受到的 Restful 请求做一些修改,例如注入 sidecar。Plugin 需要实现 admission.MutationInterface 来参与这个阶段
- **Validation 接口: ** Admission 第二阶段是 Validation,让我们的 Plugin 实现 admission.ValidationInterface 来参与到这个阶段
上面主要的 Interface:
// vendor/k8s.io/apiserver/pkg/admission/interfaces.go
// Interface is an abstract, pluggable interface for Admission Control decisions.
type Interface interface {
// Handles returns true if this admission controller can handle the given operation
// where operation can be one of CREATE, UPDATE, DELETE, or CONNECT
Handles(operation Operation) bool
}
type MutationInterface interface {
Interface
// Admit makes an admission decision based on the request attributes.
// Context is used only for timeout/deadline/cancellation and tracing information.
Admit(ctx context.Context, a Attributes, o ObjectInterfaces) (err error)
}
// ValidationInterface is an abstract, pluggable interface for Admission Control decisions.
type ValidationInterface interface {
Interface
// Validate makes an admission decision based on the request attributes. It is NOT allowed to mutate
// Context is used only for timeout/deadline/cancellation and tracing information.
Validate(ctx context.Context, a Attributes, o ObjectInterfaces) (err error)
}
// Operation is the type of resource operation being checked for admission control
type Operation string
// Operation constants
const (
Create Operation = "CREATE"
Update Operation = "UPDATE"
Delete Operation = "DELETE"
Connect Operation = "CONNECT"
)
具体的代码实现:
// cicd-apiserver
// pkg/admission/plugin/jsplugins.go
package plugin
import (
"cicd-apiserver/pkg/apis/cicd"
"context"
"fmt"
"io"
informers "cicd-apiserver/pkg/generated/informers/internalversion"
listers "cicd-apiserver/pkg/generated/listers/cicd/internalversion"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apimachinery/pkg/labels"
)
// plugin必须实现admission.Interface接口,而内嵌的admission.Handler结构体就实现了
type JenkinsServicePlugin struct {
*admission.Handler
jsLister listers.JenkinsServiceLister
}
// 把这个plugin注册进api server体系中的方法,会在server启动的代码中调用
// plugin 参数是server的plugins集合,需要把我们的放进去
func Register(plugin *admission.Plugins) {
plugin.Register("JenkinsService", func(config io.Reader) (admission.Interface, error) {
return New()
})
}
// 创建plugin结构体实例的方法
func New() (*JenkinsServicePlugin, error) {
return &JenkinsServicePlugin{
Handler: admission.NewHandler(admission.Create),
}, nil
}
// 有了validate方法就实现了admission.ValidationInterface,从而在validating阶段被调用
func (jsp *JenkinsServicePlugin) Validate(ctx context.Context, a admission.Attributes, _ admission.ObjectInterfaces) error {
if a.GetKind().GroupKind() != cicd.Kind("JenkinsService") { //所有object的valid都会进来,所以我们要验一下是不是该关心的
return nil
}
if !jsp.WaitForReady() { // 例如informer还没有把远程信息sync到本地
return admission.NewForbidden(a, fmt.Errorf("the plugin isn't ready for handling request"))
}
// 下面就可以进行我们期望的校验了
// 区别于registry部分strategy中的valid strategy,此处的校验更多是多实体之间的关联正确性,而不是单个jenkins service内容的正确
// 例如,我们规定整个系统中只能存在10 个JenkinsService对象,多了不行,就可以在这里做检查
existedJenkinsServices, err := jsp.jsLister.List(labels.Everything())
if err != nil {
return admission.NewForbidden(a, fmt.Errorf("the plugin encounter internal error during retrieve jenkins service objects from api server"))
}
if len(existedJenkinsServices) >= 10 {
return admission.NewForbidden(a, fmt.Errorf("too many service instances exist, %d exist but max is 10", len(existedJenkinsServices)))
}
return nil
}
// 有了这个方法,plugin就实现了WantsCicdInformerFactory接口,可以获取到cicd informer了
func (jsp *JenkinsServicePlugin) SetInformerFactory(f informers.SharedInformerFactory) {
jsp.jsLister = f.Autobusi().InternalVersion().JenkinsServices().Lister()
jsp.SetReadyFunc(f.Autobusi().InternalVersion().JenkinsServices().Informer().HasSynced)
}
// cicd-apiserver
// pkg/admission/informerinjector.go
package admission
import (
informers "cicd-apiserver/pkg/generated/informers/internalversion"
"k8s.io/apiserver/pkg/admission"
)
// 需要admission plugin去实现这个接口,从而保证可以接收informerfactory;
type WantsCicdInformerFactory interface {
SetInformerFactory(informers.SharedInformerFactory)
}
type cicdInformerPluginInitializer struct {
informers informers.SharedInformerFactory
}
func (i cicdInformerPluginInitializer) Initialize(plugin admission.Interface) {
if wants, ok := plugin.(WantsCicdInformerFactory); ok { //如果目标plugin通过实现接口,声明需要cicd informer,那么我们就给它
wants.SetInformerFactory(i.informers)
}
}
// server启动时在config阶段被调用,从而把informer交给plugin
func NewCicdInformerPluginInitializer(informers informers.SharedInformerFactory) cicdInformerPluginInitializer {
return cicdInformerPluginInitializer{
informers: informers,
}
}
# 5. 制作 Web Server
一个 Aggregated API Server 是一个可以响应 Restful 请求的 Web Server,在 k8s 的 apiserver 子工程也为我们提供了全部的 web server 实现,我们只需要加入客制化的配置项,然后接受用户命令行输入,转化为 server 可消费的结构,启动 server 就可以了
在 server 启动过程中,还涉及到触发 API Object 向 Scheme 的注册,初始化 Admission Plugin 相关的工作
主要步骤:
- 编写 Server 实例: 把 apiserver 包一层,插入自己的客制化逻辑;提供启动 Server 的方法
- 对接命令行: 基于 cobra 框架开发命令行对接程序,接受用户启动 Server 时在命令行的输入
- 触发初始化: 在把 Option 传递给 Server 实例的过程中,触发 API Object 的注册,Informer 的初始化,Admission 的创建和注册
关键代码
// cicd-apiserver
// cmd/server/server.go
package server
import (
"fmt"
"net"
"github.com/spf13/cobra"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apiserver/pkg/admission"
genericserver "k8s.io/apiserver/pkg/server"
genericoptions "k8s.io/apiserver/pkg/server/options"
myadmission "cicd-apiserver/pkg/admission"
"cicd-apiserver/pkg/admission/plugin"
"cicd-apiserver/pkg/apis/cicd/v1alpha"
"cicd-apiserver/pkg/apiserver"
clientset "cicd-apiserver/pkg/generated/clientset/internalversion"
informers "cicd-apiserver/pkg/generated/informers/internalversion"
)
const etcdPathPrefix = "/registry/cicd-apiserver.autobusi.com"
// 以下环节制作option,option是server启动参数,由它进一步制作config,再然后由config制造server
type ServerOptions struct {
RecommendedOptions *genericoptions.RecommendedOptions
SharedInformerFactory informers.SharedInformerFactory
}
func NewServerOptions() *ServerOptions {
o := &ServerOptions{
RecommendedOptions: genericoptions.NewRecommendedOptions(
etcdPathPrefix,
apiserver.Codecs.LegacyCodec(v1alpha.SchemeGroupVersion),
),
}
return o
}
func (o *ServerOptions) Validate() error {
errors := []error{}
errors = append(errors, o.RecommendedOptions.Validate()...)
//把errors数组合并成单独error
return utilerrors.NewAggregate(errors)
}
func (o *ServerOptions) Complete() error {
//把我们的admission plugin加进去
plugin.Register(o.RecommendedOptions.Admission.Plugins)
//plugin 的相互顺序很重要,最好不要破坏已有顺序,直接加在尾部
o.RecommendedOptions.Admission.RecommendedPluginOrder =
append(o.RecommendedOptions.Admission.RecommendedPluginOrder, "JenkinsService")
return nil
}
func (o *ServerOptions) Config() (*apiserver.Config, error) {
//申请系统分派证书
if err := o.RecommendedOptions.SecureServing.MaybeDefaultWithSelfSignedCerts("localhost", nil, []net.IP{net.ParseIP("127.0.0.1")}); err != nil {
return nil, fmt.Errorf("error creating self-signed certificates: %v", err)
}
//把我们admission用的informer做出来,放入admission初始化器。informer的初始化在post start hook中进行
o.RecommendedOptions.ExtraAdmissionInitializers = func(cfg *genericserver.RecommendedConfig) ([]admission.PluginInitializer, error) {
client, err := clientset.NewForConfig(cfg.LoopbackClientConfig)
if err != nil {
return nil, err
}
informerFactory := informers.NewSharedInformerFactory(client, cfg.LoopbackClientConfig.Timeout)
o.SharedInformerFactory = informerFactory
return []admission.PluginInitializer{myadmission.NewCicdInformerPluginInitializer(informerFactory)}, nil
}
//做config,用于返回
standardConfig := genericserver.NewRecommendedConfig(apiserver.Codecs)
if err := o.RecommendedOptions.ApplyTo(standardConfig); err != nil {
return nil, err
}
myConfig := &apiserver.Config{
GenericConfig: standardConfig,
}
return myConfig, nil
}
// 把server做出来并跑起来
func (o *ServerOptions) Run(stopCh <-chan struct{}) error {
c, err := o.Config()
if err != nil {
return err
}
s, err := c.Complete().NewServer()
if err != nil {
return err
}
s.GenericAPIServer.AddPostStartHook("start-cicd-apiserver-informers", func(context genericserver.PostStartHookContext) error {
c.GenericConfig.SharedInformerFactory.Start(context.StopCh)
o.SharedInformerFactory.Start(context.StopCh)
return nil
})
return s.GenericAPIServer.PrepareRun().Run(stopCh)
}
//以下环节制作cobra command,它可以启动server
func NewCommandStartServer(defaultOptions *ServerOptions, stopCh <-chan struct{}) *cobra.Command {
options := *defaultOptions
cmd := &cobra.Command{
Short: "my cicd api server",
Long: "my api server for demostration purpose",
RunE: func(c *cobra.Command, args []string) error {
if err := options.Complete(); err != nil {
return err
}
if err := options.Validate(); err != nil {
return err
}
if err := options.Run(stopCh); err != nil {
return err
}
return nil
},
}
flags := cmd.Flags()
options.RecommendedOptions.AddFlags(flags)
return cmd
}
// cicd-apiserver
// pkg/apiserver/apiserver.go
package apiserver
import (
"cicd-apiserver/pkg/apis/cicd"
cicdregistry "cicd-apiserver/pkg/registry"
jsstorage "cicd-apiserver/pkg/registry/cicd/jenkinsservice"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/version"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
"cicd-apiserver/pkg/apis/cicd/install"
)
var (
Scheme = runtime.NewScheme()
Codecs = serializer.NewCodecFactory(Scheme)
)
// 如下方法需要更新至相应phase,开始漏掉了
func init() {
install.Install(Scheme)
metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
unversioned := schema.GroupVersion{Group: "", Version: "v1"}
Scheme.AddUnversionedTypes(
unversioned,
&metav1.Status{},
&metav1.APIVersions{},
&metav1.APIGroupList{},
&metav1.APIGroup{},
&metav1.APIResourceList{},
)
}
//如下环节制作Server的Config
type Config struct {
GenericConfig *genericapiserver.RecommendedConfig
// ExtraConfig ExtraConfig // 如果有自己需要的config的话,可以扩展field
}
type completedConfig struct {
GenericConfig genericapiserver.CompletedConfig
}
//完善后的config
type CompletedConfig struct {
*completedConfig
}
type CicdServer struct {
GenericAPIServer *genericapiserver.GenericAPIServer
}
//完善初始的config
func (cfg *Config) Complete() CompletedConfig {
cconfig := completedConfig{
cfg.GenericConfig.Complete(),
}
cconfig.GenericConfig.Version = &version.Info{
Major: "1",
Minor: "0",
}
return CompletedConfig{&cconfig}
}
//有了这个方法,完善后的config就可以制作server的instance了
func (ccfg completedConfig) NewServer() (*CicdServer, error) {
genericServer, err := ccfg.GenericConfig.New("cicd-apiserver", genericapiserver.NewEmptyDelegate())
if err != nil {
return nil, err
}
server := &CicdServer{
GenericAPIServer: genericServer,
}
//重点是把我们各个版本的api object都注入到server中去,开始
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(
cicd.GroupName,
Scheme,
metav1.ParameterCodec,
Codecs,
)
v1alphastorage := map[string]rest.Storage{}
v1alphastorage["jenkinsservices"] = cicdregistry.RESTWithErrorHandler(jsstorage.NewREST(Scheme, ccfg.GenericConfig.RESTOptionsGetter))
apiGroupInfo.VersionedResourcesStorageMap["v1alpha"] = v1alphastorage
if err := server.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
return nil, err
}
return server, nil
}
// main.go
package main
import (
"flag"
genericserver "k8s.io/apiserver/pkg/server"
"k8s.io/component-base/logs"
"k8s.io/klog/v2"
"cicd-apiserver/cmd/server"
)
func main() {
// 把cobra cmd做出来
stopCh := genericserver.SetupSignalHandler()
options := server.NewServerOptions()
cmd := server.NewCommandStartServer(options, stopCh)
cmd.Flags().AddGoFlagSet(flag.CommandLine)
// 初始化以下log, 需要在parse flag之后
logs.InitLogs()
defer logs.FlushLogs()
// 启动
if err := cmd.Execute(); err != nil {
klog.Fatal(err)
}
}
到了这里 Web Server 就制作完成了
# 7. 部署至集群
我们需要把这个 Aggregated API Server 引入集群,让 master api server 知道它的存在。这涉及到制作镜像,创建 Deployment 和 Service 等 API Resource;由于需要和 master api server 通信,还需要 ServiceAccount 以及相应的 Role
主要工作:
- 创建 Dockerfile, 为 Aggregated API Server 制作镜像
- 制作镜像并推入镜像库(或者 local-image-load)
- 集群中创建辅助资源
- Namespace:Server 的 Pod 等资源将部署于此
- ServiceAccount:用于 Aggregated API Server 与集群 Master API Server 交互
- ServiceAccount 的权限:
- get、watch、list 集群内的 namespace
- get、watch、list 集群内的 mutationwebhookconfiguration、validationwebhookconfiguration
- system:auth-delegator
- extension-apiserver-authentication-reader
- Deployment:管理承载 Aggregated API Server 的 Pod
- Service:在集群内暴露 Aggregated API Server
- APIService:通知 Master API Server 知道 Aggregated API Server 的存在和联系
制作此镜像的 Dockerfile:
FROM golang:1.18 as build
WORKDIR /go/src/cicd-apiserver
COPY . .
RUN CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
COPY /go/src/cicd-apiserver/cicd-apiserver /
ENTRYPOINT ["/cicd-apiserver"]