「kubectl get cs」輸出格式變化

最新版本的kubernetes在執行kubectl get cs時輸出內容有一些變化,之前是這樣的:node

> kubectl get componentstatuses
NAME                 STATUS    MESSAGE             ERROR
controller-manager   Healthy   ok
scheduler            Healthy   ok
etcd-0               Healthy   {"health":"true"}

如今變成了:git

> kubectl get componentstatuses
NAME                 Age    
controller-manager   <unknown>
scheduler            <unknown>
etcd-0               <unknown>

起初還覺得集羣部署有問題,經過kubectl get cs -o yaml發現status、message等信息都有,只是沒有打印出來。原來是kubectl get cs的輸出格式有變化,那麼爲何會有此變化,咱們來一探究竟。github

定位問題緣由

嘗試以前的版本,發現1.15仍是正常的,調試代碼發現對componentstatuses的打印代碼在k8s.io/kubernetes/pkg/printers/internalversion/printers.go中,其中的AddHandlers方法負責把各類資源的打印函數註冊進來。shell

// AddHandlers adds print handlers for default Kubernetes types dealing with internal versions.
// TODO: handle errors from Handler
func AddHandlers(h printers.PrintHandler) {
......
    componentStatusColumnDefinitions := []metav1beta1.TableColumnDefinition{
        {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
        {Name: "Status", Type: "string", Description: "Status of the component conditions"},
        {Name: "Message", Type: "string", Description: "Message of the component conditions"},
        {Name: "Error", Type: "string", Description: "Error of the component conditions"},
    }
    h.TableHandler(componentStatusColumnDefinitions, printComponentStatus)
    h.TableHandler(componentStatusColumnDefinitions, printComponentStatusList)

對AddHandlers的調用位於k8s.io/kubernetes/pkg/kubectl/cmd/get/humanreadable_flags.go(正在遷移到staging中,若是找不到就去staging中找)中,以下32行位置:json

// ToPrinter receives an outputFormat and returns a printer capable of
// handling human-readable output.
func (f *HumanPrintFlags) ToPrinter(outputFormat string) (printers.ResourcePrinter, error) {
    if len(outputFormat) > 0 && outputFormat != "wide" {
        return nil, genericclioptions.NoCompatiblePrinterError{Options: f, AllowedFormats: f.AllowedFormats()}
    }

    showKind := false
    if f.ShowKind != nil {
        showKind = *f.ShowKind
    }

    showLabels := false
    if f.ShowLabels != nil {
        showLabels = *f.ShowLabels
    }

    columnLabels := []string{}
    if f.ColumnLabels != nil {
        columnLabels = *f.ColumnLabels
    }

    p := printers.NewTablePrinter(printers.PrintOptions{
        Kind:          f.Kind,
        WithKind:      showKind,
        NoHeaders:     f.NoHeaders,
        Wide:          outputFormat == "wide",
        WithNamespace: f.WithNamespace,
        ColumnLabels:  columnLabels,
        ShowLabels:    showLabels,
    })
    printersinternal.AddHandlers(p)

    // TODO(juanvallejo): handle sorting here

    return p, nil
}

查看humanreadable_flags.go文件的修改歷史,發現是在2019.6.28日特地去掉了對內部對象的打印,影響版本從1.16以後。api

image-20191031124543589.png

爲何修改

我沒有查到官方的說明,在此作一些我的猜想,還原整個過程:緩存

  1. 最初對api resource的表格打印都是在kubectl中實現
  2. 這樣對於其餘客戶端須要作重複的事情,並且可能實現的行爲不一致,所以有必要將表格打印放到服務端,也就是apiserver中
  3. 服務端打印的支持通過一個逐步的過程,因此客戶端並無徹底去除,是同時支持的,kubectl判斷服務端返回的Table就直接打印,不然使用具體對象的打印,客戶端和服務端對特定對象的打印都是調用k8s.io/kubernetes/pkg/printers/internalversion/printers.go來實現
  4. 1.11版本將kubectl的命令行參數--server-print默認設置爲true
  5. 到了1.16版本,社區可能認爲全部的對象都移到服務端了,這時就去除了客戶端kubectl中的打印
  6. 但實際上componentstatuses被遺漏了,那麼爲何遺漏,可能主要是由於componentstatuses對象跟其餘對象不同,它是每次實時獲取,而不是從緩存獲取,其餘對象,例如pod是從etcd獲取,對結果的格式化定義在k8s.io/kubernetes/pkg/registry/core/pod/storage/storage.go中,以下15行位置:restful

    // NewStorage returns a RESTStorage object that will work against pods.
    func NewStorage(optsGetter generic.RESTOptionsGetter, k client.ConnectionInfoGetter, proxyTransport http.RoundTripper, podDisruptionBudgetClient policyclient.PodDisruptionBudgetsGetter) PodStorage {
    
        store := &genericregistry.Store{
            NewFunc:                  func() runtime.Object { return &api.Pod{} },
            NewListFunc:              func() runtime.Object { return &api.PodList{} },
            PredicateFunc:            pod.MatchPod,
            DefaultQualifiedResource: api.Resource("pods"),
    
            CreateStrategy:      pod.Strategy,
            UpdateStrategy:      pod.Strategy,
            DeleteStrategy:      pod.Strategy,
            ReturnDeletedObject: true,
    
            TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
        }

    componentstatuses沒有用到真正的Storge,而它又相對不起眼,因此被遺漏了。app

暫時解決辦法

若是但願打印和原來相似的內容,目前只有經過模板:ide

kubectl get cs -o=go-template='{{printf "|NAME|STATUS|MESSAGE|\n"}}{{range .items}}{{$name := .metadata.name}}{{range .conditions}}{{printf "|%s|%s|%s|\n" $name .status .message}}{{end}}{{end}}'

輸出結果:

|NAME|STATUS|MESSAGE|
|controller-manager|True|ok|
|scheduler|True|ok|
|etcd-0|True|{"health":"true"}|

深刻打印處理流程

kubectl經過-o參數控制輸出的格式,有yaml、json、模板和表格幾種樣式,上述問題是出在表格打印時,不加-o參數默認就是表格打印,下面咱們詳細分析一下kubectl get的打印輸出過程。

kubectl

kubectl get的代碼入口在k8s.io/kubernetes/pkg/kubectl/cmd/get/get.go中,Run字段就是命令執行方法:

func NewCmdGet(parent string, f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
......
        Run: func(cmd *cobra.Command, args []string) {
            cmdutil.CheckErr(o.Complete(f, cmd, args))
            cmdutil.CheckErr(o.Validate(cmd))
            cmdutil.CheckErr(o.Run(f, cmd, args))
        },
......
    }

Complete方法完成了Printer初始化,位於k8s.io/kubernetes/pkg/kubectl/cmd/get/get_flags.go中:

func (f *PrintFlags) ToPrinter() (printers.ResourcePrinter, error) {
......
    if p, err := f.HumanReadableFlags.ToPrinter(outputFormat); !genericclioptions.IsNoCompatiblePrinterError(err) {
        return p, err
    }
......
}

不帶-o參數時,上述方法返回的是f.HumanReadableFlags.ToPrinter(outputFormat),最後返回的是HumanReadablePrinter對象,位於k8s.io/cli-runtime/pkg/printers/tableprinter.go中:

// NewTablePrinter creates a printer suitable for calling PrintObj().
func NewTablePrinter(options PrintOptions) ResourcePrinter {
    printer := &HumanReadablePrinter{
        options: options,
    }
    return printer
}

再回到命令執行主流程,Complete以後主要是Run,其中完成向apiserver發送http請求並打印結果的動做,在發送http請求前,有一個很重要的動做,加入服務端打印的header,服務端打印能夠經過--server-print參數控制,從1.11默認爲true,這樣服務端若是支持轉換就會返回metav1beta1.Table類型,置爲false也能夠禁用服務端打印:

func (o *GetOptions) transformRequests(req *rest.Request) {
......
    if !o.ServerPrint || !o.IsHumanReadablePrinter {
        return
    }

    group := metav1beta1.GroupName
    version := metav1beta1.SchemeGroupVersion.Version

    tableParam := fmt.Sprintf("application/json;as=Table;v=%s;g=%s, application/json", version, group)
    req.SetHeader("Accept", tableParam)

......
}

最後打印是調用的HumanReadablePrinter.PrintObj方法,先判斷服務端若是返回的metav1beta1.Table類型就直接打印,其次若是是metav1.Status類型也有專門的處理器,最後就會到默認處理器:

func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) error {
......
    // Parameter "obj" is a table from server; print it.
    // display tables following the rules of options
    if table, ok := obj.(*metav1beta1.Table); ok {

        return printTable(table, output, localOptions)
    }

    // Could not find print handler for "obj"; use the default or status print handler.
    // Print with the default or status handler, and use the columns from the last time
    var handler *printHandler
    if _, isStatus := obj.(*metav1.Status); isStatus {
        handler = statusHandlerEntry
    } else {
        handler = defaultHandlerEntry
    }
......
    if err := printRowsForHandlerEntry(output, handler, eventType, obj, h.options, includeHeaders); err != nil {
        return err
    }
......
    return nil
}

默認處理器會打印Name和Age兩列,由於componetstatuses是實時獲取,沒有存儲在etcd中,沒有建立時間,因此Age打印出來就是unknown。

apiserver

再來看服務端的處理流程,apiserver對外提供REST接口實如今k8s.io/apiserver/pkg/endpoints/handlers目錄下,kubectl get cs會進入get.go中ListResource方法,以下列出關鍵的三個步驟:

func ListResource(r rest.Lister, rw rest.Watcher, scope *RequestScope, forceWatch bool, minRequestTimeout time.Duration) http.HandlerFunc {
    return func(w http.ResponseWriter, req *http.Request) {
......

        outputMediaType, _, err := negotiation.NegotiateOutputMediaType(req, scope.Serializer, scope)
......

        result, err := r.List(ctx, &opts)
......
        transformResponseObject(ctx, scope, trace, req, w, http.StatusOK, outputMediaType, result)
......
    }
}

NegotiateOutputMediaType中根據客戶端的請求header設置服務端的一些行爲,包括是否服務端打印;r.List從Storage層獲取資源數據,具體實如今k8s.io/kubernetes/pkg/registry下;transformResponseObject將結果返回給客戶端。

先說transformResponseObject,其中就會根據NegotiateOutputMediaType返回的outputMediaType的Convert字段判斷是否轉爲換目標類型,若是爲Table就會將r.List返回的具體資源類型轉換爲Table類型:

func doTransformObject(ctx context.Context, obj runtime.Object, opts interface{}, mediaType negotiation.MediaTypeOptions, scope *RequestScope, req *http.Request) (runtime.Object, error) {
......

    switch target := mediaType.Convert; {
    case target == nil:
        return obj, nil        
......
    case target.Kind == "Table":
        options, ok := opts.(*metav1beta1.TableOptions)
        if !ok {
            return nil, fmt.Errorf("unexpected TableOptions, got %T", opts)
        }
        return asTable(ctx, obj, options, scope, target.GroupVersion())
......g
    }
}

上述asTable最終調用scope.TableConvertor.ConvertToTable完成表格轉換工做,在本文的問題中,就是由於mediaType.Convert爲空而沒有觸發這個轉換,那麼爲何爲空呢,問題就出在NegotiateOutputMediaType,它最終會調用到k8s.ioapiserverpkgendpointshandlersrest.go的AllowsMediaTypeTransform方法,是由於scope.TableConvertor爲空,最終轉換爲Table也是調用的它:

func (scope *RequestScope) AllowsMediaTypeTransform(mimeType, mimeSubType string, gvk *schema.GroupVersionKind) bool {
......
    if gvk.GroupVersion() == metav1beta1.SchemeGroupVersion || gvk.GroupVersion() == metav1.SchemeGroupVersion {
        switch gvk.Kind {
        case "Table":
            return scope.TableConvertor != nil &&
                mimeType == "application" &&
                (mimeSubType == "json" || mimeSubType == "yaml")
......
}

進一步跟蹤,RequestScope是在apiserver初始化的時候建立的,每類資源一個,好比componentstatuses有一個全局的,pod有一個全局的,初始化的過程以下:

apiserver初始化入口在k8s.iokubernetespkgmastermaster.go的InstallLegacyAPI和InstallAPIs方法中,前者主要針對一些老的資源,具體有哪些見下面的NewLegacyRESTStorage方法,其中就包含componentStatuses,其餘資源經過InstallAPIs初始化:

// InstallLegacyAPI will install the legacy APIs for the restStorageProviders if they are enabled.
func (m *Master) InstallLegacyAPI(c *completedConfig, restOptionsGetter generic.RESTOptionsGetter, legacyRESTStorageProvider corerest.LegacyRESTStorageProvider) error {
    legacyRESTStorage, apiGroupInfo, err := legacyRESTStorageProvider.NewLegacyRESTStorage(restOptionsGetter)
......
}

初始化Storage:

func (c LegacyRESTStorageProvider) NewLegacyRESTStorage(restOptionsGetter generic.RESTOptionsGetter) (LegacyRESTStorage, genericapiserver.APIGroupInfo, error) {
    apiGroupInfo := genericapiserver.APIGroupInfo{
        PrioritizedVersions:          legacyscheme.Scheme.PrioritizedVersionsForGroup(""),
        VersionedResourcesStorageMap: map[string]map[string]rest.Storage{},
        Scheme:                       legacyscheme.Scheme,
        ParameterCodec:               legacyscheme.ParameterCodec,
        NegotiatedSerializer:         legacyscheme.Codecs,
    }

......
    restStorageMap := map[string]rest.Storage{
        "pods":             podStorage.Pod,
        "pods/attach":      podStorage.Attach,
        "pods/status":      podStorage.Status,
        "pods/log":         podStorage.Log,
        "pods/exec":        podStorage.Exec,
        "pods/portforward": podStorage.PortForward,
        "pods/proxy":       podStorage.Proxy,
        "pods/binding":     podStorage.Binding,
        "bindings":         podStorage.LegacyBinding,

        "podTemplates": podTemplateStorage,

        "replicationControllers":        controllerStorage.Controller,
        "replicationControllers/status": controllerStorage.Status,

        "services":        serviceRest,
        "services/proxy":  serviceRestProxy,
        "services/status": serviceStatusStorage,

        "endpoints": endpointsStorage,

        "nodes":        nodeStorage.Node,
        "nodes/status": nodeStorage.Status,
        "nodes/proxy":  nodeStorage.Proxy,

        "events": eventStorage,

        "limitRanges":                   limitRangeStorage,
        "resourceQuotas":                resourceQuotaStorage,
        "resourceQuotas/status":         resourceQuotaStatusStorage,
        "namespaces":                    namespaceStorage,
        "namespaces/status":             namespaceStatusStorage,
        "namespaces/finalize":           namespaceFinalizeStorage,
        "secrets":                       secretStorage,
        "serviceAccounts":               serviceAccountStorage,
        "persistentVolumes":             persistentVolumeStorage,
        "persistentVolumes/status":      persistentVolumeStatusStorage,
        "persistentVolumeClaims":        persistentVolumeClaimStorage,
        "persistentVolumeClaims/status": persistentVolumeClaimStatusStorage,
        "configMaps":                    configMapStorage,
        
        "componentStatuses": componentstatus.NewStorage(componentStatusStorage{c.StorageFactory}.serversToValidate),
    }
......
    apiGroupInfo.VersionedResourcesStorageMap["v1"] = restStorageMap

    return restStorage, apiGroupInfo, nil
}

註冊REST接口的handler,handler中包含RequestScope,RequestScope中的TableConvertor字段是從storage取出,也就是上述NewLegacyRESTStorage建立的資源對應的storage,例如componentStatuses,就是componentstatus.NewStorage(componentStatusStorage{c.StorageFactory}.serversToValidate),提取的方法是類型斷言storage.(rest.TableConvertor),也就是storage要實現rest.TableConvertor接口,不然取出來爲空:

func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService) (*metav1.APIResource, error) {
......

    tableProvider, _ := storage.(rest.TableConvertor)

    var apiResource metav1.APIResource
......
    reqScope := handlers.RequestScope{
......

        // TODO: Check for the interface on storage
        TableConvertor: tableProvider,

......
    for _, action := range actions {
......
        switch action.Verb {
......
        case "LIST": // List all resources of a kind.
            doc := "list objects of kind " + kind
            if isSubresource {
                doc = "list " + subresource + " of objects of kind " + kind
            }
            handler := metrics.InstrumentRouteFunc(action.Verb, group, version, resource, subresource, requestScope, metrics.APIServerComponent, restfulListResource(lister, watcher, reqScope, false, a.minRequestTimeout))
......
        }
        // Note: update GetAuthorizerAttributes() when adding a custom handler.
    }
......
}

具體看componentStatuses的storage,在k8s.iokubernetespkgregistrycorecomponentstatusrest.go中,確實沒有實現rest.TableConvertor接口,因此componentStatuses的handler的RequestScope中的TableConvertor字段就爲空,最終致使了問題:

type REST struct {
    GetServersToValidate func() map[string]*Server    
}

// NewStorage returns a new REST.
func NewStorage(serverRetriever func() map[string]*Server) *REST {
    return &REST{
        GetServersToValidate: serverRetriever,    
    }
}

代碼修復

找到了根本緣由以後,修復就比較簡單了,就是storage須要實現rest.TableConvertor接口,接口定義在k8s.ioapiserverpkgregistryrestrest.go中:

type TableConvertor interface {
    ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1beta1.Table, error)
}

參照其餘資源storage代碼,修改k8s.iokubernetespkgregistrycorecomponentstatusrest.go代碼以下,問題獲得解決,kubectl get cs打印出熟悉的表格,若是使用kubectl get cs --server-print=false仍會只打印Name、Age兩列:

type REST struct {
    GetServersToValidate func() map[string]*Server    
    tableConvertor printerstorage.TableConvertor
}

// NewStorage returns a new REST.
func NewStorage(serverRetriever func() map[string]*Server) *REST {
    return &REST{
        GetServersToValidate: serverRetriever,    
        tableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},    
    }
}
func (r *REST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1beta1.Table, error) {
    return r.tableConvertor.ConvertToTable(ctx, object, tableOptions)
}

最後

本想提交一個PR給kubernetes,發現已經有人先一步提了,解決方法和我一摸同樣,只是上述tableConvertor字段是大寫開頭,我以爲小寫更好,有點遺憾。而這個問題在2019.9.23已經有人提出,也就是1.16剛發佈的時候,9.24就有人提了PR,解決速度很是之快,可見開源軟件的優點以及k8s熱度之高,有無數的開發者爲其貢獻力量,k8s就像聚光燈下的明星,無數雙眼睛注目着。不過這個PR如今尚未合入主幹,還在代碼審查階段,這個bug相對來說不是很嚴重,因此優先級不那麼高,要知道如今還有1097個PR。雖然最後有一點小遺憾,不過在解決問題的過程當中對kubernetes的理解也更進一步,仍是收穫良多。在閱讀代碼的過程當中,隨處可見各類TODO,發現代碼不斷在重構,今天代碼在這裏,明天代碼就搬到另外一個地方了,k8s這麼一個冉冉升起的新星,雖然從2017年起就成爲容器編排的事實標準,並被普遍應用到生產環境,但它自己還在不斷進化,還需不斷完善。

相關文章
相關標籤/搜索