Skip to content

Commit

Permalink
Add apiextensions v1 CustomResourceDefinition model (#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
tg90nor authored Dec 8, 2021
1 parent aebbb86 commit 83f0e58
Show file tree
Hide file tree
Showing 5 changed files with 546 additions and 20 deletions.
31 changes: 29 additions & 2 deletions k8s/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ def __new__(mcs, cls, bases, attrs):
fields = meta["fields"]
for k, v in list(attrs.items()):
if isinstance(v, Field):
v.name = k
if v.name == "__unset__":
v.name = k
v.attr_name = k
field_names.append(k)
fields.append(v)
Meta = namedtuple("Meta", meta.keys())
Expand Down Expand Up @@ -232,6 +234,9 @@ def __init__(self, new=True, **kwargs):
for field in self._meta.fields:
kwarg_names.discard(field.name)
field.set(self, kwargs)
if field.type == SelfModel:
field.type = self.__class__
field.default_value_create_instance = False
if kwarg_names:
raise TypeError(
"{}() got unexpected keyword-arguments: {}".format(self.__class__.__name__, ", ".join(kwarg_names)))
Expand All @@ -244,7 +249,7 @@ def _validate_fields(self):
raise TypeError("Value of field {} is not valid on {}".format(field.name, self))

def as_dict(self):
if all(getattr(self, field.name) == field.default_value for field in self._meta.fields):
if all(getattr(self, field.attr_name) == field.default_value for field in self._meta.fields):
return None
d = {}
for field in self._meta.fields:
Expand All @@ -254,6 +259,11 @@ def as_dict(self):
return d

def merge(self, other):
"""
`merge` sets each field in `self` to the value provided by `other`
This is mostly equivalent to just replacing `self` with `other`,
except read only fields in `self` are preserved.
"""
for field in self._meta.fields:
setattr(self, field.name, getattr(other, field.name))
update = merge # For backwards compatibility
Expand Down Expand Up @@ -338,3 +348,20 @@ def __init__(self):

def __str__(self):
return ""


class SelfModel:
"""
Use `SelfModel` as `Field.type` to set `Field.type` to the model the
`Field` was defined in during model instantiation.
This allows models to have fields with their own type.
It is not possible to reference a class in its own attributes.
Example:
```
class MyModel(Model):
submodel = Field(SelfModel) # submodel gets the type `MyModel`
```
"""
pass
80 changes: 69 additions & 11 deletions k8s/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
# -*- coding: utf-8

# Copyright 2017-2019 The FIAAS Authors
#
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#
# http://www.apache.org/licenses/LICENSE-2.0
#
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
Expand All @@ -26,14 +26,15 @@
class Field(object):
"""Generic field on a k8s model"""

def __init__(self, type, default_value=None, alt_type=None):
self.type = type
def __init__(self, field_type, default_value=None, alt_type=None, name="__unset__"):
self.type = field_type
self.alt_type = alt_type
self.name = "__unset__"
self.name = name
self._default_value = default_value
self.default_value_create_instance = True

def dump(self, instance):
value = getattr(instance, self.name)
value = getattr(instance, self.attr_name)
return self._as_dict(value)

def load(self, instance, value):
Expand Down Expand Up @@ -72,7 +73,7 @@ def __delete__(self, instance):
@property
def default_value(self):
from .base import Model
if issubclass(self.type, Model) and self._default_value is None:
if issubclass(self.type, Model) and self.default_value_create_instance and self._default_value is None:
return self.type(new=False)
return copy.copy(self._default_value)

Expand Down Expand Up @@ -131,13 +132,13 @@ def __set__(self, instance, value):
class ListField(Field):
"""ListField is a list (array) of a single type on a model"""

def __init__(self, type, default_value=None):
def __init__(self, field_type, default_value=None, name='__unset__'):
if default_value is None:
default_value = []
super(ListField, self).__init__(type, default_value)
super(ListField, self).__init__(field_type, default_value, name=name)

def dump(self, instance):
return [self._as_dict(v) for v in getattr(instance, self.name)]
return [self._as_dict(v) for v in getattr(instance, self.attr_name)]

def load(self, instance, value):
if value is None:
Expand All @@ -151,3 +152,60 @@ class RequiredField(Field):
def is_valid(self, instance):
value = self.__get__(instance)
return value is not None and super(RequiredField, self).is_valid(instance)


class JSONField(Field):
"""
Field with allowed types `bool`, `int`, `float`, `str`, `dict`, `list`
Items of dicts and lists have the same allowed types
"""

def __init__(self, default_value=None, name="__unset__"):
self.type = None
self.alt_type = None
self.allowed_types = [bool, int, float, str, dict, list]
self.name = name
self._default_value = default_value

def load(self, instance, value):
if value is None:
value = self.default_value
self.__set__(instance, value)

def is_valid(self, instance):
value = self.__get__(instance)
if value is None:
return True
try:
return self._check_allowed_types(value)
except TypeError:
return False

def __set__(self, instance, new_value):
if (new_value is None) or self._check_allowed_types(new_value, chain=[type(instance).__name__, self.name]):
instance._values[self.name] = new_value

def _check_allowed_types(self, value, chain=None):
if chain is None:
chain = []
if any(isinstance(value, t) for t in self.allowed_types):
if isinstance(value, dict):
for k, v in value.items():
self._check_allowed_types(k, chain.append(k))
self._check_allowed_types(v, chain.append(k))
if isinstance(value, list):
for v in value:
self._check_allowed_types(v, chain.append("[\"{value}\"]".format(value=v)))
return True
else:
def typename(i):
return i.__name__
raise TypeError("{name} has invalid type {type}. Allowed types are {allowed_types}.".format(
name=".".join(chain),
type=type(value).__name__,
allowed_types=", ".join(map(typename, self.allowed_types))
))

@property
def default_value(self):
return copy.copy(self._default_value)
180 changes: 180 additions & 0 deletions k8s/models/apiextensions_v1_custom_resource_definition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#!/usr/bin/env python
# -*- coding: utf-8

# Copyright 2017-2019 The FIAAS Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import absolute_import

import datetime

import six

from .common import ObjectMeta
from ..base import Model, SelfModel
from ..fields import Field, ListField, JSONField


class ExternalDocumentation(Model):
description = Field(six.text_type)
url = Field(six.text_type)


class JSONSchemaProps(Model):
ref = Field(six.text_type, name='$ref')
schema = Field(six.text_type, name='$schema')
additionalItems = Field(SelfModel, alt_type=bool)
additionalProperties = Field(SelfModel, alt_type=bool)
allOf = ListField(SelfModel)
anyOf = ListField(SelfModel)
default = JSONField()
definitions = Field(dict)
dependencies = Field(dict)
description = Field(six.text_type)
enum = JSONField()
example = JSONField()
exclusiveMaximum = Field(bool)
exclusiveMinimum = Field(bool)
externalDocs = Field(ExternalDocumentation)
format = Field(six.text_type)
id = Field(six.text_type)
items = Field(SelfModel, alt_type=list)
maxItems = Field(int)
maxLength = Field(int)
maxProperties = Field(int)
maximum = Field(int, alt_type=float)
minItems = Field(int)
minLength = Field(int)
minProperties = Field(int)
minimum = Field(int, alt_type=float)
multipleOf = Field(int, alt_type=float)
_not = Field(SelfModel)
nullable = Field(bool)
oneOf = ListField(SelfModel)
pattern = Field(six.text_type)
patternProperties = Field(dict)
properties = Field(dict)
required = ListField(six.text_type)
title = Field(six.text_type)
type = Field(six.text_type)
uniqueItems = Field(bool)
x_kubernetes_embedded_resource = Field(bool, name='x-kubernetes-embedded-resource')
x_kubernetes_int_or_string = Field(bool, name='x-kubernetes-int-or-string')
x_kubernetes_list_map_keys = ListField(six.text_type, name='x-kubernetes-list-map-keys')
x_kubernetes_list_type = Field(six.text_type, name="x-kubernetes-list-type")
x_kubernetes_map_type = Field(six.text_type, name='x-kubernetes-map-type')
x_kubernetes_preserve_unknown_fields = Field(bool, name="x-kubernetes-preserve-unknown-fields")


class ServiceReference(Model):
name = Field(six.text_type)
namespace = Field(six.text_type)
path = Field(six.text_type)
port = Field(int)


class WebhookClientConfig(Model):
caBundle = Field(six.text_type)
service = Field(ServiceReference)
url = Field(six.text_type)


class WebhookConversion(Model):
clientConfig = Field(WebhookClientConfig)
conversionReviewVersions = ListField(six.text_type)


class CustomResourceColumnDefinition(Model):
description = Field(six.text_type)
format = Field(six.text_type)
jsonPath = Field(six.text_type)
name = Field(six.text_type)
priority = Field(int)
type = Field(six.text_type)


class CustomResourceConversion(Model):
strategy = Field(six.text_type)
webhook = Field(WebhookConversion)


class CustomResourceDefinitionNames(Model):
categories = ListField(six.text_type)
kind = Field(six.text_type)
listKind = Field(six.text_type)
plural = Field(six.text_type)
shortNames = ListField(six.text_type)
singular = Field(six.text_type)


class CustomResourceValidation(Model):
openAPIV3Schema = Field(JSONSchemaProps)


class CustomResourceSubresourceScale(Model):
labelSelectorPath = Field(six.text_type)
specReplicasPath = Field(six.text_type)
statusReplicasPath = Field(six.text_type)


class CustomResourceSubresources(Model):
scale = Field(CustomResourceSubresourceScale)
# CustomResourceSubresourceStatus contains no fields,
# so we use the dict type instead
status = Field(dict)


class CustomResourceDefinitionVersion(Model):
additionalPrinterColumns = ListField(CustomResourceColumnDefinition)
deprecated = Field(bool)
deprecationWarning = Field(six.text_type)
name = Field(six.text_type)
schema = Field(CustomResourceValidation)
served = Field(bool)
storage = Field(bool)
subresources = Field(CustomResourceSubresources)


class CustomResourceDefinitionSpec(Model):
conversion = Field(CustomResourceConversion)
group = Field(six.text_type)
names = Field(CustomResourceDefinitionNames)
preserveUnknownFields = Field(bool)
scope = Field(six.text_type)
versions = ListField(CustomResourceDefinitionVersion)


class CustomResourceDefinitionCondition(Model):
lastTransitionTime = Field(datetime.datetime)
message = Field(six.text_type)
reason = Field(six.text_type)
status = Field(six.text_type)
type = Field(six.text_type)


class CustomResourceDefinitionStatus(Model):
acceptedNames = Field(CustomResourceDefinitionNames)
conditions = ListField(CustomResourceDefinitionCondition)
storedVersions = ListField(six.text_type)


class CustomResourceDefinition(Model):
class Meta:
url_template = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/{name}"
list_url = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions"
watch_list_url = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions?watch=true"

metadata = Field(ObjectMeta)
spec = Field(CustomResourceDefinitionSpec)
status = Field(CustomResourceDefinitionStatus)
Loading

0 comments on commit 83f0e58

Please sign in to comment.