CRDマニフェストの生成
コントローラーでカスタムリソースを扱うためには、そのリソースのCRD(Custom Resource Definition)を定義する必要があります。 CRDのマニフェストは複雑で、手書きで作成するにはかなりの手間がかかります。
そこでKubebuilderではcontroller-genというツールを提供しており、Goで記述したstructからCRDを生成できます。
まずはkubebuilder create api
コマンドで生成されたapi/v1/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
がカスタムリソースの本体となる構造体です。MarkdownViewList
はMarkdownView
のリストを表す構造体です。これら2つの構造体は基本的に変更することはありません。
MarkdownViewSpec
とMarkdownViewStatus
はMarkdownView
構造体を構成する要素です。この2つの構造体を書き換えてカスタムリソースを定義していきます。
一般的にカスタムリソースのSpec
はユーザーが記述するもので、システムのあるべき状態をユーザーからコントローラーに伝える用途として利用されます。
一方のStatus
は、コントローラーが処理した結果をユーザーや他のシステムに伝える用途として利用されます。
なお、CRDを利用してKubernetes APIを拡張する際には、以下の規約に従うことが推奨されています。一度目を通しておくとよいでしょう。
MarkdownViewSpec
さっそくMarkdownViewSpec
を書き換えていきましょう。
作成するカスタムコントローラにおいて、MarkdownViewコントローラーが扱うカスタムリソースとして下記のようなマニフェストを例示しました。
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"
上記のマニフェストを扱うための構造体を用意しましょう。
// 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
マーカーが付与されています。
これはこのフィールドが必須項目であることを示しており、ユーザーがマニフェストを記述する際にこの項目を省略できません。
一方のReplicas
とViewerImage
には+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
を書き換えます。
// 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
構造体のマーカーを見てみましょう。
// +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では任意のフィールドをサブリソースにはできず、status
とscale
の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のマニフェストファイルが生成されます。
---
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: {}