435 lines
17 KiB
Python
435 lines
17 KiB
Python
# 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 __future__ import print_function
|
|
import copy
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import yaml
|
|
|
|
from kolla_kubernetes.commands import base_command
|
|
from kolla_kubernetes import pathfinder
|
|
from kolla_kubernetes import service_resources
|
|
from kolla_kubernetes import utils
|
|
|
|
KKR = service_resources.KollaKubernetesResources.Get()
|
|
|
|
|
|
class ResourceBase(base_command.KollaKubernetesBaseCommand):
|
|
"""Create, delete, or query status for kolla-kubernetes resources"""
|
|
|
|
def get_parser(self, prog_name, skip_action=False):
|
|
parser = super(ResourceBase, self).get_parser(prog_name)
|
|
if not skip_action:
|
|
parser.add_argument(
|
|
"action",
|
|
metavar="<action>",
|
|
help=("One of [%s]" % ("|".join(service_resources.Service.
|
|
VALID_ACTIONS)))
|
|
)
|
|
parser.add_argument(
|
|
"resource_type",
|
|
metavar="<resource-type>",
|
|
help=("One of [%s]" % ("|".join(service_resources.Service.
|
|
VALID_RESOURCE_TYPES)))
|
|
)
|
|
return parser
|
|
|
|
def validate_args(self, args, skip_action=False):
|
|
if not skip_action and args.action not in service_resources.Service.\
|
|
VALID_ACTIONS:
|
|
msg = ("action [{}] not in valid actions [{}]".format(
|
|
args.action,
|
|
"|".join(service_resources.Service.VALID_ACTIONS)))
|
|
raise Exception(msg)
|
|
if args.resource_type not in service_resources.Service.\
|
|
VALID_RESOURCE_TYPES:
|
|
msg = ("resource_type [{}] not in valid resource_types [{}]"
|
|
.format(args.resource_type,
|
|
"|".join(service_resources.Service.
|
|
VALID_RESOURCE_TYPES)))
|
|
raise Exception(msg)
|
|
|
|
|
|
class ResourceTemplate(ResourceBase):
|
|
"""Jinja process kolla-kubernetes resource template files"""
|
|
|
|
# This command adds the CLI params as part of the Jinja vars for processing
|
|
# templates. This is needed because some of the templates will need to know
|
|
# the arguments with which this CLI is called. For example, some
|
|
# resource-type "disk" templates may reference '{{
|
|
# kolla_kubernetes.cli.args.action }}' to produce output such as "gcloud
|
|
# disk create" or "gcloud disk delete" based on the CLI params. Most
|
|
# templates will not require this, but it is needed for some.
|
|
|
|
def get_parser(self, prog_name, skip_action=False):
|
|
parser = super(ResourceTemplate, self).get_parser(prog_name,
|
|
skip_action)
|
|
parser.add_argument(
|
|
"resource_name",
|
|
metavar="<resource-name>",
|
|
nargs='+',
|
|
help=("The unique resource-name under service->resource_type")
|
|
)
|
|
parser.add_argument(
|
|
'--print-jinja-vars',
|
|
action='store_true',
|
|
help=("If this boolean is set, the final jinja vars dict used as"
|
|
" input for template processing will be printed to stderr. "
|
|
" The vars dict is created by merging configuration files "
|
|
" from several sources before applying the dict to itself.")
|
|
),
|
|
parser.add_argument(
|
|
"-o",
|
|
"--output",
|
|
metavar="output",
|
|
default="yaml",
|
|
help=("Format output into one of [%s]" % (
|
|
"|".join(['yaml', 'json'])))
|
|
),
|
|
parser.add_argument(
|
|
"-d",
|
|
"--debug-container",
|
|
metavar="container",
|
|
dest='debug_container',
|
|
action='append',
|
|
help=("Assist in the debugging of the specified container")
|
|
),
|
|
parser.add_argument(
|
|
'--print-jinja-keys-regex',
|
|
metavar='<print-jinja-keys-regex>',
|
|
type=str,
|
|
default=None,
|
|
help=("If this regex string is set, all matching keys encountered"
|
|
" during the creation of the jinja vars dict will be printed"
|
|
" to stderr at each stage of processing. The vars dict is"
|
|
" created by merging configuration files from several"
|
|
" sources before applying the dict to itself.")
|
|
)
|
|
return parser
|
|
|
|
def take_action(self, args, skip_and_return=False):
|
|
# Validate input arguments
|
|
self.validate_args(args)
|
|
|
|
multi = len(args.resource_name) != 1
|
|
multidoc = {
|
|
'apiVersion': 'v1',
|
|
'kind': 'List',
|
|
'items': []
|
|
}
|
|
|
|
if args.resource_name[0] == 'all':
|
|
services = KKR.getServices()
|
|
for service in services.keys():
|
|
service_object = services.get(service)
|
|
service_object.do_apply(args.action, args.resource_type)
|
|
return
|
|
|
|
for resource_name in args.resource_name:
|
|
service_name = KKR.getServiceNameByResourceTypeName(
|
|
args.resource_type,
|
|
resource_name)
|
|
service = KKR.getServiceByName(service_name)
|
|
rt = service.getResourceTemplateByTypeAndName(
|
|
args.resource_type, resource_name)
|
|
|
|
tmpargs = copy.deepcopy(vars(args))
|
|
tmpargs['resource_name'] = resource_name
|
|
variables = KKR.GetJinjaDict(service_name, tmpargs,
|
|
args.print_jinja_keys_regex)
|
|
|
|
# Merge the template vars with the jinja vars before processing
|
|
variables['kolla_kubernetes'].update(
|
|
{"template": {"vars": rt.getVars()}})
|
|
|
|
# handle the debug option --print-jinja-vars
|
|
if args.print_jinja_vars is True:
|
|
print(utils.YamlUtils.yaml_dict_to_string(variables),
|
|
file=sys.stderr)
|
|
|
|
if args.resource_type == 'configmap' and \
|
|
rt.getTemplate() == 'auto':
|
|
nsname = 'kolla_kubernetes_namespace'
|
|
cmd = "kubectl create configmap {} -o yaml --dry-run"
|
|
cmd = cmd.format(resource_name)
|
|
|
|
for f in pathfinder.PathFinder.find_config_files(
|
|
resource_name):
|
|
cmd += ' --from-file={}={}'.format(
|
|
os.path.basename(f), f)
|
|
|
|
# Execute the command
|
|
out, err = utils.ExecUtils.exec_command(cmd)
|
|
y = yaml.safe_load(out)
|
|
y['metadata']['namespace'] = variables[nsname]
|
|
|
|
res = y
|
|
else:
|
|
# process the template
|
|
raw_doc = utils.JinjaUtils.render_jinja(
|
|
variables,
|
|
utils.FileUtils.read_string_from_file(
|
|
rt.getTemplatePath()))
|
|
res = yaml.safe_load(raw_doc)
|
|
|
|
if args.debug_container is not None:
|
|
y = res
|
|
kind = y['kind']
|
|
if kind not in ('PetSet', 'Deployment', 'Job', 'DaemonSet',
|
|
'ReplicationController', 'Pod'):
|
|
raise Exception("Template doesn't have containers.")
|
|
pod = y
|
|
if kind != 'Pod':
|
|
pod = y['spec']['template']
|
|
alpha_init_containers = None
|
|
annotation = 'pod.alpha.kubernetes.io/init-containers'
|
|
if 'metadata' in pod and 'annotations' in pod['metadata'] and \
|
|
annotation in pod['metadata']['annotations']:
|
|
j = json.loads(pod['metadata']['annotations'][annotation])
|
|
alpha_init_containers = {}
|
|
for c in j:
|
|
alpha_init_containers[c['name']] = c
|
|
containers = {}
|
|
for c in pod['spec']['containers']:
|
|
containers[c['name']] = c
|
|
for c in args.debug_container:
|
|
found = False
|
|
warn_msg = "WARNING: container [{}] already has a" + \
|
|
" command override."
|
|
warn_msg = warn_msg.format(c)
|
|
if alpha_init_containers and c in alpha_init_containers:
|
|
if 'command' in alpha_init_containers[c]:
|
|
print(warn_msg, file=sys.stderr)
|
|
if 'args' in alpha_init_containers[c]:
|
|
del alpha_init_containers[c]['args']
|
|
alpha_init_containers[c]['command'] = \
|
|
['/bin/bash', '-c',
|
|
'while true; do sleep 1000; done']
|
|
found = True
|
|
if c in containers:
|
|
if 'command' in containers[c]:
|
|
print(warn_msg, file=sys.stderr)
|
|
if 'args' in containers[c]:
|
|
del containers[c]['args']
|
|
containers[c]['command'] = \
|
|
['/bin/bash', '-c',
|
|
'while true; do sleep 1000; done']
|
|
found = True
|
|
|
|
if not found:
|
|
raise Exception("Failed to find container: %s" % c)
|
|
|
|
if alpha_init_containers:
|
|
annotation = 'pod.alpha.kubernetes.io/init-containers'
|
|
v = alpha_init_containers.values()
|
|
pod['metadata']['annotations'][annotation] = json.dumps(v)
|
|
multidoc['items'].append(res)
|
|
|
|
if skip_and_return:
|
|
if multi:
|
|
return yaml.safe_dump(multidoc)
|
|
else:
|
|
return yaml.safe_dump(res)
|
|
|
|
if args.output == 'json':
|
|
if multi:
|
|
print(json.dumps(multidoc, indent=4), end="")
|
|
else:
|
|
print(json.dumps(res, indent=4), end="")
|
|
elif multi:
|
|
print(yaml.safe_dump(multidoc))
|
|
else:
|
|
if args.debug_container is not None:
|
|
print(yaml.safe_dump(res), end="")
|
|
else:
|
|
print(raw_doc, end="")
|
|
|
|
|
|
class Template(ResourceTemplate):
|
|
"""Jinja process kolla-kubernetes resource template files"""
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(Template, self).get_parser(prog_name,
|
|
skip_action=True)
|
|
return parser
|
|
|
|
def validate_args(self, args):
|
|
super(Template, self).validate_args(args, skip_action=True)
|
|
|
|
|
|
class Resource(ResourceTemplate):
|
|
"""Create kolla-kubernetes resources"""
|
|
|
|
def validate_args(self, args):
|
|
super(Resource, self).validate_args(args)
|
|
if args.action not in ['create', 'delete', 'status']:
|
|
msg = ("action [{}] currently not supported".format(
|
|
args.action))
|
|
raise Exception(msg)
|
|
|
|
def _kind_to_cli(self, kind):
|
|
kind_map = {
|
|
'PetSet': 'petset',
|
|
'StatefulSet': 'statefulset',
|
|
'Pod': 'pod',
|
|
'ReplicationController': 'rc',
|
|
'DaemonSet': 'daemonset',
|
|
'Job': 'job',
|
|
'Deployment': 'deployment',
|
|
'ConfigMap': 'configmap',
|
|
'Secret': 'secret',
|
|
'Service': 'svc',
|
|
'PersistentVolume': 'pv',
|
|
'PersistentVolumeClaim': 'pvc',
|
|
}
|
|
if kind not in kind_map:
|
|
msg = ("unknown template kind [{}].".format(kind))
|
|
raise Exception(msg)
|
|
return kind_map[kind]
|
|
|
|
def _process_template(self, kind, namespace, template, names, action):
|
|
nsflag = ""
|
|
if kind != 'pv':
|
|
nsflag = " --namespace={}".format(namespace)
|
|
if action == 'create':
|
|
with tempfile.NamedTemporaryFile() as tf:
|
|
tf.write(template)
|
|
tf.flush()
|
|
s = "kubectl {} -f {}{}".format(
|
|
action, tf.name, nsflag)
|
|
subprocess.call(s, shell=True)
|
|
elif action == "delete":
|
|
s = "kubectl delete {} {}{}".format(
|
|
kind, names, nsflag)
|
|
subprocess.call(s, shell=True)
|
|
elif action == 'status':
|
|
s = "kubectl get {} {}{}".format(
|
|
kind, names, nsflag)
|
|
subprocess.call(s, shell=True)
|
|
|
|
def _get_ns(self, y):
|
|
template_ns = ''
|
|
try:
|
|
template_ns = y['metadata']['namespace']
|
|
except Exception:
|
|
pass
|
|
return template_ns
|
|
|
|
def take_action(self, args):
|
|
tmpl = super(Resource, self).take_action(args, skip_and_return=True)
|
|
y = yaml.safe_load(tmpl)
|
|
kind = y['kind']
|
|
if kind == 'List':
|
|
first_item = y['items'][0]
|
|
ns = self._get_ns(first_item)
|
|
type_map = {}
|
|
for item in y['items']:
|
|
if self._get_ns(item) != ns:
|
|
msg = "Bad template in list. Different namespaces."
|
|
raise Exception(msg)
|
|
kind_cli = self._kind_to_cli(item['kind'])
|
|
t = type_map.get(kind_cli)
|
|
if t is None:
|
|
type_map[kind_cli] = t = []
|
|
t.append(item['metadata']['name'])
|
|
if args.action in ('status', 'delete'):
|
|
for (kind_cli, names) in type_map.items():
|
|
self._process_template(kind_cli, ns, tmpl, ' '.join(names),
|
|
args.action)
|
|
else:
|
|
self._process_template(self._kind_to_cli(first_item['kind']),
|
|
ns, tmpl, '', args.action)
|
|
|
|
else:
|
|
ns = self._get_ns(y)
|
|
self._process_template(self._kind_to_cli(kind),
|
|
ns,
|
|
tmpl, y['metadata']['name'],
|
|
args.action)
|
|
|
|
|
|
class ResourceMap(base_command.KollaKubernetesBaseCommand):
|
|
"""List available kolla-kubernetes resources to be created or deleted"""
|
|
|
|
# If the operator has any question on what Services have what resources,
|
|
# and what resources reference which resourcefiles (on disk), then this
|
|
# command is helpful. This command prints the available resources in a
|
|
# tree of Service->ResourceType->ResourceFiles.
|
|
|
|
def get_parser(self, prog_name):
|
|
parser = super(ResourceMap, self).get_parser(prog_name)
|
|
parser.add_argument(
|
|
"--resource-type",
|
|
metavar="<resource-type>",
|
|
help=("Filter by one of [%s]" % (
|
|
"|".join(service_resources.Service.VALID_RESOURCE_TYPES)))
|
|
)
|
|
parser.add_argument(
|
|
"--service-name",
|
|
metavar="<service-name>",
|
|
help=("Filter by one of [%s]" % (
|
|
"|".join(KKR.getServices().keys())))
|
|
)
|
|
parser.add_argument(
|
|
"-o",
|
|
"--output",
|
|
metavar="output",
|
|
default="text",
|
|
help=("Format output into one of [%s]" % (
|
|
"|".join(['txt', 'json', 'yaml'])))
|
|
)
|
|
return parser
|
|
|
|
def take_action(self, args):
|
|
resources = []
|
|
for service_name, s in KKR.getServices().items():
|
|
|
|
# Skip specific services if the user has defined a filter
|
|
if (args.service_name is not None) and (
|
|
args.service_name != service_name):
|
|
continue
|
|
|
|
if args.output == 'text':
|
|
print('service[{}]'.format(s.getName()))
|
|
|
|
for t in service_resources.Service.VALID_RESOURCE_TYPES:
|
|
# Skip specific resource_types if the user has defined a filter
|
|
if args.resource_type is not None and args.resource_type != t:
|
|
continue
|
|
|
|
resourceTemplates = s.getResourceTemplatesByType(t)
|
|
|
|
if args.output == 'text':
|
|
print(' resource_type[{}] num_items[{}]'.format(
|
|
t, len(resourceTemplates)))
|
|
|
|
# Print the resource files
|
|
for rt in s.getResourceTemplatesByType(t):
|
|
if args.output == 'text':
|
|
print(' ' + str(rt))
|
|
resources.append({
|
|
'resource_type': t,
|
|
'service_name': service_name,
|
|
'resource_name': rt.getName(),
|
|
'template': rt.getTemplate(),
|
|
'vars': rt.getVars(),
|
|
})
|
|
|
|
if args.output == 'json':
|
|
print(json.dumps(resources))
|
|
if args.output == 'yaml':
|
|
print(yaml.safe_dump(resources))
|