From 236d5d75196ad9c37cffd374710a4121d0c3970c Mon Sep 17 00:00:00 2001 From: dciangot Date: Sat, 16 Dec 2023 22:09:09 +0100 Subject: [PATCH] Fix #2380 : introduce computeclass resources field Enabling admins to create computeclass with GPU and accelarators in general. Signed-off-by: dciangot --- .../client/computeclass/computeclass_test.go | 39 +++ integration/run/run_test.go | 11 +- pkg/apis/api.acorn.io/v1/computeclass.go | 10 +- .../v1/computeclasses.go | 19 +- .../appdefinition/computeclass_test.go | 4 + .../generic-resources/existing.yaml | 25 ++ .../generic-resources/expected.golden | 272 ++++++++++++++++++ .../computeclass/generic-resources/input.yaml | 71 +++++ .../scheduling/computeclass_test.go | 4 + pkg/controller/scheduling/scheduling.go | 16 ++ .../generic-resources/existing.yaml | 25 ++ .../generic-resources/expected.golden | 89 ++++++ .../computeclass/generic-resources/input.yaml | 38 +++ 13 files changed, 609 insertions(+), 14 deletions(-) create mode 100644 pkg/controller/appdefinition/testdata/computeclass/generic-resources/existing.yaml create mode 100644 pkg/controller/appdefinition/testdata/computeclass/generic-resources/expected.golden create mode 100644 pkg/controller/appdefinition/testdata/computeclass/generic-resources/input.yaml create mode 100644 pkg/controller/scheduling/testdata/computeclass/generic-resources/existing.yaml create mode 100644 pkg/controller/scheduling/testdata/computeclass/generic-resources/expected.golden create mode 100644 pkg/controller/scheduling/testdata/computeclass/generic-resources/input.yaml diff --git a/integration/client/computeclass/computeclass_test.go b/integration/client/computeclass/computeclass_test.go index 18dd382307..15cb953aed 100644 --- a/integration/client/computeclass/computeclass_test.go +++ b/integration/client/computeclass/computeclass_test.go @@ -9,7 +9,9 @@ import ( adminv1 "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1" "github.com/acorn-io/runtime/pkg/client" kclient "github.com/acorn-io/runtime/pkg/k8sclient" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -28,6 +30,7 @@ func TestCreatingComputeClasses(t *testing.T) { checks := []struct { name string memory adminv1.ComputeClassMemory + resources corev1.ResourceRequirements cpuScaler float64 priorityClassName string runtimeClassName string @@ -40,6 +43,21 @@ func TestCreatingComputeClasses(t *testing.T) { }, fail: false, }, + { + name: "valid-custom-resources", + memory: adminv1.ComputeClassMemory{ + Max: "512Mi", + }, + resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + "mygpu/nvidia": resource.MustParse("1"), + }, + Requests: corev1.ResourceList{ + "mygpu/nvidia": resource.MustParse("1"), + }, + }, + fail: false, + }, { name: "valid-only-min", memory: adminv1.ComputeClassMemory{ @@ -144,6 +162,26 @@ func TestCreatingComputeClasses(t *testing.T) { }, fail: true, }, + // // Raise error to avoid conflicts with + // // the "first class" fields for memory and cpu scaling + // { + // name: "invalid-custom-resources-limits", + // resources: corev1.ResourceRequirements{ + // Limits: corev1.ResourceList{ + // "cpu": resource.MustParse("1"), + // }, + // }, + // fail: true, + // }, + // { + // name: "invalid-custom-resources-requests", + // resources: corev1.ResourceRequirements{ + // Requests: corev1.ResourceList{ + // "memory": resource.MustParse("1"), + // }, + // }, + // fail: true, + // }, } for _, tt := range checks { @@ -156,6 +194,7 @@ func TestCreatingComputeClasses(t *testing.T) { }, CPUScaler: tt.cpuScaler, Memory: tt.memory, + Resources: &tt.resources, PriorityClassName: tt.priorityClassName, RuntimeClassName: tt.runtimeClassName, } diff --git a/integration/run/run_test.go b/integration/run/run_test.go index 5e8e962e7a..75f4741430 100644 --- a/integration/run/run_test.go +++ b/integration/run/run_test.go @@ -966,15 +966,24 @@ func TestUsingComputeClasses(t *testing.T) { Min: "512Mi", Max: "1Gi", }, + Resources: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + //"mygpu/nvidia": *resource.NewQuantity(1, resource.DecimalSI), + }, Requests: corev1.ResourceList{ + "mygpu/nvidia": resource.MustParse("1"), + }}, SupportedRegions: []string{apiv1.LocalRegion}, }, expected: map[string]v1.Scheduling{"simple": { Requirements: corev1.ResourceRequirements{ Limits: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("1Gi")}, + corev1.ResourceMemory: resource.MustParse("1Gi"), + //"mygpu/nvidia": *resource.NewQuantity(1, resource.DecimalSI) + }, Requests: corev1.ResourceList{ corev1.ResourceMemory: resource.MustParse("1Gi"), corev1.ResourceCPU: resource.MustParse("250m"), + "mygpu/nvidia": resource.MustParse("1"), }, }, Tolerations: []corev1.Toleration{ diff --git a/pkg/apis/api.acorn.io/v1/computeclass.go b/pkg/apis/api.acorn.io/v1/computeclass.go index 8c9affc8f8..aef495bc1e 100644 --- a/pkg/apis/api.acorn.io/v1/computeclass.go +++ b/pkg/apis/api.acorn.io/v1/computeclass.go @@ -2,6 +2,7 @@ package v1 import ( v1 "github.com/acorn-io/runtime/pkg/apis/internal.admin.acorn.io/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -11,10 +12,11 @@ type ComputeClass struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` - Memory v1.ComputeClassMemory `json:"memory,omitempty"` - Description string `json:"description,omitempty"` - Default bool `json:"default"` - SupportedRegions []string `json:"supportedRegions,omitempty"` + Memory v1.ComputeClassMemory `json:"memory,omitempty"` + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` + Description string `json:"description,omitempty"` + Default bool `json:"default"` + SupportedRegions []string `json:"supportedRegions,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/internal.admin.acorn.io/v1/computeclasses.go b/pkg/apis/internal.admin.acorn.io/v1/computeclasses.go index ec40790801..5dc184053c 100644 --- a/pkg/apis/internal.admin.acorn.io/v1/computeclasses.go +++ b/pkg/apis/internal.admin.acorn.io/v1/computeclasses.go @@ -44,15 +44,16 @@ type ClusterComputeClassInstanceList struct { type ProjectComputeClassInstance struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` - Description string `json:"description,omitempty"` - CPUScaler float64 `json:"cpuScaler,omitempty"` - Default bool `json:"default"` - Affinity *corev1.Affinity `json:"affinity,omitempty"` - Tolerations []corev1.Toleration `json:"tolerations,omitempty"` - Memory ComputeClassMemory `json:"memory,omitempty"` - SupportedRegions []string `json:"supportedRegions,omitempty"` - PriorityClassName string `json:"priorityClassName,omitempty"` - RuntimeClassName string `json:"runtimeClassName,omitempty"` + Description string `json:"description,omitempty"` + CPUScaler float64 `json:"cpuScaler,omitempty"` + Default bool `json:"default"` + Affinity *corev1.Affinity `json:"affinity,omitempty"` + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + Memory ComputeClassMemory `json:"memory,omitempty"` + SupportedRegions []string `json:"supportedRegions,omitempty"` + PriorityClassName string `json:"priorityClassName,omitempty"` + RuntimeClassName string `json:"runtimeClassName,omitempty"` + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/controller/appdefinition/computeclass_test.go b/pkg/controller/appdefinition/computeclass_test.go index 490e56bc77..003be6c98d 100644 --- a/pkg/controller/appdefinition/computeclass_test.go +++ b/pkg/controller/appdefinition/computeclass_test.go @@ -38,3 +38,7 @@ func TestAllSetComputeClass(t *testing.T) { func TestAllSetOverwriteComputeClass(t *testing.T) { tester.DefaultTest(t, scheme.Scheme, "testdata/computeclass/all-set-overwrite-computeclass", DeploySpec) } + +func TestGenericResourcesComputeClass(t *testing.T) { + tester.DefaultTest(t, scheme.Scheme, "testdata/computeclass/generic-resources", DeploySpec) +} diff --git a/pkg/controller/appdefinition/testdata/computeclass/generic-resources/existing.yaml b/pkg/controller/appdefinition/testdata/computeclass/generic-resources/existing.yaml new file mode 100644 index 0000000000..fdb506c92d --- /dev/null +++ b/pkg/controller/appdefinition/testdata/computeclass/generic-resources/existing.yaml @@ -0,0 +1,25 @@ +--- +kind: ClusterComputeClassInstance +apiVersion: internal.admin.acorn.io/v1 +metadata: + name: sample-compute-class +description: Simple description for a simple ComputeClass +cpuScaler: 0.25 +memory: + min: 1Mi + max: 2Mi + default: 1Mi +resources: + limits: + gpu-vendor.example/example-limit: 1 + requests: + gpu-vendor.example/example-request: 1 +affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: foo + operator: In + values: + - bar \ No newline at end of file diff --git a/pkg/controller/appdefinition/testdata/computeclass/generic-resources/expected.golden b/pkg/controller/appdefinition/testdata/computeclass/generic-resources/expected.golden new file mode 100644 index 0000000000..c8439e019c --- /dev/null +++ b/pkg/controller/appdefinition/testdata/computeclass/generic-resources/expected.golden @@ -0,0 +1,272 @@ +`apiVersion: v1 +data: + .dockerconfigjson: eyJhdXRocyI6eyJpbmRleC5kb2NrZXIuaW8iOnsiYXV0aCI6Ik9nPT0ifX19 +kind: Secret +metadata: + creationTimestamp: null + labels: + acorn.io/managed: "true" + acorn.io/pull-secret: "true" + name: oneimage-pull-1234567890ab + namespace: app-created-namespace +type: kubernetes.io/dockerconfigjson + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + acorn.io/config-hash: "" + creationTimestamp: null + labels: + acorn.io/app-name: app-name + acorn.io/app-namespace: app-namespace + acorn.io/app-public-name: app-name + acorn.io/container-name: oneimage + acorn.io/managed: "true" + acorn.io/project-name: app-namespace + name: oneimage + namespace: app-created-namespace + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + acorn.io/config-hash: "" + creationTimestamp: null + labels: + acorn.io/app-name: app-name + acorn.io/app-namespace: app-namespace + acorn.io/app-public-name: app-name + acorn.io/container-name: oneimage + acorn.io/managed: "true" + acorn.io/project-name: app-namespace + name: oneimage + namespace: app-created-namespace +spec: + selector: + matchLabels: + acorn.io/app-name: app-name + acorn.io/app-namespace: app-namespace + acorn.io/container-name: oneimage + acorn.io/managed: "true" + strategy: {} + template: + metadata: + annotations: + acorn.io/container-spec: '{"build":{"context":".","dockerfile":"Dockerfile"},"image":"image-name","metrics":{},"ports":[{"port":80,"protocol":"http","targetPort":81}],"probes":null,"sidecars":{"left":{"image":"foo","metrics":{},"ports":[{"port":90,"protocol":"tcp","targetPort":91}],"probes":null}}}' + karpenter.sh/do-not-evict: "true" + creationTimestamp: null + labels: + acorn.io/app-name: app-name + acorn.io/app-namespace: app-namespace + acorn.io/app-public-name: app-name + acorn.io/container-name: oneimage + acorn.io/managed: "true" + acorn.io/project-name: app-namespace + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: foo + operator: In + values: + - bar + containers: + - image: image-name + name: oneimage + ports: + - containerPort: 81 + protocol: TCP + readinessProbe: + tcpSocket: + port: 81 + resources: + limits: + memory: 1Mi + requests: + cpu: 1m + memory: 1Mi + - image: foo + name: left + ports: + - containerPort: 91 + protocol: TCP + readinessProbe: + tcpSocket: + port: 91 + resources: + limits: + memory: 1Mi + requests: + cpu: 1m + memory: 1Mi + enableServiceLinks: false + hostname: oneimage + imagePullSecrets: + - name: oneimage-pull-1234567890ab + serviceAccountName: oneimage + terminationGracePeriodSeconds: 10 +status: {} + +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + annotations: + acorn.io/config-hash: "" + creationTimestamp: null + labels: + acorn.io/app-name: app-name + acorn.io/app-namespace: app-namespace + acorn.io/app-public-name: app-name + acorn.io/container-name: oneimage + acorn.io/managed: "true" + acorn.io/project-name: app-namespace + name: oneimage + namespace: app-created-namespace +spec: + maxUnavailable: 25% + selector: + matchLabels: + acorn.io/app-name: app-name + acorn.io/app-namespace: app-namespace + acorn.io/container-name: oneimage + acorn.io/managed: "true" +status: + currentHealthy: 0 + desiredHealthy: 0 + disruptionsAllowed: 0 + expectedPods: 0 + +--- +apiVersion: internal.acorn.io/v1 +kind: ServiceInstance +metadata: + annotations: + acorn.io/app-generation: "0" + acorn.io/config-hash: "" + creationTimestamp: null + labels: + acorn.io/app-name: app-name + acorn.io/app-namespace: app-namespace + acorn.io/container-name: oneimage + acorn.io/managed: "true" + acorn.io/public-name: app-name.oneimage + name: oneimage + namespace: app-created-namespace +spec: + appName: app-name + appNamespace: app-namespace + container: oneimage + default: true + labels: + acorn.io/app-name: app-name + acorn.io/app-namespace: app-namespace + acorn.io/container-name: oneimage + acorn.io/managed: "true" + ports: + - port: 80 + protocol: http + targetPort: 81 + - port: 90 + protocol: tcp + targetPort: 91 +status: {} + +--- +apiVersion: internal.acorn.io/v1 +kind: AppInstance +metadata: + creationTimestamp: null + name: app-name + namespace: app-namespace + uid: 1234567890abcdef +spec: + computeClass: + "": sample-compute-class + image: test +status: + appImage: + buildContext: {} + id: test + imageData: {} + vcs: {} + appSpec: + containers: + oneimage: + build: + context: . + dockerfile: Dockerfile + image: image-name + metrics: {} + ports: + - port: 80 + protocol: http + targetPort: 81 + probes: null + sidecars: + left: + image: foo + metrics: {} + ports: + - port: 90 + protocol: tcp + targetPort: 91 + probes: null + appStatus: {} + columns: {} + conditions: + reason: Success + status: "True" + success: true + type: defaults + reason: Success + status: "True" + success: true + type: scheduling + reason: Success + status: "True" + success: true + type: defined + defaults: + memory: + "": 0 + left: 1048576 + oneimage: 1048576 + namespace: app-created-namespace + observedGeneration: 1 + scheduling: + left: + requirements: + limits: + memory: 1Mi + requests: + cpu: 1m + memory: 1Mi + oneimage: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: foo + operator: In + values: + - bar + requirements: + limits: + memory: 1Mi + requests: + cpu: 1m + memory: 1Mi + staged: + appImage: + buildContext: {} + imageData: {} + vcs: {} + summary: {} +` diff --git a/pkg/controller/appdefinition/testdata/computeclass/generic-resources/input.yaml b/pkg/controller/appdefinition/testdata/computeclass/generic-resources/input.yaml new file mode 100644 index 0000000000..44a71471a4 --- /dev/null +++ b/pkg/controller/appdefinition/testdata/computeclass/generic-resources/input.yaml @@ -0,0 +1,71 @@ +kind: AppInstance +apiVersion: internal.acorn.io/v1 +metadata: + name: app-name + namespace: app-namespace + uid: 1234567890abcdef +spec: + image: test + computeClass: + "": sample-compute-class +status: + observedGeneration: 1 + scheduling: + oneimage: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: foo + operator: In + values: + - bar + requirements: + limits: + memory: 1Mi + requests: + cpu: 1m + memory: 1Mi + left: + requirements: + limits: + memory: 1Mi + requests: + cpu: 1m + memory: 1Mi + namespace: app-created-namespace + defaults: + memory: + "": 0 + left: 1048576 # 1Mi + oneimage: 1048576 # 1Mi + appImage: + id: test + appSpec: + containers: + oneimage: + sidecars: + left: + image: "foo" + ports: + - port: 90 + targetPort: 91 + protocol: tcp + ports: + - port: 80 + targetPort: 81 + protocol: http + image: "image-name" + build: + dockerfile: "Dockerfile" + context: "." + conditions: + - type: defaults + reason: Success + status: "True" + success: true + - type: scheduling + reason: Success + status: "True" + success: true diff --git a/pkg/controller/scheduling/computeclass_test.go b/pkg/controller/scheduling/computeclass_test.go index c3f8c3fcf5..6132eb2370 100644 --- a/pkg/controller/scheduling/computeclass_test.go +++ b/pkg/controller/scheduling/computeclass_test.go @@ -57,6 +57,10 @@ func TestPriorityClass(t *testing.T) { tester.DefaultTest(t, scheme.Scheme, "testdata/computeclass/priority-class", Calculate) } +func TestGenericResourcesComputeClass(t *testing.T) { + tester.DefaultTest(t, scheme.Scheme, "testdata/computeclass/generic-resources", Calculate) +} + func TestTwoCCCDefaultsShouldError(t *testing.T) { harness, input, err := tester.FromDir(scheme.Scheme, "testdata/computeclass/two-ccc-defaults-should-error") if err != nil { diff --git a/pkg/controller/scheduling/scheduling.go b/pkg/controller/scheduling/scheduling.go index cded2eabfa..6a29ef56c4 100644 --- a/pkg/controller/scheduling/scheduling.go +++ b/pkg/controller/scheduling/scheduling.go @@ -70,6 +70,12 @@ func addScheduling(req router.Request, appInstance *v1.AppInstance, workloads ma } for sidecarName, sidecarContainer := range container.Sidecars { + // disable extra resource indication (GPU et al. for sidecars) + if computeClass != nil { + if computeClass.Resources != nil { + computeClass.Resources = &corev1.ResourceRequirements{Limits: corev1.ResourceList{}, Requests: corev1.ResourceList{}} + } + } sidecarRequirements, err := ResourceRequirements(req, appInstance, sidecarName, sidecarContainer, computeClass) if err != nil { return err @@ -155,6 +161,16 @@ func ResourceRequirements(req router.Request, app *v1.AppInstance, containerName } requirements := &corev1.ResourceRequirements{Limits: corev1.ResourceList{}, Requests: corev1.ResourceList{}} + if computeClass != nil { + if computeClass.Resources != nil { + if computeClass.Resources.Requests != nil { + requirements.Requests = computeClass.Resources.Requests + } + if computeClass.Resources.Limits != nil { + requirements.Limits = computeClass.Resources.Limits + } + } + } var memDefault *int64 if val, ok := app.Status.Defaults.Memory[containerName]; ok && val != nil { diff --git a/pkg/controller/scheduling/testdata/computeclass/generic-resources/existing.yaml b/pkg/controller/scheduling/testdata/computeclass/generic-resources/existing.yaml new file mode 100644 index 0000000000..fdb506c92d --- /dev/null +++ b/pkg/controller/scheduling/testdata/computeclass/generic-resources/existing.yaml @@ -0,0 +1,25 @@ +--- +kind: ClusterComputeClassInstance +apiVersion: internal.admin.acorn.io/v1 +metadata: + name: sample-compute-class +description: Simple description for a simple ComputeClass +cpuScaler: 0.25 +memory: + min: 1Mi + max: 2Mi + default: 1Mi +resources: + limits: + gpu-vendor.example/example-limit: 1 + requests: + gpu-vendor.example/example-request: 1 +affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: foo + operator: In + values: + - bar \ No newline at end of file diff --git a/pkg/controller/scheduling/testdata/computeclass/generic-resources/expected.golden b/pkg/controller/scheduling/testdata/computeclass/generic-resources/expected.golden new file mode 100644 index 0000000000..f1f847b9e4 --- /dev/null +++ b/pkg/controller/scheduling/testdata/computeclass/generic-resources/expected.golden @@ -0,0 +1,89 @@ +`apiVersion: internal.acorn.io/v1 +kind: AppInstance +metadata: + creationTimestamp: null + name: app-name + namespace: app-namespace + uid: 1234567890abcdef +spec: + computeClass: + "": sample-compute-class + image: test +status: + appImage: + buildContext: {} + id: test + imageData: {} + vcs: {} + appSpec: + containers: + oneimage: + build: + context: . + dockerfile: Dockerfile + image: image-name + metrics: {} + ports: + - port: 80 + protocol: http + targetPort: 81 + probes: null + sidecars: + left: + image: foo + metrics: {} + ports: + - port: 90 + protocol: tcp + targetPort: 91 + probes: null + appStatus: {} + columns: {} + conditions: + reason: Success + status: "True" + success: true + type: scheduling + defaults: + memory: + "": 0 + left: 1048576 + oneimage: 1048576 + namespace: app-created-namespace + observedGeneration: 1 + scheduling: + left: + requirements: + limits: + memory: 1Mi + requests: + cpu: 1m + memory: 1Mi + oneimage: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: foo + operator: In + values: + - bar + requirements: + limits: + gpu-vendor.example/example-limit: "1" + memory: 1Mi + requests: + cpu: 1m + gpu-vendor.example/example-request: "1" + memory: 1Mi + tolerations: + - key: taints.acorn.io/workload + operator: Exists + staged: + appImage: + buildContext: {} + imageData: {} + vcs: {} + summary: {} +` diff --git a/pkg/controller/scheduling/testdata/computeclass/generic-resources/input.yaml b/pkg/controller/scheduling/testdata/computeclass/generic-resources/input.yaml new file mode 100644 index 0000000000..7beaac334c --- /dev/null +++ b/pkg/controller/scheduling/testdata/computeclass/generic-resources/input.yaml @@ -0,0 +1,38 @@ +kind: AppInstance +apiVersion: internal.acorn.io/v1 +metadata: + name: app-name + namespace: app-namespace + uid: 1234567890abcdef +spec: + image: test + computeClass: + "": sample-compute-class +status: + observedGeneration: 1 + defaults: + memory: + "": 0 + left: 1048576 # 1Mi + oneimage: 1048576 # 1Mi + namespace: app-created-namespace + appImage: + id: test + appSpec: + containers: + oneimage: + sidecars: + left: + image: "foo" + ports: + - port: 90 + targetPort: 91 + protocol: tcp + ports: + - port: 80 + targetPort: 81 + protocol: http + image: "image-name" + build: + dockerfile: "Dockerfile" + context: "."