Implement a Kubernetes driver
This changes implements a Kubernetes resource provider. The driver supports namespace request and pod request to enable both containers as machine and native containers workflow. Depends-On: https://review.openstack.org/605823 Change-Id: I81b5dc5abe92b71cc98b0d71c8a2863cddff6027
This commit is contained in:
parent
674d6d88b4
commit
4295ff6870
12
.zuul.yaml
12
.zuul.yaml
@ -119,6 +119,17 @@
|
|||||||
devstack_localrc:
|
devstack_localrc:
|
||||||
NODEPOOL_PAUSE_DEBIAN_STRETCH_DIB: false
|
NODEPOOL_PAUSE_DEBIAN_STRETCH_DIB: false
|
||||||
|
|
||||||
|
- job:
|
||||||
|
description: |
|
||||||
|
Test that nodepool works with kubernetes.
|
||||||
|
name: nodepool-functional-k8s
|
||||||
|
pre-run: playbooks/nodepool-functional-k8s/pre.yaml
|
||||||
|
run: playbooks/nodepool-functional-k8s/run.yaml
|
||||||
|
post-run: playbooks/nodepool-functional-k8s/post.yaml
|
||||||
|
nodeset: ubuntu-xenial
|
||||||
|
required-projects:
|
||||||
|
- openstack-infra/nodepool
|
||||||
|
|
||||||
- project:
|
- project:
|
||||||
check:
|
check:
|
||||||
jobs:
|
jobs:
|
||||||
@ -133,6 +144,7 @@
|
|||||||
voting: false
|
voting: false
|
||||||
- nodepool-functional-py35-src:
|
- nodepool-functional-py35-src:
|
||||||
voting: false
|
voting: false
|
||||||
|
- nodepool-functional-k8s
|
||||||
- pbrx-build-container-images:
|
- pbrx-build-container-images:
|
||||||
vars:
|
vars:
|
||||||
pbrx_prefix: zuul
|
pbrx_prefix: zuul
|
||||||
|
@ -1004,3 +1004,92 @@ Selecting the static driver adds the following options to the
|
|||||||
:default: 1
|
:default: 1
|
||||||
|
|
||||||
The number of jobs that can run in parallel on this node.
|
The number of jobs that can run in parallel on this node.
|
||||||
|
|
||||||
|
|
||||||
|
Kubernetes driver
|
||||||
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
A Kubernetes provider's resources are partitioned into groups called "pool"
|
||||||
|
(see :ref:`k8s_pools` for details), and within a pool, the node types which
|
||||||
|
are to be made available are listed (see :ref:`k8s_labels` for details).
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
providers:
|
||||||
|
- name: kubespray
|
||||||
|
driver: kubernetes
|
||||||
|
context: admin-cluster.local
|
||||||
|
pools:
|
||||||
|
- name: main
|
||||||
|
labels:
|
||||||
|
- name: kubernetes-namespace
|
||||||
|
type: namespace
|
||||||
|
- name: pod-fedora
|
||||||
|
type: pod
|
||||||
|
image: docker.io/fedora:28
|
||||||
|
|
||||||
|
**required**
|
||||||
|
|
||||||
|
``context``
|
||||||
|
Name of the context configured in ``kube/config``.
|
||||||
|
|
||||||
|
Before using the driver, Nodepool services need a ``kube/config`` file
|
||||||
|
manually installed with cluster admin context.
|
||||||
|
|
||||||
|
**optional**
|
||||||
|
|
||||||
|
``launch-retries``
|
||||||
|
|
||||||
|
The number of times to retry launching a node before considering the job
|
||||||
|
failed.
|
||||||
|
|
||||||
|
Default 3.
|
||||||
|
|
||||||
|
|
||||||
|
.. _k8s_pools:
|
||||||
|
|
||||||
|
Kubernetes pools
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
A pool defines a group of resources from a Kubernetes provider. Each pool has a
|
||||||
|
maximum number of namespace which can be created (Not Implemented yet).
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
pools:
|
||||||
|
- name: main
|
||||||
|
labels: []
|
||||||
|
|
||||||
|
|
||||||
|
**required**
|
||||||
|
|
||||||
|
``name``
|
||||||
|
Namespace name are prefixed with the pool's name.
|
||||||
|
|
||||||
|
|
||||||
|
.. _k8s_labels:
|
||||||
|
|
||||||
|
Kubernetes labels
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Each entry in a pool`s `labels` section indicates that the
|
||||||
|
corresponding label is available for use in this pool.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
labels:
|
||||||
|
- name: kubernetes-namespace
|
||||||
|
type: namespace
|
||||||
|
- name: pod-fedora
|
||||||
|
type: pod
|
||||||
|
image: docker.io/fedora:28
|
||||||
|
|
||||||
|
|
||||||
|
Kubernetes provider support two types of labels:
|
||||||
|
|
||||||
|
Namespace labels provide an empty namespace configured with a service account
|
||||||
|
that can creates pods, services, configmaps, ...
|
||||||
|
|
||||||
|
Pod labels provide a dedicated namespace with a single pod created using the
|
||||||
|
``image`` parameter and it is configured with a service account that can
|
||||||
|
exec and get the logs of the pod.
|
||||||
|
34
nodepool/driver/kubernetes/__init__.py
Normal file
34
nodepool/driver/kubernetes/__init__.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Copyright 2018 Red Hat
|
||||||
|
#
|
||||||
|
# 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 nodepool.driver import Driver
|
||||||
|
from nodepool.driver.kubernetes.config import KubernetesProviderConfig
|
||||||
|
from nodepool.driver.kubernetes.provider import KubernetesProvider
|
||||||
|
from openshift import config
|
||||||
|
|
||||||
|
|
||||||
|
class KubernetesDriver(Driver):
|
||||||
|
def reset(self):
|
||||||
|
try:
|
||||||
|
config.load_kube_config(persist_config=True)
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def getProviderConfig(self, provider):
|
||||||
|
return KubernetesProviderConfig(self, provider)
|
||||||
|
|
||||||
|
def getProvider(self, provider_config, use_taskmanager):
|
||||||
|
return KubernetesProvider(provider_config, use_taskmanager)
|
114
nodepool/driver/kubernetes/config.py
Normal file
114
nodepool/driver/kubernetes/config.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
# Copyright 2018 Red Hat
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import voluptuous as v
|
||||||
|
|
||||||
|
from nodepool.driver import ConfigPool
|
||||||
|
from nodepool.driver import ConfigValue
|
||||||
|
from nodepool.driver import ProviderConfig
|
||||||
|
|
||||||
|
|
||||||
|
class KubernetesLabel(ConfigValue):
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, KubernetesLabel):
|
||||||
|
return (other.name == self.name and
|
||||||
|
other.type == self.type and
|
||||||
|
other.image_pull == self.image_pull and
|
||||||
|
other.image == self.image)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<KubternetesLabel %s>" % self.name
|
||||||
|
|
||||||
|
|
||||||
|
class KubernetesPool(ConfigPool):
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, KubernetesPool):
|
||||||
|
return (super().__eq__(other) and
|
||||||
|
other.name == self.name and
|
||||||
|
other.labels == self.labels)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<KubernetesPool %s>" % self.name
|
||||||
|
|
||||||
|
|
||||||
|
class KubernetesProviderConfig(ProviderConfig):
|
||||||
|
def __init__(self, driver, provider):
|
||||||
|
self.driver_object = driver
|
||||||
|
self.__pools = {}
|
||||||
|
super().__init__(provider)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, KubernetesProviderConfig):
|
||||||
|
return (super().__eq__(other) and
|
||||||
|
other.context == self.context and
|
||||||
|
other.pools == self.pools)
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pools(self):
|
||||||
|
return self.__pools
|
||||||
|
|
||||||
|
@property
|
||||||
|
def manage_images(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def load(self, config):
|
||||||
|
self.launch_retries = int(self.provider.get('launch-retries', 3))
|
||||||
|
self.context = self.provider['context']
|
||||||
|
for pool in self.provider.get('pools', []):
|
||||||
|
pp = KubernetesPool()
|
||||||
|
pp.name = pool['name']
|
||||||
|
pp.provider = self
|
||||||
|
self.pools[pp.name] = pp
|
||||||
|
pp.labels = {}
|
||||||
|
for label in pool.get('labels', []):
|
||||||
|
pl = KubernetesLabel()
|
||||||
|
pl.name = label['name']
|
||||||
|
pl.type = label['type']
|
||||||
|
pl.image = label.get('image')
|
||||||
|
pl.image_pull = label.get('image-pull', 'IfNotPresent')
|
||||||
|
pl.pool = pp
|
||||||
|
pp.labels[pl.name] = pl
|
||||||
|
config.labels[label['name']].pools.append(pp)
|
||||||
|
|
||||||
|
def getSchema(self):
|
||||||
|
k8s_label = {
|
||||||
|
v.Required('name'): str,
|
||||||
|
v.Required('type'): str,
|
||||||
|
'image': str,
|
||||||
|
'image-pull': str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pool = {
|
||||||
|
v.Required('name'): str,
|
||||||
|
v.Required('labels'): [k8s_label],
|
||||||
|
}
|
||||||
|
|
||||||
|
provider = {
|
||||||
|
v.Required('pools'): [pool],
|
||||||
|
v.Required('context'): str,
|
||||||
|
'launch-retries': int,
|
||||||
|
}
|
||||||
|
return v.Schema(provider)
|
||||||
|
|
||||||
|
def getSupportedLabels(self, pool_name=None):
|
||||||
|
labels = set()
|
||||||
|
for pool in self.pools.values():
|
||||||
|
if not pool_name or (pool.name == pool_name):
|
||||||
|
labels.update(pool.labels.keys())
|
||||||
|
return labels
|
126
nodepool/driver/kubernetes/handler.py
Normal file
126
nodepool/driver/kubernetes/handler.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
# Copyright 2018 Red Hat
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from kazoo import exceptions as kze
|
||||||
|
|
||||||
|
from nodepool import zk
|
||||||
|
from nodepool.driver.utils import NodeLauncher
|
||||||
|
from nodepool.driver import NodeRequestHandler
|
||||||
|
|
||||||
|
|
||||||
|
class K8SLauncher(NodeLauncher):
|
||||||
|
def __init__(self, handler, node, provider_config, provider_label):
|
||||||
|
super().__init__(handler.zk, node, provider_config)
|
||||||
|
self.handler = handler
|
||||||
|
self.zk = handler.zk
|
||||||
|
self.label = provider_label
|
||||||
|
self._retries = provider_config.launch_retries
|
||||||
|
|
||||||
|
def _launchLabel(self):
|
||||||
|
self.log.debug("Creating resource")
|
||||||
|
if self.label.type == "namespace":
|
||||||
|
resource = self.handler.manager.createNamespace(
|
||||||
|
self.node, self.handler.pool.name)
|
||||||
|
else:
|
||||||
|
resource = self.handler.manager.createPod(
|
||||||
|
self.node, self.handler.pool.name, self.label)
|
||||||
|
|
||||||
|
self.node.state = zk.READY
|
||||||
|
# NOTE: resource access token may be encrypted here
|
||||||
|
self.node.connection_port = resource
|
||||||
|
if self.label.type == "namespace":
|
||||||
|
self.node.connection_type = "namespace"
|
||||||
|
else:
|
||||||
|
self.node.connection_type = "kubectl"
|
||||||
|
self.node.interface_ip = resource['pod']
|
||||||
|
self.zk.storeNode(self.node)
|
||||||
|
self.log.info("Resource %s is ready" % resource['name'])
|
||||||
|
|
||||||
|
def launch(self):
|
||||||
|
attempts = 1
|
||||||
|
while attempts <= self._retries:
|
||||||
|
try:
|
||||||
|
self._launchLabel()
|
||||||
|
break
|
||||||
|
except kze.SessionExpiredError:
|
||||||
|
# If we lost our ZooKeeper session, we've lost our node lock
|
||||||
|
# so there's no need to continue.
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
if attempts <= self._retries:
|
||||||
|
self.log.exception(
|
||||||
|
"Launch attempt %d/%d failed for node %s:",
|
||||||
|
attempts, self._retries, self.node.id)
|
||||||
|
# If we created an instance, delete it.
|
||||||
|
if self.node.external_id:
|
||||||
|
self.handler.manager.cleanupNode(self.node.external_id)
|
||||||
|
self.handler.manager.waitForNodeCleanup(
|
||||||
|
self.node.external_id)
|
||||||
|
self.node.external_id = None
|
||||||
|
self.node.interface_ip = None
|
||||||
|
self.zk.storeNode(self.node)
|
||||||
|
if attempts == self._retries:
|
||||||
|
raise
|
||||||
|
attempts += 1
|
||||||
|
|
||||||
|
|
||||||
|
class KubernetesNodeRequestHandler(NodeRequestHandler):
|
||||||
|
log = logging.getLogger("nodepool.driver.kubernetes."
|
||||||
|
"KubernetesNodeRequestHandler")
|
||||||
|
|
||||||
|
def __init__(self, pw, request):
|
||||||
|
super().__init__(pw, request)
|
||||||
|
self._threads = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def alive_thread_count(self):
|
||||||
|
count = 0
|
||||||
|
for t in self._threads:
|
||||||
|
if t.isAlive():
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
def imagesAvailable(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def launchesComplete(self):
|
||||||
|
'''
|
||||||
|
Check if all launch requests have completed.
|
||||||
|
|
||||||
|
When all of the Node objects have reached a final state (READY or
|
||||||
|
FAILED), we'll know all threads have finished the launch process.
|
||||||
|
'''
|
||||||
|
if not self._threads:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Give the NodeLaunch threads time to finish.
|
||||||
|
if self.alive_thread_count:
|
||||||
|
return False
|
||||||
|
|
||||||
|
node_states = [node.state for node in self.nodeset]
|
||||||
|
|
||||||
|
# NOTE: It very important that NodeLauncher always sets one of
|
||||||
|
# these states, no matter what.
|
||||||
|
if not all(s in (zk.READY, zk.FAILED) for s in node_states):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def launch(self, node):
|
||||||
|
label = self.pool.labels[node.type[0]]
|
||||||
|
thd = K8SLauncher(self, node, self.provider, label)
|
||||||
|
thd.start()
|
||||||
|
self._threads.append(thd)
|
289
nodepool/driver/kubernetes/provider.py
Normal file
289
nodepool/driver/kubernetes/provider.py
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
# Copyright 2018 Red Hat
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import urllib3
|
||||||
|
import time
|
||||||
|
|
||||||
|
from kubernetes.config import config_exception as kce
|
||||||
|
from kubernetes import client as k8s_client
|
||||||
|
from openshift import config
|
||||||
|
|
||||||
|
from nodepool import exceptions
|
||||||
|
from nodepool.driver import Provider
|
||||||
|
from nodepool.driver.kubernetes import handler
|
||||||
|
|
||||||
|
urllib3.disable_warnings()
|
||||||
|
|
||||||
|
|
||||||
|
class KubernetesProvider(Provider):
|
||||||
|
log = logging.getLogger("nodepool.driver.kubernetes.KubernetesProvider")
|
||||||
|
|
||||||
|
def __init__(self, provider, *args):
|
||||||
|
self.provider = provider
|
||||||
|
self.ready = False
|
||||||
|
try:
|
||||||
|
self.k8s_client, self.rbac_client = self._get_client(
|
||||||
|
provider.context)
|
||||||
|
except kce.ConfigException:
|
||||||
|
self.log.exception("Couldn't load client from config")
|
||||||
|
self.log.info("Get context list using this command: "
|
||||||
|
"python3 -c \"from openshift import config; "
|
||||||
|
"print('\\n'.join([i['name'] for i in "
|
||||||
|
"config.list_kube_config_contexts()[0]]))\"")
|
||||||
|
self.k8s_client = None
|
||||||
|
self.rbac_client = None
|
||||||
|
self.namespace_names = set()
|
||||||
|
for pool in provider.pools.values():
|
||||||
|
self.namespace_names.add(pool.name)
|
||||||
|
|
||||||
|
def _get_client(self, context):
|
||||||
|
conf = config.new_client_from_config(context=context)
|
||||||
|
return (
|
||||||
|
k8s_client.CoreV1Api(conf),
|
||||||
|
k8s_client.RbacAuthorizationV1beta1Api(conf))
|
||||||
|
|
||||||
|
def start(self, zk_conn):
|
||||||
|
self.log.debug("Starting")
|
||||||
|
if self.ready or not self.k8s_client or not self.rbac_client:
|
||||||
|
return
|
||||||
|
self.ready = True
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.log.debug("Stopping")
|
||||||
|
self.ready = False
|
||||||
|
|
||||||
|
def listNodes(self):
|
||||||
|
servers = []
|
||||||
|
|
||||||
|
class FakeServer:
|
||||||
|
def __init__(self, namespace, provider, valid_names):
|
||||||
|
self.id = namespace.metadata.name
|
||||||
|
self.name = namespace.metadata.name
|
||||||
|
self.metadata = {}
|
||||||
|
|
||||||
|
if [True for valid_name in valid_names
|
||||||
|
if namespace.metadata.name.startswith("%s-" % valid_name)]:
|
||||||
|
node_id = namespace.metadata.name.split('-')[-1]
|
||||||
|
try:
|
||||||
|
# Make sure last component of name is an id
|
||||||
|
int(node_id)
|
||||||
|
self.metadata['nodepool_provider_name'] = provider
|
||||||
|
self.metadata['nodepool_node_id'] = node_id
|
||||||
|
except Exception:
|
||||||
|
# Probably not a managed namespace, let's skip metadata
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get(self, name, default=None):
|
||||||
|
return getattr(self, name, default)
|
||||||
|
|
||||||
|
if self.ready:
|
||||||
|
for namespace in self.k8s_client.list_namespace().items:
|
||||||
|
servers.append(FakeServer(
|
||||||
|
namespace, self.provider.name, self.namespace_names))
|
||||||
|
return servers
|
||||||
|
|
||||||
|
def labelReady(self, name):
|
||||||
|
# Labels are always ready
|
||||||
|
return True
|
||||||
|
|
||||||
|
def join(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cleanupLeakedResources(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def cleanupNode(self, server_id):
|
||||||
|
if not self.ready:
|
||||||
|
return
|
||||||
|
self.log.debug("%s: removing namespace" % server_id)
|
||||||
|
delete_body = {
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "DeleteOptions",
|
||||||
|
"propagationPolicy": "Background"
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
self.k8s_client.delete_namespace(server_id, delete_body)
|
||||||
|
self.log.info("%s: namespace removed" % server_id)
|
||||||
|
except Exception:
|
||||||
|
# TODO: implement better exception handling
|
||||||
|
self.log.exception("Couldn't remove namespace %s" % server_id)
|
||||||
|
|
||||||
|
def waitForNodeCleanup(self, server_id):
|
||||||
|
for retry in range(300):
|
||||||
|
try:
|
||||||
|
self.k8s_client.read_namespace(server_id)
|
||||||
|
except Exception:
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def createNamespace(self, node, pool, restricted_access=False):
|
||||||
|
name = node.id
|
||||||
|
namespace = "%s-%s" % (pool, name)
|
||||||
|
user = "zuul-worker"
|
||||||
|
|
||||||
|
self.log.debug("%s: creating namespace" % namespace)
|
||||||
|
# Create the namespace
|
||||||
|
ns_body = {
|
||||||
|
'apiVersion': 'v1',
|
||||||
|
'kind': 'Namespace',
|
||||||
|
'metadata': {
|
||||||
|
'name': namespace,
|
||||||
|
'nodepool_node_id': name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proj = self.k8s_client.create_namespace(ns_body)
|
||||||
|
node.external_id = namespace
|
||||||
|
|
||||||
|
# Create the service account
|
||||||
|
sa_body = {
|
||||||
|
'apiVersion': 'v1',
|
||||||
|
'kind': 'ServiceAccount',
|
||||||
|
'metadata': {'name': user}
|
||||||
|
}
|
||||||
|
self.k8s_client.create_namespaced_service_account(namespace, sa_body)
|
||||||
|
|
||||||
|
# Wait for the token to be created
|
||||||
|
for retry in range(30):
|
||||||
|
sa = self.k8s_client.read_namespaced_service_account(
|
||||||
|
user, namespace)
|
||||||
|
ca_crt = None
|
||||||
|
token = None
|
||||||
|
if sa.secrets:
|
||||||
|
for secret_obj in sa.secrets:
|
||||||
|
secret = self.k8s_client.read_namespaced_secret(
|
||||||
|
secret_obj.name, namespace)
|
||||||
|
ca_crt = secret.data.get('ca.crt')
|
||||||
|
token = secret.data.get('token')
|
||||||
|
if token and ca_crt:
|
||||||
|
break
|
||||||
|
if token and ca_crt:
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
if not token or not ca_crt:
|
||||||
|
raise exceptions.LaunchNodepoolException(
|
||||||
|
"%s: couldn't find token for service account %s" %
|
||||||
|
(namespace, sa))
|
||||||
|
|
||||||
|
# Create service account role
|
||||||
|
all_verbs = ["create", "delete", "get", "list", "patch",
|
||||||
|
"update", "watch"]
|
||||||
|
if restricted_access:
|
||||||
|
role_name = "zuul-restricted"
|
||||||
|
role_body = {
|
||||||
|
'kind': 'Role',
|
||||||
|
'apiVersion': 'rbac.authorization.k8s.io/v1beta1',
|
||||||
|
'metadata': {
|
||||||
|
'name': role_name,
|
||||||
|
},
|
||||||
|
'rules': [{
|
||||||
|
'apiGroups': [""],
|
||||||
|
'resources': ["pods"],
|
||||||
|
'verbs': ["get", "list"],
|
||||||
|
}, {
|
||||||
|
'apiGroups': [""],
|
||||||
|
'resources': ["pods/exec"],
|
||||||
|
'verbs': all_verbs
|
||||||
|
}, {
|
||||||
|
'apiGroups': [""],
|
||||||
|
'resources': ["pods/logs"],
|
||||||
|
'verbs': all_verbs
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
role_name = "zuul"
|
||||||
|
role_body = {
|
||||||
|
'kind': 'Role',
|
||||||
|
'apiVersion': 'rbac.authorization.k8s.io/v1beta1',
|
||||||
|
'metadata': {
|
||||||
|
'name': role_name,
|
||||||
|
},
|
||||||
|
'rules': [{
|
||||||
|
'apiGroups': [""],
|
||||||
|
'resources': ["pods", "pods/exec", "pods/log",
|
||||||
|
"services", "endpoints", "crontabs", "jobs",
|
||||||
|
"deployments", "replicasets",
|
||||||
|
"configmaps", "secrets"],
|
||||||
|
'verbs': all_verbs,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
self.rbac_client.create_namespaced_role(namespace, role_body)
|
||||||
|
|
||||||
|
# Give service account admin access
|
||||||
|
role_binding_body = {
|
||||||
|
'apiVersion': 'rbac.authorization.k8s.io/v1beta1',
|
||||||
|
'kind': 'RoleBinding',
|
||||||
|
'metadata': {'name': 'zuul-role'},
|
||||||
|
'roleRef': {
|
||||||
|
'apiGroup': 'rbac.authorization.k8s.io',
|
||||||
|
'kind': 'Role',
|
||||||
|
'name': role_name,
|
||||||
|
},
|
||||||
|
'subjects': [{
|
||||||
|
'kind': 'ServiceAccount',
|
||||||
|
'name': user,
|
||||||
|
'namespace': namespace,
|
||||||
|
}],
|
||||||
|
'userNames': ['system:serviceaccount:%s:zuul-worker' % namespace]
|
||||||
|
}
|
||||||
|
self.rbac_client.create_namespaced_role_binding(
|
||||||
|
namespace, role_binding_body)
|
||||||
|
|
||||||
|
resource = {
|
||||||
|
'name': proj.metadata.name,
|
||||||
|
'namespace': namespace,
|
||||||
|
'host': self.k8s_client.api_client.configuration.host,
|
||||||
|
'skiptls': not self.k8s_client.api_client.configuration.verify_ssl,
|
||||||
|
'token': token,
|
||||||
|
'ca_crt': ca_crt,
|
||||||
|
'user': user,
|
||||||
|
}
|
||||||
|
self.log.info("%s: namespace created" % namespace)
|
||||||
|
return resource
|
||||||
|
|
||||||
|
def createPod(self, node, pool, label):
|
||||||
|
resource = self.createNamespace(node, pool, restricted_access=True)
|
||||||
|
namespace = resource['namespace']
|
||||||
|
pod_body = {
|
||||||
|
'apiVersion': 'v1',
|
||||||
|
'kind': 'Pod',
|
||||||
|
'metadata': {'name': label.name},
|
||||||
|
'spec': {
|
||||||
|
'containers': [{
|
||||||
|
'name': label.name,
|
||||||
|
'image': label.image,
|
||||||
|
'imagePullPolicy': label.image_pull,
|
||||||
|
'command': ["/bin/bash", "-c", "--"],
|
||||||
|
'args': ["while true; do sleep 30; done;"],
|
||||||
|
'workingDir': '/tmp'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
'restartPolicy': 'Never',
|
||||||
|
}
|
||||||
|
self.k8s_client.create_namespaced_pod(namespace, pod_body)
|
||||||
|
for retry in range(300):
|
||||||
|
pod = self.k8s_client.read_namespaced_pod(label.name, namespace)
|
||||||
|
if pod.status.phase == "Running":
|
||||||
|
break
|
||||||
|
self.log.debug("%s: pod status is %s", namespace, pod.status.phase)
|
||||||
|
time.sleep(1)
|
||||||
|
if retry == 299:
|
||||||
|
raise exceptions.LaunchNodepoolException(
|
||||||
|
"%s: pod failed to initialize (%s)" % (
|
||||||
|
namespace, pod.status.phase))
|
||||||
|
resource["pod"] = label.name
|
||||||
|
return resource
|
||||||
|
|
||||||
|
def getRequestHandler(self, poolworker, request):
|
||||||
|
return handler.KubernetesNodeRequestHandler(poolworker, request)
|
@ -19,6 +19,8 @@ labels:
|
|||||||
- name: trusty-external
|
- name: trusty-external
|
||||||
min-ready: 1
|
min-ready: 1
|
||||||
- name: trusty-static
|
- name: trusty-static
|
||||||
|
- name: kubernetes-namespace
|
||||||
|
- name: pod-fedora
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
- name: cloud1
|
- name: cloud1
|
||||||
@ -99,6 +101,18 @@ providers:
|
|||||||
username: zuul
|
username: zuul
|
||||||
max-parallel-jobs: 1
|
max-parallel-jobs: 1
|
||||||
|
|
||||||
|
- name: kubespray
|
||||||
|
driver: kubernetes
|
||||||
|
context: admin-cluster.local
|
||||||
|
pools:
|
||||||
|
- name: main
|
||||||
|
labels:
|
||||||
|
- name: kubernetes-namespace
|
||||||
|
type: namespace
|
||||||
|
- name: pod-fedora
|
||||||
|
type: pod
|
||||||
|
image: docker.io/fedora:28
|
||||||
|
|
||||||
diskimages:
|
diskimages:
|
||||||
- name: trusty
|
- name: trusty
|
||||||
formats:
|
formats:
|
||||||
|
21
nodepool/tests/fixtures/kubernetes.yaml
vendored
Normal file
21
nodepool/tests/fixtures/kubernetes.yaml
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
zookeeper-servers:
|
||||||
|
- host: {zookeeper_host}
|
||||||
|
port: {zookeeper_port}
|
||||||
|
chroot: {zookeeper_chroot}
|
||||||
|
|
||||||
|
labels:
|
||||||
|
- name: pod-fedora
|
||||||
|
- name: kubernetes-namespace
|
||||||
|
|
||||||
|
providers:
|
||||||
|
- name: kubespray
|
||||||
|
driver: kubernetes
|
||||||
|
context: admin-cluster.local
|
||||||
|
pools:
|
||||||
|
- name: main
|
||||||
|
labels:
|
||||||
|
- name: kubernetes-namespace
|
||||||
|
type: namespace
|
||||||
|
- name: pod-fedora
|
||||||
|
type: pod
|
||||||
|
image: docker.io/fedora:28
|
155
nodepool/tests/test_driver_kubernetes.py
Normal file
155
nodepool/tests/test_driver_kubernetes.py
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
# Copyright (C) 2018 Red Hat
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
import fixtures
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from nodepool import tests
|
||||||
|
from nodepool import zk
|
||||||
|
from nodepool.driver.kubernetes import provider
|
||||||
|
|
||||||
|
|
||||||
|
class FakeCoreClient(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.namespaces = []
|
||||||
|
|
||||||
|
class FakeApi:
|
||||||
|
class configuration:
|
||||||
|
host = "http://localhost:8080"
|
||||||
|
verify_ssl = False
|
||||||
|
self.api_client = FakeApi()
|
||||||
|
|
||||||
|
def list_namespace(self):
|
||||||
|
class FakeNamespaces:
|
||||||
|
items = self.namespaces
|
||||||
|
return FakeNamespaces
|
||||||
|
|
||||||
|
def create_namespace(self, ns_body):
|
||||||
|
class FakeNamespace:
|
||||||
|
class metadata:
|
||||||
|
name = ns_body['metadata']['name']
|
||||||
|
self.namespaces.append(FakeNamespace)
|
||||||
|
return FakeNamespace
|
||||||
|
|
||||||
|
def delete_namespace(self, name, delete_body):
|
||||||
|
to_delete = None
|
||||||
|
for namespace in self.namespaces:
|
||||||
|
if namespace.metadata.name == name:
|
||||||
|
to_delete = namespace
|
||||||
|
break
|
||||||
|
if not to_delete:
|
||||||
|
raise RuntimeError("Unknown namespace %s" % name)
|
||||||
|
self.namespaces.remove(to_delete)
|
||||||
|
|
||||||
|
def create_namespaced_service_account(self, ns, sa_body):
|
||||||
|
return
|
||||||
|
|
||||||
|
def read_namespaced_service_account(self, user, ns):
|
||||||
|
class FakeSA:
|
||||||
|
class secret:
|
||||||
|
name = "fake"
|
||||||
|
FakeSA.secrets = [FakeSA.secret]
|
||||||
|
return FakeSA
|
||||||
|
|
||||||
|
def read_namespaced_secret(self, name, ns):
|
||||||
|
class FakeSecret:
|
||||||
|
data = {'ca.crt': 'fake-ca', 'token': 'fake-token'}
|
||||||
|
return FakeSecret
|
||||||
|
|
||||||
|
def create_namespaced_pod(self, ns, pod_body):
|
||||||
|
return
|
||||||
|
|
||||||
|
def read_namespaced_pod(self, name, ns):
|
||||||
|
class FakePod:
|
||||||
|
class status:
|
||||||
|
phase = "Running"
|
||||||
|
return FakePod
|
||||||
|
|
||||||
|
|
||||||
|
class FakeRbacClient(object):
|
||||||
|
def create_namespaced_role(self, ns, role_body):
|
||||||
|
return
|
||||||
|
|
||||||
|
def create_namespaced_role_binding(self, ns, role_binding_body):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class TestDriverKubernetes(tests.DBTestCase):
|
||||||
|
log = logging.getLogger("nodepool.TestDriverKubernetes")
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.fake_k8s_client = FakeCoreClient()
|
||||||
|
self.fake_rbac_client = FakeRbacClient()
|
||||||
|
|
||||||
|
def fake_get_client(*args):
|
||||||
|
return self.fake_k8s_client, self.fake_rbac_client
|
||||||
|
|
||||||
|
self.useFixture(fixtures.MockPatchObject(
|
||||||
|
provider.KubernetesProvider, '_get_client',
|
||||||
|
fake_get_client
|
||||||
|
))
|
||||||
|
|
||||||
|
def test_kubernetes_machine(self):
|
||||||
|
configfile = self.setup_config('kubernetes.yaml')
|
||||||
|
pool = self.useNodepool(configfile, watermark_sleep=1)
|
||||||
|
pool.start()
|
||||||
|
req = zk.NodeRequest()
|
||||||
|
req.state = zk.REQUESTED
|
||||||
|
req.node_types.append('pod-fedora')
|
||||||
|
self.zk.storeNodeRequest(req)
|
||||||
|
|
||||||
|
self.log.debug("Waiting for request %s", req.id)
|
||||||
|
req = self.waitForNodeRequest(req)
|
||||||
|
self.assertEqual(req.state, zk.FULFILLED)
|
||||||
|
|
||||||
|
self.assertNotEqual(req.nodes, [])
|
||||||
|
node = self.zk.getNode(req.nodes[0])
|
||||||
|
self.assertEqual(node.allocated_to, req.id)
|
||||||
|
self.assertEqual(node.state, zk.READY)
|
||||||
|
self.assertIsNotNone(node.launcher)
|
||||||
|
self.assertEqual(node.connection_type, 'kubectl')
|
||||||
|
self.assertEqual(node.connection_port.get('token'), 'fake-token')
|
||||||
|
|
||||||
|
node.state = zk.DELETING
|
||||||
|
self.zk.storeNode(node)
|
||||||
|
|
||||||
|
self.waitForNodeDeletion(node)
|
||||||
|
|
||||||
|
def test_kubernetes_native(self):
|
||||||
|
configfile = self.setup_config('kubernetes.yaml')
|
||||||
|
pool = self.useNodepool(configfile, watermark_sleep=1)
|
||||||
|
pool.start()
|
||||||
|
req = zk.NodeRequest()
|
||||||
|
req.state = zk.REQUESTED
|
||||||
|
req.node_types.append('kubernetes-namespace')
|
||||||
|
self.zk.storeNodeRequest(req)
|
||||||
|
|
||||||
|
self.log.debug("Waiting for request %s", req.id)
|
||||||
|
req = self.waitForNodeRequest(req)
|
||||||
|
self.assertEqual(req.state, zk.FULFILLED)
|
||||||
|
|
||||||
|
self.assertNotEqual(req.nodes, [])
|
||||||
|
node = self.zk.getNode(req.nodes[0])
|
||||||
|
self.assertEqual(node.allocated_to, req.id)
|
||||||
|
self.assertEqual(node.state, zk.READY)
|
||||||
|
self.assertIsNotNone(node.launcher)
|
||||||
|
self.assertEqual(node.connection_type, 'namespace')
|
||||||
|
self.assertEqual(node.connection_port.get('token'), 'fake-token')
|
||||||
|
|
||||||
|
node.state = zk.DELETING
|
||||||
|
self.zk.storeNode(node)
|
||||||
|
|
||||||
|
self.waitForNodeDeletion(node)
|
15
playbooks/nodepool-functional-k8s/post.yaml
Normal file
15
playbooks/nodepool-functional-k8s/post.yaml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
- hosts: all
|
||||||
|
vars:
|
||||||
|
nodepool_log_dir: '{{ ansible_user_dir }}/work/logs/nodepool'
|
||||||
|
nodepool_etc_dir: '{{ ansible_user_dir }}/work/etc'
|
||||||
|
tasks:
|
||||||
|
- name: 'Copy files from {{ nodepool_log_dir }}'
|
||||||
|
synchronize:
|
||||||
|
src: '{{ nodepool_log_dir }}'
|
||||||
|
dest: '{{ zuul.executor.log_root }}/{{ inventory_hostname }}'
|
||||||
|
mode: pull
|
||||||
|
- name: 'Copy files from {{ nodepool_etc_dir }}'
|
||||||
|
synchronize:
|
||||||
|
src: '{{ nodepool_etc_dir }}'
|
||||||
|
dest: '{{ zuul.executor.log_root }}/{{ inventory_hostname }}'
|
||||||
|
mode: pull
|
14
playbooks/nodepool-functional-k8s/pre.yaml
Normal file
14
playbooks/nodepool-functional-k8s/pre.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
- hosts: all
|
||||||
|
roles:
|
||||||
|
- role: bindep
|
||||||
|
bindep_profile: dev
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Ensure nodepool services directories
|
||||||
|
file:
|
||||||
|
path: '{{ ansible_user_dir }}/work/{{ item }}'
|
||||||
|
state: directory
|
||||||
|
with_items:
|
||||||
|
- logs/nodepool
|
||||||
|
- etc
|
||||||
|
- images
|
5
playbooks/nodepool-functional-k8s/run.yaml
Normal file
5
playbooks/nodepool-functional-k8s/run.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
- hosts: all
|
||||||
|
roles:
|
||||||
|
- install-nodepool
|
||||||
|
- install-kubernetes
|
||||||
|
- nodepool-k8s-functional
|
@ -12,3 +12,4 @@ voluptuous
|
|||||||
kazoo
|
kazoo
|
||||||
Paste
|
Paste
|
||||||
WebOb>=1.8.1
|
WebOb>=1.8.1
|
||||||
|
openshift
|
||||||
|
88
roles/install-nodepool/tasks/main.yaml
Normal file
88
roles/install-nodepool/tasks/main.yaml
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
- name: Install zookeeper
|
||||||
|
package:
|
||||||
|
name: zookeeperd
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
- name: Start zookeeper
|
||||||
|
service:
|
||||||
|
name: zookeeper
|
||||||
|
state: started
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
- name: Install nodepool
|
||||||
|
command: pip3 install .
|
||||||
|
args:
|
||||||
|
chdir: "{{ zuul.projects['git.openstack.org/openstack-infra/nodepool'].src_dir }}"
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
- name: Setup logging.conf
|
||||||
|
copy:
|
||||||
|
content: |
|
||||||
|
[loggers]
|
||||||
|
keys=root,nodepool,requests,openstack
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys=console,normal
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys=simple
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level=WARNING
|
||||||
|
handlers=console
|
||||||
|
|
||||||
|
[logger_requests]
|
||||||
|
level=WARNING
|
||||||
|
handlers=normal
|
||||||
|
qualname=requests
|
||||||
|
|
||||||
|
[logger_openstack]
|
||||||
|
level=WARNING
|
||||||
|
handlers=normal
|
||||||
|
qualname=openstack
|
||||||
|
|
||||||
|
[logger_gear]
|
||||||
|
level=DEBUG
|
||||||
|
handlers=normal
|
||||||
|
qualname=gear
|
||||||
|
|
||||||
|
[logger_nodepool]
|
||||||
|
level=DEBUG
|
||||||
|
handlers=normal
|
||||||
|
qualname=nodepool
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
level=WARNING
|
||||||
|
class=StreamHandler
|
||||||
|
formatter=simple
|
||||||
|
args=(sys.stdout,)
|
||||||
|
|
||||||
|
[handler_normal]
|
||||||
|
level=DEBUG
|
||||||
|
class=FileHandler
|
||||||
|
formatter=simple
|
||||||
|
args=('{{ ansible_user_dir }}/work/logs/nodepool/launcher.log',)
|
||||||
|
|
||||||
|
[formatter_simple]
|
||||||
|
format=%(asctime)s %(levelname)s %(name)s: %(message)s
|
||||||
|
datefmt=
|
||||||
|
dest: "{{ ansible_user_dir }}/work/etc/logging.conf"
|
||||||
|
|
||||||
|
- name: Setup nodepool.yaml
|
||||||
|
copy:
|
||||||
|
content: |
|
||||||
|
zookeeper-servers:
|
||||||
|
- host: localhost
|
||||||
|
images-dir: "{{ ansible_user_dir }}/work/images/"
|
||||||
|
build-log-dir: "{{ ansible_user_dir }}/work/logs/nodepool/"
|
||||||
|
dest: "{{ ansible_user_dir }}/work/etc/nodepool.yaml"
|
||||||
|
|
||||||
|
- name: Setup secure.conf
|
||||||
|
copy:
|
||||||
|
content: ""
|
||||||
|
dest: "{{ ansible_user_dir }}/work/etc/secure.conf"
|
||||||
|
|
||||||
|
- name: Start the service
|
||||||
|
command: nodepool-launcher -c etc/nodepool.yaml -s etc/secure.conf -l etc/logging.conf -p launcher.pid
|
||||||
|
args:
|
||||||
|
chdir: "{{ ansible_user_dir }}/work/"
|
59
roles/nodepool-k8s-functional/tasks/main.yaml
Normal file
59
roles/nodepool-k8s-functional/tasks/main.yaml
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
- name: debug context names
|
||||||
|
command: kubectl config get-contexts
|
||||||
|
|
||||||
|
- name: Update nodepool.yaml
|
||||||
|
copy:
|
||||||
|
content: |
|
||||||
|
zookeeper-servers:
|
||||||
|
- host: localhost
|
||||||
|
images-dir: "{{ ansible_user_dir }}/work/images/"
|
||||||
|
build-log-dir: "{{ ansible_user_dir }}/work/logs/nodepool/"
|
||||||
|
labels:
|
||||||
|
- name: kubernetes-namespace
|
||||||
|
min-ready: 1
|
||||||
|
- name: pod-fedora
|
||||||
|
min-ready: 1
|
||||||
|
providers:
|
||||||
|
- name: minikube
|
||||||
|
driver: kubernetes
|
||||||
|
context: minikube
|
||||||
|
pools:
|
||||||
|
- name: main
|
||||||
|
labels:
|
||||||
|
- name: kubernetes-namespace
|
||||||
|
type: namespace
|
||||||
|
- name: pod-fedora
|
||||||
|
type: pod
|
||||||
|
image: docker.io/fedora:28
|
||||||
|
dest: "{{ ansible_user_dir }}/work/etc/nodepool.yaml"
|
||||||
|
|
||||||
|
- name: Set nodepool_command facts
|
||||||
|
set_fact:
|
||||||
|
nodepool_command: nodepool -c "{{ ansible_user_dir }}/work/etc/nodepool.yaml"
|
||||||
|
|
||||||
|
- name: Wait for nodes
|
||||||
|
command: "{{ nodepool_command }} list"
|
||||||
|
register: nodepool_list
|
||||||
|
until: nodepool_list.stdout
|
||||||
|
retries: 120
|
||||||
|
delay: 2
|
||||||
|
|
||||||
|
- name: Show nodes
|
||||||
|
command: "{{ nodepool_command }} list --detail"
|
||||||
|
|
||||||
|
- name: Wait for fedora pod to be running
|
||||||
|
shell: "{{ nodepool_command }} list | grep 'pod-fedora.*running'"
|
||||||
|
register: nodepool_list
|
||||||
|
until: nodepool_list.stdout
|
||||||
|
retries: 120
|
||||||
|
delay: 2
|
||||||
|
ignore_errors: yes
|
||||||
|
|
||||||
|
- name: Show nodes
|
||||||
|
command: "{{ nodepool_command }} list --detail"
|
||||||
|
|
||||||
|
- name: Show namespace
|
||||||
|
command: kubectl get namespaces
|
||||||
|
|
||||||
|
- name: Show pods
|
||||||
|
command: kubectl get --all-namespaces=true pods
|
Loading…
Reference in New Issue
Block a user