diff --git a/api/api/openapi.bundle.yaml b/api/api/openapi.bundle.yaml index b0c1abd80..88b1548f0 100644 --- a/api/api/openapi.bundle.yaml +++ b/api/api/openapi.bundle.yaml @@ -1951,6 +1951,11 @@ components: max_replica: 6 memory_request: memory_request cpu_request: cpu_request + env: + - name: name + value: value + - name: name + value: value timeout: timeout updated_at: 2000-01-23T04:56:07.000+00:00 standard_config: @@ -2212,6 +2217,11 @@ components: max_replica: 6 memory_request: memory_request cpu_request: cpu_request + env: + - name: name + value: value + - name: name + value: value timeout: timeout updated_at: 2000-01-23T04:56:07.000+00:00 standard_config: @@ -2347,6 +2357,11 @@ components: max_replica: 6 memory_request: memory_request cpu_request: cpu_request + env: + - name: name + value: value + - name: name + value: value timeout: timeout nullable: true properties: @@ -2359,6 +2374,10 @@ components: timeout: pattern: ^[0-9]+(ms|s|m|h)$ type: string + env: + items: + $ref: '#/components/schemas/EnvVar' + type: array required: - ensembler_id - project_id @@ -2502,6 +2521,11 @@ components: max_replica: 6 memory_request: memory_request cpu_request: cpu_request + env: + - name: name + value: value + - name: name + value: value timeout: timeout updated_at: 2000-01-23T04:56:07.000+00:00 standard_config: @@ -2634,6 +2658,11 @@ components: max_replica: 6 memory_request: memory_request cpu_request: cpu_request + env: + - name: name + value: value + - name: name + value: value timeout: timeout updated_at: 2000-01-23T04:56:07.000+00:00 standard_config: diff --git a/api/api/specs/routers.yaml b/api/api/specs/routers.yaml index ab12eb234..d5bfbcfa7 100644 --- a/api/api/specs/routers.yaml +++ b/api/api/specs/routers.yaml @@ -13,7 +13,7 @@ info: .timeout: &timeout type: "string" pattern: '^[0-9]+(ms|s|m|h)$' - + paths: "/projects/{project_id}/routers": get: @@ -851,6 +851,10 @@ components: $ref: "#/components/schemas/ResourceRequest" timeout: <<: *timeout + env: + type: "array" + items: + $ref: "common.yaml#/components/schemas/EnvVar" ResourceRequest: type: "object" diff --git a/api/db-migrations/000010_add_pyfunc_config.down.sql b/api/db-migrations/000010_add_pyfunc_config.down.sql index 34aff002e..0a2280569 100644 --- a/api/db-migrations/000010_add_pyfunc_config.down.sql +++ b/api/db-migrations/000010_add_pyfunc_config.down.sql @@ -1,4 +1,4 @@ --- Removes py_func_ref_config from table. +-- Removes pyfunc_config from table. -- Hence, this migration involves data loss for ensemblers with type that is "pyfunc" ALTER TABLE ensembler_configs DROP COLUMN pyfunc_config; diff --git a/api/db-migrations/000010_add_pyfunc_config.up.sql b/api/db-migrations/000010_add_pyfunc_config.up.sql index d8bcfed2e..21854b7e4 100644 --- a/api/db-migrations/000010_add_pyfunc_config.up.sql +++ b/api/db-migrations/000010_add_pyfunc_config.up.sql @@ -1,3 +1,3 @@ --- Adds py_func_ref_config to table. +-- Adds pyfunc_config to table. ALTER TABLE ensembler_configs ADD pyfunc_config jsonb; diff --git a/api/turing/api/request/request.go b/api/turing/api/request/request.go index 96cce2585..725b5e8fe 100644 --- a/api/turing/api/request/request.go +++ b/api/turing/api/request/request.go @@ -148,7 +148,7 @@ func (r RouterConfig) BuildRouterVersion( } if r.Ensembler.Type == models.EnsemblerPyFuncType { if r.Ensembler.PyfuncConfig == nil { - return nil, errors.New("missing ensembler pyfunc reference config") + return nil, errors.New("missing ensembler pyfunc config") } // Verify if the ensembler given by its ProjectID and EnsemblerID exist diff --git a/api/turing/models/ensembler.go b/api/turing/models/ensembler.go index b4842c5b8..feb812d2f 100644 --- a/api/turing/models/ensembler.go +++ b/api/turing/models/ensembler.go @@ -58,6 +58,8 @@ type EnsemblerPyfuncConfig struct { ResourceRequest *ResourceRequest `json:"resource_request" validate:"required"` // Request timeout in duration format e.g. 60s Timeout string `json:"timeout" validate:"required"` + // Environment variables to set in the container + Env EnvVars `json:"env" validate:"required"` } type ExperimentMapping struct { diff --git a/api/turing/service/router_deployment_service.go b/api/turing/service/router_deployment_service.go index 819108212..799bab870 100644 --- a/api/turing/service/router_deployment_service.go +++ b/api/turing/service/router_deployment_service.go @@ -455,7 +455,7 @@ func (ds *deploymentService) buildEnsemblerServiceImage( Timeout: routerVersion.Ensembler.PyfuncConfig.Timeout, Endpoint: PyFuncEnsemblerServiceEndpoint, Port: PyFuncEnsemblerServicePort, - Env: models.EnvVars{}, + Env: routerVersion.Ensembler.PyfuncConfig.Env, ServiceAccount: "", } diff --git a/sdk/tests/conftest.py b/sdk/tests/conftest.py index 14450a450..875c9a365 100644 --- a/sdk/tests/conftest.py +++ b/sdk/tests/conftest.py @@ -334,12 +334,13 @@ def generic_ensembler_docker_config(generic_resource_request, generic_env_var): @pytest.fixture -def generic_ensembler_pyfunc_config(generic_resource_request): +def generic_ensembler_pyfunc_config(generic_resource_request, generic_env_var): return turing.generated.models.EnsemblerPyfuncConfig( project_id=77, ensembler_id=11, resource_request=generic_resource_request, - timeout="500ms" + timeout="500ms", + env=[generic_env_var] ) diff --git a/sdk/tests/router/config/router_ensembler_config_test.py b/sdk/tests/router/config/router_ensembler_config_test.py index 3497c7b3c..529de9139 100644 --- a/sdk/tests/router/config/router_ensembler_config_test.py +++ b/sdk/tests/router/config/router_ensembler_config_test.py @@ -66,8 +66,9 @@ def test_create_router_ensembler_config(id, type, standard_config, docker_config ).to_open_api() assert actual == request.getfixturevalue(expected) + @pytest.mark.parametrize( - "project_id,ensembler_id,resource_request,timeout,expected", [ + "project_id,ensembler_id,resource_request,timeout,env,expected", [ pytest.param( 77, 11, @@ -78,6 +79,11 @@ def test_create_router_ensembler_config(id, type, standard_config, docker_config memory_request='512Mi' ), "500ms", + [ + EnvVar( + name="env_name", + value="env_val") + ], "generic_pyfunc_router_ensembler_config" ) ]) @@ -86,6 +92,7 @@ def test_create_pyfunc_router_ensembler_config( ensembler_id, resource_request, timeout, + env, expected, request ): @@ -94,12 +101,13 @@ def test_create_pyfunc_router_ensembler_config( ensembler_id=ensembler_id, resource_request=resource_request, timeout=timeout, + env=env ).to_open_api() assert actual == request.getfixturevalue(expected) @pytest.mark.parametrize( - "project_id,ensembler_id,resource_request,timeout,expected", [ + "project_id,ensembler_id,resource_request,timeout,env,expected", [ pytest.param( 77, 11, @@ -110,6 +118,11 @@ def test_create_pyfunc_router_ensembler_config( memory_request='512Mi' ), "500ks", + [ + EnvVar( + name="env_name", + value="env_val") + ], ApiValueError ) ]) @@ -118,6 +131,7 @@ def test_create_pyfunc_router_ensembler_config_with_invalid_timeout( ensembler_id, resource_request, timeout, + env, expected ): with pytest.raises(expected): @@ -126,6 +140,7 @@ def test_create_pyfunc_router_ensembler_config_with_invalid_timeout( ensembler_id=ensembler_id, resource_request=resource_request, timeout=timeout, + env=env ).to_open_api() diff --git a/sdk/turing/generated/model/ensembler_pyfunc_config.py b/sdk/turing/generated/model/ensembler_pyfunc_config.py index 554b38712..a6a00fb13 100644 --- a/sdk/turing/generated/model/ensembler_pyfunc_config.py +++ b/sdk/turing/generated/model/ensembler_pyfunc_config.py @@ -27,7 +27,9 @@ ) def lazy_import(): + from turing.generated.model.env_var import EnvVar from turing.generated.model.resource_request import ResourceRequest + globals()['EnvVar'] = EnvVar globals()['ResourceRequest'] = ResourceRequest @@ -86,6 +88,7 @@ def openapi_types(): 'ensembler_id': (int,), # noqa: E501 'resource_request': (ResourceRequest,), # noqa: E501 'timeout': (str,), # noqa: E501 + 'env': ([EnvVar],), # noqa: E501 } @cached_property @@ -98,6 +101,7 @@ def discriminator(): 'ensembler_id': 'ensembler_id', # noqa: E501 'resource_request': 'resource_request', # noqa: E501 'timeout': 'timeout', # noqa: E501 + 'env': 'env', # noqa: E501 } _composed_schemas = {} @@ -152,6 +156,7 @@ def __init__(self, project_id, ensembler_id, resource_request, timeout, *args, * Animal class but this time we won't travel through its discriminator because we passed in _visited_composed_classes = (Animal,) + env ([EnvVar]): [optional] # noqa: E501 """ _check_type = kwargs.pop('_check_type', True) diff --git a/sdk/turing/router/config/experiment_config.py b/sdk/turing/router/config/experiment_config.py index 0fe174cff..ffc67e417 100644 --- a/sdk/turing/router/config/experiment_config.py +++ b/sdk/turing/router/config/experiment_config.py @@ -29,9 +29,9 @@ def config(self) -> Dict: @config.setter def config(self, config: Dict): - if config is not None and 'project_id' in config: - config['project_id'] = int(config['project_id']) self._config = config + if self._config is not None and 'project_id' in self._config: + self.config['project_id'] = int(self._config['project_id']) def to_open_api(self) -> OpenApiModel: if self.config is None: diff --git a/sdk/turing/router/config/router_ensembler_config.py b/sdk/turing/router/config/router_ensembler_config.py index ccac92522..6588b4ea0 100644 --- a/sdk/turing/router/config/router_ensembler_config.py +++ b/sdk/turing/router/config/router_ensembler_config.py @@ -62,11 +62,12 @@ def standard_config(self, standard_config: Union[turing.generated.models.Ensembl if isinstance(standard_config, turing.generated.models.EnsemblerStandardConfig): self._standard_config = standard_config elif isinstance(standard_config, dict): - standard_config["experiment_mappings"] = [ + openapi_standard_config = standard_config.copy() + openapi_standard_config["experiment_mappings"] = [ turing.generated.models.EnsemblerStandardConfigExperimentMappings(**mapping) for mapping in standard_config["experiment_mappings"] ] - self._standard_config = turing.generated.models.EnsemblerStandardConfig(**standard_config) + self._standard_config = turing.generated.models.EnsemblerStandardConfig(**openapi_standard_config) else: self._standard_config = standard_config @@ -79,11 +80,12 @@ def docker_config(self, docker_config: turing.generated.models.EnsemblerDockerCo if isinstance(docker_config, turing.generated.models.EnsemblerDockerConfig): self._docker_config = docker_config elif isinstance(docker_config, dict): - docker_config["resource_request"] = \ - turing.generated.models.ResourceRequest(**docker_config["resource_request"]) - docker_config["env"] = [turing.generated.models.EnvVar(**env_var) for env_var in docker_config["env"]] + openapi_docker_config = docker_config.copy() + openapi_docker_config["resource_request"] = \ + turing.generated.models.ResourceRequest(**openapi_docker_config['resource_request']) + openapi_docker_config["env"] = [turing.generated.models.EnvVar(**env_var) for env_var in docker_config['env']] self._docker_config = turing.generated.models.EnsemblerDockerConfig( - **docker_config + **openapi_docker_config ) else: self._docker_config = docker_config @@ -97,10 +99,12 @@ def pyfunc_config(self, pyfunc_config: turing.generated.models.EnsemblerPyfuncCo if isinstance(pyfunc_config, turing.generated.models.EnsemblerPyfuncConfig): self._pyfunc_config = pyfunc_config elif isinstance(pyfunc_config, dict): - pyfunc_config["resource_request"] = \ + openapi_pyfunc_config = pyfunc_config.copy() + openapi_pyfunc_config["resource_request"] = \ turing.generated.models.ResourceRequest(**pyfunc_config["resource_request"]) + openapi_pyfunc_config["env"] = [turing.generated.models.EnvVar(**env_var) for env_var in pyfunc_config["env"]] self._pyfunc_config = turing.generated.models.EnsemblerPyfuncConfig( - **pyfunc_config + **openapi_pyfunc_config ) else: self._pyfunc_config = pyfunc_config @@ -127,7 +131,8 @@ def __init__(self, project_id: int, ensembler_id: int, timeout: str, - resource_request: ResourceRequest): + resource_request: ResourceRequest, + env: List['EnvVar']): """ Method to create a new Pyfunc ensembler @@ -135,11 +140,13 @@ def __init__(self, :param ensembler_id: ensembler_id of the ensembler :param resource_request: ResourceRequest instance containing configs related to the resources required :param timeout: request timeout which when exceeded, the request to the ensembler will be terminated + :param env: environment variables required by the container """ self.project_id = project_id self.ensembler_id = ensembler_id self.resource_request = resource_request self.timeout = timeout + self.env = env super().__init__(type="pyfunc") @property @@ -174,12 +181,23 @@ def timeout(self) -> str: def timeout(self, timeout: str): self._timeout = timeout + @property + def env(self) -> List['EnvVar']: + return self._env + + @env.setter + def env(self, env: List['EnvVar']): + self._env = env + def to_open_api(self) -> OpenApiModel: + assert all(isinstance(env_var, EnvVar) for env_var in self.env) + self.pyfunc_config = turing.generated.models.EnsemblerPyfuncConfig( project_id=self.project_id, ensembler_id=self.ensembler_id, resource_request=self.resource_request.to_open_api(), timeout=self.timeout, + env=[env_var.to_open_api() for env_var in self.env], ) return super().to_open_api() diff --git a/sdk/turing/router/config/router_version.py b/sdk/turing/router/config/router_version.py index acd1759fd..0106aedb8 100644 --- a/sdk/turing/router/config/router_version.py +++ b/sdk/turing/router/config/router_version.py @@ -1,7 +1,6 @@ from __future__ import annotations import turing -import turing.generated.models import dataclasses from enum import Enum diff --git a/sdk/turing/router/router.py b/sdk/turing/router/router.py index 250750961..68dea112e 100644 --- a/sdk/turing/router/router.py +++ b/sdk/turing/router/router.py @@ -33,7 +33,6 @@ def __init__(self, environment_name: str, monitoring_url: str, status: str, - version: int = None, config: Dict = None, endpoint: str = None, **kwargs): @@ -45,13 +44,7 @@ def __init__(self, self._endpoint = endpoint self._monitoring_url = monitoring_url self._status = RouterStatus(status) - - if config is not None: - self._config = RouterConfig(name=name, environment_name=environment_name, **config) - self._version = config.get('version') - else: - self._config = config - self._version = version + self._config = config @property def id(self) -> int: @@ -83,11 +76,11 @@ def status(self) -> RouterStatus: @property def config(self) -> 'RouterConfig': - return self._config + return RouterConfig(name=self.name, environment_name=self.environment_name, **self._config) @property def version(self) -> int: - return self._version + return self._config.get('version') if self._config else None @classmethod def list(cls) -> List['Router']: @@ -135,8 +128,7 @@ def update(self, config: RouterConfig) -> 'Router': Update the current router with a new set of configs specified in the RouterConfig argument :param config: configuration of router - :return: instance of router (self); this router contains details of the currently deployed router; it contains - the details of the currently DEPLOYED router version + :return: instance of router (self); this router contains details of the router and its currently deployed version """ self._config = config updated_router = Router.from_open_api( diff --git a/ui/src/router/components/configuration/components/pyfunc_config_section/PyFuncRefConfigTable.js b/ui/src/router/components/configuration/components/pyfunc_config_section/PyFuncConfigTable.js similarity index 91% rename from ui/src/router/components/configuration/components/pyfunc_config_section/PyFuncRefConfigTable.js rename to ui/src/router/components/configuration/components/pyfunc_config_section/PyFuncConfigTable.js index 70344e46a..eaf756a46 100644 --- a/ui/src/router/components/configuration/components/pyfunc_config_section/PyFuncRefConfigTable.js +++ b/ui/src/router/components/configuration/components/pyfunc_config_section/PyFuncConfigTable.js @@ -2,7 +2,7 @@ import React, { useContext } from "react"; import { EuiDescriptionList } from "@elastic/eui"; import EnsemblersContext from "../../../../../providers/ensemblers/context"; -export const PyFuncRefConfigTable = ({ config: { ensembler_id } }) => { +export const PyFuncConfigTable = ({ config: { ensembler_id } }) => { const { ensemblers } = useContext(EnsemblersContext); const ensembler_name = Object.values(ensemblers) diff --git a/ui/src/router/components/configuration/components/pyfunc_config_section/PyFuncConfigViewGroup.js b/ui/src/router/components/configuration/components/pyfunc_config_section/PyFuncConfigViewGroup.js index 2e7cc0c17..b44607306 100644 --- a/ui/src/router/components/configuration/components/pyfunc_config_section/PyFuncConfigViewGroup.js +++ b/ui/src/router/components/configuration/components/pyfunc_config_section/PyFuncConfigViewGroup.js @@ -4,7 +4,8 @@ import { ConfigSectionPanel } from "../../../../../components/config_section"; import { ContainerConfigTable } from "../docker_config_section/ContainerConfigTable"; import { ResourcesConfigTable } from "../ResourcesConfigTable"; import { ConfigMultiSectionPanel } from "../../../../../components/config_multi_section_panel/ConfigMultiSectionPanel"; -import { PyFuncRefConfigTable } from "./PyFuncRefConfigTable"; +import { PyFuncConfigTable } from "./PyFuncConfigTable"; +import { EnvVariablesConfigTable } from "../docker_config_section/EnvVariablesConfigTable"; export const PyFuncConfigViewGroup = ({ componentName, @@ -14,7 +15,7 @@ export const PyFuncConfigViewGroup = ({ const items = [ { title: "Pyfunc Ensembler Details", - children: , + children: , }, ]; @@ -25,22 +26,23 @@ export const PyFuncConfigViewGroup = ({ }); } + items.push({ + title: "Environment Variables", + children: , + }); + return ( -
- {!!dockerConfig ? ( - - - - - - ) : null} -
+ + + + +
); }; diff --git a/ui/src/router/components/form/components/pyfunc_config/PyFuncConfigFormGroup.js b/ui/src/router/components/form/components/pyfunc_config/PyFuncConfigFormGroup.js index 8317d290b..3b3eedc8d 100644 --- a/ui/src/router/components/form/components/pyfunc_config/PyFuncConfigFormGroup.js +++ b/ui/src/router/components/form/components/pyfunc_config/PyFuncConfigFormGroup.js @@ -7,6 +7,7 @@ import { useOnChangeHandler } from "../../../../../components/form/hooks/useOnCh import { PyFuncEnsembler } from "../../../../../services/ensembler"; import { PyFuncDeploymentPanel } from "./PyFuncDeploymentPanel"; import { EnsemblersContextContextProvider } from "../../../../../providers/ensemblers/context"; +import { EnvVariablesPanel } from "../docker_config/EnvVariablesPanel"; export const PyFuncConfigFormGroup = ({ projectId, @@ -43,6 +44,14 @@ export const PyFuncConfigFormGroup = ({ + + + + ensembler_id: yup.number().integer().required("Ensembler ID is required"), timeout: timeoutSchema.required("Timeout is required"), resource_request: resourceRequestSchema(maxAllowedReplica), + env: yup.array(environmentVariableSchema), }); const mappingSchema = yup.object().shape({ diff --git a/ui/src/services/ensembler/PyFuncEnsembler.js b/ui/src/services/ensembler/PyFuncEnsembler.js index 4cc64c698..871f268ab 100644 --- a/ui/src/services/ensembler/PyFuncEnsembler.js +++ b/ui/src/services/ensembler/PyFuncEnsembler.js @@ -25,6 +25,7 @@ export class PyFuncEnsembler extends Ensembler { min_replica: 0, max_replica: 2, }, + env: [], timeout: "60ms", }; }