Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Sig API Machinery Deep Dive

Sig API Machinery Deep Dive

KubeCon Copenhagen 2018

Dr. Stefan Schimanski

July 04, 2018
Tweet

Transcript

  1. @the_sttts Agenda • Outlook to Kubernetes 1.11+ • Deep Dive

    into CustomResourceDefinitions • Questions
  2. @the_sttts Outlook – Custom Resources • Kubernetes 1.11+ • ⍺:

    Multiple versions without conversion – design proposal • ⍺: Pruning – in validation spec unspecified fields are removed – blocker for GA • ⍺: Defaulting – defaults from OpenAPI validation schema are applied • ⍺: Graceful Deletion – maybe, to be discussed – #63162 • ⍺: Server Side Printing Columns – “kubectl get” customization – #60991 • β: Subresources – ⍺ since 1.10 – #62786 • OpenAPI additionalProperties allowed now (mutually exclusive with properties) • Kubernetes 1.12+ • Multiple versions with declarative field renames • Strict create mode? Discuss: #5889 – my favorite CRD UX issue Related: CRD OpenAPI validation spec not served by kube-apiserver
  3. @the_sttts The Future: Versioning • Most asked for feature for

    long time • It is coming, but slowly "NoConversion": maybe in 1.11 apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: contibutorsummit.kubecon.io spec: group: kubecon.io version: v1 versions: - name: v1 storage: true - name: v1alpha1 "Declarative Conversions": maybe in 1.12+ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: contibutorsummit.kubecon.io spec: group: kubecon.io version: v1 conversions: declarative: renames: from: v1alpha1 to: v1 old: .spec.foo new: bar
  4. @the_sttts Outlook – Prepare for Pruning • Deep change of

    semantics of Custom Resources • From JSON blob store to schema based storage OpenAPIv3Schema: { properties: { foo: {} } } • Example CR: { "foo": 1, "bar": 2 } → { "foo": 1 } Opt-in in CRD v1beta1 Mandatory in GA
  5. @the_sttts apiextensions-apiserver CustomResourceDefinitions are served by https://github.com/kubernetes/apiextensions-apiserver usually embedded into

    kube-apiserver via delegation. kube-apiserver kube-aggregator kube resources apiextensions- apiserver 404 etcd "delegation" "aggregation"
  6. @the_sttts api-machinery-session.kubecon.io.yaml apiVersion: kubecon.io/v1 kind: Session metadata: name: api-machinery namespace:

    eu2018 spec: type: deepdive title: "SIG API Machinery Deep Dive" capacity: 42 status: attendees: 23 conditions: - lastTransitionTime: 2018-05-04T12:47:54Z status: "True" type: Started
  7. @the_sttts sessions.kubecon.io.yaml apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: sessions.kubecon.io spec:

    group: kubecon.io version: v1 scope: Namespaced names: plural: sessions singular: session kind: Session # shortNames: # - talks must match the kind: - usually capital singular - like the Go type the resource: - usually lower-case singular - in http path
  8. @the_sttts Create & wait for Established $ kubectl create –f

    sessions.kubecon.io.yaml ... and then watch status.conditions["Established"]. Conditions: → NamesAccepted → Established = no name conflicts = CRD is served* * There is a race ! – to be fixed in #63068. Better wait 5 seconds in ≤1.10.
  9. @the_sttts kubectl get sessions –v=7 • I0429 21:17:53.042783 66743 round_trippers.go:383]

    GET https://localhost:6443/apis • I0429 21:17:53.135811 66743 round_trippers.go:383] GET https://localhost:6443/apis/kubecon.io/v1 • I0429 21:17:53.138353 66743 round_trippers.go:383] GET https://localhost:6443/apis/kubecon.io/v1/namespaces/default/sessions No resources found. sessions → kind Session resource sessions discovery LIST note: we also support "shortNames" in API group kubecon.io/v1 We call this "REST mapping"
  10. @the_sttts api-machinery-session.kubecon.io.yaml apiVersion: kubecon.io/v1 kind: Session metadata: name: api-machinery namespace:

    eu2018 spec: type: deepdive title: "SIG API Machinery Deep Dive" capacity: 42 status: attendees: 23 conditions: - lastTransitionTime: 2018-05-04T12:47:54Z status: "True" type: Started Recommended to follow the spec+status pattern. Important for /status subresource.
  11. @the_sttts etcd Storage $ export ETCDCTL_API=3 $ etcdctl get /

    --prefix --keys-only | grep kubecon /registry/apiextensions.k8s.io/customresourcedefinitions/sessions.kubecon.io /registry/apiregistration.k8s.io/apiservices/v1.kubecon.io /registry/kubecon.io/sessions/eu2018/api-machinery $ etcdctl get /registry/kubecon.io/sessions/eu2018/api-machinery {"apiVersion":"kubecon.io/v1","kind":"Session","metadata":{"clusterName":"","creat ionTimestamp":"2018-04-29T20:30:27Z","generation":1,"name":"api- machinery","namespace":"eu2018","resourceVersion":"","selfLink":"","uid":"273a1ae3 -4bec-11e8-8d91-4c3275978b79"},"spec":{"capacity":10,"title":"SIG API Machinery Deep Dive","type":"deepdive"},"status":{"attendees":10,"conditions":[{"lastTransitionTi me":"2018-05-04T12:47:54Z","status":"True","type":"Started"}]}} unverified JSON blob
  12. @the_sttts unstructured.Unstructured Internally, CustomResources are import "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" unstructured.Unstructured{ Object: map[string]interface{}

    } i.e. maps+slices+values. Dynamic Client • client-go counterpart: k8s.io/client-go/dynamic • in 1.11+ with sane interface #62913: dynamic.NewForConfig(cfg).Resource(gvr).Namespace(ns).Get(name, opts) • generated, typed clients are generally preferred json.Unmarshal
  13. @the_sttts Zoom into apiextensions-apiserver kube-apiserver kube- aggregator kube resources apiextensions-apiserver

    404 etcd "delegation" "aggregation" authn/z CR handlers CR handlers CR handlers ⟲Naming Controller ⟲CRD Finalizer request conversion & defaulting storage conversion & defaulting REST logic result conversion validation admission decoding encode GET CREATE LIST UPDATE DELETE WATCH mutating webhooks validating webhooks NoOps json.Unmarshal
  14. spec: type: deepdive title: "SIG API Machinery De… capacity: 42

    status: attendees: 23 conditions: - lastTransitionTime: 2018… status: "True" type: Started properties: spec: properties: type: anyOf: [{"pattern": "^deepdive$"}, …] title: {"type": "string"} capacity: {"type": "format": "integer", "minimum": 0, "default": 0} required: ["type", "title", "capacity"] status: properties: attendees: {"type": "number", "format": "integer", "minimum": 0} conditions: type: "array" items: properties: lastTransitionTime: {"type": "dateTime"} status: anyOf: [{"pattern": "^True$"}, …] type: anyOf: [{"pattern": "^Started$"}, …] required: ["lastTransitionTime", "status", "type"] OpenAPI v3 Schema a quantor (anyOf, oneOf, allOf exist) note: enum is forbidden (why?) regular expression maybe in 1.11+ Custom Resource Helpful tools: kubernetes/kube-openapi#37 tamalsaha/kube-openapi-generator Some other tool from prometheus-operator? Rancher has another one, speak to @lemonjet
  15. @the_sttts etcd Storage – Pruning $ export ETCDCTL_API=3 $ etcdctl

    get / --prefix --keys-only | grep kubecon /registry/apiextensions.k8s.io/customresourcedefinitions/sessions.kubecon.io /registry/apiregistration.k8s.io/apiservices/v1.kubecon.io /registry/kubecon.io/sessions/eu2018/api-machinery $ etcdctl get /registry/kubecon.io/sessions/eu2018/api-machinery {"apiVersion":"kubecon.io/v1","kind":"Session","metadata":{"clusterName":""," creationTimestamp":"2018-04-29T20:30:27Z","generation":1,"name":"api- machinery","namespace":"eu2018","resourceVersion":"","selfLink":"","uid":"273 a1ae3-4bec-11e8-8d91-4c3275978b79"},"spec":{"capacity":10,"title":"SIG API Machinery Deep Dive","type":"deepdive"},"status":{"attendees":10,"conditions":[{"lastTransit ionTime":"2018-05-04T12:47:54Z","status":"True","type":"Started", "someUnknownField":"someValue", "someFutureField":"dangerous value"}]}} unverified JSON blob with possibly unspecified fields we need pruning! Kube 1.11+
  16. @the_sttts Deeper Dive – go-openapi/validate validator := validate.NewSchemaValidator(schema, …) result

    := validator.Validate(obj) specSchema := result.FieldSchemata()[ validator.NewFieldKey(obj, "spec") ] = OpenAPIv3Schema = JSON object OpenAPI validation result gives us a mapping: JSON nodes → OpenAPI schemata: spec: type: deepdive title: "SIG API Machinery De… capacity: 42 status: properties: spec: properties: type: anyOf: [{"pattern": "^deepdive$"}, …] title: {"type": "string"} capacity: {"type": "format": "integer", "minimum": 0, "default": 0}
  17. @the_sttts Deeper Dive – go-openapi/validate func ApplyDefaults(r *validate.Result) { fieldSchemata

    := r.FieldSchemata() for key, schemata := range fieldSchemata { LookForDefaultingScheme: for _, s := range schemata { if s.Default != nil { if _, found := key.Object()[key.Field()]; !found { key.Object()[key.Field()] = s.Default break LookForDefaultingScheme } } } } } ← defaulting algorithm on half a slide spec: type: deepdive title: "SIG API Machinery De… capacity: 42 "someFutureField":"…" properties: spec: properties: type: anyOf: [{"pattern": "^deepdive title: {"type": "string"} capacity: {"type": "format": "in required: ["type", "title", "capac sketch of pruning→
  18. @the_sttts Zoom into apiextensions-apiserver kube-apiserver kube- aggregator kube resources apiextensions-apiserver

    404 etcd "delegation" "aggregation" authn/z CR handlers CR handlers CR handlers ⟲Naming Controller ⟲CRD Finalizer conversion & pruning & defaulting REST logic result conversion validation admission decoding defaulting & pruning & conversion encode GET CREATE LIST UPDATE DELETE WATCH mutating webhooks validating webhooks
  19. @the_sttts Scaling the session $ kubectl scale --replicas=10 -n eu2018

    sessions/api-machinery --v=7 I0429 22:33:03.083150 74535 round_trippers.go:383] GET https://localhost:6443/apis/kubecon.io/v1/namespaces/eu2018/sessions/api- machinery/scale I0429 22:33:03.083725 74535 round_trippers.go:408] Response Status: 404 Not Found in 0 milliseconds We call this "subresource /scale".
  20. spec: type: deepdive title: "SIG API Machinery De… capacity: 42

    status: attendees: 23 conditions: - lastTransitionTime: 2018… status: "True" type: Started apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: sessions.kubecon.io spec: … subresources: scale: specReplicasPath: .spec.capacity statusReplicasPath: .status.attendees # status: {} alpha in 1.10 hopefully beta in 1.11 JSON paths
  21. @the_sttts Scaling the session $ kubectl scale --replicas=10 -n eu2018

    sessions/api-machinery --v=7 • I0429 22:43:14.757286 80725 round_trippers.go:405] GET https://localhost:6443/apis/kubecon.io/v1/namespaces/eu2018/sessions/api-machinery/scale 200 OK in 0 milliseconds • I0429 22:43:14.757318 80725 request.go:897] Response Body: { "kind": "Scale", "apiVersion": "autoscaling/v1", "metadata": {...}, "spec": {"replicas":42}, "status":{"replicas":23} } • PUT https://localhost:6443/apis/kubecon.io/v1/namespaces/eu2018/sessions/api-machinery/scale 200 OK in 2 milliseconds session.kubecon.io/api-machinery scaled
  22. @the_sttts (polymorphic) scale client import ( "k8s.io/client-go/discovery/cached" "k8s.io/client-go/scale" ) cachedDiscovery

    := discocache.NewMemCacheClient(hpaClientGoClient.Discovery()) restMapper := discovery.NewDeferredDiscoveryRESTMapper(cachedDiscovery) scaleKindResolver := scale.NewDiscoveryScaleKindResolver(hpaClientGoClient.Discovery()) scaleClient, err := scale.NewForConfig(cfg, restMapper, dynamic.LegacyAPIPathResolverFunc, scaleKindResolver)
  23. spec: type: deepdive title: "SIG API Machinery De… capacity: 42

    status: attendees: 23 conditions: - lastTransitionTime: 2018… status: "True" type: Started apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: name: sessions.kubecon.io spec: … subresources: scale: specReplicasPath: .spec.capacity statusReplicasPath: .status.attendees status: {} alpha in 1.10 hopefully beta in 1.11 JSON paths spec/status split main endpoint only changes .spec /status changes .status
  24. @the_sttts Outlook – Prepare for Pruning • Deep change of

    semantics of Custom Resources • From JSON blob store to schema based storage OpenAPIv3Schema: { properties: { foo: {} } } • Example CR: { "foo": 1, "bar": 2 } → { "foo": 1 } Opt-in in CRD v1beta1 Mandatory in GA
  25. @the_sttts Outlook – Custom Resources • Kubernetes 1.11+ • ⍺:

    Multiple versions without conversion – design proposal • ⍺: Pruning – in validation spec unspecified fields are removed – blocker for GA • ⍺: Defaulting – defaults from OpenAPI validation schema are applied • ⍺: Graceful Deletion – maybe, to be discussed – #63162 • ⍺: Server Side Printing Columns – “kubectl get” customization – #60991 • β: Subresources – ⍺ since 1.10 – #62786 • OpenAPI additionalProperties allowed now (mutually exclusive with properties) • Kubernetes 1.12+ • Multiple versions with declarative field renames • Strict create mode? Discuss: #5889 – my favorite CRD UX issue Related: CRD OpenAPI validation spec not served by kube-apiserver
  26. @the_sttts Towards a controller: Golang types pkg/apis/kubecon.io/v1/types.go type Session struct

    { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec SessionSpec `json:"spec"` Status SessionStatus `json:"status"` } type SessionSpec struct { Type SessionType `json:"type"` Capacity int `json:"capacity"` Title string `json:"title"` } type SessionStatus struct { Attendees int `json:"attendees,omitempty"` Conditions []SessionCondition `json:"conditions,omitempty"` }
  27. @the_sttts type Session struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec

    SessionSpec `json:"spec"` Status SessionStatus `json:"status"` } type SessionSpec struct { Type SessionType `json:"type"` Capacity int `json:"capacity"` Title string `json:"title"` } type SessionStatus struct { Attendees int `json:"attendees,omitempty"` Conditions []SessionCondition `json:"conditions,omitempty"` } Towards a controller: Golang types pkg/apis/kubecon.io/v1/types.go doc.go: // +k8s:deepcopy-gen=package package v1
  28. @the_sttts // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type Session struct { metav1.TypeMeta

    `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec SessionSpec `json:"spec"` Status SessionStatus `json:"status"` } type SessionSpec struct { Type SessionType `json:"type"` Capacity int `json:"capacity"` Title string `json:"title"` } type SessionStatus struct { Attendees int `json:"attendees,omitempty"` Conditions []SessionCondition `json:"conditions,omitempty"` Towards a controller: Golang types pkg/apis/kubecon.io/v1/types.go doc.go: // +k8s:deepcopy-gen=package package v1 More info about code generation in my OpenShift blog post Code Generation for CustomResources
  29. @the_sttts Interface runtime.Object type Object interface { GetObjectKind() schema.ObjectKind DeepCopyObject()

    Object } = everything that has TypeMeta func (p *Pod) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } generated through: // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
  30. @the_sttts // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type Session struct { metav1.TypeMeta

    `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec SessionSpec `json:"spec"` Status SessionStatus `json:"status"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type SessionList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata"` Items []Session `json:"items"` } Towards a controller: Golang types pkg/apis/kubecon.io/v1/types.go doc.go: // +k8s:deepcopy-gen=package package v1
  31. @the_sttts Towards a controller: Golang types pkg/apis/kubecon.io/v1/register.go const GroupName =

    "kubecon.io" var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"} func Kind(kind string) schema.GroupKind { return SchemeGroupVersion.WithKind(kind).GroupKind() } func Resource(resource string) schema.GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() } var SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) var AddToScheme = SchemeBuilder.AddToScheme // Adds the list of known types to Scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &Session{}, &SessionList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil } hint: copy from some other API group
  32. @the_sttts Code Generation $ vendor/k8s.io/code-generator/generate-groups.sh all \ k8s.io/kubecon/pkg/client k8s.io/kubecon/pkg/apis \

    "kubecon.io:v1,v2,v3 someothergroup.io:v1" \ --output-base "$GOPATH/src" \ --go-header-file hack/boilerplate.go.txt Usually put into hack/update-codegen.sh (compare k8s.io/sample-controller). all = deepcopy,defaulter,client,lister,informer doc.go for crazy group and package names: // +groupName=example.test.crd.code-generator.k8s.io // +groupGoName=SecondExample Alternative, wrapping this: kubebuilder
  33. @the_sttts Native CRD Client import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/clientcmd" kubecon

    "k8s.io/kubecon/pkg/client/clientset/versioned" ) kubeconfig = flag.String("kubeconfig", "~/.kube/config", "path to the kubeconfig file") flag.Parse() config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig) clientset, err := kubecon.NewForConfig(config) session, err := clientset.KubeconV1().Sessions("eu2018").Get("api-machinery", metav1.GetOptions{}) Our Session Golang type
  34. @the_sttts References for controllers • k8s.io/sample-controller • Maciej Szulik's talk

    from Thursday • Writing Kubernetes Custom Controllers by Devdatta Kulkarni • the controllers in core: k8s.io/kubernetes/pkg/controller