本文为 K8s API 和控制器 系列文章之一

看文章的同时,你可以

  1. 拉取项目 x-kubernetes 设置测试环境(并顺手 star ⭐🤩🌈

     git clone https://github.com/phosae/x-kubernetes.git
     cd x-kubernetes
     make localenv
    
  1. 一键部署本项目

     cd api-aggregation-lib
     make deploy
    

🔮 API 定义和代码生成

实现一个极简 K8s apiserver 展示了 apiserver 的极简实现方式。但它还欠缺一些 apiserver 功能,比如 watch 和数据持久。 而 library k8s.io/apiserver 补全了所有欠缺,包括配置即用的鉴权/授权、etcd 集成等。

本文将使用 libary k8s.io/apiserver 实现 apiserver 全部功能。

首先, API 相关可以变得正式一些。仿照 k8s.io/api 风格 创建独立 API module x-kubernetes/api

  • 目录结构为 {group}/{version}
  • types.go 放置 API structs
  • doc.go 存放代码生成定义
  • register.go 提供 API 注册函数。类型被注册到 runtime.Scheme 之后,apiserver 库中持有 runtime.Scheme 的组件便会知道
    • 反序列化 /apis/hello.zeng.dev/{version}/namespaces/{ns}/foos requestBody ➡️ go struct object Foo/FooList
    • 序列化 go struct object Foo/FooList ➡️ /apis/hello.zeng.dev/{version}/namespaces/{ns}/foos responseBody
    • 如何设置 structs 默认值,如何处理 structs 外部版本与内部版本转换 🔄,等等

原先的 API structs 被放置在 hello.zeng.dev/v1。同时,目录 hello.zeng.dev/v2 增设了 v2 版本 api。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
~/x-kubernetes/api# tree hello.zeng.dev/
hello.zeng.dev/
├── v1
│   ├── doc.go
│   ├── register.go
│   └── types.go
└── v2
    ├── doc.go
    ├── register.go
    └── types.go

hello.zeng.dev/v1 types.go 与 极简 K8s apiserver types 保持了一致。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
type Foo struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

    Spec FooSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"`
}

type FooSpec struct {
    // Msg says hello world!
    Msg string `json:"msg" protobuf:"bytes,1,opt,name=msg"`
    // Msg1 provides some verbose information
    // +optional
    Msg1 string `json:"msg1" protobuf:"bytes,2,opt,name=msg1"`
}

hello.zeng.dev/v2 types.go 基于 v1 做了升级,spec 中引入了 image,并将 msg 和 msg1 移到了 spec.config。同时,引入了 spec 同级字段 status,用来描述实际状态。

v2 版本 API 变得非常符合 Kubernetes-style API types

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Foo struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

    Spec   FooSpec   `json:"spec" protobuf:"bytes,2,opt,name=spec"`
    Status FooStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}

type FooSpec struct {
    // Container image that the container is running to do our foo work
    Image string `json:"image" protobuf:"bytes,1,opt,name=image"`
    // Config is the configuration used by foo container
    Config FooConfig `json:"config" protobuf:"bytes,2,opt,name=config"`
}

type FooConfig struct {
    // Msg says hello world!
    Msg string `json:"msg" protobuf:"bytes,1,opt,name=msg"`
    // Msg1 provides some verbose information
    // +optional
    Msg1 string `json:"msg1,omitempty" protobuf:"bytes,2,opt,name=msg1"`
}

按照 最不厌其烦的 K8s 代码生成教程 生成代码后,即可着手开始构建 apiserver。

👋 The hello.zeng.dev/v1’s CRUD Implementation

支持 hello.zeng.dev/v1 的新 apiserver 主要看 2 个 commits 即可,commit: build apiserver ontop librarycommit: add etcd store

