| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you 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. |
| # |
| import kopf, logging, json, time |
| import os |
| import openserverless.kube as kube |
| import openserverless.kustomize as kus |
| import openserverless.config as cfg |
| import openserverless.openwhisk as openwhisk |
| import openserverless.apihost_util as apihost_util |
| import openserverless.util as util |
| import openserverless.operator_util as operator_util |
| |
| from openserverless.ingress_data import IngressData |
| from openserverless.route_data import RouteData |
| from openserverless.user_metadata import UserMetadata |
| |
| def ingress_secret_name(namespace, ingress="apihost"): |
| return f"{namespace}-{ingress}-crt" |
| |
| def api_ingress_name(namespace, ingress="apihost"): |
| return namespace == "openserverless" and ingress or f"{namespace}-{ingress}-api-ingress" |
| |
| def api_route_name(namespace,route="apihost"): |
| return namespace == "openserverless" and route or f"{namespace}-{route}-api-route" |
| |
| def api_secret_name(namespace): |
| return f"{namespace}-crt" |
| |
| def api_middleware_ingress_name(namespace,ingress): |
| return f"{namespace}-{ingress}-api-ingress-add-prefix" |
| |
| def deploy_info_route(apihost,namespace): |
| info = RouteData(apihost) |
| info.with_route_name(api_route_name(namespace,"apihost-info")) |
| info.with_service_name("controller-ip") |
| info.with_service_kind("Service") |
| info.with_service_port("8080") |
| info.with_context_path("/api/info") |
| info.with_rewrite_target("/") |
| |
| logging.info(f"*** configuring route for apihost-info") |
| path_to_template_yaml = info.render_template(namespace) |
| res = kube.kubectl("apply", "-f",path_to_template_yaml) |
| os.remove(path_to_template_yaml) |
| return res |
| |
| def deploy_api_routes(apihost,namespace,should_create_www=False): |
| logging.info(f"**** configuring openshift route based endpoint for apihost {apihost}") |
| |
| api = RouteData(apihost) |
| api.with_route_name(api_route_name(namespace,"apihost")) |
| api.with_service_name("controller-ip") |
| api.with_service_kind("Service") |
| api.with_service_port("8080") |
| api.with_context_path("/api/v1") |
| api.with_rewrite_target("/api/v1") |
| |
| my = RouteData(apihost) |
| my.with_route_name(api_route_name(namespace,"apihost-my")) |
| my.with_service_name("controller-ip") |
| my.with_service_kind("Service") |
| my.with_service_port("8080") |
| my.with_context_path("/api/my") |
| my.with_rewrite_target(f"/api/v1/web/namespace/{namespace}") |
| |
| logging.info(f"*** configuring route for apihost") |
| path_to_template_yaml = api.render_template(namespace) |
| res = kube.kubectl("apply", "-f",path_to_template_yaml) |
| os.remove(path_to_template_yaml) |
| |
| logging.info(f"*** configuring route for apihost-my") |
| path_to_template_yaml = my.render_template(namespace) |
| res += kube.kubectl("apply", "-f",path_to_template_yaml) |
| os.remove(path_to_template_yaml) |
| |
| if should_create_www: |
| www_my = RouteData(apihost_util.append_prefix_to_url(apihost,"www")) |
| www_my.with_route_name(api_route_name(namespace,"apihost-www-my")) |
| www_my.with_service_name("controller-ip") |
| www_my.with_service_kind("Service") |
| www_my.with_service_port("8080") |
| www_my.with_context_path("/api/my") |
| www_my.with_rewrite_target(f"/api/v1/web/namespace/{namespace}") |
| |
| logging.info(f"*** configuring route for apihost-www-my") |
| path_to_template_yaml = www_my.render_template(namespace) |
| res += kube.kubectl("apply", "-f",path_to_template_yaml) |
| os.remove(path_to_template_yaml) |
| |
| return res |
| |
| def deploy_info_ingress(apihost, namespace): |
| res = "" |
| info = IngressData(apihost) |
| info.with_ingress_name(api_ingress_name(namespace,"apihost-info")) |
| info.with_secret_name(api_secret_name(namespace)) |
| info.with_context_path("/api/info") |
| info.with_context_regexp("(/|$)(.*)") |
| info.with_rewrite_target("/$2") |
| info.with_service_name("controller") |
| info.with_service_port("3233") |
| info.with_middleware_ingress_name(api_middleware_ingress_name(namespace,"apihost-info")) |
| |
| if info.requires_traefik_middleware(): |
| logging.info("*** configuring traefik middleware for apihost-info ingress") |
| path_to_template_yaml = info.render_traefik_middleware_template(namespace) |
| res += kube.kubectl("apply", "-f",path_to_template_yaml) |
| os.remove(path_to_template_yaml) |
| |
| logging.info(f"*** configuring static ingress for apihost-info") |
| path_to_template_yaml = info.render_template(namespace) |
| res += kube.kubectl("apply", "-f",path_to_template_yaml) |
| os.remove(path_to_template_yaml) |
| |
| return res |
| |
| |
| def deploy_api_ingresses(apihost, namespace,should_create_www=False): |
| logging.info(f"**** configuring ingresses based endpoint for apihost {apihost}") |
| res = "" |
| api = IngressData(apihost) |
| api.with_ingress_name(api_ingress_name(namespace,"apihost")) |
| api.with_secret_name(api_secret_name(namespace)) |
| api.with_context_path("/api/v1") |
| api.with_context_regexp("(/|$)(.*)") |
| api.with_rewrite_target("/api/v1/$2") |
| api.with_service_name("controller") |
| api.with_service_port("3233") |
| api.with_middleware_ingress_name(api_middleware_ingress_name(namespace,"apihost")) |
| |
| my = IngressData(apihost) |
| my.with_ingress_name(api_ingress_name(namespace,"apihost-my")) |
| my.with_secret_name(api_secret_name(namespace)) |
| my.with_context_path("/api/my") |
| my.with_context_regexp("(/|$)(.*)") |
| my.with_rewrite_target(f"/api/v1/web/{namespace}/$2") |
| my.with_service_name("controller") |
| my.with_service_port("3233") |
| my.with_middleware_ingress_name(api_middleware_ingress_name(namespace,"apihost-my")) |
| |
| if api.requires_traefik_middleware(): |
| logging.info("*** configuring traefik middleware for apihost ingress") |
| path_to_template_yaml = api.render_traefik_middleware_template(namespace) |
| res += kube.kubectl("apply", "-f",path_to_template_yaml) |
| os.remove(path_to_template_yaml) |
| |
| if my.requires_traefik_middleware(): |
| logging.info("*** configuring traefik middleware for apihost-my ingress") |
| path_to_template_yaml = my.render_traefik_middleware_template(namespace) |
| res += kube.kubectl("apply", "-f",path_to_template_yaml) |
| os.remove(path_to_template_yaml) |
| |
| logging.info(f"*** configuring static ingress for apihost") |
| path_to_template_yaml = api.render_template(namespace) |
| res += kube.kubectl("apply", "-f",path_to_template_yaml) |
| os.remove(path_to_template_yaml) |
| |
| logging.info(f"*** configuring static ingress for apihost-my") |
| path_to_template_yaml = my.render_template(namespace) |
| res += kube.kubectl("apply", "-f",path_to_template_yaml) |
| os.remove(path_to_template_yaml) |
| |
| if should_create_www: |
| www_my = IngressData(apihost_util.append_prefix_to_url(apihost,"www")) |
| www_my.with_ingress_name(api_ingress_name(namespace,"apihost-www-my")) |
| www_my.with_secret_name(api_secret_name(namespace)+"-www") |
| www_my.with_context_path("/api/my") |
| www_my.with_context_regexp("(/|$)(.*)") |
| www_my.with_rewrite_target(f"/api/v1/web/{namespace}/$2") |
| www_my.with_service_name("controller") |
| www_my.with_service_port("3233") |
| www_my.with_middleware_ingress_name(api_middleware_ingress_name(namespace,"apihost-www-my")) |
| |
| if www_my.requires_traefik_middleware(): |
| logging.info("*** configuring traefik middleware for apihost-www-my ingress") |
| path_to_template_yaml = www_my.render_traefik_middleware_template(namespace) |
| res += kube.kubectl("apply", "-f",path_to_template_yaml) |
| os.remove(path_to_template_yaml) |
| |
| logging.info(f"*** configuring static ingress for apihost-www-my") |
| path_to_template_yaml = www_my.render_template(namespace) |
| res += kube.kubectl("apply", "-f",path_to_template_yaml) |
| os.remove(path_to_template_yaml) |
| |
| return res |
| |
| def create(owner=None): |
| runtime = cfg.get('openserverless.kube') |
| apihost = apihost_util.get_apihost(runtime) |
| hostname = apihost_util.extract_hostname(apihost) |
| should_create_www = "www" not in hostname and runtime not in ["kind"] |
| |
| logging.info(f"*** Saving configuration for OpenWishk apihost={apihost}") |
| openwhisk.annotate(f"apihost={apihost}") |
| cfg.put("config.apihost", apihost) |
| |
| if runtime == 'openshift': |
| res = deploy_info_route(apihost,"openserverless") |
| return deploy_api_routes(apihost,"openserverless",should_create_www) |
| else: |
| res = deploy_info_ingress(apihost,"openserverless") |
| return deploy_api_ingresses(apihost,"openserverless",should_create_www) |
| |
| def delete(owner=None): |
| """ |
| undeploys ingresses for openserverless apihost |
| """ |
| logging.info(f"*** removing ingresses for openserverless apihost") |
| namespace = "openserverless" |
| runtime = cfg.get('openserverless.kube') |
| ingress_class = util.get_ingress_class(runtime) |
| apihost = apihost_util.get_apihost(runtime) |
| hostname = apihost_util.extract_hostname(apihost) |
| should_delete_www = "www" not in hostname and runtime not in ["kind"] |
| |
| try: |
| res = "" |
| if(runtime=='openshift'): |
| res = kube.kubectl("delete", "route",api_route_name(namespace,"apihost")) |
| res += kube.kubectl("delete", "route",api_route_name(namespace,"apihost-my")) |
| res += kube.kubectl("delete", "route",api_route_name(namespace,"apihost-info")) |
| |
| if should_delete_www: |
| res += kube.kubectl("delete", "route",api_route_name(namespace,"apihost-www-my")) |
| return res |
| |
| if(ingress_class == 'traefik'): |
| middleware_resource = util.get_traefik_middleware_resource() |
| res = kube.kubectl("delete", middleware_resource,api_middleware_ingress_name(namespace,"apihost")) |
| res += kube.kubectl("delete", middleware_resource,api_middleware_ingress_name(namespace,"apihost-my")) |
| res += kube.kubectl("delete", middleware_resource,api_middleware_ingress_name(namespace,"apihost-info")) |
| if should_delete_www: |
| res += kube.kubectl("delete", middleware_resource,api_middleware_ingress_name(namespace,"apihost-www-my")) |
| |
| res += kube.kubectl("delete", "ingress",api_ingress_name(namespace,"apihost")) |
| res += kube.kubectl("delete", "ingress",api_ingress_name(namespace,"apihost-my")) |
| res += kube.kubectl("delete", "ingress",api_ingress_name(namespace,"apihost-info")) |
| if should_delete_www: |
| res += kube.kubectl("delete", "ingress",api_ingress_name(namespace,"apihost-www-my")) |
| return res |
| except Exception as e: |
| logging.warn(e) |
| return False |
| |
| def patch(status, action, owner=None): |
| """ |
| Called by the operator patcher to create/update/delete endpoint for apihost |
| """ |
| try: |
| logging.info(f"*** handling request to {action} endpoint") |
| if action == 'create': |
| msg = create(owner) |
| operator_util.patch_operator_status(status,'endpoint','on') |
| elif action == 'delete': |
| msg = delete(owner) |
| operator_util.patch_operator_status(status,'endpoint','off') |
| else: |
| msg = create(owner) |
| operator_util.patch_operator_status(status,'endpoint','updated') |
| |
| logging.info(msg) |
| logging.info(f"*** handled request to {action} endpoint") |
| except Exception as e: |
| logging.error('*** failed to update endpoint: %s' % e) |
| operator_util.patch_operator_status(status,'endpoint','error') |
| |
| def create_ow_api_endpoint(ucfg, user_metadata: UserMetadata, owner=None): |
| """ |
| deploy ingresses to access a generic user api and ow api in a CORS friendly way |
| currently this is not supported for openshift |
| """ |
| runtime = cfg.get('openserverless.kube') |
| namespace = ucfg.get("namespace") |
| apihost = ucfg.get("apihost") or "auto" |
| hostname = apihost_util.get_user_static_hostname(runtime, namespace, apihost) |
| logging.debug(f"using hostname {hostname} to configure access to user openwhisk api") |
| |
| try: |
| apihost_url = apihost_util.get_user_static_url(runtime, hostname) |
| my_url = apihost_util.get_user_api_url(runtime, hostname,"api/my") |
| user_metadata.add_metadata("USER_REST_API_URL",my_url) |
| user_metadata.add_metadata("USER_V1_API_URL",apihost_url) |
| |
| if runtime == 'openshift': |
| return deploy_api_routes(apihost_url, namespace) |
| else: |
| return deploy_api_ingresses(apihost_url, namespace) |
| except Exception as e: |
| logging.warn(e) |
| return False |
| |
| def delete_ow_api_endpoint(ucfg): |
| """ |
| undeploy the ingresses |
| """ |
| namespace = ucfg.get("namespace") |
| runtime = cfg.get('openserverless.kube') |
| logging.info(f"*** removing api endpoint for {namespace}") |
| runtime = cfg.get('openserverless.kube') |
| ingress_class = util.get_ingress_class(runtime) |
| |
| try: |
| res = "" |
| if(runtime=='openshift'): |
| res = kube.kubectl("delete", "route",api_route_name(namespace,"apihost")) |
| res += kube.kubectl("delete", "route",api_route_name(namespace,"apihost-my")) |
| return res |
| |
| if(ingress_class == 'traefik'): |
| middleware_resource = util.get_traefik_middleware_resource() |
| res += kube.kubectl("delete", middleware_resource,api_middleware_ingress_name(namespace,"apihost")) |
| res += kube.kubectl("delete", middleware_resource,api_middleware_ingress_name(namespace,"apihost-my")) |
| |
| res += kube.kubectl("delete", "ingress",api_ingress_name(namespace,"apihost")) |
| res += kube.kubectl("delete", "ingress",api_ingress_name(namespace,"apihost-my")) |
| return res |
| except Exception as e: |
| logging.warn(e) |
| return False |