任意のリソースに対してmanagedFieldsに基づいたフィールドの抽出をしたい
まずはExtractXXXX関数の確認。
こんな感じのYAMLを用意。
apiVersion: apps/v1
kind: Deployment
metadata:
name: sample
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
Server-Side Applyで適用。
kubectl --server-side apply -f deploy.yaml
以下のようにExtractDeployment
を利用すると、kubectlで適用したフィールドだけを抽出することができる。
package main
import (
"context"
"encoding/json"
"fmt"
"log"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/runtime"
appsv1apply "k8s.io/client-go/applyconfigurations/apps/v1"
"k8s.io/client-go/discovery"
"k8s.io/client-go/discovery/cached/memory"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/restmapper"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)
func main() {
cli, err := getClient()
if err != nil {
log.Fatal(err)
}
dep := &appsv1.Deployment{}
err = cli.Get(context.Background(), client.ObjectKey{Namespace: "default", Name: "sample"}, dep)
if err != nil {
log.Fatal(err)
}
depac, err := appsv1apply.ExtractDeployment(dep, "kubectl")
if err != nil {
log.Fatal(err)
}
j, err := json.MarshalIndent(depac, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(j))
}
func getClient() (client.Client, error) {
config := ctrl.GetConfigOrDie()
dc, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
return nil, err
}
mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(dc))
scheme := runtime.NewScheme()
err = clientgoscheme.AddToScheme(scheme)
if err != nil {
return nil, err
}
cli, err := client.New(config, client.Options{Scheme: scheme, Mapper: mapper})
if err != nil {
return nil, err
}
return cli, nil
}
実行結果
{
"kind": "Deployment",
"apiVersion": "apps/v1",
"metadata": {
"name": "sample",
"namespace": "default",
"labels": {
"app": "nginx"
}
},
"spec": {
"replicas": 1,
"selector": {
"matchLabels": {
"app": "nginx"
}
},
"template": {
"metadata": {
"labels": {
"app": "nginx"
}
},
"spec": {
"containers": [
{
"name": "nginx",
"image": "nginx:latest",
"ports": [
{
"containerPort": 80
}
]
}
]
}
}
}
}
上記のExtractXXXX関数は、標準リソースに対するものが用意されている。
これをカスタムリソースを含む任意のリソースに対して実行できると便利なのではないか。
というわけで書いてみた。
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
"k8s.io/client-go/discovery/cached/memory"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/restmapper"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/typed"
)
func main() {
cli, err := getClient()
if err != nil {
log.Fatal(err)
}
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(schema.GroupVersionKind{
Group: appsv1.GroupName,
Version: appsv1.SchemeGroupVersion.Version,
Kind: "Deployment",
})
err = cli.Get(context.Background(), client.ObjectKey{Namespace: "default", Name: "sample"}, u)
if err != nil {
log.Fatal(err)
}
fieldset := &fieldpath.Set{}
objManagedFields := u.GetManagedFields()
for _, mf := range objManagedFields {
if mf.Manager != "kubectl" || mf.Operation != metav1.ManagedFieldsOperationApply {
continue
}
fs := &fieldpath.Set{}
err = fs.FromJSON(bytes.NewReader(mf.FieldsV1.Raw))
if err != nil {
log.Fatal(err)
}
fieldset = fieldset.Union(fs)
}
d, err := typed.DeducedParseableType.FromUnstructured(u.Object)
if err != nil {
log.Fatal(err)
}
x := d.ExtractItems(fieldset.Leaves()).AsValue().Unstructured()
m, ok := x.(map[string]interface{})
if !ok {
log.Fatal("cannot cast")
}
j, err := json.MarshalIndent(m, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(j))
}
実行結果
{
"metadata": {
"labels": {
"app": "nginx"
}
},
"spec": {
"replicas": 1,
"selector": null,
"template": {
"metadata": {
"labels": {
"app": "nginx"
}
},
"spec": {
"containers": [
{
"image": "nginx:latest",
"imagePullPolicy": "Always",
"name": "nginx",
"ports": [
{
"containerPort": 80,
"protocol": "TCP"
}
],
"resources": {},
"terminationMessagePath": "/dev/termination-log",
"terminationMessagePolicy": "File"
}
]
}
}
}
}
うまくいったかと思いきや、selector
がnullになってしまっている。
あと、resources
, terminationMessagePath
, terminationMessagePolicy
が入ってるのはなぜ?
なぜselector
がnullになってしまうのか。
まずは、DeploymentリソースのmanagedFieldsを見てみる。
managedFields:
- apiVersion: apps/v1
fieldsType: FieldsV1
fieldsV1:
f:metadata:
f:labels:
f:app: {}
f:spec:
f:replicas: {}
f:selector: {}
f:template:
f:metadata:
f:labels:
f:app: {}
f:spec:
f:containers:
k:{"name":"nginx"}:
.: {}
f:image: {}
f:name: {}
f:ports:
k:{"containerPort":80,"protocol":"TCP"}:
.: {}
f:containerPort: {}
manager: kubectl
operation: Apply
time: "2021-09-11T02:30:32Z"
f:selector
の下の要素がない…。
というわけで、LabelSelector
の構造体の定義を見てみる。
// A label selector is a label query over a set of resources. The result of matchLabels and
// matchExpressions are ANDed. An empty label selector matches all objects. A null
// label selector matches no objects.
// +structType=atomic
type LabelSelector struct {
// matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
// map is equivalent to an element of matchExpressions, whose key field is "key", the
// operator is "In", and the values array contains only "value". The requirements are ANDed.
// +optional
MatchLabels map[string]string `json:"matchLabels,omitempty" protobuf:"bytes,1,rep,name=matchLabels"`
// matchExpressions is a list of label selector requirements. The requirements are ANDed.
// +optional
MatchExpressions []LabelSelectorRequirement `json:"matchExpressions,omitempty" protobuf:"bytes,2,rep,name=matchExpressions"`
}
// A label selector requirement is a selector that contains values, a key, and an operator that
// relates the key and values.
type LabelSelectorRequirement struct {
// key is the label key that the selector applies to.
// +patchMergeKey=key
// +patchStrategy=merge
Key string `json:"key" patchStrategy:"merge" patchMergeKey:"key" protobuf:"bytes,1,opt,name=key"`
// operator represents a key's relationship to a set of values.
// Valid operators are In, NotIn, Exists and DoesNotExist.
Operator LabelSelectorOperator `json:"operator" protobuf:"bytes,2,opt,name=operator,casttype=LabelSelectorOperator"`
// values is an array of string values. If the operator is In or NotIn,
// the values array must be non-empty. If the operator is Exists or DoesNotExist,
// the values array must be empty. This array is replaced during a strategic
// merge patch.
// +optional
Values []string `json:"values,omitempty" protobuf:"bytes,3,rep,name=values"`
}
MatchLabels
のほうはただのmapなので問題ないように見える。
MatchExpressions
のほうは、もしこれがカスタムリソースなのであれば、+listType=map
, +listMapKey=key
などのマーカーをつけてやるとよさそう。(参考)
(ところで、patchMergeKey
をつける場所間違ってね?)
ちなみに、カスタムリソースにLabelSelector
を持たせてみるとmanagedFields
は以下のようになり、Extractも問題なく動く。なぜこうなるのか分からん…
managedFields:
- apiVersion: sample.zoetrope.github.io/v1
fieldsType: FieldsV1
fieldsV1:
f:spec:
f:selector:
f:matchLabels:
f:key: {}
manager: kubectl
operation: Apply
time: "2021-09-11T02:56:27Z"
つぎに、ExtractDeployment
ではselector
が抽出できるのに、unstructured
方式だと抽出できないのはなぜか調べてみる。
どうやら以下のschemaYAML
というものが鍵らしい。
このYAMLはapplyconfiguration-genというツールで生成されている。
さらにapplyconfiguration-genの入力となるデータは、models-schemaというツールで生成されている。
そしてmodels-schemaはgoの型定義から生成された情報を元にしている。
カスタムリソースの場合は、goの型定義を持ってくることは難しい。
じゃあ、CRDに含まれてるOpenAPIV3Schemaから、Extractに必要なschemaを生成してあげればうれしいのでは?と思ってコードを書いてみた。
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
openapiv2 "github.com/googleapis/gnostic/openapiv2"
"gopkg.in/yaml.v2"
extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
"k8s.io/client-go/discovery/cached/memory"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/restmapper"
"k8s.io/kube-openapi/pkg/schemaconv"
utilproto "k8s.io/kube-openapi/pkg/util/proto"
"k8s.io/kube-openapi/pkg/validation/spec"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"sigs.k8s.io/structured-merge-diff/v4/typed"
)
func main() {
cli, err := getClient()
if err != nil {
log.Fatal(err)
}
u := &unstructured.Unstructured{}
u.SetGroupVersionKind(schema.GroupVersionKind{
Group: "sample.zoetrope.github.io",
Version: "v1",
Kind: "Test",
})
err = cli.Get(context.Background(), client.ObjectKey{Namespace: "default", Name: "sample"}, u)
if err != nil {
log.Fatal(err)
}
fieldset := &fieldpath.Set{}
objManagedFields := u.GetManagedFields()
for _, mf := range objManagedFields {
if mf.Manager != "kubectl" || mf.Operation != metav1.ManagedFieldsOperationApply {
continue
}
fs := &fieldpath.Set{}
err = fs.FromJSON(bytes.NewReader(mf.FieldsV1.Raw))
if err != nil {
log.Fatal(err)
}
fieldset = fieldset.Union(fs)
}
schema, err := genSchema()
if err != nil {
log.Fatal(err)
}
objectType := parser(schema).Type("v1.Test")
d, err := objectType.FromStructured(u)
if err != nil {
log.Fatal(err)
}
x := d.ExtractItems(fieldset.Leaves()).AsValue().Unstructured()
m, ok := x.(map[string]interface{})
if !ok {
log.Fatal("cannot cast")
}
j, err := json.MarshalIndent(m, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(j))
}
func parser(schema string) *typed.Parser {
var err error
parser, err := typed.NewParser(typed.YAMLObject(schema))
if err != nil {
panic(fmt.Sprintf("Failed to parse schema: %v", err))
}
return parser
}
func genSchema() (string, error) {
cli, err := getClient()
if err != nil {
return "", err
}
crd := &extv1.CustomResourceDefinition{}
err = cli.Get(context.Background(), client.ObjectKey{Name: "tests.sample.zoetrope.github.io"}, crd)
if err != nil {
return "", err
}
schemaDefs := make(map[string]spec.Schema, len(crd.Spec.Versions))
for _, version := range crd.Spec.Versions {
b, err := json.Marshal(version.Schema.OpenAPIV3Schema)
if err != nil {
return "", err
}
schema := spec.Schema{}
err = json.Unmarshal(b, &schema)
if err != nil {
return "", err
}
schemaDefs[version.Name+"."+crd.Spec.Names.Kind] = schema
}
model := &spec.Swagger{
SwaggerProps: spec.SwaggerProps{
Definitions: schemaDefs,
Info: &spec.Info{
InfoProps: spec.InfoProps{
Title: "Kubernetes",
Version: "unversioned",
},
},
Swagger: "2.0",
},
}
models, err := toValidatedModels(model)
if err != nil {
return "", err
}
schema, err := schemaconv.ToSchema(models)
if err != nil {
return "", err
}
schemaYAML, err := yaml.Marshal(schema)
if err != nil {
return "", err
}
return string(schemaYAML), nil
}
func toValidatedModels(openAPISchema *spec.Swagger) (utilproto.Models, error) {
rawMinimalOpenAPISchema, err := json.Marshal(openAPISchema)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal openAPI as JSON: %w", err)
}
document, err := openapiv2.ParseDocument(rawMinimalOpenAPISchema)
if err != nil {
return nil, fmt.Errorf("failed to parse OpenAPI document for file: %w", err)
}
models, err := utilproto.NewOpenAPIData(document)
if err != nil {
return nil, fmt.Errorf("failed to create OpenAPI models for file: %w", err)
}
return models, nil
}
func getClient() (client.Client, error) {
config := ctrl.GetConfigOrDie()
dc, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
return nil, err
}
mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(dc))
scheme := runtime.NewScheme()
err = clientgoscheme.AddToScheme(scheme)
if err != nil {
return nil, err
}
err = extv1.AddToScheme(scheme)
if err != nil {
return nil, err
}
cli, err := client.New(config, client.Options{Scheme: scheme, Mapper: mapper})
if err != nil {
return nil, err
}
return cli, nil
}
で、書いてみたのはいいんだけど、そもそもカスタムリソースの場合は問題なくExtractできてるから、こんなことしなくてもいいのでは…?
あと、Goの構造体から情報をとってくるのに比べて、CRDに含まれている情報は少ないので、schemaとして不十分かもしれない。
結論としては
- 標準リソースは、用意されているExtractXXXX関数を利用する
- カスタムリソースは、2番目の投稿に書いた方式で抽出する
でOK?