commit: build apiserver ontop library 提供了 实现一个极简 K8s apiserverk8s.io/apiserver 库实现版,数据存储在内存。 提交文件很少,且大部分代码都在和库打交道

  ├── main.go              # 入口,设置 signal handler,调用 package cmd 并运行之
  └── pkg
      ├── apiserver
      │   └── apiserver.go # 组合各模块:设置 Scheme,创建 rest.Storage,初始化并启动 apiserver
      ├── cmd
      │   └── start.go     # 解析框架和自定义命令行参数,补全并校验配置,创建并运行 custom apiserver
      └── registry
          └── foo.go       # 实现框架接口 rest.StandardStorage,实现 foo CRUD

pkg/apiserver/apiserver.go 作用就是构建 k8s.io/apiserver 枢纽 —— GenericAPIServer。 所有组件都会体现在这个结构体中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import(
    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"
    genericapiserver "k8s.io/apiserver/pkg/server"
)

var (
    // Scheme defines methods for serializing and deserializing API objects.
    Scheme = runtime.NewScheme()
    // Codecs provides methods for retrieving codecs and serializers for specific
    // versions and content types.
    Codecs = serializer.NewCodecFactory(Scheme)
)

func init() {
    hello.Install(Scheme)

    metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Group: "", Version: "v1"})
    unversioned := schema.GroupVersion{Group: "", Version: "v1"}
    Scheme.AddUnversionedTypes(unversioned,
        &metav1.Status{},
        &metav1.APIVersions{},
        &metav1.APIGroupList{},
        &metav1.APIGroup{},
        &metav1.APIResourceList{},
    )
}

// New returns a new instance of WardleServer from the given config.
func (c completedConfig) New() (*HelloApiServer, error) {
    genericServer, err := c.GenericConfig.New("hello.zeng.dev-apiserver", genericapiserver.NewEmptyDelegate())
    s := &HelloApiServer{ GenericAPIServer: genericServer}

    apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(hellov1.GroupName, Scheme, metav1.ParameterCodec, Codecs)

    apiGroupInfo.VersionedResourcesStorageMap["v1"] = map[string]rest.Storage{
        "foos": registry.NewFooApi(),
    }

    if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
        return nil, err
    }

    return s, nil
}

pkg/registry/foo.go 实现了 interface rest.StandardStorage 除 rest.Watcher 之外所有接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
"k8s.io/apiserver/pkg/registry/rest"

var _ rest.ShortNamesProvider = &fooApi{}
var _ rest.SingularNameProvider = &fooApi{}
var _ rest.Getter = &fooApi{}
var _ rest.Lister = &fooApi{}
var _ rest.CreaterUpdater = &fooApi{}
var _ rest.GracefulDeleter = &fooApi{}
var _ rest.CollectionDeleter = &fooApi{}

// var _ rest.StandardStorage = &fooApi{} // implements all interfaces of rest.StandardStorage except rest.Watcher

GenericAPIServer 接到 fooApi 和 Scheme 注册之后,便会按照框架协议将它们转化为对应 REST Handlers。

commit: add etcd store 支持了 etcd 存储

pkg
├── apiserver
│   └── apiserver.go # --enable-etcd-storage=true 则加载 etcd 存储实现
├── cmd
│   └── start.go     # --enable-etcd-storage=true 则加载 etcd 配置项们
└── registry/hello.zeng.dev/foo
    ├── etcd.go      # 初始化框架 etcd 存储实现,加载各种策略(CRUD、
    ├── strategy.go  # 实现各种策略
    └── mem.go       # mv pkg/registry/foo.go ---> pkg/registry/hello.zeng.dev/foo/mem.go

