Rancher API Serverの基礎となるNorman API Framework
Rancher API Serverの基礎となるNorman API Framework
ここまで紹介した、pkg/api/customization、pkg/api/storeは、このAPI Framework機能をふんだんに利用して実装しているため、実装されているロジックを理解するために、まずはこのAPI Framework自体を理解する必要があります。そのため、このAPI Frameworkの機能もRancher APIの実装を用いながら紹介します。
先ほど紹介したとおり、pkg/api/server/server.goは、Rancher APIに関する初期化処理に責任を持ち、このファイルの中で、NormanのAPI Serverも初期化されています。
リスト10:pkg/api/server/server.go
1 package server
2
3 import (
... <省略>
9 rancherapi "github.com/rancher/rancher/pkg/api"
... <省略>
22 )
23
24 func New(ctx context.Context, scaledContext *config.ScaledContext, clusterManager *clustermanager.Manager,
25 k8sProxy http.Handler) (http.Handler, error) {
... <省略>
39 server, err := rancherapi.NewServer(scaledContext.Schemas)
40 if err != nil {
41 return nil, err
42 }
... <省略>
51 return server, err
52 }
39行目のrancherapi.NewServerにSchema(vendor/github.com/rancher/types/apis/management.cattle.io/v3/schema/schema.go)を渡して、API Serverを初期化しています。
rancherapi.NewServerは、下記のpkg/api/server.go内で定義されています。
リスト11:pkg/api/server.go(の一部)
1 package api
2
3 import (
... <省略>
7 normanapi "github.com/rancher/norman/api"
... <省略>
10 )
11
12 func NewServer(schemas *types.Schemas) (*api.Server, error) {
13 server := normanapi.NewAPIServer()
14 if err := server.AddSchemas(schemas); err != nil {
15 return nil, err
16 }
17 server.CustomAPIUIResponseWriter(cssURL, jsURL, settings.APIUIVersion.Get)
18 return server, nil
19 }
20
<省略>
NewServer内ではNormanのAPI Serverを初期化(13行目)し、Schemaを設定(14行目)しています。また17行目では、UI用のcss、jsのURLを設定しています。
このAPI Serverは、複数のSchema(types/types.go)オブジェクトを保持することができます。ServeHTTP関数を実装しており、HTTPリクエストを受けると、登録されているSchemaをバージョン、リソースタイプ名で検索し、適切なSchemaオブジェクトを探します。適切なSchemaオブジェクトが見つかった場合、SchemaオブジェクトにされているHandler関数を実行します。
Schemaオブジェクトは、次のような構造体になっています。
リスト12:Schemaオブジェクト(vendor/github.com/rancher/norman/types/types.goにある構造体)
type Schema struct {
ID string `json:"id,omitempty"`
Embed bool `json:"embed,omitempty"`
EmbedType string `json:"embedType,omitempty"`
CodeName string `json:"-"`
CodeNamePlural string `json:"-"`
PkgName string `json:"-"`
Type string `json:"type,omitempty"`
BaseType string `json:"baseType,omitempty"`
Links map[string]string `json:"links"`
Version APIVersion `json:"version"`
PluralName string `json:"pluralName,omitempty"`
ResourceMethods []string `json:"resourceMethods,omitempty"`
ResourceFields map[string]Field `json:"resourceFields"`
ResourceActions map[string]Action `json:"resourceActions,omitempty"`
CollectionMethods []string `json:"collectionMethods,omitempty"`
CollectionFields map[string]Field `json:"collectionFields,omitempty"`
CollectionActions map[string]Action `json:"collectionActions,omitempty"`
CollectionFilters map[string]Filter `json:"collectionFilters,omitempty"`
DynamicSchemaVersion string `json:"dynamicSchemaVersion,omitempty"`
Scope TypeScope `json:"-"`
InternalSchema *Schema `json:"-"`
Mapper Mapper `json:"-"`
ActionHandler ActionHandler `json:"-"`
LinkHandler RequestHandler `json:"-"`
ListHandler RequestHandler `json:"-"`
CreateHandler RequestHandler `json:"-"`
DeleteHandler RequestHandler `json:"-"`
UpdateHandler RequestHandler `json:"-"`
InputFormatter InputFormatter `json:"-"`
Formatter Formatter `json:"-"`
CollectionFormatter CollectionFormatter `json:"-"`
ErrorHandler ErrorHandler `json:"-"`
Validator Validator `json:"-"`
Store Store `json:"-"`
}
このSchemaは、リソースごとに作成されることが期待されています。ここで、いくつか注目してほしいフィールドを紹介します。
Schemaオブジェクトのフィールド(一部)
| フィールド名 | 説明 |
|---|---|
| ID | リソースタイプが指定される |
| Version | APIのVersionが指定される |
| CollectionMethods | このSchemaが実装しているAPI Path(/<Version>/<ID>)に対するHTTP Methodが指定される |
| CollectionActions | このSchemaが実装しているAPI Path(/<Version>/<ID>)に対するActionが指定されている |
| ResourceMethods | このSchemaが実装しているAPI Path(/<Version>/<ID>/<Resource Name>)に対するHTTP Methodが指定される API PathよりSchemaが特定できたとしても、対応していないHTTP Methodで呼ばれていた場合405ステータスコードが返却される |
| ResourceActions | このSchemaが実装しているAPI Path(/<Version>/<ID>/<Resource Name>)に対するActionが指定されている |
| ResourceFields | このSchemaで登録されるリソースがどのようなフィールドをどのような型で持っているのかが指定されている |
| ActionHandler | actionという名前のURLパラメータが指定された際に実行される関数 (/<Version>/<ID>?action=<Action Name>) (/<Version>/<ID>/<Resource Name>?action=<Action Name> |
| ListHandler | GET /<Version>/<ID>が呼ばれた際に実行される関数 |
| CreateHandler | POST /<Version>/<ID>が呼ばれた際に実行される関数 |
| UpdateHandler | PUT /<Version>/<ID>/<Resource Name>が呼ばれた際に実行される関数 |
| DeleteHandler | DELETE /<Version>/<ID>/<Resource Name>が呼ばれた際に実行される関数 |
| Store | ByID、List、Create、Update、Delete、Watchを実装したオブジェクトが指定される このSchemaのXXXHandlerを通じて、リソースの取得、更新、削除をする際は、このオブジェクトの関数を通して実現している |
| Mapper | Storeに保存される際(ToInternal)と、Storeから取得する際(FromInternal)に呼び出されるロジックを実装したオブジェクトが指定される |
このように、Schemaでは特定のリソースタイプのAPI(/<Version>/<ID>)の具体的な処理(XXXHandler、Store、Mapper)、どのHTTP Methodでアクセス可能か(XXXXMethods)、リソースのフィールドはどのようなものがあるのか(ResourceFields)が指定されています。
Handler、Store、Mapperの関係性にいまいちピンとこない方もいらっしゃると思いますので、もう少し詳しく紹介したいと思います。Norman API Serverでは、SchemaのHandler、Store、Mapperを入れ替えることによって、リソースごとにロジックや保存先を決定することができます。しかしRancherは、基本的にはHandlerを入れ替えることはせず、デフォルトのHandler(api/handler/XXX.go)を利用して、Handler内で呼び出しているStoreを入れ替えて、保存先を指定しています。また、Storeについても基本的には、store/proxy/proxy_store.goで定義されているNewProxyStore関数を使って生成できるKubernetesにリソースを保存/取得するためのStoreオブジェクトを、リソースタイプごとに生成し利用しています。そのため、保存/取得の際のリソースの変換処理については、NewProxyStore関数で生成されるStore内で呼び出しているMapperで指定しています。
Rancherは、これらのStore、ActionHandler、Formatterなどを、pkg/api/server内で指定しています。また肝心のリソースごとのSchemaですが、Schemaはvendor/github.com/rancher/types/apis/management.cattle.io/v3/schemaで定義されています。実はSchemaオブジェクトを直接生成しているのではなく、Normanで提供されているSchemaの自動生成機能を利用しています。
Schemaの自動生成を使う場合は、まず対象となるリソースのフィールドを定義した構造体を用意する必要があります。この構造体は、特定のリソースタイプのみを対象とするKubernetes Client(typed client、listeners)を自動生成するためにも利用されており、Kubernetesのカスタムコントローラを実装する際は常に必要になります。
Rancherは、Rancherが作成するすべてのCRDについて、この構造体を下記のファイルに定義しています。
- vendor/github.com/rancher/types/apis/management.cattle.io/v3/<Resourceの種類>_types.go
- vendor/github.com/rancher/types/apis/management.cattle.io/v3public/<Resourceの種類>_types.go
- vendor/github.com/rancher/types/apis/project.cattle.io/v3/<Resourceの種類>_types.go
サンプルとして、Catalogリソースの構造体定義を掲載します。
リスト13:vendor/github.com/rancher/types/apis/management.cattle.io/v3/catalog_types.go
1 package v3
2
3 import (
4 "github.com/rancher/norman/condition"
5 "k8s.io/api/core/v1"
6 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
7 )
8
9 type Catalog struct {
10 metav1.TypeMeta `json:",inline"`
11 // Standard object’s metadata. More info:
12 // https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#metadata
13 metav1.ObjectMeta `json:"metadata,omitempty"`
14 // Specification of the desired behavior of the the cluster. More info:
15 // https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#spec-and-status
16 Spec CatalogSpec `json:"spec"`
17 Status CatalogStatus `json:"status"`
18 }
... <省略>
Normanは、このリソース構造体を元に、このリソースに関するSchemaを自動生成することができます。Rancherは、下記のファイルで自動生成しています。Catalog関連のリソースのSchemaを自動生成するロジックを例に、どのように自動生成しているか紹介します。
リスト14:vendor/github.com/rancher/types/apis/management.cattle.io/v3/schema/schema.go
... <省略>
52 func catalogTypes(schemas *types.Schemas) *types.Schemas {
53 return schemas.
54 AddMapperForType(&Version, v3.Catalog{},
55 &m.Move{From: "catalogKind", To: "kind"},
56 &m.Embed{Field: "status"},
57 &m.Drop{Field: "helmVersionCommits"},
58 ).
59 MustImportAndCustomize(&Version, v3.Catalog{}, func(schema *types.Schema) {
60 schema.ResourceActions = map[string]types.Action{
61 "refresh": {},
62 }
63 schema.CollectionActions = map[string]types.Action{
64 "refresh": {},
65 }
66 }).
67 AddMapperForType(&Version, v3.Template{},
68 m.DisplayName{},
69 ).
70 MustImport(&Version, v3.Template{}, struct {
71 VersionLinks map[string]string
72 }{}).
73 MustImport(&Version, v3.TemplateVersion{}).
74 MustImport(&Version, v3.TemplateContent{})
75 }
... <省略>
このcatalogTypes関数は、Schemaを複数含有するSchemasオブジェクト(schemas)を引数に取ります。このオブジェクトは新しくSchemaを作成し、Schemasに追加するための関数をいくつか提供し、Rancherもそれを利用しています。
AddMapperForType(norman/types/reflection.go)
この関数は、Schemaの生成はせず、SchemasオブジェクトでリソースタイプをKeyに作成したMapperオブジェクトを保持します。そしてSchemasオブジェクト経由で該当リソースタイプのSchemaオブジェクトが生成された際に、MapperがSchemaに設定されます。
バージョン(第一引数)、リソース(第二引数)を指定し、該当のMapperを作成します。Mapperは、リソースの変換ルールを第三引数以降に複数指定することができます。上述のリストの55~57行目でリソースの変換ルールを定義しています
- &m.Move{From: "catalogKind", To: "kind"}
- Kubernetes上のCatalogリソースの"catalogKind"というフィールドをAPIリソースに変換する際に"kind"に変換します
- &m.Embed{Field: "status"}
- Kubernetes上のCatalogリソースの"status"というフィールドをAPIリソースに変換する際に、"status"フィールド内のサブフィールドを展開します
- &m.Drop{Field: "helmVersionCommits"}
- Kubernetes上のCatalogリソースの"helmVersionCommits"というフィールドをAPIリソースに変換する際に削除します
MustImport(norman/types/reflection.go)
バージョン(第一引数)、リソース(第二引数)を指定し、該当リソースタイプのSchemaを作成します。
MustImportAndCustomize(norman/types/reflection.go)
バージョン(第一引数)、リソース(第二引数)を指定し、該当リソースタイプのSchemaを作成します。この関数は、Schema作成後に、Schemaに対して実施したい追加ロジックを第三引数の関数で指定することができます。この関数は、Schema作成後にSchemaを引数に実行されます。59行目のCatalogリソースのMustImportAndCustomizeでは、生成したCatalogリソースのSchemaに対してActionを指定しています。
このような関数を利用して、Rancherはすべてのリソースに対してSchemaを自動生成し、Norman API Serverを初期化することで、Rancher Management APIを実装しています。
まとめ
この記事では、Rancher Agentのロジックが含まれるpkg/agentとRancher APIのロジックが含まれているpkg/api、またpkg/api内でも利用されているNormanのAPI Frameworkについて詳しく紹介しました。Rancher APIがどのように実現されているかある程度イメージを掴んでいただけたと思います。次回の記事では、Rancher Controller(pkg/controllers)がどのように実装されているのかを紹介します。
- この記事のキーワード