Using Hikaru with Kubernetes¶
Starting with release 0.4 of Hikaru, you can now use Hikaru objects to interact with
Kubernetes using the underlying Kubernetes Python client. Depending on your use case and
interaction method, it can be possible to not use a single Kubernetes Python call and work
entirely in Hikaru objects. Hikaru also provides a way for the user to explicitly set the
kubernetes.client.ApiClient
instance to use when interacting with Kubernetes.
Hikaru provides two levels of interface to the underlying Kubernetes client:
A higher-level CRUD-style set of instance methods that have consistent names and behaviours across all top-level Hikaru objects (that is, subclasses of HikaruDocumentBase) but which hide some of the details of the underlying operations, and
A lower-level (but not much lower) set of instance and class methods that are direct analogs of the operations defined in the K8s swagger file; the names of these methods follow the operationIDs in the swagger file, and expose return codes, headers, and allow for async operation.
The CRUD operations are simply veneers over the lower-level methods exposed by Hikaru, and should provide the user a simplified API to work with.
Hikaru CRUD methods¶
Hikaru implements a CRUD method when there is a suitable matching lower-level method to
base it on. If no such method exists, then a CRUD method won’t be generated. For example,
the ComponentStatus
object only has a read()
method defined for it.
All Hikaru CRUD methods work on single objects with the exception of objects that are
inherently containers for collections of objects such as PodList
. Outside of these
objects, there is no CRUD support for getting lists of objects– consult the lower-level
API for methods that support fetching lists.
All Hikaru CRUD methods share share the following characteristics:
they are all instance methods (lower-level methods are a mix of instance and static methods),
they all have
self
as their return value,they also all modify
self
in place if the underlying method returns an object of the same type asself
; the return’s values are merged into self using themerge()
method ofHikaruBase
,they can only be run in blocking mode,
they support the other optional parameters of the underlying call,
they duplicate the docstring of the underlying low-level call so you review the doc for each method.
Each of the methods are discussed below.
create()¶
The create()
method provides a way to create a resource in Kubernetes. This is an independent
action from creating the Hikaru object; you must first create the Hikaru object, and then call
create()
on it to create it within Kubernetes.
Typically, you create the Hikaru object either using the Python model objects or by creating the
object by loading YAML, JSON, or a Python dict. You may tailor the object at any point, and then
instruct Kubernetes to create it with create()
. For example, if you had a Pod
Hikaru
object, you can ask Kubernetes to create it with:
pod.create()
For namespaced resources, you can leave the namespace out of the object’s metadata
(ObjectMeta) and instead supply it as a keyword argument to create()
:
pod.create(namespace='some-name')
Bear in mind that the same restrictions apply as in the underlying Kubernetes client, namely that if you supply a namespace as an argument, the metadata for the object either must not have a namespace value or else it must have the same value as the argument.
If Kubernetes returns the same type of resource as was used in the create()
call, then the
values of those fields are merged into the original object. Note that this frequently isn’t the
final state– trying to modify the object now and then issuing an update()
will often result
in an error as the returned value from create()
may not be referring to the most recent state
of the object. Typically you’ll need to call read()
first.
read()¶
The read()
method provides you a way to acquire the details of an existing resource.
You typically have a couple of ways to read: either by supplying an object that contains a
name and/or namespace (as appropriate), or by supplying these as keyword arguments. So these
two calls to read the details of a Pod are equivalent:
p = Pod(metadata=ObjectMeta(name='theName', namespace='the-namespace')).read()
# and:
p = Pod().read(name='theName', namespace='the-namespace')
# or, for a Pod that already has the name/namespace in its metadata:
p.read()
Or, you could have the name in the Pod and supply the namespace in the read line, as long as the namespace in the Pod is None or the same value as provided in the arguments.
As mentioned above in the section on create()
, you generally are advised to invoke read prior
to any update operations to ensure you are only trying to make changes on the latest version of
the object.
update()¶
Calls to update()
behave like calls to create()
, although you generally
don’t need to specify a namespace
parameter since you are usually updating with an object
in which the namespace was previously specified. However, you can supply the value if needed using
the namespace
keyword argument to update()
:
pod.update(namespace='whatever')
patch vs replace¶
The Kubernetes spec identifies two different operations that could be thought of as implementing
update semantics, patch and replace. Since replace is meant to fully replace an
existing resource with another one, it was decided that the update()
method would be a
wrapper around the the patch operation, since patching an existing resource more closely
matches the semantics of update()
. You can still access the replace method for the resource
by using the lower-level API.
update() and Context Managers¶
Any HikaruDocumentBase
subclass that has an update()
method is also a context manager.
When the with
block that the object manages closes, the object automatically calls the
update()
method on the object. So constructions like the following can be created:
with Pod().read(name='thename', namespace='the-namespace') as p:
p.labels['new-label'] = 'value'
# and other actions that change the content of the Pod p
# once here, the Pod p has automatically invoked update()
The instance that serves as the context manager can come from any usual source. So if a previously created Pod was stored as YAML, you can load it and use that to manage the context:
p = load_full_yaml(path="/some/path")[0]
with p.read() as pod: # always read before update to make sure you have the latest rev!
# and carry on modifying pod here...
There is also a helper function, rollback_cm(), which sets up
the context manager to roll
back to the original state of the object if an exception is raised inside the with
block.
This allows you to restore your object to the original condition from when the with block
started in the case of an error. Applying this function to the example from above, we’d then
have:
p = load_full_yaml(path="/some/path")[0]
try:
with rollback_cm(p.read()) as pod:
# and carry on modifying pod here...
except:
# pod (p) will have the same content as at the start of the with block
delete()¶
The delete()
method allows you to delete the modelled resource in Kubernetes. This does
not delete the Hikaru object; it simply gets rid of the underlying Kubernetes resource.
Unlike update()
, delete()
doesn’t need the latest version of the object to perform
its actions; in general, all is necessary are the name and namespace (if applicable) for the
resource in question. That allows issuing a delete()
from an anonymous object:
Pod().delete(name='podname', namespace='podnamespace')
…as well as deleting from a resource that has metadata with both name and namespace filled in:
# let's assume we previously persisted a Pod that we had created with its name
# and namespace we can then load and delete it
p = load_full_yaml(path='/path/to/saved/pod')[0]
p.delete()
…or the unhelpfully verbose:
p = Pod(metadata=ObjectMeta(name='podname', namespace='podnamespace'))
p.delete()
Hikaru low-level methods¶
The lower-level Hikaru methods are all direct analogs of the operations defined in the
Kubernetes swagger API specification file. The names of the methods are taken from the
operationID
property of each operation in that file, although in some cases version
information has been scrubbed out of the name. Each method supports all of the parameters
documented in that file, including the flag to indicate asynchronous operation.
All methods return a Response object. These objects contain references to the returned result code, HTTP headers, and any object returned by Kubernetes (as a Hikaru object).
If you requested an operation to be done asynchronously using the async_req=True
argument,
then the above three attributes aren’t filled out when the method returns and instead the
Response can be used
to sync with the arrival of the response data with a calling thread. Using the get()
method call on the
Response object, you can block the caller (with optional timeout) until Kubernetes
responds to your request. When get() returns, the code, object, and header fields will be
filled out in the Response object. The get()
call also returns a three-tuple
containing this same data.
To illustrate this, we’ll start with a fully explicit version with commented interaction and then show how you can pare it down based on defaults. In this example, we’ll create and delete a Pod using the K3s lightweight Kubernetes package.
import time
from hikaru import load_full_yaml, Response
from hikaru.model import Pod
# here are the two bits we need from K8s
from kubernetes import config
from kubernetes.client import ApiClient
def do_it():
# configure the Kubernetes client library by telling it where
# to find the K3s configuration file
config.load_kube_config(config_file="/etc/rancher/k3s/k3s.yaml")
# create a client
client = ApiClient()
# load a Pod from YAML
f = open('pod.yaml', 'r')
pod: Pod = load_full_yaml(stream=f)[0]
# inform the Pod object about the client
pod.set_client(client)
print("creating")
# use the createNamespacedPod() instance method to create the pod
# and get the full Pod definition back in the response
result: Response = pod.createNamespacedPod(namespace='default')
newpod: Pod = result.obj
time.sleep(5) # smoke 'em if ya got 'em...
print("deleting")
# use the static method deleteNamespacedPod() to delete the
# previously created Pod, passing the API client object into
# the call
fres: Response = Pod.deleteNamespacedPod(newpod.metadata.name, 'default',
client=client)
return fres
if __name__ == "__main__":
do_it()
Notice that for instances of HikaruDocumentBase
subclasses we can set_client()
on the instance or pass the client in as a keyword parameter. For static methods on
a subclass itself you must pass the client in (if you don’t use a default client).
Using a default client allows you to shorten the above. Once you’ve told the Kubernetes library where the configuration file is, you no longer need to explicitly make client objects– if an object is needed but not supplied, one is created for you by the underlying system. That reduces the above to:
import time
from hikaru import load_full_yaml, Response
from hikaru.model import Pod
from kubernetes import config
def do_it():
config.load_kube_config(config_file="/etc/rancher/k3s/k3s.yaml")
f = open('pod.yaml', 'r')
pod: Pod = load_full_yaml(stream=f)[0]
print("creating")
result: Response = pod.createNamespacedPod(namespace='default')
newpod: Pod = result.obj
time.sleep(5)
print("deleting")
fres: Response = Pod.deleteNamespacedPod(newpod.metadata.name, 'default')
return fres
if __name__ == "__main__":
do_it()
All we need to is load the configuration file and the underlying Kubernetes system will handle making clients.