pkg/apiserver/apiserver.go 改动很小,即支持根据配置调整存储实现,当 enable-etcd-storage 为 true 时使用 etcd 存储实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func (c completedConfig) New() (*HelloApiServer, error) {
    genericServer, _ := c.GenericConfig.New("hello.zeng.dev-apiserver", genericapiserver.NewEmptyDelegate())
    ...
    s := &HelloApiServer{GenericAPIServer: genericServer}

    apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(
        hellov1.GroupName, Scheme, metav1.ParameterCodec, Codecs)

    apiGroupInfo.VersionedResourcesStorageMap["v1"] = map[string]rest.Storage{}
    if c.ExtraConfig.EnableEtcdStorage {
        etcdstorage, err := fooregistry.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter)
        if err != nil {
            return nil, err
        }
        apiGroupInfo.VersionedResourcesStorageMap["v1"]["foos"] = etcdstorage
    } else {
        apiGroupInfo.VersionedResourcesStorageMap["v1"]["foos"] = fooregistry.NewMemStore()
    }

    if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
        return nil, err
    }
    return s, nil
}

pkg/registry/hello.zeng.dev/foo/etcd.go 只有一个 func NewREST,它干的活是

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package foo

import (
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apiserver/pkg/registry/generic"
    genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"

    hellov1 "github.com/phosae/x-kubernetes/api/hello.zeng.dev/v1"
)

// NewREST returns a RESTStorage object that will work against API services.
func NewREST(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*genericregistry.Store, error) {
    strategy := NewStrategy(scheme)

    store := &genericregistry.Store{
        NewFunc:                   func() runtime.Object { return &hellov1.Foo{} },
        NewListFunc:               func() runtime.Object { return &hellov1.FooList{} },
        PredicateFunc:             MatchFoo,
        DefaultQualifiedResource:  hellov1.Resource("foos"),
        SingularQualifiedResource: hellov1.Resource("foos"),

        CreateStrategy: strategy,
        UpdateStrategy: strategy,
        DeleteStrategy: strategy,
        TableConvertor: strategy,
    }
    options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: GetAttrs}
    if err := store.CompleteWithOptions(options); err != nil {
        return nil, err
    }
    return store, nil
}

pkg/registry/hello.zeng.dev/foo/strategy.go 实现了 Create/Update/Delete 策略,但它们基本都是空函数,主要就写了个 TableConvertor…。部分策略由 nested runtime.ObjectType 和 names.NameGenerator 实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type fooStrategy struct {
    runtime.ObjectTyper
    names.NameGenerator
}

func (fooStrategy) NamespaceScoped() bool {
    return true
}

func (fooStrategy) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (
    *metav1.Table, error) {/*...*/}

由于 k8s.io/apiserver pkg/registry/generic/registry.Store 提供了 etcd 存储实现,因此项目需要做的就是在框架内涂鸦——提供策略。官方库 kubernertes/pkg/registry 也采用了这种方式。

✍️ 主要组件梳理

回顾两次 commits,可以发现 k8s.io/apiserver 架构相对简单

每个 APIGroupInfo 中包含了

  • 存储接口实现集 map[string/*(version*)/][string/*(kind_plural*)/]rest.Storage (rest.Storage 仅是支持注册 GroupVersion 级 API,类似 /apis/hello.zeng.dev/v1,所以实际实现一般为 rest.StandardStorage,这样就可以支持资源 kind 的 CRUD,类似 /apis/hello.zeng.dev/v1/foos)
  • 包含资源 group kinds 的编解码、默认值、转化等信息的 runtime.Scheme
  • Codecs
    • 支持将 URL Query Params 转化 metav1.CreateOptions,metav1.GetOptions,metav1.UpdateOptions 等的 metav1.ParameterCodec
    • 负责 API structs(注册在 runtime.Scheme 中)序列化和反序列化的 struct runtime/serializer.CodecFactory

APIGroupInfo install 到 GenericAPIServer 后,就转化为

  • Discovery API handlers( supports /apis/{group} /apis/{group}/{version}
  • Object/Resource API Handlers (supports CRUD /apis/{group}/{version}/**/{kind_plural})

GenericAPIServer 集成了通用的 HTTP REST Handlers 模块 k8s.io/apiserver pkg/endpoints。而 interface rest.StandardStoragek8s.io/apiserver pkg/endpoints handlers 提供存储策略。

