1052 lines
46 KiB
Python
1052 lines
46 KiB
Python
|
|
# ============================================================
|
|||
|
|
# Система: Единая библиотека, Центр ИИ НИУ ВШЭ
|
|||
|
|
# Модуль: APIComponent
|
|||
|
|
# Авторы: Полежаев В.А., Хританков А.С.
|
|||
|
|
# Дата создания: 2025 г.
|
|||
|
|
# ============================================================
|
|||
|
|
import base64
|
|||
|
|
import posixpath
|
|||
|
|
import secrets
|
|||
|
|
import string
|
|||
|
|
import urllib.parse
|
|||
|
|
|
|||
|
|
import kopf
|
|||
|
|
import yaml
|
|||
|
|
from kubernetes.client import ApiClient
|
|||
|
|
|
|||
|
|
from apicmp.config import UNIP_API_CMP_CORS_ENABLED, UNIP_API_CMP_APPS_CORS_ENABLED, UNIP_API_CMP_CORS_ALLOW_METHODS, \
|
|||
|
|
UNIP_API_CMP_CORS_ALLOW_HEADERS, UNIP_API_CMP_CORS_EXPOSE_HEADERS, UNIP_API_CMP_CORS_ALLOW_ORIGIN, \
|
|||
|
|
UNIP_API_CMP_CORS_MAX_AGE
|
|||
|
|
from apicmp.jinja import basic_jinja_env, api_cmp_jinja_env
|
|||
|
|
from auth import create_htpasswd_password
|
|||
|
|
from basic_resources.deployments import delete_deployment_if_exists, create_or_update_deployment
|
|||
|
|
from basic_resources.ingresses import delete_ingress_if_exists, create_or_update_ingress
|
|||
|
|
from basic_resources.network_policies import delete_network_policy_if_exists, create_or_update_network_policy
|
|||
|
|
from basic_resources.secrets import prepare_secret_manifest, delete_secret_if_exists, \
|
|||
|
|
create_or_update_secret
|
|||
|
|
from basic_resources.services import delete_svc_if_exists, create_or_update_service
|
|||
|
|
from bound import not_dev_namespace
|
|||
|
|
from cmplink.basic_auth_link import get_basic_auth_joint_secret_name, create_or_update_basic_auth_joint_secret
|
|||
|
|
from cmplink.keycloak_group_link import get_oauth2_proxy_deployment_name, create_or_update_groups_joint_secret, \
|
|||
|
|
get_groups_joint_secret_name
|
|||
|
|
from config import UNIP_DOMAIN, PIPELINES_SERVICE_PORT, PIPELINES_EXTERNAL_SERVICE_NAME, OIDC_END_USERS_CLIENT_SECRET, \
|
|||
|
|
OIDC_END_USERS_ISSUER_URL, OIDC_END_USERS_CLIENT_ID, OIDC_END_USERS_COOKIE_SECRET, OIDC_ROBOTS_AUD, \
|
|||
|
|
OIDC_ROBOTS_ISSUER_URL, OIDC_END_USERS_AUD
|
|||
|
|
from exceptions import InputValidationPermanentError
|
|||
|
|
from exp_pipeline.client_config import INTERNAL_API_USER_ID
|
|||
|
|
from exp_pipeline.pipeline import INTERNAL_API_SECRET_SUFFIX
|
|||
|
|
from kopf_k8s_client import api_client
|
|||
|
|
from parse import get_user_namespace
|
|||
|
|
|
|||
|
|
ML_CMP_SERVICE_PORT = 80
|
|||
|
|
FILES_SERVICE_PORT = 80
|
|||
|
|
OAUTH2_PROXY_SERVICE_PORT = 4180
|
|||
|
|
OAUTH2_PROXY_CONTAINER_PORT = 4180
|
|||
|
|
|
|||
|
|
INTERNAL_API_BA_SECRET_SUFFIX = '-internal-ba-cred'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def join_url_segments(parts):
|
|||
|
|
return "/".join(map(lambda x: str(x).strip("/"), parts))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _prepare_manifest(template_name, variables):
|
|||
|
|
template = basic_jinja_env.get_template(template_name)
|
|||
|
|
text = template.render(variables)
|
|||
|
|
data = yaml.safe_load(text)
|
|||
|
|
return data
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _prepare_manifest_api_cmp_env(template_name, variables):
|
|||
|
|
template = api_cmp_jinja_env.get_template(template_name)
|
|||
|
|
text = template.render(variables)
|
|||
|
|
data = yaml.safe_load(text)
|
|||
|
|
return data
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_ingress_name(name):
|
|||
|
|
return f'{name}-ingress'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_oauth2_proxy_ingress_name(name):
|
|||
|
|
return f'{name}-oauth2-ingress'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_internal_ingress_name(name):
|
|||
|
|
return f'{name}-internal-ingress'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_mlcmp_paths(namespace, restful_api_section):
|
|||
|
|
if 'path' not in restful_api_section:
|
|||
|
|
raise InputValidationPermanentError('MLComponent requires path in restfulApi section')
|
|||
|
|
path = restful_api_section['path']
|
|||
|
|
prefix = '/' + namespace + '/' + path
|
|||
|
|
prefix = posixpath.normpath(prefix)
|
|||
|
|
quoted_prefix = urllib.parse.quote(prefix)
|
|||
|
|
|
|||
|
|
return [
|
|||
|
|
f'{quoted_prefix}/(predict/?)',
|
|||
|
|
f'{quoted_prefix}/(modelversion/.*/license/?)',
|
|||
|
|
f'{quoted_prefix}/(modelversion/?)'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_files_paths(namespace):
|
|||
|
|
return ['/' + namespace + '/files']
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_pipelines_general_paths(namespace):
|
|||
|
|
pipelines = '/' + namespace + '/pipelines'
|
|||
|
|
trials = '/' + namespace + '/trials'
|
|||
|
|
name = '[0-9A-Za-z_-]+'
|
|||
|
|
return [
|
|||
|
|
f'{trials}/{name}',
|
|||
|
|
f'{trials}',
|
|||
|
|
f'{pipelines}'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_pipeline_internal_paths(namespace, pipeline_name):
|
|||
|
|
pipelines = '/' + namespace + '/pipelines'
|
|||
|
|
name = '[0-9A-Za-z_-]+'
|
|||
|
|
return [
|
|||
|
|
f'{pipelines}/{pipeline_name}/check',
|
|||
|
|
f'{pipelines}/{pipeline_name}/trials/continue',
|
|||
|
|
f'{pipelines}/{pipeline_name}/trials/{name}/status/conditions'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_pipeline_paths(namespace, pipeline_name):
|
|||
|
|
pipelines = '/' + namespace + '/pipelines'
|
|||
|
|
return [
|
|||
|
|
f'{pipelines}/{pipeline_name}/version',
|
|||
|
|
f'{pipelines}/{pipeline_name}/trials',
|
|||
|
|
f'{pipelines}/{pipeline_name}'
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_oauth2_proxy_path_prefix(namespace, upstream_name):
|
|||
|
|
return '/' + namespace + '/proxies/' + upstream_name
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_oauth2_proxy_path(namespace, upstream_name):
|
|||
|
|
return _get_oauth2_proxy_path_prefix(namespace, upstream_name) + '(/|$)(.*)'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_mlcmp_rewrite_target():
|
|||
|
|
return '/$1'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_mlcmp_service_name(mlcmp_name):
|
|||
|
|
return f'{mlcmp_name}-svc'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_files_service_name():
|
|||
|
|
return 'files-svc'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_pipelines_general_service_name():
|
|||
|
|
return 'pipelines-svc'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_pipeline_internal_service_name(name):
|
|||
|
|
return f'{name}-internal-svc'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_pipeline_service_name(name):
|
|||
|
|
return f'{name}-svc'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_files_external_service_name():
|
|||
|
|
# todo: использовать константу аналогично PIPELINES_EXTERNAL_SERVICE_NAME
|
|||
|
|
return 'files-svc.unip-system-controller'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_oauth2_proxy_service_name(name):
|
|||
|
|
return f'{name}-oauth2-svc'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_oauth2_proxy_selector_label(name):
|
|||
|
|
return f'{name}-oauth2-proxy'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_oauth2_proxy_np_name(name):
|
|||
|
|
return f'ingress-{name}-oauth2-traffic'
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _append_oidc_to_ingress_model(name, namespace, ingress_model: dict, oidc: dict | None):
|
|||
|
|
if oidc is None:
|
|||
|
|
return ingress_model
|
|||
|
|
if not oidc.get('enabled', True):
|
|||
|
|
return ingress_model
|
|||
|
|
base_url = 'https://$host/'
|
|||
|
|
|
|||
|
|
proxy_path_prefix = _get_oauth2_proxy_path_prefix(namespace, name)
|
|||
|
|
|
|||
|
|
auth_url_parts = [proxy_path_prefix, 'oauth2/auth']
|
|||
|
|
auth_url = join_url_segments(auth_url_parts)
|
|||
|
|
auth_url = urllib.parse.urljoin(base_url, auth_url)
|
|||
|
|
|
|||
|
|
auth_signin_parts = [proxy_path_prefix, 'oauth2/start?rd=$escaped_request_uri']
|
|||
|
|
auth_signin = join_url_segments(auth_signin_parts)
|
|||
|
|
auth_signin = urllib.parse.urljoin(base_url, auth_signin)
|
|||
|
|
|
|||
|
|
ingress_model['oidc'] = {
|
|||
|
|
'auth_url': auth_url,
|
|||
|
|
'auth_signin': auth_signin
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _append_basic_to_ingress_model(name, ingress_model: dict, basic: dict | None):
|
|||
|
|
if basic is None:
|
|||
|
|
return ingress_model
|
|||
|
|
if not basic.get('enabled', True):
|
|||
|
|
return ingress_model
|
|||
|
|
|
|||
|
|
ingress_model['basic'] = {
|
|||
|
|
'secret_name': get_basic_auth_joint_secret_name(name)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _parse_cors_config_param(cors_enabled, app_cors_enabled, cors, config_value, spec_param_name, default_value,
|
|||
|
|
preprocess=None):
|
|||
|
|
|
|||
|
|
def process_param_values(values_):
|
|||
|
|
vals = map(lambda m: m.strip(), values_)
|
|||
|
|
if preprocess:
|
|||
|
|
vals = map(preprocess, vals)
|
|||
|
|
res = set(vals)
|
|||
|
|
return res
|
|||
|
|
|
|||
|
|
result = set()
|
|||
|
|
if cors_enabled:
|
|||
|
|
if config_value:
|
|||
|
|
result = process_param_values(config_value.split(','))
|
|||
|
|
else:
|
|||
|
|
if app_cors_enabled:
|
|||
|
|
if not cors or spec_param_name in cors:
|
|||
|
|
result = default_value
|
|||
|
|
if app_cors_enabled:
|
|||
|
|
if cors and spec_param_name in cors:
|
|||
|
|
to_join = process_param_values(cors[spec_param_name])
|
|||
|
|
result = result | to_join
|
|||
|
|
elif config_value:
|
|||
|
|
to_join = process_param_values(config_value.split(','))
|
|||
|
|
result = result | to_join
|
|||
|
|
result = ','.join(result)
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _parse_cors_max_age_param(cors_enabled, app_cors_enabled, cors):
|
|||
|
|
result = None
|
|||
|
|
if cors_enabled:
|
|||
|
|
if UNIP_API_CMP_CORS_MAX_AGE:
|
|||
|
|
result = UNIP_API_CMP_CORS_MAX_AGE
|
|||
|
|
if app_cors_enabled:
|
|||
|
|
if cors and 'maxAge' in cors:
|
|||
|
|
result = cors['maxAge']
|
|||
|
|
elif UNIP_API_CMP_CORS_MAX_AGE:
|
|||
|
|
result = UNIP_API_CMP_CORS_MAX_AGE
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _append_cors_to_ingress_model(ingress_model, cors: dict | None):
|
|||
|
|
cors_enabled = UNIP_API_CMP_CORS_ENABLED
|
|||
|
|
app_cors_enabled = UNIP_API_CMP_APPS_CORS_ENABLED
|
|||
|
|
if cors:
|
|||
|
|
app_cors_enabled = cors.get('enabled', app_cors_enabled)
|
|||
|
|
if not cors_enabled and not app_cors_enabled:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
allow_methods_default = {'GET', 'PUT', 'POST', 'DELETE', 'PATCH', 'OPTIONS'}
|
|||
|
|
allow_methods = _parse_cors_config_param(cors_enabled, app_cors_enabled, cors,
|
|||
|
|
UNIP_API_CMP_CORS_ALLOW_METHODS, 'allowMethods',
|
|||
|
|
allow_methods_default)
|
|||
|
|
allow_headers_default = {'DNT', 'Keep-Alive', 'User-Agent', 'X-Requested-With', 'If-Modified-Since',
|
|||
|
|
'Cache-Control', 'Content-Type', 'Range', 'Authorization'}
|
|||
|
|
allow_headers = _parse_cors_config_param(cors_enabled, app_cors_enabled, cors,
|
|||
|
|
UNIP_API_CMP_CORS_ALLOW_HEADERS, 'allowHeaders',
|
|||
|
|
allow_headers_default)
|
|||
|
|
expose_headers = _parse_cors_config_param(cors_enabled, app_cors_enabled, cors,
|
|||
|
|
UNIP_API_CMP_CORS_EXPOSE_HEADERS, 'exposeHeaders',
|
|||
|
|
set())
|
|||
|
|
allow_origin = _parse_cors_config_param(cors_enabled, app_cors_enabled, cors,
|
|||
|
|
UNIP_API_CMP_CORS_ALLOW_ORIGIN, 'allowOrigin',
|
|||
|
|
set(), preprocess=lambda m: m.rstrip('/'))
|
|||
|
|
max_age = _parse_cors_max_age_param(cors_enabled, app_cors_enabled, cors)
|
|||
|
|
|
|||
|
|
cors_model = {}
|
|||
|
|
if allow_methods:
|
|||
|
|
cors_model['allow_methods'] = allow_methods
|
|||
|
|
if allow_headers:
|
|||
|
|
cors_model['allow_headers'] = allow_headers
|
|||
|
|
if expose_headers:
|
|||
|
|
cors_model['expose_headers'] = expose_headers
|
|||
|
|
if allow_origin:
|
|||
|
|
cors_model['allow_origin'] = allow_origin
|
|||
|
|
if max_age:
|
|||
|
|
cors_model['max_age'] = max_age
|
|||
|
|
ingress_model['cors'] = cors_model
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _construct_files_ingress_model(name: str, namespace: str, spec: dict,
|
|||
|
|
basic: dict | None, oidc: dict | None,
|
|||
|
|
cors: dict | None):
|
|||
|
|
restful_api = spec['restfulApi']
|
|||
|
|
auth = restful_api['auth']
|
|||
|
|
ingress_model = {
|
|||
|
|
'name': _get_ingress_name(name),
|
|||
|
|
'namespace': namespace,
|
|||
|
|
'auth_realm': name,
|
|||
|
|
'paths': _get_files_paths(namespace),
|
|||
|
|
'service_name': _get_files_service_name(),
|
|||
|
|
'service_port': FILES_SERVICE_PORT,
|
|||
|
|
'identity_pass_through': auth['identityPassThrough'],
|
|||
|
|
'domain': UNIP_DOMAIN
|
|||
|
|
}
|
|||
|
|
_append_oidc_to_ingress_model(name, namespace, ingress_model, oidc)
|
|||
|
|
_append_basic_to_ingress_model(name, ingress_model, basic)
|
|||
|
|
_append_cors_to_ingress_model(ingress_model, cors)
|
|||
|
|
return ingress_model
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _construct_pipelines_general_ingress_model(name: str, namespace: str, spec: dict,
|
|||
|
|
basic: dict | None, oidc: dict | None,
|
|||
|
|
cors: dict | None):
|
|||
|
|
restful_api = spec['restfulApi']
|
|||
|
|
auth = restful_api['auth']
|
|||
|
|
ingress_model = {
|
|||
|
|
'name': _get_ingress_name(name),
|
|||
|
|
'namespace': namespace,
|
|||
|
|
'auth_realm': name,
|
|||
|
|
'paths': _get_pipelines_general_paths(namespace),
|
|||
|
|
'service_name': _get_pipelines_general_service_name(),
|
|||
|
|
'service_port': PIPELINES_SERVICE_PORT,
|
|||
|
|
'identity_pass_through': auth['identityPassThrough'],
|
|||
|
|
'domain': UNIP_DOMAIN
|
|||
|
|
}
|
|||
|
|
_append_oidc_to_ingress_model(name, namespace, ingress_model, oidc)
|
|||
|
|
_append_basic_to_ingress_model(name, ingress_model, basic)
|
|||
|
|
_append_cors_to_ingress_model(ingress_model, cors)
|
|||
|
|
return ingress_model
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _construct_pipeline_internal_ingress_model(name: str, namespace: str, spec: dict):
|
|||
|
|
pipeline = spec['experimentPipeline']
|
|||
|
|
pipeline_name = pipeline['name']
|
|||
|
|
|
|||
|
|
ingress_model = {
|
|||
|
|
'name': _get_internal_ingress_name(name),
|
|||
|
|
'namespace': namespace,
|
|||
|
|
'secret_name': _get_internal_api_ba_secret_name(name),
|
|||
|
|
'auth_realm': name,
|
|||
|
|
'paths': _get_pipeline_internal_paths(namespace, pipeline_name),
|
|||
|
|
'service_name': _get_pipeline_internal_service_name(name),
|
|||
|
|
'service_port': PIPELINES_SERVICE_PORT,
|
|||
|
|
'identity_pass_through': True,
|
|||
|
|
'domain': UNIP_DOMAIN
|
|||
|
|
}
|
|||
|
|
return ingress_model
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _construct_pipeline_ingress_model(name: str, namespace: str, spec: dict,
|
|||
|
|
basic: dict | None, oidc: dict | None,
|
|||
|
|
cors: dict | None):
|
|||
|
|
pipeline = spec['experimentPipeline']
|
|||
|
|
pipeline_name = pipeline['name']
|
|||
|
|
|
|||
|
|
ingress_model = {
|
|||
|
|
'name': _get_ingress_name(name),
|
|||
|
|
'namespace': namespace,
|
|||
|
|
'auth_realm': name,
|
|||
|
|
'paths': _get_pipeline_paths(namespace, pipeline_name),
|
|||
|
|
'service_name': _get_pipeline_service_name(name),
|
|||
|
|
'service_port': PIPELINES_SERVICE_PORT,
|
|||
|
|
'identity_pass_through': True,
|
|||
|
|
'domain': UNIP_DOMAIN
|
|||
|
|
}
|
|||
|
|
_append_oidc_to_ingress_model(name, namespace, ingress_model, oidc)
|
|||
|
|
_append_basic_to_ingress_model(name, ingress_model, basic)
|
|||
|
|
_append_cors_to_ingress_model(ingress_model, cors)
|
|||
|
|
return ingress_model
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _construct_mlcmp_ingress_model(name: str, namespace: str, spec: dict,
|
|||
|
|
basic: dict | None, oidc: dict | None,
|
|||
|
|
cors: dict | None):
|
|||
|
|
restful_api = spec['restfulApi']
|
|||
|
|
auth = restful_api['auth']
|
|||
|
|
ml_cmp = spec['mlComponent']
|
|||
|
|
|
|||
|
|
ingress_model = {
|
|||
|
|
'name': _get_ingress_name(name),
|
|||
|
|
'namespace': namespace,
|
|||
|
|
'auth_realm': name,
|
|||
|
|
'rewrite_target': _get_mlcmp_rewrite_target(),
|
|||
|
|
'paths': _get_mlcmp_paths(namespace, restful_api),
|
|||
|
|
'service_name': _get_mlcmp_service_name(ml_cmp['name']),
|
|||
|
|
'service_port': ML_CMP_SERVICE_PORT,
|
|||
|
|
'identity_pass_through': auth['identityPassThrough'],
|
|||
|
|
'domain': UNIP_DOMAIN
|
|||
|
|
}
|
|||
|
|
_append_oidc_to_ingress_model(name, namespace, ingress_model, oidc)
|
|||
|
|
_append_basic_to_ingress_model(name, ingress_model, basic)
|
|||
|
|
_append_cors_to_ingress_model(ingress_model, cors)
|
|||
|
|
|
|||
|
|
return ingress_model
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _construct_oauth2_proxy_ingress_model(name: str, namespace: str, cors: dict | None):
|
|||
|
|
ingress_model = {
|
|||
|
|
'name': _get_oauth2_proxy_ingress_name(name),
|
|||
|
|
'namespace': namespace,
|
|||
|
|
'domain': UNIP_DOMAIN,
|
|||
|
|
'path': _get_oauth2_proxy_path(namespace, name),
|
|||
|
|
'service_name': _get_oauth2_proxy_service_name(name),
|
|||
|
|
'service_port': OAUTH2_PROXY_SERVICE_PORT
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_append_cors_to_ingress_model(ingress_model, cors)
|
|||
|
|
|
|||
|
|
return ingress_model
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _construct_oauth2_proxy_deployment_model(name: str, namespace: str, oidc: dict):
|
|||
|
|
base_url = 'https://' + UNIP_DOMAIN
|
|||
|
|
|
|||
|
|
proxy_path_prefix = _get_oauth2_proxy_path_prefix(namespace, name)
|
|||
|
|
redirect_url_parts = [proxy_path_prefix, 'oauth2/callback']
|
|||
|
|
redirect_url = join_url_segments(redirect_url_parts)
|
|||
|
|
redirect_url = urllib.parse.urljoin(base_url, redirect_url)
|
|||
|
|
|
|||
|
|
proxy_prefix_parts = [proxy_path_prefix, 'oauth2']
|
|||
|
|
proxy_prefix = '/' + join_url_segments(proxy_prefix_parts)
|
|||
|
|
|
|||
|
|
oidc_extra_audience = f'{OIDC_END_USERS_AUD},{OIDC_ROBOTS_AUD}' \
|
|||
|
|
if OIDC_END_USERS_AUD != OIDC_ROBOTS_AUD \
|
|||
|
|
else OIDC_END_USERS_AUD
|
|||
|
|
extra_jwt_issuers = f'{OIDC_END_USERS_ISSUER_URL}={OIDC_END_USERS_AUD},{OIDC_ROBOTS_ISSUER_URL}={OIDC_ROBOTS_AUD}' \
|
|||
|
|
if OIDC_END_USERS_AUD != OIDC_ROBOTS_AUD \
|
|||
|
|
else f'{OIDC_END_USERS_ISSUER_URL}={OIDC_END_USERS_AUD}'
|
|||
|
|
|
|||
|
|
deployment_model = {
|
|||
|
|
'deployment_name': get_oauth2_proxy_deployment_name(name),
|
|||
|
|
'selector_label': _get_oauth2_proxy_selector_label(name),
|
|||
|
|
'namespace': namespace,
|
|||
|
|
'client_secret': OIDC_END_USERS_CLIENT_SECRET,
|
|||
|
|
'client_id': OIDC_END_USERS_CLIENT_ID,
|
|||
|
|
'cookie_secret': OIDC_END_USERS_COOKIE_SECRET,
|
|||
|
|
'redirect_url': redirect_url,
|
|||
|
|
'oidc_issuer_url': OIDC_END_USERS_ISSUER_URL,
|
|||
|
|
'oidc_extra_audience': oidc_extra_audience,
|
|||
|
|
'extra_jwt_issuers': extra_jwt_issuers,
|
|||
|
|
'proxy_prefix': proxy_prefix,
|
|||
|
|
'container_port': OAUTH2_PROXY_CONTAINER_PORT,
|
|||
|
|
'groups_secret_name': get_groups_joint_secret_name(name)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if 'roles' in oidc:
|
|||
|
|
deployment_model['roles'] = ','.join(oidc['roles'])
|
|||
|
|
|
|||
|
|
return deployment_model
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _construct_oauth2_proxy_service_model(name: str, namespace: str):
|
|||
|
|
service_model = {
|
|||
|
|
'selector_label': _get_oauth2_proxy_selector_label(name),
|
|||
|
|
'namespace': namespace,
|
|||
|
|
'service_name': _get_oauth2_proxy_service_name(name),
|
|||
|
|
'service_port': OAUTH2_PROXY_SERVICE_PORT,
|
|||
|
|
'container_port': OAUTH2_PROXY_CONTAINER_PORT
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return service_model
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _construct_oauth2_proxy_np(name: str, namespace: str):
|
|||
|
|
np_model = {
|
|||
|
|
'np_name': _get_oauth2_proxy_np_name(name),
|
|||
|
|
'selector_label': _get_oauth2_proxy_selector_label(name),
|
|||
|
|
'namespace': namespace
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return np_model
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_or_update_ingress(api_client_: ApiClient, name, namespace, ingress_model, logger):
|
|||
|
|
ingress_manifest = _prepare_manifest('ingress-multi-auth.yaml', ingress_model)
|
|||
|
|
create_or_update_ingress(api_client_, name, namespace, ingress_manifest, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_or_update_oauth2_proxy_ingress(api_client_: ApiClient, name, namespace, ingress_model, logger):
|
|||
|
|
ingress_manifest = _prepare_manifest_api_cmp_env('oauth2-proxy-ingress.yaml', ingress_model)
|
|||
|
|
create_or_update_ingress(api_client_, name, namespace, ingress_manifest, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_or_update_external_name_files_service(api_client_: ApiClient, namespace, logger):
|
|||
|
|
svc_name = _get_files_service_name()
|
|||
|
|
svc_model = {
|
|||
|
|
'service_name': _get_files_service_name(),
|
|||
|
|
'namespace': namespace,
|
|||
|
|
'external_service_name': _get_files_external_service_name()
|
|||
|
|
}
|
|||
|
|
svc_manifest = _prepare_manifest('external-name-svc.yaml', svc_model)
|
|||
|
|
create_or_update_service(api_client_, svc_name, namespace, svc_manifest, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_or_update_external_name_pipelines_service(api_client_: ApiClient, namespace, svc_name, logger):
|
|||
|
|
svc_model = {
|
|||
|
|
'service_name': svc_name,
|
|||
|
|
'namespace': namespace,
|
|||
|
|
'external_service_name': PIPELINES_EXTERNAL_SERVICE_NAME
|
|||
|
|
}
|
|||
|
|
svc_manifest = _prepare_manifest('external-name-svc.yaml', svc_model)
|
|||
|
|
create_or_update_service(api_client_, svc_name, namespace, svc_manifest, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_or_update_external_name_pipelines_general_service(api_client_: ApiClient, namespace, logger):
|
|||
|
|
svc_name = _get_pipelines_general_service_name()
|
|||
|
|
_create_or_update_external_name_pipelines_service(api_client_, namespace, svc_name, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_or_update_external_name_pipeline_internal_service(api_client_: ApiClient, name, namespace, logger):
|
|||
|
|
svc_name = _get_pipeline_internal_service_name(name)
|
|||
|
|
_create_or_update_external_name_pipelines_service(api_client_, namespace, svc_name, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_or_update_external_name_pipeline_service(api_client_: ApiClient, name, namespace, logger):
|
|||
|
|
svc_name = _get_pipeline_service_name(name)
|
|||
|
|
_create_or_update_external_name_pipelines_service(api_client_, namespace, svc_name, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_or_update_oauth2_proxy_deployment(api_client_: ApiClient, name, namespace, deployment_model, logger):
|
|||
|
|
deployment_manifest = _prepare_manifest_api_cmp_env('oauth2-proxy-deployment.yaml', deployment_model)
|
|||
|
|
create_or_update_deployment(api_client_, name, namespace, deployment_manifest, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_or_update_oauth2_proxy_service(api_client_: ApiClient, name, namespace, service_model, logger):
|
|||
|
|
svc_manifest = _prepare_manifest_api_cmp_env('oauth2-proxy-svc.yaml', service_model)
|
|||
|
|
create_or_update_service(api_client_, name, namespace, svc_manifest, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_or_update_oauth2_proxy_np(api_client_: ApiClient, name, namespace, np_model, logger):
|
|||
|
|
np_manifest = _prepare_manifest('allow-ingress-traffic-np.yaml', np_model)
|
|||
|
|
create_or_update_network_policy(api_client_, name, namespace, np_manifest, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
_passwords_alphabet = string.ascii_letters + string.digits + string.punctuation
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_password():
|
|||
|
|
return ''.join(secrets.choice(_passwords_alphabet) for _ in range(10))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_internal_api_secret_name(name):
|
|||
|
|
return name + INTERNAL_API_SECRET_SUFFIX
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_internal_api_ba_secret_name(name):
|
|||
|
|
return name + INTERNAL_API_BA_SECRET_SUFFIX
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _create_pipeline_internal_api_secrets(api_client_: ApiClient, name, namespace, logger):
|
|||
|
|
password = _create_password()
|
|||
|
|
|
|||
|
|
record = f'{INTERNAL_API_USER_ID}:{password}'
|
|||
|
|
encoded_value = base64.b64encode(record.encode('utf-8')).decode('ascii')
|
|||
|
|
data = [{'key': 'credentials', 'value': encoded_value}]
|
|||
|
|
|
|||
|
|
secret_name = _get_internal_api_secret_name(name)
|
|||
|
|
manifest = prepare_secret_manifest(name=secret_name,
|
|||
|
|
namespace=namespace,
|
|||
|
|
type_='Opaque',
|
|||
|
|
data=data,
|
|||
|
|
data_attr='data')
|
|||
|
|
create_or_update_secret(api_client=api_client_,
|
|||
|
|
name=secret_name,
|
|||
|
|
namespace=namespace,
|
|||
|
|
manifest=manifest,
|
|||
|
|
logger=logger)
|
|||
|
|
|
|||
|
|
encrypted_password = create_htpasswd_password(password)
|
|||
|
|
htpasswd_record = f'{INTERNAL_API_USER_ID}:{encrypted_password}'
|
|||
|
|
encoded_value = base64.b64encode(htpasswd_record.encode('utf-8')).decode('ascii')
|
|||
|
|
basic_auth_data = [
|
|||
|
|
{'key': 'auth', 'value': encoded_value}
|
|||
|
|
]
|
|||
|
|
secret_name = _get_internal_api_ba_secret_name(name)
|
|||
|
|
manifest = prepare_secret_manifest(name=secret_name,
|
|||
|
|
namespace=namespace,
|
|||
|
|
type_='Opaque',
|
|||
|
|
data=basic_auth_data,
|
|||
|
|
data_attr='data')
|
|||
|
|
create_or_update_secret(api_client=api_client_,
|
|||
|
|
name=secret_name,
|
|||
|
|
namespace=namespace,
|
|||
|
|
manifest=manifest,
|
|||
|
|
logger=logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _delete_ingress_if_exists(api_client_: ApiClient, name: str, namespace: str,
|
|||
|
|
logger):
|
|||
|
|
ingress_name = _get_ingress_name(name)
|
|||
|
|
delete_ingress_if_exists(api_client=api_client_, name=ingress_name, namespace=namespace, logger=logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _delete_oauth2_proxy_ingress_if_exists(api_client_: ApiClient, name: str, namespace: str,
|
|||
|
|
logger):
|
|||
|
|
ingress_name = _get_oauth2_proxy_ingress_name(name)
|
|||
|
|
delete_ingress_if_exists(api_client=api_client_, name=ingress_name, namespace=namespace, logger=logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _delete_internal_ingress_if_exists(api_client_: ApiClient, name: str, namespace: str,
|
|||
|
|
logger):
|
|||
|
|
ingress_name = _get_internal_ingress_name(name)
|
|||
|
|
delete_ingress_if_exists(api_client=api_client_, name=ingress_name, namespace=namespace, logger=logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _delete_files_svc_if_exists(api_client_: ApiClient, namespace: str, logger):
|
|||
|
|
svc_name = _get_files_service_name()
|
|||
|
|
delete_svc_if_exists(api_client_, namespace, svc_name, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _delete_pipelines_general_svc_if_exists(api_client_: ApiClient, namespace: str, logger):
|
|||
|
|
svc_name = _get_pipelines_general_service_name()
|
|||
|
|
delete_svc_if_exists(api_client_, namespace, svc_name, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _delete_pipeline_svc_if_exists(api_client_: ApiClient, name: str, namespace: str, logger):
|
|||
|
|
svc_name = _get_pipeline_service_name(name)
|
|||
|
|
delete_svc_if_exists(api_client_, namespace, svc_name, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _delete_pipeline_internal_svc_if_exists(api_client_: ApiClient, name: str, namespace: str, logger):
|
|||
|
|
svc_name = _get_pipeline_internal_service_name(name)
|
|||
|
|
delete_svc_if_exists(api_client_, namespace, svc_name, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _delete_pipeline_internal_api_secrets_if_exist(api_client_: ApiClient, name: str, namespace: str, logger):
|
|||
|
|
secret_name = _get_internal_api_secret_name(name)
|
|||
|
|
delete_secret_if_exists(api_client=api_client_, name=secret_name, namespace=namespace, logger=logger)
|
|||
|
|
|
|||
|
|
secret_name = _get_internal_api_ba_secret_name(name)
|
|||
|
|
delete_secret_if_exists(api_client=api_client_, name=secret_name, namespace=namespace, logger=logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _delete_oauth2_proxy_deployment_if_exists(api_client_: ApiClient, name: str, namespace: str, logger):
|
|||
|
|
deployment_name = get_oauth2_proxy_deployment_name(name)
|
|||
|
|
delete_deployment_if_exists(api_client_, namespace, deployment_name, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _delete_oauth2_proxy_service_if_exists(api_client_: ApiClient, name: str, namespace: str, logger):
|
|||
|
|
service_name = _get_oauth2_proxy_service_name(name)
|
|||
|
|
delete_svc_if_exists(api_client_, namespace, service_name, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _delete_oauth2_proxy_np_if_exists(api_client_: ApiClient, name: str, namespace: str, logger):
|
|||
|
|
np_name = _get_oauth2_proxy_np_name(name)
|
|||
|
|
delete_network_policy_if_exists(api_client_, np_name, namespace, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_oidc_from_spec(spec):
|
|||
|
|
restful_api = spec['restfulApi']
|
|||
|
|
auth = restful_api['auth']
|
|||
|
|
return auth.get('oidc')
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_basic_from_spec(spec):
|
|||
|
|
restful_api = spec['restfulApi']
|
|||
|
|
auth = restful_api['auth']
|
|||
|
|
return auth.get('basic')
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _get_cors_from_spec(spec):
|
|||
|
|
restful_api = spec['restfulApi']
|
|||
|
|
return restful_api.get('cors')
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _should_api_be_published(spec):
|
|||
|
|
published_flag = spec['published']
|
|||
|
|
if not published_flag:
|
|||
|
|
return False
|
|||
|
|
basic = _get_basic_from_spec(spec)
|
|||
|
|
basic_enabled = basic.get('enabled', True) if basic is not None else False
|
|||
|
|
oidc = _get_oidc_from_spec(spec)
|
|||
|
|
oidc_enabled = oidc.get('enabled', True) if oidc is not None else False
|
|||
|
|
if basic_enabled or oidc_enabled:
|
|||
|
|
return True
|
|||
|
|
# пока считаем, что если любая аутентификация выключена, то доступ закрыт
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
@kopf.on.create('unified-platform.cs.hse.ru', 'apicomponents', retries=18, when=not_dev_namespace)
|
|||
|
|
def create_api_component(name, namespace, spec, patch, logger, **_):
|
|||
|
|
"""
|
|||
|
|
Обработчик создания ресурса APIComponent.
|
|||
|
|
Создает ресурсы Service, Ingress для предоставления доступа к сервисам фреймворка и сервисам приложения.
|
|||
|
|
При спецификации OIDC аутентификации настраивает подчиненные ресурсы Deployment, Service, Ingress
|
|||
|
|
для OAuth2 Proxy.
|
|||
|
|
Обновляет объединенный секрет с реквизитами Basic аутентификации.
|
|||
|
|
|
|||
|
|
:param spec: спецификация ресурса APIComponent
|
|||
|
|
:param name: имя ресурса APIComponent
|
|||
|
|
:param namespace: пространство имен ресурса APIComponent
|
|||
|
|
:param patch: объект patch фреймворка kopf
|
|||
|
|
:param logger: kopf logger
|
|||
|
|
:param _: остальные параметры kopf, которые не используются обработчиком
|
|||
|
|
:return: None
|
|||
|
|
"""
|
|||
|
|
published = _should_api_be_published(spec)
|
|||
|
|
oidc = _get_oidc_from_spec(spec)
|
|||
|
|
basic = _get_basic_from_spec(spec)
|
|||
|
|
cors = _get_cors_from_spec(spec)
|
|||
|
|
|
|||
|
|
if 'status' not in patch:
|
|||
|
|
patch['status'] = {}
|
|||
|
|
|
|||
|
|
user_namespace = get_user_namespace(namespace)
|
|||
|
|
ingress_name = _get_ingress_name(name)
|
|||
|
|
|
|||
|
|
_update_oauth2_proxy(name, namespace, spec, oidc, cors, logger)
|
|||
|
|
|
|||
|
|
@kopf.subhandler(id=f'create-or-update-ba-joint-secret-for-{namespace}.{name}', retries=3)
|
|||
|
|
def _create_or_update_ba_joint_secret_handler(**_):
|
|||
|
|
create_or_update_basic_auth_joint_secret(api_client=api_client,
|
|||
|
|
target=name,
|
|||
|
|
target_namespace=namespace,
|
|||
|
|
user_namespace=user_namespace,
|
|||
|
|
logger=logger)
|
|||
|
|
|
|||
|
|
if 'mlComponent' in spec and published:
|
|||
|
|
@kopf.subhandler(id=f'create-ingress-{name}', retries=3)
|
|||
|
|
def _create_mlcmp_ingress_handler(**_):
|
|||
|
|
ingress_model = _construct_mlcmp_ingress_model(name, namespace, spec, basic, oidc, cors)
|
|||
|
|
_create_or_update_ingress(api_client, ingress_name, namespace, ingress_model, logger)
|
|||
|
|
elif 'files' in spec:
|
|||
|
|
files = spec['files']
|
|||
|
|
enabled = files['enabled']
|
|||
|
|
if enabled:
|
|||
|
|
@kopf.subhandler(id=f'create-files-svc-{name}', retries=3)
|
|||
|
|
def _create_files_svc_handler(**_):
|
|||
|
|
_create_or_update_external_name_files_service(api_client, namespace, logger)
|
|||
|
|
|
|||
|
|
if published:
|
|||
|
|
@kopf.subhandler(id=f'create-ingress-{name}', retries=3)
|
|||
|
|
def _create_files_ingress_handler(**_):
|
|||
|
|
ingress_model = _construct_files_ingress_model(name, namespace, spec, basic, oidc, cors)
|
|||
|
|
_create_or_update_ingress(api_client, ingress_name, namespace, ingress_model, logger)
|
|||
|
|
elif 'pipelines' in spec:
|
|||
|
|
pipelines = spec['pipelines']
|
|||
|
|
enabled = pipelines['enabled']
|
|||
|
|
if enabled:
|
|||
|
|
@kopf.subhandler(id=f'create-pipelines-svc-{name}', retries=3)
|
|||
|
|
def _create_pipelines_svc_handler(**_):
|
|||
|
|
_create_or_update_external_name_pipelines_general_service(api_client, namespace, logger)
|
|||
|
|
|
|||
|
|
if published:
|
|||
|
|
@kopf.subhandler(id=f'create-ingress-{name}', retries=3)
|
|||
|
|
def _create_pipelines_general_ingress_handler(**_):
|
|||
|
|
ingress_model = _construct_pipelines_general_ingress_model(name, namespace, spec, basic, oidc, cors)
|
|||
|
|
_create_or_update_ingress(api_client, ingress_name, namespace, ingress_model, logger)
|
|||
|
|
elif 'experimentPipeline' in spec and published:
|
|||
|
|
@kopf.subhandler(id=f'create-ingress-{name}', retries=3)
|
|||
|
|
def _create_pipeline_ingress_handler(**_):
|
|||
|
|
ingress_model = _construct_pipeline_ingress_model(name, namespace, spec, basic, oidc, cors)
|
|||
|
|
_create_or_update_ingress(api_client, ingress_name, namespace, ingress_model, logger)
|
|||
|
|
|
|||
|
|
@kopf.subhandler(id=f'create-pipeline-svc-{name}', retries=3)
|
|||
|
|
def _create_pipeline_svc_handler(**_):
|
|||
|
|
_create_or_update_external_name_pipeline_service(api_client, name, namespace, logger)
|
|||
|
|
|
|||
|
|
@kopf.subhandler(id=f'create-ingress-internal-{name}', retries=3)
|
|||
|
|
def _create_pipeline_internal_ingress_handler(**_):
|
|||
|
|
ingress_model = _construct_pipeline_internal_ingress_model(name, namespace, spec)
|
|||
|
|
_create_or_update_ingress(api_client, ingress_name, namespace, ingress_model, logger)
|
|||
|
|
|
|||
|
|
@kopf.subhandler(id=f'create-pipeline-internal-svc-{name}', retries=3)
|
|||
|
|
def _create_pipeline_internal_svc_handler(**_):
|
|||
|
|
_create_or_update_external_name_pipeline_internal_service(api_client, name, namespace, logger)
|
|||
|
|
|
|||
|
|
@kopf.subhandler(id=f'create-pipeline-internal-secrets-{name}', retries=3)
|
|||
|
|
def _create_pipeline_internal_secrets_handler(**_):
|
|||
|
|
patch['status'].setdefault('experimentPipeline', {})
|
|||
|
|
patch['status']['experimentPipeline']['internalApiSecretName'] = _get_internal_api_secret_name(name)
|
|||
|
|
_create_pipeline_internal_api_secrets(api_client, name, namespace, logger)
|
|||
|
|
else:
|
|||
|
|
if not published:
|
|||
|
|
logger.info('Published if False, ingress is not created')
|
|||
|
|
else:
|
|||
|
|
raise InputValidationPermanentError('Only files, pipelines, experimentPipeline or mlComponent '
|
|||
|
|
'are supported now, but not specified')
|
|||
|
|
|
|||
|
|
|
|||
|
|
@kopf.on.delete('unified-platform.cs.hse.ru', 'apicomponents', retries=3,
|
|||
|
|
when=not_dev_namespace)
|
|||
|
|
def delete_api_component(spec, name, namespace, logger, **_):
|
|||
|
|
"""
|
|||
|
|
Обработчик удаления ресурса APIComponent.
|
|||
|
|
Удаляет ресурсы Service, Ingress, которые использовались для предоставления доступа
|
|||
|
|
к сервисам фреймворка и сервисам приложения.
|
|||
|
|
Если была специфицирована OIDC аутентификация, удаляет подчиненные ресурсы
|
|||
|
|
Deployment, Service, Ingress OAuth2 Proxy.
|
|||
|
|
Обновляет объединенный секрет с реквизитами Basic аутентификации.
|
|||
|
|
|
|||
|
|
:param spec: спецификация ресурса APIComponent
|
|||
|
|
:param name: имя ресурса APIComponent
|
|||
|
|
:param namespace: пространство имен ресурса APIComponent
|
|||
|
|
:param patch: объект patch фреймворка kopf
|
|||
|
|
:param logger: kopf logger
|
|||
|
|
:param _: остальные параметры kopf, которые не используются обработчиком
|
|||
|
|
:return: None
|
|||
|
|
"""
|
|||
|
|
_delete_ingress_if_exists(api_client, name, namespace, logger)
|
|||
|
|
user_namespace = get_user_namespace(namespace)
|
|||
|
|
|
|||
|
|
_delete_oauth2_proxy(name, namespace, logger)
|
|||
|
|
|
|||
|
|
if 'files' in spec:
|
|||
|
|
_delete_files_svc_if_exists(api_client, namespace, logger)
|
|||
|
|
if 'pipelines' in spec:
|
|||
|
|
_delete_pipelines_general_svc_if_exists(api_client, namespace, logger)
|
|||
|
|
if 'experimentPipeline' in spec:
|
|||
|
|
_delete_pipeline_svc_if_exists(api_client, name, namespace, logger)
|
|||
|
|
# дополнительно удаляется internal ingress для ExperimentPipeline,
|
|||
|
|
# потому что у APIComponent для ExperimentPipeline есть два ингресса ("обычный" и internal)
|
|||
|
|
# internal_ingress, internal_svc - специальный случай;
|
|||
|
|
_delete_internal_ingress_if_exists(api_client, name, namespace, logger)
|
|||
|
|
_delete_pipeline_internal_svc_if_exists(api_client, name, namespace, logger)
|
|||
|
|
_delete_pipeline_internal_api_secrets_if_exist(api_client, name, namespace, logger)
|
|||
|
|
|
|||
|
|
if 'mlComponent':
|
|||
|
|
# удалять mlcmp svc не нужно, потому что он управляется контроллером MLComponent,
|
|||
|
|
# это может измениться в будущем
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# обновить общий basic auth секрет
|
|||
|
|
create_or_update_basic_auth_joint_secret(api_client=api_client,
|
|||
|
|
target=name,
|
|||
|
|
target_namespace=namespace,
|
|||
|
|
user_namespace=user_namespace,
|
|||
|
|
logger=logger,
|
|||
|
|
exclude_direct_spec=True)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@kopf.on.update('unified-platform.cs.hse.ru', 'apicomponents',
|
|||
|
|
retries=24, when=not_dev_namespace, field='spec')
|
|||
|
|
def update_api_component(name, namespace, old, new, patch, logger, **_):
|
|||
|
|
"""
|
|||
|
|
Обработчик изменения ресурса APIComponent.
|
|||
|
|
Обновляет ресурсы Service, Ingress для предоставления доступа к сервисам фреймворка и сервисам приложения.
|
|||
|
|
При спецификации OIDC аутентификации настраивает подчиненные ресурсы Deployment, Service, Ingress
|
|||
|
|
для OAuth2 Proxy.
|
|||
|
|
Обновляет объединенный секрет с реквизитами Basic аутентификации.
|
|||
|
|
|
|||
|
|
С небольшими доработками update_api_component и create_api_component
|
|||
|
|
могут быть объединены. С точки зрения level-based triggering не так важно,
|
|||
|
|
какое именно событие инициирует изменение (создание или изменение).
|
|||
|
|
Важно только перевести текущее состояние в целевое, событие -
|
|||
|
|
это просто дополнительная информация.
|
|||
|
|
|
|||
|
|
:param old: предыдущая версия спецификации ресурса APIComponent
|
|||
|
|
:param new: новая версия спецификации ресурса APIComponent
|
|||
|
|
:param name: имя ресурса APIComponent
|
|||
|
|
:param namespace: пространство имен ресурса APIComponent
|
|||
|
|
:param patch: объект patch фреймворка kopf
|
|||
|
|
:param logger: kopf logger
|
|||
|
|
:param _: остальные параметры kopf, которые не используются обработчиком
|
|||
|
|
:return: None
|
|||
|
|
"""
|
|||
|
|
old_spec = old if old else {}
|
|||
|
|
spec = new
|
|||
|
|
oidc = _get_oidc_from_spec(spec)
|
|||
|
|
basic = _get_basic_from_spec(spec)
|
|||
|
|
cors = _get_cors_from_spec(spec)
|
|||
|
|
|
|||
|
|
published = _should_api_be_published(spec)
|
|||
|
|
if 'status' not in patch:
|
|||
|
|
patch['status'] = {}
|
|||
|
|
|
|||
|
|
ingress_name = _get_ingress_name(name)
|
|||
|
|
|
|||
|
|
_update_oauth2_proxy(name, namespace, spec, oidc, cors, logger)
|
|||
|
|
|
|||
|
|
@kopf.subhandler(id=f'update-ingress-{name}', retries=3)
|
|||
|
|
def _update_ingress_handler(**_):
|
|||
|
|
# здесь и в других местах можно в целом более гранулярно управлять распространением изменений -
|
|||
|
|
# не удалять и создавать целые подчиненные ресурсы;
|
|||
|
|
_delete_ingress_if_exists(api_client, name, namespace, logger)
|
|||
|
|
ingress_model = None
|
|||
|
|
if 'mlComponent' in spec and published:
|
|||
|
|
ingress_model = _construct_mlcmp_ingress_model(name, namespace, spec, basic, oidc, cors)
|
|||
|
|
elif 'files' in spec and published:
|
|||
|
|
files = spec['files']
|
|||
|
|
enabled = files['enabled']
|
|||
|
|
if enabled:
|
|||
|
|
ingress_model = _construct_files_ingress_model(name, namespace, spec, basic, oidc, cors)
|
|||
|
|
elif 'pipelines' in spec and published:
|
|||
|
|
pipelines = spec['pipelines']
|
|||
|
|
enabled = pipelines['enabled']
|
|||
|
|
if enabled:
|
|||
|
|
ingress_model = _construct_pipelines_general_ingress_model(name, namespace, spec, basic, oidc, cors)
|
|||
|
|
elif 'experimentPipeline' in spec and published:
|
|||
|
|
ingress_model = _construct_pipeline_ingress_model(name, namespace, spec, basic, oidc, cors)
|
|||
|
|
else:
|
|||
|
|
if not published:
|
|||
|
|
logger.info('Published if False, ingress is not created')
|
|||
|
|
else:
|
|||
|
|
raise InputValidationPermanentError(
|
|||
|
|
'Only files, pipelines, experimentPipeline or mlComponent are supported '
|
|||
|
|
'now, but not specified')
|
|||
|
|
if ingress_model:
|
|||
|
|
_create_or_update_ingress(api_client, ingress_name, namespace, ingress_model, logger)
|
|||
|
|
|
|||
|
|
@kopf.subhandler(id=f'update-files-svc-{name}', retries=3)
|
|||
|
|
def _update_files_svc_handler(**_):
|
|||
|
|
if 'files' in old_spec:
|
|||
|
|
_delete_files_svc_if_exists(api_client, namespace, logger)
|
|||
|
|
if 'files' in spec and published:
|
|||
|
|
files = spec['files']
|
|||
|
|
enabled = files['enabled']
|
|||
|
|
if enabled:
|
|||
|
|
_create_or_update_external_name_files_service(api_client, namespace, logger)
|
|||
|
|
|
|||
|
|
@kopf.subhandler(id=f'update-pipeline-svc-{name}', retries=3)
|
|||
|
|
def _update_pipeline_svc_handler(**_):
|
|||
|
|
if 'experimentPipeline' in old_spec:
|
|||
|
|
_delete_pipeline_svc_if_exists(api_client, name, namespace, logger)
|
|||
|
|
if 'experimentPipeline' in spec and published:
|
|||
|
|
_create_or_update_external_name_pipeline_service(api_client, name, namespace, logger)
|
|||
|
|
|
|||
|
|
@kopf.subhandler(id=f'update-pipelines-general-svc-{name}', retries=3)
|
|||
|
|
def _update_pipelines_general_svc_handler(**_):
|
|||
|
|
if 'pipelines' in old_spec:
|
|||
|
|
_delete_pipelines_general_svc_if_exists(api_client, namespace, logger)
|
|||
|
|
if 'pipelines' in spec and published:
|
|||
|
|
pipelines = spec['pipelines']
|
|||
|
|
enabled = pipelines['enabled']
|
|||
|
|
if enabled:
|
|||
|
|
_create_or_update_external_name_pipelines_general_service(api_client, namespace, logger)
|
|||
|
|
|
|||
|
|
@kopf.subhandler(id=f'update-internal-ingress-{name}', retries=3)
|
|||
|
|
def _update_internal_ingress_handler(**_):
|
|||
|
|
# для internal ingress нужен отдельный обработчик, это специальный случай,
|
|||
|
|
# поскольку у APIComponent для ExperimentPipeline два ингресса;
|
|||
|
|
# а общий обработчик update_ingress обрабатывает обновление одного ингресса;
|
|||
|
|
if 'experimentPipeline' in old_spec:
|
|||
|
|
_delete_internal_ingress_if_exists(api_client, name, namespace, logger)
|
|||
|
|
if 'experimentPipeline' in spec and published:
|
|||
|
|
ingress_model = _construct_pipeline_internal_ingress_model(name, namespace, spec)
|
|||
|
|
_create_or_update_ingress(api_client, ingress_name, namespace, ingress_model, logger)
|
|||
|
|
|
|||
|
|
@kopf.subhandler(id=f'update-pipeline-internal-svc-{name}', retries=3)
|
|||
|
|
def _update_pipeline_internal_svc_handler(**_):
|
|||
|
|
if 'experimentPipeline' in old_spec:
|
|||
|
|
_delete_pipeline_internal_svc_if_exists(api_client, name, namespace, logger)
|
|||
|
|
if 'experimentPipeline' in spec and published:
|
|||
|
|
_create_or_update_external_name_pipeline_internal_service(api_client, name, namespace, logger)
|
|||
|
|
|
|||
|
|
@kopf.subhandler(id=f'update-pipeline-internal-secrets-{name}', retries=3)
|
|||
|
|
def _update_pipeline_internal_secrets_handler(**_):
|
|||
|
|
if 'experimentPipeline' in old_spec:
|
|||
|
|
_delete_pipeline_internal_api_secrets_if_exist(api_client, name, namespace, logger)
|
|||
|
|
if 'experimentPipeline' in spec and published:
|
|||
|
|
patch['status'].setdefault('experimentPipeline', {})
|
|||
|
|
patch['status']['experimentPipeline']['internalApiSecretName'] = _get_internal_api_secret_name(name)
|
|||
|
|
_create_pipeline_internal_api_secrets(api_client, name, namespace, logger)
|
|||
|
|
|
|||
|
|
@kopf.subhandler(id=f'create-or-update-joint-ba-secret-for-{namespace}.{name}', retries=3)
|
|||
|
|
def _create_or_update_ba_joint_secret_handler(**_):
|
|||
|
|
# теоретически, можно выделить в отдельный обработчик @kopf.on.field
|
|||
|
|
|
|||
|
|
old_api = old_spec['restfulApi']
|
|||
|
|
old_auth = old_api['auth']
|
|||
|
|
old_basic = old_auth.get('basic')
|
|||
|
|
old_secret = old_basic.get('credentials') if old_basic else None
|
|||
|
|
|
|||
|
|
api = spec['restfulApi']
|
|||
|
|
auth = api['auth']
|
|||
|
|
new_basic = auth.get('basic')
|
|||
|
|
new_secret = new_basic.get('credentials') if basic else None
|
|||
|
|
|
|||
|
|
if old_secret != new_secret:
|
|||
|
|
user_namespace = get_user_namespace(namespace)
|
|||
|
|
create_or_update_basic_auth_joint_secret(api_client=api_client,
|
|||
|
|
target=name,
|
|||
|
|
target_namespace=namespace,
|
|||
|
|
user_namespace=user_namespace,
|
|||
|
|
logger=logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _should_oauth2_proxy_be_created(api_cmp_spec, new_oidc):
|
|||
|
|
if new_oidc is None:
|
|||
|
|
return False
|
|||
|
|
if not new_oidc.get('enabled', True):
|
|||
|
|
return False
|
|||
|
|
published = api_cmp_spec['published']
|
|||
|
|
if 'mlComponent' in api_cmp_spec and published:
|
|||
|
|
return True
|
|||
|
|
elif 'experimentPipeline' in api_cmp_spec and published:
|
|||
|
|
return True
|
|||
|
|
elif 'files' in api_cmp_spec and published:
|
|||
|
|
files = api_cmp_spec['files']
|
|||
|
|
enabled = files['enabled']
|
|||
|
|
if enabled:
|
|||
|
|
return True
|
|||
|
|
elif 'pipelines' in api_cmp_spec and published:
|
|||
|
|
pipelines = api_cmp_spec['pipelines']
|
|||
|
|
enabled = pipelines['enabled']
|
|||
|
|
if enabled:
|
|||
|
|
return True
|
|||
|
|
else:
|
|||
|
|
if published:
|
|||
|
|
raise InputValidationPermanentError(
|
|||
|
|
'Only files, pipelines, experimentPipeline or mlComponent are supported '
|
|||
|
|
'now, but not specified')
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _update_oauth2_proxy(api_cmp_name, namespace, api_cmp_spec, new_oidc, cors, logger):
|
|||
|
|
create_oauth2_proxy = _should_oauth2_proxy_be_created(api_cmp_spec, new_oidc)
|
|||
|
|
|
|||
|
|
@kopf.subhandler(id=f'update-oauth2-proxy-ingress-{api_cmp_name}', retries=3)
|
|||
|
|
def _update_oauth2_proxy_ingress_handler(**_):
|
|||
|
|
_delete_oauth2_proxy_ingress_if_exists(api_client, api_cmp_name, namespace, logger)
|
|||
|
|
if create_oauth2_proxy:
|
|||
|
|
ingress_name = _get_oauth2_proxy_ingress_name(api_cmp_name)
|
|||
|
|
ingress_model = _construct_oauth2_proxy_ingress_model(api_cmp_name, namespace, cors)
|
|||
|
|
_create_or_update_oauth2_proxy_ingress(api_client, ingress_name, namespace, ingress_model,
|
|||
|
|
logger)
|
|||
|
|
|
|||
|
|
@kopf.subhandler(id=f'update-oauth2-proxy-deployment-{api_cmp_name}', retries=3)
|
|||
|
|
def _update_oauth2_proxy_deployment_handler(**_):
|
|||
|
|
_delete_oauth2_proxy_deployment_if_exists(api_client, api_cmp_name, namespace, logger)
|
|||
|
|
if create_oauth2_proxy:
|
|||
|
|
deployment_name = get_oauth2_proxy_deployment_name(api_cmp_name)
|
|||
|
|
deployment_model = _construct_oauth2_proxy_deployment_model(api_cmp_name, namespace, new_oidc)
|
|||
|
|
user_namespace = get_user_namespace(namespace)
|
|||
|
|
create_or_update_groups_joint_secret(api_client, api_cmp_name, namespace, user_namespace, logger)
|
|||
|
|
_create_or_update_oauth2_proxy_deployment(api_client, deployment_name, namespace, deployment_model, logger)
|
|||
|
|
|
|||
|
|
@kopf.subhandler(id=f'update-oauth2-proxy-service-{api_cmp_name}', retries=3)
|
|||
|
|
def _update_oauth2_proxy_service_handler(**_):
|
|||
|
|
_delete_oauth2_proxy_service_if_exists(api_client, api_cmp_name, namespace, logger)
|
|||
|
|
if create_oauth2_proxy:
|
|||
|
|
service_name = _get_oauth2_proxy_service_name(api_cmp_name)
|
|||
|
|
service_model = _construct_oauth2_proxy_service_model(api_cmp_name, namespace)
|
|||
|
|
_create_or_update_oauth2_proxy_service(api_client, service_name, namespace, service_model, logger)
|
|||
|
|
|
|||
|
|
@kopf.subhandler(id=f'update-oauth2-proxy-np-{api_cmp_name}', retries=3)
|
|||
|
|
def _update_oauth2_proxy_np_handler(**_):
|
|||
|
|
_delete_oauth2_proxy_np_if_exists(api_client, api_cmp_name, namespace, logger)
|
|||
|
|
if create_oauth2_proxy:
|
|||
|
|
np_name = _get_oauth2_proxy_np_name(api_cmp_name)
|
|||
|
|
np_model = _construct_oauth2_proxy_np(api_cmp_name, namespace)
|
|||
|
|
_create_or_update_oauth2_proxy_np(api_client, np_name, namespace, np_model, logger)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _delete_oauth2_proxy(api_cmp_name, namespace, logger):
|
|||
|
|
_delete_oauth2_proxy_ingress_if_exists(api_client, api_cmp_name, namespace, logger)
|
|||
|
|
_delete_oauth2_proxy_deployment_if_exists(api_client, api_cmp_name, namespace, logger)
|
|||
|
|
_delete_oauth2_proxy_service_if_exists(api_client, api_cmp_name, namespace, logger)
|
|||
|
|
_delete_oauth2_proxy_np_if_exists(api_client, api_cmp_name, namespace, logger)
|