diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index fdff02c4..18acebc1 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Go 1.x uses: actions/setup-go@v5 with: - go-version: ^1.19 + go-version: ^1.21 id: go - name: Check out code into the Go module directory diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index bb9593dd..79326f11 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,7 +5,7 @@ jobs: lint: strategy: matrix: - go: [1.18, 1.19] + go: [1.21, 1.22] name: lint runs-on: ubuntu-latest steps: @@ -16,4 +16,4 @@ jobs: with: go-version: ${{ matrix.go }} - name: "Run go vet" - run: "go vet ./pkg/..." \ No newline at end of file + run: "go vet ./pkg/..." diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4f34ac1a..e178aaf3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go: [1.18, 1.19] + go: [1.21, 1.22] cross_compile: [true, false] steps: - uses: actions/checkout@v4 @@ -35,7 +35,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: 1.19 + go-version: ^1.21 - name: Install dyff run: go install github.com/homeport/dyff/cmd/dyff@v1.5.8 - name: Create .coverprofile for each targeted directory by re:running tests diff --git a/docs/user-guide.md b/docs/user-guide.md index be213f5b..4b97baa5 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -211,9 +211,13 @@ The currently supported options are: | kompose.cronjob.schedule | kubernetes cronjob schedule (for example: '1 * * * *') | | kompose.cronjob.concurrency_policy | 'Forbid' / 'Allow' / 'Never' / '' | | kompose.cronjob.backoff_limit | kubernetes cronjob backoff limit (for example: '6') | -| kompose.init.containers.name | kubernetes init container name | -| kompose.init.containers.image | kubernetes init container image | -| kompose.init.containers.command | kubernetes init container commands | +| kompose.init.containers.name | kubernetes init container name | +| kompose.init.containers.image | kubernetes init container image | +| kompose.init.containers.command | kubernetes init container commands | +| kompose.hpa.replicas.min | defines Horizontal Pod Autoscaler minimum number of pod replicas | +| kompose.hpa.replicas.max | defines Horizontal Pod Autoscaler maximum number of pod replicas | +| kompose.hpa.cpu | defines Horizontal Pod Autoscaler cpu utilization trigger | +| kompose.hpa.memory | defines Horizontal Pod Autoscaler memory utilization trigger | **Note**: `kompose.service.type` label should be defined with `ports` only (except for headless service), otherwise `kompose` will fail. @@ -512,6 +516,55 @@ services: kompose.init.containers.image: perl ``` + +- `kompose.hpa.replicas.min` defines the floor for the number of replicas that the HPA can scale down to during a scaling event. Default value is set to 1. This means that, regardless of the load on the system, the HPA will always maintain at least one replica. More info: [HPA Min Replicas](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#autoscaling-on-multiple-metrics-and-custom-metrics). + +For example: + +```yaml +services: + pgadmin: + image: postgres + labels: + kompose.hpa.replicas.min: 1 +``` + +- `kompose.hpa.replicas.max` defines the upper limit for the number of replicas that the HPA can create during a scaling event. Default value is set to 3. This default value serves as a safeguard, providing a conservative starting point for your HPA configuration. More info: [HPA Max Replicas](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#autoscaling-on-multiple-metrics-and-custom-metrics). + +For example: + +```yaml +services: + pgadmin: + image: postgres + labels: + kompose.hpa.replicas.max: 10 +``` + +- `kompose.hpa.cpu` defines % cpu utilization that triggers to scale the number of pods. It is represented as a percentage of a resource. Default value is set to 50. This default value serves as a safeguard, providing a conservative starting point for your HPA configuration. More info: [HPA CPU Utilization](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#autoscaling-on-multiple-metrics-and-custom-metrics). + +For example: + +```yaml +services: + pgadmin: + image: postgres + labels: + kompose.hpa.cpu: 50 +``` + +- `kompose.hpa.memory` defines memory utilization that triggers to scale the number of pods. It is represented as a percentage of a resource. Default value is set to 70. This default value serves as a safeguard, providing a conservative starting point for your HPA configuration. More info: [HPA Memory Utilization](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/#autoscaling-on-multiple-metrics-and-custom-metrics). + +For example: + +```yaml +services: + pgadmin: + image: postgres + labels: + kompose.hpa.memory: 50 +``` + ## Restart If you want to create normal pods without controller you can use `restart` construct of compose to define that. Follow table below to see what happens on the `restart` value. diff --git a/go.mod b/go.mod index 775aec3e..b93508fe 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,11 @@ module github.com/kubernetes/kompose -go 1.18 +go 1.21 + +toolchain go1.21.8 require ( - github.com/compose-spec/compose-go v1.20.2 + github.com/compose-spec/compose-go/v2 v2.0.0-rc.8 github.com/deckarep/golang-set v1.8.0 github.com/fatih/structs v1.1.0 github.com/fsouza/go-dockerclient v1.9.7 @@ -39,12 +41,13 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.9 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/patternmatcher v0.5.0 // indirect github.com/moby/sys/sequential v0.5.0 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect @@ -61,7 +64,7 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect - golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect + golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect golang.org/x/mod v0.16.0 // indirect golang.org/x/net v0.22.0 // indirect golang.org/x/sync v0.6.0 // indirect diff --git a/go.sum b/go.sum index a748849a..31339c06 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,7 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8 h1:V8krnnfGj4pV65YLUm3C0/8bl7V5Nry2Pwvy3ru/wLc= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -44,6 +45,7 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/Microsoft/hcsshim v0.9.10 h1:TxXGNmcbQxBKVWvjvTocNb6jrPyeHlk5EiDhhgHgggs= +github.com/Microsoft/hcsshim v0.9.10/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -54,8 +56,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/compose-spec/compose-go v1.20.2 h1:u/yfZHn4EaHGdidrZycWpxXgFffjYULlTbRfJ51ykjQ= -github.com/compose-spec/compose-go v1.20.2/go.mod h1:+MdqXV4RA7wdFsahh/Kb8U0pAJqkg7mr4PM9tFKU8RM= +github.com/compose-spec/compose-go/v2 v2.0.0-rc.8 h1:b7l+GqFF+2W4M4kLQUDRTGhqmTiRwT3bYd9X7xrxp5Q= +github.com/compose-spec/compose-go/v2 v2.0.0-rc.8/go.mod h1:bEPizBkIojlQ20pi2vNluBa58tevvj0Y18oUSHPyfdc= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/containerd v1.6.26 h1:VVfrE6ZpyisvB1fzoY8Vkiq4sy+i5oF4uk7zu03RaHs= github.com/containerd/containerd v1.6.26/go.mod h1:I4TRdsdoo5MlKob5khDJS2EPT1l1oMNaE2MBm6FrwxM= @@ -89,6 +91,7 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsouza/go-dockerclient v1.9.7 h1:FlIrT71E62zwKgRvCvWGdxRD+a/pIy+miY/n3MXgfuw= @@ -172,8 +175,6 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= -github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -190,15 +191,21 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/patternmatcher v0.5.0 h1:YCZgJOeULcxLw1Q+sVR636pmS7sPEn1Qo2iAN6M7DBo= github.com/moby/patternmatcher v0.5.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= @@ -237,6 +244,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= @@ -269,6 +277,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -308,8 +317,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -439,6 +448,7 @@ golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -601,6 +611,7 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= @@ -639,3 +650,4 @@ sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h6 sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/pkg/kobject/kobject.go b/pkg/kobject/kobject.go index 4217d663..399ceb9e 100644 --- a/pkg/kobject/kobject.go +++ b/pkg/kobject/kobject.go @@ -21,7 +21,7 @@ import ( "strconv" "time" - "github.com/compose-spec/compose-go/types" + "github.com/compose-spec/compose-go/v2/types" deployapi "github.com/openshift/api/apps/v1" "github.com/pkg/errors" "github.com/spf13/cast" @@ -251,7 +251,7 @@ func (s *ServiceConfig) GetConfigMapKeyFromMeta(name string) (string, error) { } config := s.ConfigsMetaData[name] - if config.External.External { + if config.External { return "", errors.Errorf("config %s is external", name) } diff --git a/pkg/loader/compose/compose.go b/pkg/loader/compose/compose.go index ab7c3213..e2439cbd 100644 --- a/pkg/loader/compose/compose.go +++ b/pkg/loader/compose/compose.go @@ -17,6 +17,7 @@ limitations under the License. package compose import ( + "context" "fmt" "os" "reflect" @@ -24,8 +25,8 @@ import ( "strings" "time" - "github.com/compose-spec/compose-go/cli" - "github.com/compose-spec/compose-go/types" + "github.com/compose-spec/compose-go/v2/cli" + "github.com/compose-spec/compose-go/v2/types" "github.com/fatih/structs" "github.com/google/shlex" "github.com/kubernetes/kompose/pkg/kobject" @@ -167,7 +168,7 @@ func (c *Compose) LoadFile(files []string, profiles []string) (kobject.KomposeOb return kobject.KomposeObject{}, errors.Wrap(err, "Unable to create compose options") } - project, err := cli.ProjectFromOptions(projectOptions) + project, err := cli.ProjectFromOptions(context.Background(), projectOptions) if err != nil { return kobject.KomposeObject{}, errors.Wrap(err, "Unable to load files") } @@ -566,7 +567,7 @@ func dockerComposeToKomposeMapping(composeObject *types.Project) (kobject.Kompos parseEnvironment(&composeServiceConfig, &serviceConfig) // Get env_file - serviceConfig.EnvFile = composeServiceConfig.EnvFile + parseEnvFiles(&composeServiceConfig, &serviceConfig) // Parse the ports // v3 uses a new format called "long syntax" starting in 3.2 @@ -695,6 +696,13 @@ func parseEnvironment(composeServiceConfig *types.ServiceConfig, serviceConfig * } } +func parseEnvFiles(composeServiceConfig *types.ServiceConfig, serviceConfig *kobject.ServiceConfig) { + for _, value := range composeServiceConfig.EnvFiles { + serviceConfig.EnvFile = append(serviceConfig.EnvFile, value.Path) + // value.Required is ignored + } +} + func handleCronJobConcurrencyPolicy(policy string) (batchv1.ConcurrencyPolicy, error) { switch policy { case "Allow": diff --git a/pkg/loader/compose/compose_test.go b/pkg/loader/compose/compose_test.go index eb002e11..af9dfa5a 100644 --- a/pkg/loader/compose/compose_test.go +++ b/pkg/loader/compose/compose_test.go @@ -24,7 +24,7 @@ import ( "testing" "time" - "github.com/compose-spec/compose-go/types" + "github.com/compose-spec/compose-go/v2/types" "github.com/google/go-cmp/cmp" "github.com/kubernetes/kompose/pkg/kobject" "github.com/pkg/errors" @@ -429,6 +429,52 @@ func TestLoadEnvVar(t *testing.T) { } } +func TestParseEnvFiles(t *testing.T) { + tests := []struct { + service types.ServiceConfig + want []string + }{ + {service: types.ServiceConfig{ + Name: "baz", + Image: "foo/baz", + EnvFiles: []types.EnvFile{ + { + Path: "", + Required: false, + }, + { + Path: "foo", + Required: false, + }, + { + Path: "bar", + Required: true, + }, + }, + }, + want: []string{"", "foo", "bar"}, + }, + { + service: types.ServiceConfig{ + Name: "baz", + Image: "foo/baz", + EnvFiles: []types.EnvFile{}, + }, + want: []string{}, + }, + } + + for _, tt := range tests { + sc := kobject.ServiceConfig{ + EnvFile: []string{}, + } + parseEnvFiles(&tt.service, &sc) + if !reflect.DeepEqual(sc.EnvFile, tt.want) { + t.Errorf("Expected %q, got %q", tt.want, sc.EnvFile) + } + } +} + // TestUnsupportedKeys test checkUnsupportedKey function with various // docker-compose projects func TestUnsupportedKeys(t *testing.T) { @@ -441,7 +487,7 @@ func TestUnsupportedKeys(t *testing.T) { }, }, Services: types.Services{ - types.ServiceConfig{ + "foo": types.ServiceConfig{ Name: "foo", Image: "foo/bar", Build: &types.BuildConfig{ @@ -453,7 +499,7 @@ func TestUnsupportedKeys(t *testing.T) { "net1": {}, }, }, - types.ServiceConfig{ + "bar": types.ServiceConfig{ Name: "bar", Image: "bar/foo", Build: &types.BuildConfig{ @@ -476,7 +522,7 @@ func TestUnsupportedKeys(t *testing.T) { projectWithDefaultNetwork := &types.Project{ Services: types.Services{ - types.ServiceConfig{ + "foo": types.ServiceConfig{ Networks: map[string]*types.ServiceNetworkConfig{ "default": {}, }, diff --git a/pkg/loader/compose/utils.go b/pkg/loader/compose/utils.go index 2bdccac1..92b4062c 100644 --- a/pkg/loader/compose/utils.go +++ b/pkg/loader/compose/utils.go @@ -93,6 +93,14 @@ const ( LabelInitContainerImage = "kompose.init.containers.image" // LabelInitContainerCommand defines commands LabelInitContainerCommand = "kompose.init.containers.command" + // LabelHpaMinReplicas defines min pod replicas + LabelHpaMinReplicas = "kompose.hpa.replicas.min" + // LabelHpaMaxReplicas defines max pod replicas + LabelHpaMaxReplicas = "kompose.hpa.replicas.max" + // LabelHpaCpu defines scaling decisions based on CPU utilization + LabelHpaCPU = "kompose.hpa.cpu" + // LabelHpaMemory defines scaling decisions based on memory utilization + LabelHpaMemory = "kompose.hpa.memory" ) // load environment variables from compose file diff --git a/pkg/transformer/kubernetes/k8sutils.go b/pkg/transformer/kubernetes/k8sutils.go index 064516e1..42e24c97 100644 --- a/pkg/transformer/kubernetes/k8sutils.go +++ b/pkg/transformer/kubernetes/k8sutils.go @@ -31,7 +31,7 @@ import ( "text/template" "time" - "github.com/compose-spec/compose-go/types" + "github.com/compose-spec/compose-go/v2/types" "github.com/joho/godotenv" "github.com/kubernetes/kompose/pkg/kobject" "github.com/kubernetes/kompose/pkg/loader/compose" @@ -41,6 +41,7 @@ import ( log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" appsv1 "k8s.io/api/apps/v1" + hpa "k8s.io/api/autoscaling/v2beta2" api "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -48,6 +49,29 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +// Default values for Horizontal Pod Autoscaler (HPA) +const ( + DefaultMinReplicas = 1 + DefaultMaxReplicas = 3 + DefaultCPUUtilization = 50 + DefaultMemoryUtilization = 70 +) + +// LabelKeys are the keys for HPA related labels in the service +var LabelKeys = []string{ + compose.LabelHpaCPU, + compose.LabelHpaMemory, + compose.LabelHpaMinReplicas, + compose.LabelHpaMaxReplicas, +} + +type HpaValues struct { + MinReplicas int32 + MaxReplicas int32 + CPUtilization int32 + MemoryUtilization int32 +} + /** * Generate Helm Chart configuration */ @@ -1030,3 +1054,122 @@ func parseContainerCommandsFromStr(line string) []string { } return commands } + +// searchHPAValues is useful to check if labels +// contains any labels related to Horizontal Pod Autoscaler +func searchHPAValues(labels map[string]string) bool { + for _, value := range LabelKeys { + if _, ok := labels[value]; ok { + return true + } + } + return false +} + +// createHPAResources creates a HorizontalPodAutoscaler (HPA) resource +// It sets the number of replicas in the service to 0 because +// the number of replicas will be managed by the HPA +func createHPAResources(name string, service *kobject.ServiceConfig) hpa.HorizontalPodAutoscaler { + valuesHpa := getResourceHpaValues(service) + service.Replicas = 0 + metrics := getHpaMetricSpec(valuesHpa) + scalerSpecs := hpa.HorizontalPodAutoscaler{ + TypeMeta: metav1.TypeMeta{ + Kind: "HorizontalPodAutoscaler", + APIVersion: "autoscaling/v2", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: hpa.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: hpa.CrossVersionObjectReference{ + Kind: "Deployment", + Name: name, + APIVersion: "apps/v1", + }, + MinReplicas: &valuesHpa.MinReplicas, + MaxReplicas: valuesHpa.MaxReplicas, + Metrics: metrics, + }, + } + + return scalerSpecs +} + +// getResourceHpaValues retrieves the min/max replicas and CPU/memory utilization values +// control if maxReplicas is less than minReplicas +func getResourceHpaValues(service *kobject.ServiceConfig) HpaValues { + minReplicas := getHpaValue(service, compose.LabelHpaMinReplicas, DefaultMinReplicas) + maxReplicas := getHpaValue(service, compose.LabelHpaMaxReplicas, DefaultMaxReplicas) + + if maxReplicas < minReplicas { + log.Warnf("maxReplicas %d is less than minReplicas %d. Using minReplicas value %d", maxReplicas, minReplicas, minReplicas) + maxReplicas = minReplicas + } + + cpuUtilization := validatePercentageMetric(service, compose.LabelHpaCPU, DefaultCPUUtilization) + memoryUtilization := validatePercentageMetric(service, compose.LabelHpaMemory, DefaultMemoryUtilization) + + return HpaValues{ + MinReplicas: minReplicas, + MaxReplicas: maxReplicas, + CPUtilization: cpuUtilization, + MemoryUtilization: memoryUtilization, + } +} + +// validatePercentageMetric validates the CPU or memory metrics value +// ensuring that it falls within the acceptable range [1, 100]. +func validatePercentageMetric(service *kobject.ServiceConfig, metricLabel string, defaultValue int32) int32 { + metricValue := getHpaValue(service, metricLabel, defaultValue) + if metricValue > 100 || metricValue < 1 { + log.Warnf("Metric value %d is not within the acceptable range [1, 100]. Using default value %d", metricValue, defaultValue) + return defaultValue + } + return metricValue +} + +// getHpaValue convert the label value to integer +// If the label is not present or the conversion fails +// it returns the provided default value +func getHpaValue(service *kobject.ServiceConfig, label string, defaultValue int32) int32 { + valueFromLabel, err := strconv.Atoi(service.Labels[label]) + if err != nil || valueFromLabel < 0 { + log.Warnf("Error converting label %s. Using default value %d", label, defaultValue) + return defaultValue + } + return int32(valueFromLabel) +} + +// getHpaMetricSpec returns a list of metric specs for the HPA resource +// Target type is hardcoded to hpa.UtilizationMetricType +// Each MetricSpec specifies the type metric CPU/memory and average utilization value +// to trigger scaling +func getHpaMetricSpec(hpaValues HpaValues) []hpa.MetricSpec { + var metrics []hpa.MetricSpec + if hpaValues.CPUtilization > 0 { + metrics = append(metrics, hpa.MetricSpec{ + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: api.ResourceCPU, + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &hpaValues.CPUtilization, + }, + }, + }) + } + if hpaValues.MemoryUtilization > 0 { + metrics = append(metrics, hpa.MetricSpec{ + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: api.ResourceMemory, + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &hpaValues.MemoryUtilization, + }, + }, + }) + } + return metrics +} diff --git a/pkg/transformer/kubernetes/k8sutils_test.go b/pkg/transformer/kubernetes/k8sutils_test.go index d70d06d9..a7a0f11e 100644 --- a/pkg/transformer/kubernetes/k8sutils_test.go +++ b/pkg/transformer/kubernetes/k8sutils_test.go @@ -29,8 +29,10 @@ import ( "github.com/kubernetes/kompose/pkg/testutils" "github.com/pkg/errors" appsv1 "k8s.io/api/apps/v1" + hpa "k8s.io/api/autoscaling/v2beta2" api "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) /* @@ -945,3 +947,1278 @@ func Test_fillInitContainers(t *testing.T) { }) } } + +func Test_getHpaValue(t *testing.T) { + type args struct { + service *kobject.ServiceConfig + label string + defaultValue int32 + } + tests := []struct { + name string + args args + want int32 + }{ + // LabelHpaMinReplicas + { + name: "LabelHpaMinReplicas with 1 value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaMinReplicas, + defaultValue: 1, + }, + want: 1, + }, + { + name: "LabelHpaMinReplicas with 0 value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "0", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaMinReplicas, + defaultValue: 1, + }, + want: 0, + }, + { + name: "LabelHpaMinReplicas with error value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "cannot transform", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaMinReplicas, + defaultValue: 1, + }, + want: 1, + }, + // LabelHpaMaxReplicas + { + name: "LabelHpaMaxReplicas with 10 value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaMaxReplicas, + defaultValue: 30, + }, + want: 10, + }, + { + name: "LabelHpaMaxReplicas with 0 value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "0", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaMaxReplicas, + defaultValue: DefaultMaxReplicas, + }, + want: 0, + }, + { + name: "LabelHpaMaxReplicas with error value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "cannot transform", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaMaxReplicas, + defaultValue: DefaultMaxReplicas, + }, + want: DefaultMaxReplicas, + }, + // LabelHpaCPU + { + name: "LabelHpaCPU with 50 value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaCPU, + defaultValue: 30, + }, + want: 50, + }, + { + name: "LabelHpaCPU with 0 value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "0", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaCPU, + defaultValue: DefaultCPUUtilization, + }, + want: 0, + }, + { + name: "LabelHpaCPU with error value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "cannot transform", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaCPU, + defaultValue: DefaultCPUUtilization, + }, + want: DefaultCPUUtilization, + }, + // LabelHpaMemory + { + name: "LabelHpaMemory with 70 value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + label: compose.LabelHpaMemory, + defaultValue: 30, + }, + want: 70, + }, + { + name: "LabelHpaMemory with 0 value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "0", + }, + }, + label: compose.LabelHpaMemory, + defaultValue: DefaultMemoryUtilization, + }, + want: 0, + }, + { + name: "LabelHpaMemory with error value", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "cannot transform", + }, + }, + label: compose.LabelHpaMemory, + defaultValue: DefaultMemoryUtilization, + }, + want: DefaultMemoryUtilization, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getHpaValue(tt.args.service, tt.args.label, tt.args.defaultValue); got != tt.want { + t.Errorf("getHpaValue() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getResourceHpaValues(t *testing.T) { + type args struct { + service *kobject.ServiceConfig + } + tests := []struct { + name string + args args + want HpaValues + }{ + { + name: "check default values", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "3", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + }, + want: HpaValues{ + MinReplicas: 1, + MaxReplicas: 3, + CPUtilization: 50, + MemoryUtilization: 70, + }, + }, + { + name: "check if max replicas are less than min replicas, and max replicas set to min replicas", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "5", + compose.LabelHpaMaxReplicas: "3", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + }, + want: HpaValues{ + MinReplicas: 5, + MaxReplicas: 5, // same as min replicas + CPUtilization: 50, + MemoryUtilization: 70, + }, + }, + { + name: "with error values and use default values from LabelHpaMinReplicas", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "cannot transform", + compose.LabelHpaMaxReplicas: "3", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: 3, + CPUtilization: 50, + MemoryUtilization: 70, + }, + }, + { + name: "LabelHpaMaxReplicas is minor to LabelHpaMinReplicas", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "6", + compose.LabelHpaMaxReplicas: "5", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + }, + want: HpaValues{ + MinReplicas: 6, + MaxReplicas: 6, // set min replicas number + CPUtilization: 50, + MemoryUtilization: 70, + }, + }, + { + name: "error label and LabelHpaMaxReplicas is minor to LabelHpaMinReplicas", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "6", + compose.LabelHpaMaxReplicas: "5", + compose.LabelHpaCPU: "cannot transform", + compose.LabelHpaMemory: "70", + }, + }, + }, + want: HpaValues{ + MinReplicas: 6, + MaxReplicas: 6, // same as min replicas number + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: 70, + }, + }, + { + name: "error label and LabelHpaMaxReplicas is minor to LabelHpaMinReplicas and cannot transform hpa mmemor utilization", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "6", + compose.LabelHpaMaxReplicas: "5", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "cannot transform", + }, + }, + }, + want: HpaValues{ + MinReplicas: 6, + MaxReplicas: 6, + CPUtilization: 50, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "all error label, set all default values", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "cannot transform", + compose.LabelHpaMaxReplicas: "cannot transform", + compose.LabelHpaCPU: "cannot transform", + compose.LabelHpaMemory: "cannot transform", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "error label without some labels, missing labels set to default", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "cannot transform", + compose.LabelHpaMaxReplicas: "cannot transform", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "without labels, should return default values", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{}, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "only min replicas label is provided", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "3", + }, + }, + }, + want: HpaValues{ + MinReplicas: 3, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "only max replicas label is provided", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMaxReplicas: "5", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: 5, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "check default values when all labels contain invalid values", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "cannot transform", + compose.LabelHpaMaxReplicas: "cannot transform", + compose.LabelHpaCPU: "cannot transform", + compose.LabelHpaMemory: "cannot transform", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "only cpu utilization label is provided", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaCPU: "80", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: 80, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "only memory utilization label is provided", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMemory: "90", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: 90, + }, + }, + { + name: "only cpu and memory utilization labels are provided", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaCPU: "80", + compose.LabelHpaMemory: "90", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: 80, + MemoryUtilization: 90, + }, + }, + { + name: "check default values when labels are empty strings", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "", + compose.LabelHpaMaxReplicas: "", + compose.LabelHpaCPU: "", + compose.LabelHpaMemory: "", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "check default values when labels contain invalid characters", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "abc", + compose.LabelHpaMaxReplicas: "xyz", + compose.LabelHpaCPU: "-100", + compose.LabelHpaMemory: "invalid", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "check default values when labels are set to zero", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "0", + compose.LabelHpaMaxReplicas: "0", + compose.LabelHpaCPU: "0", + compose.LabelHpaMemory: "0", + }, + }, + }, + want: HpaValues{ + MinReplicas: 0, + MaxReplicas: 0, + CPUtilization: 50, + MemoryUtilization: 70, + }, + }, + { + name: "check default values when all labels are negative", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "-5", + compose.LabelHpaMaxReplicas: "-10", + compose.LabelHpaCPU: "-20", + compose.LabelHpaMemory: "-30", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + { + name: "check default values when labels cpu and memory are over", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "-2", + compose.LabelHpaMaxReplicas: "-2", + compose.LabelHpaCPU: "120", + compose.LabelHpaMemory: "120", + }, + }, + }, + want: HpaValues{ + MinReplicas: DefaultMinReplicas, + MaxReplicas: DefaultMaxReplicas, + CPUtilization: DefaultCPUUtilization, + MemoryUtilization: DefaultMemoryUtilization, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getResourceHpaValues(tt.args.service); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getResourceHpaValues() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_validatePercentageMetric(t *testing.T) { + type args struct { + service *kobject.ServiceConfig + metricLabel string + defaultValue int32 + } + tests := []struct { + name string + args args + want int32 + }{ + { + name: "0 cpu utilization", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaCPU: "0", + }, + }, + metricLabel: compose.LabelHpaCPU, + defaultValue: DefaultCPUUtilization, + }, + want: 50, + }, + { + name: "default cpu valid range", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaCPU: "120", + }, + }, + metricLabel: compose.LabelHpaCPU, + defaultValue: DefaultCPUUtilization, + }, + want: DefaultCPUUtilization, + }, + { + name: "cpu invalid range", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaCPU: "-120", + }, + }, + metricLabel: compose.LabelHpaCPU, + defaultValue: DefaultCPUUtilization, + }, + want: DefaultCPUUtilization, + }, + { + name: "cpu utilization set to 100", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaCPU: "100", + }, + }, + metricLabel: compose.LabelHpaCPU, + defaultValue: DefaultCPUUtilization, + }, + want: 100, + }, + { + name: "cpu utlization set to 101", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaCPU: "101", + }, + }, + metricLabel: compose.LabelHpaCPU, + defaultValue: DefaultCPUUtilization, + }, + want: DefaultCPUUtilization, + }, + { + name: "cannot convert value in cpu label", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaCPU: "not converted", + }, + }, + metricLabel: compose.LabelHpaCPU, + defaultValue: DefaultCPUUtilization, + }, + want: DefaultCPUUtilization, + }, + { + name: "0 memory utilization", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMemory: "0", + }, + }, + metricLabel: compose.LabelHpaMemory, + defaultValue: DefaultMemoryUtilization, + }, + want: 70, + }, + { + name: "memory over 100 utilization", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMemory: "120", + }, + }, + metricLabel: compose.LabelHpaMemory, + defaultValue: DefaultMemoryUtilization, + }, + want: DefaultMemoryUtilization, + }, + { + name: "-120 utilization memory wrong range", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMemory: "-120", + }, + }, + metricLabel: compose.LabelHpaMemory, + defaultValue: DefaultMemoryUtilization, + }, + want: DefaultMemoryUtilization, + }, + { + name: "memory 100 usage", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMemory: "100", + }, + }, + metricLabel: compose.LabelHpaMemory, + defaultValue: DefaultMemoryUtilization, + }, + want: 100, + }, + { + name: "101 memory utilization", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMemory: "101", + }, + }, + metricLabel: compose.LabelHpaMemory, + defaultValue: DefaultMemoryUtilization, + }, + want: DefaultMemoryUtilization, + }, + { + name: "cannot convert memory from label", + args: args{ + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMemory: "not converted", + }, + }, + metricLabel: compose.LabelHpaMemory, + defaultValue: DefaultMemoryUtilization, + }, + want: DefaultMemoryUtilization, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := validatePercentageMetric(tt.args.service, tt.args.metricLabel, tt.args.defaultValue); got != tt.want { + t.Errorf("validatePercentageMetric() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getHpaMetricSpec(t *testing.T) { + valueCPUFixed := int32(50) + valueMemoryFixed := int32(70) + valueOver100 := int32(120) + valueUnderZero := int32(-120) + // valueZero := int32(0) + type args struct { + hpaValues HpaValues + } + tests := []struct { + name string + args args + want []hpa.MetricSpec + }{ + { + name: "no values", + args: args{ + hpaValues: HpaValues{}, // set all values to 0 + }, + want: nil, + }, + { + name: "only cpu", + args: args{ + hpaValues: HpaValues{ + CPUtilization: valueCPUFixed, + }, + }, + want: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueCPUFixed, + }, + }, + }, + }, + }, + { + name: "only memory", + args: args{ + hpaValues: HpaValues{ + MemoryUtilization: 70, + }, + }, + want: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueMemoryFixed, + }, + }, + }, + }, + }, + { + name: "cpu and memory", + args: args{ + hpaValues: HpaValues{ + CPUtilization: valueCPUFixed, + MemoryUtilization: valueMemoryFixed, + }, + }, + want: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueCPUFixed, + }, + }, + }, + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueMemoryFixed, + }, + }, + }, + }, + }, + { + name: "memory over 100", + args: args{ + hpaValues: HpaValues{ + MemoryUtilization: valueOver100, + }, + }, + want: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueOver100, + }, + }, + }, + }, + }, + { + name: "cpu and memory over 100", + args: args{ + hpaValues: HpaValues{ + CPUtilization: valueOver100, + MemoryUtilization: valueOver100, + }, + }, + want: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueOver100, + }, + }, + }, + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueOver100, + }, + }, + }, + }, + }, + { + name: "cpu and memory under 0", + args: args{ + hpaValues: HpaValues{ + CPUtilization: valueUnderZero, + MemoryUtilization: valueUnderZero, + }, + }, + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := getHpaMetricSpec(tt.args.hpaValues); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getHpaMetricSpec() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_createHPAResources(t *testing.T) { + valueCPUFixed := int32(50) + valueMemoryFixed := int32(70) + fixedMinReplicas := int32(1) + type args struct { + name string + service *kobject.ServiceConfig + } + tests := []struct { + name string + args args + want hpa.HorizontalPodAutoscaler + }{ + { + name: "all labels", + args: args{ + name: "web", + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "10", + compose.LabelHpaCPU: "50", + compose.LabelHpaMemory: "70", + }, + }, + }, + want: hpa.HorizontalPodAutoscaler{ + TypeMeta: metav1.TypeMeta{ + Kind: "HorizontalPodAutoscaler", + APIVersion: "autoscaling/v2", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "web", + }, + Spec: hpa.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: hpa.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "web", + APIVersion: "apps/v1", + }, + MinReplicas: &fixedMinReplicas, + MaxReplicas: 10, + Metrics: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueCPUFixed, + }, + }, + }, + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueMemoryFixed, + }, + }, + }, + }, + }, + }, + }, + { + name: "minimum labels", + args: args{ + name: "api", + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaCPU: "50", + }, + }, + }, + want: hpa.HorizontalPodAutoscaler{ + TypeMeta: metav1.TypeMeta{ + Kind: "HorizontalPodAutoscaler", + APIVersion: "autoscaling/v2", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "api", + }, + Spec: hpa.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: hpa.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "api", + APIVersion: "apps/v1", + }, + MinReplicas: &fixedMinReplicas, + MaxReplicas: DefaultMaxReplicas, + Metrics: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueCPUFixed, + }, + }, + }, + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueMemoryFixed, + }, + }, + }, + }, + }, + }, + }, + { + name: "missing CPU utilization label", + args: args{ + name: "app", + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "5", + compose.LabelHpaMemory: "70", + }, + }, + }, + want: hpa.HorizontalPodAutoscaler{ + TypeMeta: metav1.TypeMeta{ + Kind: "HorizontalPodAutoscaler", + APIVersion: "autoscaling/v2", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "app", + }, + Spec: hpa.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: hpa.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "app", + APIVersion: "apps/v1", + }, + MinReplicas: &fixedMinReplicas, + MaxReplicas: 5, + Metrics: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueCPUFixed, + }, + }, + }, + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueMemoryFixed, + }, + }, + }, + }, + }, + }, + }, + { + name: "missing memory utilization label", + args: args{ + name: "db", + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "8", + compose.LabelHpaCPU: "50", + }, + }, + }, + want: hpa.HorizontalPodAutoscaler{ + TypeMeta: metav1.TypeMeta{ + Kind: "HorizontalPodAutoscaler", + APIVersion: "autoscaling/v2", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "db", + }, + Spec: hpa.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: hpa.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "db", + APIVersion: "apps/v1", + }, + MinReplicas: &fixedMinReplicas, + MaxReplicas: 8, + Metrics: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueCPUFixed, + }, + }, + }, + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueMemoryFixed, + }, + }, + }, + }, + }, + }, + }, + { + name: "wrong labels", + args: args{ + name: "db", + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "not converted", + compose.LabelHpaMaxReplicas: "not converted", + }, + }, + }, + want: hpa.HorizontalPodAutoscaler{ + TypeMeta: metav1.TypeMeta{ + Kind: "HorizontalPodAutoscaler", + APIVersion: "autoscaling/v2", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "db", + }, + Spec: hpa.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: hpa.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "db", + APIVersion: "apps/v1", + }, + MinReplicas: &fixedMinReplicas, + MaxReplicas: DefaultMaxReplicas, + Metrics: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueCPUFixed, + }, + }, + }, + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueMemoryFixed, + }, + }, + }, + }, + }, + }, + }, + { + name: "missing both CPU and memory utilization labels", + args: args{ + name: "db", + service: &kobject.ServiceConfig{ + Labels: map[string]string{ + compose.LabelHpaMinReplicas: "1", + compose.LabelHpaMaxReplicas: "5", + }, + }, + }, + want: hpa.HorizontalPodAutoscaler{ + TypeMeta: metav1.TypeMeta{ + Kind: "HorizontalPodAutoscaler", + APIVersion: "autoscaling/v2", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "db", + }, + Spec: hpa.HorizontalPodAutoscalerSpec{ + ScaleTargetRef: hpa.CrossVersionObjectReference{ + Kind: "Deployment", + Name: "db", + APIVersion: "apps/v1", + }, + MinReplicas: &fixedMinReplicas, + MaxReplicas: 5, + Metrics: []hpa.MetricSpec{ + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "cpu", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueCPUFixed, + }, + }, + }, + { + Type: hpa.ResourceMetricSourceType, + Resource: &hpa.ResourceMetricSource{ + Name: "memory", + Target: hpa.MetricTarget{ + Type: hpa.UtilizationMetricType, + AverageUtilization: &valueMemoryFixed, + }, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := createHPAResources(tt.args.name, tt.args.service); !reflect.DeepEqual(got, tt.want) { + t.Errorf("createHPAResources() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/transformer/kubernetes/kubernetes.go b/pkg/transformer/kubernetes/kubernetes.go index 360ebc1d..f3ff6c70 100644 --- a/pkg/transformer/kubernetes/kubernetes.go +++ b/pkg/transformer/kubernetes/kubernetes.go @@ -29,7 +29,7 @@ import ( "strconv" "strings" - "github.com/compose-spec/compose-go/types" + "github.com/compose-spec/compose-go/v2/types" "github.com/fatih/structs" "github.com/kubernetes/kompose/pkg/kobject" "github.com/kubernetes/kompose/pkg/loader/compose" @@ -1314,7 +1314,7 @@ func (k *Kubernetes) createConfigMapFromComposeConfig(name string, service kobje for _, config := range service.Configs { currentConfigName := config.Source currentConfigObj := service.ConfigsMetaData[currentConfigName] - if currentConfigObj.External.External { + if currentConfigObj.External { continue } currentFileName := currentConfigObj.File @@ -1654,6 +1654,10 @@ func (k *Kubernetes) Transform(komposeObject kobject.KomposeObject, opt kobject. return nil, err } } + err = k.configHorizontalPodScaler(name, service, opt, &objects) + if err != nil { + return nil, errors.Wrap(err, "Error creating Kubernetes HPA") + } allobjects = append(allobjects, objects...) } @@ -1718,3 +1722,16 @@ func (k *Kubernetes) UpdateController(obj runtime.Object, updateTemplate func(*a } return nil } + +// configHorizontalPodScaler create Hpa resource also append to the objects +// first checks if the service labels contain any HPA labels using the searchHPAValues +func (k *Kubernetes) configHorizontalPodScaler(name string, service kobject.ServiceConfig, opt kobject.ConvertOptions, objects *[]runtime.Object) (err error) { + found := searchHPAValues(service.Labels) + if !found { + return nil + } + + hpa := createHPAResources(name, &service) + *objects = append(*objects, &hpa) + return nil +} diff --git a/pkg/transformer/kubernetes/kubernetes_test.go b/pkg/transformer/kubernetes/kubernetes_test.go index eb9991d1..1bcc0a6e 100644 --- a/pkg/transformer/kubernetes/kubernetes_test.go +++ b/pkg/transformer/kubernetes/kubernetes_test.go @@ -23,7 +23,7 @@ import ( "strings" "testing" - "github.com/compose-spec/compose-go/types" + "github.com/compose-spec/compose-go/v2/types" "github.com/kubernetes/kompose/pkg/kobject" "github.com/kubernetes/kompose/pkg/loader/compose" diff --git a/pkg/transformer/utils.go b/pkg/transformer/utils.go index b5d60d25..fe2e1aea 100644 --- a/pkg/transformer/utils.go +++ b/pkg/transformer/utils.go @@ -121,7 +121,7 @@ func parseVolume(volume string) (name, host, container, mode string, err error) return } -// parseVolume parses window volume. +// parseWindowsVolume parses window volume. // example: windows host mount to windows container // volume = dataVolumeName:C:\Users\Data:D:\config:rw // it can be parsed: diff --git a/script/test/cmd/tests_new.sh b/script/test/cmd/tests_new.sh index 0d5c7766..c919f374 100755 --- a/script/test/cmd/tests_new.sh +++ b/script/test/cmd/tests_new.sh @@ -345,6 +345,11 @@ k8s_cmd="kompose -f $KOMPOSE_ROOT/script/test/fixtures/initcontainer/compose.yam k8s_output="$KOMPOSE_ROOT/script/test/fixtures/initcontainer/output-k8s.yaml" convert::expect_success_and_warning "$k8s_cmd" "$k8s_output" || exit 1 +# Test HPA +k8s_cmd="kompose -f $KOMPOSE_ROOT/script/test/fixtures/hpa/compose.yaml convert --stdout --with-kompose-annotation=false" +k8s_output="$KOMPOSE_ROOT/script/test/fixtures/hpa/output-k8s.yaml" +convert::expect_success "$k8s_cmd" "$k8s_output" || exit 1 + # Test env var with status k8s_cmd="kompose -f $KOMPOSE_ROOT/script/test/fixtures/envvars-with-status/compose.yaml convert --stdout --with-kompose-annotation=false" k8s_output="$KOMPOSE_ROOT/script/test/fixtures/envvars-with-status/output-k8s.yaml" diff --git a/script/test/fixtures/hpa/compose.yaml b/script/test/fixtures/hpa/compose.yaml new file mode 100644 index 00000000..80a2c432 --- /dev/null +++ b/script/test/fixtures/hpa/compose.yaml @@ -0,0 +1,10 @@ +version: "3.8" +services: + web: + image: nginx + labels: + kompose.hpa.cpu: 50 + kompose.hpa.memory: 70 + kompose.hpa.replicas.min: 1 + kompose.hpa.replicas.max: 10 + diff --git a/script/test/fixtures/hpa/output-k8s.yaml b/script/test/fixtures/hpa/output-k8s.yaml new file mode 100644 index 00000000..6f249d29 --- /dev/null +++ b/script/test/fixtures/hpa/output-k8s.yaml @@ -0,0 +1,49 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + io.kompose.service: web + name: web +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: web + template: + metadata: + labels: + io.kompose.network/hpa-default: "true" + io.kompose.service: web + spec: + containers: + - image: nginx + name: web + restartPolicy: Always + +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: web +spec: + maxReplicas: 10 + metrics: + - resource: + name: cpu + target: + averageUtilization: 50 + type: Utilization + type: Resource + - resource: + name: memory + target: + averageUtilization: 70 + type: Utilization + type: Resource + minReplicas: 1 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: web +