实现方可以从 0 到 1 实现 interface rest.StandardStorage,类似这里的 mem.go fooApi。 k8s.io/apiserver pkg/registry/generic/registry.Store 实现了 interface rest.StandardStorage, 使用方只需要提供简单 CRUD、校验等策略即可集成到存储层,比如这里的 fooStratedy。

registry.Store 并不直接与 etcd 交互,而是持有了抽象接口 sotrage.Interface。 storage 下一级 package etcd3/store.go 提供了 etcd3 实现,package cacher/cacher.go 提供了缓存层实现。 sotrage.Interfaceinterface rest.StandardStorage 等抽象解耦了业务层和存储层,使得项目可以采纳非 etcd 存储,比如

🔄 Supports multiversion GroupKind

为支持 hello.zeng.dev/v2,新 apiserver commits 如下

k8s.io/apiserver 使用多版本 API 时 (这里是 x-kubernetes/api),涉及一系列类型转换

  1. 任意类型不论对外有多少个版本,其内存版本唯一。 该内存版本一般称之为 Memory/Internal/Hub Version(以下称之为内存版本或者内部版本)
  2. func (s *Scheme) SetVersionPriority(versions ...schema.GroupVersion) error –> 在多个版本之间,需要显式设置 preferredVersion。 kubectl {action} {kind} 默认取 preferredVersion,写入存储的一般也是 preferredVersion。 GET /apis/{group} 可以获取该 group 的 preferredVersion 信息
  3. 由外而内经过计算写入存储,会经历这个转换 RequestVersion kind ➡️ MemoryVersion kind ➡️ StorageVersion kind
  4. 从存储经过计算返回客户端,则经历这个转换 StorageVersion kind ➡️ MemoryVersion kind ➡️ RequestVersion kind
  5. 普通版本🔄内存版本:核心在于版本之间的两两转换。 因此需要向 runtime.Scheme 注册转换函数 func (s *Scheme) AddConversionFunc(from, to interface{}, fn conversion.ConversionFunc) error

k8s.io/apiserver 版本转换实现是 struct runtime/serializer.CodecFactory,实现了 interface runtime.NegotiatedSerializer(面向 HTTP 层)和 interface runtime.StorageSerializer(面向存储层)。核心使用方式

  1. func SupportedMediaTypes() []SerializerInfo 返回最底层的 encode/decode 实现 (struct 🔄 binary),使用方根据 mediaType 选择最佳 encoder/decoder
  2. func EncoderForVersion(serializer Encoder, gv GroupVersioner) EncoderDecoderToVersion(serializer Decoder, gv GroupVersioner) Decoder 接收 encoder/decoder 和 GroupVersioner,返回出支持将 struct encode/decode 到某个特定版本的包装实现(正是这个包装实现提供了 encode/decode 增强,支持版本转换、设置默认值等)

类似 Kubernetes, 对外 API 库 (k8s.io/api) 仅包含外部 API 定义,仅提供了注册、protobuf 定义和 deepcopy, 内部库(pkg/api 和 pkg/apis)则提供了类型默认值设置、类型字段校验、内外类型转换这些贴近业务的函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
k8s.io/kubernetes $ tree vendor/k8s.io/api/storage

vendor/k8s.io/api/storage
├── OWNERS
├── v1
│   ├── doc.go
│   ├── generated.pb.go
│   ├── generated.proto
│   ├── register.go
│   ├── types.go
│   └── zz_generated.deepcopy.go
├── v1alpha1
│   ├── doc.go
│   ├── generated.pb.go
│   ├── generated.proto
│   ├── register.go
│   ├── types.go
│   ├── zz_generated.deepcopy.go
└── v1beta1
    ├── ...
    ├── ...

k8s.io/kubernetes $ tree pkg/apis/storage

pkg/apis/storage
├── install
│   └── install.go
├── v1
│   ├── defaults.go
│   ├── doc.go
│   ├── register.go
│   ├── zz_generated.conversion.go
│   └── zz_generated.defaults.go
├── v1alpha1
│   ├── doc.go
│   ├── register.go
│   ├── zz_generated.conversion.go
│   └── zz_generated.defaults.go
├── v1beta1
│   ├── ...
│   ├── ...
├── doc.go
├── register.go
├── types.go
└── zz_generated.deepcopy.go

