Adding port, labels and environment parser

This commit is contained in:
Peter Jausovec 2017-01-03 16:49:22 -08:00
Родитель 2d96222603
Коммит eac67e6ef1
2 изменённых файлов: 261 добавлений и 131 удалений

Просмотреть файл

@ -0,0 +1,199 @@
class PortParser(object):
def __init__(self, service_info):
self.service_info = service_info
def parse_private_ports(self):
"""
Parses the 'expose' key in the docker-compose file and returns a
list of tuples with port numbers. These tuples are used
to create portMappings (blue/green only) in the marathon.json file
"""
port_tuple_list = []
if 'expose' not in self.service_info:
return port_tuple_list
for port_entry in self.service_info['expose']:
if self._is_number(port_entry):
port_tuple_list.append((int(port_entry), int(port_entry)))
else:
raise ValueError(
'Port number "%s" is not a valid number', port_entry)
return port_tuple_list
def parse_internal_ports(self):
"""
Parses the 'ports' key in the docker-compose file and returns a list of
tuples with port numbers. These tuples are used to create
portMappings (blue/green and cyan) in the marathon.json file
"""
port_tuple_list = []
if 'ports' not in self.service_info:
return port_tuple_list
for port_entry in self.service_info['ports']:
if ':' in str(port_entry):
split = port_entry.split(':')
vip_port = split[0]
container_port = split[1]
if self._is_port_range(vip_port) and self._is_port_range(container_port):
# "8080-8090:9080-9090"
if self._are_port_ranges_same_length(vip_port, container_port):
vip_start, vip_end = self._split_port_range(vip_port)
container_start, container_end = self._split_port_range(
container_port)
# vp = vip_port, cp = container_port; we do +1 on the end range to
# include the last port as well
for vp, cp in zip(range(vip_start, vip_end + 1), range(container_start, container_end + 1)):
port_tuple_list.append((int(vp), int(cp)))
else:
raise ValueError('Port ranges "{}" and "{}" are not equal in length',
vip_port, container_port)
else:
# "8080:8080"
if self._is_number(vip_port) and self._is_number(container_port):
port_tuple_list.append(
(int(vip_port), int(container_port)))
else:
# e.g. invalid entry: 8080-8082:9000
raise ValueError(
'One of the ports is not a valid number or a valid range')
else:
if self._is_port_range(port_entry):
# "3000-3005"
range_start, range_end = self._split_port_range(port_entry)
for i in range(range_start, range_end + 1):
port_tuple_list.append((i, i))
else:
# "3000"
if self._is_number(port_entry):
port_tuple_list.append(
(int(port_entry), int(port_entry)))
else:
raise ValueError(
'One of the ports is not a valid number')
return port_tuple_list
def get_all_vhosts(self):
"""
Gets a dictionary with all vhosts and their ports
"""
vhost_label = 'com.microsoft.acs.kubernetes.vhost'
vhosts_label = 'com.microsoft.acs.kubernetes.vhosts'
all_vhosts = {}
if 'labels' not in self.service_info:
return {}
for label in self.service_info['labels']:
if label.lower() == vhosts_label:
parsed = self._parse_vhost_json(self.service_info['labels'][label])
all_vhosts = self._merge_dicts(all_vhosts, parsed)
elif label.lower() == vhost_label:
vhost_item = self.service_info['labels'][label]
vhost, port = self._parse_vhost_label(vhost_item)
all_vhosts[vhost] = port
else:
if '=' in label:
split = label.split('=')
if split[0].lower() == vhost_label:
# "vhost='www.contoto.com:80'"
vhost, port = self._parse_vhost_label(split[1])
all_vhosts[vhost] = port
elif split[0].lower() == vhosts_label:
# "vhosts=['www.blah.com:80','api.blah.com:81']"
parsed = self._parse_vhost_json(split[1].replace("'", '"'))
all_vhosts = self._merge_dicts(all_vhosts, parsed)
return all_vhosts
def _parse_vhost_label(self, vhost_label):
"""
Parses the vhost label string (host:[port]) and
returns a tuple (host, port)
"""
if not vhost_label:
return None
vhost = vhost_label
vhost_port = 80
if ':' in vhost_label:
vhost_split = vhost_label.split(':')
vhost = vhost_split[0]
vhost_port = vhost_split[1]
return vhost, int(vhost_port)
def _parse_vhost_json(self, vhost_json):
"""
Parse the vhosts JSON value
"""
if not vhost_json:
return None
vhost_items = json.loads(vhost_json)
parsed = {}
for item in vhost_items:
vhost, port = self._parse_vhost_label(item)
parsed[vhost] = port
return parsed
def _is_number(self, input_str):
"""
Checks if the string is a number or not
"""
try:
int(input_str)
return True
except ValueError:
return False
def _is_port_range(self, port_entry):
"""
Checks if the provided string is a port entry or not
"""
if not port_entry:
return False
if '-' in str(port_entry) and str(port_entry).count('-') == 1:
split = port_entry.split('-')
first_part = split[0]
second_part = split[1]
return self._is_number(first_part) and self._is_number(second_part)
return False
def _split_port_range(self, port_range):
"""
Splits a port range and returns a tuple with start and end port
"""
if not self._is_port_range(port_range):
raise ValueError(
'Provided value "%s" is not a port range', port_range)
split = port_range.split('-')
return (int(split[0]), int(split[1]))
def _are_port_ranges_same_length(self, first_range, second_range):
"""
Checks if two port ranges are the same length
"""
if not self._is_port_range(first_range) or not self._is_port_range(second_range):
raise ValueError(
'At least one of the provided values is not a port range')
first_split_start, first_split_end = self._split_port_range(
first_range)
second_split_start, second_split_end = self._split_port_range(
second_range)
return len(range(first_split_start, first_split_end)) == len(range(second_split_start, second_split_end))
def _merge_dicts(self, dict_a, dict_b):
"""
Merges two dictionaries
"""
result = dict_a.copy()
result.update(dict_b)
return result

