diff --git a/api/api/openapi.bundle.yaml b/api/api/openapi.bundle.yaml index afb58455d..b0c1abd80 100644 --- a/api/api/openapi.bundle.yaml +++ b/api/api/openapi.bundle.yaml @@ -476,7 +476,7 @@ paths: /projects/{project_id}/routers: get: parameters: - - description: project id of the project to retrieve routers from + - description: id of the project to retrieve routers from in: path name: project_id required: true @@ -501,7 +501,7 @@ paths: - Router post: parameters: - - description: project id of the project to save router + - description: id of the project to save router in: path name: project_id required: true @@ -532,7 +532,7 @@ paths: /projects/{project_id}/routers/{router_id}: delete: parameters: - - description: project id of the project of the router + - description: id of the project of the router in: path name: project_id required: true @@ -564,7 +564,7 @@ paths: - Router get: parameters: - - description: project id of the project to retrieve routers from + - description: id of the project to retrieve routers from in: path name: project_id required: true @@ -594,7 +594,7 @@ paths: - Router put: parameters: - - description: project id of the project of the router + - description: id of the project of the router in: path name: project_id required: true @@ -696,7 +696,7 @@ paths: /projects/{project_id}/routers/{router_id}/versions: get: parameters: - - description: project id of the project to retrieve routers from + - description: id of the project to retrieve routers from in: path name: project_id required: true @@ -726,10 +726,47 @@ paths: summary: List router config versions tags: - Router + post: + parameters: + - description: id of the project of the router + in: path + name: project_id + required: true + schema: + format: int32 + type: integer + - description: id of the router to create a new version for + in: path + name: router_id + required: true + schema: + format: int32 + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/RouterVersionConfig' + description: router configuration to save + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/RouterVersion' + description: OK + "400": + description: Invalid project_id, router_id or router configuration + "500": + description: Unable to save configuration + summary: Create router version without deploying it + tags: + - Router /projects/{project_id}/routers/{router_id}/versions/{version}: delete: parameters: - - description: project id of the project of the router + - description: id of the project of the router in: path name: project_id required: true @@ -768,7 +805,7 @@ paths: - Router get: parameters: - - description: project id of the project to retrieve routers from + - description: id of the project to retrieve routers from in: path name: project_id required: true @@ -2515,12 +2552,161 @@ components: pattern: ^[a-z0-9-]*$ type: string config: - $ref: '#/components/schemas/RouterConfig_config' + $ref: '#/components/schemas/RouterVersionConfig' required: - config - environment_name - name type: object + RouterVersionConfig: + example: + enricher: + service_account: secret-name-for-google-service-account + image: image + endpoint: endpoint + updated_at: 2000-01-23T04:56:07.000+00:00 + port: 5 + created_at: 2000-01-23T04:56:07.000+00:00 + resource_request: + min_replica: 0 + max_replica: 6 + memory_request: memory_request + cpu_request: cpu_request + id: 1 + env: + - name: name + value: value + - name: name + value: value + timeout: timeout + routes: + - endpoint: endpoint + annotations: '{}' + id: id + type: type + timeout: timeout + - endpoint: endpoint + annotations: '{}' + id: id + type: type + timeout: timeout + default_route_id: default_route_id + rules: + - routes: + - routes + - routes + conditions: + - field: field + values: + - values + - values + operator: in + - field: field + values: + - values + - values + operator: in + - routes: + - routes + - routes + conditions: + - field: field + values: + - values + - values + operator: in + - field: field + values: + - values + - values + operator: in + resource_request: + min_replica: 0 + max_replica: 6 + memory_request: memory_request + cpu_request: cpu_request + ensembler: + pyfunc_config: + project_id: 7 + ensembler_id: 9 + resource_request: + min_replica: 0 + max_replica: 6 + memory_request: memory_request + cpu_request: cpu_request + timeout: timeout + updated_at: 2000-01-23T04:56:07.000+00:00 + standard_config: + experiment_mappings: + - treatment: treatment-1 + route: route-1 + experiment: experiment-1 + - treatment: treatment-1 + route: route-1 + experiment: experiment-1 + created_at: 2000-01-23T04:56:07.000+00:00 + id: 5 + type: standard + docker_config: + service_account: secret-name-for-google-service-account + image: image + endpoint: endpoint + port: 2 + resource_request: + min_replica: 0 + max_replica: 6 + memory_request: memory_request + cpu_request: cpu_request + env: + - name: name + value: value + - name: name + value: value + timeout: timeout + log_config: + bigquery_config: + batch_load: true + table: table + service_account_secret: service_account_secret + kafka_config: + brokers: brokers + topic: topic + serialization_format: json + timeout: timeout + experiment_engine: + type: nop + config: '{}' + properties: + routes: + items: + $ref: '#/components/schemas/Route' + type: array + rules: + items: + $ref: '#/components/schemas/TrafficRule' + type: array + default_route_id: + type: string + experiment_engine: + $ref: '#/components/schemas/ExperimentConfig' + resource_request: + $ref: '#/components/schemas/ResourceRequest' + timeout: + pattern: ^[0-9]+(ms|s|m|h)$ + type: string + log_config: + $ref: '#/components/schemas/RouterVersionConfig_log_config' + enricher: + $ref: '#/components/schemas/Enricher' + ensembler: + $ref: '#/components/schemas/RouterEnsemblerConfig' + required: + - default_route_id + - experiment_engine + - log_config + - routes + - timeout + type: object RouterId: $ref: '#/components/schemas/IdObject' RouterIdAndVersion: @@ -3044,7 +3230,7 @@ components: - route - treatment type: object - RouterConfig_config_log_config: + RouterVersionConfig_log_config: example: bigquery_config: batch_load: true @@ -3062,155 +3248,6 @@ components: kafka_config: $ref: '#/components/schemas/KafkaConfig' type: object - RouterConfig_config: - example: - enricher: - service_account: secret-name-for-google-service-account - image: image - endpoint: endpoint - updated_at: 2000-01-23T04:56:07.000+00:00 - port: 5 - created_at: 2000-01-23T04:56:07.000+00:00 - resource_request: - min_replica: 0 - max_replica: 6 - memory_request: memory_request - cpu_request: cpu_request - id: 1 - env: - - name: name - value: value - - name: name - value: value - timeout: timeout - routes: - - endpoint: endpoint - annotations: '{}' - id: id - type: type - timeout: timeout - - endpoint: endpoint - annotations: '{}' - id: id - type: type - timeout: timeout - default_route_id: default_route_id - rules: - - routes: - - routes - - routes - conditions: - - field: field - values: - - values - - values - operator: in - - field: field - values: - - values - - values - operator: in - - routes: - - routes - - routes - conditions: - - field: field - values: - - values - - values - operator: in - - field: field - values: - - values - - values - operator: in - resource_request: - min_replica: 0 - max_replica: 6 - memory_request: memory_request - cpu_request: cpu_request - ensembler: - pyfunc_config: - project_id: 7 - ensembler_id: 9 - resource_request: - min_replica: 0 - max_replica: 6 - memory_request: memory_request - cpu_request: cpu_request - timeout: timeout - updated_at: 2000-01-23T04:56:07.000+00:00 - standard_config: - experiment_mappings: - - treatment: treatment-1 - route: route-1 - experiment: experiment-1 - - treatment: treatment-1 - route: route-1 - experiment: experiment-1 - created_at: 2000-01-23T04:56:07.000+00:00 - id: 5 - type: standard - docker_config: - service_account: secret-name-for-google-service-account - image: image - endpoint: endpoint - port: 2 - resource_request: - min_replica: 0 - max_replica: 6 - memory_request: memory_request - cpu_request: cpu_request - env: - - name: name - value: value - - name: name - value: value - timeout: timeout - log_config: - bigquery_config: - batch_load: true - table: table - service_account_secret: service_account_secret - kafka_config: - brokers: brokers - topic: topic - serialization_format: json - timeout: timeout - experiment_engine: - type: nop - config: '{}' - properties: - routes: - items: - $ref: '#/components/schemas/Route' - type: array - rules: - items: - $ref: '#/components/schemas/TrafficRule' - type: array - default_route_id: - type: string - experiment_engine: - $ref: '#/components/schemas/ExperimentConfig' - resource_request: - $ref: '#/components/schemas/ResourceRequest' - timeout: - pattern: ^[0-9]+(ms|s|m|h)$ - type: string - log_config: - $ref: '#/components/schemas/RouterConfig_config_log_config' - enricher: - $ref: '#/components/schemas/Enricher' - ensembler: - $ref: '#/components/schemas/RouterEnsemblerConfig' - required: - - default_route_id - - experiment_engine - - log_config - - routes - - timeout - type: object StandardExperimentEngine_allOf_standard_experiment_manager_config: nullable: true properties: diff --git a/api/api/specs/routers.yaml b/api/api/specs/routers.yaml index 0d23d0c6a..ab12eb234 100644 --- a/api/api/specs/routers.yaml +++ b/api/api/specs/routers.yaml @@ -22,7 +22,7 @@ paths: parameters: - in: "path" name: "project_id" - description: "project id of the project to retrieve routers from" + description: "id of the project to retrieve routers from" schema: <<: *id required: true @@ -45,7 +45,7 @@ paths: parameters: - in: "path" name: "project_id" - description: "project id of the project to save router" + description: "id of the project to save router" schema: <<: *id required: true @@ -74,7 +74,7 @@ paths: parameters: - in: "path" name: "project_id" - description: "project id of the project to retrieve routers from" + description: "id of the project to retrieve routers from" schema: <<: *id required: true @@ -101,7 +101,7 @@ paths: parameters: - in: "path" name: "project_id" - description: "project id of the project of the router" + description: "id of the project of the router" schema: <<: *id required: true @@ -136,7 +136,7 @@ paths: parameters: - in: "path" name: "project_id" - description: "project id of the project of the router" + description: "id of the project of the router" schema: <<: *id required: true @@ -227,7 +227,7 @@ paths: parameters: - in: "path" name: "project_id" - description: "project id of the project to retrieve routers from" + description: "id of the project to retrieve routers from" schema: <<: *id required: true @@ -250,6 +250,40 @@ paths: description: "Invalid project_id or router_id" 404: description: "No router versions found" + post: + tags: *tags + summary: "Create router version without deploying it" + parameters: + - in: "path" + name: "project_id" + description: "id of the project of the router" + schema: + <<: *id + required: true + - in: "path" + name: "router_id" + description: "id of the router to create a new version for" + schema: + <<: *id + required: true + requestBody: + description: "router configuration to save" + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/RouterVersionConfig" + responses: + 200: + description: "OK" + content: + application/json: + schema: + $ref: "#/components/schemas/RouterVersion" + 400: + description: "Invalid project_id, router_id or router configuration" + 500: + description: "Unable to save configuration" "/projects/{project_id}/routers/{router_id}/versions/{version}": get: @@ -258,7 +292,7 @@ paths: parameters: - in: "path" name: "project_id" - description: "project id of the project to retrieve routers from" + description: "id of the project to retrieve routers from" schema: <<: *id required: true @@ -292,7 +326,7 @@ paths: parameters: - in: "path" name: "project_id" - description: "project id of the project of the router" + description: "id of the project of the router" schema: <<: *id required: true @@ -565,43 +599,46 @@ components: type: "string" pattern: '^[a-z0-9-]*$' config: + $ref: "#/components/schemas/RouterVersionConfig" + + RouterVersionConfig: + type: "object" + required: + - routes + - default_route_id + - experiment_engine + - timeout + - log_config + properties: + routes: + type: "array" + items: + $ref: "#/components/schemas/Route" + rules: + type: "array" + items: + $ref: "#/components/schemas/TrafficRule" + default_route_id: + type: "string" + experiment_engine: + $ref: "experiment-engines.yaml#/components/schemas/ExperimentConfig" + resource_request: + $ref: "#/components/schemas/ResourceRequest" + timeout: + <<: *timeout + log_config: type: "object" - required: - - routes - - default_route_id - - experiment_engine - - timeout - - log_config properties: - routes: - type: "array" - items: - $ref: "#/components/schemas/Route" - rules: - type: "array" - items: - $ref: "#/components/schemas/TrafficRule" - default_route_id: - type: "string" - experiment_engine: - $ref: "experiment-engines.yaml#/components/schemas/ExperimentConfig" - resource_request: - $ref: "#/components/schemas/ResourceRequest" - timeout: - <<: *timeout - log_config: - type: "object" - properties: - result_logger_type: - $ref: "#/components/schemas/ResultLoggerType" - bigquery_config: - $ref: "#/components/schemas/BigQueryConfig" - kafka_config: - $ref: "#/components/schemas/KafkaConfig" - enricher: - $ref: "#/components/schemas/Enricher" - ensembler: - $ref: "#/components/schemas/RouterEnsemblerConfig" + result_logger_type: + $ref: "#/components/schemas/ResultLoggerType" + bigquery_config: + $ref: "#/components/schemas/BigQueryConfig" + kafka_config: + $ref: "#/components/schemas/KafkaConfig" + enricher: + $ref: "#/components/schemas/Enricher" + ensembler: + $ref: "#/components/schemas/RouterEnsemblerConfig" Route: type: "object" diff --git a/api/e2e/test/00_all_e2e_test.go b/api/e2e/test/00_all_e2e_test.go index 2ad3d746b..8954edf10 100644 --- a/api/e2e/test/00_all_e2e_test.go +++ b/api/e2e/test/00_all_e2e_test.go @@ -70,6 +70,7 @@ func TestEndToEnd(t *testing.T) { t.Parallel() t.Run("CreateRouter_KnativeServices", TestCreateRouter) t.Run("UpdateRouter_InvalidConfig", TestUpdateRouterInvalidConfig) + t.Run("CreateRouterVersion", TestCreateRouterVersion) t.Run("UndeployRouter", TestUndeployRouter) t.Run("DeployRouterVersion_InvalidConfig", TestDeployRouterInvalidConfig) t.Run("DeployRouter", TestDeployValidConfig) diff --git a/api/e2e/test/03_create_router_version_test.go b/api/e2e/test/03_create_router_version_test.go new file mode 100644 index 000000000..3878fdaae --- /dev/null +++ b/api/e2e/test/03_create_router_version_test.go @@ -0,0 +1,148 @@ +//go:build e2e +// +build e2e + +package e2e + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "net/http" + "path/filepath" + "testing" + + "github.com/tidwall/gjson" + + "github.com/gojek/turing/api/turing/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +/* +Pre: +testCreateRouter run successfully. +Steps: +Create a router version with a valid router config +b. GET router version 3 > shows status "undeployed" +c. GET router > config still shows version 1 +d. Test cluster state that all deployments exist +e. Make a request to the router, validate the response. +*/ +func TestCreateRouterVersion(t *testing.T) { + // Read existing router that MUST have already exists from previous create router e2e test + // Router name is assumed to follow this format: e2e-experiment-{{.TestID}} + routerName := "e2e-experiment-" + globalTestContext.TestID + t.Log(fmt.Sprintf("Retrieving router with name '%s' created from previous test step", routerName)) + existingRouter, err := getRouterByName( + globalTestContext.httpClient, globalTestContext.APIBasePath, globalTestContext.ProjectID, routerName) + require.NoError(t, err) + + // Read router config test data + data := makeRouterPayload(filepath.Join("testdata", "create_router_version.json.tmpl"), globalTestContext) + + // Update router + url := fmt.Sprintf( + "%s/projects/%d/routers/%d/versions", + globalTestContext.APIBasePath, + globalTestContext.ProjectID, + existingRouter.ID, + ) + t.Log("Creating router version: POST " + url) + req, err := http.NewRequest("POST", url, bytes.NewReader(data)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + resp, err := globalTestContext.httpClient.Do(req) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode, readBody(t, resp)) + + // Get router version 3 + t.Log("Testing GET router version") + routerVersion, err := getRouterVersion( + globalTestContext.httpClient, + globalTestContext.APIBasePath, + globalTestContext.ProjectID, + int(existingRouter.ID), + 3, + ) + require.NoError(t, err) + assert.Equal(t, models.RouterVersionStatusUndeployed, routerVersion.Status) + + t.Log("Ensure existing router did not update the version to the new version i.e. the version still unchanged at 1") + router, err := getRouter( + globalTestContext.httpClient, + globalTestContext.APIBasePath, + globalTestContext.ProjectID, + int(existingRouter.ID), + ) + require.NoError(t, err) + require.NotNil(t, router.CurrRouterVersion) + assert.Equal(t, 1, int(router.CurrRouterVersion.Version)) + + downstream, err := getRouterDownstream(globalTestContext.clusterClients, + globalTestContext.ProjectName, + fmt.Sprintf("%s-turing-router", router.Name)) + assert.NoError(t, err) + assert.Equal(t, downstream, fmt.Sprintf("%s-turing-router-%d.%s.%s", + router.Name, 1, globalTestContext.ProjectName, globalTestContext.KServiceDomain)) + + // Check that previous enricher, router, ensembler deployments exist + t.Log("Checking cluster state") + assert.True(t, isDeploymentExists( + globalTestContext.clusterClients, + globalTestContext.ProjectName, + fmt.Sprintf("%s-turing-router-%d-0-deployment", router.Name, 1))) + assert.True(t, isDeploymentExists( + globalTestContext.clusterClients, + globalTestContext.ProjectName, + fmt.Sprintf("%s-turing-enricher-%d-0-deployment", router.Name, 1))) + assert.True(t, isDeploymentExists( + globalTestContext.clusterClients, + globalTestContext.ProjectName, + fmt.Sprintf("%s-turing-ensembler-%d-0-deployment", router.Name, 1))) + + // Make request to router + t.Log("Testing router endpoint") + router, err = getRouter( + globalTestContext.httpClient, + globalTestContext.APIBasePath, + globalTestContext.ProjectID, + int(router.ID), + ) + require.NoError(t, err) + assert.Equal(t, + fmt.Sprintf( + "http://%s-turing-router.%s.%s/v1/predict", + router.Name, + globalTestContext.ProjectName, + globalTestContext.KServiceDomain, + ), + router.Endpoint, + ) + req, err = http.NewRequestWithContext( + context.Background(), + http.MethodPost, + router.Endpoint, + ioutil.NopCloser(bytes.NewReader([]byte(`{}`))), + ) + require.NoError(t, err) + resp, err = globalTestContext.httpClient.Do(req) + require.NoError(t, err) + responseBytes, err := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + require.NoError(t, err) + actualResponse := gjson.GetBytes(responseBytes, "json.response").String() + expectedResponse := `{ + "experiment": {}, + "route_responses": [ + { + "data": { + "version": "control" + }, + "is_default": true, + "route": "control" + } + ] + }` + assert.JSONEq(t, expectedResponse, actualResponse) +} diff --git a/api/e2e/test/03_undeploy_router_test.go b/api/e2e/test/04_undeploy_router_test.go similarity index 100% rename from api/e2e/test/03_undeploy_router_test.go rename to api/e2e/test/04_undeploy_router_test.go diff --git a/api/e2e/test/04_deploy_invalid_config_test.go b/api/e2e/test/05_deploy_invalid_config_test.go similarity index 100% rename from api/e2e/test/04_deploy_invalid_config_test.go rename to api/e2e/test/05_deploy_invalid_config_test.go diff --git a/api/e2e/test/05_deploy_valid_config_test.go b/api/e2e/test/06_deploy_valid_config_test.go similarity index 100% rename from api/e2e/test/05_deploy_valid_config_test.go rename to api/e2e/test/06_deploy_valid_config_test.go diff --git a/api/e2e/test/06_delete_router_test.go b/api/e2e/test/07_delete_router_test.go similarity index 100% rename from api/e2e/test/06_delete_router_test.go rename to api/e2e/test/07_delete_router_test.go diff --git a/api/e2e/test/07_deploy_router_with_traffic_rules_test.go b/api/e2e/test/08_deploy_router_with_traffic_rules_test.go similarity index 100% rename from api/e2e/test/07_deploy_router_with_traffic_rules_test.go rename to api/e2e/test/08_deploy_router_with_traffic_rules_test.go diff --git a/api/e2e/test/testdata/create_router_version.json.tmpl b/api/e2e/test/testdata/create_router_version.json.tmpl new file mode 100644 index 000000000..908c90734 --- /dev/null +++ b/api/e2e/test/testdata/create_router_version.json.tmpl @@ -0,0 +1,63 @@ +{ + "routes": [ + { + "id": "control", + "type": "PROXY", + "endpoint": "{{.MockserverEndpoint}}/control", + "timeout": "5s" + } + ], + "default_route_id": "control", + "experiment_engine": { + "type": "nop" + }, + "resource_request": { + "min_replica": 1, + "max_replica": 1, + "cpu_request": "200m", + "memory_request": "250Mi" + }, + "timeout": "5s", + "log_config": { + "result_logger_type": "nop" + }, + "enricher": { + "image": "{{.TestEchoImage}}", + "resource_request": { + "min_replica": 1, + "max_replica": 1, + "cpu_request": "10", + "memory_request": "1Gi" + }, + "endpoint": "anything", + "timeout": "2s", + "port": 80, + "env": [ + { + "name": "TEST_ENV", + "value": "enricher" + } + ] + }, + "ensembler": { + "type": "docker", + "docker_config": { + "image": "{{.TestEchoImage}}", + "resource_request": { + "min_replica": 2, + "max_replica": 2, + "cpu_request": "200m", + "memory_request": "256Mi" + }, + "endpoint": "anything", + "timeout": "3s", + "port": 80, + "env": [ + { + "name": "TEST_ENV", + "value": "ensembler" + } + ] + } + } +} diff --git a/api/turing/api/request/request.go b/api/turing/api/request/request.go index 93cbf1195..96cce2585 100644 --- a/api/turing/api/request/request.go +++ b/api/turing/api/request/request.go @@ -107,58 +107,55 @@ func (r CreateOrUpdateRouterRequest) BuildRouter(projectID models.ID) *models.Ro } // BuildRouterVersion builds the router version model from the entire request payload -func (r CreateOrUpdateRouterRequest) BuildRouterVersion( +func (r RouterConfig) BuildRouterVersion( router *models.Router, defaults *config.RouterDefaults, cryptoSvc service.CryptoService, expSvc service.ExperimentsService, ensemblersSvc service.EnsemblersService, ) (rv *models.RouterVersion, err error) { - if r.Config == nil { - return nil, errors.New("router config is empty") - } rv = &models.RouterVersion{ RouterID: router.ID, Router: router, Image: defaults.Image, Status: models.RouterVersionStatusPending, - Routes: r.Config.Routes, - DefaultRouteID: r.Config.DefaultRouteID, - TrafficRules: r.Config.TrafficRules, + Routes: r.Routes, + DefaultRouteID: r.DefaultRouteID, + TrafficRules: r.TrafficRules, ExperimentEngine: &models.ExperimentEngine{ - Type: r.Config.ExperimentEngine.Type, + Type: r.ExperimentEngine.Type, }, - ResourceRequest: r.Config.ResourceRequest, - Timeout: r.Config.Timeout, + ResourceRequest: r.ResourceRequest, + Timeout: r.Timeout, LogConfig: &models.LogConfig{ LogLevel: routercfg.LogLevel(defaults.LogLevel), CustomMetricsEnabled: defaults.CustomMetricsEnabled, FiberDebugLogEnabled: defaults.FiberDebugLogEnabled, JaegerEnabled: defaults.JaegerEnabled, - ResultLoggerType: r.Config.LogConfig.ResultLoggerType, + ResultLoggerType: r.LogConfig.ResultLoggerType, }, } - if r.Config.Enricher != nil { - rv.Enricher = r.Config.Enricher.BuildEnricher() + if r.Enricher != nil { + rv.Enricher = r.Enricher.BuildEnricher() } - if r.Config.Ensembler != nil { + if r.Ensembler != nil { // Ensure ensembler config is set based on the ensembler type - if r.Config.Ensembler.Type == models.EnsemblerDockerType && r.Config.Ensembler.DockerConfig == nil { + if r.Ensembler.Type == models.EnsemblerDockerType && r.Ensembler.DockerConfig == nil { return nil, errors.New("missing ensembler docker config") } - if r.Config.Ensembler.Type == models.EnsemblerStandardType && r.Config.Ensembler.StandardConfig == nil { + if r.Ensembler.Type == models.EnsemblerStandardType && r.Ensembler.StandardConfig == nil { return nil, errors.New("missing ensembler standard config") } - if r.Config.Ensembler.Type == models.EnsemblerPyFuncType { - if r.Config.Ensembler.PyfuncConfig == nil { + if r.Ensembler.Type == models.EnsemblerPyFuncType { + if r.Ensembler.PyfuncConfig == nil { return nil, errors.New("missing ensembler pyfunc reference config") } // Verify if the ensembler given by its ProjectID and EnsemblerID exist ensembler, err := ensemblersSvc.FindByID( - *r.Config.Ensembler.PyfuncConfig.EnsemblerID, + *r.Ensembler.PyfuncConfig.EnsemblerID, service.EnsemblersFindByIDOptions{ - ProjectID: r.Config.Ensembler.PyfuncConfig.ProjectID, + ProjectID: r.Ensembler.PyfuncConfig.ProjectID, }) if err != nil { return nil, fmt.Errorf("failed to find specified ensembler: %w", err) @@ -172,20 +169,20 @@ func (r CreateOrUpdateRouterRequest) BuildRouterVersion( return nil, fmt.Errorf("only pyfunc ensemblers allowed; ensembler type given: %T", v) } } - rv.Ensembler = r.Config.Ensembler + rv.Ensembler = r.Ensembler } switch rv.LogConfig.ResultLoggerType { case models.BigQueryLogger: rv.LogConfig.BigQueryConfig = &models.BigQueryConfig{ - Table: r.Config.LogConfig.BigQueryConfig.Table, - ServiceAccountSecret: r.Config.LogConfig.BigQueryConfig.ServiceAccountSecret, + Table: r.LogConfig.BigQueryConfig.Table, + ServiceAccountSecret: r.LogConfig.BigQueryConfig.ServiceAccountSecret, BatchLoad: true, // default for now } case models.KafkaLogger: rv.LogConfig.KafkaConfig = &models.KafkaConfig{ - Brokers: r.Config.LogConfig.KafkaConfig.Brokers, - Topic: r.Config.LogConfig.KafkaConfig.Topic, - SerializationFormat: r.Config.LogConfig.KafkaConfig.SerializationFormat, + Brokers: r.LogConfig.KafkaConfig.Brokers, + Topic: r.LogConfig.KafkaConfig.Topic, + SerializationFormat: r.LogConfig.KafkaConfig.SerializationFormat, } } if rv.ExperimentEngine.Type != models.ExperimentEngineTypeNop { @@ -203,15 +200,15 @@ func (r CreateOrUpdateRouterRequest) BuildRouterVersion( } // BuildExperimentEngineConfig creates the Experiment config from the given input properties -func (r CreateOrUpdateRouterRequest) BuildExperimentEngineConfig( +func (r RouterConfig) BuildExperimentEngineConfig( router *models.Router, cryptoSvc service.CryptoService, expSvc service.ExperimentsService, ) (json.RawMessage, error) { - rawExpConfig := r.Config.ExperimentEngine.Config + rawExpConfig := r.ExperimentEngine.Config // Handle missing passkey / encrypt it in Standard experiment config - if expSvc.IsStandardExperimentManager(r.Config.ExperimentEngine.Type) { + if expSvc.IsStandardExperimentManager(r.ExperimentEngine.Type) { // Convert the new config to the standard type expConfig, err := manager.ParseStandardExperimentConfig(rawExpConfig) if err != nil { @@ -221,7 +218,7 @@ func (r CreateOrUpdateRouterRequest) BuildExperimentEngineConfig( if expConfig.Client.Passkey == "" { // Extract existing router version config if router.CurrRouterVersion != nil && - router.CurrRouterVersion.ExperimentEngine.Type == r.Config.ExperimentEngine.Type { + router.CurrRouterVersion.ExperimentEngine.Type == r.ExperimentEngine.Type { currVerExpConfig, err := manager.ParseStandardExperimentConfig(router.CurrRouterVersion.ExperimentEngine.Config) if err != nil { return nil, fmt.Errorf("Error parsing existing experiment config: %v", err) diff --git a/api/turing/api/request/request_test.go b/api/turing/api/request/request_test.go index b46e53ac4..1abb25ba7 100644 --- a/api/turing/api/request/request_test.go +++ b/api/turing/api/request/request_test.go @@ -65,23 +65,40 @@ func makeTuringExperimentConfig(clientPasskey string) json.RawMessage { return expEngineConfig } -var createOrUpdateRequest = CreateOrUpdateRouterRequest{ - Environment: "env", - Name: "router", - Config: &RouterConfig{ - Routes: []*models.Route{ - { - ID: "default", - Type: "PROXY", - Endpoint: "endpoint", - Timeout: "6s", - }, +var validRouterConfig = RouterConfig{ + Routes: []*models.Route{ + { + ID: "default", + Type: "PROXY", + Endpoint: "endpoint", + Timeout: "6s", }, - DefaultRouteID: "default", - ExperimentEngine: &ExperimentEngineConfig{ - Type: "standard", - Config: makeTuringExperimentConfig("dummy_passkey"), + }, + DefaultRouteID: "default", + ExperimentEngine: &ExperimentEngineConfig{ + Type: "standard", + Config: makeTuringExperimentConfig("dummy_passkey"), + }, + ResourceRequest: &models.ResourceRequest{ + MinReplica: 0, + MaxReplica: 5, + CPURequest: resource.Quantity{ + Format: "500M", + }, + MemoryRequest: resource.Quantity{ + Format: "1G", + }, + }, + Timeout: "10s", + LogConfig: &LogConfig{ + ResultLoggerType: "bigquery", + BigQueryConfig: &BigQueryConfig{ + Table: "project.dataset.table", + ServiceAccountSecret: "service_account", }, + }, + Enricher: &EnricherEnsemblerConfig{ + Image: "lala", ResourceRequest: &models.ResourceRequest{ MinReplica: 0, MaxReplica: 5, @@ -92,67 +109,63 @@ var createOrUpdateRequest = CreateOrUpdateRouterRequest{ Format: "1G", }, }, - Timeout: "10s", - LogConfig: &LogConfig{ - ResultLoggerType: "bigquery", - BigQueryConfig: &BigQueryConfig{ - Table: "project.dataset.table", - ServiceAccountSecret: "service_account", + Endpoint: "endpoint", + Timeout: "6s", + Port: 8080, + Env: []*models.EnvVar{ + { + Name: "key", + Value: "value", }, }, - Enricher: &EnricherEnsemblerConfig{ - Image: "lala", + }, + Ensembler: &models.Ensembler{ + Type: "docker", + DockerConfig: &models.EnsemblerDockerConfig{ + Image: "nginx", ResourceRequest: &models.ResourceRequest{ - MinReplica: 0, - MaxReplica: 5, - CPURequest: resource.Quantity{ - Format: "500M", - }, - MemoryRequest: resource.Quantity{ - Format: "1G", - }, - }, - Endpoint: "endpoint", - Timeout: "6s", - Port: 8080, - Env: []*models.EnvVar{ - { - Name: "key", - Value: "value", - }, - }, - }, - Ensembler: &models.Ensembler{ - Type: "docker", - DockerConfig: &models.EnsemblerDockerConfig{ - Image: "nginx", - ResourceRequest: &models.ResourceRequest{ - CPURequest: resource.Quantity{Format: "500m"}, - MemoryRequest: resource.Quantity{Format: "1Gi"}, - }, - Timeout: "5s", + CPURequest: resource.Quantity{Format: "500m"}, + MemoryRequest: resource.Quantity{Format: "1Gi"}, }, + Timeout: "5s", }, }, } -var createOrUpdateInvalidRequest = CreateOrUpdateRouterRequest{ - Environment: "env", - Name: "router", - Config: &RouterConfig{ - Routes: []*models.Route{ - { - ID: "default", - Type: "PROXY", - Endpoint: "endpoint", - Timeout: "6s", - }, +var invalidRouterConfig = RouterConfig{ + Routes: []*models.Route{ + { + ID: "default", + Type: "PROXY", + Endpoint: "endpoint", + Timeout: "6s", }, - DefaultRouteID: "default", - ExperimentEngine: &ExperimentEngineConfig{ - Type: "standard", - Config: makeTuringExperimentConfig("dummy_passkey"), + }, + DefaultRouteID: "default", + ExperimentEngine: &ExperimentEngineConfig{ + Type: "standard", + Config: makeTuringExperimentConfig("dummy_passkey"), + }, + ResourceRequest: &models.ResourceRequest{ + MinReplica: 0, + MaxReplica: 5, + CPURequest: resource.Quantity{ + Format: "500M", }, + MemoryRequest: resource.Quantity{ + Format: "1G", + }, + }, + Timeout: "10s", + LogConfig: &LogConfig{ + ResultLoggerType: "bigquery", + BigQueryConfig: &BigQueryConfig{ + Table: "project.dataset.table", + ServiceAccountSecret: "service_account", + }, + }, + Enricher: &EnricherEnsemblerConfig{ + Image: "lala", ResourceRequest: &models.ResourceRequest{ MinReplica: 0, MaxReplica: 5, @@ -163,51 +176,36 @@ var createOrUpdateInvalidRequest = CreateOrUpdateRouterRequest{ Format: "1G", }, }, - Timeout: "10s", - LogConfig: &LogConfig{ - ResultLoggerType: "bigquery", - BigQueryConfig: &BigQueryConfig{ - Table: "project.dataset.table", - ServiceAccountSecret: "service_account", + Endpoint: "endpoint", + Timeout: "6s", + Port: 8080, + Env: []*models.EnvVar{ + { + Name: "key", + Value: "value", }, }, - Enricher: &EnricherEnsemblerConfig{ - Image: "lala", + }, + Ensembler: &models.Ensembler{ + Type: "pyfunc", + PyfuncConfig: &models.EnsemblerPyfuncConfig{ + ProjectID: models.NewID(11), + EnsemblerID: models.NewID(12), ResourceRequest: &models.ResourceRequest{ - MinReplica: 0, - MaxReplica: 5, - CPURequest: resource.Quantity{ - Format: "500M", - }, - MemoryRequest: resource.Quantity{ - Format: "1G", - }, - }, - Endpoint: "endpoint", - Timeout: "6s", - Port: 8080, - Env: []*models.EnvVar{ - { - Name: "key", - Value: "value", - }, - }, - }, - Ensembler: &models.Ensembler{ - Type: "pyfunc", - PyfuncConfig: &models.EnsemblerPyfuncConfig{ - ProjectID: models.NewID(11), - EnsemblerID: models.NewID(12), - ResourceRequest: &models.ResourceRequest{ - CPURequest: resource.Quantity{Format: "500m"}, - MemoryRequest: resource.Quantity{Format: "1Gi"}, - }, - Timeout: "5s", + CPURequest: resource.Quantity{Format: "500m"}, + MemoryRequest: resource.Quantity{Format: "1Gi"}, }, + Timeout: "5s", }, }, } +var createOrUpdateRequest = CreateOrUpdateRouterRequest{ + Environment: "env", + Name: "router", + Config: &validRouterConfig, +} + func TestRequestBuildRouter(t *testing.T) { projectID := models.ID(1) expected := &models.Router{ @@ -309,7 +307,7 @@ func TestRequestBuildRouterVersionLoggerConfiguration(t *testing.T) { // Set up mock Ensembler service ensemblerSvc := &mocks.EnsemblersService{} - got, err := baseRequest.BuildRouterVersion(router, &routerDefault, cryptoSvc, expSvc, ensemblerSvc) + got, err := baseRequest.Config.BuildRouterVersion(router, &routerDefault, cryptoSvc, expSvc, ensemblerSvc) assert.NoError(t, err) assert.Equal(t, got.LogConfig, tt.expectedLogConfig) }) @@ -418,7 +416,7 @@ func TestRequestBuildRouterVersionWithDefaults(t *testing.T) { // Set up mock Ensembler service ensemblerSvc := &mocks.EnsemblersService{} - got, err := createOrUpdateRequest.BuildRouterVersion(router, &defaults, cryptoSvc, expSvc, ensemblerSvc) + got, err := validRouterConfig.BuildRouterVersion(router, &defaults, cryptoSvc, expSvc, ensemblerSvc) require.NoError(t, err) expected.Model = got.Model assertgotest.DeepEqual(t, expected, *got) @@ -455,7 +453,7 @@ func TestRequestBuildRouterVersionWithUnavailablePyFuncEnsembler(t *testing.T) { ensemblerSvc.On("FindByID", mock.Anything, mock.Anything).Return(nil, errors.New("record not found")) // Update the router with an invalid request - got, err := createOrUpdateInvalidRequest.BuildRouterVersion(router, &defaults, cryptoSvc, expSvc, ensemblerSvc) + got, err := invalidRouterConfig.BuildRouterVersion(router, &defaults, cryptoSvc, expSvc, ensemblerSvc) assert.EqualError(t, err, "failed to find specified ensembler: record not found") assert.Nil(t, got) @@ -475,42 +473,36 @@ func TestBuildExperimentEngineConfig(t *testing.T) { // Define tests tests := map[string]struct { - req CreateOrUpdateRouterRequest + req RouterConfig router *models.Router expected json.RawMessage err string }{ "failure | std engine | missing curr version passkey": { - req: CreateOrUpdateRouterRequest{ - Config: &RouterConfig{ - ExperimentEngine: &ExperimentEngineConfig{ - Type: "standard-manager", - Config: json.RawMessage(`{"client": {"username": "client-name"}}`), - }, + req: RouterConfig{ + ExperimentEngine: &ExperimentEngineConfig{ + Type: "standard-manager", + Config: json.RawMessage(`{"client": {"username": "client-name"}}`), }, }, router: &models.Router{}, err: "Passkey must be configured", }, "failure | std engine | encrypt passkey error": { - req: CreateOrUpdateRouterRequest{ - Config: &RouterConfig{ - ExperimentEngine: &ExperimentEngineConfig{ - Type: "standard-manager", - Config: json.RawMessage(`{"client": {"username": "client-name", "passkey": "passkey-bad"}}`), - }, + req: RouterConfig{ + ExperimentEngine: &ExperimentEngineConfig{ + Type: "standard-manager", + Config: json.RawMessage(`{"client": {"username": "client-name", "passkey": "passkey-bad"}}`), }, }, router: &models.Router{}, err: "Passkey could not be encrypted: test-encrypt-error", }, "success | std engine | use curr version passkey": { - req: CreateOrUpdateRouterRequest{ - Config: &RouterConfig{ - ExperimentEngine: &ExperimentEngineConfig{ - Type: "standard-manager", - Config: json.RawMessage(`{"client": {"username": "client-name"}}`), - }, + req: RouterConfig{ + ExperimentEngine: &ExperimentEngineConfig{ + Type: "standard-manager", + Config: json.RawMessage(`{"client": {"username": "client-name"}}`), }, }, router: &models.Router{ @@ -528,12 +520,10 @@ func TestBuildExperimentEngineConfig(t *testing.T) { }`), }, "success | std engine | use new passkey": { - req: CreateOrUpdateRouterRequest{ - Config: &RouterConfig{ - ExperimentEngine: &ExperimentEngineConfig{ - Type: "standard-manager", - Config: json.RawMessage(`{"client": {"username": "client-name", "passkey": "passkey"}}`), - }, + req: RouterConfig{ + ExperimentEngine: &ExperimentEngineConfig{ + Type: "standard-manager", + Config: json.RawMessage(`{"client": {"username": "client-name", "passkey": "passkey"}}`), }, }, router: &models.Router{}, @@ -544,12 +534,10 @@ func TestBuildExperimentEngineConfig(t *testing.T) { }`), }, "success | custom engine": { - req: CreateOrUpdateRouterRequest{ - Config: &RouterConfig{ - ExperimentEngine: &ExperimentEngineConfig{ - Type: "custom-manager", - Config: json.RawMessage("[1, 2]"), - }, + req: RouterConfig{ + ExperimentEngine: &ExperimentEngineConfig{ + Type: "custom-manager", + Config: json.RawMessage("[1, 2]"), }, }, router: &models.Router{}, diff --git a/api/turing/api/router_versions_api.go b/api/turing/api/router_versions_api.go index 67a0997b1..b26be6b90 100644 --- a/api/turing/api/router_versions_api.go +++ b/api/turing/api/router_versions_api.go @@ -4,6 +4,7 @@ import ( "net/http" mlp "github.com/gojek/mlp/api/client" + "github.com/gojek/turing/api/turing/api/request" "github.com/gojek/turing/api/turing/log" "github.com/gojek/turing/api/turing/models" ) @@ -33,6 +34,44 @@ func (c RouterVersionsController) ListRouterVersions( return Ok(routerVersions) } +// CreateRouterVersion creates a router version from the provided configuration. If no router exists +// within the provided project with the provided id, this method will throw an error. +// If the update is valid, a new RouterVersion will be created but NOT deployed. +func (c RouterVersionsController) CreateRouterVersion(r *http.Request, vars RequestVars, body interface{}) *Response { + // Parse request vars + var errResp *Response + var router *models.Router + if _, errResp = c.getProjectFromRequestVars(vars); errResp != nil { + return errResp + } + if router, errResp = c.getRouterFromRequestVars(vars); errResp != nil { + return errResp + } + + request := body.(*request.RouterConfig) + + // Create new version + var routerVersion *models.RouterVersion + if request == nil { + return InternalServerError("unable to create router version", "router config is empty") + } + + routerVersion, err := request.BuildRouterVersion( + router, c.RouterDefaults, c.AppContext.CryptoService, c.AppContext.ExperimentsService, c.EnsemblersService) + + if err == nil { + // Save router version, re-assign the value of err + routerVersion.Status = models.RouterVersionStatusUndeployed + routerVersion, err = c.RouterVersionsService.Save(routerVersion) + } + + if err != nil { + return InternalServerError("unable to create router version", err.Error()) + } + + return Ok(routerVersion) +} + // GetRouterVersion gets the router version for the provided router id and version number. func (c RouterVersionsController) GetRouterVersion( r *http.Request, @@ -139,6 +178,12 @@ func (c RouterVersionsController) Routes() []Route { path: "/projects/{project_id}/routers/{router_id}/versions", handler: c.ListRouterVersions, }, + { + method: http.MethodPost, + path: "/projects/{project_id}/routers/{router_id}/versions", + body: request.RouterConfig{}, + handler: c.CreateRouterVersion, + }, { method: http.MethodGet, path: "/projects/{project_id}/routers/{router_id}/versions/{version}", diff --git a/api/turing/api/router_versions_api_test.go b/api/turing/api/router_versions_api_test.go index 0c0cb7c3d..95584a16a 100644 --- a/api/turing/api/router_versions_api_test.go +++ b/api/turing/api/router_versions_api_test.go @@ -6,6 +6,7 @@ import ( merlin "github.com/gojek/merlin/client" mlp "github.com/gojek/mlp/api/client" + "github.com/gojek/turing/api/turing/api/request" "github.com/gojek/turing/api/turing/config" "github.com/gojek/turing/api/turing/models" "github.com/gojek/turing/api/turing/service/mocks" @@ -81,6 +82,110 @@ func TestListRouterVersions(t *testing.T) { } } +func TestCreateRouterVersion(t *testing.T) { + // Create mock services + // MLP service + mlpSvc := &mocks.MLPService{} + mlpSvc.On("GetProject", models.ID(1)).Return(nil, errors.New("test project error")) + mlpSvc.On("GetProject", models.ID(2)).Return(&mlp.Project{Id: 2}, nil) + mlpSvc.On("GetEnvironment", "dev-invalid").Return(nil, errors.New("test env error")) + mlpSvc.On("GetEnvironment", "dev").Return(&merlin.Environment{}, nil) + + // Router Service + router2 := &models.Router{ + Name: "router2", + ProjectID: 2, + EnvironmentName: "dev", + Model: models.Model{ + ID: 2, + }, + } + routerSvc := &mocks.RoutersService{} + routerSvc.On("FindByID", models.ID(1)). + Return(nil, errors.New("test router error")) + routerSvc.On("FindByID", models.ID(2)).Return(router2, nil) + + // Router Version Service + routerVersion := &models.RouterVersion{ + Router: router2, + RouterID: 2, + ExperimentEngine: &models.ExperimentEngine{ + Type: models.ExperimentEngineTypeNop, + }, + LogConfig: &models.LogConfig{ + ResultLoggerType: models.NopLogger, + }, + Status: models.RouterVersionStatusUndeployed, + } + routerVersionSvc := &mocks.RouterVersionsService{} + routerVersionSvc.On("Save", routerVersion).Return(routerVersion, nil) + + // Define tests + tests := map[string]struct { + vars RequestVars + expected *Response + body *request.RouterConfig + }{ + "failure | bad request (missing project_id)": { + vars: RequestVars{}, + expected: BadRequest("invalid project id", "key project_id not found in vars"), + }, + "failure | not found (project not found)": { + vars: RequestVars{"project_id": {"1"}}, + expected: NotFound("project not found", "test project error"), + }, + "failure | bad request (missing router_id)": { + vars: RequestVars{"project_id": {"2"}}, + expected: BadRequest("invalid router id", "key router_id not found in vars"), + }, + "failure | not found (router_id not found)": { + vars: RequestVars{"project_id": {"2"}, "router_id": {"1"}}, + expected: NotFound("router not found", "test router error"), + }, + "failure | build router version": { + body: nil, + vars: RequestVars{"project_id": {"2"}, "router_id": {"2"}}, + expected: InternalServerError("unable to create router version", "router config is empty"), + }, + "success": { + body: &request.RouterConfig{ + ExperimentEngine: &request.ExperimentEngineConfig{ + Type: "nop", + }, + LogConfig: &request.LogConfig{ + ResultLoggerType: models.NopLogger, + }, + }, + vars: RequestVars{"router_id": {"2"}, "project_id": {"2"}}, + expected: &Response{ + code: 200, + data: routerVersion, + }, + }, + } + + // Run tests + for name, data := range tests { + t.Run(name, func(t *testing.T) { + ctrl := &RouterVersionsController{ + RouterDeploymentController{ + BaseController{ + AppContext: &AppContext{ + MLPService: mlpSvc, + RoutersService: routerSvc, + RouterVersionsService: routerVersionSvc, + RouterDefaults: &config.RouterDefaults{}, + }, + }, + }, + } + // Run test method and validate + response := ctrl.CreateRouterVersion(nil, data.vars, data.body) + assert.Equal(t, data.expected, response) + }) + } +} + func TestGetRouterVersion(t *testing.T) { // Create mock services routerSvc := &mocks.RoutersService{} diff --git a/api/turing/api/routers_api.go b/api/turing/api/routers_api.go index 0940a3142..21c60ecbe 100644 --- a/api/turing/api/routers_api.go +++ b/api/turing/api/routers_api.go @@ -91,7 +91,11 @@ func (c RoutersController) CreateRouter( // then create the new version var routerVersion *models.RouterVersion - rVersion, err := request.BuildRouterVersion( + if request.Config == nil { + return InternalServerError("unable to create router", "router config is empty") + } + + rVersion, err := request.Config.BuildRouterVersion( router, c.RouterDefaults, c.AppContext.CryptoService, c.AppContext.ExperimentsService, c.EnsemblersService) if err == nil { // Save router version @@ -152,7 +156,11 @@ func (c RoutersController) UpdateRouter(r *http.Request, vars RequestVars, body // Create new version var routerVersion *models.RouterVersion - rVersion, err := request.BuildRouterVersion( + if request.Config == nil { + return InternalServerError("unable to update router", "router config is empty") + } + + rVersion, err := request.Config.BuildRouterVersion( router, c.RouterDefaults, c.AppContext.CryptoService, c.AppContext.ExperimentsService, c.EnsemblersService) if err == nil { // Save router version, re-assign the value of err diff --git a/docs/.gitbook/assets/edit_router_header.png b/docs/.gitbook/assets/edit_router_header.png new file mode 100644 index 000000000..465d35aba Binary files /dev/null and b/docs/.gitbook/assets/edit_router_header.png differ diff --git a/docs/.gitbook/assets/edit_router_next_button.png b/docs/.gitbook/assets/edit_router_next_button.png new file mode 100644 index 000000000..23a546c32 Binary files /dev/null and b/docs/.gitbook/assets/edit_router_next_button.png differ diff --git a/docs/.gitbook/assets/edit_router_version_comparison.png b/docs/.gitbook/assets/edit_router_version_comparison.png new file mode 100644 index 000000000..8e38c3e07 Binary files /dev/null and b/docs/.gitbook/assets/edit_router_version_comparison.png differ diff --git a/docs/how-to/viewing-routers/edit-router.md b/docs/how-to/viewing-routers/edit-router.md new file mode 100644 index 000000000..4eacdeb49 --- /dev/null +++ b/docs/how-to/viewing-routers/edit-router.md @@ -0,0 +1,31 @@ +## Edit Router + +Heading to the `Edit Router` page allows you to make changes to the configuration of the selected router. You will +be able to view panels corresponding to various components of a Turing Router. + +![](../../.gitbook/assets/edit_router_header.png) + +Once you are done editing the configuration of your router, click on `Next` to proceed to the following step: + +![](../../.gitbook/assets/edit_router_next_button.png) + +### Version Comparison + +Clicking on next brings you to a version comparison page which shows the differences in the current router +configuration as well as your proposed changes: + +![](../../.gitbook/assets/edit_router_version_comparison.png) + +If you are content with the proposed changes and wish to proceed, there are 2 options available: +- `Save` your proposed changes +- `Deploy` your proposed changes + +Saving your router simply creates a new router version without deploying the new configuration. Your existing router +version remains active. You can visit the `History` tab to find the new router version that you have saved. Note +that this new router version will be in a `Not Deployed` state. + +Deploying a router will create a new router version **and** deploy the new version immediately. If the deployment is +successful, instances of the router with the current version will be taken down and be replaced by the new version. + +If you wish to make any additional changes instead, click on the `Previous` button. You will be returned to the +previous page where you can continue making changes. \ No newline at end of file diff --git a/sdk/requirements.txt b/sdk/requirements.txt index 8249208d1..5bfa2b812 100644 --- a/sdk/requirements.txt +++ b/sdk/requirements.txt @@ -7,3 +7,4 @@ pandas==1.2.2 python-dateutil requests==2.25.1 urllib3 >= 1.25.3 +cloudpickle==1.2.2 diff --git a/sdk/samples/router/edit_a_router.py b/sdk/samples/router/edit_a_router.py new file mode 100644 index 000000000..422005730 --- /dev/null +++ b/sdk/samples/router/edit_a_router.py @@ -0,0 +1,64 @@ +import turing +import turing.batch +import turing.batch.config +import turing.router.config.router_config +import turing.router.config.router_version +from turing.router.config.common.env_var import EnvVar + + +def main(turing_api: str, project: str): + # Initialize Turing client + turing.set_url(turing_api) + turing.set_project(project) + + # Imagine we only have a router's id, and would like to retrieve it + router_1 = turing.Router.get(1) + + # Now we'd like to update the config of router_1, but with some fields modified + # Reminder: When trying to replicate configuration from an existing router, always retrieve the underlying + # `RouterConfig` from the `Router` instance by accessing its `config` attribute. + + # Get the router config from router_1 + new_router_config_to_deploy = router_1.config + + # Make your desired changes to the config + # Note that router_config.enricher.env is a regular Python list; so you can use methods such as append or extend + new_router_config_to_deploy.enricher.env.append( + EnvVar( + name="WORKERS", + value="2" + ) + ) + + new_router_config_to_deploy.resource_request.max_replica = 5 + + # When editing a router, you can either 1. UPDATE the router, which would create a new router version and deploy it + # immediately, or 2. CREATE the router version, which would only create a new router version without deploying it + + # 1. When you UPDATE a router, Turing will save the new version and attempt to deploy it immediately + router_1.update(new_router_config_to_deploy) + + # Notice that the latest router version is pending deployment while the current router version is still active + versions = router_1.list_versions() + for v in versions: + print(v) + + # 2. When you CREATE a router version, Turing will save the new version, but not deploy it. + new_router_config_to_save = router_1.config + new_router_config_to_save.resource_request.min_replica = 0 + + router_1.create_version(new_router_config_to_save) + + # Notice that the latest router version is undeployed (Turing has created the new version without deploying it) + versions = router_1.list_versions() + for v in versions: + print(v) + + # Alternatively, you may also do the following to create a new version for your router without deploying it. + new_undeployed_version = turing.router.config.router_version.RouterVersion.create(new_router_config_to_save, + router_id=1) + + +if __name__ == '__main__': + import fire + fire.Fire(main) diff --git a/sdk/tests/conftest.py b/sdk/tests/conftest.py index e49260dfb..14450a450 100644 --- a/sdk/tests/conftest.py +++ b/sdk/tests/conftest.py @@ -444,7 +444,10 @@ def generic_router_version( id=2, created_at=datetime.now() + timedelta(seconds=20), updated_at=datetime.now() + timedelta(seconds=20), - router=None, + router=turing.generated.models.Router( + environment_name="test_env", + name="test_router" + ), version=1, status=generic_router_version_status, error="NONE", diff --git a/sdk/tests/router/config/log_config_test.py b/sdk/tests/router/config/log_config_test.py index 284bdf965..a6588cd65 100644 --- a/sdk/tests/router/config/log_config_test.py +++ b/sdk/tests/router/config/log_config_test.py @@ -35,7 +35,7 @@ def test_create_result_logger_type(result_logger_type, expected): ResultLoggerType.NOP, None, None, - turing.generated.models.RouterConfigConfigLogConfig( + turing.generated.models.RouterVersionConfigLogConfig( result_logger_type=turing.generated.models.ResultLoggerType('nop'), ) ), @@ -46,7 +46,7 @@ def test_create_result_logger_type(result_logger_type, expected): service_account_secret="my-little-secret" ), None, - turing.generated.models.RouterConfigConfigLogConfig( + turing.generated.models.RouterVersionConfigLogConfig( result_logger_type=turing.generated.models.ResultLoggerType('bigquery'), bigquery_config=turing.generated.models.BigQueryConfig( table="bigqueryproject.bigquerydataset.bigquerytable", @@ -62,7 +62,7 @@ def test_create_result_logger_type(result_logger_type, expected): topic="new_topics", serialization_format="json" ), - turing.generated.models.RouterConfigConfigLogConfig( + turing.generated.models.RouterVersionConfigLogConfig( result_logger_type=turing.generated.models.ResultLoggerType('kafka'), kafka_config=turing.generated.models.KafkaConfig( brokers="1.2.3.4:5678,9.0.1.2:3456", @@ -92,7 +92,7 @@ def test_create_log_config_with_valid_params( "bigqueryproject.bigquerydataset.bigquerytable", "my-little-secret", None, - turing.generated.models.RouterConfigConfigLogConfig( + turing.generated.models.RouterVersionConfigLogConfig( result_logger_type=turing.generated.models.ResultLoggerType('bigquery'), bigquery_config=turing.generated.models.BigQueryConfig( table="bigqueryproject.bigquerydataset.bigquerytable", @@ -135,7 +135,7 @@ def test_create_bigquery_log_config_with_invalid_table(table, service_account_se "1.2.3.4:5678,9.0.1.2:3456", "new_topics", KafkaConfigSerializationFormat.JSON, - turing.generated.models.RouterConfigConfigLogConfig( + turing.generated.models.RouterVersionConfigLogConfig( result_logger_type=turing.generated.models.ResultLoggerType('kafka'), kafka_config=turing.generated.models.KafkaConfig( brokers="1.2.3.4:5678,9.0.1.2:3456", @@ -148,7 +148,7 @@ def test_create_bigquery_log_config_with_invalid_table(table, service_account_se "1.2.3.4:5678,9.0.1.2:3456", "new_topics", KafkaConfigSerializationFormat.PROTOBUF, - turing.generated.models.RouterConfigConfigLogConfig( + turing.generated.models.RouterVersionConfigLogConfig( result_logger_type=turing.generated.models.ResultLoggerType('kafka'), kafka_config=turing.generated.models.KafkaConfig( brokers="1.2.3.4:5678,9.0.1.2:3456", diff --git a/sdk/tests/router/config/router_version_test.py b/sdk/tests/router/config/router_version_test.py new file mode 100644 index 000000000..aa333f5c7 --- /dev/null +++ b/sdk/tests/router/config/router_version_test.py @@ -0,0 +1,58 @@ +import json +import tests +import pytest +import turing +import turing.generated.models + +from turing.router.config.router_version import RouterVersion +from urllib3_mock import Responses + +responses = Responses('requests.packages.urllib3') + + +@pytest.fixture(scope="module", name="responses") +def _responses(): + return responses + + +@responses.activate +def test_create_version(turing_api, active_project, generic_router_config, generic_router_version, use_google_oauth): + turing.set_url(turing_api, use_google_oauth) + turing.set_project(active_project.name) + + test_router_id = 1 + + responses.add( + method="POST", + url=f"/v1/projects/{active_project.id}/routers/{test_router_id}/versions", + body=json.dumps(generic_router_version, default=tests.json_serializer), + status=200, + content_type="application/json" + ) + + actual_response = RouterVersion.create(generic_router_config, test_router_id) + + assert actual_response.id == generic_router_version.id + assert actual_response.monitoring_url == generic_router_version.monitoring_url + assert actual_response.status.value == generic_router_version.status.value + assert actual_response.created_at == generic_router_version.created_at + assert actual_response.updated_at == generic_router_version.updated_at + + assert len(actual_response.rules) == len(generic_router_version.rules) + for i in range((len(actual_response.rules))): + assert actual_response.rules[i].to_open_api() == generic_router_version.rules[i] + + assert actual_response.default_route_id == generic_router_version.default_route_id + assert actual_response.experiment_engine.to_open_api() == generic_router_version.experiment_engine + assert actual_response.resource_request.to_open_api() == generic_router_version.resource_request + assert actual_response.timeout == generic_router_version.timeout + + assert actual_response.log_config.to_open_api().result_logger_type == generic_router_version.log_config.result_logger_type + + assert actual_response.enricher.image == generic_router_version.enricher.image + assert actual_response.enricher.resource_request.to_open_api() == generic_router_version.enricher.resource_request + assert actual_response.enricher.endpoint == generic_router_version.enricher.endpoint + assert actual_response.enricher.timeout == generic_router_version.enricher.timeout + assert actual_response.enricher.port == generic_router_version.enricher.port + + assert actual_response.ensembler.type == generic_router_version.ensembler.type diff --git a/sdk/tests/router/router_test.py b/sdk/tests/router/router_test.py index a3e673bb4..bdbb3104e 100644 --- a/sdk/tests/router/router_test.py +++ b/sdk/tests/router/router_test.py @@ -152,7 +152,7 @@ def test_update_router(turing_api, active_project, actual, expected, use_google_ turing.set_project(active_project.name) base_router = turing.Router( - id=1, + id=191, name="router-1", project_id=active_project.id, environment_name="id-dev", diff --git a/sdk/turing/generated/api/router_api.py b/sdk/turing/generated/api/router_api.py index 7e10488d6..989bffb51 100644 --- a/sdk/turing/generated/api/router_api.py +++ b/sdk/turing/generated/api/router_api.py @@ -28,6 +28,7 @@ from turing.generated.model.router_id_and_version import RouterIdAndVersion from turing.generated.model.router_id_object import RouterIdObject from turing.generated.model.router_version import RouterVersion +from turing.generated.model.router_version_config import RouterVersionConfig class RouterApi(object): @@ -56,7 +57,7 @@ def __projects_project_id_routers_get( >>> result = thread.get() Args: - project_id (int): project id of the project to retrieve routers from + project_id (int): id of the project to retrieve routers from Keyword Args: _return_http_data_only (bool): response data without head status @@ -173,7 +174,7 @@ def __projects_project_id_routers_post( >>> result = thread.get() Args: - project_id (int): project id of the project to save router + project_id (int): id of the project to save router router_config (RouterConfig): router configuration to save Keyword Args: @@ -300,7 +301,7 @@ def __projects_project_id_routers_router_id_delete( >>> result = thread.get() Args: - project_id (int): project id of the project of the router + project_id (int): id of the project of the router router_id (int): id of the router to delete Keyword Args: @@ -678,7 +679,7 @@ def __projects_project_id_routers_router_id_get( >>> result = thread.get() Args: - project_id (int): project id of the project to retrieve routers from + project_id (int): id of the project to retrieve routers from router_id (int): id of the router to be retrieved Keyword Args: @@ -805,7 +806,7 @@ def __projects_project_id_routers_router_id_put( >>> result = thread.get() Args: - project_id (int): project id of the project of the router + project_id (int): id of the project of the router router_id (int): id of the router to update router_config (RouterConfig): router configuration to save @@ -1067,7 +1068,7 @@ def __projects_project_id_routers_router_id_versions_get( >>> result = thread.get() Args: - project_id (int): project id of the project to retrieve routers from + project_id (int): id of the project to retrieve routers from router_id (int): id of the router to be retrieved Keyword Args: @@ -1178,6 +1179,143 @@ def __projects_project_id_routers_router_id_versions_get( callable=__projects_project_id_routers_router_id_versions_get ) + def __projects_project_id_routers_router_id_versions_post( + self, + project_id, + router_id, + router_version_config, + **kwargs + ): + """Create router version without deploying it # noqa: E501 + + This method makes a synchronous HTTP request by default. To make an + asynchronous HTTP request, please pass async_req=True + + >>> thread = api.projects_project_id_routers_router_id_versions_post(project_id, router_id, router_version_config, async_req=True) + >>> result = thread.get() + + Args: + project_id (int): id of the project of the router + router_id (int): id of the router to create a new version for + router_version_config (RouterVersionConfig): router configuration to save + + Keyword Args: + _return_http_data_only (bool): response data without head status + code and headers. Default is True. + _preload_content (bool): if False, the urllib3.HTTPResponse object + will be returned without reading/decoding response data. + Default is True. + _request_timeout (float/tuple): timeout setting for this request. If one + number provided, it will be total request timeout. It can also + be a pair (tuple) of (connection, read) timeouts. + Default is None. + _check_input_type (bool): specifies if type checking + should be done one the data sent to the server. + Default is True. + _check_return_type (bool): specifies if type checking + should be done one the data received from the server. + Default is True. + _host_index (int/None): specifies the index of the server + that we want to use. + Default is read from the configuration. + async_req (bool): execute request asynchronously + + Returns: + RouterVersion + If the method is called asynchronously, returns the request + thread. + """ + kwargs['async_req'] = kwargs.get( + 'async_req', False + ) + kwargs['_return_http_data_only'] = kwargs.get( + '_return_http_data_only', True + ) + kwargs['_preload_content'] = kwargs.get( + '_preload_content', True + ) + kwargs['_request_timeout'] = kwargs.get( + '_request_timeout', None + ) + kwargs['_check_input_type'] = kwargs.get( + '_check_input_type', True + ) + kwargs['_check_return_type'] = kwargs.get( + '_check_return_type', True + ) + kwargs['_host_index'] = kwargs.get('_host_index') + kwargs['project_id'] = \ + project_id + kwargs['router_id'] = \ + router_id + kwargs['router_version_config'] = \ + router_version_config + return self.call_with_http_info(**kwargs) + + self.projects_project_id_routers_router_id_versions_post = _Endpoint( + settings={ + 'response_type': (RouterVersion,), + 'auth': [], + 'endpoint_path': '/projects/{project_id}/routers/{router_id}/versions', + 'operation_id': 'projects_project_id_routers_router_id_versions_post', + 'http_method': 'POST', + 'servers': None, + }, + params_map={ + 'all': [ + 'project_id', + 'router_id', + 'router_version_config', + ], + 'required': [ + 'project_id', + 'router_id', + 'router_version_config', + ], + 'nullable': [ + ], + 'enum': [ + ], + 'validation': [ + ] + }, + root_map={ + 'validations': { + }, + 'allowed_values': { + }, + 'openapi_types': { + 'project_id': + (int,), + 'router_id': + (int,), + 'router_version_config': + (RouterVersionConfig,), + }, + 'attribute_map': { + 'project_id': 'project_id', + 'router_id': 'router_id', + }, + 'location_map': { + 'project_id': 'path', + 'router_id': 'path', + 'router_version_config': 'body', + }, + 'collection_format_map': { + } + }, + headers_map={ + 'accept': [ + 'application/json' + ], + 'content_type': [ + 'application/json' + ] + }, + api_client=api_client, + callable=__projects_project_id_routers_router_id_versions_post + ) + def __projects_project_id_routers_router_id_versions_version_delete( self, project_id, @@ -1194,7 +1332,7 @@ def __projects_project_id_routers_router_id_versions_version_delete( >>> result = thread.get() Args: - project_id (int): project id of the project of the router + project_id (int): id of the project of the router router_id (int): id of the router version (int): version of router configuration to delete @@ -1466,7 +1604,7 @@ def __projects_project_id_routers_router_id_versions_version_get( >>> result = thread.get() Args: - project_id (int): project id of the project to retrieve routers from + project_id (int): id of the project to retrieve routers from router_id (int): id of the router to be retrieved version (int): version of router configuration to be retrieved diff --git a/sdk/turing/generated/model/router_config.py b/sdk/turing/generated/model/router_config.py index 904cbe3cc..6d80e7e7a 100644 --- a/sdk/turing/generated/model/router_config.py +++ b/sdk/turing/generated/model/router_config.py @@ -27,8 +27,8 @@ ) def lazy_import(): - from turing.generated.model.router_config_config import RouterConfigConfig - globals()['RouterConfigConfig'] = RouterConfigConfig + from turing.generated.model.router_version_config import RouterVersionConfig + globals()['RouterVersionConfig'] = RouterVersionConfig class RouterConfig(ModelNormal): @@ -84,7 +84,7 @@ def openapi_types(): return { 'environment_name': (str,), # noqa: E501 'name': (str,), # noqa: E501 - 'config': (RouterConfigConfig,), # noqa: E501 + 'config': (RouterVersionConfig,), # noqa: E501 } @cached_property @@ -116,7 +116,7 @@ def __init__(self, environment_name, name, config, *args, **kwargs): # noqa: E5 Args: environment_name (str): name (str): - config (RouterConfigConfig): + config (RouterVersionConfig): Keyword Args: _check_type (bool): if True, values for parameters in openapi_types diff --git a/sdk/turing/generated/model/router_config_config.py b/sdk/turing/generated/model/router_version_config.py similarity index 95% rename from sdk/turing/generated/model/router_config_config.py rename to sdk/turing/generated/model/router_version_config.py index fe9b1dd86..78de0c7c3 100644 --- a/sdk/turing/generated/model/router_config_config.py +++ b/sdk/turing/generated/model/router_version_config.py @@ -31,19 +31,19 @@ def lazy_import(): from turing.generated.model.experiment_config import ExperimentConfig from turing.generated.model.resource_request import ResourceRequest from turing.generated.model.route import Route - from turing.generated.model.router_config_config_log_config import RouterConfigConfigLogConfig from turing.generated.model.router_ensembler_config import RouterEnsemblerConfig + from turing.generated.model.router_version_config_log_config import RouterVersionConfigLogConfig from turing.generated.model.traffic_rule import TrafficRule globals()['Enricher'] = Enricher globals()['ExperimentConfig'] = ExperimentConfig globals()['ResourceRequest'] = ResourceRequest globals()['Route'] = Route - globals()['RouterConfigConfigLogConfig'] = RouterConfigConfigLogConfig globals()['RouterEnsemblerConfig'] = RouterEnsemblerConfig + globals()['RouterVersionConfigLogConfig'] = RouterVersionConfigLogConfig globals()['TrafficRule'] = TrafficRule -class RouterConfigConfig(ModelNormal): +class RouterVersionConfig(ModelNormal): """NOTE: This class is auto generated by OpenAPI Generator. Ref: https://openapi-generator.tech @@ -98,7 +98,7 @@ def openapi_types(): 'default_route_id': (str,), # noqa: E501 'experiment_engine': (ExperimentConfig,), # noqa: E501 'timeout': (str,), # noqa: E501 - 'log_config': (RouterConfigConfigLogConfig,), # noqa: E501 + 'log_config': (RouterVersionConfigLogConfig,), # noqa: E501 'rules': ([TrafficRule],), # noqa: E501 'resource_request': (ResourceRequest,), # noqa: E501 'enricher': (Enricher,), # noqa: E501 @@ -135,14 +135,14 @@ def discriminator(): @convert_js_args_to_python_args def __init__(self, routes, default_route_id, experiment_engine, timeout, log_config, *args, **kwargs): # noqa: E501 - """RouterConfigConfig - a model defined in OpenAPI + """RouterVersionConfig - a model defined in OpenAPI Args: routes ([Route]): default_route_id (str): experiment_engine (ExperimentConfig): timeout (str): - log_config (RouterConfigConfigLogConfig): + log_config (RouterVersionConfigLogConfig): Keyword Args: _check_type (bool): if True, values for parameters in openapi_types diff --git a/sdk/turing/generated/model/router_config_config_log_config.py b/sdk/turing/generated/model/router_version_config_log_config.py similarity index 98% rename from sdk/turing/generated/model/router_config_config_log_config.py rename to sdk/turing/generated/model/router_version_config_log_config.py index 84f663043..42a255198 100644 --- a/sdk/turing/generated/model/router_config_config_log_config.py +++ b/sdk/turing/generated/model/router_version_config_log_config.py @@ -35,7 +35,7 @@ def lazy_import(): globals()['ResultLoggerType'] = ResultLoggerType -class RouterConfigConfigLogConfig(ModelNormal): +class RouterVersionConfigLogConfig(ModelNormal): """NOTE: This class is auto generated by OpenAPI Generator. Ref: https://openapi-generator.tech @@ -110,7 +110,7 @@ def discriminator(): @convert_js_args_to_python_args def __init__(self, *args, **kwargs): # noqa: E501 - """RouterConfigConfigLogConfig - a model defined in OpenAPI + """RouterVersionConfigLogConfig - a model defined in OpenAPI Keyword Args: _check_type (bool): if True, values for parameters in openapi_types diff --git a/sdk/turing/generated/models/__init__.py b/sdk/turing/generated/models/__init__.py index b04219a04..27c0a9450 100644 --- a/sdk/turing/generated/models/__init__.py +++ b/sdk/turing/generated/models/__init__.py @@ -65,8 +65,6 @@ from turing.generated.model.route import Route from turing.generated.model.router import Router from turing.generated.model.router_config import RouterConfig -from turing.generated.model.router_config_config import RouterConfigConfig -from turing.generated.model.router_config_config_log_config import RouterConfigConfigLogConfig from turing.generated.model.router_details import RouterDetails from turing.generated.model.router_details_all_of import RouterDetailsAllOf from turing.generated.model.router_ensembler_config import RouterEnsemblerConfig @@ -76,6 +74,8 @@ from turing.generated.model.router_id_object import RouterIdObject from turing.generated.model.router_status import RouterStatus from turing.generated.model.router_version import RouterVersion +from turing.generated.model.router_version_config import RouterVersionConfig +from turing.generated.model.router_version_config_log_config import RouterVersionConfigLogConfig from turing.generated.model.router_version_log_config import RouterVersionLogConfig from turing.generated.model.router_version_status import RouterVersionStatus from turing.generated.model.save_mode import SaveMode diff --git a/sdk/turing/router/config/log_config.py b/sdk/turing/router/config/log_config.py index cbdf2e89f..4de8c6e7a 100644 --- a/sdk/turing/router/config/log_config.py +++ b/sdk/turing/router/config/log_config.py @@ -85,7 +85,7 @@ def to_open_api(self) -> OpenApiModel: if self.kafka_config is not None: kwargs["kafka_config"] = self.kafka_config - return turing.generated.models.RouterConfigConfigLogConfig( + return turing.generated.models.RouterVersionConfigLogConfig( result_logger_type=self.result_logger_type.to_open_api(), **kwargs ) diff --git a/sdk/turing/router/config/router_config.py b/sdk/turing/router/config/router_config.py index b8fd81661..492063731 100644 --- a/sdk/turing/router/config/router_config.py +++ b/sdk/turing/router/config/router_config.py @@ -36,8 +36,8 @@ class RouterConfig: :param enricher: enricher config settings to be used with the router :param ensembler: ensembler config settings to be used with the router """ - environment_name: str - name: str + environment_name: str = "" + name: str = "" routes: Union[List[Route], List[Dict[str, str]]] = None rules: Union[List[TrafficRule], List[Dict]] = None default_route_id: str = None @@ -49,8 +49,8 @@ class RouterConfig: ensembler: Union[RouterEnsemblerConfig, Dict] = None def __init__(self, - environment_name: str, - name: str, + environment_name: str = "", + name: str = "", routes: Union[List[Route], List[Dict[str, str]]] = None, rules: Union[List[TrafficRule], List[Dict]] = None, default_route_id: str = None, @@ -220,7 +220,7 @@ def to_open_api(self) -> OpenApiModel: return turing.generated.models.RouterConfig( environment_name=self.environment_name, name=self.name, - config=turing.generated.models.RouterConfigConfig( + config=turing.generated.models.RouterVersionConfig( routes=[route.to_open_api() for route in self.routes], default_route_id=self.default_route_id, experiment_engine=self.experiment_engine.to_open_api(), diff --git a/sdk/turing/router/config/router_version.py b/sdk/turing/router/config/router_version.py index 6eba225dd..acd1759fd 100644 --- a/sdk/turing/router/config/router_version.py +++ b/sdk/turing/router/config/router_version.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +import turing +import turing.generated.models import dataclasses from enum import Enum @@ -53,3 +57,22 @@ def get_config(self) -> RouterConfig: :return: a new RouterConfig instance containing attributes of this router version """ return RouterConfig(**self.to_dict()) + + @classmethod + def create(cls, config: RouterConfig, router_id: int) -> RouterVersion: + """ + Creates a new router version for the router with the given router_id WITHOUT deploying it + + :param config: configuration of router + :param router_id: router id of the router for which this router version will be created + :return: the new router version + """ + version = turing.active_session.create_router_version( + router_id=router_id, + router_version_config=config.to_open_api().config + ) + return RouterVersion( + environment_name=version.router.environment_name, + name=version.router.name, + **version.to_dict() + ) diff --git a/sdk/turing/router/router.py b/sdk/turing/router/router.py index df103a5d9..250750961 100644 --- a/sdk/turing/router/router.py +++ b/sdk/turing/router/router.py @@ -145,6 +145,15 @@ def update(self, config: RouterConfig) -> 'Router': self.__dict__ = updated_router.__dict__ return self + def create_version(self, config: RouterConfig) -> 'RouterVersion': + """ + Creates a new router version for the router WITHOUT deploying it + + :param config: configuration of router + :return: the new router version + """ + return RouterVersion.create(config=config, router_id=self.id) + def deploy(self) -> Dict[str, int]: """ Deploy this router diff --git a/sdk/turing/session.py b/sdk/turing/session.py index e85e3ec3b..23c736b10 100644 --- a/sdk/turing/session.py +++ b/sdk/turing/session.py @@ -8,7 +8,7 @@ from turing.generated.apis import EnsemblerApi, EnsemblingJobApi, ProjectApi, RouterApi from turing.generated.models import (Project, Ensembler, EnsemblingJob, EnsemblerJobStatus, EnsemblersPaginatedResults, EnsemblingJobPaginatedResults, JobId, RouterId, RouterIdObject, RouterIdAndVersion, - Router, RouterDetails, RouterConfig, RouterVersion) + Router, RouterDetails, RouterConfig, RouterVersion, RouterVersionConfig) def require_active_project(f): @@ -273,6 +273,14 @@ def list_router_versions(self, router_id: int) -> List[RouterVersion]: router_id=router_id ) + @require_active_project + def create_router_version(self, router_id: int, router_version_config: RouterVersionConfig) -> RouterVersion: + return RouterApi(self._api_client).projects_project_id_routers_router_id_versions_post( + project_id=self.active_project.id, + router_id=router_id, + router_version_config=router_version_config + ) + @require_active_project def get_router_version(self, router_id: int, version: int) -> RouterVersion: """ diff --git a/ui/src/router/components/form/CreateRouterForm.js b/ui/src/router/components/form/CreateRouterForm.js index fece56b5d..f4a225b6b 100644 --- a/ui/src/router/components/form/CreateRouterForm.js +++ b/ui/src/router/components/form/CreateRouterForm.js @@ -7,7 +7,7 @@ import { OutcomeStep } from "./steps/OutcomeStep"; import schema from "./validation/schema"; import { useTuringApi } from "../../../hooks/useTuringApi"; import { ConfirmationModal } from "../../../components/confirmation_modal/ConfirmationModal"; -import { DeploymentSummary } from "./components/DeploymentSummary"; +import { RouterCreationSummary } from "./components/RouterCreationSummary"; import { FormContext, StepsWizardHorizontal, addToast } from "@gojek/mlp-ui"; import ExperimentEngineContext from "../../../providers/experiments/context"; import { useConfig } from "../../../config"; @@ -86,7 +86,7 @@ export const CreateRouterForm = ({ projectId, onCancel, onSuccess }) => { return ( } + content={} isLoading={submissionResponse.isLoading} onConfirm={onSubmit} confirmButtonText="Deploy" diff --git a/ui/src/router/components/form/components/DeploymentSummary.js b/ui/src/router/components/form/components/RouterCreationSummary.js similarity index 85% rename from ui/src/router/components/form/components/DeploymentSummary.js rename to ui/src/router/components/form/components/RouterCreationSummary.js index dc568d0fc..61cfb1396 100644 --- a/ui/src/router/components/form/components/DeploymentSummary.js +++ b/ui/src/router/components/form/components/RouterCreationSummary.js @@ -1,7 +1,7 @@ import React, { Fragment } from "react"; import { EuiSpacer } from "@elastic/eui"; -export const DeploymentSummary = ({ router }) => { +export const RouterCreationSummary = ({ router }) => { return (

