Slide 1

Slide 1 text

SIG API Machinery Deep Dive Stefan Schimanski – [email protected] – @the_sttts

Slide 2

Slide 2 text

@the_sttts Agenda • Outlook to Kubernetes 1.11+ • Deep Dive into CustomResourceDefinitions • Questions

Slide 3

Slide 3 text

@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

Slide 4

Slide 4 text

@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

Slide 5

Slide 5 text

@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

Slide 6

Slide 6 text

@the_sttts Deep Dive

Slide 7

Slide 7 text

@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"

Slide 8

Slide 8 text

@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

Slide 9

Slide 9 text

@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

Slide 10

Slide 10 text

@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.

Slide 11

Slide 11 text

@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"

Slide 12

Slide 12 text

@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.

Slide 13

Slide 13 text

@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

Slide 14

Slide 14 text

@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

Slide 15

Slide 15 text

@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

Slide 16

Slide 16 text

@the_sttts Validation • The standard: OpenAPI v3 schema https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaObject • based on JSON Schema: https://tools.ietf.org/html/draft-wright-json-schema-validation-00

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

@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+

Slide 19

Slide 19 text

@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}

Slide 20

Slide 20 text

@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→

Slide 21

Slide 21 text

@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

Slide 22

Slide 22 text

@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".

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

@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

Slide 25

Slide 25 text

@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)

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

@the_sttts Recap

Slide 28

Slide 28 text

@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

Slide 29

Slide 29 text

@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

Slide 30

Slide 30 text

Backup

Slide 31

Slide 31 text

@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"` }

Slide 32

Slide 32 text

@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

Slide 33

Slide 33 text

@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

Slide 34

Slide 34 text

@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

Slide 35

Slide 35 text

@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

Slide 36

Slide 36 text

@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

Slide 37

Slide 37 text

@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

Slide 38

Slide 38 text

@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

Slide 39

Slide 39 text

Image by @cloudark / Devdatta Kulkarni https://medium.com/@cloudark/kubernetes-custom-controllers-b6c7d0668fdf Inner workings of a Controller

Slide 40

Slide 40 text

@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