Files
community.okd/plugins/modules/openshift_route.py
Mike Graves f60ded17b0 Update to work with k8s 2.0 (#93)
* Update to work with k8s 2.0

This makes the necessary changes to get the collection working with
kubernetes.core 2.0. The biggest changes here will be switching from the
openshift client to the kubernetes client, and dropping Python 2
support.

* Install kubernetes not openshift

* Add changelog fragment
2021-06-21 14:41:56 -04:00

547 lines
20 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Red Hat
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
# STARTREMOVE (downstream)
DOCUMENTATION = r'''
module: openshift_route
short_description: Expose a Service as an OpenShift Route.
version_added: "0.3.0"
author: "Fabian von Feilitzsch (@fabianvf)"
description:
- Looks up a Service and creates a new Route based on it.
- Analogous to `oc expose` and `oc create route` for creating Routes, but does not support creating Services.
- For creating Services from other resources, see kubernetes.core.k8s_expose
extends_documentation_fragment:
- kubernetes.core.k8s_auth_options
- kubernetes.core.k8s_wait_options
- kubernetes.core.k8s_state_options
requirements:
- "python >= 3.6"
- "kubernetes >= 12.0.0"
- "PyYAML >= 3.11"
options:
service:
description:
- The name of the service to expose.
- Required when I(state) is not absent.
type: str
aliases: ['svc']
namespace:
description:
- The namespace of the resource being targeted.
- The Route will be created in this namespace as well.
required: yes
type: str
labels:
description:
- Specify the labels to apply to the created Route.
- 'A set of key: value pairs.'
type: dict
name:
description:
- The desired name of the Route to be created.
- Defaults to the value of I(service)
type: str
hostname:
description:
- The hostname for the Route.
type: str
path:
description:
- The path for the Route
type: str
wildcard_policy:
description:
- The wildcard policy for the hostname.
- Currently only Subdomain is supported.
- If not provided, the default of None will be used.
choices:
- Subdomain
type: str
port:
description:
- Name or number of the port the Route will route traffic to.
type: str
tls:
description:
- TLS configuration for the newly created route.
- Only used when I(termination) is set.
type: dict
suboptions:
ca_certificate:
description:
- Path to a CA certificate file on the target host.
- Not supported when I(termination) is set to passthrough.
type: str
certificate:
description:
- Path to a certificate file on the target host.
- Not supported when I(termination) is set to passthrough.
type: str
destination_ca_certificate:
description:
- Path to a CA certificate file used for securing the connection.
- Only used when I(termination) is set to reencrypt.
- Defaults to the Service CA.
type: str
key:
description:
- Path to a key file on the target host.
- Not supported when I(termination) is set to passthrough.
type: str
insecure_policy:
description:
- Sets the InsecureEdgeTerminationPolicy for the Route.
- Not supported when I(termination) is set to reencrypt.
- When I(termination) is set to passthrough, only redirect is supported.
- If not provided, insecure traffic will be disallowed.
type: str
choices:
- allow
- redirect
- disallow
default: disallow
termination:
description:
- The termination type of the Route.
- If left empty no termination type will be set, and the route will be insecure.
- When set to insecure I(tls) will be ignored.
choices:
- edge
- passthrough
- reencrypt
- insecure
default: insecure
type: str
'''
EXAMPLES = r'''
- name: Create hello-world deployment
community.okd.k8s:
definition:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-kubernetes
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: hello-kubernetes
template:
metadata:
labels:
app: hello-kubernetes
spec:
containers:
- name: hello-kubernetes
image: paulbouwer/hello-kubernetes:1.8
ports:
- containerPort: 8080
- name: Create Service for the hello-world deployment
community.okd.k8s:
definition:
apiVersion: v1
kind: Service
metadata:
name: hello-kubernetes
namespace: default
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: hello-kubernetes
- name: Expose the insecure hello-world service externally
community.okd.openshift_route:
service: hello-kubernetes
namespace: default
insecure_policy: allow
register: route
'''
RETURN = r'''
result:
description:
- The Route object that was created or updated. Will be empty in the case of deletion.
returned: success
type: complex
contains:
apiVersion:
description: The versioned schema of this representation of an object.
returned: success
type: str
kind:
description: Represents the REST resource this object represents.
returned: success
type: str
metadata:
description: Standard object metadata. Includes name, namespace, annotations, labels, etc.
returned: success
type: complex
contains:
name:
description: The name of the created Route
type: str
namespace:
description: The namespace of the create Route
type: str
spec:
description: Specification for the Route
returned: success
type: complex
contains:
host:
description: Host is an alias/DNS that points to the service.
type: str
path:
description: Path that the router watches for, to route traffic for to the service.
type: str
port:
description: Defines a port mapping from a router to an endpoint in the service endpoints.
type: complex
contains:
targetPort:
description: The target port on pods selected by the service this route points to.
type: str
tls:
description: Defines config used to secure a route and provide termination.
type: complex
contains:
caCertificate:
description: Provides the cert authority certificate contents.
type: str
certificate:
description: Provides certificate contents.
type: str
destinationCACertificate:
description: Provides the contents of the ca certificate of the final destination.
type: str
insecureEdgeTerminationPolicy:
description: Indicates the desired behavior for insecure connections to a route.
type: str
key:
description: Provides key file contents.
type: str
termination:
description: Indicates termination type.
type: str
to:
description: Specifies the target that resolve into endpoints.
type: complex
contains:
kind:
description: The kind of target that the route is referring to. Currently, only 'Service' is allowed.
type: str
name:
description: Name of the service/target that is being referred to. e.g. name of the service.
type: str
weight:
description: Specifies the target's relative weight against other target reference objects.
type: int
wildcardPolicy:
description: Wildcard policy if any for the route.
type: str
status:
description: Current status details for the Route
returned: success
type: complex
contains:
ingress:
description: List of places where the route may be exposed.
type: complex
contains:
conditions:
description: Array of status conditions for the Route ingress.
type: complex
contains:
type:
description: The type of the condition. Currently only 'Ready'.
type: str
status:
description: The status of the condition. Can be True, False, Unknown.
type: str
host:
description: The host string under which the route is exposed.
type: str
routerCanonicalHostname:
description: The external host name for the router that can be used as a CNAME for the host requested for this route. May not be set.
type: str
routerName:
description: A name chosen by the router to identify itself.
type: str
wildcardPolicy:
description: The wildcard policy that was allowed where this route is exposed.
type: str
duration:
description: elapsed time of task in seconds
returned: when C(wait) is true
type: int
sample: 48
'''
# ENDREMOVE (downstream)
import copy
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
try:
from ansible_collections.kubernetes.core.plugins.module_utils.common import K8sAnsibleMixin, get_api_client
from ansible_collections.kubernetes.core.plugins.module_utils.args_common import (
AUTH_ARG_SPEC, WAIT_ARG_SPEC, COMMON_ARG_SPEC
)
HAS_KUBERNETES_COLLECTION = True
except ImportError as e:
HAS_KUBERNETES_COLLECTION = False
k8s_collection_import_exception = e
K8S_COLLECTION_ERROR = traceback.format_exc()
K8sAnsibleMixin = object
AUTH_ARG_SPEC = WAIT_ARG_SPEC = COMMON_ARG_SPEC = {}
try:
from kubernetes.dynamic.exceptions import DynamicApiError, NotFoundError
except ImportError:
pass
class OpenShiftRoute(K8sAnsibleMixin):
def __init__(self):
self.module = AnsibleModule(
argument_spec=self.argspec,
supports_check_mode=True,
)
self.fail_json = self.module.fail_json
if not HAS_KUBERNETES_COLLECTION:
self.module.fail_json(
msg="The kubernetes.core collection must be installed",
exception=K8S_COLLECTION_ERROR,
error=to_native(k8s_collection_import_exception)
)
super(OpenShiftRoute, self).__init__(self.module)
self.params = self.module.params
# TODO: should probably make it so that at least some of these aren't required for perform_action to work
# Or at least explicitly pass them in
self.append_hash = False
self.apply = False
self.check_mode = self.module.check_mode
self.warnings = []
self.params['merge_type'] = None
@property
def argspec(self):
spec = copy.deepcopy(AUTH_ARG_SPEC)
spec.update(copy.deepcopy(WAIT_ARG_SPEC))
spec.update(copy.deepcopy(COMMON_ARG_SPEC))
spec['service'] = dict(type='str', aliases=['svc'])
spec['namespace'] = dict(required=True, type='str')
spec['labels'] = dict(type='dict')
spec['name'] = dict(type='str')
spec['hostname'] = dict(type='str')
spec['path'] = dict(type='str')
spec['wildcard_policy'] = dict(choices=['Subdomain'], type='str')
spec['port'] = dict(type='str')
spec['tls'] = dict(type='dict', options=dict(
ca_certificate=dict(type='str'),
certificate=dict(type='str'),
destination_ca_certificate=dict(type='str'),
key=dict(type='str', no_log=False),
insecure_policy=dict(type='str', choices=['allow', 'redirect', 'disallow'], default='disallow'),
))
spec['termination'] = dict(choices=['edge', 'passthrough', 'reencrypt', 'insecure'], default='insecure')
return spec
def execute_module(self):
self.client = get_api_client(self.module)
v1_routes = self.find_resource('Route', 'route.openshift.io/v1', fail=True)
service_name = self.params.get('service')
namespace = self.params['namespace']
termination_type = self.params.get('termination')
if termination_type == 'insecure':
termination_type = None
state = self.params.get('state')
if state != 'absent' and not service_name:
self.fail_json("If 'state' is not 'absent' then 'service' must be provided")
# We need to do something a little wonky to wait if the user doesn't supply a custom condition
custom_wait = self.params.get('wait') and not self.params.get('wait_condition') and state != 'absent'
if custom_wait:
# Don't use default wait logic in perform_action
self.params['wait'] = False
route_name = self.params.get('name') or service_name
labels = self.params.get('labels')
hostname = self.params.get('hostname')
path = self.params.get('path')
wildcard_policy = self.params.get('wildcard_policy')
port = self.params.get('port')
if termination_type and self.params.get('tls'):
tls_ca_cert = self.params['tls'].get('ca_certificate')
tls_cert = self.params['tls'].get('certificate')
tls_dest_ca_cert = self.params['tls'].get('destination_ca_certificate')
tls_key = self.params['tls'].get('key')
tls_insecure_policy = self.params['tls'].get('insecure_policy')
if tls_insecure_policy == 'disallow':
tls_insecure_policy = None
else:
tls_ca_cert = tls_cert = tls_dest_ca_cert = tls_key = tls_insecure_policy = None
route = {
'apiVersion': 'route.openshift.io/v1',
'kind': 'Route',
'metadata': {
'name': route_name,
'namespace': namespace,
'labels': labels,
},
'spec': {}
}
if state != 'absent':
route['spec'] = self.build_route_spec(
service_name, namespace,
port=port,
wildcard_policy=wildcard_policy,
hostname=hostname,
path=path,
termination_type=termination_type,
tls_insecure_policy=tls_insecure_policy,
tls_ca_cert=tls_ca_cert,
tls_cert=tls_cert,
tls_key=tls_key,
tls_dest_ca_cert=tls_dest_ca_cert,
)
result = self.perform_action(v1_routes, route)
timeout = self.params.get('wait_timeout')
sleep = self.params.get('wait_sleep')
if custom_wait:
success, result['result'], result['duration'] = self._wait_for(v1_routes, route_name, namespace, wait_predicate, sleep, timeout, state)
self.module.exit_json(**result)
def build_route_spec(self, service_name, namespace, port=None, wildcard_policy=None, hostname=None, path=None, termination_type=None,
tls_insecure_policy=None, tls_ca_cert=None, tls_cert=None, tls_key=None, tls_dest_ca_cert=None):
v1_services = self.find_resource('Service', 'v1', fail=True)
try:
target_service = v1_services.get(name=service_name, namespace=namespace)
except NotFoundError:
if not port:
self.module.fail_json(msg="You need to provide the 'port' argument when exposing a non-existent service")
target_service = None
except DynamicApiError as exc:
self.module.fail_json(msg='Failed to retrieve service to be exposed: {0}'.format(exc.body),
error=exc.status, status=exc.status, reason=exc.reason)
except Exception as exc:
self.module.fail_json(msg='Failed to retrieve service to be exposed: {0}'.format(to_native(exc)),
error='', status='', reason='')
route_spec = {
'tls': {},
'to': {
'kind': 'Service',
'name': service_name,
},
'port': {
'targetPort': self.set_port(target_service, port),
},
'wildcardPolicy': wildcard_policy
}
# Want to conditionally add these so we don't overwrite what is automically added when nothing is provided
if termination_type:
route_spec['tls'] = dict(termination=termination_type.capitalize())
if tls_insecure_policy:
if termination_type == 'edge':
route_spec['tls']['insecureEdgeTerminationPolicy'] = tls_insecure_policy.capitalize()
elif termination_type == 'passthrough':
if tls_insecure_policy != 'redirect':
self.module.fail_json("'redirect' is the only supported insecureEdgeTerminationPolicy for passthrough routes")
route_spec['tls']['insecureEdgeTerminationPolicy'] = tls_insecure_policy.capitalize()
elif termination_type == 'reencrypt':
self.module.fail_json("'tls.insecure_policy' is not supported with reencrypt routes")
else:
route_spec['tls']['insecureEdgeTerminationPolicy'] = None
if tls_ca_cert:
if termination_type == 'passthrough':
self.module.fail_json("'tls.ca_certificate' is not supported with passthrough routes")
route_spec['tls']['caCertificate'] = tls_ca_cert
if tls_cert:
if termination_type == 'passthrough':
self.module.fail_json("'tls.certificate' is not supported with passthrough routes")
route_spec['tls']['certificate'] = tls_cert
if tls_key:
if termination_type == 'passthrough':
self.module.fail_json("'tls.key' is not supported with passthrough routes")
route_spec['tls']['key'] = tls_key
if tls_dest_ca_cert:
if termination_type != 'reencrypt':
self.module.fail_json("'destination_certificate' is only valid for reencrypt routes")
route_spec['tls']['destinationCACertificate'] = tls_dest_ca_cert
else:
route_spec['tls'] = None
if hostname:
route_spec['host'] = hostname
if path:
route_spec['path'] = path
return route_spec
def set_port(self, service, port_arg):
if port_arg:
return port_arg
for p in service.spec.ports:
if p.protocol == 'TCP':
if p.name is not None:
return p.name
return p.targetPort
return None
def wait_predicate(route):
if not(route.status and route.status.ingress):
return False
for ingress in route.status.ingress:
match = [x for x in ingress.conditions if x.type == 'Admitted']
if not match:
return False
match = match[0]
if match.status != "True":
return False
return True
def main():
OpenShiftRoute().execute_module()
if __name__ == '__main__':
main()