Skip to content

Commit

Permalink
Allow specifying resource requirements in docker-compose service labe…
Browse files Browse the repository at this point in the history
…ls (#1)

* Add Resources to ServiceValues - read from compose service labels.
* Allow service resource label to specify override per git branch
* Add tests
  • Loading branch information
MaxCWhitehead authored Oct 1, 2024
1 parent 8d06f23 commit 7628242
Show file tree
Hide file tree
Showing 13 changed files with 797 additions and 0 deletions.
18 changes: 18 additions & 0 deletions cmd/template_lagoonservices_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,24 @@ func TestTemplateLagoonServices(t *testing.T) {
templatePath: "testoutput",
want: "internal/testdata/basic/service-templates/test-basic-spot-affinity",
},
{
name: "test16-nginx-php-resource-requests-from-env",
description: "tests an nginx-php deployment with service label resource requests set from env var",
args: testdata.GetSeedData(
testdata.TestData{
ProjectName: "example-project",
EnvironmentName: "main",
Branch: "main",
LagoonYAML: "internal/testdata/complex/lagoon.resources.yml",
ImageReferences: map[string]string{
"nginx": "harbor.example/example-project/main/nginx@sha256:b2001babafaa8128fe89aa8fd11832cade59931d14c3de5b3ca32e2a010fbaa8",
"php": "harbor.example/example-project/main/php@sha256:b2001babafaa8128fe89aa8fd11832cade59931d14c3de5b3ca32e2a010fbaa8",
"cli": "harbor.example/example-project/main/cli@sha256:b2001babafaa8128fe89aa8fd11832cade59931d14c3de5b3ca32e2a010fbaa8",
},
}, true),
templatePath: "testoutput",
want: "internal/testdata/complex/service-templates/test16-nginx-php-resources",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
4 changes: 4 additions & 0 deletions internal/generator/buildvalues.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,14 @@ type Resources struct {
}

type ResourceLimits struct {
Cpu string `json:"cpu"`
Memory string `json:"memory"`
EphemeralStorage string `json:"ephemeral-storage"`
}

type ResourceRequests struct {
Cpu string `json:"cpu"`
Memory string `json:"memory"`
EphemeralStorage string `json:"ephemeral-storage"`
}

Expand Down Expand Up @@ -200,6 +203,7 @@ type ServiceValues struct {
IsDBaaS bool `json:"isDBaaS"`
IsSingle bool `json:"isSingle"`
AdditionalVolumes []ServiceVolume `json:"additonalVolumes,omitempty"`
Resources Resources `json:"resources,omitempty"`
}

type ImageBuild struct {
Expand Down
48 changes: 48 additions & 0 deletions internal/generator/services.go
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,53 @@ func composeToServiceValues(

}

// Helper lambda to look for resource requirement label, validate it, and update value
updateResourceRequirement := func(dest *string, resource_suffix string) (err error) {
// First check for branch override labels (example: lagoon.resources.override-branch.dev.requests.cpu)
{
branchOverrideLabel := fmt.Sprintf("lagoon.resources.override-branch.%s.%s", buildValues.Branch, resource_suffix)

overrideResourceValue := lagoon.CheckDockerComposeLagoonLabel(composeServiceValues.Labels, branchOverrideLabel)
if overrideResourceValue != "" {
err := ValidateResourceQuantity(overrideResourceValue)
if err != nil {
return fmt.Errorf("Value of resource requirement label %s for %s is not valid resource quantity: %v", branchOverrideLabel, servicePersistentName, err)
}
*dest = overrideResourceValue
return nil
}
}

// If no branch override, check for base service label (example: lagoon.resources.requests.cpu)
{
baseLabel := fmt.Sprintf("lagoon.resources.%s", resource_suffix)
resourceValue := lagoon.CheckDockerComposeLagoonLabel(composeServiceValues.Labels, baseLabel)
if resourceValue != "" {
err := ValidateResourceQuantity(resourceValue)
if err != nil {
return fmt.Errorf("Value of resource requirement label %s for %s is not valid resource quantity: %v", baseLabel, servicePersistentName, err)
}
*dest = resourceValue
}
}
return nil
}

// Build container resource requirements from service labels, either base label or branch specific override.
resources := Resources{}
if err := updateResourceRequirement(&resources.Requests.Cpu, "requests.cpu"); err != nil {
return nil, err
}
if err := updateResourceRequirement(&resources.Requests.Memory, "requests.memory"); err != nil {
return nil, err
}
if err := updateResourceRequirement(&resources.Limits.Cpu, "limits.cpu"); err != nil {
return nil, err
}
if err := updateResourceRequirement(&resources.Limits.Memory, "limits.memory"); err != nil {
return nil, err
}

// create the service values
cService := &ServiceValues{
Name: composeService,
Expand All @@ -535,6 +582,7 @@ func composeToServiceValues(
IsSingle: svcIsSingle,
BackupsEnabled: backupsEnabled,
AdditionalVolumes: serviceVolumes,
Resources: resources,
}

// work out the images here and the associated dockerfile and contexts
Expand Down
59 changes: 59 additions & 0 deletions internal/generator/services_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1236,6 +1236,65 @@ func Test_composeToServiceValues(t *testing.T) {
want: nil,
wantErr: true,
},
{
name: "test24 - service label resource requirements into ServiceValues",
args: args{
buildValues: &BuildValues{
Namespace: "example-project-main",
Project: "example-project",
ImageRegistry: "harbor.example",
Environment: "main",
Branch: "main",
BuildType: "branch",
ServiceTypeOverrides: &lagoon.EnvironmentVariable{},
LagoonYAML: lagoon.YAML{
Environments: lagoon.Environments{
"main": lagoon.Environment{},
},
},
},
composeService: "nginx",
composeServiceValues: composetypes.ServiceConfig{
Labels: composetypes.Labels{
"lagoon.type": "nginx",
"lagoon.name": "nginx-php",
"lagoon.resources.requests.cpu": "200m",
"lagoon.resources.requests.memory": "200Mi",
"lagoon.resources.limits.cpu": "400m",
"lagoon.resources.limits.memory": "1Gi",
},
Build: &composetypes.BuildConfig{
Context: ".",
Dockerfile: "../testdata/basic/docker/basic.dockerfile",
},
},
},
want: &ServiceValues{
Name: "nginx",
OverrideName: "nginx-php",
Type: "nginx",
AutogeneratedRoutesEnabled: true,
AutogeneratedRoutesTLSAcme: true,
InPodCronjobs: []lagoon.Cronjob{},
NativeCronjobs: []lagoon.Cronjob{},
ImageBuild: &ImageBuild{
TemporaryImage: "example-project-main-nginx",
Context: ".",
DockerFile: "../testdata/basic/docker/basic.dockerfile",
BuildImage: "harbor.example/example-project/main/nginx:latest",
},
Resources: Resources{
Requests: ResourceRequests{
Cpu: "200m",
Memory: "200Mi",
},
Limits: ResourceLimits{
Cpu: "400m",
Memory: "1Gi",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
62 changes: 62 additions & 0 deletions internal/templating/services/templates_deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/uselagoon/build-deploy-tool/internal/servicetypes"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
apivalidation "k8s.io/apimachinery/pkg/api/validation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -595,6 +596,8 @@ func GenerateDeploymentTemplate(
}

// set the resource limit overrides if htey are provided
//
// TODO: cpu limit, and cpu/mem requests have been added, should consider wiring up BuildValues resource override
if buildValues.Resources.Limits.Memory != "" {
if container.Container.Resources.Limits == nil {
container.Container.Resources.Limits = corev1.ResourceList{}
Expand All @@ -614,6 +617,35 @@ func GenerateDeploymentTemplate(
container.Container.Resources.Requests[corev1.ResourceEphemeralStorage] = resource.MustParse(buildValues.Resources.Requests.EphemeralStorage)
}

// If service resource overrides are present, use those
if serviceValues.Resources.Requests.Cpu != "" {
if container.Container.Resources.Requests == nil {
container.Container.Resources.Requests = corev1.ResourceList{}
}
container.Container.Resources.Requests[v1.ResourceCPU] = resource.MustParse(serviceValues.Resources.Requests.Cpu)
}

if serviceValues.Resources.Requests.Memory != "" {
if container.Container.Resources.Requests == nil {
container.Container.Resources.Requests = corev1.ResourceList{}
}
container.Container.Resources.Requests[v1.ResourceMemory] = resource.MustParse(serviceValues.Resources.Requests.Memory)
}

if serviceValues.Resources.Limits.Cpu != "" {
if container.Container.Resources.Limits == nil {
container.Container.Resources.Limits = corev1.ResourceList{}
}
container.Container.Resources.Limits[v1.ResourceCPU] = resource.MustParse(serviceValues.Resources.Limits.Cpu)
}

if serviceValues.Resources.Limits.Memory != "" {
if container.Container.Resources.Limits == nil {
container.Container.Resources.Limits = corev1.ResourceList{}
}
container.Container.Resources.Limits[v1.ResourceMemory] = resource.MustParse(serviceValues.Resources.Limits.Memory)
}

// append the final defined container to the spec
deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, container.Container)

Expand Down Expand Up @@ -693,6 +725,36 @@ func GenerateDeploymentTemplate(
helpers.TemplateThings(tpld, svm, &volumeMount)
linkedContainer.Container.VolumeMounts = append(linkedContainer.Container.VolumeMounts, volumeMount)
}

// If service resource overrides are present, use those
if serviceValues.LinkedService.Resources.Requests.Cpu != "" {
if linkedContainer.Container.Resources.Requests == nil {
linkedContainer.Container.Resources.Requests = corev1.ResourceList{}
}
linkedContainer.Container.Resources.Requests[v1.ResourceCPU] = resource.MustParse(serviceValues.LinkedService.Resources.Requests.Cpu)
}

if serviceValues.LinkedService.Resources.Requests.Memory != "" {
if linkedContainer.Container.Resources.Requests == nil {
linkedContainer.Container.Resources.Requests = corev1.ResourceList{}
}
linkedContainer.Container.Resources.Requests[v1.ResourceMemory] = resource.MustParse(serviceValues.LinkedService.Resources.Requests.Memory)
}

if serviceValues.LinkedService.Resources.Limits.Cpu != "" {
if linkedContainer.Container.Resources.Limits == nil {
linkedContainer.Container.Resources.Limits = corev1.ResourceList{}
}
linkedContainer.Container.Resources.Limits[v1.ResourceCPU] = resource.MustParse(serviceValues.LinkedService.Resources.Limits.Cpu)
}

if serviceValues.LinkedService.Resources.Limits.Memory != "" {
if linkedContainer.Container.Resources.Limits == nil {
linkedContainer.Container.Resources.Limits = corev1.ResourceList{}
}
linkedContainer.Container.Resources.Limits[v1.ResourceMemory] = resource.MustParse(serviceValues.LinkedService.Resources.Limits.Memory)
}

deployment.Spec.Template.Spec.Containers = append(deployment.Spec.Template.Spec.Containers, linkedContainer.Container)
}

Expand Down
65 changes: 65 additions & 0 deletions internal/templating/services/templates_deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,71 @@ func TestGenerateDeploymentTemplate(t *testing.T) {
},
want: "test-resources/deployment/result-basic-4.yaml",
},
{
name: "test20 - nginx-php ServiceValues Resource Override",
args: args{
buildValues: generator.BuildValues{
Project: "example-project",
Environment: "environment-name",
EnvironmentType: "production",
Namespace: "myexample-project-environment-name",
BuildType: "branch",
LagoonVersion: "v2.x.x",
Kubernetes: "generator.local",
Branch: "environment-name",
FeatureFlags: map[string]bool{
"rootlessworkloads": true,
},
PodSecurityContext: generator.PodSecurityContext{
RunAsGroup: 0,
RunAsUser: 10000,
FsGroup: 10001,
},
GitSHA: "0",
ConfigMapSha: "32bf1359ac92178c8909f0ef938257b477708aa0d78a5a15ad7c2d7919adf273",
ImageReferences: map[string]string{
"nginx": "harbor.example.com/example-project/environment-name/nginx@latest",
"php": "harbor.example.com/example-project/environment-name/php@latest",
},
// BuildValues.Resouces not expected to be used as is overriden by ServiceValues resource
// (Included to verify ServiceValues is final word)
Resources: generator.Resources{Limits: generator.ResourceLimits{Memory: "2Gi"}},
Services: []generator.ServiceValues{
{
Name: "nginx",
OverrideName: "nginx",
Type: "nginx-php",
DBaaSEnvironment: "production",
// Leave primary mem request as default, ensure that default from ServiceType is still templated
Resources: generator.Resources{
Requests: generator.ResourceRequests{Cpu: "500m"},
Limits: generator.ResourceLimits{
Cpu: "2",
Memory: "1Gi",
},
},
},
{
Name: "php",
OverrideName: "nginx",
Type: "nginx-php",
DBaaSEnvironment: "production",
Resources: generator.Resources{
Requests: generator.ResourceRequests{
Cpu: "500m",
Memory: "200Mi",
},
Limits: generator.ResourceLimits{
Cpu: "500m",
Memory: "1Gi",
},
},
},
},
},
},
want: "test-resources/deployment/result-nginx-php-resources-1.yaml",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
Loading

0 comments on commit 7628242

Please sign in to comment.