Webhookの実装

Kubernetesでは、リソースの作成・更新・削除をおこなう直前にWebhookで任意の処理を実行するとことができます。 MutatingWebhookではリソースの値を書き換えることができ、ValidatingWebhookでは値の検証をおこなうことができます。

controller-runtimeでは、MutatingWebhookを実装するためのDefaulterとValidatingWebhookを実装するためのValidatorが用意されています。

Defaulterの実装

まずはDefaulterの実装です。 Defaultメソッドでは、MarkdownViewリソースの値を書き換えることができます。

markdownview_webhook.go

package v1

import (
    apierrors "k8s.io/apimachinery/pkg/api/errors"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/runtime/schema"
    "k8s.io/apimachinery/pkg/util/validation/field"
    ctrl "sigs.k8s.io/controller-runtime"
    logf "sigs.k8s.io/controller-runtime/pkg/log"
    "sigs.k8s.io/controller-runtime/pkg/webhook"
)

// log is for logging in this package.
var markdownviewlog = logf.Log.WithName("markdownview-resource")

func (r *MarkdownView) SetupWebhookWithManager(mgr ctrl.Manager) error {
    return ctrl.NewWebhookManagedBy(mgr).
        For(r).
        Complete()
}

//+kubebuilder:webhook:path=/mutate-view-zoetrope-github-io-v1-markdownview,mutating=true,failurePolicy=fail,sideEffects=None,groups=view.zoetrope.github.io,resources=markdownviews,verbs=create;update,versions=v1,name=mmarkdownview.kb.io,admissionReviewVersions=v1

var _ webhook.Defaulter = &MarkdownView{}

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *MarkdownView) Default() {
    markdownviewlog.Info("default", "name", r.Name)

    if len(r.Spec.ViewerImage) == 0 {
        r.Spec.ViewerImage = "peaceiris/mdbook:latest"
    }
}

ここではr.Spec.ViewerImageが空だった場合に、デフォルトのコンテナイメージを指定しています。

Validatorの実装

次にValidatorの実装です。 ValidateCreate, ValidateUpdate, ValidateDeleteは、それぞれリソースの作成・更新・削除のタイミングで呼び出される関数です。 これらの関数の中でMarkdownViewリソースの内容をチェックし、エラーを返すことでリソースの操作を失敗させることができます。

markdownview_webhook.go

package v1

import (
    apierrors "k8s.io/apimachinery/pkg/api/errors"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/runtime/schema"
    "k8s.io/apimachinery/pkg/util/validation/field"
    ctrl "sigs.k8s.io/controller-runtime"
    logf "sigs.k8s.io/controller-runtime/pkg/log"
    "sigs.k8s.io/controller-runtime/pkg/webhook"
)

// log is for logging in this package.
var markdownviewlog = logf.Log.WithName("markdownview-resource")

func (r *MarkdownView) SetupWebhookWithManager(mgr ctrl.Manager) error {
    return ctrl.NewWebhookManagedBy(mgr).
        For(r).
        Complete()
}

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
//+kubebuilder:webhook:path=/validate-view-zoetrope-github-io-v1-markdownview,mutating=false,failurePolicy=fail,sideEffects=None,groups=view.zoetrope.github.io,resources=markdownviews,verbs=create;update,versions=v1,name=vmarkdownview.kb.io,admissionReviewVersions=v1

var _ webhook.Validator = &MarkdownView{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *MarkdownView) ValidateCreate() error {
    markdownviewlog.Info("validate create", "name", r.Name)

    return r.validate()
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *MarkdownView) ValidateUpdate(old runtime.Object) error {
    markdownviewlog.Info("validate update", "name", r.Name)

    return r.validate()
}

func (r *MarkdownView) validate() error {
    var errs field.ErrorList

    if r.Spec.Replicas < 1 || r.Spec.Replicas > 5 {
        errs = append(errs, field.Invalid(field.NewPath("spec", "replicas"), r.Spec.Replicas, "replicas must be in the range of 1 to 5."))
    }

    hasSummary := false
    for name := range r.Spec.Markdowns {
        if name == "SUMMARY.md" {
            hasSummary = true
        }
    }
    if !hasSummary {
        errs = append(errs, field.Required(field.NewPath("spec", "markdowns"), "markdowns must have SUMMARY.md."))
    }

    if len(errs) > 0 {
        err := apierrors.NewInvalid(schema.GroupKind{Group: GroupVersion.Group, Kind: "MarkdownView"}, r.Name, errs)
        markdownviewlog.Error(err, "validation error", "name", r.Name)
        return err
    }

    return nil
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *MarkdownView) ValidateDelete() error {
    markdownviewlog.Info("validate delete", "name", r.Name)

    return nil
}

今回はValidateCreateとValidateUpdateで同じバリデーションをおこなうことにしましょう。 .Spec.Replicasの値が1から5の範囲にない場合と、.Spec.MarkdownsSUMMARY.mdが含まれない場合はエラーとします。

なお、ValidationWebhookを実装する際には"k8s.io/apimachinery/pkg/util/validation/field"パッケージが役立ちます。 このパッケージを利用してエラーの原因や問題のあるフィールドを指定することで、バリデーションエラー時のメッセージがわかりやすいものになります。

動作確認

それでは、Webhookの動作確認をしてみましょう。

Webhookの実装をおこなったカスタムコントローラーをKubernetesクラスターにデプロイし、下記のようなViewerImageを指定していないマニフェストを適用します。

apiVersion: view.zoetrope.github.io/v1
kind: MarkdownView
metadata:
  name: markdownview-sample
spec:
  markdowns:
    SUMMARY.md: |
      # Summary

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

      一ページ目のコンテンツです。
  replicas: 1

作成されたリソースを確認して、ViewerImageにデフォルトのコンテナイメージ名が入っていれば成功です。

$ kubectl get markdownview markdownview-sample -o jsonpath="{.spec.viewerImage}"
peaceiris/mdbook:latest

続いてバリデーションWebhookの動作も確認してみましょう。

先ほど作成したリソースを編集してreplicasを大きな値にしたり、markdownsSUMMARY.mdを含めないようにしたりしてみましょう。 以下のようなエラーが発生すれば成功です。

$ kubectl edit markdownview markdownview-sample

markdownviews.view.zoetrope.github.io "markdownview-sample" was not valid:
 * spec.replicas: Invalid value: 10: replicas must be in the range of 1 to 5.
 * spec.markdowns: Required value: markdowns must have SUMMARY.md.

results matching ""

    No results matching ""