回过头看 commit: add hello.zeng.dev/v2 internal 也采用了类似结构

  • pkg/api/{group}/types.go 存放 internal version
  • pkg/api/{group}/{version}/ 有外部 version 默认值函数 defaults.go,有 conversion.go 协助版本转换 external 🔄 internal,有 register.go 简单引用并包装 x-kubernetes/api 注册
  • pkg/install/install.go 注册所有版本到 runtime.Scheme

⚠️⚠️⚠️ 实现上,在 {group}/types.go 文件中定义 internal struct 非必要。比如可以挑选最新的 API struct,同时将它注册为 external version 和 internal version,只要定义好版本之间的转换即可。

  ~/x-kubernetes/api-aggregation-lib# tree pkg/api/
  pkg/api/
  └── hello.zeng.dev
      ├── install
      │   └── install.go
      ├── v1
      │   ├── conversion.go
      │   ├── defaults.go
      │   ├── doc.go
      │   └── register.go
      ├── v2
      │   ├── defaults.go
      │   ├── doc.go
      │   └── register.go
      ├── doc.go
      ├── register.go
      └── types.go

大部分 default funcs, conversion funcs 全部由自动生成。执行 ./hack/update-codegen-docker.sh 之后,由各目录 doc.go 生成声明产生这些生成文件 commit: gen hello.zeng.dev/v2 internal codes

  pkg/api/
  └── hello.zeng.dev
      ├── v1
      │   └── zz_generated.conversion.go
      ├── v2
      │   ├── zz_generated.conversion.go
      │   └── zz_generated.defaults.go
      └── zz_generated.deepcopy.go

引入 API、定义好内部类型、默认值设置函数、转换函数,准备好它们的注册函数之后,实际的业务逻辑改动非常小 commit: supports CRUD hello.zeng.dev/v2 foos: 71 additions and 52 deletions (而 commit: add hello.zeng.dev/v2 internal: 261 additions and 4 deletions)。改动仅是保证 pkg/api 们都注册到 runtime.Scheme,全部引用外部类型改为只引用内部类型,在 APIGroupInfo 中设置好多版本而已。这说明 k8s.io/apiserver 包办了大部分事情。

⚙️ 按配置引入组件

搞懂 K8s apiserver aggregation 提到了官方 kube-apiserver 处理请求的一般流程

request ➡️ filterchain ➡️ kube-aggregator ➡️ apiservers

而使用 k8s.io/apiserver library,custom apiserver 也会按照配置加载 通用 filters/middlewares

