自定义资源实际上是为了扩展 kubernetes 的 API,向 kubenetes API 中增加新类型,可以使用以下三种方式:
Aggregated(聚合的)API server
是为了将原来的 API server 这个巨石(monolithic)应用给拆分开,为了方便用户开发自己的 API server 集成进来,而不用直接修改 Kubernetes 官方仓库的代码,这样一来也能将 API server 解耦,方便用户使用实验特性,简而言之,它是允许k8s的开发人员编写一个自己的服务,可以把这个服务注册到k8s的api里面,这样,就像k8s自己的api一样,自定义的服务只要运行在k8s集群里面,k8s 的Aggregate通过service名称就可以转发到我们自定义的service里面去了。这些 API server 可以跟 kube-apiserver
无缝衔接,使用 kubectl 也可以管理它们。
在 1.7+
版本及以后,聚合层apiserver和 kube-apiserver 一起运行。在扩展资源被注册前,聚合层不执行任何操,要注册其 API,用户必需添加一个 APIService
对象,该对象需在 Kubernetes API 中声明 URL 路径,聚合层将发送到该 API 路径(e.g. /apis/myextension.mycompany.io/v1/…)的所有对象代理到注册的 APIService。
通常,通过在集群中的一个 Pod 中运行一个 extension-apiserver
来实现 APIService。如果已添加的资源需要主动管理,这个 extension-apiserver 通常需要和一个或多个controller配对。
我们说的自定义API其实就是和Metrics Server的实现方式一样,都是通过注册API的形式来完成和Kubernetes的集成的,也就是在API Server增加原本没有的API。不过添加API还可以通过CRD的方式完成,不过我们这里直说聚合方式。看下图:
Kube-Aggregator类似于一个七层负载均衡,将来自用户的请求拦截转发给其他服务器,并且负责整个 APIServer 的 Discovery 功能。
通过APIServices对象关联到某个Service来进行请求的转发,其关联的Service类型进一步决定了请求转发形式。Aggregator包括一个GenericAPIServer和维护自身状态的Controller。其中 GenericAPIServer主要处理apiregistration.k8s.io组下的APIService资源请求。
主要controller包括:
假设有两个路由分别访问API,实际上我们访问API的时候访问的是一个aggregator的代理层,下面橙色的都是可用的服务后端。我们访问上图中的2个URL其实是被代理到不同的后端,在这个机制下你可以添加更多的后端,比如举例说说Custome-metrics-apiserver绿色线条的路径。
该文件的主要作用就是向Api server注册一个api,此API名称是关联到一个service名称上。
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1beta1.custom.metrics.k8s.io
labels:
api: custom-metrics-apiserver
apiserver: "true"
spec:
version: v1beta1 #API版本
group: custom.metrics.k8s.io #API所属的组
groupPriorityMinimum: 2000
service:
name: custom-metrics-apiserver #自定义API所关联的service名称,当访问这个自定义API后转发到哪个service处理,就根据这个service名称选择
namespace: default
versionPriority: 10
caBundle: "LS0tLS1CRUdJTiBDRVJUSUZJQ0"
上面定义了资源类型为APIService,service名称为custom-metrics-apiserver,空间为default的一个资源聚合接口。
下面带大家从源代码的角度来看,代码路径:
staging/src/k8s.io/kube-aggregator/pkg/apiserver/apiservice_controller.go ,和k8s其它controller一样,watch变化分发到add、update和delete方法
apiServiceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: c.addAPIService,
UpdateFunc: c.updateAPIService,
DeleteFunc: c.deleteAPIService,
})
主要监听两种资源apiService和service,路径:
staging/src/k8s.io/kube-aggregator/pkg/apiserver/apiserver.go
func (s *APIAggregator) AddAPIService(apiService *v1.APIService) error {
// if the proxyHandler already exists, it needs to be updated. The aggregation bits do not
// since they are wired against listers because they require multiple resources to respond
if proxyHandler, exists := s.proxyHandlers[apiService.Name]; exists {
proxyHandler.updateAPIService(apiService)
if s.openAPIAggregationController != nil {
s.openAPIAggregationController.UpdateAPIService(proxyHandler, apiService)
}
return nil
}
proxyPath := "/apis/" + apiService.Spec.Group + "/" + apiService.Spec.Version
// v1. is a special case for the legacy API. It proxies to a wider set of endpoints.
if apiService.Name == legacyAPIServiceName {
proxyPath = "/api"
}
// register the proxy handler
proxyHandler := &proxyHandler{
localDelegate: s.delegateHandler,
proxyCurrentCertKeyContent: s.proxyCurrentCertKeyContent,
proxyTransport: s.proxyTransport,
serviceResolver: s.serviceResolver,
egressSelector: s.egressSelector,
}
proxyHandler.updateAPIService(apiService)
if s.openAPIAggregationController != nil {
s.openAPIAggregationController.AddAPIService(proxyHandler, apiService)
}
s.proxyHandlers[apiService.Name] = proxyHandler
s.GenericAPIServer.Handler.NonGoRestfulMux.Handle(proxyPath, proxyHandler)
s.GenericAPIServer.Handler.NonGoRestfulMux.UnlistedHandlePrefix(proxyPath+"/", proxyHandler)
// if we're dealing with the legacy group, we're done here
if apiService.Name == legacyAPIServiceName {
return nil
}
// if we've already registered the path with the handler, we don't want to do it again.
if s.handledGroups.Has(apiService.Spec.Group) {
return nil
}
// it's time to register the group aggregation endpoint
groupPath := "/apis/" + apiService.Spec.Group
groupDiscoveryHandler := &apiGroupHandler{
codecs: aggregatorscheme.Codecs,
groupName: apiService.Spec.Group,
lister: s.lister,
delegate: s.delegateHandler,
}
// aggregation is protected
s.GenericAPIServer.Handler.NonGoRestfulMux.Handle(groupPath, groupDiscoveryHandler)
s.GenericAPIServer.Handler.NonGoRestfulMux.UnlistedHandle(groupPath+"/", groupDiscoveryHandler)
s.handledGroups.Insert(apiService.Spec.Group)
return nil
}
结合上面的源码:
proxyPath := "/apis/" + apiService.Spec.Group + "/" + apiService.Spec.Version
例子就是/apis/custom-metrics.k8s.io/v1beta1,而处理方法请求的handle就是
// register the proxy handler
proxyHandler := &proxyHandler{
localDelegate: s.delegateHandler,
proxyCurrentCertKeyContent: s.proxyCurrentCertKeyContent,
proxyTransport: s.proxyTransport,
serviceResolver: s.serviceResolver,
egressSelector: s.egressSelector,
}
proxyHandler.updateAPIService(apiService)
updateAPIService就是更新这个proxy的后端service,路径:
staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy.go
func (r *proxyHandler) updateAPIService(apiService *apiregistrationv1api.APIService) {
if apiService.Spec.Service == nil {
r.handlingInfo.Store(proxyHandlingInfo{local: true})
return
}
proxyClientCert, proxyClientKey := r.proxyCurrentCertKeyContent()
newInfo := proxyHandlingInfo{
name: apiService.Name,
restConfig: &restclient.Config{
TLSClientConfig: restclient.TLSClientConfig{
Insecure: apiService.Spec.InsecureSkipTLSVerify,
ServerName: apiService.Spec.Service.Name + "." + apiService.Spec.Service.Namespace + ".svc",
CertData: proxyClientCert,
KeyData: proxyClientKey,
CAData: apiService.Spec.CABundle,
},
},
serviceName: apiService.Spec.Service.Name,
serviceNamespace: apiService.Spec.Service.Namespace,
servicePort: *apiService.Spec.Service.Port,
serviceAvailable: apiregistrationv1apihelper.IsAPIServiceConditionTrue(apiService, apiregistrationv1api.Available),
}
if r.egressSelector != nil {
networkContext := egressselector.Cluster.AsNetworkContext()
var egressDialer utilnet.DialFunc
egressDialer, err := r.egressSelector.Lookup(networkContext)
if err != nil {
klog.Warning(err.Error())
} else {
newInfo.restConfig.Dial = egressDialer
}
} else if r.proxyTransport != nil && r.proxyTransport.DialContext != nil {
newInfo.restConfig.Dial = r.proxyTransport.DialContext
}
newInfo.proxyRoundTripper, newInfo.transportBuildingError = restclient.TransportFor(newInfo.restConfig)
if newInfo.transportBuildingError != nil {
klog.Warning(newInfo.transportBuildingError.Error())
}
r.handlingInfo.Store(newInfo)
}
上述源码中restConfig就是调用service的客户端参数,其中
ServerName: apiService.Spec.Service.Name + "." + apiService.Spec.Service.Namespace + ".svc"
指的就是具体的service。
与自定义资源定义(CRD)不同,除标准的 Kubernetes kube-apiserver 外,Aggregation API 还涉及另一个服务器:Extension apiserver
。Kubernetes kube-apiserver 将需要与自定义的 Extension apiserver 通信,并且自定义的 Extension apiserver 也需要与 Kubernetes kube-apiserver 通信。为了确保此通信的安全,Kubernetes kube-apiserver 使用 x509 证书向 Extension apiserver 认证。具体流程如下:
假设我们已经在 Kubernetes kube-apiserver 注册了 Extension apiserver。
当用户请求访问 path ,Kubernetes kube-apiserver 使用它的标准认证和授权配置来对用户认证,以及对特定 path 的鉴权,到目前为止,所有内容都是标准的 Kubernetes API 请求,认证与鉴权,接下来 Kubernetes kube-apiserver 现在准备将请求发送到 Extension apiserver。
Kubernetes kube-apiserver认证时,认证接受会将认证信息删除,处理逻辑如下:
context
获取user信息proxyRoundTripper
转发请求代码路径:
staging/src/k8s.io/kube-aggregator/pkg/apiserver/handler_proxy.go
func (r *proxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
value := r.handlingInfo.Load()
if value == nil {
r.localDelegate.ServeHTTP(w, req)
return
}
handlingInfo := value.(proxyHandlingInfo)
if handlingInfo.local {
if r.localDelegate == nil {
http.Error(w, "", http.StatusNotFound)
return
}
r.localDelegate.ServeHTTP(w, req)
return
}
if !handlingInfo.serviceAvailable {
proxyError(w, req, "service unavailable", http.StatusServiceUnavailable)
return
}
if handlingInfo.transportBuildingError != nil {
proxyError(w, req, handlingInfo.transportBuildingError.Error(), http.StatusInternalServerError)
return
}
// 通过context获取user
user, ok := genericapirequest.UserFrom(req.Context())
if !ok {
proxyError(w, req, "missing user", http.StatusInternalServerError)
return
}
// write a new location based on the existing request pointed at the target service
// 构造请求url,通过apiservice配置的service/namespace随机得到某个endpoint后端
location := &url.URL{}
location.Scheme = "https"
rloc, err := r.serviceResolver.ResolveEndpoint(handlingInfo.serviceNamespace, handlingInfo.serviceName, handlingInfo.servicePort)
if err != nil {
klog.Errorf("error resolving %s/%s: %v", handlingInfo.serviceNamespace, handlingInfo.serviceName, err)
proxyError(w, req, "service unavailable", http.StatusServiceUnavailable)
return
}
location.Host = rloc.Host
location.Path = req.URL.Path
location.RawQuery = req.URL.Query().Encode()
newReq, cancelFn := newRequestForProxy(location, req)
defer cancelFn()
if handlingInfo.proxyRoundTripper == nil {
proxyError(w, req, "", http.StatusNotFound)
return
}
// we need to wrap the roundtripper in another roundtripper which will apply the front proxy headers
proxyRoundTripper, upgrade, err := maybeWrapForConnectionUpgrades(handlingInfo.restConfig, handlingInfo.proxyRoundTripper, req)
if err != nil {
proxyError(w, req, err.Error(), http.StatusInternalServerError)
return
}
// 包裹请求信息,将user信息放到header中
proxyRoundTripper = transport.NewAuthProxyRoundTripper(user.GetName(), user.GetGroups(), user.GetExtra(), proxyRoundTripper)
// if we are upgrading, then the upgrade path tries to use this request with the TLS config we provide, but it does
// NOT use the roundtripper. Its a direct call that bypasses the round tripper. This means that we have to
// attach the "correct" user headers to the request ahead of time. After the initial upgrade, we'll be back
// at the roundtripper flow, so we only have to muck with this request, but we do have to do it.
if upgrade {
transport.SetAuthProxyHeaders(newReq, user.GetName(), user.GetGroups(), user.GetExtra())
}
// 调用后端
handler := proxy.NewUpgradeAwareHandler(location, proxyRoundTripper, true, upgrade, &responder{w: w})
handler.ServeHTTP(w, newReq)
}
根据扩展apiserver找到后端时通过service获取对应endpoint列表,随机选择某个endpoint、实现如下,源码路径:
staging/src/k8s.io/apiserver/pkg/util/proxy/proxy.go
func ResolveEndpoint(services listersv1.ServiceLister, endpoints listersv1.EndpointsLister, namespace, id string, port int32) (*url.URL, error) {
svc, err := services.Services(namespace).Get(id)
if err != nil {
return nil, err
}
svcPort, err := findServicePort(svc, port)
if err != nil {
return nil, err
}
switch {
case svc.Spec.Type == v1.ServiceTypeClusterIP, svc.Spec.Type == v1.ServiceTypeLoadBalancer, svc.Spec.Type == v1.ServiceTypeNodePort:
// these are fine
default:
return nil, fmt.Errorf("unsupported service type %q", svc.Spec.Type)
}
eps, err := endpoints.Endpoints(namespace).Get(svc.Name)
if err != nil {
return nil, err
}
if len(eps.Subsets) == 0 {
return nil, errors.NewServiceUnavailable(fmt.Sprintf("no endpoints available for service %q", svc.Name))
}
// Pick a random Subset to start searching from.
ssSeed := rand.Intn(len(eps.Subsets))
// Find a Subset that has the port.
for ssi := 0; ssi < len(eps.Subsets); ssi++ {
ss := &eps.Subsets[(ssSeed+ssi)%len(eps.Subsets)]
if len(ss.Addresses) == 0 {
continue
}
for i := range ss.Ports {
if ss.Ports[i].Name == svcPort.Name {
// Pick a random address.
ip := ss.Addresses[rand.Intn(len(ss.Addresses))].IP
port := int(ss.Ports[i].Port)
return &url.URL{
Scheme: "https",
Host: net.JoinHostPort(ip, strconv.Itoa(port)),
}, nil
}
}
}
return nil, errors.NewServiceUnavailable(fmt.Sprintf("no endpoints available for service %q", id))
}
ProxyRoundTripper创建路径:
staging/src/k8s.io/client-go/transport/round_trippers.go
func NewAuthProxyRoundTripper(username string, groups []string, extra map[string][]string, rt http.RoundTripper) http.RoundTripper {
return &authProxyRoundTripper{
username: username,
groups: groups,
extra: extra,
rt: rt,
}
}
func (rt *authProxyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
req = utilnet.CloneRequest(req)
// 设置user信息
SetAuthProxyHeaders(req, rt.username, rt.groups, rt.extra)
return rt.rt.RoundTrip(req)
}
// SetAuthProxyHeaders stomps the auth proxy header fields. It mutates its argument.
func SetAuthProxyHeaders(req *http.Request, username string, groups []string, extra map[string][]string) {
// 清除原始url的requestheader信息
req.Header.Del("X-Remote-User")
req.Header.Del("X-Remote-Group")
for key := range req.Header {
if strings.HasPrefix(strings.ToLower(key), strings.ToLower("X-Remote-Extra-")) {
req.Header.Del(key)
}
}
// 通过user重新填充信息
req.Header.Set("X-Remote-User", username)
for _, group := range groups {
req.Header.Add("X-Remote-Group", group)
}
for key, values := range extra {
for _, value := range values {
req.Header.Add("X-Remote-Extra-"+headerKeyEscape(key), value)
}
}
}
Kubernetes kube-apiserver 现在将请求发送或代理到注册以处理该请求的 Extension apiserver。为此,它需要了解几件事:
简而言之,就是 Kubernetes kube-apiserver 已经认证和鉴权用户的请求,怎么将这些信息传递给 Extension apiserver,为提供这两条信息,我们必须使用若干启动参数来配置 Kubernetes apiserver。
Kubernetes kube-apiserver 通过 TLS 连接到 Extension apiserver,并使用客户端证书认证,这里 Kubernetes kube-apiserver (aggregator or proxy) 是 Extension apiserver 的客户端。必须在启动时使用提供的参数向 Kubernetes kube-apiserver 提供以下内容:
--proxy-client-key-file
指定签名私钥文件--proxy-client-cert-file
指定验签证书文件--requestheader-client-ca-file
签署客户端证书文件的 CA 证书--requestheader-allowed-names
在签署的客户证书中有效的公用名(CN)Kubernetes kube-apiserver 将使用由 –proxy-client-*-file
指示的文件来通过 Extension apiserver 验证。为了使合规的 Extension apiserver 能够将该请求视为有效,必须满足以下条件:
--requestheader-client-ca-file
中。--requestheader-allowed-names
中列出的证书之一。 **注意:**您可以将此选项设置为空白,即为--requestheader-allowed-names=""
。这将向扩展 apiserver 指示任何 CN 是可接受的。使用这些选项启动时,Kubernetes kube-apiserver 将:
kube-system
命名空间中创建一个 configmap extension-apiserver-authentication
,它将在其中放置 CA 证书和允许的 CN。反过来,Extension apiserver 可以检索这些内容以验证请求。当 Kubernetes kube-apiserver 将请求代理到 Extension apiserver 时,它将向 Extension apiserver 通知原始请求已成功通过其验证的用户名和组。它在其代理请求的 http 标头中提供这些。您必须将要使用的标头名称告知 Kubernetes kube-apiserver。
--requestheader-username-headers
标明用来保存用户名的头部--requestheader-group-headers
标明用来保存 group 的头部--requestheader-extra-headers-prefix
标明用来保存拓展信息前缀的头部这些标头名称也放置在extension-apiserver-authentication
的 configmap 中,因此 Extension apiserver 可以检索和使用它们。
Extension apiserver 在收到来自 Kubernetes kube-apiserver 的代理请求后,必须验证该请求确实来自有效的身份验证代理,该认证代理由 Kubernetes kube-apiserver 履行。Extension apiserver 通过以下方式对其认证:
kube-system
中的 configmap 中检索以下内容:--requestheader-client-ca-file
。--requestheader-allowed-names
。如果以上均通过,则该请求是来自合法认证代理(在本例中为 Kubernetes kube-apiserver)的有效代理请求。
为了具有检索 configmap 的权限,Extension apiserver 需要适当的角色。在 kube-system
名字空间中有一个默认角色extension-apiserver-authentication-reader
可用于设置。
如果SubjectAccessReview
通过,则扩展 apiserver 执行请求。
wget https://pkg.cfssl.org/R1.2/cfssl_linux-amd64 -O /usr/local/bin/cfssl
wget https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64 -O /usr/local/bin/cfssljson
wget https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64 -O /usr/local/bin/cfssl-certinfo
cd /usr/local/bin/
chmod +x cfssl cfssljson cfssl-certinfo
$ cat > aggregator-ca-config.json <<EOF
{
"signing": {
"default": {
"expiry": "87600h"
},
"profiles": {
"aggregator": {
"usages": [
"signing",
"key encipherment",
"server auth",
"client auth"
],
"expiry": "87600h"
}
}
}
}
EOF
profiles
: 可以定义多个 profiles,分别指定不同的过期时间、使用场景等参数;后续在签名证书时使用某个 profile。signing
:表示该证书可用于签名其它证书;生成的 aggregator-ca.pem 证书中 CA=TRUE
。server auth
:表示 Client 可以用该 CA 对 Server 提供的证书进行验证。client auth
:表示 Server 可以用该 CA 对 Client 提供的证书进行验证。$ cat > aggregator-ca-csr.json <<EOF
{
"CN": "aggregator",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"ST": "Shanghai",
"L": "Shanghai",
"O": "k8s",
"OU": "wzlinux"
}
],
"ca": {
"expiry": "87600h"
}
}
Common Name
,kube-apiserver 从证书中提取该字段作为请求的用户名 (User Name);浏览器使用该字段验证网站是否合法。Organization
,kube-apiserver 从证书中提取该字段作为请求用户所属的组 (Group);cfssl gencert -initca aggregator-ca-csr.json | cfssljson -bare aggregator-ca
$ cat > aggregator-csr.json <<EOF
{
"CN": "aggregator",
"hosts": [
"127.0.0.1",
"172.18.0.101",
"172.18.0.102",
"172.18.0.103",
"10.96.0.1",
"kubernetes",
"kubernetes.default",
"kubernetes.default.svc",
"kubernetes.default.svc.cluster",
"kubernetes.default.svc.cluster.local"
],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"ST": "Shanghai",
"L": "Shanghai",
"O": "k8s",
"OU": "wzlinux"
}
]
}
如果 hosts 字段不为空则需要指定授权使用该证书的 IP 或域名列表,由于该证书后续kubernetes master 集群使用,所以上面指定kubernetes master
集群的主机 IP 和 kubernetes 服务的服务 IP(一般是 kube-apiserver 指定的 service-cluster-ip-range
网段的第一个 IP,如 10.96.0.1)。
cfssl gencert -ca=aggregator-ca.pem -ca-key=aggregator-ca-key.pem -config=aggregator-ca-config.json -profile=aggregator aggregator-csr.json | cfssljson -bare aggregator
将生成的证书和秘钥文件(后缀名为.pem)拷贝到 Master 节点的 /etc/kubernetes/pki
目录下备用。
kube-apiserver
增加以下启动配置:
--requestheader-client-ca-file=/etc/kubernetes/pki/aggregator-ca.pem
--requestheader-allowed-names=aggregator
--requestheader-extra-headers-prefix=X-Remote-Extra-
--requestheader-group-headers=X-Remote-Group
--requestheader-username-headers=X-Remote-User
--proxy-client-cert-file=/etc/kubernetes/pki/aggregator.pem
--proxy-client-key-file=/etc/kubernetes/pki/aggregator-key.pem
前面创建的证书的 CN
字段的值必须和参数 –requestheader-allowed-names
指定的值 aggregator
相同。
重启 kube-apiserver:
$ systemctl daemon-reload
$ systemctl restart kube-apiserver
如果 kube-proxy
没有在 Master 上面运行,kube-proxy
还需要添加配置:
--enable-aggregator-routing=true
API聚合这个方式实现相对复杂一点,但灵活度很高,基本业务上的大部分需求都可以满足。
虽然官方给了一个sample-apiserver,我们可以照着实现自己的Aggregated APIServer。但完全手工编写还是太费劲了,这里使用官方推荐的工具apiserver-builder帮助快速创建项目骨架。
apiserver-builder构建AA方案的API接口服务的原理还是比较清晰的,总之就是kubernetes里最常见的控制器模式,这里就不具体介绍了,官方文档既有文字又有图片讲得还是挺细致的,强烈推荐大家多看看,学习一下。
apiserver-builder这个工具与kubebuilder和operator-sdk非常相似,他们都依赖一个底层库controller-gen,apiserver-builder该工具生成的工程与kubebuilder生成的工程也非常相似,其中一个不同的地方就是kubebuilder因为不需要自定义apiserver,因此,apiserver-builder生成的工程会有一个自定义apiserver,控制器部分的逻辑两者都一样,都是通过调协方法实现自定义资源状态的维护。
git clone https://github.com/kubernetes/sample-apiserver.git
cd sample-apiserver
编译二进制文件:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o artifacts/simple-image/kube-sample-apiserver
编译docker镜像:
docker build -t kube-sample-apiserver:latest ./artifacts/simple-image
踩坑:
artifacts/example/deployment.yaml文件中需要修改镜像及版本,要与集群中对应;
artifacts/example下所有文件中涉及的命名空间统一修改为default,auth-reader.yaml中metadata下的命名空间为kube-system保持不变。
kubectl apply -f artifacts/example
kubectl get sa apiserver -o json
kubectl describe secret apiserver
kubectl get clusterrole
kubectl get clusterrolebinding
curl -k -H "Authorization: Bearer $(cat test.log)" https://10.131.180.168:6443/apis/wardle.example.com/v1alpha1/flunders
工具使用当前最新版本V2.0beta
初始化项目
apiserver-boot init repo --domain example.com
创建一个非命名空间范围的api-resource
apiserver-boot create group version resource --group demo --version v1beta1 --non-namespaced=true --kind Foo
创建Foo这个api-resource的子资源
apiserver-boot create subresource --subresource bar --group demo --version v1beta1 --kind Foo
生成上述创建的api-resource类型的相关代码,包括deepcopy接口实现代码、versioned/unversioned类型转换代码、api-resource类型注册代码、api-resource类型的Controller代码、api-resource类型的AdmissionController代码
解决工程依赖问题并生成脚手架代码
go mod init
go mod vendor
make generate
可以直接在本地将etcd, apiserver, controller运行起来
apiserver-boot run local
上述这样操作之后,就可以访问我们的APIServer了,如下面的命令:
curl -k https://127.0.0.1:9443/apis/demo.example.com/v1beta1/foos
当然也可以新建一个yaml文件,然后用kubectl命令直接对api-resource进行操作:
创建Foo资源的yaml
echo 'apiVersion: demo.example.com/v1beta1
kind: Foo
metadata:
name: foo-example
namespace: test
spec: {}' > sample/foo.yaml
如果在apiserver的main方法里补上一些代码,以开启swagger-ui,还能更方便地看到这些API接口:
func main() {
version := "v0"
server.StartApiServer("/registry/example.com", apis.GetAllApiBuilders(), openapi.GetOpenAPIDefinitions, "Api", version, func(apiServerConfig *apiserver.Config) error {
...
apiServerConfig.RecommendedConfig.EnableSwaggerUI = true
apiServerConfig.RecommendedConfig.SwaggerConfig = genericapiserver.DefaultSwaggerConfig()
return nil
})
}
然后浏览器访问https://127.0.0.1:9443/swagger-ui/就可以在swagger的Web页面上看到创建出来的所有API接口
像上面这样创建的API接口,接口是都有了,但接口没有啥意义,一般要根据实际情况定义api-resource的spec、status等结构体。
type Foo struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec FooSpec `json:"spec,omitempty"`
Status FooStatus `json:"status,omitempty"`
}
// FooSpec defines the desired state of Foo
type FooSpec struct {
}
// FooStatus defines the observed state of Foo
type FooStatus struct {
}
默认生成的api-resource的Reconcile逻辑如下:
// Reconcile reads that state of the cluster for a Foo object and makes changes based on the state read
// and what is in the Foo.Spec
// TODO(user): Modify this Reconcile function to implement your Controller logic. The scaffolding writes
// a Deployment as an example
// +kubebuilder:rbac:groups=demo.jeremyxu2010.me,resources=foos,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=demo.jeremyxu2010.me,resources=foos/status,verbs=get;update;patch
func (r *ReconcileFoo) Reconcile(request reconcile.Request) (reconcile.Result, error) {
// Fetch the Foo instance
instance := &demov1beta1.Foo{}
err := r.Get(context.TODO(), request.NamespacedName, instance)
if err != nil {
if errors.IsNotFound(err) {
// Object not found, return. Created objects are automatically garbage collected.
// For additional cleanup logic use finalizers.
return reconcile.Result{}, nil
}
// Error reading the object - requeue the request.
return reconcile.Result{}, err
}
return reconcile.Result{}, nil
}
可以参考:operator-sdk-samples
程序写好后,通过以下命令即可生成容器镜像及kubernetes的部署manifest文件:
生成二进制文件
apiserver-boot build executables
生成容器镜像
apiserver-boot build container --image demo/foo-apiserver:latest
生成kubernetes的部署manifest文件,可直接在kubernetes里apply即完成部署
apiserver-boot build config --name foo-apiserver --namespace default --image demo/foo-apiserver:latest
观察生成的kubernetes部署manifest文件config/apiserver.yaml,可以发现最终会创建一个Deployment,一个Service和一个APIService类型的kubernetes资源,同时APIService的caBundle及apiserver的TLS证书也配置妥当了。