238 lines
8.0 KiB
Python
238 lines
8.0 KiB
Python
# 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 client as os_client
|
|
from openshift import config
|
|
|
|
from nodepool import exceptions
|
|
from nodepool.driver import Provider
|
|
from nodepool.driver.openshift import handler
|
|
|
|
urllib3.disable_warnings()
|
|
|
|
|
|
class OpenshiftProvider(Provider):
|
|
log = logging.getLogger("nodepool.driver.openshift.OpenshiftProvider")
|
|
|
|
def __init__(self, provider, *args):
|
|
self.provider = provider
|
|
self.ready = False
|
|
try:
|
|
self.os_client, self.k8s_client = self._get_client(
|
|
provider.context)
|
|
except kce.ConfigException:
|
|
self.log.exception(
|
|
"Couldn't load context %s from config", provider.context)
|
|
self.os_client = None
|
|
self.k8s_client = None
|
|
self.project_names = set()
|
|
for pool in provider.pools.values():
|
|
self.project_names.add(pool.name)
|
|
|
|
def _get_client(self, context):
|
|
conf = config.new_client_from_config(context=context)
|
|
return (
|
|
os_client.OapiApi(conf),
|
|
k8s_client.CoreV1Api(conf))
|
|
|
|
def start(self, zk_conn):
|
|
self.log.debug("Starting")
|
|
if self.ready or not self.os_client or not self.k8s_client:
|
|
return
|
|
self.ready = True
|
|
|
|
def stop(self):
|
|
self.log.debug("Stopping")
|
|
|
|
def listNodes(self):
|
|
servers = []
|
|
|
|
class FakeServer:
|
|
def __init__(self, project, provider, valid_names):
|
|
self.id = project.metadata.name
|
|
self.name = project.metadata.name
|
|
self.metadata = {}
|
|
|
|
if [True for valid_name in valid_names
|
|
if project.metadata.name.startswith("%s-" % valid_name)]:
|
|
node_id = project.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 project, let's skip metadata
|
|
pass
|
|
|
|
def get(self, name, default=None):
|
|
return getattr(self, name, default)
|
|
|
|
if self.ready:
|
|
for project in self.os_client.list_project().items:
|
|
servers.append(FakeServer(
|
|
project, self.provider.name, self.project_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 project" % server_id)
|
|
try:
|
|
self.os_client.delete_project(server_id)
|
|
self.log.info("%s: project removed" % server_id)
|
|
except Exception:
|
|
# TODO: implement better exception handling
|
|
self.log.exception("Couldn't remove project %s" % server_id)
|
|
|
|
def waitForNodeCleanup(self, server_id):
|
|
for retry in range(300):
|
|
try:
|
|
self.os_client.read_project(server_id)
|
|
except Exception:
|
|
break
|
|
time.sleep(1)
|
|
|
|
def createProject(self, project):
|
|
self.log.debug("%s: creating project" % project)
|
|
# Create the project
|
|
proj_body = {
|
|
'apiVersion': 'v1',
|
|
'kind': 'ProjectRequest',
|
|
'metadata': {
|
|
'name': project,
|
|
}
|
|
}
|
|
self.os_client.create_project_request(proj_body)
|
|
return project
|
|
|
|
def prepareProject(self, project):
|
|
user = "zuul-worker"
|
|
|
|
# Create the service account
|
|
sa_body = {
|
|
'apiVersion': 'v1',
|
|
'kind': 'ServiceAccount',
|
|
'metadata': {'name': user}
|
|
}
|
|
self.k8s_client.create_namespaced_service_account(project, sa_body)
|
|
|
|
# Wait for the token to be created
|
|
for retry in range(30):
|
|
sa = self.k8s_client.read_namespaced_service_account(
|
|
user, project)
|
|
token = None
|
|
if sa.secrets:
|
|
for secret_obj in sa.secrets:
|
|
secret = self.k8s_client.read_namespaced_secret(
|
|
secret_obj.name, project)
|
|
token = secret.metadata.annotations.get(
|
|
'openshift.io/token-secret.value')
|
|
if token:
|
|
break
|
|
if token:
|
|
break
|
|
time.sleep(1)
|
|
if not token:
|
|
raise exceptions.LaunchNodepoolException(
|
|
"%s: couldn't find token for service account %s" %
|
|
(project, sa))
|
|
|
|
# Give service account admin access
|
|
role_body = {
|
|
'apiVersion': 'v1',
|
|
'kind': 'RoleBinding',
|
|
'metadata': {'name': 'admin-0'},
|
|
'roleRef': {'name': 'admin'},
|
|
'subjects': [{
|
|
'kind': 'ServiceAccount',
|
|
'name': user,
|
|
'namespace': project,
|
|
}],
|
|
'userNames': ['system:serviceaccount:%s:zuul-worker' % project]
|
|
}
|
|
try:
|
|
self.os_client.create_namespaced_role_binding(project, role_body)
|
|
except ValueError:
|
|
# https://github.com/ansible/ansible/issues/36939
|
|
pass
|
|
|
|
resource = {
|
|
'namespace': project,
|
|
'host': self.os_client.api_client.configuration.host,
|
|
'skiptls': not self.os_client.api_client.configuration.verify_ssl,
|
|
'token': token,
|
|
'user': user,
|
|
}
|
|
self.log.info("%s: project created" % project)
|
|
return resource
|
|
|
|
def createPod(self, project, label):
|
|
spec_body = {
|
|
'name': label.name,
|
|
'image': label.image,
|
|
'imagePullPolicy': label.image_pull,
|
|
'command': ["/bin/bash", "-c", "--"],
|
|
'args': ["while true; do sleep 30; done;"],
|
|
'workingDir': '/tmp',
|
|
}
|
|
if label.cpu or label.memory:
|
|
spec_body['resources'] = {}
|
|
for rtype in ('requests', 'limits'):
|
|
rbody = {}
|
|
if label.cpu:
|
|
rbody['cpu'] = int(label.cpu)
|
|
if label.memory:
|
|
rbody['memory'] = '%dMi' % int(label.memory)
|
|
spec_body['resources'][rtype] = rbody
|
|
pod_body = {
|
|
'apiVersion': 'v1',
|
|
'kind': 'Pod',
|
|
'metadata': {'name': label.name},
|
|
'spec': {
|
|
'containers': [spec_body],
|
|
},
|
|
'restartPolicy': 'Never',
|
|
}
|
|
self.k8s_client.create_namespaced_pod(project, pod_body)
|
|
for retry in range(300):
|
|
pod = self.k8s_client.read_namespaced_pod(label.name, project)
|
|
if pod.status.phase == "Running":
|
|
break
|
|
self.log.debug("%s: pod status is %s", project, pod.status.phase)
|
|
time.sleep(1)
|
|
if retry == 299:
|
|
raise exceptions.LaunchNodepoolException(
|
|
"%s: pod failed to initialize (%s)" % (
|
|
project, pod.status.phase))
|
|
|
|
def getRequestHandler(self, poolworker, request):
|
|
return handler.OpenshiftNodeRequestHandler(poolworker, request)
|