CRDマニフェストの生成

コントローラーでカスタムリソースを扱うためには、そのリソースのCRD(Custom Resource Definition)を定義する必要があります。 CRDのマニフェストは複雑で、手書きで作成するにはかなりの手間がかかります。

そこでKubebuilderではcontroller-genというツールを提供しており、Goで記述したstructからCRDを生成できます。

まずはkubebuilder create apiコマンドで生成されたapi/v1/markdownview_types.goを見てみましょう。

markdownview_types.go

/*
Copyright 2024.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1

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

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// MarkdownViewSpec defines the desired state of MarkdownView
type MarkdownViewSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // Foo is an example field of MarkdownView. Edit markdownview_types.go to remove/update
    Foo string `json:"foo,omitempty"`
}

// MarkdownViewStatus defines the observed state of MarkdownView
type MarkdownViewStatus struct {
    // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
    // Important: Run "make" to regenerate code after modifying this file
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// MarkdownView is the Schema for the markdownviews API
type MarkdownView struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec   MarkdownViewSpec   `json:"spec,omitempty"`
    Status MarkdownViewStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// MarkdownViewList contains a list of MarkdownView
type MarkdownViewList struct {
    metav1.TypeMeta `json:",inline"`
    metav1.ListMeta `json:"metadata,omitempty"`
    Items           []MarkdownView `json:"items"`
}

func init() {
    SchemeBuilder.Register(&MarkdownView{}, &MarkdownViewList{})
}

MarkdownViewSpec, MarkdownViewStatus, MarkdownView, MarkdownViewListという構造体が定義されており、//+kubebuilder:から始まるマーカーコメントがいくつか付与されています。 controller-genは、これらの構造体とマーカーを頼りにCRDの生成をおこないます。

MarkdownViewがカスタムリソースの本体となる構造体です。MarkdownViewListMarkdownViewのリストを表す構造体です。これら2つの構造体は基本的に変更することはありません。 MarkdownViewSpecMarkdownViewStatusMarkdownView構造体を構成する要素です。この2つの構造体を書き換えてカスタムリソースを定義していきます。

一般的にカスタムリソースのSpecはユーザーが記述するもので、システムのあるべき状態をユーザーからコントローラーに伝える用途として利用されます。 一方のStatusは、コントローラーが処理した結果をユーザーや他のシステムに伝える用途として利用されます。

なお、CRDを利用してKubernetes APIを拡張する際には、以下の規約に従うことが推奨されています。一度目を通しておくとよいでしょう。

MarkdownViewSpec

さっそくMarkdownViewSpecを書き換えていきましょう。

作成するカスタムコントローラにおいて、MarkdownViewコントローラーが扱うカスタムリソースとして下記のようなマニフェストを例示しました。

view_v1_markdownview.yaml

apiVersion: view.zoetrope.github.io/v1
kind: MarkdownView
metadata:
  labels:
    app.kubernetes.io/name: markdown-view
    app.kubernetes.io/managed-by: kustomize
  name: markdownview-sample
spec:
  markdowns:
    SUMMARY.md: |
      # Summary

      - [Page1](page1.md)
    page1.md: |
      # Page 1

      一ページ目のコンテンツです。
  replicas: 1
  viewerImage: "peaceiris/mdbook:latest"

上記のマニフェストを扱うための構造体を用意しましょう。

markdownview_types.go

// MarkdownViewSpec defines the desired state of MarkdownView
type MarkdownViewSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "make" to regenerate code after modifying this file

    // Markdowns contain the markdown files you want to display.
    // The key indicates the file name and must not overlap with the keys.
    // The value is the content in markdown format.
    //+kubebuilder:validation:Required
    //+kubebuilder:validation:MinProperties=1
    Markdowns map[string]string `json:"markdowns,omitempty"`

    // Replicas is the number of viewers.
    // +kubebuilder:default=1
    // +optional
    Replicas int32 `json:"replicas,omitempty"`

    // ViewerImage is the image name of the viewer.
    // +optional
    ViewerImage string `json:"viewerImage,omitempty"`
}

まず下記の3つのフィールドを定義します。

  • Markdowns: 表示したいマークダウンファイルの一覧
  • Replicas: Viewerのレプリカ数
  • ViewerImage: Markdownの表示に利用するViewerのイメージ名

各フィールドの上に// +kubebuilderという文字列から始まるマーカーと呼ばれるコメントが記述されています。 これらのマーカーによって、生成されるCRDの内容を制御できます。

付与できるマーカーはcontroller-gen crd -wコマンドで確認できます。

Required/Optional

Markdownsフィールドには+kubebuiler:validation:Requiredマーカーが付与されています。 これはこのフィールドが必須項目であることを示しており、ユーザーがマニフェストを記述する際にこの項目を省略できません。 一方のReplicasViewerImageには+optionalが付与されており、この項目が省略可能であることを示しています。

マーカーを指定しなかった場合はデフォルトでRequiredなフィールドになります。

なお、ファイル内に下記のマーカーを配置すると、デフォルトの挙動をOptionalに変更できます。

// +kubebuilder:validation:Optional

+optionalマーカーを付与しなくても、フィールドの後ろのJSONタグにomitemptyを付与した場合は、自動的にOptionalなフィールドとなります。

type SampleSpec struct {
    Value string `json:"value,omitempty"`
}

Optionalなフィールドは、以下のようにフィールドの型をポインタにできます。 これによりマニフェストで値を指定しなかった場合の挙動が異なります。 ポインタ型にした場合はnullが入り、実体にした場合はその型の初期値(intの場合は0)が入ります。

type SampleSpec struct {
    // +optional
    Value1 int  `json:"value1"`
    // +optional
    Value2 *int `json:"value2"`
}

Validation

KubebuilderにはRequired以外にも様々なバリデーションが用意されています。 詳しくはcontroller-gen crd -wコマンドで確認してください。

  • リストの最小要素数、最大要素数
  • 文字列の最小長、最大長
  • 数値の最小値、最大値
  • 正規表現にマッチするかどうか
  • リスト内の要素がユニークかどうか

MarkdownViewStatus

次にMarkdownViewリソースの状態を表現するためのMarkdownViewStatusを書き換えます。

markdownview_types.go

// MarkdownViewStatus defines the observed state of MarkdownView
type MarkdownViewStatus struct {
    // Conditions represent the latest available observations of an object's state
    // +listType=map
    // +listMapKey=type
    // +optional
    Conditions []metav1.Condition `json:"conditions,omitempty"`
}

const (
    TypeMarkdownViewAvailable = "Available"
    TypeMarkdownViewDegraded  = "Degraded"
)

カスタムリソースの状態を表現するには、metav1.Conditionを利用することが一般的です。 今回のカスタムコントローラーでは、ConditionのTypeとしてAvailable,Degradedの3つの状態を表現できるようにしました。

  • Available: レンダリングされたMarkdownが閲覧可能な状態
  • Degraded: Reconcile処理に失敗した状態

MarkdownView

続いてMarkdownView構造体のマーカーを見てみましょう。

markdownview_types.go

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=".spec.replicas"
// +kubebuilder:printcolumn:name="Available",type="string",JSONPath=".status.conditions[?(@.type==\"Available\")].status"

// MarkdownView is the Schema for the markdownviews API
type MarkdownView struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec   MarkdownViewSpec   `json:"spec,omitempty"`
    Status MarkdownViewStatus `json:"status,omitempty"`
}

Kubebuilderが生成した初期状態では、+kubebuilder:object:root=true+kubebuilder:subresourceの2つのマーカーが指定されています。 ここではさらに+kubebuilder:printcolumnを追加することとします。

+kubebuilder:object:root=trueは、MarkdownView構造体がカスタムリソースのrootオブジェクトであることを表すマーカーです。

+kubebuilder:subresource+kubebuilder:printcolumnマーカーについて、以降で解説します。

subresource

+kubebuilder:subresource:statusというマーカーを追加すると、statusフィールドがサブリソースとして扱われるようになります。

Kubernetesでは、すべてのリソースはそれぞれ独立したAPIエンドポイントを持っており、APIサーバー経由でリソースの取得・作成・変更・削除をおこなうことができます。

サブリソースを有効にするとstatusフィールドがメインのリソースと独立したAPIエンドポイントを持つようになります。

これによりメインのリソース全体を取得・更新しなくても、statusのみの取得や更新が可能になります。 ただし、あくまでもメインのリソースに属するサブのリソースなので、個別の作成や削除はできません。

ユーザーがspecフィールドを記述し、コントローラーがstatusフィールドを記述するという役割分担を明確にできるので、基本的にはstatusはサブリソースにしておくのがよいでしょう。 なおKubebuilder v3では、statusフィールドがサブリソースに指定するマーカーが最初から指定されるようになりました。

CRDでは任意のフィールドをサブリソースにはできず、statusscaleの2つのフィールドのみに対応しています。

printcolumn

+kubebuilder:printcolumnマーカーを付与すると、kubectlでカスタムリソースを取得したときに表示する項目を指定できます。

表示対象のフィールドはJSONPathで指定可能です。 例えば、JSONPath=".spec.replicas"と記述すると、kubectl getしたときに.spec.replicasの値が表示されます。

kubectlでMarkdownViewリソースを取得すると、下記のようにREPLICASやSTATUSの値が表示されていることが確認できます。

$ kubectl get markdownview
NAME                  REPLICAS   AVAILABLE
MarkdownView-sample   1

CRDマニフェストの生成

最後にGoで記述したstructからCRDを生成してみましょう。

以下のコマンドを実行してください。

$ make manifests

すると、以下のようなCRDのマニフェストファイルが生成されます。

view.zoetrope.github.io_markdownviews.yaml

---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.15.0
  name: markdownviews.view.zoetrope.github.io
spec:
  group: view.zoetrope.github.io
  names:
    kind: MarkdownView
    listKind: MarkdownViewList
    plural: markdownviews
    singular: markdownview
  scope: Namespaced
  versions:
  - additionalPrinterColumns:
    - jsonPath: .spec.replicas
      name: Replicas
      type: integer
    - jsonPath: .status.conditions[?(@.type=="Available")].status
      name: Available
      type: string
    name: v1
    schema:
      openAPIV3Schema:
        description: MarkdownView is the Schema for the markdownviews API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: MarkdownViewSpec defines the desired state of MarkdownView
            properties:
              markdowns:
                additionalProperties:
                  type: string
                description: |-
                  Markdowns contain the markdown files you want to display.
                  The key indicates the file name and must not overlap with the keys.
                  The value is the content in markdown format.
                minProperties: 1
                type: object
              replicas:
                default: 1
                description: Replicas is the number of viewers.
                format: int32
                type: integer
              viewerImage:
                description: ViewerImage is the image name of the viewer.
                type: string
            type: object
          status:
            description: MarkdownViewStatus defines the observed state of MarkdownView
            properties:
              conditions:
                description: Conditions represent the latest available observations
                  of an object's state
                items:
                  description: "Condition contains details for one aspect of the current
                    state of this API Resource.\n---\nThis struct is intended for
                    direct use as an array at the field path .status.conditions.  For
                    example,\n\n\n\ttype FooStatus struct{\n\t    // Represents the
                    observations of a foo's current state.\n\t    // Known .status.conditions.type
                    are: \"Available\", \"Progressing\", and \"Degraded\"\n\t    //
                    +patchMergeKey=type\n\t    // +patchStrategy=merge\n\t    // +listType=map\n\t
                    \   // +listMapKey=type\n\t    Conditions []metav1.Condition `json:\"conditions,omitempty\"
                    patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t
                    \   // other fields\n\t}"
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: |-
                        type of condition in CamelCase or in foo.example.com/CamelCase.
                        ---
                        Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be
                        useful (see .node.status.conditions), the ability to deconflict is important.
                        The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        type: object
    served: true
    storage: true
    subresources:
      status: {}

results matching ""

    No results matching ""