mirror of
https://github.com/optim-enterprises-bv/kubernetes.git
synced 2025-11-26 19:35:10 +00:00
Rework cluster/juju to reflect current work
This commit imports the latest development focus from the Charmer team working to deliver Kubernetes charms with Juju. Notable Changes: - The charm is now assembled from layers in $JUJU_ROOT/layers - Prior, the juju provider would compile and fat-pack the charms, this new approach delivers the entirety of Kubernetes via hyperkube. - Adds Kubedns as part of `cluster/kube-up.sh` and verification - Removes the hard-coded port 8080 for the Kubernetes Master - Includes TLS validation - Validates kubernetes config from leader charm - Targets Juju 2.0 commands
This commit is contained in:
321
cluster/juju/layers/kubernetes/reactive/k8s.py
Normal file
321
cluster/juju/layers/kubernetes/reactive/k8s.py
Normal file
@@ -0,0 +1,321 @@
|
||||
import os
|
||||
|
||||
from shlex import split
|
||||
from shutil import copy2
|
||||
from subprocess import check_call
|
||||
|
||||
from charms.docker.compose import Compose
|
||||
from charms.reactive import hook
|
||||
from charms.reactive import remove_state
|
||||
from charms.reactive import set_state
|
||||
from charms.reactive import when
|
||||
from charms.reactive import when_not
|
||||
|
||||
from charmhelpers.core import hookenv
|
||||
from charmhelpers.core.hookenv import is_leader
|
||||
from charmhelpers.core.hookenv import status_set
|
||||
from charmhelpers.core.templating import render
|
||||
from charmhelpers.core import unitdata
|
||||
from charmhelpers.core.host import chdir
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
@hook('config-changed')
|
||||
def config_changed():
|
||||
'''If the configuration values change, remove the available states.'''
|
||||
config = hookenv.config()
|
||||
if any(config.changed(key) for key in config.keys()):
|
||||
hookenv.log('Configuration options have changed.')
|
||||
# Use the Compose class that encapsulates the docker-compose commands.
|
||||
compose = Compose('files/kubernetes')
|
||||
hookenv.log('Removing kubelet container and kubelet.available state.')
|
||||
# Stop and remove the Kubernetes kubelet container..
|
||||
compose.kill('kubelet')
|
||||
compose.rm('kubelet')
|
||||
# Remove the state so the code can react to restarting kubelet.
|
||||
remove_state('kubelet.available')
|
||||
hookenv.log('Removing proxy container and proxy.available state.')
|
||||
# Stop and remove the Kubernetes proxy container.
|
||||
compose.kill('proxy')
|
||||
compose.rm('proxy')
|
||||
# Remove the state so the code can react to restarting proxy.
|
||||
remove_state('proxy.available')
|
||||
|
||||
if config.changed('version'):
|
||||
hookenv.log('Removing kubectl.downloaded state so the new version'
|
||||
' of kubectl will be downloaded.')
|
||||
remove_state('kubectl.downloaded')
|
||||
|
||||
|
||||
@when('tls.server.certificate available')
|
||||
@when_not('k8s.server.certificate available')
|
||||
def server_cert():
|
||||
'''When the server certificate is available, get the server certificate from
|
||||
the charm unit data and write it to the proper directory. '''
|
||||
destination_directory = '/srv/kubernetes'
|
||||
# Save the server certificate from unitdata to /srv/kubernetes/server.crt
|
||||
save_certificate(destination_directory, 'server')
|
||||
# Copy the unitname.key to /srv/kubernetes/server.key
|
||||
copy_key(destination_directory, 'server')
|
||||
set_state('k8s.server.certificate available')
|
||||
|
||||
|
||||
@when('tls.client.certificate available')
|
||||
@when_not('k8s.client.certficate available')
|
||||
def client_cert():
|
||||
'''When the client certificate is available, get the client certificate
|
||||
from the charm unitdata and write it to the proper directory. '''
|
||||
destination_directory = '/srv/kubernetes'
|
||||
if not os.path.isdir(destination_directory):
|
||||
os.makedirs(destination_directory)
|
||||
os.chmod(destination_directory, 0o770)
|
||||
# The client certificate is also available on charm unitdata.
|
||||
client_cert_path = 'easy-rsa/easyrsa3/pki/issued/client.crt'
|
||||
kube_cert_path = os.path.join(destination_directory, 'client.crt')
|
||||
if os.path.isfile(client_cert_path):
|
||||
# Copy the client.crt to /srv/kubernetes/client.crt
|
||||
copy2(client_cert_path, kube_cert_path)
|
||||
# The client key is only available on the leader.
|
||||
client_key_path = 'easy-rsa/easyrsa3/pki/private/client.key'
|
||||
kube_key_path = os.path.join(destination_directory, 'client.key')
|
||||
if os.path.isfile(client_key_path):
|
||||
# Copy the client.key to /srv/kubernetes/client.key
|
||||
copy2(client_key_path, kube_key_path)
|
||||
|
||||
|
||||
@when('tls.certificate.authority available')
|
||||
@when_not('k8s.certificate.authority available')
|
||||
def ca():
|
||||
'''When the Certificate Authority is available, copy the CA from the
|
||||
/usr/local/share/ca-certificates/k8s.crt to the proper directory. '''
|
||||
# Ensure the /srv/kubernetes directory exists.
|
||||
directory = '/srv/kubernetes'
|
||||
if not os.path.isdir(directory):
|
||||
os.makedirs(directory)
|
||||
os.chmod(directory, 0o770)
|
||||
# Normally the CA is just on the leader, but the tls layer installs the
|
||||
# CA on all systems in the /usr/local/share/ca-certificates directory.
|
||||
ca_path = '/usr/local/share/ca-certificates/{0}.crt'.format(
|
||||
hookenv.service_name())
|
||||
# The CA should be copied to the destination directory and named 'ca.crt'.
|
||||
destination_ca_path = os.path.join(directory, 'ca.crt')
|
||||
if os.path.isfile(ca_path):
|
||||
copy2(ca_path, destination_ca_path)
|
||||
set_state('k8s.certificate.authority available')
|
||||
|
||||
|
||||
@when('kubelet.available', 'proxy.available', 'cadvisor.available')
|
||||
def final_messaging():
|
||||
'''Lower layers emit messages, and if we do not clear the status messaging
|
||||
queue, we are left with whatever the last method call sets status to. '''
|
||||
# It's good UX to have consistent messaging that the cluster is online
|
||||
if is_leader():
|
||||
status_set('active', 'Kubernetes leader running')
|
||||
else:
|
||||
status_set('active', 'Kubernetes follower running')
|
||||
|
||||
|
||||
@when('kubelet.available', 'proxy.available', 'cadvisor.available')
|
||||
@when_not('skydns.available')
|
||||
def launch_skydns():
|
||||
'''Create a kubernetes service and resource controller for the skydns
|
||||
service. '''
|
||||
# Only launch and track this state on the leader.
|
||||
# Launching duplicate SkyDNS rc will raise an error
|
||||
if not is_leader():
|
||||
return
|
||||
cmd = "kubectl create -f files/manifests/skydns-rc.yml"
|
||||
check_call(split(cmd))
|
||||
cmd = "kubectl create -f files/manifests/skydns-svc.yml"
|
||||
check_call(split(cmd))
|
||||
set_state('skydns.available')
|
||||
|
||||
|
||||
@when('docker.available')
|
||||
@when_not('etcd.available')
|
||||
def relation_message():
|
||||
'''Take over messaging to let the user know they are pending a relationship
|
||||
to the ETCD cluster before going any further. '''
|
||||
status_set('waiting', 'Waiting for relation to ETCD')
|
||||
|
||||
|
||||
@when('etcd.available', 'tls.server.certificate available')
|
||||
@when_not('kubelet.available', 'proxy.available')
|
||||
def master(etcd):
|
||||
'''Install and run the hyperkube container that starts kubernetes-master.
|
||||
This actually runs the kubelet, which in turn runs a pod that contains the
|
||||
other master components. '''
|
||||
render_files(etcd)
|
||||
# Use the Compose class that encapsulates the docker-compose commands.
|
||||
compose = Compose('files/kubernetes')
|
||||
status_set('maintenance', 'Starting the Kubernetes kubelet container.')
|
||||
# Start the Kubernetes kubelet container using docker-compose.
|
||||
compose.up('kubelet')
|
||||
set_state('kubelet.available')
|
||||
# Open the secure port for api-server.
|
||||
hookenv.open_port(6443)
|
||||
status_set('maintenance', 'Starting the Kubernetes proxy container')
|
||||
# Start the Kubernetes proxy container using docker-compose.
|
||||
compose.up('proxy')
|
||||
set_state('proxy.available')
|
||||
status_set('active', 'Kubernetes started')
|
||||
|
||||
|
||||
@when('proxy.available')
|
||||
@when_not('kubectl.downloaded')
|
||||
def download_kubectl():
|
||||
'''Download the kubectl binary to test and interact with the cluster.'''
|
||||
status_set('maintenance', 'Downloading the kubectl binary')
|
||||
version = hookenv.config()['version']
|
||||
cmd = 'wget -nv -O /usr/local/bin/kubectl https://storage.googleapis.com/' \
|
||||
'kubernetes-release/release/{0}/bin/linux/amd64/kubectl'
|
||||
cmd = cmd.format(version)
|
||||
hookenv.log('Downloading kubelet: {0}'.format(cmd))
|
||||
check_call(split(cmd))
|
||||
cmd = 'chmod +x /usr/local/bin/kubectl'
|
||||
check_call(split(cmd))
|
||||
set_state('kubectl.downloaded')
|
||||
status_set('active', 'Kubernetes installed')
|
||||
|
||||
|
||||
@when('kubectl.downloaded')
|
||||
@when_not('kubectl.package.created')
|
||||
def package_kubectl():
|
||||
'''Package the kubectl binary and configuration to a tar file for users
|
||||
to consume and interact directly with Kubernetes.'''
|
||||
if not is_leader():
|
||||
return
|
||||
context = 'default-context'
|
||||
cluster_name = 'kubernetes'
|
||||
public_address = hookenv.unit_public_ip()
|
||||
directory = '/srv/kubernetes'
|
||||
key = 'client.key'
|
||||
ca = 'ca.crt'
|
||||
cert = 'client.crt'
|
||||
user = 'ubuntu'
|
||||
port = '6443'
|
||||
with chdir(directory):
|
||||
# Create the config file with the external address for this server.
|
||||
cmd = 'kubectl config set-cluster --kubeconfig={0}/config {1} ' \
|
||||
'--server=https://{2}:{3} --certificate-authority={4}'
|
||||
check_call(split(cmd.format(directory, cluster_name, public_address,
|
||||
port, ca)))
|
||||
# Create the credentials.
|
||||
cmd = 'kubectl config set-credentials --kubeconfig={0}/config {1} ' \
|
||||
'--client-key={2} --client-certificate={3}'
|
||||
check_call(split(cmd.format(directory, user, key, cert)))
|
||||
# Create a default context with the cluster.
|
||||
cmd = 'kubectl config set-context --kubeconfig={0}/config {1}' \
|
||||
' --cluster={2} --user={3}'
|
||||
check_call(split(cmd.format(directory, context, cluster_name, user)))
|
||||
# Now make the config use this new context.
|
||||
cmd = 'kubectl config use-context --kubeconfig={0}/config {1}'
|
||||
check_call(split(cmd.format(directory, context)))
|
||||
# Copy the kubectl binary to this directory
|
||||
cmd = 'cp -v /usr/local/bin/kubectl {0}'.format(directory)
|
||||
check_call(split(cmd))
|
||||
|
||||
# Create an archive with all the necessary files.
|
||||
cmd = 'tar -cvzf /home/ubuntu/kubectl_package.tar.gz ca.crt client.crt client.key config kubectl' # noqa
|
||||
check_call(split(cmd))
|
||||
set_state('kubectl.package.created')
|
||||
|
||||
|
||||
@when('proxy.available')
|
||||
@when_not('cadvisor.available')
|
||||
def start_cadvisor():
|
||||
'''Start the cAdvisor container that gives metrics about the other
|
||||
application containers on this system. '''
|
||||
compose = Compose('files/kubernetes')
|
||||
compose.up('cadvisor')
|
||||
set_state('cadvisor.available')
|
||||
status_set('active', 'cadvisor running on port 8088')
|
||||
hookenv.open_port(8088)
|
||||
|
||||
|
||||
@when('sdn.available')
|
||||
def gather_sdn_data():
|
||||
'''Get the Software Defined Network (SDN) information and return it as a
|
||||
dictionary.'''
|
||||
# SDN Providers pass data via the unitdata.kv module
|
||||
db = unitdata.kv()
|
||||
# Generate an IP address for the DNS provider
|
||||
subnet = db.get('sdn_subnet')
|
||||
if subnet:
|
||||
ip = subnet.split('/')[0]
|
||||
dns_server = '.'.join(ip.split('.')[0:-1]) + '.10'
|
||||
addedcontext = {}
|
||||
addedcontext['dns_server'] = dns_server
|
||||
return addedcontext
|
||||
return {}
|
||||
|
||||
|
||||
def copy_key(directory, prefix):
|
||||
'''Copy the key from the easy-rsa/easyrsa3/pki/private directory to the
|
||||
specified directory. '''
|
||||
if not os.path.isdir(directory):
|
||||
os.makedirs(directory)
|
||||
os.chmod(directory, 0o770)
|
||||
# Must remove the path characters from the local unit name.
|
||||
path_name = hookenv.local_unit().replace('/', '_')
|
||||
# The key is not in unitdata it is in the local easy-rsa directory.
|
||||
local_key_path = 'easy-rsa/easyrsa3/pki/private/{0}.key'.format(path_name)
|
||||
key_name = '{0}.key'.format(prefix)
|
||||
# The key should be copied to this directory.
|
||||
destination_key_path = os.path.join(directory, key_name)
|
||||
# Copy the key file from the local directory to the destination.
|
||||
copy2(local_key_path, destination_key_path)
|
||||
|
||||
|
||||
def render_files(reldata=None):
|
||||
'''Use jinja templating to render the docker-compose.yml and master.json
|
||||
file to contain the dynamic data for the configuration files.'''
|
||||
context = {}
|
||||
# Load the context manager with sdn and config data.
|
||||
context.update(gather_sdn_data())
|
||||
context.update(hookenv.config())
|
||||
if reldata:
|
||||
context.update({'connection_string': reldata.connection_string()})
|
||||
charm_dir = hookenv.charm_dir()
|
||||
rendered_kube_dir = os.path.join(charm_dir, 'files/kubernetes')
|
||||
if not os.path.exists(rendered_kube_dir):
|
||||
os.makedirs(rendered_kube_dir)
|
||||
rendered_manifest_dir = os.path.join(charm_dir, 'files/manifests')
|
||||
if not os.path.exists(rendered_manifest_dir):
|
||||
os.makedirs(rendered_manifest_dir)
|
||||
# Add the manifest directory so the docker-compose file can have.
|
||||
context.update({'manifest_directory': rendered_manifest_dir,
|
||||
'private_address': hookenv.unit_get('private-address')})
|
||||
|
||||
# Render the files/kubernetes/docker-compose.yml file that contains the
|
||||
# definition for kubelet and proxy.
|
||||
target = os.path.join(rendered_kube_dir, 'docker-compose.yml')
|
||||
render('docker-compose.yml', target, context)
|
||||
# Render the files/manifests/master.json that contains parameters for the
|
||||
# apiserver, controller, and controller-manager
|
||||
target = os.path.join(rendered_manifest_dir, 'master.json')
|
||||
render('master.json', target, context)
|
||||
# Render files/kubernetes/skydns-svc.yaml for SkyDNS service
|
||||
target = os.path.join(rendered_manifest_dir, 'skydns-svc.yml')
|
||||
render('skydns-svc.yml', target, context)
|
||||
# Render files/kubernetes/skydns-rc.yaml for SkyDNS pods
|
||||
target = os.path.join(rendered_manifest_dir, 'skydns-rc.yml')
|
||||
render('skydns-rc.yml', target, context)
|
||||
|
||||
|
||||
def save_certificate(directory, prefix):
|
||||
'''Get the certificate from the charm unitdata, and write it to the proper
|
||||
directory. The parameters are: destination directory, and prefix to use
|
||||
for the key and certificate name.'''
|
||||
if not os.path.isdir(directory):
|
||||
os.makedirs(directory)
|
||||
os.chmod(directory, 0o770)
|
||||
# Grab the unitdata key value store.
|
||||
store = unitdata.kv()
|
||||
certificate_data = store.get('tls.{0}.certificate'.format(prefix))
|
||||
certificate_name = '{0}.crt'.format(prefix)
|
||||
# The certificate should be saved to this directory.
|
||||
certificate_path = os.path.join(directory, certificate_name)
|
||||
# write the server certificate out to the correct location
|
||||
with open(certificate_path, 'w') as fp:
|
||||
fp.write(certificate_data)
|
||||
Reference in New Issue
Block a user