Skip to content

Commit

Permalink
first attempt to support deploymentConfig (#15)
Browse files Browse the repository at this point in the history
without proper tests and without checking the docs for correct defaults
  • Loading branch information
druppelt committed Sep 12, 2024
1 parent dd06500 commit bb39808
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 3 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ go 1.22.0
toolchain go1.23.0

require (
github.com/openshift/api v0.0.0-20240911192208-3e5de946111c
github.com/openshift/client-go v0.0.0-20240906181530-b2f7c4ab0984
github.com/rs/zerolog v1.33.0
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
github.com/openshift/api v0.0.0-20240911192208-3e5de946111c h1:46hH/7XmmaPmeJWTyrzh8TRB6I7TCwzJdxxWeyK8blM=
github.com/openshift/api v0.0.0-20240911192208-3e5de946111c/go.mod h1:OOh6Qopf21pSzqNVCB5gomomBXb8o5sGKZxG2KNpaXM=
github.com/openshift/client-go v0.0.0-20240906181530-b2f7c4ab0984 h1:4OVV/fm6ea+51rZbA/52SFbHdjlzjCKK6OCE7Xtn834=
github.com/openshift/client-go v0.0.0-20240906181530-b2f7c4ab0984/go.mod h1:K+5rEJpGf5LpcwdNtkGsvV3u8wU7m3oHzcVZzuGTRZ4=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
Expand Down
24 changes: 21 additions & 3 deletions internal/calc/calc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ package calc
import (
"errors"
"fmt"

openshiftAppsV1 "github.com/openshift/api/apps/v1"
openshiftScheme "github.com/openshift/client-go/apps/clientset/versioned/scheme"
"github.com/rs/zerolog/log"
appsv1 "k8s.io/api/apps/v1"
batchV1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/client-go/kubernetes/scheme"
)

Expand Down Expand Up @@ -101,7 +103,13 @@ func ResourceQuotaFromYaml(yamlData []byte) (*ResourceUsage, error) {

var kind string

object, gvk, err := scheme.Codecs.UniversalDeserializer().Decode(yamlData, nil, nil)
combinedScheme := runtime.NewScheme()
_ = scheme.AddToScheme(combinedScheme)
_ = openshiftScheme.AddToScheme(combinedScheme)
codecs := serializer.NewCodecFactory(combinedScheme)
decoder := codecs.UniversalDeserializer()

object, gvk, err := decoder.Decode(yamlData, nil, nil)

if err != nil {
// when the kind is not found, I just warn and skip
Expand All @@ -110,7 +118,7 @@ func ResourceQuotaFromYaml(yamlData []byte) (*ResourceUsage, error) {

unknown := runtime.Unknown{Raw: yamlData}

if _, gvk1, err := scheme.Codecs.UniversalDeserializer().Decode(yamlData, nil, &unknown); err == nil {
if _, gvk1, err := decoder.Decode(yamlData, nil, &unknown); err == nil {
kind = gvk1.Kind
version = gvk1.Version
}
Expand All @@ -123,6 +131,16 @@ func ResourceQuotaFromYaml(yamlData []byte) (*ResourceUsage, error) {
}

switch obj := object.(type) {
case *openshiftAppsV1.DeploymentConfig:
usage, err := deploymentConfig(*obj)
if err != nil {
return nil, CalculationError{
Version: gvk.Version,
Kind: gvk.Kind,
err: err,
}
}
return usage, nil

Check failure on line 143 in internal/calc/calc.go

View workflow job for this annotation

GitHub Actions / lint

return statements should not be cuddled if block has more than two lines (wsl)
case *appsv1.Deployment:
usage, err := deployment(*obj)
if err != nil {
Expand Down
46 changes: 46 additions & 0 deletions internal/calc/calc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,52 @@ spec:
kind: Service
name: coffee-svc `

var normalDeploymentConfig = `---
apiVersion: apps.openshift.io/v1
kind: DeploymentConfig
metadata:
labels:
app: normal
name: normal
spec:
progressDeadlineSeconds: 600
replicas: 10
revisionHistoryLimit: 10
selector:
app: normal
strategy:
rollingParams:
maxSurge: 25%
maxUnavailable: 25%
type: Rolling
template:
metadata:
creationTimestamp: null
labels:
app: normal
spec:
containers:
- image: myapp:v1.0.7
command:
- sleep
- infinity
imagePullPolicy: IfNotPresent
name: normal
resources:
limits:
cpu: '500m'
memory: 4Gi
requests:
cpu: '250m'
memory: 2Gi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30`

var normalDeployment = `---
apiVersion: apps/v1
kind: Deployment
Expand Down
127 changes: 127 additions & 0 deletions internal/calc/deploymentConfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package calc

import (
"fmt"
openshiftAppsV1 "github.com/openshift/api/apps/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/util/intstr"
"math"
)

// calculates the cpu/memory resources a single deployment needs. Replicas and the deployment
// strategy are taken into account.
func deploymentConfig(deploymentConfig openshiftAppsV1.DeploymentConfig) (*ResourceUsage, error) {
var (
resourceOverhead float64 // max overhead compute resources (percent)
podOverhead int32 // max overhead pods during deploymentConfig
)

replicas := deploymentConfig.Spec.Replicas
strategy := deploymentConfig.Spec.Strategy

if replicas == 0 {
return &ResourceUsage{
CPUMin: new(resource.Quantity),
CPUMax: new(resource.Quantity),
MemoryMin: new(resource.Quantity),
MemoryMax: new(resource.Quantity),
Details: Details{
Version: deploymentConfig.APIVersion,
Kind: deploymentConfig.Kind,
Name: deploymentConfig.Name,
Replicas: replicas,
MaxReplicas: replicas,
Strategy: string(strategy.Type),
},
}, nil
}
// TODO lookup default values, these are copied from kubernetes Deployment
switch strategy.Type {
case openshiftAppsV1.DeploymentStrategyTypeRecreate:
// no overhead on recreate
resourceOverhead = 1
podOverhead = 0
case "":
// Rolling is the default and can be an empty string. If so, set the defaults
// (https://pkg.go.dev/k8s.io/api/apps/v1?tab=doc#RollingUpdateDeployment) and continue calculation.
defaults := intstr.FromString("25%")
strategy = openshiftAppsV1.DeploymentStrategy{
Type: openshiftAppsV1.DeploymentStrategyTypeRolling,
RollingParams: &openshiftAppsV1.RollingDeploymentStrategyParams{
MaxUnavailable: &defaults,
MaxSurge: &defaults,
},
}

fallthrough
case openshiftAppsV1.DeploymentStrategyTypeRolling:
// Documentation: https://pkg.go.dev/k8s.io/api/apps/v1?tab=doc#RollingUpdateDeployment
// all default values are set as stated in the docs
var (
maxUnavailableValue intstr.IntOrString
maxSurgeValue intstr.IntOrString
)

// can be nil, if so apply default value
if strategy.RollingParams == nil {
maxUnavailableValue = intstr.FromString("25%")
maxSurgeValue = intstr.FromString("25%")
} else {
maxUnavailableValue = *strategy.RollingParams.MaxUnavailable
maxSurgeValue = *strategy.RollingParams.MaxSurge
}

// docs say, that the absolute number is calculated by rounding down.
maxUnavailable, err := intstr.GetScaledValueFromIntOrPercent(&maxUnavailableValue, int(replicas), false)
if err != nil {
return nil, err
}

// docs say, absolute number is calculated by rounding up.
maxSurge, err := intstr.GetScaledValueFromIntOrPercent(&maxSurgeValue, int(replicas), true)
if err != nil {
return nil, err
}

// podOverhead is the number of pods which can run more during a deployment
podOverheadInt := maxSurge - maxUnavailable
if podOverheadInt > math.MaxInt32 || podOverheadInt < math.MinInt32 {
return nil, fmt.Errorf("deploymentConfig: %s maxSurge - maxUnavailable (%d-%d) was out of bounds for int32", deploymentConfig.Name, maxSurge, maxUnavailable)
}
podOverhead = int32(podOverheadInt) //nolint:gosec,wsl // gosec doesn't understand that the int conversion is already guarded, wsl wants to group the assignment with the next block

resourceOverhead = (float64(podOverhead) / float64(replicas)) + 1
default:
return nil, fmt.Errorf("deploymentConfig: %s deploymentConfig strategy %q is unknown", deploymentConfig.Name, strategy.Type)
}

cpuMin, cpuMax, memoryMin, memoryMax := podResources(&deploymentConfig.Spec.Template.Spec)
strategyResources := &deploymentConfig.Spec.Strategy.Resources

memMin := float64(memoryMin.Value())*float64(replicas)*resourceOverhead + float64(strategyResources.Requests.Memory().Value())
memoryMin.Set(int64(math.Round(memMin)))

memMax := float64(memoryMax.Value())*float64(replicas)*resourceOverhead + float64(strategyResources.Limits.Memory().Value())
memoryMax.Set(int64(math.Round(memMax)))

cpuMin.SetMilli(int64(math.Round(float64(cpuMin.MilliValue())*float64(replicas)*resourceOverhead)) + strategyResources.Requests.Cpu().MilliValue())

cpuMax.SetMilli(int64(math.Round(float64(cpuMax.MilliValue())*float64(replicas)*resourceOverhead)) + strategyResources.Limits.Cpu().MilliValue())

resourceUsage := ResourceUsage{
CPUMin: cpuMin,
CPUMax: cpuMax,
MemoryMin: memoryMin,
MemoryMax: memoryMax,
Details: Details{
Version: deploymentConfig.APIVersion,
Kind: deploymentConfig.Kind,
Name: deploymentConfig.Name,
Replicas: replicas,
Strategy: string(strategy.Type),
MaxReplicas: replicas + podOverhead,
},
}

return &resourceUsage, nil
}
54 changes: 54 additions & 0 deletions internal/calc/deploymentConfig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package calc

import (
"testing"

openshiftAppsV1 "github.com/openshift/api/apps/v1"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/resource"
)

func TestDeploymentConfig(t *testing.T) {
var tests = []struct {
name string
deploymentConfig string
cpuMin resource.Quantity
cpuMax resource.Quantity
memoryMin resource.Quantity
memoryMax resource.Quantity
replicas int32
maxReplicas int32
strategy openshiftAppsV1.DeploymentStrategyType
}{
{
name: "normal deploymentConfig",
deploymentConfig: normalDeploymentConfig,
cpuMin: resource.MustParse("2750m"),
cpuMax: resource.MustParse("5500m"),
memoryMin: resource.MustParse("22Gi"),
memoryMax: resource.MustParse("44Gi"),
replicas: 10,
maxReplicas: 11,
strategy: openshiftAppsV1.DeploymentStrategyTypeRolling,
},
//TODO add more tests
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
r := require.New(t)

usage, err := ResourceQuotaFromYaml([]byte(test.deploymentConfig))
r.NoError(err)
r.NotEmpty(usage)

AssertEqualQuantities(r, test.cpuMin, *usage.CPUMin, "cpu request value")
AssertEqualQuantities(r, test.cpuMax, *usage.CPUMax, "cpu limit value")
AssertEqualQuantities(r, test.memoryMin, *usage.MemoryMin, "memory request value")
AssertEqualQuantities(r, test.memoryMax, *usage.MemoryMax, "memory limit value")
r.Equal(test.replicas, usage.Details.Replicas, "replicas")
r.Equal(string(test.strategy), usage.Details.Strategy, "strategy")
r.Equal(test.maxReplicas, usage.Details.MaxReplicas, "maxReplicas")
})
}
}

0 comments on commit bb39808

Please sign in to comment.