Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introduce runtime groups #87

Draft
wants to merge 11 commits into
base: dev
Choose a base branch
from
70 changes: 44 additions & 26 deletions microservices/gatewayApi/v2/routes/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ def write_config(namespace: str) -> object:
ns_attributes = ns_svc.get_namespace_attributes(namespace)

dp = get_data_plane(ns_attributes)
runtime_group_admin = is_allowed_to_manage_runtime_group(ns_attributes)

# Build a list of existing hosts that are outside this namespace
# They become reserved and any conflict will return an error
Expand All @@ -176,7 +177,6 @@ def write_config(namespace: str) -> object:
reserved_hosts.append(host)
reserved_hosts = list(set(reserved_hosts))


dfile = None

if 'configFile' in request.files and not request.files['configFile'].filename == '':
Expand Down Expand Up @@ -231,7 +231,7 @@ def write_config(namespace: str) -> object:
#######################

# Transformation route hosts if in non-prod environment (HOST_TRANSFORM_ENABLED)
host_transformation(namespace, dp, gw_config)
host_transformation(runtime_group_admin, gw_config)

# If there is a tag with a pipeline qualifier (i.e./ ns.<namespace>.dev)
# then add to tags automatically the tag: ns.<namespace>
Expand All @@ -252,6 +252,10 @@ def write_config(namespace: str) -> object:
try:
validate_base_entities(gw_config, ns_attributes)
validate_tags(gw_config, selectTag)

if runtime_group_admin:
validate_runtime_group_config (gw_config, dp)

except Exception as ex:
traceback.print_exc()
log.error("%s - %s" % (namespace, " Tag Validation Errors: %s" % ex))
Expand Down Expand Up @@ -391,7 +395,7 @@ def validate_base_entities(yaml, ns_attributes):
traversables = ['_format_version', '_plugin_configs', 'services', 'upstreams', 'certificates', 'caCertificates']

allow_protected_ns = ns_attributes.get('perm-protected-ns', ['deny'])[0] == 'allow'
if allow_protected_ns:
if allow_protected_ns or is_allowed_to_manage_runtime_group(ns_attributes):
traversables.append('plugins')

for k in yaml:
Expand All @@ -414,6 +418,21 @@ def validate_tags(yaml, required_tag):
if len(errors) != 0:
raise Exception('\n'.join(errors))

def validate_runtime_group_config (yaml, dp):
required_tag = 'dp.%s' % dp
errors = []
for k in yaml:
if k == 'plugins':
for index, item in enumerate(yaml[k]):
if item['enabled'] is True:
errors.append("%s.%s global plugin must have enabled set to false" % (k, item['name']))
if 'tags' in item:
if required_tag not in item['tags']:
errors.append("%s.%s missing required tag %s" % (k, item['name'], required_tag))
else:
errors.append("%s.%s no tags found" % (k, item['name']))
if len(errors) != 0:
raise Exception('\n'.join(errors))

def traverse(source, errors, yaml, required_tag, qualifiers):
traversables = ['services', 'routes', 'plugins', 'upstreams', 'consumers', 'certificates', 'caCertificates']
Expand All @@ -438,30 +457,24 @@ def traverse(source, errors, yaml, required_tag, qualifiers):
traverse("%s.%s.%s" % (source, k, nm), errors, item, required_tag, qualifiers)


def host_transformation(namespace, data_plane, yaml):
log = app.logger

transforms = 0
def host_transformation(runtime_group_admin, yaml):
if 'services' in yaml:
for service in yaml['services']:
if 'routes' in service:
for route in service['routes']:
if 'hosts' in route:
new_hosts = []
for host in route['hosts']:
if is_host_local(host):
new_hosts.append(transform_local_host(data_plane, host))
elif is_host_transform_enabled():
new_hosts.append(transform_host(host))
transforms = transforms + 1
else:
new_hosts.append(host)
new_hosts.append(transform_host(runtime_group_admin, host))
route['hosts'] = new_hosts
log.debug("[%s] Host transformations %d" % (namespace, transforms))

def is_host_local (host):
return host.endswith(".cluster.local")

# Is the namespace responsible for configuring the Runtime Group
def is_allowed_to_manage_runtime_group (ns_attributes):
return ns_attributes.get('perm-admin-runtime-group', [''])[0] == 'allow'

def has_namespace_local_host_permission (ns_attributes):
for domain in ns_attributes.get('perm-domains', ['.api.gov.bc.ca']):
if is_host_local(domain):
Expand All @@ -481,8 +494,10 @@ def transform_local_host(data_plane, host):
name_part = host[:-suffix_len]
return "gw-%s.%s.svc.cluster.local" % (name_part, kube_ns)

def transform_host(host):
if is_host_local(host):
def transform_host(runtime_group_admin, host):
if runtime_group_admin:
return host
elif is_host_local(host):
return host
elif is_host_transform_enabled():
conf = app.config['hostTransformation']
Expand All @@ -494,6 +509,7 @@ def validate_upstream(yaml, ns_attributes, protected_kube_namespaces):
errors = []

allow_protected_ns = ns_attributes.get('perm-protected-ns', ['deny'])[0] == 'allow'
runtime_group_admin = is_allowed_to_manage_runtime_group(ns_attributes)