diff --git a/ui/src/router/components/form/components/RouterUpdateSummary.js b/ui/src/router/components/form/components/RouterUpdateSummary.js new file mode 100644 index 000000000..c6c8dc6ae --- /dev/null +++ b/ui/src/router/components/form/components/RouterUpdateSummary.js @@ -0,0 +1,15 @@ +import React, { Fragment } from "react"; +import { EuiSpacer } from "@elastic/eui"; + +export const RouterUpdateSummary = ({ router }) => { + return ( + +

+ You're about to deploy your changes for the Turing router{" "} + {router.name}. +

+ + +
+ ); +}; diff --git a/ui/src/router/components/form/components/VersionCreationSummary.js b/ui/src/router/components/form/components/VersionCreationSummary.js new file mode 100644 index 000000000..954e4df96 --- /dev/null +++ b/ui/src/router/components/form/components/VersionCreationSummary.js @@ -0,0 +1,20 @@ +import React, { Fragment } from "react"; +import { EuiSpacer } from "@elastic/eui"; + +export const VersionCreationSummary = ({ router }) => { + return ( + +

+ You're about to save your changes as a new version for the Turing + router {router.name}. +

+ +

+ The new version can be deployed at any time from the router history + page. +

+ + +
+ ); +}; diff --git a/ui/src/router/edit/EditRouterView.js b/ui/src/router/edit/EditRouterView.js index b0cb4a7a3..341ea029c 100644 --- a/ui/src/router/edit/EditRouterView.js +++ b/ui/src/router/edit/EditRouterView.js @@ -1,4 +1,4 @@ -import React, { useContext, useEffect } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { TuringRouter } from "../../services/router/TuringRouter"; import { FormContext, FormContextProvider } from "@gojek/mlp-ui"; import { addToast, replaceBreadcrumbs, useToggle } from "@gojek/mlp-ui"; @@ -6,14 +6,15 @@ import { UpdateRouterForm } from "../components/form/UpdateRouterForm"; import { ExperimentEngineContextProvider } from "../../providers/experiments/ExperimentEngineContextProvider"; import { VersionComparisonView } from "./components/VersionComparisonView"; import { useTuringApi } from "../../hooks/useTuringApi"; -import { DeploymentSummary } from "../components/form/components/DeploymentSummary"; +import { RouterUpdateSummary } from "../components/form/components/RouterUpdateSummary"; import { ConfirmationModal } from "../../components/confirmation_modal/ConfirmationModal"; +import { VersionCreationSummary } from "../components/form/components/VersionCreationSummary"; const EditRouterView = ({ projectId, currentRouter, ...props }) => { - const { data: updatedRouter } = useContext(FormContext); + const { data: routerConfig } = useContext(FormContext); const [showDiffView, toggleDiffView] = useToggle(); - const [submissionResponse, submitForm] = useTuringApi( + const [updateRouterResponse, submitUpdateRouter] = useTuringApi( `/projects/${projectId}/routers/${currentRouter.id}`, { method: "PUT", @@ -23,10 +24,20 @@ const EditRouterView = ({ projectId, currentRouter, ...props }) => { false ); + const [createRouterVersionResponse, submitCreateRouterVersion] = useTuringApi( + `/projects/${projectId}/routers/${currentRouter.id}/versions`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + }, + {}, + false + ); + useEffect(() => { - if (submissionResponse.isLoaded && !submissionResponse.error) { + if (updateRouterResponse.isLoaded && !updateRouterResponse.error) { addToast({ - id: "submit-success-create", + id: "submit-success-update-router", title: "Router configuration is updated!", color: "success", iconType: "check", @@ -34,20 +45,60 @@ const EditRouterView = ({ projectId, currentRouter, ...props }) => { props.navigate("../", { state: { refresh: true } }); } - }, [submissionResponse, props]); + }, [updateRouterResponse, props]); - const onSubmit = () => submitForm({ body: JSON.stringify(updatedRouter) }); + useEffect(() => { + if ( + createRouterVersionResponse.isLoaded && + !createRouterVersionResponse.error + ) { + addToast({ + id: "submit-success-create-router-version", + title: `Router version ${createRouterVersionResponse.data.version} is saved (but not deployed)!`, + color: "success", + iconType: "check", + }); + + props.navigate("../", { state: { refresh: true } }); + } + }, [createRouterVersionResponse, props]); + + const [withDeployment, setWithDeployment] = useState(null); + + const onSubmit = () => { + if (withDeployment === true) { + return submitUpdateRouter({ + body: JSON.stringify(routerConfig), + }); + } else if (withDeployment === false) { + return submitCreateRouterVersion({ + body: JSON.stringify(routerConfig.toJSON().config), + }); + } + }; return ( } - isLoading={submissionResponse.isLoading} + title={ + withDeployment + ? "Deploy Turing Router Version" + : "Save Turing Router Version" + } + content={ + withDeployment ? ( + + ) : ( + + ) + } + isLoading={ + updateRouterResponse.isLoading || createRouterVersionResponse.isLoading + } onConfirm={onSubmit} - confirmButtonText="Deploy" - confirmButtonColor="primary"> - {(onSubmit) => - !showDiffView ? ( + confirmButtonText={withDeployment ? "Deploy" : "Save"} + confirmButtonColor={"primary"}> + {(onSubmit) => { + return !showDiffView ? ( props.navigate("../")} @@ -57,13 +108,23 @@ const EditRouterView = ({ projectId, currentRouter, ...props }) => { ) : ( { + setWithDeployment(true); + return onSubmit(); + }} + onSave={() => { + setWithDeployment(false); + return onSubmit(); + }} /> - ) - } + ); + }} ); }; diff --git a/ui/src/router/edit/components/VersionComparisonView.js b/ui/src/router/edit/components/VersionComparisonView.js index 5b5cc2ce4..d9037f8ce 100644 --- a/ui/src/router/edit/components/VersionComparisonView.js +++ b/ui/src/router/edit/components/VersionComparisonView.js @@ -1,6 +1,11 @@ import React, { useContext } from "react"; -import { EuiFlexGroup, EuiFlexItem, EuiPanel } from "@elastic/eui"; -import { StepActions } from "@gojek/mlp-ui"; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, +} from "@elastic/eui"; import { ConfigSection } from "../../../components/config_section"; import { VersionComparisonPanel } from "../../versions/components/version_diff/VersionComparisonPanel"; import { RouterVersion } from "../../../services/version/RouterVersion"; @@ -10,8 +15,9 @@ export const VersionComparisonView = ({ currentRouter, updatedRouter, onPrevious, - onSubmit, isSubmitting, + onDeploy, + onSave, }) => { const { getEngineProperties } = useContext(ExperimentEngineContext); const currentVersionContext = { @@ -45,13 +51,33 @@ export const VersionComparisonView = ({ - + + + + Previous + + + + + Save + + + + + Deploy + + + );