zuul-operator/zuul_operator/operator.py

172 lines
5.8 KiB
Python

# Copyright 2021 Acme Gating, LLC
#
# 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 asyncio
import collections
import kopf
import pykube
from . import objects
from .zuul import Zuul
ConfigResource = collections.namedtuple('ConfigResource', [
'attr', 'namespace', 'zuul_name', 'resource_name'])
def memoize_secrets(memo, logger):
# (zuul_namespace, zuul) -> list of resources
memo.config_resources.clear()
new_resources = {}
# lookup all zuuls and update configmaps
api = pykube.HTTPClient(pykube.KubeConfig.from_env())
for namespace in objects.Namespace.objects(api):
for zuul in objects.ZuulObject.objects(api).filter(
namespace=namespace.name):
resources = new_resources.\
setdefault((namespace.name, zuul.name), [])
# Zuul tenant config
secret = zuul.obj['spec']['scheduler']['config']['secretName']
res = ConfigResource('spec.scheduler.config.secretName',
namespace.name, zuul.name, secret)
resources.append(res)
# Nodepool config
secret = zuul.obj['spec']['launcher']['config']['secretName']
res = ConfigResource('spec.launcher.config.secretName',
namespace.name, zuul.name, secret)
resources.append(res)
# Mutate the global instance
memo.config_resources.clear()
memo.config_resources.update(new_resources)
@kopf.on.startup()
def startup(memo, logger, **kwargs):
# Operator handlers (like this one) get a single global memo
# object; resource handlers (like update) get a memo object for
# that specific resource with items shallow-copied from the global
# memo.
#
# Initialize a dictionary here that we will mutate (but never
# overwrite) in all the handlers.
memo.config_resources = {}
memoize_secrets(memo, logger)
@kopf.on.update('secrets')
def update_secret(name, namespace, logger, memo, new, **kwargs):
# if this configmap isn't known, ignore
logger.info(f"Update secret {namespace}/{name}")
api = pykube.HTTPClient(pykube.KubeConfig.from_env())
for ((zuul_namespace, zuul_name), resources) in \
memo.config_resources.items():
for resource in resources:
if (resource.namespace != namespace or
resource.resource_name != name):
continue
logger.info(f"Affects zuul {zuul_namespace}/{zuul_name}")
zuul_obj = objects.ZuulObject.objects(api).filter(
namespace=zuul_namespace).get(name=zuul_name)
zuul = Zuul(namespace, zuul_name, logger, zuul_obj.obj['spec'])
if resource.attr == 'spec.scheduler.config.secretName':
zuul.smart_reconfigure()
if resource.attr == 'spec.launcher.config.secretName':
zuul.create_nodepool()
@kopf.on.create('zuuls', backoff=10)
def create_fn(spec, name, namespace, logger, memo, **kwargs):
logger.info(f"Create zuul {namespace}/{name}")
zuul = Zuul(namespace, name, logger, spec)
# Get DB installation started first; it's slow and has no
# dependencies.
zuul.install_db()
# Install Cert-Manager and request the CA cert before installing
# ZK because the CRDs must exist.
zuul.install_cert_manager()
zuul.wait_for_cert_manager()
zuul.create_cert_manager_ca()
# Now we can install ZK
zuul.install_zk()
# Wait for both to finish
zuul.wait_for_zk()
zuul.wait_for_db()
zuul.write_zuul_conf()
zuul.create_zuul()
memoize_secrets(memo, logger)
# We can set a status with something like:
# return {'message': 'hello world'}
@kopf.on.update('zuuls', backoff=10)
def update_fn(name, namespace, logger, old, new, memo, **kwargs):
logger.info(f"Update zuul {namespace}/{name}")
old = old['spec']
new = new['spec']
zuul = Zuul(namespace, name, logger, new)
conf_changed = False
spec_changed = False
if new.get('database') != old.get('database'):
logger.info("Database changed")
conf_changed = True
# redo db stuff
zuul.install_db()
zuul.wait_for_db()
if new.get('zookeeper') != old.get('zookeeper'):
logger.info("ZooKeeper changed")
conf_changed = True
# redo zk
zuul.install_cert_manager()
zuul.wait_for_cert_manager()
zuul.create_cert_manager_ca()
# Now we can install ZK
zuul.install_zk()
zuul.wait_for_zk()
if new.get('connections') != old.get('connections'):
logger.info("Connections changed")
conf_changed = True
for key in ['executor', 'merger', 'scheduler', 'registry',
'launcher', 'connections', 'externalConfig',
'imagePrefix', 'imagePullSecrets', 'zuulImageVersion',
'zuulPreviewImageVersion', 'zuulRegistryImageVersion',
'nodepoolImageVersion']:
if new.get(key) != old.get(key):
logger.info(f"{key} changed")
spec_changed = True
if conf_changed:
spec_changed = True
zuul.write_zuul_conf()
if spec_changed:
zuul.create_zuul()
memoize_secrets(memo, logger)
class ZuulOperator:
def run(self):
loop = asyncio.get_event_loop()
loop.run_until_complete(kopf.operator())