# A host must not contain a list of protected
if 'services' in yaml:
Expand All @@ -504,24 +520,24 @@ def validate_upstream(yaml, ns_attributes, protected_kube_namespaces):
if u.hostname is None:
errors.append("service upstream has invalid url specified (e1)")
else:
validate_upstream_host(u.hostname, errors, allow_protected_ns, protected_kube_namespaces)
validate_upstream_host(u.hostname, errors, runtime_group_admin, allow_protected_ns, protected_kube_namespaces)
except Exception as e:
errors.append("service upstream has invalid url specified (e2)")

if 'host' in service:
host = service["host"]
validate_upstream_host(host, errors, allow_protected_ns, protected_kube_namespaces)
validate_upstream_host(host, errors, runtime_group_admin, allow_protected_ns, protected_kube_namespaces)

if len(errors) != 0:
raise Exception('\n'.join(errors))


def validate_upstream_host(_host, errors, allow_protected_ns, protected_kube_namespaces):
def validate_upstream_host(_host, errors, runtime_group_admin, allow_protected_ns, protected_kube_namespaces):
host = _host.lower()

restricted = ['localhost', '127.0.0.1', '0.0.0.0']

if host in restricted:
if host in restricted and runtime_group_admin is False:
errors.append("service upstream is invalid (e1)")
if host.endswith('svc'):
partials = host.split('.')
Expand All @@ -530,7 +546,7 @@ def validate_upstream_host(_host, errors, allow_protected_ns, protected_kube_nam
errors.append("service upstream is invalid (e2)")
elif partials[1] in protected_kube_namespaces and allow_protected_ns is False:
errors.append("service upstream is invalid (e3)")
if host.endswith('svc.cluster.local'):
elif host.endswith('svc.cluster.local'):
partials = host.split('.')
# get the namespace, and make sure it is not in the protected_kube_namespaces list
if len(partials) != 5:
Expand All @@ -547,6 +563,8 @@ def update_routes_check(yaml):
def validate_hosts(yaml, reserved_hosts, ns_attributes):
errors = []

runtime_group_admin = is_allowed_to_manage_runtime_group(ns_attributes)

allowed_domains = []
for domain in ns_attributes.get('perm-domains', ['.api.gov.bc.ca']):
allowed_domains.append("%s" % domain)
Expand All @@ -558,14 +576,14 @@ def validate_hosts(yaml, reserved_hosts, ns_attributes):
for route in service['routes']:
if 'hosts' in route:
for host in route['hosts']:
if host in reserved_hosts:
if transform_host(runtime_group_admin, host) in reserved_hosts:
errors.append("service.%s.route.%s The host is already used in another namespace '%s'" % (
service['name'], route['name'], host))
if host_valid(host) is False:
errors.append("Host not passing DNS-952 validation '%s'" % host)
if validate_local_host(host) is False:
errors.append("Host failed validation for data plane '%s'" % host)
if host_ends_with_one_of_list(host, allowed_domains) is False:
if host_ends_with_one_of_list(runtime_group_admin, host, allowed_domains) is False:
errors.append("Host invalid: %s %s. Route hosts must end with one of [%s] for this namespace." % (
route['name'], host, ','.join(allowed_domains)))
else:
Expand All @@ -576,9 +594,9 @@ def validate_hosts(yaml, reserved_hosts, ns_attributes):
raise Exception('\n'.join(errors))


def host_ends_with_one_of_list(a_str, a_list):
def host_ends_with_one_of_list(runtime_group_admin, a_str, a_list):
for item in a_list:
if a_str.endswith(transform_host(item)):
if a_str.endswith(transform_host(runtime_group_admin, item)):
return True
return False

Expand Down
3 changes: 2 additions & 1 deletion microservices/kubeApi/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from fastapi.responses import JSONResponse
from fastapi.exceptions import HTTPException
from starlette.responses import HTMLResponse
from routers import routes
from routers import routes, noop
from config import settings
import logging
import logging.config
Expand Down Expand Up @@ -37,6 +37,7 @@
app = FastAPI(title="GWA Kubernetes API",
description="Description: API to create resources in Openshift using Kubectl",
version="1.0.0")
app.include_router(noop.router)
app.include_router(routes.router)

logger = logging.getLogger(__name__)
Expand Down
29 changes: 29 additions & 0 deletions microservices/kubeApi/routers/noop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from fastapi import APIRouter, Depends, Request
from pydantic.main import BaseModel
from starlette.responses import Response
from auth.basic_auth import verify_credentials

router = APIRouter(
prefix="/noop",
tags=["routes"],
responses={404: {"description": "Not found"}},
)


class OCPRoute(BaseModel):
hosts: list
select_tag: str
ns_attributes: dict


@router.put("/namespaces/{namespace}/routes", status_code=201, dependencies=[Depends(verify_credentials)])
def add_routes(namespace: str, route: OCPRoute):
return {"message": "created"}

@router.delete("/namespaces/{namespace}/routes/{name}", status_code=204, dependencies=[Depends(verify_credentials)])
def delete_route(name: str):
return Response(status_code=204)

@router.post("/namespaces/{namespace}/routes/sync", status_code=200, dependencies=[Depends(verify_credentials)])
async def verify_and_create_routes(namespace: str, request: Request):
return Response(status_code=200)