commit: add authn/authz 通过少量代码即开启了 authn/authz。默认情况下,对应 middleware 会加载 InCluster kubeconfig

  • 提供 authn:对于任何资源请求 /apis/{group}/{version}/**,校验 HTTPS 证书和 Headers,如果鉴别请求来自 kube-apiserver,authn 通过。否则发起 tokenreviews,委托 kube-apiserver 认证用户信息
  • 提供 authz:对于任何资源请求 /apis/{group}/{version}/**,发起 subjectaccessreviews, 委托 kube-apiserver 给用户授权

具体原理和细节可以进一步查阅 搞懂 K8s apiserver aggregation

commit: add etcd store 也是类似,引入 etcd 配置项,Complete 完善 etcd 配置之后,registry 层通过 GenericConfig.RESTOptionsGetter 即可集成 etcd 存储。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
func (o *Options) Flags() (fs cliflag.NamedFlagSets) {
    ......
    msfs.BoolVar(&o.EnableEtcdStorage, "enable-etcd-storage", false, "If true, store objects in etcd")
    o.Etcd.AddFlags(fs.FlagSet("Etcd"))
    return fs
}
---
func (o Options) ServerConfig() (*myapiserver.Config, error) {
    ......
    if o.EnableEtcdStorage {
        if err := o.Etcd.Complete(apiservercfg.Config.StorageObjectCountTracker, apiservercfg.Config.DrainedNotify(), apiservercfg.Config.AddPostStartHook); err != nil {
            return nil, err
        }
        if o.Etcd.ApplyWithStorageFactoryTo(serverstorage.NewDefaultStorageFactory(
            o.Etcd.StorageConfig,
            o.Etcd.DefaultStorageMediaType,
            myapiserver.Codecs,
            serverstorage.NewDefaultResourceEncodingConfig(myapiserver.Scheme),
            apiservercfg.MergedResourceConfig,
            nil), &apiservercfg.Config); err != nil {
            return nil, err
        }
        klog.Infof("etcd cfg: %v", o.Etcd)
        o.Etcd.StorageConfig.Paging = utilfeature.DefaultFeatureGate.Enabled(features.APIListChunking)
    }
    ......
}
---
func (c completedConfig) New() (*HelloApiServer, error) {
    ......
    if c.ExtraConfig.EnableEtcdStorage {
        etcdstorage, err := fooregistry.NewREST(Scheme, c.GenericConfig.RESTOptionsGetter)
        if err != nil {
            return nil, err
        }
        apiGroupInfo.VersionedResourcesStorageMap["v1"]["foos"] = etcdstorage
    }
    ......
}

🎮 Play

Watch

分页

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# paging
kubectl get --raw '/apis/hello.zeng.dev/v2/foos?limit=1' \
| jq -r '.apiVersion,.kind,("item: " + .items[].metadata.namespace + "/" + .items[].metadata.name),("continue: "+ .metadata.continue)'

hello.zeng.dev/v2
FooList
item: default/myfoo
continue: eyJ2IjoibWV0YS5rOHMuaW8vdjEiLCJydiI6MTQ2LCJzdGFydCI6ImRlZmF1bHQvbXlmb29cdTAwMDAifQ

kubectl get --raw '/apis/hello.zeng.dev/v2/foos?limit=1&continue=eyJ2IjoibWV0YS5rOHMuaW8vdjEiLCJydiI6MTQ2LCJzdGFydCI6ImRlZmF1bHQvbXlmb29cdTAwMDAifQ' \
| jq -r '.apiVersion,.kind,("item: " + .items[].metadata.namespace + "/" + .items[].metadata.name),("continue: "+ .metadata.continue)'

hello.zeng.dev/v2
FooList
item: default/test
continue: eyJ2IjoibWV0YS5rOHMuaW8vdjEiLCJydiI6MTQ2LCJzdGFydCI6ImRlZmF1bHQvdGVzdFx1MDAwMCJ9

kubectl get --raw '/apis/hello.zeng.dev/v2/foos?limit=1&continue=eyJ2IjoibWV0YS5rOHMuaW8vdjEiLCJydiI6MTQ2LCJzdGFydCI6ImRlZmF1bHQvdGVzdFx1MDAwMCJ9' \
| jq '.apiVersion,.kind,("item: " + .items[].metadata.namespace + "/" + .items[].metadata.name),("continue: "+ .metadata.continue)'

hello.zeng.dev/v2
FooList
item: kube-public/myfoo
continue:

custom apiserver authn/authz 集成 kube-apiserver RBAC

创建 default/readuser,通过 kube-apiserver RBAC 授予官方资源读取权限

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
kubectl create -f << EOF -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: readuser
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: readuser::view
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: view
subjects:
  - kind: ServiceAccount
    name: readuser
    namespace: default
EOF

利用 x-kubernetes/gen-sa-kubeconfig.sh 生成 readuser kubeconfig

1
2
3
4
5
6
7
8
root@dev:~/x-kubernetes# ./hack/gen-sa-kubeconfig.sh default readuser
Cluster "kind-kind" set.
User "default-readuser" set.
Context "default" modified.
Switched to context "default".

root@dev:~/x-kubernetes# ls default-readuser.kubeconfig 
default-readuser.kubeconfig

测试 custom apiserver authn/authz,因为 readuser 只能访问官方资源,所以访问 foos 会遭拒

1
2
3
4
5
6
7
8
# forward local 6443 to cluster custom apiserver service 443
kubectl -n hello port-forward svc/apiserver 6443:443
---

KUBECONFIG=default-readuser.kubeconfig k -s https://localhost:6443 --insecure-skip-tls-verify get fo
Error from server (Forbidden): foos.hello.zeng.dev is forbidden: 
User "system:serviceaccount:default:readuser" 
cannot list resource "foos" in API group "hello.zeng.dev" in the namespace "default"

通过 kube-apiserver RBAC 授予 readuser hello.zeng.dev group 读取权限

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
cat << EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: hello-view
rules:
- apiGroups:
  - hello.zeng.dev
  resources:
  - '*'
  verbs:
  - get
  - list
  - watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: readuser::hello-view
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: hello-view
subjects:
  - kind: ServiceAccount
    name: readuser
    namespace: default
EOF

再测试时 readuser 已经获得了读取权限

1
2
3
4
5
6
7
8
# forward local 6443 to cluster custom apiserver service 443
kubectl -n hello port-forward svc/apiserver 6443:443
---

KUBECONFIG=default-readuser.kubeconfig k -s https://localhost:6443 --insecure-skip-tls-verify get fo
NAME    STATUS   AGE
myfoo            13h
test             12h

🔢 总结

k8s.io/apiserver 主要 package 如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ tree k8s.io/apiserver/pkg -L 1
k8s.io/apiserver/pkg
├── admission           # admission 库,支持 Resource validation, mutation, conversion 等
├── apis                # library 内部 API,主要是配置定义
├── audit               # 审计 HTTP middleware
├── authentication      # authn HTTP middleware
├── authorization       # authz HTTP middleware
├── cel                 # Google Common Expression Language support,支持将处理逻辑内嵌到 Object field 中
├── endpoints           # HTTP 通用实现: filters, REST handlers 等
├── features            # APIServer 功能开关
├── quota               # resource quota 库
├── registry            # 通用 storage 层,支持注册各种类型如 Pod Foo 的 CRUD 实现和策略
├── server              # 聚合其他所有层,the plumbing to create kubernetes-like API server comman
├── storage             # 存储层抽象
├── ...      

使用库代码,或引用、或简单配置,即解决了 实现一个极简 K8s apiserver 中遗留问题

  • ✅ authentication 和 authorization,不区分请求来源,接收任意客户端请求,且没有权限控制,任意用户都拥有增删改查权限
  • ✅ watch,比如 GET /apis/hello.zeng.dev/v1/watch/foos,或者 GET /apis/hello.zeng.dev/v1/foos?watch=true
  • ✅ list 分页
  • ✅ 数据持久

且带来了附加好处

  • 🍺🍖 官方 apiserver 同款类库,方便借鉴/集成社区成果
  • 🍺🍖 多版本 API 支持
  • 🍺🍖 依赖接口而非实现,等等

k8s.io/apiserver 是一个接近框架的类库,这意味着使用上有一定学习成本。 比如需要理解各模块配置项的集成、补全和校验,需要理解资源类型的内部版本和外部版本转换,需要学习代码生成。

高级类库是把双刃剑。引入抽象一方面实现了依赖解耦,另一方面增加了复杂性。 观察本文贴出的 commits 可以发现,custom apiserver 中很多代码只是在加载和适配类库。 随着项目扩大和定制化增加到一定程度,类库相关代码比例才会逐渐减少,起到纯辅助的作用。

总之,k8s.io/apiserver 许多功能均可通过配置插拔,灵活度较高。抽象也相对简单,枢纽是 GenericAPIServer,类型序列化/反序列化和转换在 runtime.Scheme,存储是 interface rest.StandardStorage(通用实现是 k8s.io/apiserver pkg/registry/generic/registry.Store),HTTP 层是 k8s.io/apiserver pkg/endpoints