diff --git a/.github/workflows/codecov-config/codecov.yml b/.github/workflows/codecov-config/codecov.yml new file mode 100644 index 000000000..8901728c4 --- /dev/null +++ b/.github/workflows/codecov-config/codecov.yml @@ -0,0 +1,5 @@ +coverage: + status: + patch: + default: + threshold: 0.03% \ No newline at end of file diff --git a/.github/workflows/sdk.yaml b/.github/workflows/sdk.yaml index 431a93272..e4134bec6 100644 --- a/.github/workflows/sdk.yaml +++ b/.github/workflows/sdk.yaml @@ -58,6 +58,7 @@ jobs: flags: sdk-test-${{ matrix.python-version }} name: sdk-test-${{ matrix.python-version }} token: ${{ secrets.CODECOV_TOKEN }} + codecov_yml_path: ./.github/workflows/codecov-config/codecov.yml release-rules: runs-on: ubuntu-latest diff --git a/.github/workflows/turing.yaml b/.github/workflows/turing.yaml index 65b89bb4c..31a817b4f 100644 --- a/.github/workflows/turing.yaml +++ b/.github/workflows/turing.yaml @@ -282,6 +282,7 @@ jobs: name: api-test token: ${{ secrets.CODECOV_TOKEN }} working-directory: api + codecov_yml_path: ../.github/workflows/codecov-config/codecov.yml test-engines-router: runs-on: ubuntu-latest diff --git a/api/api/openapi.bundle.yaml b/api/api/openapi.bundle.yaml index ab625aef3..0c8f8cd48 100644 --- a/api/api/openapi.bundle.yaml +++ b/api/api/openapi.bundle.yaml @@ -2125,6 +2125,7 @@ components: port: 5 created_at: 2000-01-23T04:56:07.000+00:00 resource_request: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request @@ -2161,6 +2162,7 @@ components: updated_at: 2000-01-23T04:56:07.000+00:00 default_route_id: default_route_id resource_request: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request @@ -2174,6 +2176,7 @@ components: project_id: 7 ensembler_id: 9 resource_request: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request @@ -2207,6 +2210,7 @@ components: target: target port: 2 resource_request: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request @@ -2311,6 +2315,7 @@ components: type: object ResourceRequest: example: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request @@ -2323,6 +2328,10 @@ components: cpu_request: pattern: ^(\d{1,3}(\.\d{1,3})?)$|^(\d{2,5}m)$ type: string + cpu_limit: + nullable: true + pattern: ^(\d{1,3}(\.\d{1,3})?)$|^(\d{2,5}m)$ + type: string memory_request: pattern: ^\d+(Ei?|Pi?|Ti?|Gi?|Mi?|Ki?)?$ type: string @@ -2419,6 +2428,7 @@ components: port: 5 created_at: 2000-01-23T04:56:07.000+00:00 resource_request: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request @@ -2481,6 +2491,7 @@ components: project_id: 7 ensembler_id: 9 resource_request: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request @@ -2514,6 +2525,7 @@ components: target: target port: 2 resource_request: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request @@ -2588,6 +2600,7 @@ components: target: target port: 2 resource_request: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request @@ -2640,6 +2653,7 @@ components: project_id: 7 ensembler_id: 9 resource_request: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request @@ -2770,6 +2784,7 @@ components: port: 5 created_at: 2000-01-23T04:56:07.000+00:00 resource_request: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request @@ -2830,6 +2845,7 @@ components: - values operator: in resource_request: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request @@ -2842,6 +2858,7 @@ components: project_id: 7 ensembler_id: 9 resource_request: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request @@ -2875,6 +2892,7 @@ components: target: target port: 2 resource_request: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request @@ -2929,6 +2947,7 @@ components: port: 5 created_at: 2000-01-23T04:56:07.000+00:00 resource_request: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request @@ -2989,6 +3008,7 @@ components: - values operator: in resource_request: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request @@ -3001,6 +3021,7 @@ components: project_id: 7 ensembler_id: 9 resource_request: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request @@ -3034,6 +3055,7 @@ components: target: target port: 2 resource_request: + cpu_limit: cpu_limit min_replica: 0 max_replica: 6 memory_request: memory_request diff --git a/api/api/specs/routers.yaml b/api/api/specs/routers.yaml index a8debcd09..e81cb8551 100644 --- a/api/api/specs/routers.yaml +++ b/api/api/specs/routers.yaml @@ -949,6 +949,10 @@ components: cpu_request: type: "string" pattern: '^(\d{1,3}(\.\d{1,3})?)$|^(\d{2,5}m)$' + cpu_limit: + type: "string" + pattern: '^(\d{1,3}(\.\d{1,3})?)$|^(\d{2,5}m)$' + nullable: true memory_request: type: "string" pattern: '^\d+(Ei?|Pi?|Ti?|Gi?|Mi?|Ki?)?$' diff --git a/api/turing/cluster/knative_service.go b/api/turing/cluster/knative_service.go index 48e900e6d..dc00550b5 100644 --- a/api/turing/cluster/knative_service.go +++ b/api/turing/cluster/knative_service.go @@ -50,9 +50,7 @@ type KnativeService struct { TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints"` // Resource properties - QueueProxyResourcePercentage int `json:"queueProxyResourcePercentage"` - UserContainerCPULimitRequestFactor float64 `json:"userContainerLimitCPURequestFactor"` - UserContainerMemoryLimitRequestFactor float64 `json:"userContainerLimitMemoryRequestFactor"` + QueueProxyResourcePercentage int `json:"queueProxyResourcePercentage"` } // Creates a new config object compatible with the knative serving API, from @@ -131,12 +129,6 @@ func (cfg *KnativeService) buildSvcSpec( // Revision name revisionName := getDefaultRevisionName(cfg.Name) - // Build resource requirements for the user container - resourceReqs := cfg.buildResourceReqs( - cfg.UserContainerCPULimitRequestFactor, - cfg.UserContainerMemoryLimitRequestFactor, - ) - // Build container spec var portName string // If protocol is using GRPC, add "h2c" which is required for grpc knative @@ -151,7 +143,7 @@ func (cfg *KnativeService) buildSvcSpec( ContainerPort: cfg.ContainerPort, }, }, - Resources: resourceReqs, + Resources: cfg.buildResourceReqs(), VolumeMounts: cfg.VolumeMounts, Env: cfg.Envs, } diff --git a/api/turing/cluster/knative_service_test.go b/api/turing/cluster/knative_service_test.go index 20ff03dc3..e9d36c017 100644 --- a/api/turing/cluster/knative_service_test.go +++ b/api/turing/cluster/knative_service_test.go @@ -17,13 +17,16 @@ import ( ) func TestBuildKnativeServiceConfig(t *testing.T) { + cpuRequest := resource.MustParse("400m") + memoryRequest := resource.MustParse("512Mi") + // Test configuration - baseSvc := &BaseService{ + baseSvc := BaseService{ Name: "test-svc", Namespace: "test-namespace", Image: "asia.gcr.io/gcp-project-id/turing-router:latest", - CPURequests: resource.MustParse("400m"), - MemoryRequests: resource.MustParse("512Mi"), + CPURequests: cpuRequest, + MemoryRequests: memoryRequest, ProbePort: 8080, LivenessHTTPGetPath: "/v1/internal/live", ReadinessHTTPGetPath: "/v1/internal/ready", @@ -87,6 +90,10 @@ func TestBuildKnativeServiceConfig(t *testing.T) { }, } + baseSvcWithResourceLimits := baseSvc + baseSvcWithResourceLimits.CPULimit = &cpuRequest + baseSvcWithResourceLimits.MemoryLimit = &memoryRequest + // Expected specs var defaultConcurrency, defaultTrafficPercent int64 = 0, 100 var defaultLatestRevision = true @@ -100,13 +107,10 @@ func TestBuildKnativeServiceConfig(t *testing.T) { }, } resources := corev1.ResourceRequirements{ - Limits: map[corev1.ResourceName]resource.Quantity{ - corev1.ResourceCPU: resource.MustParse("600m"), - corev1.ResourceMemory: resource.MustParse("768Mi"), - }, + Limits: map[corev1.ResourceName]resource.Quantity{}, Requests: map[corev1.ResourceName]resource.Quantity{ - corev1.ResourceCPU: resource.MustParse("400m"), - corev1.ResourceMemory: resource.MustParse("512Mi"), + corev1.ResourceCPU: cpuRequest, + corev1.ResourceMemory: memoryRequest, }, } envs := []corev1.EnvVar{ @@ -123,47 +127,57 @@ func TestBuildKnativeServiceConfig(t *testing.T) { {Name: "APP_BQ_TABLE", Value: "turing_log_test"}, {Name: "APP_BQ_BATCH_LOAD", Value: "false"}, } - podSpec := corev1.PodSpec{ - Containers: []corev1.Container{ + + containerSpec := corev1.Container{ + Name: "user-container", + Image: "asia.gcr.io/gcp-project-id/turing-router:latest", + Ports: []corev1.ContainerPort{ { - Name: "user-container", - Image: "asia.gcr.io/gcp-project-id/turing-router:latest", - Ports: []corev1.ContainerPort{ - { - ContainerPort: 8080, - }, - }, - Resources: resources, - LivenessProbe: &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Port: intstr.FromInt(8080), - Path: "/v1/internal/live", - }, - }, - InitialDelaySeconds: 20, - PeriodSeconds: 10, - TimeoutSeconds: 5, - FailureThreshold: 5, + ContainerPort: 8080, + }, + }, + Resources: resources, + LivenessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Port: intstr.FromInt(8080), + Path: "/v1/internal/live", }, - ReadinessProbe: &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Port: intstr.FromInt(8080), - Path: "/v1/internal/ready", - }, - }, - InitialDelaySeconds: 20, - PeriodSeconds: 10, - SuccessThreshold: 1, - TimeoutSeconds: 5, - FailureThreshold: 5, + }, + InitialDelaySeconds: 20, + PeriodSeconds: 10, + TimeoutSeconds: 5, + FailureThreshold: 5, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Port: intstr.FromInt(8080), + Path: "/v1/internal/ready", }, - VolumeMounts: baseSvc.VolumeMounts, - Env: envs, }, + InitialDelaySeconds: 20, + PeriodSeconds: 10, + SuccessThreshold: 1, + TimeoutSeconds: 5, + FailureThreshold: 5, }, - Volumes: baseSvc.Volumes, + VolumeMounts: baseSvc.VolumeMounts, + Env: envs, + } + podSpec := corev1.PodSpec{ + Containers: []corev1.Container{containerSpec}, + Volumes: baseSvc.Volumes, + } + + containerSpecWithResourceLimits := containerSpec + containerSpecWithResourceLimits.Resources.Limits = map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: cpuRequest, + corev1.ResourceMemory: memoryRequest, + } + podSpecWithResourceLimits := corev1.PodSpec{ + Containers: []corev1.Container{containerSpecWithResourceLimits}, + Volumes: baseSvc.Volumes, } tests := map[string]struct { @@ -172,16 +186,14 @@ func TestBuildKnativeServiceConfig(t *testing.T) { }{ "basic": { serviceCfg: KnativeService{ - BaseService: baseSvc, - ContainerPort: 8080, - MinReplicas: 1, - MaxReplicas: 2, - AutoscalingMetric: "concurrency", - AutoscalingTarget: "1", - IsClusterLocal: true, - QueueProxyResourcePercentage: 30, - UserContainerCPULimitRequestFactor: 1.5, - UserContainerMemoryLimitRequestFactor: 1.5, + BaseService: &baseSvc, + ContainerPort: 8080, + MinReplicas: 1, + MaxReplicas: 2, + AutoscalingMetric: "concurrency", + AutoscalingTarget: "1", + IsClusterLocal: true, + QueueProxyResourcePercentage: 30, }, expectedSpec: knservingv1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -220,20 +232,66 @@ func TestBuildKnativeServiceConfig(t *testing.T) { }, }, }, + "basic with limits": { + serviceCfg: KnativeService{ + BaseService: &baseSvcWithResourceLimits, + ContainerPort: 8080, + MinReplicas: 1, + MaxReplicas: 2, + AutoscalingMetric: "concurrency", + AutoscalingTarget: "1", + IsClusterLocal: true, + QueueProxyResourcePercentage: 30, + }, + expectedSpec: knservingv1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc", + Namespace: "test-namespace", + Labels: map[string]string{ + "labelKey": "labelVal", + "networking.knative.dev/visibility": "cluster-local", + }, + }, + Spec: knservingv1.ServiceSpec{ + ConfigurationSpec: knservingv1.ConfigurationSpec{ + Template: knservingv1.RevisionTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-svc-0", + Labels: map[string]string{ + "labelKey": "labelVal", + }, + Annotations: map[string]string{ + "autoscaling.knative.dev/minScale": "1", + "autoscaling.knative.dev/maxScale": "2", + "autoscaling.knative.dev/metric": "concurrency", + "autoscaling.knative.dev/target": "1.00", + "autoscaling.knative.dev/class": "kpa.autoscaling.knative.dev", + "queue.sidecar.serving.knative.dev/resourcePercentage": "30", + }, + }, + Spec: knservingv1.RevisionSpec{ + PodSpec: podSpecWithResourceLimits, + TimeoutSeconds: &timeout, + ContainerConcurrency: &defaultConcurrency, + }, + }, + }, + RouteSpec: defaultRouteSpec, + }, + }, + }, // upi has no liveness probe in pod spec and user-container is using h2c "upi": { serviceCfg: KnativeService{ - BaseService: baseSvc, - ContainerPort: 8080, - MinReplicas: 1, - MaxReplicas: 2, - AutoscalingMetric: "concurrency", - AutoscalingTarget: "1", - IsClusterLocal: true, - QueueProxyResourcePercentage: 30, - UserContainerCPULimitRequestFactor: 1.5, - UserContainerMemoryLimitRequestFactor: 1.5, - Protocol: routerConfig.UPI, + BaseService: &baseSvc, + ContainerPort: 8080, + MinReplicas: 1, + MaxReplicas: 2, + AutoscalingMetric: "concurrency", + AutoscalingTarget: "1", + IsClusterLocal: true, + QueueProxyResourcePercentage: 30, + Protocol: routerConfig.UPI, }, expectedSpec: knservingv1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -293,15 +351,13 @@ func TestBuildKnativeServiceConfig(t *testing.T) { }, "annotations": { serviceCfg: KnativeService{ - BaseService: baseSvc, - ContainerPort: 8080, - MinReplicas: 5, - MaxReplicas: 6, - AutoscalingMetric: "memory", - AutoscalingTarget: "70", - IsClusterLocal: false, - UserContainerCPULimitRequestFactor: 1.5, - UserContainerMemoryLimitRequestFactor: 1.5, + BaseService: &baseSvc, + ContainerPort: 8080, + MinReplicas: 5, + MaxReplicas: 6, + AutoscalingMetric: "memory", + AutoscalingTarget: "70", + IsClusterLocal: false, }, expectedSpec: knservingv1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -340,7 +396,7 @@ func TestBuildKnativeServiceConfig(t *testing.T) { }, "topology spread constraints": { serviceCfg: KnativeService{ - BaseService: baseSvc, + BaseService: &baseSvc, ContainerPort: 8080, MinReplicas: 1, MaxReplicas: 2, @@ -384,11 +440,9 @@ func TestBuildKnativeServiceConfig(t *testing.T) { }, }, }, - IsClusterLocal: true, - QueueProxyResourcePercentage: 30, - UserContainerCPULimitRequestFactor: 1.5, - UserContainerMemoryLimitRequestFactor: 1.5, - Protocol: routerConfig.UPI, + IsClusterLocal: true, + QueueProxyResourcePercentage: 30, + Protocol: routerConfig.UPI, }, expectedSpec: knservingv1.Service{ ObjectMeta: metav1.ObjectMeta{ diff --git a/api/turing/cluster/kubernetes_service.go b/api/turing/cluster/kubernetes_service.go index 91c281231..c3de0e954 100644 --- a/api/turing/cluster/kubernetes_service.go +++ b/api/turing/cluster/kubernetes_service.go @@ -7,15 +7,6 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" ) -const ( - // defaultCPULimitRequestFactor is the default multiplication factor applied to the CPU request, - // to be set as the limit - defaultCPULimitRequestFactor = 1.0 - // defaultMemoryLimitRequestFactor is the default multiplication factor applied to the memory request, - // to be set as the limit - defaultMemoryLimitRequestFactor = 2.0 -) - // KubernetesService defines the properties for Kubernetes services type KubernetesService struct { *BaseService @@ -73,7 +64,7 @@ func (cfg *KubernetesService) buildStatefulSet(labels map[string]string) *appsv1 Args: cfg.Command, Ports: cfg.buildContainerPorts(), Env: cfg.Envs, - Resources: cfg.buildResourceReqs(defaultCPULimitRequestFactor, defaultMemoryLimitRequestFactor), + Resources: cfg.buildResourceReqs(), VolumeMounts: cfg.VolumeMounts, LivenessProbe: cfg.buildContainerProbe(livenessProbeType, int(cfg.ProbePort)), ReadinessProbe: cfg.buildContainerProbe(readinessProbeType, int(cfg.ProbePort)), diff --git a/api/turing/cluster/kubernetes_service_test.go b/api/turing/cluster/kubernetes_service_test.go index 65b0e1aa3..433f3c4f0 100644 --- a/api/turing/cluster/kubernetes_service_test.go +++ b/api/turing/cluster/kubernetes_service_test.go @@ -15,13 +15,17 @@ import ( func TestBuildKubernetesServiceConfig(t *testing.T) { id := int64(999) + cpuLimit := resource.MustParse("1") + memoryLimit := resource.MustParse("2") svcConf := KubernetesService{ BaseService: &BaseService{ Name: "test-svc-fluentd-logger", Namespace: "namespace", Image: "fluentdimage:1.0.0", CPURequests: resource.MustParse("1"), + CPULimit: &cpuLimit, MemoryRequests: resource.MustParse("1"), + MemoryLimit: &memoryLimit, ProbePort: 8080, LivenessHTTPGetPath: "/fluentd.pod.healthcheck?json=%7B%22log%22%3A+%22health+check%22%7D", ReadinessHTTPGetPath: "/fluentd.pod.healthcheck?json=%7B%22log%22%3A+%22health+check%22%7D", diff --git a/api/turing/cluster/models.go b/api/turing/cluster/models.go index f046b3343..07f68b9ef 100644 --- a/api/turing/cluster/models.go +++ b/api/turing/cluster/models.go @@ -35,8 +35,10 @@ type BaseService struct { Image string `json:"image"` // Resources - CPURequests resource.Quantity `json:"cpu_requests"` - MemoryRequests resource.Quantity `json:"memory_requests"` + CPURequests resource.Quantity `json:"cpu_requests"` + CPULimit *resource.Quantity `json:"cpu_limit"` + MemoryRequests resource.Quantity `json:"memory_requests"` + MemoryLimit *resource.Quantity `json:"memory_limit"` // Health Checks ProbePort int32 `json:"probe_port"` @@ -62,22 +64,21 @@ type BaseService struct { InitContainers []Container `json:"init_containers"` } -func (cfg *BaseService) buildResourceReqs( - UserContainerCPULimitRequestFactor float64, - UserContainerMemoryLimitRequestFactor float64, -) corev1.ResourceRequirements { +// buildResourceReqs sets the necessary resource requests and limits based on the values stored in BaseService. These +// values are expected to have already been computed (e.g. setting of default values) upstream by the clusterSvcBuilder +// that creates the BaseService +func (cfg *BaseService) buildResourceReqs() corev1.ResourceRequirements { reqs := map[corev1.ResourceName]resource.Quantity{ corev1.ResourceCPU: cfg.CPURequests, corev1.ResourceMemory: cfg.MemoryRequests, } - // Set resource limits to request * userContainerCPULimitRequestFactor or UserContainerMemoryLimitRequestFactor limits := map[corev1.ResourceName]resource.Quantity{} - if UserContainerCPULimitRequestFactor != 0 { - limits[corev1.ResourceCPU] = ComputeResource(cfg.CPURequests, UserContainerCPULimitRequestFactor) + if cfg.CPULimit != nil { + limits[corev1.ResourceCPU] = *cfg.CPULimit } - if UserContainerMemoryLimitRequestFactor != 0 { - limits[corev1.ResourceMemory] = ComputeResource(cfg.MemoryRequests, UserContainerMemoryLimitRequestFactor) + if cfg.MemoryLimit != nil { + limits[corev1.ResourceMemory] = *cfg.MemoryLimit } return corev1.ResourceRequirements{ diff --git a/api/turing/cluster/servicebuilder/fluentd.go b/api/turing/cluster/servicebuilder/fluentd.go index a729cd086..afb1750c4 100644 --- a/api/turing/cluster/servicebuilder/fluentd.go +++ b/api/turing/cluster/servicebuilder/fluentd.go @@ -21,6 +21,13 @@ const ( fluentdPort = 24224 cacheVolumeMountPath = "/cache/" cacheVolumeSize = "2Gi" + + // defaultCPULimitRequestFactor is the default multiplication factor applied to the CPU request, + // to be set as the limit + defaultCPULimitRequestFactor = 1.0 + // defaultMemoryLimitRequestFactor is the default multiplication factor applied to the memory request, + // to be set as the limit + defaultMemoryLimitRequestFactor = 2.0 ) // NewFluentdService builds a fluentd KubernetesService configuration @@ -61,13 +68,20 @@ func (sb *clusterSvcBuilder) NewFluentdService( // Overriding the security context so that fluentd is able to write logs // to the persistent volume. securityContextID := int64(999) + + cpuRequest := resource.MustParse(fluentdCPURequest) + cpuLimit := cluster.ComputeResource(cpuRequest, defaultCPULimitRequestFactor) + memoryRequest := resource.MustParse(fluentdMemRequest) + memoryLimit := cluster.ComputeResource(memoryRequest, defaultMemoryLimitRequestFactor) return &cluster.KubernetesService{ BaseService: &cluster.BaseService{ Name: name, Namespace: project.Name, Image: fluentdConfig.Image, CPURequests: resource.MustParse(fluentdCPURequest), + CPULimit: &cpuLimit, MemoryRequests: resource.MustParse(fluentdMemRequest), + MemoryLimit: &memoryLimit, ProbePort: 9880, LivenessHTTPGetPath: fluentdHealthCheckPath, ReadinessHTTPGetPath: fluentdHealthCheckPath, diff --git a/api/turing/cluster/servicebuilder/fluentd_test.go b/api/turing/cluster/servicebuilder/fluentd_test.go index 1c86640ac..707ad0e9d 100644 --- a/api/turing/cluster/servicebuilder/fluentd_test.go +++ b/api/turing/cluster/servicebuilder/fluentd_test.go @@ -19,11 +19,9 @@ import ( ) func TestNewFluentdService(t *testing.T) { - cpuLimit := resource.MustParse("400m") - memoryLimit := resource.MustParse("512Mi") sb := clusterSvcBuilder{ - MaxCPU: cpuLimit, - MaxMemory: memoryLimit, + MaxCPU: resource.MustParse("400m"), + MaxMemory: resource.MustParse("512Mi"), } testDataBasePath := filepath.Join("..", "..", "testdata", "cluster", "servicebuilder") @@ -42,6 +40,11 @@ func TestNewFluentdService(t *testing.T) { Team: "test-team", } + cpuRequest := resource.MustParse(fluentdCPURequest) + cpuLimits := cluster.ComputeResource(cpuRequest, defaultCPULimitRequestFactor) + memoryRequest := resource.MustParse(fluentdMemRequest) + memoryLimits := cluster.ComputeResource(memoryRequest, defaultMemoryLimitRequestFactor) + id := int64(999) volSize, _ := resource.ParseQuantity(cacheVolumeSize) expected := &cluster.KubernetesService{ @@ -49,8 +52,10 @@ func TestNewFluentdService(t *testing.T) { Name: "test-svc-turing-fluentd-logger-1", Namespace: project.Name, Image: "fluentdimage:1.0.0", - CPURequests: resource.MustParse(fluentdCPURequest), - MemoryRequests: resource.MustParse(fluentdMemRequest), + CPURequests: cpuRequest, + CPULimit: &cpuLimits, + MemoryRequests: memoryRequest, + MemoryLimit: &memoryLimits, LivenessHTTPGetPath: "/fluentd.pod.healthcheck?json=%7B%22log%22%3A+%22health+check%22%7D", ReadinessHTTPGetPath: "/fluentd.pod.healthcheck?json=%7B%22log%22%3A+%22health+check%22%7D", ProbeInitDelaySeconds: 10, diff --git a/api/turing/cluster/servicebuilder/router.go b/api/turing/cluster/servicebuilder/router.go index 86a083f33..87c3ce35a 100644 --- a/api/turing/cluster/servicebuilder/router.go +++ b/api/turing/cluster/servicebuilder/router.go @@ -104,9 +104,6 @@ func (sb *clusterSvcBuilder) NewRouterService( routerDefaults *config.RouterDefaults, sentryEnabled bool, sentryDSN string, - knativeQueueProxyResourcePercentage int, - userContainerCPULimitRequestFactor float64, - userContainerMemoryLimitRequestFactor float64, initialScale *int, ) (*cluster.KnativeService, error) { // Create service name @@ -141,7 +138,9 @@ func (sb *clusterSvcBuilder) NewRouterService( Namespace: namespace, Image: routerVersion.Image, CPURequests: routerVersion.ResourceRequest.CPURequest, + CPULimit: sb.getCPULimit(routerVersion.ResourceRequest), MemoryRequests: routerVersion.ResourceRequest.MemoryRequest, + MemoryLimit: sb.getMemoryLimit(routerVersion.ResourceRequest), LivenessHTTPGetPath: routerLivenessPath, ReadinessHTTPGetPath: routerReadinessPath, Envs: envs, @@ -151,18 +150,16 @@ func (sb *clusterSvcBuilder) NewRouterService( VolumeMounts: volumeMounts, InitContainers: initContainers, }, - IsClusterLocal: false, - ContainerPort: routerPort, - Protocol: routerVersion.Protocol, - MinReplicas: routerVersion.ResourceRequest.MinReplica, - MaxReplicas: routerVersion.ResourceRequest.MaxReplica, - InitialScale: initialScale, - AutoscalingMetric: string(routerVersion.AutoscalingPolicy.Metric), - AutoscalingTarget: routerVersion.AutoscalingPolicy.Target, - TopologySpreadConstraints: topologySpreadConstraints, - QueueProxyResourcePercentage: knativeQueueProxyResourcePercentage, - UserContainerCPULimitRequestFactor: userContainerCPULimitRequestFactor, - UserContainerMemoryLimitRequestFactor: userContainerMemoryLimitRequestFactor, + IsClusterLocal: false, + ContainerPort: routerPort, + Protocol: routerVersion.Protocol, + MinReplicas: routerVersion.ResourceRequest.MinReplica, + MaxReplicas: routerVersion.ResourceRequest.MaxReplica, + InitialScale: initialScale, + AutoscalingMetric: string(routerVersion.AutoscalingPolicy.Metric), + AutoscalingTarget: routerVersion.AutoscalingPolicy.Target, + TopologySpreadConstraints: topologySpreadConstraints, + QueueProxyResourcePercentage: sb.knativeServiceConfig.QueueProxyResourcePercentage, } return sb.validateKnativeService(svc) } @@ -213,26 +210,29 @@ func (sb *clusterSvcBuilder) buildRouterEnvs( sentryDSN string, ver *models.RouterVersion, ) ([]corev1.EnvVar, error) { + envs := sb.getEnvVars(ver.ResourceRequest, nil) + // Add app name, router timeout, jaeger collector - envs := []corev1.EnvVar{ - {Name: envAppName, Value: fmt.Sprintf("%s-%d.%s", ver.Router.Name, ver.Version, namespace)}, - {Name: envAppEnvironment, Value: environmentType}, - {Name: envRouterTimeout, Value: ver.Timeout}, - {Name: envJaegerEndpoint, Value: routerDefaults.JaegerCollectorEndpoint}, - {Name: envRouterConfigFile, Value: routerConfigMapMountPath + routerConfigFileName}, - {Name: envRouterProtocol, Value: string(ver.Protocol)}, - {Name: envSentryEnabled, Value: strconv.FormatBool(sentryEnabled)}, - {Name: envSentryDSN, Value: sentryDSN}, - } + envs = mergeEnvVars(envs, + []corev1.EnvVar{ + {Name: envAppName, Value: fmt.Sprintf("%s-%d.%s", ver.Router.Name, ver.Version, namespace)}, + {Name: envAppEnvironment, Value: environmentType}, + {Name: envRouterTimeout, Value: ver.Timeout}, + {Name: envJaegerEndpoint, Value: routerDefaults.JaegerCollectorEndpoint}, + {Name: envRouterConfigFile, Value: routerConfigMapMountPath + routerConfigFileName}, + {Name: envRouterProtocol, Value: string(ver.Protocol)}, + {Name: envSentryEnabled, Value: strconv.FormatBool(sentryEnabled)}, + {Name: envSentryDSN, Value: sentryDSN}, + }) // Add enricher / ensembler related env vars, if enabled if ver.Enricher != nil { endpoint := buildPrePostProcessorEndpoint(ver, namespace, ComponentTypes.Enricher, ver.Enricher.Endpoint) - envs = append(envs, []corev1.EnvVar{ + envs = mergeEnvVars(envs, []corev1.EnvVar{ {Name: envEnricherEndpoint, Value: endpoint}, {Name: envEnricherTimeout, Value: ver.Enricher.Timeout}, - }...) + }) } if ver.HasDockerConfig() { endpoint := buildPrePostProcessorEndpoint( @@ -241,28 +241,28 @@ func (sb *clusterSvcBuilder) buildRouterEnvs( ComponentTypes.Ensembler, ver.Ensembler.DockerConfig.Endpoint, ) - envs = append(envs, []corev1.EnvVar{ + envs = mergeEnvVars(envs, []corev1.EnvVar{ {Name: envEnsemblerEndpoint, Value: endpoint}, {Name: envEnsemblerTimeout, Value: ver.Ensembler.DockerConfig.Timeout}, - }...) + }) } // Add exp engine secret path as env var if service account key file path is specified for exp engine if ver.ExperimentEngine != nil && ver.ExperimentEngine.ServiceAccountKeyFilePath != nil { - envs = append(envs, []corev1.EnvVar{ + envs = mergeEnvVars(envs, []corev1.EnvVar{ {Name: envExpGoogleApplicationCredentials, Value: secretMountPathExpEngine + secretKeyNameExpEngine}, - }...) + }) } // Process Log config logConfig := ver.LogConfig - envs = append(envs, []corev1.EnvVar{ + envs = mergeEnvVars(envs, []corev1.EnvVar{ {Name: envLogLevel, Value: string(logConfig.LogLevel)}, {Name: envCustomMetrics, Value: strconv.FormatBool(logConfig.CustomMetricsEnabled)}, {Name: envJaegerEnabled, Value: strconv.FormatBool(logConfig.JaegerEnabled)}, {Name: envResultLogger, Value: string(logConfig.ResultLoggerType)}, {Name: envFiberDebugLog, Value: strconv.FormatBool(logConfig.FiberDebugLogEnabled)}, - }...) + }) // Add BQ config switch logConfig.ResultLoggerType { @@ -275,29 +275,29 @@ func (sb *clusterSvcBuilder) buildRouterEnvs( return envs, fmt.Errorf("Invalid BigQuery table name %s", logConfig.BigQueryConfig.Table) } - envs = append(envs, []corev1.EnvVar{ + envs = mergeEnvVars(envs, []corev1.EnvVar{ {Name: envGcpProject, Value: bqFQN[0]}, {Name: envBQDataset, Value: bqFQN[1]}, {Name: envBQTable, Value: bqFQN[2]}, {Name: envBQBatchLoad, Value: strconv.FormatBool(logConfig.BigQueryConfig.BatchLoad)}, {Name: envGoogleApplicationCredentials, Value: secretMountPathRouter + secretKeyNameRouter}, - }...) + }) if logConfig.BigQueryConfig.BatchLoad { - envs = append(envs, []corev1.EnvVar{ + envs = mergeEnvVars(envs, []corev1.EnvVar{ {Name: envFluentdHost, Value: buildFluentdHost(ver, namespace)}, {Name: envFluentdPort, Value: strconv.Itoa(fluentdPort)}, {Name: envFluentdTag, Value: routerDefaults.FluentdConfig.Tag}, - }...) + }) } case models.KafkaLogger, models.UPILogger: // UPILogger's kafka details are created in BuildRouterVersion so that information are persisted in DB - envs = append(envs, []corev1.EnvVar{ + envs = mergeEnvVars(envs, []corev1.EnvVar{ {Name: envKafkaBrokers, Value: logConfig.KafkaConfig.Brokers}, {Name: envKafkaTopic, Value: logConfig.KafkaConfig.Topic}, {Name: envKafkaSerializationFormat, Value: string(logConfig.KafkaConfig.SerializationFormat)}, {Name: envKafkaMaxMessageBytes, Value: strconv.Itoa(routerDefaults.KafkaConfig.MaxMessageBytes)}, {Name: envKafkaCompressionType, Value: routerDefaults.KafkaConfig.CompressionType}, - }...) + }) } return envs, nil diff --git a/api/turing/cluster/servicebuilder/router_test.go b/api/turing/cluster/servicebuilder/router_test.go index 5ccd64b10..a4fd0d491 100644 --- a/api/turing/cluster/servicebuilder/router_test.go +++ b/api/turing/cluster/servicebuilder/router_test.go @@ -20,7 +20,22 @@ import ( ) func TestNewRouterService(t *testing.T) { - sb := NewClusterServiceBuilder(resource.MustParse("2"), resource.MustParse("2Gi"), 30, testTopologySpreadConstraints) + userContainerMemoryLimitRequestFactor := 1.5 + abcDefaultEnvVar := corev1.EnvVar{Name: "ABC", Value: "true"} + defDefaultEnvVar := corev1.EnvVar{Name: "DEF", Value: "false"} + + sb := NewClusterServiceBuilder( + resource.MustParse("2"), + resource.MustParse("2Gi"), + 30, + testTopologySpreadConstraints, + &config.KnativeServiceDefaults{ + QueueProxyResourcePercentage: 20, + UserContainerCPULimitRequestFactor: 0, + UserContainerMemoryLimitRequestFactor: userContainerMemoryLimitRequestFactor, + DefaultEnvVarsWithoutCPULimits: []corev1.EnvVar{abcDefaultEnvVar, defDefaultEnvVar}, + }, + ) testDataBasePath := filepath.Join("..", "..", "testdata", "cluster", "servicebuilder") enrEndpoint := "http://test-svc-turing-enricher-1.test-project.svc.cluster.local/echo?delay=10ms" ensEndpoint := "http://test-svc-turing-ensembler-1.test-project.svc.cluster.local/echo?delay=20ms" @@ -69,6 +84,9 @@ func TestNewRouterService(t *testing.T) { cfgmapNoDefaultRoute, err := tu.ReadFile(filepath.Join(testDataBasePath, "router_configmap_no_default_route.yml")) require.NoError(t, err) + memoryRequest := resource.MustParse("512Mi") + memoryLimit := cluster.ComputeResource(memoryRequest, userContainerMemoryLimitRequestFactor) + testInitialScale := 3 // Define tests @@ -83,7 +101,8 @@ func TestNewRouterService(t *testing.T) { Namespace: "test-project", Image: "asia.gcr.io/gcp-project-id/turing-router:latest", CPURequests: resource.MustParse("400m"), - MemoryRequests: resource.MustParse("512Mi"), + MemoryRequests: memoryRequest, + MemoryLimit: &memoryLimit, LivenessHTTPGetPath: "/v1/internal/live", ReadinessHTTPGetPath: "/v1/internal/ready", ConfigMap: &cluster.ConfigMap{ @@ -99,6 +118,8 @@ func TestNewRouterService(t *testing.T) { }, }, Envs: []corev1.EnvVar{ + abcDefaultEnvVar, + defDefaultEnvVar, {Name: "APP_NAME", Value: "test-svc-1.test-project"}, {Name: "APP_ENVIRONMENT", Value: "test-env"}, {Name: "ROUTER_TIMEOUT", Value: "5s"}, @@ -162,17 +183,15 @@ func TestNewRouterService(t *testing.T) { }, }, }, - ContainerPort: 8080, - Protocol: routerConfig.HTTP, - MinReplicas: 2, - MaxReplicas: 4, - InitialScale: &testInitialScale, - AutoscalingMetric: "concurrency", - AutoscalingTarget: "1", - TopologySpreadConstraints: testTopologySpreadConstraints, - QueueProxyResourcePercentage: 20, - UserContainerCPULimitRequestFactor: 0, - UserContainerMemoryLimitRequestFactor: 1.5, + ContainerPort: 8080, + Protocol: routerConfig.HTTP, + MinReplicas: 2, + MaxReplicas: 4, + InitialScale: &testInitialScale, + AutoscalingMetric: "concurrency", + AutoscalingTarget: "1", + TopologySpreadConstraints: testTopologySpreadConstraints, + QueueProxyResourcePercentage: 20, }, }, "success | basic upi": { @@ -185,7 +204,8 @@ func TestNewRouterService(t *testing.T) { Namespace: "test-project", Image: "asia.gcr.io/gcp-project-id/turing-router:latest", CPURequests: resource.MustParse("400m"), - MemoryRequests: resource.MustParse("512Mi"), + MemoryRequests: memoryRequest, + MemoryLimit: &memoryLimit, LivenessHTTPGetPath: "/v1/internal/live", ReadinessHTTPGetPath: "/v1/internal/ready", ConfigMap: &cluster.ConfigMap{ @@ -201,6 +221,8 @@ func TestNewRouterService(t *testing.T) { }, }, Envs: []corev1.EnvVar{ + abcDefaultEnvVar, + defDefaultEnvVar, {Name: "APP_NAME", Value: "test-svc-1.test-project"}, {Name: "APP_ENVIRONMENT", Value: "test-env"}, {Name: "ROUTER_TIMEOUT", Value: "5s"}, @@ -264,17 +286,15 @@ func TestNewRouterService(t *testing.T) { }, }, }, - ContainerPort: 8080, - Protocol: routerConfig.UPI, - MinReplicas: 2, - MaxReplicas: 4, - InitialScale: &testInitialScale, - AutoscalingMetric: "concurrency", - AutoscalingTarget: "1", - TopologySpreadConstraints: testTopologySpreadConstraints, - QueueProxyResourcePercentage: 20, - UserContainerCPULimitRequestFactor: 0, - UserContainerMemoryLimitRequestFactor: 1.5, + ContainerPort: 8080, + Protocol: routerConfig.UPI, + MinReplicas: 2, + MaxReplicas: 4, + InitialScale: &testInitialScale, + AutoscalingMetric: "concurrency", + AutoscalingTarget: "1", + TopologySpreadConstraints: testTopologySpreadConstraints, + QueueProxyResourcePercentage: 20, }, }, "success | all components": { @@ -286,7 +306,8 @@ func TestNewRouterService(t *testing.T) { Namespace: "test-project", Image: "asia.gcr.io/gcp-project-id/turing-router:latest", CPURequests: resource.MustParse("400m"), - MemoryRequests: resource.MustParse("512Mi"), + MemoryRequests: memoryRequest, + MemoryLimit: &memoryLimit, LivenessHTTPGetPath: "/v1/internal/live", ReadinessHTTPGetPath: "/v1/internal/ready", ConfigMap: &cluster.ConfigMap{ @@ -302,6 +323,8 @@ func TestNewRouterService(t *testing.T) { }, }, Envs: []corev1.EnvVar{ + abcDefaultEnvVar, + defDefaultEnvVar, {Name: "APP_NAME", Value: "test-svc-1.test-project"}, {Name: "APP_ENVIRONMENT", Value: "test-env"}, {Name: "ROUTER_TIMEOUT", Value: "5s"}, @@ -373,16 +396,14 @@ func TestNewRouterService(t *testing.T) { }, }, }, - ContainerPort: 8080, - Protocol: routerConfig.HTTP, - MinReplicas: 2, - MaxReplicas: 4, - AutoscalingMetric: "concurrency", - AutoscalingTarget: "1", - TopologySpreadConstraints: testTopologySpreadConstraints, - QueueProxyResourcePercentage: 20, - UserContainerCPULimitRequestFactor: 0, - UserContainerMemoryLimitRequestFactor: 1.5, + ContainerPort: 8080, + Protocol: routerConfig.HTTP, + MinReplicas: 2, + MaxReplicas: 4, + AutoscalingMetric: "concurrency", + AutoscalingTarget: "1", + TopologySpreadConstraints: testTopologySpreadConstraints, + QueueProxyResourcePercentage: 20, }, }, "success | standard ensembler with experiment mappings": { @@ -394,7 +415,8 @@ func TestNewRouterService(t *testing.T) { Namespace: "test-project", Image: "asia.gcr.io/gcp-project-id/turing-router:latest", CPURequests: resource.MustParse("400m"), - MemoryRequests: resource.MustParse("512Mi"), + MemoryRequests: memoryRequest, + MemoryLimit: &memoryLimit, LivenessHTTPGetPath: "/v1/internal/live", ReadinessHTTPGetPath: "/v1/internal/ready", ConfigMap: &cluster.ConfigMap{ @@ -410,6 +432,8 @@ func TestNewRouterService(t *testing.T) { }, }, Envs: []corev1.EnvVar{ + abcDefaultEnvVar, + defDefaultEnvVar, {Name: "APP_NAME", Value: "test-svc-1.test-project"}, {Name: "APP_ENVIRONMENT", Value: "test-env"}, {Name: "ROUTER_TIMEOUT", Value: "5s"}, @@ -473,16 +497,14 @@ func TestNewRouterService(t *testing.T) { }, }, }, - ContainerPort: 8080, - Protocol: routerConfig.HTTP, - MinReplicas: 2, - MaxReplicas: 4, - AutoscalingMetric: "rps", - AutoscalingTarget: "100", - TopologySpreadConstraints: testTopologySpreadConstraints, - QueueProxyResourcePercentage: 20, - UserContainerCPULimitRequestFactor: 0, - UserContainerMemoryLimitRequestFactor: 1.5, + ContainerPort: 8080, + Protocol: routerConfig.HTTP, + MinReplicas: 2, + MaxReplicas: 4, + AutoscalingMetric: "rps", + AutoscalingTarget: "100", + TopologySpreadConstraints: testTopologySpreadConstraints, + QueueProxyResourcePercentage: 20, }, }, "success | standard ensembler with route name path": { @@ -494,7 +516,8 @@ func TestNewRouterService(t *testing.T) { Namespace: "test-project", Image: "asia.gcr.io/gcp-project-id/turing-router:latest", CPURequests: resource.MustParse("400m"), - MemoryRequests: resource.MustParse("512Mi"), + MemoryRequests: memoryRequest, + MemoryLimit: &memoryLimit, LivenessHTTPGetPath: "/v1/internal/live", ReadinessHTTPGetPath: "/v1/internal/ready", ConfigMap: &cluster.ConfigMap{ @@ -510,6 +533,8 @@ func TestNewRouterService(t *testing.T) { }, }, Envs: []corev1.EnvVar{ + abcDefaultEnvVar, + defDefaultEnvVar, {Name: "APP_NAME", Value: "test-svc-1.test-project"}, {Name: "APP_ENVIRONMENT", Value: "test-env"}, {Name: "ROUTER_TIMEOUT", Value: "5s"}, @@ -573,16 +598,14 @@ func TestNewRouterService(t *testing.T) { }, }, }, - ContainerPort: 8080, - Protocol: routerConfig.HTTP, - MinReplicas: 2, - MaxReplicas: 4, - AutoscalingMetric: "rps", - AutoscalingTarget: "100", - TopologySpreadConstraints: testTopologySpreadConstraints, - QueueProxyResourcePercentage: 20, - UserContainerCPULimitRequestFactor: 0, - UserContainerMemoryLimitRequestFactor: 1.5, + ContainerPort: 8080, + Protocol: routerConfig.HTTP, + MinReplicas: 2, + MaxReplicas: 4, + AutoscalingMetric: "rps", + AutoscalingTarget: "100", + TopologySpreadConstraints: testTopologySpreadConstraints, + QueueProxyResourcePercentage: 20, }, }, "success | standard ensembler lazy routing": { @@ -594,7 +617,8 @@ func TestNewRouterService(t *testing.T) { Namespace: "test-project", Image: "asia.gcr.io/gcp-project-id/turing-router:latest", CPURequests: resource.MustParse("400m"), - MemoryRequests: resource.MustParse("512Mi"), + MemoryRequests: memoryRequest, + MemoryLimit: &memoryLimit, LivenessHTTPGetPath: "/v1/internal/live", ReadinessHTTPGetPath: "/v1/internal/ready", ConfigMap: &cluster.ConfigMap{ @@ -610,6 +634,8 @@ func TestNewRouterService(t *testing.T) { }, }, Envs: []corev1.EnvVar{ + abcDefaultEnvVar, + defDefaultEnvVar, {Name: "APP_NAME", Value: "test-svc-1.test-project"}, {Name: "APP_ENVIRONMENT", Value: "test-env"}, {Name: "ROUTER_TIMEOUT", Value: "5s"}, @@ -673,16 +699,14 @@ func TestNewRouterService(t *testing.T) { }, }, }, - ContainerPort: 8080, - Protocol: routerConfig.HTTP, - MinReplicas: 2, - MaxReplicas: 4, - AutoscalingMetric: "rps", - AutoscalingTarget: "100", - TopologySpreadConstraints: testTopologySpreadConstraints, - QueueProxyResourcePercentage: 20, - UserContainerCPULimitRequestFactor: 0, - UserContainerMemoryLimitRequestFactor: 1.5, + ContainerPort: 8080, + Protocol: routerConfig.HTTP, + MinReplicas: 2, + MaxReplicas: 4, + AutoscalingMetric: "rps", + AutoscalingTarget: "100", + TopologySpreadConstraints: testTopologySpreadConstraints, + QueueProxyResourcePercentage: 20, }, }, "success | traffic-splitting": { @@ -694,7 +718,8 @@ func TestNewRouterService(t *testing.T) { Namespace: "test-project", Image: "asia.gcr.io/gcp-project-id/turing-router:latest", CPURequests: resource.MustParse("400m"), - MemoryRequests: resource.MustParse("512Mi"), + MemoryRequests: memoryRequest, + MemoryLimit: &memoryLimit, LivenessHTTPGetPath: "/v1/internal/live", ReadinessHTTPGetPath: "/v1/internal/ready", ConfigMap: &cluster.ConfigMap{ @@ -710,6 +735,8 @@ func TestNewRouterService(t *testing.T) { }, }, Envs: []corev1.EnvVar{ + abcDefaultEnvVar, + defDefaultEnvVar, {Name: "APP_NAME", Value: "test-svc-1.test-project"}, {Name: "APP_ENVIRONMENT", Value: "test-env"}, {Name: "ROUTER_TIMEOUT", Value: "5s"}, @@ -773,16 +800,14 @@ func TestNewRouterService(t *testing.T) { }, }, }, - ContainerPort: 8080, - Protocol: routerConfig.HTTP, - MinReplicas: 2, - MaxReplicas: 4, - AutoscalingMetric: "concurrency", - AutoscalingTarget: "1", - TopologySpreadConstraints: testTopologySpreadConstraints, - QueueProxyResourcePercentage: 20, - UserContainerCPULimitRequestFactor: 0, - UserContainerMemoryLimitRequestFactor: 1.5, + ContainerPort: 8080, + Protocol: routerConfig.HTTP, + MinReplicas: 2, + MaxReplicas: 4, + AutoscalingMetric: "concurrency", + AutoscalingTarget: "1", + TopologySpreadConstraints: testTopologySpreadConstraints, + QueueProxyResourcePercentage: 20, }, }, "success | experiment engine": { @@ -794,7 +819,8 @@ func TestNewRouterService(t *testing.T) { Namespace: "test-project", Image: "ghcr.io/caraml-dev/turing/turing-router:latest", CPURequests: resource.MustParse("400m"), - MemoryRequests: resource.MustParse("512Mi"), + MemoryRequests: memoryRequest, + MemoryLimit: &memoryLimit, LivenessHTTPGetPath: "/v1/internal/live", ReadinessHTTPGetPath: "/v1/internal/ready", ConfigMap: &cluster.ConfigMap{ @@ -810,6 +836,8 @@ func TestNewRouterService(t *testing.T) { }, }, Envs: []corev1.EnvVar{ + abcDefaultEnvVar, + defDefaultEnvVar, {Name: "APP_NAME", Value: "router-with-exp-engine-1.test-project"}, {Name: "APP_ENVIRONMENT", Value: "test-env"}, {Name: "ROUTER_TIMEOUT", Value: "5s"}, @@ -902,16 +930,14 @@ func TestNewRouterService(t *testing.T) { }, }, }, - ContainerPort: 8080, - Protocol: routerConfig.HTTP, - MinReplicas: 2, - MaxReplicas: 4, - AutoscalingMetric: "rps", - AutoscalingTarget: "100", - TopologySpreadConstraints: testTopologySpreadConstraints, - QueueProxyResourcePercentage: 20, - UserContainerCPULimitRequestFactor: 0, - UserContainerMemoryLimitRequestFactor: 1.5, + ContainerPort: 8080, + Protocol: routerConfig.HTTP, + MinReplicas: 2, + MaxReplicas: 4, + AutoscalingMetric: "rps", + AutoscalingTarget: "100", + TopologySpreadConstraints: testTopologySpreadConstraints, + QueueProxyResourcePercentage: 20, }, }, "success | no default route": { @@ -923,7 +949,8 @@ func TestNewRouterService(t *testing.T) { Namespace: "test-project", Image: "asia.gcr.io/gcp-project-id/turing-router:latest", CPURequests: resource.MustParse("400m"), - MemoryRequests: resource.MustParse("512Mi"), + MemoryRequests: memoryRequest, + MemoryLimit: &memoryLimit, LivenessHTTPGetPath: "/v1/internal/live", ReadinessHTTPGetPath: "/v1/internal/ready", ConfigMap: &cluster.ConfigMap{ @@ -939,6 +966,8 @@ func TestNewRouterService(t *testing.T) { }, }, Envs: []corev1.EnvVar{ + abcDefaultEnvVar, + defDefaultEnvVar, {Name: "APP_NAME", Value: "test-svc-1.test-project"}, {Name: "APP_ENVIRONMENT", Value: "test-env"}, {Name: "ROUTER_TIMEOUT", Value: "5s"}, @@ -979,16 +1008,14 @@ func TestNewRouterService(t *testing.T) { }, }, }, - ContainerPort: 8080, - Protocol: routerConfig.HTTP, - MinReplicas: 2, - MaxReplicas: 4, - AutoscalingMetric: "memory", - AutoscalingTarget: "90", - TopologySpreadConstraints: testTopologySpreadConstraints, - QueueProxyResourcePercentage: 20, - UserContainerCPULimitRequestFactor: 0, - UserContainerMemoryLimitRequestFactor: 1.5, + ContainerPort: 8080, + Protocol: routerConfig.HTTP, + MinReplicas: 2, + MaxReplicas: 4, + AutoscalingMetric: "memory", + AutoscalingTarget: "90", + TopologySpreadConstraints: testTopologySpreadConstraints, + QueueProxyResourcePercentage: 20, }, }, "failure missing bigquery": { @@ -1025,7 +1052,7 @@ func TestNewRouterService(t *testing.T) { }, true, "sentry-dsn", - 20, 0, 1.5, data.initialScale, + data.initialScale, ) if data.err == "" { @@ -1040,7 +1067,17 @@ func TestNewRouterService(t *testing.T) { func TestNewRouterEndpoint(t *testing.T) { // Get router version - sb := NewClusterServiceBuilder(resource.MustParse("2"), resource.MustParse("2Gi"), 30, testTopologySpreadConstraints) + sb := NewClusterServiceBuilder( + resource.MustParse("2"), + resource.MustParse("2Gi"), + 30, + testTopologySpreadConstraints, + &config.KnativeServiceDefaults{ + QueueProxyResourcePercentage: 10, + UserContainerCPULimitRequestFactor: 0, + UserContainerMemoryLimitRequestFactor: 1.5, + }, + ) testDataBasePath := filepath.Join("..", "..", "testdata", "cluster", "servicebuilder") fileBytes, err := tu.ReadFile(filepath.Join(testDataBasePath, "router_version_success.json")) require.NoError(t, err) diff --git a/api/turing/cluster/servicebuilder/service_builder.go b/api/turing/cluster/servicebuilder/service_builder.go index 2a6e31a69..4dd50fdf9 100644 --- a/api/turing/cluster/servicebuilder/service_builder.go +++ b/api/turing/cluster/servicebuilder/service_builder.go @@ -66,18 +66,12 @@ type ClusterServiceBuilder interface { ver *models.RouterVersion, project *mlp.Project, secretName string, - knativeQueueProxyResourcePercentage int, - userContainerCPULimitRequestFactor float64, - userContainerMemoryLimitRequestFactor float64, initialScale *int, ) (*cluster.KnativeService, error) NewEnsemblerService( ver *models.RouterVersion, project *mlp.Project, secretName string, - knativeQueueProxyResourcePercentage int, - userContainerCPULimitRequestFactor float64, - userContainerMemoryLimitRequestFactor float64, initialScale *int, ) (*cluster.KnativeService, error) NewRouterService( @@ -89,9 +83,6 @@ type ClusterServiceBuilder interface { routerDefaults *config.RouterDefaults, sentryEnabled bool, sentryDSN string, - knativeQueueProxyResourcePercentage int, - userContainerCPULimitRequestFactor float64, - userContainerMemoryLimitRequestFactor float64, initialScale *int, ) (*cluster.KnativeService, error) NewFluentdService( @@ -128,6 +119,9 @@ type clusterSvcBuilder struct { MaxMemory resource.Quantity MaxAllowedReplica int TopologySpreadConstraints []corev1.TopologySpreadConstraint + + // Knative service configs + knativeServiceConfig *config.KnativeServiceDefaults } // NewClusterServiceBuilder creates a new service builder with the supplied configs for defaults @@ -136,12 +130,14 @@ func NewClusterServiceBuilder( memoryLimit resource.Quantity, maxAllowedReplica int, topologySpreadConstraints []corev1.TopologySpreadConstraint, + knativeServiceConfig *config.KnativeServiceDefaults, ) ClusterServiceBuilder { return &clusterSvcBuilder{ MaxCPU: cpuLimit, MaxMemory: memoryLimit, MaxAllowedReplica: maxAllowedReplica, TopologySpreadConstraints: topologySpreadConstraints, + knativeServiceConfig: knativeServiceConfig, } } @@ -151,9 +147,6 @@ func (sb *clusterSvcBuilder) NewEnricherService( routerVersion *models.RouterVersion, project *mlp.Project, secretName string, - knativeQueueProxyResourcePercentage int, - userContainerCPULimitRequestFactor float64, - userContainerMemoryLimitRequestFactor float64, initialScale *int, ) (*cluster.KnativeService, error) { // Get the enricher reference @@ -213,23 +206,23 @@ func (sb *clusterSvcBuilder) NewEnricherService( Namespace: namespace, Image: enricher.Image, CPURequests: enricher.ResourceRequest.CPURequest, + CPULimit: sb.getCPULimit(enricher.ResourceRequest), MemoryRequests: enricher.ResourceRequest.MemoryRequest, - Envs: enricher.Env.ToKubernetesEnvVars(), + MemoryLimit: sb.getMemoryLimit(enricher.ResourceRequest), + Envs: sb.getEnvVars(enricher.ResourceRequest, &enricher.Env), Labels: buildLabels(project, routerVersion.Router), Volumes: volumes, VolumeMounts: volumeMounts, }, - IsClusterLocal: true, - ContainerPort: int32(enricher.Port), - MinReplicas: enricher.ResourceRequest.MinReplica, - MaxReplicas: enricher.ResourceRequest.MaxReplica, - InitialScale: initialScale, - AutoscalingMetric: string(enricher.AutoscalingPolicy.Metric), - AutoscalingTarget: enricher.AutoscalingPolicy.Target, - TopologySpreadConstraints: topologySpreadConstraints, - QueueProxyResourcePercentage: knativeQueueProxyResourcePercentage, - UserContainerCPULimitRequestFactor: userContainerCPULimitRequestFactor, - UserContainerMemoryLimitRequestFactor: userContainerMemoryLimitRequestFactor, + IsClusterLocal: true, + ContainerPort: int32(enricher.Port), + MinReplicas: enricher.ResourceRequest.MinReplica, + MaxReplicas: enricher.ResourceRequest.MaxReplica, + InitialScale: initialScale, + AutoscalingMetric: string(enricher.AutoscalingPolicy.Metric), + AutoscalingTarget: enricher.AutoscalingPolicy.Target, + TopologySpreadConstraints: topologySpreadConstraints, + QueueProxyResourcePercentage: sb.knativeServiceConfig.QueueProxyResourcePercentage, }) } @@ -239,9 +232,6 @@ func (sb *clusterSvcBuilder) NewEnsemblerService( routerVersion *models.RouterVersion, project *mlp.Project, secretName string, - knativeQueueProxyResourcePercentage int, - userContainerCPULimitRequestFactor float64, - userContainerMemoryLimitRequestFactor float64, initialScale *int, ) (*cluster.KnativeService, error) { // Get the ensembler reference @@ -302,23 +292,23 @@ func (sb *clusterSvcBuilder) NewEnsemblerService( Namespace: namespace, Image: docker.Image, CPURequests: docker.ResourceRequest.CPURequest, + CPULimit: sb.getCPULimit(docker.ResourceRequest), MemoryRequests: docker.ResourceRequest.MemoryRequest, - Envs: docker.Env.ToKubernetesEnvVars(), + MemoryLimit: sb.getMemoryLimit(docker.ResourceRequest), + Envs: sb.getEnvVars(docker.ResourceRequest, &docker.Env), Labels: buildLabels(project, routerVersion.Router), Volumes: volumes, VolumeMounts: volumeMounts, }, - IsClusterLocal: true, - ContainerPort: int32(docker.Port), - MinReplicas: docker.ResourceRequest.MinReplica, - MaxReplicas: docker.ResourceRequest.MaxReplica, - InitialScale: initialScale, - AutoscalingMetric: string(docker.AutoscalingPolicy.Metric), - AutoscalingTarget: docker.AutoscalingPolicy.Target, - TopologySpreadConstraints: topologySpreadConstraints, - QueueProxyResourcePercentage: knativeQueueProxyResourcePercentage, - UserContainerCPULimitRequestFactor: userContainerCPULimitRequestFactor, - UserContainerMemoryLimitRequestFactor: userContainerMemoryLimitRequestFactor, + IsClusterLocal: true, + ContainerPort: int32(docker.Port), + MinReplicas: docker.ResourceRequest.MinReplica, + MaxReplicas: docker.ResourceRequest.MaxReplica, + InitialScale: initialScale, + AutoscalingMetric: string(docker.AutoscalingPolicy.Metric), + AutoscalingTarget: docker.AutoscalingPolicy.Target, + TopologySpreadConstraints: topologySpreadConstraints, + QueueProxyResourcePercentage: sb.knativeServiceConfig.QueueProxyResourcePercentage, }) } @@ -414,6 +404,45 @@ func (sb *clusterSvcBuilder) getTopologySpreadConstraints() ([]corev1.TopologySp return topologySpreadConstraints, nil } +func (sb *clusterSvcBuilder) getCPULimit(resourceRequest *models.ResourceRequest) *resource.Quantity { + if resourceRequest == nil { + return nil + } + + if resourceRequest.CPULimit != nil && !resourceRequest.CPULimit.IsZero() { + return resourceRequest.CPULimit + } + + if sb.knativeServiceConfig.UserContainerCPULimitRequestFactor == 0 { + return nil + } + + cpuLimit := cluster.ComputeResource(resourceRequest.CPURequest, + sb.knativeServiceConfig.UserContainerCPULimitRequestFactor) + return &cpuLimit +} + +func (sb *clusterSvcBuilder) getMemoryLimit(resourceRequest *models.ResourceRequest) *resource.Quantity { + if resourceRequest != nil && sb.knativeServiceConfig.UserContainerMemoryLimitRequestFactor != 0 { + memoryLimit := cluster.ComputeResource(resourceRequest.MemoryRequest, + sb.knativeServiceConfig.UserContainerMemoryLimitRequestFactor) + return &memoryLimit + } + return nil +} + +func (sb *clusterSvcBuilder) getEnvVars(resourceRequest *models.ResourceRequest, + userEnvVars *models.EnvVars) (newEnvVars []corev1.EnvVar) { + if resourceRequest != nil && (resourceRequest.CPULimit == nil || resourceRequest.CPULimit.IsZero()) && + sb.knativeServiceConfig.UserContainerCPULimitRequestFactor == 0 { + newEnvVars = mergeEnvVars(newEnvVars, sb.knativeServiceConfig.DefaultEnvVarsWithoutCPULimits) + } + if userEnvVars != nil { + newEnvVars = mergeEnvVars(newEnvVars, userEnvVars.ToKubernetesEnvVars()) + } + return +} + func GetComponentName(routerVersion *models.RouterVersion, componentType string) string { return fmt.Sprintf("%s-turing-%s-%d", routerVersion.Router.Name, componentType, routerVersion.Version) } @@ -434,3 +463,23 @@ func buildLabels( } return labeller.BuildLabels(r) } + +// mergeEnvVars merges multiple sets of environment variables and return the merging result. +// All the EnvVars passed as arguments will be not mutated. +// EnvVars to the right have higher precedence. +func mergeEnvVars(left []corev1.EnvVar, rightEnvVars ...[]corev1.EnvVar) []corev1.EnvVar { + for _, right := range rightEnvVars { + envIndexMap := make(map[string]int, len(left)+len(right)) + for index, ev := range left { + envIndexMap[ev.Name] = index + } + for _, add := range right { + if index, exist := envIndexMap[add.Name]; exist { + left[index].Value = add.Value + } else { + left = append(left, add) + } + } + } + return left +} diff --git a/api/turing/cluster/servicebuilder/service_builder_test.go b/api/turing/cluster/servicebuilder/service_builder_test.go index a1c595a9a..7f40cf3ce 100644 --- a/api/turing/cluster/servicebuilder/service_builder_test.go +++ b/api/turing/cluster/servicebuilder/service_builder_test.go @@ -6,6 +6,7 @@ import ( "testing" mlp "github.com/caraml-dev/mlp/api/client" + "github.com/caraml-dev/turing/api/turing/config" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -42,7 +43,27 @@ type testSuiteNewService struct { } func TestNewEnricherService(t *testing.T) { - sb := NewClusterServiceBuilder(resource.MustParse("2"), resource.MustParse("2Gi"), 30, testTopologySpreadConstraints) + userContainerCPULimitRequestFactor := 2.0 + userContainerMemoryLimitRequestFactor := 1.5 + + sb := NewClusterServiceBuilder( + resource.MustParse("2"), + resource.MustParse("2Gi"), + 30, + testTopologySpreadConstraints, + &config.KnativeServiceDefaults{ + QueueProxyResourcePercentage: 10, + UserContainerCPULimitRequestFactor: userContainerCPULimitRequestFactor, + UserContainerMemoryLimitRequestFactor: userContainerMemoryLimitRequestFactor, + }, + ) + + cpuRequest := resource.MustParse("400m") + cpuLimit := cluster.ComputeResource(cpuRequest, userContainerCPULimitRequestFactor) + + memoryRequest := resource.MustParse("256Mi") + memoryLimit := cluster.ComputeResource(memoryRequest, userContainerMemoryLimitRequestFactor) + testDataBasePath := filepath.Join("..", "..", "testdata", "cluster", "servicebuilder") testInitialScale := 5 @@ -55,8 +76,10 @@ func TestNewEnricherService(t *testing.T) { Name: "test-svc-turing-enricher-1", Namespace: "test-project", Image: "asia.gcr.io/gcp-project-id/echo:1.0.2", - CPURequests: resource.MustParse("400m"), - MemoryRequests: resource.MustParse("256Mi"), + CPURequests: cpuRequest, + CPULimit: &cpuLimit, + MemoryRequests: memoryRequest, + MemoryLimit: &memoryLimit, Envs: []corev1.EnvVar{ {Name: "TEST_ENV", Value: "enricher"}, {Name: "GOOGLE_APPLICATION_CREDENTIALS", Value: "/var/secret/enricher-service-account.json"}, @@ -80,17 +103,15 @@ func TestNewEnricherService(t *testing.T) { }, VolumeMounts: []corev1.VolumeMount{{Name: secretVolume, MountPath: secretMountPath}}, }, - IsClusterLocal: true, - ContainerPort: 8080, - MinReplicas: 1, - MaxReplicas: 2, - InitialScale: &testInitialScale, - AutoscalingMetric: "concurrency", - AutoscalingTarget: "1", - TopologySpreadConstraints: testTopologySpreadConstraints, - QueueProxyResourcePercentage: 10, - UserContainerCPULimitRequestFactor: 0, - UserContainerMemoryLimitRequestFactor: 1.5, + IsClusterLocal: true, + ContainerPort: 8080, + MinReplicas: 1, + MaxReplicas: 2, + InitialScale: &testInitialScale, + AutoscalingMetric: "concurrency", + AutoscalingTarget: "1", + TopologySpreadConstraints: testTopologySpreadConstraints, + QueueProxyResourcePercentage: 10, }, }, "failure": { @@ -111,7 +132,7 @@ func TestNewEnricherService(t *testing.T) { Team: "test-team", Labels: []mlp.Label{{Key: "custom-label-key", Value: "value-1"}}, } - svc, err := sb.NewEnricherService(routerVersion, project, "secret", 10, 0, 1.5, data.initialScale) + svc, err := sb.NewEnricherService(routerVersion, project, "secret", data.initialScale) if data.err == "" { assert.NoError(t, err) assert.Equal(t, data.expected, svc) @@ -123,7 +144,27 @@ func TestNewEnricherService(t *testing.T) { } func TestNewEnsemblerService(t *testing.T) { - sb := NewClusterServiceBuilder(resource.MustParse("2"), resource.MustParse("2Gi"), 30, testTopologySpreadConstraints) + userContainerCPULimitRequestFactor := 2.0 + userContainerMemoryLimitRequestFactor := 1.5 + + sb := NewClusterServiceBuilder( + resource.MustParse("2"), + resource.MustParse("2Gi"), + 30, + testTopologySpreadConstraints, + &config.KnativeServiceDefaults{ + QueueProxyResourcePercentage: 20, + UserContainerCPULimitRequestFactor: userContainerCPULimitRequestFactor, + UserContainerMemoryLimitRequestFactor: userContainerMemoryLimitRequestFactor, + }, + ) + + cpuRequest := resource.MustParse("200m") + cpuLimit := cluster.ComputeResource(cpuRequest, userContainerCPULimitRequestFactor) + + memoryRequest := resource.MustParse("1024Mi") + memoryLimit := cluster.ComputeResource(memoryRequest, userContainerMemoryLimitRequestFactor) + testDataBasePath := filepath.Join("..", "..", "testdata", "cluster", "servicebuilder") testInitialScale := 5 @@ -136,8 +177,10 @@ func TestNewEnsemblerService(t *testing.T) { Name: "test-svc-turing-ensembler-1", Namespace: "test-project", Image: "asia.gcr.io/gcp-project-id/echo:1.0.2", - CPURequests: resource.MustParse("200m"), - MemoryRequests: resource.MustParse("1024Mi"), + CPURequests: cpuRequest, + CPULimit: &cpuLimit, + MemoryRequests: memoryRequest, + MemoryLimit: &memoryLimit, Envs: []corev1.EnvVar{ {Name: "TEST_ENV", Value: "ensembler"}, {Name: "GOOGLE_APPLICATION_CREDENTIALS", Value: "/var/secret/ensembler-service-account.json"}, @@ -160,17 +203,15 @@ func TestNewEnsemblerService(t *testing.T) { }, VolumeMounts: []corev1.VolumeMount{{Name: secretVolume, MountPath: secretMountPath}}, }, - IsClusterLocal: true, - ContainerPort: 8080, - MinReplicas: 2, - MaxReplicas: 3, - AutoscalingMetric: "concurrency", - AutoscalingTarget: "1", - InitialScale: &testInitialScale, - TopologySpreadConstraints: testTopologySpreadConstraints, - QueueProxyResourcePercentage: 20, - UserContainerCPULimitRequestFactor: 0, - UserContainerMemoryLimitRequestFactor: 1.5, + IsClusterLocal: true, + ContainerPort: 8080, + MinReplicas: 2, + MaxReplicas: 3, + AutoscalingMetric: "concurrency", + AutoscalingTarget: "1", + InitialScale: &testInitialScale, + TopologySpreadConstraints: testTopologySpreadConstraints, + QueueProxyResourcePercentage: 20, }, }, "success with ensembler docker type": { @@ -180,8 +221,10 @@ func TestNewEnsemblerService(t *testing.T) { Name: "test-svc-turing-ensembler-1", Namespace: "test-project", Image: "asia.gcr.io/gcp-project-id/echo:1.0.2", - CPURequests: resource.MustParse("200m"), - MemoryRequests: resource.MustParse("1024Mi"), + CPURequests: cpuRequest, + CPULimit: &cpuLimit, + MemoryRequests: memoryRequest, + MemoryLimit: &memoryLimit, Envs: []corev1.EnvVar{ {Name: "TEST_ENV", Value: "ensembler"}, {Name: "GOOGLE_APPLICATION_CREDENTIALS", Value: "/var/secret/ensembler-service-account.json"}, @@ -204,16 +247,14 @@ func TestNewEnsemblerService(t *testing.T) { }, VolumeMounts: []corev1.VolumeMount{{Name: secretVolume, MountPath: secretMountPath}}, }, - IsClusterLocal: true, - ContainerPort: 8080, - MinReplicas: 2, - MaxReplicas: 3, - AutoscalingMetric: "cpu", - AutoscalingTarget: "90", - TopologySpreadConstraints: testTopologySpreadConstraints, - QueueProxyResourcePercentage: 20, - UserContainerCPULimitRequestFactor: 0, - UserContainerMemoryLimitRequestFactor: 1.5, + IsClusterLocal: true, + ContainerPort: 8080, + MinReplicas: 2, + MaxReplicas: 3, + AutoscalingMetric: "cpu", + AutoscalingTarget: "90", + TopologySpreadConstraints: testTopologySpreadConstraints, + QueueProxyResourcePercentage: 20, }, }, "failure": { @@ -233,7 +274,7 @@ func TestNewEnsemblerService(t *testing.T) { Stream: "test-stream", Team: "test-team", } - svc, err := sb.NewEnsemblerService(routerVersion, project, "secret", 20, 0, 1.5, data.initialScale) + svc, err := sb.NewEnsemblerService(routerVersion, project, "secret", data.initialScale) if data.err == "" { assert.NoError(t, err) assert.Equal(t, data.expected, svc) diff --git a/api/turing/config/config.go b/api/turing/config/config.go index 2eb40bf7f..cc3ef5fbc 100644 --- a/api/turing/config/config.go +++ b/api/turing/config/config.go @@ -263,8 +263,9 @@ type KubernetesLabelConfigs struct { // Knative services type KnativeServiceDefaults struct { QueueProxyResourcePercentage int - UserContainerCPULimitRequestFactor float64 `json:"userContainerLimitCPURequestFactor"` - UserContainerMemoryLimitRequestFactor float64 `json:"userContainerLimitMemoryRequestFactor"` + UserContainerCPULimitRequestFactor float64 + UserContainerMemoryLimitRequestFactor float64 + DefaultEnvVarsWithoutCPULimits []corev1.EnvVar } // SinglePageApplicationConfig holds configuration required for serving SPAs diff --git a/api/turing/config/config_test.go b/api/turing/config/config_test.go index c07180fde..a08ea9104 100644 --- a/api/turing/config/config_test.go +++ b/api/turing/config/config_test.go @@ -283,6 +283,12 @@ func TestLoad(t *testing.T) { QueueProxyResourcePercentage: 20, UserContainerCPULimitRequestFactor: 0, UserContainerMemoryLimitRequestFactor: 1.25, + DefaultEnvVarsWithoutCPULimits: []corev1.EnvVar{ + { + Name: "foo", + Value: "bar", + }, + }, }, RouterDefaults: &config.RouterDefaults{ LogLevel: "INFO", @@ -425,6 +431,12 @@ func TestLoad(t *testing.T) { QueueProxyResourcePercentage: 20, UserContainerCPULimitRequestFactor: 0, UserContainerMemoryLimitRequestFactor: 1.25, + DefaultEnvVarsWithoutCPULimits: []corev1.EnvVar{ + { + Name: "foo", + Value: "bar", + }, + }, }, RouterDefaults: &config.RouterDefaults{ LogLevel: "INFO", @@ -554,6 +566,12 @@ func TestLoad(t *testing.T) { QueueProxyResourcePercentage: 20, UserContainerCPULimitRequestFactor: 0, UserContainerMemoryLimitRequestFactor: 1.25, + DefaultEnvVarsWithoutCPULimits: []corev1.EnvVar{ + { + Name: "foo", + Value: "bar", + }, + }, }, DeployConfig: &config.DeploymentConfig{ EnvironmentType: "dev", diff --git a/api/turing/config/testdata/config-1.yaml b/api/turing/config/testdata/config-1.yaml index ff2a7aeac..968217574 100644 --- a/api/turing/config/testdata/config-1.yaml +++ b/api/turing/config/testdata/config-1.yaml @@ -48,6 +48,9 @@ KnativeServiceDefaults: QueueProxyResourcePercentage: 20 UserContainerCPULimitRequestFactor: 0 UserContainerMemoryLimitRequestFactor: 1.25 + DefaultEnvVarsWithoutCPULimits: + - Name: foo + Value: bar RouterDefaults: FluentdConfig: FlushIntervalSeconds: 60 diff --git a/api/turing/internal/testutils/validation.go b/api/turing/internal/testutils/validation.go index 822c3fc4f..a3ed80f0c 100644 --- a/api/turing/internal/testutils/validation.go +++ b/api/turing/internal/testutils/validation.go @@ -1,7 +1,6 @@ package testutils import ( - "encoding/json" "fmt" "reflect" @@ -16,21 +15,7 @@ func CompareObjects(actual interface{}, expected interface{}) error { allowUnexportedOn = reflect.ValueOf(actual).Elem().Interface() } if !cmp.Equal(actual, expected, cmp.AllowUnexported(allowUnexportedOn)) { - actualString := fmt.Sprintf("%+v", actual) - expectedString := fmt.Sprintf("%+v", expected) - - // Attempt to encode values to JSON, for logging - jsonActual, err := json.Marshal(actual) - if err == nil { - actualString = string(jsonActual) - } - jsonExpected, err := json.Marshal(expected) - if err == nil { - expectedString = string(jsonExpected) - } - - return fmt.Errorf("Did not get expected configuration.\nEXPECTED:\n%v\nGOT:\n%v", - expectedString, actualString) + return fmt.Errorf(cmp.Diff(actual, expected, cmp.AllowUnexported(allowUnexportedOn))) } return nil } diff --git a/api/turing/models/resource_request.go b/api/turing/models/resource_request.go index 0d3e3f044..ab4dcf764 100644 --- a/api/turing/models/resource_request.go +++ b/api/turing/models/resource_request.go @@ -16,6 +16,8 @@ type ResourceRequest struct { // CPU request of inference service CPURequest resource.Quantity `json:"cpu_request"` + // CPU limit of inference service + CPULimit *resource.Quantity `json:"cpu_limit,omitempty"` // Memory request of inference service MemoryRequest resource.Quantity `json:"memory_request"` } diff --git a/api/turing/service/router_deployment_service.go b/api/turing/service/router_deployment_service.go index 269c708b8..734e2866f 100644 --- a/api/turing/service/router_deployment_service.go +++ b/api/turing/service/router_deployment_service.go @@ -66,9 +66,6 @@ type deploymentService struct { sentryDSN string routerDefaults *config.RouterDefaults - // Knative service configs - knativeServiceConfig *config.KnativeServiceDefaults - // Ensembler service image builder for real time ensemblers ensemblerServiceImageBuilder imagebuilder.ImageBuilder @@ -94,6 +91,7 @@ func NewDeploymentService( resource.Quantity(cfg.DeployConfig.MaxMemory), cfg.DeployConfig.MaxAllowedReplica, cfg.DeployConfig.TopologySpreadConstraints, + cfg.KnativeServiceDefaults, ) return &deploymentService{ @@ -101,7 +99,6 @@ func NewDeploymentService( deploymentDeletionTimeout: cfg.DeployConfig.DeletionTimeout, environmentType: cfg.DeployConfig.EnvironmentType, routerDefaults: cfg.RouterDefaults, - knativeServiceConfig: cfg.KnativeServiceDefaults, ensemblerServiceImageBuilder: ensemblerServiceImageBuilder, sentryEnabled: cfg.Sentry.Enabled, sentryDSN: cfg.Sentry.DSN, @@ -176,9 +173,6 @@ func (ds *deploymentService) DeployRouterVersion( routerVersion, currRouterVersion, project, ds.environmentType, secretName, experimentConfig, ds.routerDefaults, ds.sentryEnabled, ds.sentryDSN, - ds.knativeServiceConfig.QueueProxyResourcePercentage, - ds.knativeServiceConfig.UserContainerCPULimitRequestFactor, - ds.knativeServiceConfig.UserContainerMemoryLimitRequestFactor, ) if err != nil { return endpoint, err @@ -277,9 +271,6 @@ func (ds *deploymentService) UndeployRouterVersion( ctx, controller, routerVersion, nil, project, ds.environmentType, "", nil, ds.routerDefaults, ds.sentryEnabled, ds.sentryDSN, - ds.knativeServiceConfig.QueueProxyResourcePercentage, - ds.knativeServiceConfig.UserContainerCPULimitRequestFactor, - ds.knativeServiceConfig.UserContainerMemoryLimitRequestFactor, ) if err != nil { return err @@ -367,9 +358,6 @@ func (ds *deploymentService) createServices( routerDefaults *config.RouterDefaults, sentryEnabled bool, sentryDSN string, - knativeQueueProxyResourcePercentage int, - userContainerCPULimitRequestFactor float64, - userContainerMemoryLimitRequestFactor float64, ) ([]*cluster.KnativeService, error) { services := []*cluster.KnativeService{} namespace := servicebuilder.GetNamespace(project) @@ -387,9 +375,7 @@ func (ds *deploymentService) createServices( } } enricherSvc, err := ds.svcBuilder.NewEnricherService( - routerVersion, project, secretName, - knativeQueueProxyResourcePercentage, userContainerCPULimitRequestFactor, - userContainerMemoryLimitRequestFactor, currEnricherReplicas, + routerVersion, project, secretName, currEnricherReplicas, ) if err != nil { return services, err @@ -410,9 +396,7 @@ func (ds *deploymentService) createServices( } } ensemblerSvc, err := ds.svcBuilder.NewEnsemblerService( - routerVersion, project, secretName, - knativeQueueProxyResourcePercentage, userContainerCPULimitRequestFactor, - userContainerMemoryLimitRequestFactor, currEnsemblerReplicas, + routerVersion, project, secretName, currEnsemblerReplicas, ) if err != nil { return services, err @@ -431,9 +415,7 @@ func (ds *deploymentService) createServices( } routerService, err := ds.svcBuilder.NewRouterService( routerVersion, project, envType, secretName, experimentConfig, - routerDefaults, sentryEnabled, sentryDSN, - knativeQueueProxyResourcePercentage, userContainerCPULimitRequestFactor, - userContainerMemoryLimitRequestFactor, currRouterReplicas, + routerDefaults, sentryEnabled, sentryDSN, currRouterReplicas, ) if err != nil { return services, err diff --git a/api/turing/service/router_deployment_service_test.go b/api/turing/service/router_deployment_service_test.go index 4f8873d11..b77961217 100644 --- a/api/turing/service/router_deployment_service_test.go +++ b/api/turing/service/router_deployment_service_test.go @@ -33,7 +33,8 @@ import ( // mockClusterServiceBuilder implements the servicebuilder.ClusterServiceBuilder interface type mockClusterServiceBuilder struct { - rv *models.RouterVersion + rv *models.RouterVersion + knativeServiceConfig *config.KnativeServiceDefaults } func (msb *mockClusterServiceBuilder) NewRouterEndpoint( @@ -75,9 +76,6 @@ func (msb *mockClusterServiceBuilder) NewEnricherService( rv *models.RouterVersion, project *mlp.Project, _ string, - queueProxyResourcePercentage int, - userContainerCPULimitRequestFactor float64, - userContainerMemoryLimitRequestFactor float64, _ *int, ) (*cluster.KnativeService, error) { if rv != msb.rv { @@ -88,9 +86,7 @@ func (msb *mockClusterServiceBuilder) NewEnricherService( Name: fmt.Sprintf("%s-enricher-%d", rv.Router.Name, rv.Version), Namespace: project.Name, }, - QueueProxyResourcePercentage: queueProxyResourcePercentage, - UserContainerCPULimitRequestFactor: userContainerCPULimitRequestFactor, - UserContainerMemoryLimitRequestFactor: userContainerMemoryLimitRequestFactor, + QueueProxyResourcePercentage: msb.knativeServiceConfig.QueueProxyResourcePercentage, }, nil } @@ -98,9 +94,6 @@ func (msb *mockClusterServiceBuilder) NewEnsemblerService( rv *models.RouterVersion, project *mlp.Project, _ string, - queueProxyResourcePercentage int, - userContainerCPULimitRequestFactor float64, - userContainerMemoryLimitRequestFactor float64, _ *int, ) (*cluster.KnativeService, error) { if rv != msb.rv { @@ -111,9 +104,7 @@ func (msb *mockClusterServiceBuilder) NewEnsemblerService( Name: fmt.Sprintf("%s-ensembler-%d", rv.Router.Name, rv.Version), Namespace: project.Name, }, - QueueProxyResourcePercentage: queueProxyResourcePercentage, - UserContainerCPULimitRequestFactor: userContainerCPULimitRequestFactor, - UserContainerMemoryLimitRequestFactor: userContainerMemoryLimitRequestFactor, + QueueProxyResourcePercentage: msb.knativeServiceConfig.QueueProxyResourcePercentage, }, nil } @@ -126,9 +117,6 @@ func (msb *mockClusterServiceBuilder) NewRouterService( routerDefaults *config.RouterDefaults, sentryEnabled bool, sentryDSN string, - queueProxyResourcePercentage int, - userContainerCPULimitRequestFactor float64, - userContainerMemoryLimitRequestFactor float64, _ *int, ) (*cluster.KnativeService, error) { if rv != msb.rv { @@ -150,9 +138,7 @@ func (msb *mockClusterServiceBuilder) NewRouterService( Data: string(expConfig), }, }, - QueueProxyResourcePercentage: queueProxyResourcePercentage, - UserContainerCPULimitRequestFactor: userContainerCPULimitRequestFactor, - UserContainerMemoryLimitRequestFactor: userContainerMemoryLimitRequestFactor, + QueueProxyResourcePercentage: msb.knativeServiceConfig.QueueProxyResourcePercentage, }, nil } @@ -225,7 +211,14 @@ func TestDeployEndpoint(t *testing.T) { Return(&policyv1.PodDisruptionBudget{}, nil) // Create mock service builder - svcBuilder := &mockClusterServiceBuilder{routerVersion} + svcBuilder := &mockClusterServiceBuilder{ + rv: routerVersion, + knativeServiceConfig: &config.KnativeServiceDefaults{ + QueueProxyResourcePercentage: 20, + UserContainerCPULimitRequestFactor: 1.75, + UserContainerMemoryLimitRequestFactor: 1.75, + }, + } // Create test endpoint service with mock controller and service builder ds := &deploymentService{ @@ -238,11 +231,6 @@ func TestDeployEndpoint(t *testing.T) { environmentType: envType, sentryEnabled: true, sentryDSN: "test:dsn", - knativeServiceConfig: &config.KnativeServiceDefaults{ - QueueProxyResourcePercentage: 20, - UserContainerCPULimitRequestFactor: 1.75, - UserContainerMemoryLimitRequestFactor: 1.75, - }, clusterControllers: map[string]cluster.Controller{ testEnv: controller, }, @@ -304,18 +292,14 @@ func TestDeployEndpoint(t *testing.T) { Name: fmt.Sprintf("%s-enricher-%d", routerVersion.Router.Name, routerVersion.Version), Namespace: testNamespace, }, - QueueProxyResourcePercentage: 20, - UserContainerCPULimitRequestFactor: 1.75, - UserContainerMemoryLimitRequestFactor: 1.75, + QueueProxyResourcePercentage: 20, }) controller.AssertCalled(t, "DeployKnativeService", mock.Anything, &cluster.KnativeService{ BaseService: &cluster.BaseService{ Name: fmt.Sprintf("%s-ensembler-%d", routerVersion.Router.Name, routerVersion.Version), Namespace: testNamespace, }, - QueueProxyResourcePercentage: 20, - UserContainerCPULimitRequestFactor: 1.75, - UserContainerMemoryLimitRequestFactor: 1.75, + QueueProxyResourcePercentage: 20, }) controller.AssertCalled(t, "ApplyConfigMap", mock.Anything, testNamespace, &cluster.ConfigMap{Name: fmt.Sprintf("%s-fiber-config-%d", routerVersion.Router.Name, routerVersion.Version)}) @@ -337,9 +321,7 @@ func TestDeployEndpoint(t *testing.T) { ), }, }, - QueueProxyResourcePercentage: 20, - UserContainerCPULimitRequestFactor: 1.75, - UserContainerMemoryLimitRequestFactor: 1.75, + QueueProxyResourcePercentage: 20, }) controller.AssertCalled(t, "CreateSecret", mock.Anything, &cluster.Secret{ Name: fmt.Sprintf("%s-svc-acct-secret-%d", routerVersion.Router.Name, routerVersion.Version), @@ -427,7 +409,14 @@ func TestDeleteEndpoint(t *testing.T) { routerVersion := tu.GetRouterVersion(t, filePath) // Create mock service builder - svcBuilder := &mockClusterServiceBuilder{routerVersion} + svcBuilder := &mockClusterServiceBuilder{ + rv: routerVersion, + knativeServiceConfig: &config.KnativeServiceDefaults{ + QueueProxyResourcePercentage: 20, + UserContainerCPULimitRequestFactor: 1.75, + UserContainerMemoryLimitRequestFactor: 1.75, + }, + } // Create test endpoint service with mock controller and service builder ds := &deploymentService{ @@ -437,11 +426,6 @@ func TestDeleteEndpoint(t *testing.T) { }, deploymentTimeout: timeout, deploymentDeletionTimeout: timeout, - knativeServiceConfig: &config.KnativeServiceDefaults{ - QueueProxyResourcePercentage: 20, - UserContainerCPULimitRequestFactor: 1.75, - UserContainerMemoryLimitRequestFactor: 1.75, - }, clusterControllers: map[string]cluster.Controller{ testEnv: controller, }, @@ -779,6 +763,7 @@ func TestCreatePodDisruptionBudgets(t *testing.T) { resource.MustParse("200Mi"), 10, []corev1.TopologySpreadConstraint{}, + nil, ), } pdbs := ds.createPodDisruptionBudgets(tt.rv, &mlp.Project{Name: "ns"}) diff --git a/docs/.gitbook/assets/pyfunc_ensembler_config.png b/docs/.gitbook/assets/pyfunc_ensembler_config.png index 7ffe61f86..739c8c2ef 100644 Binary files a/docs/.gitbook/assets/pyfunc_ensembler_config.png and b/docs/.gitbook/assets/pyfunc_ensembler_config.png differ diff --git a/docs/.gitbook/assets/resources_panel.png b/docs/.gitbook/assets/resources_panel.png index 9ac0f15ac..14f923f41 100644 Binary files a/docs/.gitbook/assets/resources_panel.png and b/docs/.gitbook/assets/resources_panel.png differ diff --git a/docs/how-to/create-a-router/configure-enricher.md b/docs/how-to/create-a-router/configure-enricher.md index ace7f1caa..7011aff66 100644 --- a/docs/how-to/create-a-router/configure-enricher.md +++ b/docs/how-to/create-a-router/configure-enricher.md @@ -47,6 +47,11 @@ Configure the resources required for the enricher. There are 3 required inputs, **Min/Max Replicas**: Min/max number of replicas for your enricher. Scaling of the enricher based on traffic volume will be automatically done for you. +**CPU Limit**: By default, Turing determines the CPU limits of all deployed components using platform-level configured +values. These CPU limits is calculated as a factor of the user-defined CPU request value for each component (e.g. 2x +of the CPU request value). However, you can override this platform-level configured value by setting this value +explicitly on the UI (as seen above) or via the SDK. + Optionally, modify the autoscaling policy on the enricher. ![](../../.gitbook/assets/autoscaling_policy_panel.png) diff --git a/docs/how-to/create-a-router/configure-ensembler.md b/docs/how-to/create-a-router/configure-ensembler.md index 90a0d888c..17716afdf 100644 --- a/docs/how-to/create-a-router/configure-ensembler.md +++ b/docs/how-to/create-a-router/configure-ensembler.md @@ -83,6 +83,11 @@ Configure the resources required for the ensembler. There are 3 required inputs, **Min/Max Replicas**: Min/max number of replicas for your ensembler. Scaling of the ensembler based on traffic volume will be automatically done for you. +**CPU Limit**: By default, Turing determines the CPU limits of all deployed components using platform-level configured +values. These CPU limits is calculated as a factor of the user-defined CPU request value for each component (e.g. 2x +of the CPU request value). However, you can override this platform-level configured value by setting this value +explicitly on the UI (as seen above) or via the SDK. + Optionally, modify the autoscaling policy on the ensembler. ![](../../.gitbook/assets/autoscaling_policy_panel.png) @@ -125,6 +130,11 @@ registered in your current project. You'll also need to indicate your desired ti **Min/Max Replicas**: Min/max number of replicas for your ensembler. Scaling of the ensembler based on traffic volume will be automatically done for you. +**CPU Limit**: By default, Turing determines the CPU limits of all deployed components using platform-level configured +values. These CPU limits is calculated as a factor of the user-defined CPU request value for each component (e.g. 2x +of the CPU request value). However, you can override this platform-level configured value by setting this value +explicitly on the UI (as seen above) or via the SDK. + Optionally, modify the autoscaling policy on the ensembler. ![](../../.gitbook/assets/autoscaling_policy_panel.png) diff --git a/sdk/tests/conftest.py b/sdk/tests/conftest.py index 5899a7114..e75cfd9a0 100644 --- a/sdk/tests/conftest.py +++ b/sdk/tests/conftest.py @@ -277,7 +277,7 @@ def generic_router_version_status(): @pytest.fixture def generic_resource_request(): return turing.generated.models.ResourceRequest( - min_replica=1, max_replica=3, cpu_request="100m", memory_request="512Mi" + min_replica=1, max_replica=3, cpu_request="100m", memory_request="512Mi", cpu_limit=None, ) diff --git a/sdk/tests/router/config/router_ensembler_config_test.py b/sdk/tests/router/config/router_ensembler_config_test.py index bb19f788b..878d4cfd1 100644 --- a/sdk/tests/router/config/router_ensembler_config_test.py +++ b/sdk/tests/router/config/router_ensembler_config_test.py @@ -600,7 +600,7 @@ def test_create_nop_router_ensembler_config_with_invalid_route( image="test.io/just-a-test/turing-ensembler:0.0.0-build.0", port=5120, resource_request=turing.generated.models.ResourceRequest( - cpu_request="100m", max_replica=3, + cpu_request="100m", cpu_limit=None, max_replica=3, memory_request="512Mi", min_replica=1, ), service_account="secret-name-for-google-service-account", @@ -619,8 +619,8 @@ def test_create_nop_router_ensembler_config_with_invalid_route( env=[turing.generated.models.EnvVar(name="env_name", value="env_val")], project_id=77, resource_request=turing.generated.models.ResourceRequest( - cpu_request="100m", max_replica=3, - memory_request="512Mi",min_replica=1, + cpu_request="100m", cpu_limit=None, max_replica=3, + memory_request="512Mi", min_replica=1, ), timeout="500ms" ), diff --git a/sdk/turing/generated/model/resource_request.py b/sdk/turing/generated/model/resource_request.py index c352380f5..a76eef61b 100644 --- a/sdk/turing/generated/model/resource_request.py +++ b/sdk/turing/generated/model/resource_request.py @@ -60,6 +60,11 @@ class ResourceRequest(ModelNormal): 'pattern': r'^(\d{1,3}(\.\d{1,3})?)$|^(\d{2,5}m)$', # noqa: E501 }, }, + ('cpu_limit',): { + 'regex': { + 'pattern': r'^(\d{1,3}(\.\d{1,3})?)$|^(\d{2,5}m)$', # noqa: E501 + }, + }, ('memory_request',): { 'regex': { 'pattern': r'^\d+(Ei?|Pi?|Ti?|Gi?|Mi?|Ki?)?$', # noqa: E501 @@ -85,6 +90,7 @@ def openapi_types(): 'min_replica': (int,), # noqa: E501 'max_replica': (int,), # noqa: E501 'cpu_request': (str,), # noqa: E501 + 'cpu_limit': (str, none_type,), # noqa: E501 'memory_request': (str,), # noqa: E501 } @@ -97,6 +103,7 @@ def discriminator(): 'min_replica': 'min_replica', # noqa: E501 'max_replica': 'max_replica', # noqa: E501 'cpu_request': 'cpu_request', # noqa: E501 + 'cpu_limit': 'cpu_limit', # noqa: E501 'memory_request': 'memory_request', # noqa: E501 } @@ -149,6 +156,7 @@ def __init__(self, *args, **kwargs): # noqa: E501 min_replica (int): [optional] # noqa: E501 max_replica (int): [optional] # noqa: E501 cpu_request (str): [optional] # noqa: E501 + cpu_limit (str, none_type): [optional] # noqa: E501 memory_request (str): [optional] # noqa: E501 """ diff --git a/sdk/turing/router/config/resource_request.py b/sdk/turing/router/config/resource_request.py index 45885b7d9..65a8db8f0 100644 --- a/sdk/turing/router/config/resource_request.py +++ b/sdk/turing/router/config/resource_request.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from typing import ClassVar +from typing import Optional import turing.generated.models from turing.generated.model_utils import OpenApiModel @@ -11,11 +11,13 @@ class ResourceRequest: max_replica: int cpu_request: str memory_request: str + cpu_limit: Optional[str] = None _min_replica: int = field(init=False, repr=False) _max_replica: int = field(init=False, repr=False) _cpu_request: str = field(init=False, repr=False) _memory_request: str = field(init=False, repr=False) + _cpu_limit: Optional[str] = field(init=False, repr=False, default=None) @property def min_replica(self) -> int: @@ -45,6 +47,16 @@ def cpu_request(self) -> str: def cpu_request(self, cpu_request: str): self._cpu_request = cpu_request + @property + def cpu_limit(self) -> str: + return self._cpu_limit + + @cpu_limit.setter + def cpu_limit(self, cpu_limit): + if type(cpu_limit) is property: + cpu_limit = ResourceRequest._cpu_limit + self._cpu_limit = cpu_limit + @property def memory_request(self) -> str: return self._memory_request @@ -66,6 +78,7 @@ def to_open_api(self) -> OpenApiModel: min_replica=self.min_replica, max_replica=self.max_replica, cpu_request=self.cpu_request, + cpu_limit=self.cpu_limit, memory_request=self.memory_request, ) diff --git a/ui/src/router/components/configuration/components/ResourcesConfigTable.js b/ui/src/router/components/configuration/components/ResourcesConfigTable.js index 32ce7e2f8..136037c25 100644 --- a/ui/src/router/components/configuration/components/ResourcesConfigTable.js +++ b/ui/src/router/components/configuration/components/ResourcesConfigTable.js @@ -5,13 +5,19 @@ import { autoscalingPolicyOptions } from "../../form/components/autoscaling_poli import { ConfigMultiSectionPanel } from "../../../../components/config_multi_section_panel/ConfigMultiSectionPanel" const ResourcesSection = ({ - resourceRequest: { cpu_request, memory_request, min_replica, max_replica }, + resourceRequest: { cpu_request, cpu_limit, memory_request, min_replica, max_replica }, }) => { const items = [ { title: "CPU Request", description: cpu_request, }, + ...(cpu_limit !== undefined && cpu_limit !== "0" && cpu_limit !== "") ? [ + { + title: "CPU Limit", + description: cpu_limit, + } + ] : [], { title: "Memory Request", description: memory_request, diff --git a/ui/src/router/components/form/components/CPULimitsFormGroup.js b/ui/src/router/components/form/components/CPULimitsFormGroup.js new file mode 100644 index 000000000..4c10d0a63 --- /dev/null +++ b/ui/src/router/components/form/components/CPULimitsFormGroup.js @@ -0,0 +1,44 @@ +import React, { Fragment } from "react"; +import { FormLabelWithToolTip } from "@caraml-dev/ui-lib"; +import { EuiDescribedFormGroup, EuiFieldText, EuiFormRow } from "@elastic/eui"; + + +export const CPULimitsFormGroup = ({ + resourcesConfig, + onChange, + errors, +}) => { + return ( + CPU Limit

} + description={ + + Use this field to override the platform-level default CPU limit. + + } + fullWidth + > + + } + isInvalid={!!errors} + error={errors} + fullWidth + > + + +
+ ) +} \ No newline at end of file diff --git a/ui/src/router/components/form/components/ResourcesPanel.js b/ui/src/router/components/form/components/ResourcesPanel.js index 9442646e3..e2b1f66cc 100644 --- a/ui/src/router/components/form/components/ResourcesPanel.js +++ b/ui/src/router/components/form/components/ResourcesPanel.js @@ -8,9 +8,11 @@ import { EuiForm, EuiFormRow, EuiSpacer, + EuiAccordion, } from "@elastic/eui"; import { FormLabelWithToolTip } from "../../../../components/form/label_with_tooltip/FormLabelWithToolTip"; import { useOnChangeHandler } from "../../../../components/form/hooks/useOnChangeHandler"; +import { CPULimitsFormGroup } from "./CPULimitsFormGroup"; export const ResourcesPanel = ({ resourcesConfig, @@ -46,6 +48,7 @@ export const ResourcesPanel = ({ onChange={(e) => onChange("cpu_request")(e.target.value)} isInvalid={!!errors.cpu_request} name="cpu" + fullWidth /> @@ -67,6 +70,7 @@ export const ResourcesPanel = ({ onChange={(e) => onChange("memory_request")(e.target.value)} isInvalid={!!errors.memory_request} name="memory" + fullWidth /> @@ -102,6 +106,17 @@ export const ResourcesPanel = ({ aria-label="autoscaling" /> + + + + onChange("cpu_limit")(e.target.value)} + errors={errors.cpu_limit} + /> + ); diff --git a/ui/src/router/components/form/validation/schema.js b/ui/src/router/components/form/validation/schema.js index a7cfbdc8e..13f22294a 100644 --- a/ui/src/router/components/form/validation/schema.js +++ b/ui/src/router/components/form/validation/schema.js @@ -197,6 +197,12 @@ const resourceRequestSchema = (maxAllowedReplica) => cpuRequestRegex, 'Valid CPU value is required, e.g "2" or "500m"' ), + cpu_limit: yup + .string() + .matches( + cpuRequestRegex, + { message: 'Valid CPU value is required, e.g "2" or "500m"', excludeEmptyString: true } + ), memory_request: yup .string() .matches(memRequestRegex, "Valid RAM value is required, e.g. 512Mi"), diff --git a/ui/src/services/ensembler/DockerEnsembler.js b/ui/src/services/ensembler/DockerEnsembler.js index 87a88dd47..07971a719 100644 --- a/ui/src/services/ensembler/DockerEnsembler.js +++ b/ui/src/services/ensembler/DockerEnsembler.js @@ -23,6 +23,7 @@ export class DockerEnsembler extends Ensembler { port: 8080, resource_request: { cpu_request: "500m", + cpu_limit: "", memory_request: "512Mi", min_replica: 0, max_replica: 2, diff --git a/ui/src/services/ensembler/PyFuncEnsembler.js b/ui/src/services/ensembler/PyFuncEnsembler.js index 9799fcc2c..79ab3cf58 100644 --- a/ui/src/services/ensembler/PyFuncEnsembler.js +++ b/ui/src/services/ensembler/PyFuncEnsembler.js @@ -24,6 +24,7 @@ export class PyFuncEnsembler extends Ensembler { project_id: project_id, resource_request: { cpu_request: "500m", + cpu_limit: "", memory_request: "512Mi", min_replica: 0, max_replica: 2, diff --git a/ui/src/services/router/TuringRouter.js b/ui/src/services/router/TuringRouter.js index bbd364841..ef1aea7bb 100644 --- a/ui/src/services/router/TuringRouter.js +++ b/ui/src/services/router/TuringRouter.js @@ -22,6 +22,7 @@ export class TuringRouter { rules: [], resource_request: { cpu_request: "500m", + cpu_limit: "", memory_request: "512Mi", min_replica: 0, max_replica: 2, @@ -40,6 +41,7 @@ export class TuringRouter { port: 8080, resource_request: { cpu_request: "500m", + cpu_limit: "", memory_request: "512Mi", min_replica: 0, max_replica: 2, @@ -117,11 +119,19 @@ export class TuringRouter { if (!obj.config.default_traffic_rule) { delete obj.config["default_traffic_rule"]; } + // Router CPU limit + if (obj.config.resource_request?.cpu_limit === "") { + delete obj.config.resource_request.cpu_limit; + } // Enricher if (obj.config.enricher && obj.config.enricher.type === "nop") { delete obj.config["enricher"]; } + // Enricher CPU limit + if (obj.config.enricher?.resource_request?.cpu_limit === "") { + delete obj.config.enricher.resource_request.cpu_limit; + } // Ensembler if (obj.config.ensembler.type === "nop") { @@ -143,6 +153,10 @@ export class TuringRouter { // Delete the docker config delete obj.config["ensembler"].docker_config; } + // Docker/Pyfunc ensembler CPU limit + if (obj.config.ensembler.resource_request?.cpu_limit === "") { + delete obj.config.ensembler.resource_request.cpu_limit; + } } // Outcome Logging