Просмотреть файл

@ -1,11 +1,12 @@
import json
import logging
import pipes
import re
import json
from portparser import PortParser
class Parser(object):
def __init__(self, group_info, registry_info, service_name, service_info):
self.service_name = service_name
self.service_info = service_info
@ -15,6 +16,8 @@ class Parser(object):
self.service_json = self._get_empty_service_json()
self.service_added = False
self.port_parser = PortParser(self.service_info)
def _add_label(self, name, value):
"""
Adds a label to deployment JSON
@ -31,11 +34,6 @@ class Parser(object):
container['image'] = image
break
# self.deployment_json['spec']['template']['spec']['containers'].append({
# "name": name,
# "image": image
# })
def _add_image_pull_secret(self, name):
"""
Adds image pull secret to the deployment JSON
@ -49,7 +47,6 @@ class Parser(object):
"""
# TODO: Do we always grab the first container? Or do we need
# to pass in the name of the container to find the right one
if not 'ports' in self.deployment_json['spec']['template']['spec']['containers'][0]:
self.deployment_json['spec']['template']['spec']['containers'][0]['ports'] = []
@ -63,6 +60,7 @@ class Parser(object):
if self.service_added:
return json.dumps(self.service_json)
return None
# TODO: This should return an object with everything that needs to be deployed
# e.g. deployment.json, service.json, ???
def get_deployment_json(self):
@ -90,6 +88,11 @@ class Parser(object):
logging.info('Parsing key "%s"', key)
method_to_call = getattr(self, method_name)
method_to_call(key)
vhost = self.port_parser.get_all_vhosts()
if vhost:
# Add the loadbalancer
return json.dumps(self.deployment_json)
def _get_empty_service_json(self):
@ -107,11 +110,11 @@ class Parser(object):
"group_id": self.group_info.get_id(),
"service_name": self.service_name
},
"ports": []
"ports": [],
# TO EXPOSE ON PUBLIC IP: "type": "LoadBalancer"
}
}
def _parse_image(self, key):
"""
Parses the 'image' key
@ -121,23 +124,46 @@ class Parser(object):
def _create_service(self, port_tuple):
# TODO: Do we need to create multiple ports if we have 'expose' and 'ports' key?
if self.service_added:
return
self.service_added = True
self.service_json['spec']['ports'].append({
"name": "port-{}".format(port_tuple[1]),
"protocol": "TCP",
"targetPort": port_tuple[0],
"port": port_tuple[1]
})
def _parse_environment(self, key):
"""
Parses the 'environment' key
"""
containers_key = self.deployment_json['spec']['template']['spec']['containers'][0]
if key in self.service_info:
if 'env' not in containers_key:
containers_key['env'] = []
for env_pair in self.service_info[key]:
if isinstance(self.service_info[key], list):
if '=' in env_pair:
env_split = env_pair.split('=')
env_var_name = env_split[0]
env_var_value = env_split[1]
containers_key['env'].append({"name": env_var_name, "value": env_var_value})
else:
# If environment var does not have a value set
containers_key['env'].append({"name": env_pair, "value": ''})
else:
value = self.service_info[key][env_pair]
if value is None:
containers_key['env'].append({"name": env_pair, "value": ''})
else:
containers_key['env'].append({"name": env_pair, "value": str(value)})
def _parse_expose(self, key):
"""
Parses the 'expose' key
"""
# TODO: How is 'expose' different from 'ports' ???
if key in self.service_info:
private_ports = self._parse_private_ports()
private_ports = self.port_parser.parse_private_ports()
for port_tuple in private_ports:
self._add_container_port(port_tuple[1])
self._create_service(port_tuple)
@ -147,7 +173,7 @@ class Parser(object):
Parses the 'ports' key
"""
if key in self.service_info:
internal_ports = self._parse_internal_ports()
internal_ports = self.port_parser.parse_internal_ports()
for port_tuple in internal_ports:
# TODO: What do we do with host port???
# (hostPort:containerPort)
@ -155,128 +181,33 @@ class Parser(object):
self._add_container_port(port_tuple[1])
self._create_service(port_tuple)
def _parse_private_ports(self):
def _parse_labels(self, key):
"""
Parses the 'expose' key in the docker-compose file and returns a
list of tuples with port numbers. These tuples are used
to create portMappings (blue/green only) in the marathon.json file
Parses the 'labels' key
"""
port_tuple_list = []
if key in self.service_info:
# TODO (peterj): Parse healthcheck labels here
# # Add healthchecks (if any healthcheck labels are set)
# healthcheck_helper = healthcheck.HealthCheck(self.service_info[key])
# healthcheck_json = healthcheck_helper.get_health_check_config()
# if not healthcheck_json is None:
# self.app_json['healthChecks'] = healthcheck_json
if 'expose' not in self.service_info:
return port_tuple_list
for label in self.service_info[key]:
if label.lower().startswith('com.microsoft.acs.kubernetes.vhost'):
continue
for port_entry in self.service_info['expose']:
if self._is_number(port_entry):
port_tuple_list.append((int(port_entry), int(port_entry)))
else:
raise ValueError(
'Port number "%s" is not a valid number', port_entry)
return port_tuple_list
def _parse_internal_ports(self):
"""
Parses the 'ports' key in the docker-compose file and returns a list of
tuples with port numbers. These tuples are used to create
portMappings (blue/green and cyan) in the marathon.json file
"""
port_tuple_list = []
if 'ports' not in self.service_info:
return port_tuple_list
for port_entry in self.service_info['ports']:
if ':' in str(port_entry):
split = port_entry.split(':')
vip_port = split[0]
container_port = split[1]
if self._is_port_range(vip_port) and self._is_port_range(container_port):
# "8080-8090:9080-9090"
if self._are_port_ranges_same_length(vip_port, container_port):
vip_start, vip_end = self._split_port_range(vip_port)
container_start, container_end = self._split_port_range(
container_port)
# vp = vip_port, cp = container_port; we do +1 on the end range to
# include the last port as well
for vp, cp in zip(range(vip_start, vip_end + 1), range(container_start, container_end + 1)):
port_tuple_list.append((int(vp), int(cp)))
else:
raise ValueError('Port ranges "{}" and "{}" are not equal in length',
vip_port, container_port)
if isinstance(self.service_info[key], dict):
self._add_label(label, str(self.service_info[key][label]))
else:
# "8080:8080"
if self._is_number(vip_port) and self._is_number(container_port):
port_tuple_list.append(
(int(vip_port), int(container_port)))
if '=' in label:
label_split = label.split('=')
label_name = label_split[0]
label_value = label_split[1]
self._add_label(label_name, str(label_value))
else:
# e.g. invalid entry: 8080-8082:9000
raise ValueError(
'One of the ports is not a valid number or a valid range')
else:
if self._is_port_range(port_entry):
# "3000-3005"
range_start, range_end = self._split_port_range(port_entry)
for i in range(range_start, range_end + 1):
port_tuple_list.append((i, i))
else:
# "3000"
if self._is_number(port_entry):
port_tuple_list.append(
(int(port_entry), int(port_entry)))
else:
raise ValueError(
'One of the ports is not a valid number')
return port_tuple_list
def _is_number(self, input_str):
"""
Checks if the string is a number or not
"""
try:
int(input_str)
return True
except ValueError:
return False
def _is_port_range(self, port_entry):
"""
Checks if the provided string is a port entry or not
"""
if not port_entry:
return False
if '-' in str(port_entry) and str(port_entry).count('-') == 1:
split = port_entry.split('-')
first_part = split[0]
second_part = split[1]
return self._is_number(first_part) and self._is_number(second_part)
return False
def _split_port_range(self, port_range):
"""
Splits a port range and returns a tuple with start and end port
"""
if not self._is_port_range(port_range):
raise ValueError(
'Provided value "%s" is not a port range', port_range)
split = port_range.split('-')
return (int(split[0]), int(split[1]))
def _are_port_ranges_same_length(self, first_range, second_range):
"""
Checks if two port ranges are the same length
"""
if not self._is_port_range(first_range) or not self._is_port_range(second_range):
raise ValueError(
'At least one of the provided values is not a port range')
first_split_start, first_split_end = self._split_port_range(
first_range)
second_split_start, second_split_end = self._split_port_range(
second_range)
return len(range(first_split_start, first_split_end)) == len(range(second_split_start, second_split_end))
# label without a value
self._add_label(label, '')
def _get_empty_deployment_json(self):
deployment_json = {